From c19382d954b101ea4719da31543b475d4dddb122 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Fri, 3 Apr 2020 14:27:39 -0400 Subject: [PATCH 01/19] initial plugin setup --- .github/CODEOWNERS | 1 + x-pack/.i18nrc.json | 1 + x-pack/plugins/ingest_pipelines/README.md | 9 ++ .../ingest_pipelines/common/constants.ts | 16 ++++ .../ingest_pipelines/common/lib/index.ts | 7 ++ .../common/lib/pipeline_serialization.test.ts | 52 ++++++++++++ .../common/lib/pipeline_serialization.ts | 24 ++++++ .../plugins/ingest_pipelines/common/types.ts | 22 +++++ x-pack/plugins/ingest_pipelines/kibana.json | 17 ++++ .../public/application/app.tsx | 24 ++++++ .../public/application/constants/index.ts | 9 ++ .../public/application/index.tsx | 39 +++++++++ .../public/application/sections/index.ts | 7 ++ .../sections/pipelines_list/index.ts | 7 ++ .../pipelines_list/pipelines_list.tsx | 73 +++++++++++++++++ .../public/application/services/api.ts | 37 +++++++++ .../application/services/documentation.ts | 23 ++++++ .../public/application/services/index.ts | 11 +++ .../public/application/services/ui_metric.ts | 32 ++++++++ .../ingest_pipelines/public/index.scss | 0 .../plugins/ingest_pipelines/public/index.ts | 13 +++ .../plugins/ingest_pipelines/public/plugin.ts | 61 ++++++++++++++ .../ingest_pipelines/public/shared_imports.ts | 13 +++ .../plugins/ingest_pipelines/public/types.ts | 13 +++ .../plugins/ingest_pipelines/server/index.ts | 12 +++ .../ingest_pipelines/server/lib/index.ts | 7 ++ .../server/lib/is_es_error.ts | 13 +++ .../plugins/ingest_pipelines/server/plugin.ts | 59 +++++++++++++ .../ingest_pipelines/server/routes/api/get.ts | 37 +++++++++ .../server/routes/api/index.ts | 7 ++ .../ingest_pipelines/server/routes/index.ts | 15 ++++ .../ingest_pipelines/server/services/index.ts | 7 ++ .../server/services/license.ts | 82 +++++++++++++++++++ .../plugins/ingest_pipelines/server/types.ts | 22 +++++ 34 files changed, 772 insertions(+) create mode 100644 x-pack/plugins/ingest_pipelines/README.md create mode 100644 x-pack/plugins/ingest_pipelines/common/constants.ts create mode 100644 x-pack/plugins/ingest_pipelines/common/lib/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.test.ts create mode 100644 x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.ts create mode 100644 x-pack/plugins/ingest_pipelines/common/types.ts create mode 100644 x-pack/plugins/ingest_pipelines/kibana.json create mode 100644 x-pack/plugins/ingest_pipelines/public/application/app.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/constants/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/index.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/pipelines_list.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/services/api.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/services/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/services/ui_metric.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/index.scss create mode 100644 x-pack/plugins/ingest_pipelines/public/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/plugin.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/shared_imports.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/types.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/lib/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/lib/is_es_error.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/plugin.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/routes/api/get.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/routes/api/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/routes/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/services/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/services/license.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/types.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index da85fb986ae01..88855c851f409 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -196,6 +196,7 @@ /x-pack/plugins/snapshot_restore/ @elastic/es-ui /x-pack/plugins/upgrade_assistant/ @elastic/es-ui /x-pack/plugins/watcher/ @elastic/es-ui +/x-pack/plugins/ingest_pipelines/ @elastic/es-ui # Endpoint /x-pack/plugins/endpoint/ @elastic/endpoint-app-team diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index ae8d61769b14c..a8927e062044d 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -21,6 +21,7 @@ "xpack.indexLifecycleMgmt": "legacy/plugins/index_lifecycle_management", "xpack.infra": "plugins/infra", "xpack.ingestManager": "plugins/ingest_manager", + "xpack.ingestPipelines": "plugins/ingest_pipelines", "xpack.lens": "legacy/plugins/lens", "xpack.licenseMgmt": "plugins/license_management", "xpack.licensing": "plugins/licensing", diff --git a/x-pack/plugins/ingest_pipelines/README.md b/x-pack/plugins/ingest_pipelines/README.md new file mode 100644 index 0000000000000..030bba760499d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/README.md @@ -0,0 +1,9 @@ +# ingest_pipelines + +> Ingest node pipelines UI + +--- + +## Development + +See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. diff --git a/x-pack/plugins/ingest_pipelines/common/constants.ts b/x-pack/plugins/ingest_pipelines/common/constants.ts new file mode 100644 index 0000000000000..13cece33838df --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/common/constants.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { LicenseType } from '../../licensing/common/types'; + +const basicLicense: LicenseType = 'basic'; + +export const PLUGIN_ID = 'ingest_pipelines'; + +export const PLUGIN_MIN_LICENSE_TYPE = basicLicense; + +export const BASE_PATH = '/management/elasticsearch/ingest_pipelines'; + +export const API_BASE_PATH = '/api/ingest_pipelines'; diff --git a/x-pack/plugins/ingest_pipelines/common/lib/index.ts b/x-pack/plugins/ingest_pipelines/common/lib/index.ts new file mode 100644 index 0000000000000..a976f66bc7c40 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/common/lib/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { deserializePipelines } from './pipeline_serialization'; diff --git a/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.test.ts b/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.test.ts new file mode 100644 index 0000000000000..5c6f22d0eff94 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { deserializePipelines } from './pipeline_serialization'; + +describe('pipeline_serialization', () => { + describe('deserializePipelines()', () => { + it('should deserialize pipelines', () => { + expect( + deserializePipelines({ + pipeline1: { + description: 'pipeline 1 description', + version: 1, + processors: [ + { + script: { + source: 'ctx._type = null', + }, + }, + ], + }, + pipeline2: { + description: 'pipeline2 description', + version: 1, + processors: [], + }, + }) + ).toEqual([ + { + name: 'pipeline1', + description: 'pipeline 1 description', + version: 1, + processors: [ + { + script: { + source: 'ctx._type = null', + }, + }, + ], + }, + { + name: 'pipeline2', + description: 'pipeline2 description', + version: 1, + processors: [], + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.ts b/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.ts new file mode 100644 index 0000000000000..e137502d4dc37 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PipelinesByName, Pipeline } from '../types'; + +export function deserializePipelines(pipelinesByName: PipelinesByName): Pipeline[] { + const pipelineNames: string[] = Object.keys(pipelinesByName); + + const deserializedTemplates = pipelineNames.map((name: string) => { + const { description, version, processors } = pipelinesByName[name]; + + return { + name, + description, + version, + processors, + }; + }); + + return deserializedTemplates; +} diff --git a/x-pack/plugins/ingest_pipelines/common/types.ts b/x-pack/plugins/ingest_pipelines/common/types.ts new file mode 100644 index 0000000000000..383d170441581 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/common/types.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +interface Processor { + [key: string]: { + [key: string]: unknown; + }; +} + +export interface Pipeline { + name: string; + description: string; + version?: number; + processors: Processor[]; +} + +export interface PipelinesByName { + [key: string]: Omit; +} diff --git a/x-pack/plugins/ingest_pipelines/kibana.json b/x-pack/plugins/ingest_pipelines/kibana.json new file mode 100644 index 0000000000000..ec02c5f80edf9 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/kibana.json @@ -0,0 +1,17 @@ +{ + "id": "ingestPipelines", + "version": "8.0.0", + "server": true, + "ui": true, + "requiredPlugins": [ + "licensing", + "management" + ], + "optionalPlugins": [ + "usageCollection" + ], + "configPath": [ + "xpack", + "ingest_pipelines" + ] +} diff --git a/x-pack/plugins/ingest_pipelines/public/application/app.tsx b/x-pack/plugins/ingest_pipelines/public/application/app.tsx new file mode 100644 index 0000000000000..2b6bcc3f63a05 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/app.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { HashRouter, Switch, Route } from 'react-router-dom'; +import { BASE_PATH } from '../../common'; +import { PipelinesList } from './sections'; + +export const App = () => { + return ( + + + + ); +}; + +export const AppWithoutRouter = () => ( + + + +); diff --git a/x-pack/plugins/ingest_pipelines/public/application/constants/index.ts b/x-pack/plugins/ingest_pipelines/public/application/constants/index.ts new file mode 100644 index 0000000000000..122b2fbd9f382 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/constants/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// UI metric constants +export const UIM_APP_NAME = 'ingest_pipelines'; +export const UIM_PIPELINES_LIST_LOAD = 'pipelines_list_load'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/index.tsx b/x-pack/plugins/ingest_pipelines/public/application/index.tsx new file mode 100644 index 0000000000000..752d1ef270128 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/index.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { ReactNode } from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { ChromeBreadcrumb } from 'src/core/public'; +import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; + +import { App } from './app'; +import { DocumentationService, UiMetricService, ApiService } from './services'; + +export interface AppServices { + setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; + metric: UiMetricService; + documentation: DocumentationService; + api: ApiService; +} + +export const renderApp = ( + element: HTMLElement, + I18nContext: ({ children }: { children: ReactNode }) => JSX.Element, + services: AppServices +) => { + render( + + + + + , + element + ); + + return () => { + unmountComponentAtNode(element); + }; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts new file mode 100644 index 0000000000000..d750aaba177ba --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PipelinesList } from './pipelines_list'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/index.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/index.ts new file mode 100644 index 0000000000000..d750aaba177ba --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PipelinesList } from './pipelines_list'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/pipelines_list.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/pipelines_list.tsx new file mode 100644 index 0000000000000..71dab47a999de --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/pipelines_list.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect } from 'react'; + +import { + EuiPageBody, + EuiPageContent, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSpacer, EuiText } from '@elastic/eui'; + +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; + +import { UIM_PIPELINES_LIST_LOAD } from '../../constants'; + +export const PipelinesList: React.FunctionComponent = () => { + const { services } = useKibana(); + + // Track component loaded + useEffect(() => { + services.metric.trackUiMetric(UIM_PIPELINES_LIST_LOAD); + }, [services.metric]); + + return ( + + + + + +

+ +

+
+ + + + + +
+
+ + + + + + + + +
+
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/api.ts b/x-pack/plugins/ingest_pipelines/public/application/services/api.ts new file mode 100644 index 0000000000000..71ebb4b25d829 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/services/api.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { HttpSetup } from 'src/core/public'; + +import { API_BASE_PATH } from '../../../common/constants'; +import { + UseRequestConfig, + sendRequest as _sendRequest, + useRequest as _useRequest, +} from '../../shared_imports'; + +export class ApiService { + private client: HttpSetup | undefined; + + private useRequest(config: UseRequestConfig) { + if (!this.client) { + throw new Error('Api service has not be initialized.'); + } + return _useRequest(this.client, config); + } + + public setup(httpClient: HttpSetup): void { + this.client = httpClient; + } + + public useLoadPipelines() { + return this.useRequest({ + path: API_BASE_PATH, + method: 'get', + }); + } +} + +export const apiService = new ApiService(); diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts b/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts new file mode 100644 index 0000000000000..d968d9762cdf8 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { DocLinksStart } from 'src/core/public'; + +export class DocumentationService { + private esDocBasePath: string = ''; + + public setup(docLinks: DocLinksStart): void { + const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } = docLinks; + const docsBase = `${ELASTIC_WEBSITE_URL}guide/en`; + + this.esDocBasePath = `${docsBase}/elasticsearch/reference/${DOC_LINK_VERSION}`; + } + + public getIngestNodeUrl() { + return `${this.esDocBasePath}/ingest.html`; + } +} + +export const documentationService = new DocumentationService(); diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/index.ts b/x-pack/plugins/ingest_pipelines/public/application/services/index.ts new file mode 100644 index 0000000000000..7f69660daefa1 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/services/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { documentationService, DocumentationService } from './documentation'; + +export { uiMetricService, UiMetricService } from './ui_metric'; + +export { apiService, ApiService } from './api'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/ui_metric.ts b/x-pack/plugins/ingest_pipelines/public/application/services/ui_metric.ts new file mode 100644 index 0000000000000..f99bb9ba331d2 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/services/ui_metric.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; + +import { UIM_APP_NAME } from '../constants'; + +export class UiMetricService { + private usageCollection: UsageCollectionSetup | undefined; + + public setup(usageCollection: UsageCollectionSetup) { + this.usageCollection = usageCollection; + } + + private track(name: string) { + if (!this.usageCollection) { + // Usage collection is an optional plugin and might be disabled + return; + } + + const { reportUiStats, METRIC_TYPE } = this.usageCollection; + reportUiStats(UIM_APP_NAME, METRIC_TYPE.COUNT, name); + } + + public trackUiMetric(eventName: string) { + return this.track(eventName); + } +} + +export const uiMetricService = new UiMetricService(); diff --git a/x-pack/plugins/ingest_pipelines/public/index.scss b/x-pack/plugins/ingest_pipelines/public/index.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/x-pack/plugins/ingest_pipelines/public/index.ts b/x-pack/plugins/ingest_pipelines/public/index.ts new file mode 100644 index 0000000000000..1acfe8f36b457 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import './index.scss'; + +import { IngestPipelinesPlugin } from './plugin'; + +export function plugin() { + return new IngestPipelinesPlugin(); +} diff --git a/x-pack/plugins/ingest_pipelines/public/plugin.ts b/x-pack/plugins/ingest_pipelines/public/plugin.ts new file mode 100644 index 0000000000000..0e505f3a8d394 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/plugin.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { CoreSetup, Plugin } from 'src/core/public'; + +import { PLUGIN_ID } from '../common/constants'; +import { documentationService, uiMetricService, apiService } from './application/services'; +import { Dependencies } from './types'; + +export class IngestPipelinesPlugin implements Plugin { + public setup(coreSetup: CoreSetup, plugins: Dependencies): void { + const { management, usageCollection } = plugins; + const { http, getStartServices } = coreSetup; + + // Initialize services + apiService.setup(http); + uiMetricService.setup(usageCollection); + + management.sections.getSection('elasticsearch')!.registerApp({ + id: PLUGIN_ID, + title: i18n.translate('xpack.ingestPipelines.appTitle', { + defaultMessage: 'Ingest Pipelines', + }), + mount: async ({ element, setBreadcrumbs }) => { + const [coreStart] = await getStartServices(); + const { + docLinks, + i18n: { Context: I18nContext }, + } = coreStart; + + documentationService.setup(docLinks); + + setBreadcrumbs([ + { + text: i18n.translate('xpack.ingestPipelines.breadcrumbsTitle', { + defaultMessage: 'Ingest Pipelines', + }), + }, + ]); + + const services = { + setBreadcrumbs, + metric: uiMetricService, + documentation: documentationService, + api: apiService, + }; + + const { renderApp } = await import('./application'); + return renderApp(element, I18nContext, services); + }, + }); + } + + public start() {} + + public stop() {} +} diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts new file mode 100644 index 0000000000000..1a278a04adedf --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + SendRequestConfig, + SendRequestResponse, + UseRequestConfig, + sendRequest, + useRequest, +} from '../../../../src/plugins/es_ui_shared/public/request/np_ready_request'; diff --git a/x-pack/plugins/ingest_pipelines/public/types.ts b/x-pack/plugins/ingest_pipelines/public/types.ts new file mode 100644 index 0000000000000..91783ea04fa9a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ManagementSetup } from 'src/plugins/management/public'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; + +export interface Dependencies { + management: ManagementSetup; + usageCollection: UsageCollectionSetup; +} diff --git a/x-pack/plugins/ingest_pipelines/server/index.ts b/x-pack/plugins/ingest_pipelines/server/index.ts new file mode 100644 index 0000000000000..dc162a5d67cb6 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext } from '../../../../src/core/server'; +import { IngestPipelinesPlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new IngestPipelinesPlugin(initializerContext); +} diff --git a/x-pack/plugins/ingest_pipelines/server/lib/index.ts b/x-pack/plugins/ingest_pipelines/server/lib/index.ts new file mode 100644 index 0000000000000..a9a3c61472d8c --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/lib/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { isEsError } from './is_es_error'; diff --git a/x-pack/plugins/ingest_pipelines/server/lib/is_es_error.ts b/x-pack/plugins/ingest_pipelines/server/lib/is_es_error.ts new file mode 100644 index 0000000000000..4137293cf39c0 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/lib/is_es_error.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as legacyElasticsearch from 'elasticsearch'; + +const esErrorsParent = legacyElasticsearch.errors._Abstract; + +export function isEsError(err: Error) { + return err instanceof esErrorsParent; +} diff --git a/x-pack/plugins/ingest_pipelines/server/plugin.ts b/x-pack/plugins/ingest_pipelines/server/plugin.ts new file mode 100644 index 0000000000000..b27ca417c3e3c --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/plugin.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; + +import { PluginInitializerContext, CoreSetup, Plugin, Logger } from 'kibana/server'; + +import { PLUGIN_ID, PLUGIN_MIN_LICENSE_TYPE } from '../common/constants'; + +import { License } from './services'; +import { ApiRoutes } from './routes'; +import { isEsError } from './lib'; +import { Dependencies } from './types'; + +export class IngestPipelinesPlugin implements Plugin { + private readonly logger: Logger; + private readonly license: License; + private readonly apiRoutes: ApiRoutes; + + constructor({ logger }: PluginInitializerContext) { + this.logger = logger.get(); + this.license = new License(); + this.apiRoutes = new ApiRoutes(); + } + + public setup({ http, elasticsearch }: CoreSetup, { licensing }: Dependencies) { + this.logger.debug('ingest_pipelines: setup'); + + const router = http.createRouter(); + + this.license.setup( + { + pluginId: PLUGIN_ID, + minimumLicenseType: PLUGIN_MIN_LICENSE_TYPE, + defaultErrorMessage: i18n.translate('xpack.ingestPipelines.licenseCheckErrorMessage', { + defaultMessage: 'License check failed', + }), + }, + { + licensing, + logger: this.logger, + } + ); + + this.apiRoutes.setup({ + router, + license: this.license, + lib: { + isEsError, + }, + }); + } + + public start() {} + + public stop() {} +} diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts new file mode 100644 index 0000000000000..3c39ac8a81b45 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { deserializePipelines } from '../../../common/lib'; +import { API_BASE_PATH } from '../../../common/constants'; +import { RouteDependencies } from '../../types'; + +export const registerGetRoutes = ({ + router, + license, + lib: { isEsError }, +}: RouteDependencies): void => { + router.get( + { path: API_BASE_PATH, validate: false }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; + + try { + const pipelines = await callAsCurrentUser('ingest.getPipeline'); + + return res.ok({ body: deserializePipelines(pipelines) }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts new file mode 100644 index 0000000000000..28e327e6c2d3c --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { registerGetRoutes } from './get'; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/index.ts b/x-pack/plugins/ingest_pipelines/server/routes/index.ts new file mode 100644 index 0000000000000..b2c940a53f8f2 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDependencies } from '../types'; + +import { registerGetRoutes } from './api'; + +export class ApiRoutes { + setup(dependencies: RouteDependencies) { + registerGetRoutes(dependencies); + } +} diff --git a/x-pack/plugins/ingest_pipelines/server/services/index.ts b/x-pack/plugins/ingest_pipelines/server/services/index.ts new file mode 100644 index 0000000000000..b7a45e59549eb --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/services/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { License } from './license'; diff --git a/x-pack/plugins/ingest_pipelines/server/services/license.ts b/x-pack/plugins/ingest_pipelines/server/services/license.ts new file mode 100644 index 0000000000000..c490aa7b57ed9 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/services/license.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Logger } from 'src/core/server'; +import { + KibanaRequest, + KibanaResponseFactory, + RequestHandler, + RequestHandlerContext, +} from 'kibana/server'; + +import { LicensingPluginSetup } from '../../../licensing/server'; +import { LicenseType, LICENSE_CHECK_STATE } from '../../../licensing/common/types'; + +export interface LicenseStatus { + isValid: boolean; + message?: string; +} + +interface SetupSettings { + pluginId: string; + minimumLicenseType: LicenseType; + defaultErrorMessage: string; +} + +export class License { + private licenseStatus: LicenseStatus = { + isValid: false, + message: 'Invalid License', + }; + + setup( + { pluginId, minimumLicenseType, defaultErrorMessage }: SetupSettings, + { licensing, logger }: { licensing: LicensingPluginSetup; logger: Logger } + ) { + licensing.license$.subscribe(license => { + const { state, message } = license.check(pluginId, minimumLicenseType); + const hasRequiredLicense = state === LICENSE_CHECK_STATE.Valid; + + if (hasRequiredLicense) { + this.licenseStatus = { isValid: true }; + } else { + this.licenseStatus = { + isValid: false, + message: message || defaultErrorMessage, + }; + if (message) { + logger.info(message); + } + } + }); + } + + guardApiRoute(handler: RequestHandler) { + const license = this; + + return function licenseCheck( + ctx: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory + ) { + const licenseStatus = license.getStatus(); + + if (!licenseStatus.isValid) { + return response.customError({ + body: { + message: licenseStatus.message || '', + }, + statusCode: 403, + }); + } + + return handler(ctx, request, response); + }; + } + + getStatus() { + return this.licenseStatus; + } +} diff --git a/x-pack/plugins/ingest_pipelines/server/types.ts b/x-pack/plugins/ingest_pipelines/server/types.ts new file mode 100644 index 0000000000000..0135ae8e2f07d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/types.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'src/core/server'; +import { LicensingPluginSetup } from '../../licensing/server'; +import { License } from './services'; +import { isEsError } from './lib'; + +export interface Dependencies { + licensing: LicensingPluginSetup; +} + +export interface RouteDependencies { + router: IRouter; + license: License; + lib: { + isEsError: typeof isEsError; + }; +} From bf94f1c1fe84608eaae40cbeb00c433ae3dc69ec Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Sat, 4 Apr 2020 21:12:44 -0400 Subject: [PATCH 02/19] add smoke test --- .../public/application/app.tsx | 2 +- .../pipelines_list/pipelines_list.tsx | 2 +- .../functional/apps/ingest_pipelines/index.ts | 14 ++++++++++ .../apps/ingest_pipelines/ingest_pipelines.ts | 27 +++++++++++++++++++ x-pack/test/functional/config.js | 5 ++++ x-pack/test/functional/page_objects/index.ts | 2 ++ .../page_objects/ingest_pipelines_page.ts | 17 ++++++++++++ 7 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 x-pack/test/functional/apps/ingest_pipelines/index.ts create mode 100644 x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts create mode 100644 x-pack/test/functional/page_objects/ingest_pipelines_page.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/app.tsx b/x-pack/plugins/ingest_pipelines/public/application/app.tsx index 2b6bcc3f63a05..9f994e75256a4 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/app.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/app.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { HashRouter, Switch, Route } from 'react-router-dom'; -import { BASE_PATH } from '../../common'; +import { BASE_PATH } from '../../common/constants'; import { PipelinesList } from './sections'; export const App = () => { diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/pipelines_list.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/pipelines_list.tsx index 71dab47a999de..d2ea0f77ebcf3 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/pipelines_list.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/pipelines_list.tsx @@ -35,7 +35,7 @@ export const PipelinesList: React.FunctionComponent = () => { -

+

{ + describe('Ingest pipelines app', function() { + this.tags('ciGroup3'); + loadTestFile(require.resolve('./ingest_pipelines')); + }); +}; diff --git a/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts b/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts new file mode 100644 index 0000000000000..c0a0d8595bf88 --- /dev/null +++ b/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const pageObjects = getPageObjects(['common', 'ingestPipelines']); + const log = getService('log'); + + describe('Ingest Pipelines', function() { + this.tags('smoke'); + before(async () => { + await pageObjects.common.navigateToApp('ingestPipelines'); + }); + + it('Loads the app', async () => { + await log.debug('Checking for section heading to say Ingest Pipelines.'); + + const headingText = await pageObjects.ingestPipelines.sectionHeadingText(); + expect(headingText).to.be('Ingest Pipelines'); + }); + }); +}; diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index cff555feace18..de46995dc9ba6 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -53,6 +53,7 @@ export default async function({ readConfigFile }) { resolve(__dirname, './apps/index_patterns'), resolve(__dirname, './apps/index_management'), resolve(__dirname, './apps/index_lifecycle_management'), + resolve(__dirname, './apps/ingest_pipelines'), resolve(__dirname, './apps/snapshot_restore'), resolve(__dirname, './apps/cross_cluster_replication'), resolve(__dirname, './apps/remote_clusters'), @@ -176,6 +177,10 @@ export default async function({ readConfigFile }) { pathname: '/app/kibana', hash: '/management/elasticsearch/index_lifecycle_management', }, + ingestPipelines: { + pathname: '/app/kibana', + hash: '/management/elasticsearch/ingest_pipelines', + }, snapshotRestore: { pathname: '/app/kibana', hash: '/management/elasticsearch/snapshot_restore', diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index 07c5719ae53c5..00019795fad0c 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -48,6 +48,7 @@ import { RoleMappingsPageProvider } from './role_mappings_page'; import { SpaceSelectorPageProvider } from './space_selector_page'; import { EndpointPageProvider } from './endpoint_page'; import { EndpointAlertsPageProvider } from './endpoint_alerts_page'; +import { IngestPipelinesPageProvider } from './ingest_pipelines_page'; // just like services, PageObjects are defined as a map of // names to Providers. Merge in Kibana's or pick specific ones @@ -83,4 +84,5 @@ export const pageObjects = { roleMappings: RoleMappingsPageProvider, endpoint: EndpointPageProvider, endpointAlerts: EndpointAlertsPageProvider, + ingestPipelines: IngestPipelinesPageProvider, }; diff --git a/x-pack/test/functional/page_objects/ingest_pipelines_page.ts b/x-pack/test/functional/page_objects/ingest_pipelines_page.ts new file mode 100644 index 0000000000000..abc85277a3617 --- /dev/null +++ b/x-pack/test/functional/page_objects/ingest_pipelines_page.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export function IngestPipelinesPageProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async sectionHeadingText() { + return await testSubjects.getVisibleText('appTitle'); + }, + }; +} From 5a536bdbc682da98639c6dab1667b15c902e8fc4 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 6 Apr 2020 17:21:51 -0400 Subject: [PATCH 03/19] fix license check --- x-pack/plugins/ingest_pipelines/server/services/license.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ingest_pipelines/server/services/license.ts b/x-pack/plugins/ingest_pipelines/server/services/license.ts index c490aa7b57ed9..31d3654c51e3e 100644 --- a/x-pack/plugins/ingest_pipelines/server/services/license.ts +++ b/x-pack/plugins/ingest_pipelines/server/services/license.ts @@ -12,7 +12,7 @@ import { } from 'kibana/server'; import { LicensingPluginSetup } from '../../../licensing/server'; -import { LicenseType, LICENSE_CHECK_STATE } from '../../../licensing/common/types'; +import { LicenseType } from '../../../licensing/common/types'; export interface LicenseStatus { isValid: boolean; @@ -37,7 +37,7 @@ export class License { ) { licensing.license$.subscribe(license => { const { state, message } = license.check(pluginId, minimumLicenseType); - const hasRequiredLicense = state === LICENSE_CHECK_STATE.Valid; + const hasRequiredLicense = state === 'valid'; if (hasRequiredLicense) { this.licenseStatus = { isValid: true }; From ef2f1abaedceff63d4ee9745ed09173eadbd1883 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 6 Apr 2020 17:43:46 -0400 Subject: [PATCH 04/19] refactor plugin setup --- .../application/mount_management_section.ts | 42 +++++++++++++++++++ .../plugins/ingest_pipelines/public/plugin.ts | 34 +++------------ 2 files changed, 48 insertions(+), 28 deletions(-) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts b/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts new file mode 100644 index 0000000000000..29db501488e53 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { CoreSetup } from 'src/core/public'; +import { ManagementAppMountParams } from 'src/plugins/management/public'; +import { i18n } from '@kbn/i18n'; + +import { documentationService, uiMetricService, apiService } from './services'; +import { renderApp } from '.'; + +export async function mountManagementSection( + coreSetup: CoreSetup, + params: ManagementAppMountParams +) { + const { element, setBreadcrumbs } = params; + const [coreStart] = await coreSetup.getStartServices(); + const { + docLinks, + i18n: { Context: I18nContext }, + } = coreStart; + + documentationService.setup(docLinks); + + setBreadcrumbs([ + { + text: i18n.translate('xpack.ingestPipelines.breadcrumbsTitle', { + defaultMessage: 'Ingest Pipelines', + }), + }, + ]); + + const services = { + setBreadcrumbs, + metric: uiMetricService, + documentation: documentationService, + api: apiService, + }; + + return renderApp(element, I18nContext, services); +} diff --git a/x-pack/plugins/ingest_pipelines/public/plugin.ts b/x-pack/plugins/ingest_pipelines/public/plugin.ts index 0e505f3a8d394..7d8eb73d35a19 100644 --- a/x-pack/plugins/ingest_pipelines/public/plugin.ts +++ b/x-pack/plugins/ingest_pipelines/public/plugin.ts @@ -8,13 +8,13 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, Plugin } from 'src/core/public'; import { PLUGIN_ID } from '../common/constants'; -import { documentationService, uiMetricService, apiService } from './application/services'; +import { uiMetricService, apiService } from './application/services'; import { Dependencies } from './types'; export class IngestPipelinesPlugin implements Plugin { public setup(coreSetup: CoreSetup, plugins: Dependencies): void { const { management, usageCollection } = plugins; - const { http, getStartServices } = coreSetup; + const { http } = coreSetup; // Initialize services apiService.setup(http); @@ -25,32 +25,10 @@ export class IngestPipelinesPlugin implements Plugin { title: i18n.translate('xpack.ingestPipelines.appTitle', { defaultMessage: 'Ingest Pipelines', }), - mount: async ({ element, setBreadcrumbs }) => { - const [coreStart] = await getStartServices(); - const { - docLinks, - i18n: { Context: I18nContext }, - } = coreStart; - - documentationService.setup(docLinks); - - setBreadcrumbs([ - { - text: i18n.translate('xpack.ingestPipelines.breadcrumbsTitle', { - defaultMessage: 'Ingest Pipelines', - }), - }, - ]); - - const services = { - setBreadcrumbs, - metric: uiMetricService, - documentation: documentationService, - api: apiService, - }; - - const { renderApp } = await import('./application'); - return renderApp(element, I18nContext, services); + mount: async params => { + const { mountManagementSection } = await import('./application/mount_management_section'); + + return await mountManagementSection(coreSetup, params); }, }); } From 7de73c8e2b936d31e4ac9a895025d7aea0a9be12 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Wed, 8 Apr 2020 15:25:39 -0400 Subject: [PATCH 05/19] Server-side create/update ingest pipelines (#62744) --- .../common/lib/pipeline_serialization.test.ts | 16 +++ .../common/lib/pipeline_serialization.ts | 11 +- .../plugins/ingest_pipelines/common/types.ts | 8 +- .../server/routes/api/create.ts | 83 +++++++++++ .../server/routes/api/index.ts | 4 + .../server/routes/api/update.ts | 70 +++++++++ .../ingest_pipelines/server/routes/index.ts | 4 +- .../server/services/license.ts | 4 +- .../api_integration/apis/management/index.js | 1 + .../apis/management/ingest_pipelines/index.ts | 12 ++ .../ingest_pipelines/ingest_pipelines.ts | 135 ++++++++++++++++++ .../ingest_pipelines/lib/elasticsearch.ts | 39 +++++ .../management/ingest_pipelines/lib/index.ts | 7 + 13 files changed, 386 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/ingest_pipelines/server/routes/api/create.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/routes/api/update.ts create mode 100644 x-pack/test/api_integration/apis/management/ingest_pipelines/index.ts create mode 100644 x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts create mode 100644 x-pack/test/api_integration/apis/management/ingest_pipelines/lib/elasticsearch.ts create mode 100644 x-pack/test/api_integration/apis/management/ingest_pipelines/lib/index.ts diff --git a/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.test.ts b/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.test.ts index 5c6f22d0eff94..2e9147065ea15 100644 --- a/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.test.ts +++ b/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.test.ts @@ -20,6 +20,14 @@ describe('pipeline_serialization', () => { }, }, ], + on_failure: [ + { + set: { + field: 'error.message', + value: '{{ failure_message }}', + }, + }, + ], }, pipeline2: { description: 'pipeline2 description', @@ -39,6 +47,14 @@ describe('pipeline_serialization', () => { }, }, ], + onFailure: [ + { + set: { + field: 'error.message', + value: '{{ failure_message }}', + }, + }, + ], }, { name: 'pipeline2', diff --git a/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.ts b/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.ts index e137502d4dc37..9fd41c5695881 100644 --- a/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.ts +++ b/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.ts @@ -9,16 +9,19 @@ import { PipelinesByName, Pipeline } from '../types'; export function deserializePipelines(pipelinesByName: PipelinesByName): Pipeline[] { const pipelineNames: string[] = Object.keys(pipelinesByName); - const deserializedTemplates = pipelineNames.map((name: string) => { - const { description, version, processors } = pipelinesByName[name]; + const deserializedPipelines = pipelineNames.map((name: string) => { + const { description, version, processors, on_failure } = pipelinesByName[name]; - return { + const pipeline = { name, description, version, processors, + onFailure: on_failure, }; + + return pipeline; }); - return deserializedTemplates; + return deserializedPipelines; } diff --git a/x-pack/plugins/ingest_pipelines/common/types.ts b/x-pack/plugins/ingest_pipelines/common/types.ts index 383d170441581..6e02922a71018 100644 --- a/x-pack/plugins/ingest_pipelines/common/types.ts +++ b/x-pack/plugins/ingest_pipelines/common/types.ts @@ -15,8 +15,14 @@ export interface Pipeline { description: string; version?: number; processors: Processor[]; + onFailure?: Processor[]; } export interface PipelinesByName { - [key: string]: Omit; + [key: string]: { + description: string; + version?: number; + processors: Processor[]; + on_failure?: Processor[]; + }; } diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts new file mode 100644 index 0000000000000..013681fa2f4b7 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; + +import { Pipeline } from '../../../common/types'; +import { API_BASE_PATH } from '../../../common/constants'; +import { RouteDependencies } from '../../types'; + +const bodySchema = schema.object({ + name: schema.string(), + description: schema.string(), + processors: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), + version: schema.maybe(schema.number()), + onFailure: schema.maybe(schema.arrayOf(schema.recordOf(schema.string(), schema.any()))), +}); + +export const registerCreateRoute = ({ + router, + license, + lib: { isEsError }, +}: RouteDependencies): void => { + router.put( + { + path: API_BASE_PATH, + validate: { + body: bodySchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; + const pipeline = req.body as Pipeline; + + const { name, description, processors, version, onFailure } = pipeline; + + try { + // Check that a pipeline with the same name doesn't already exist + const pipelineByName = await callAsCurrentUser('ingest.getPipeline', { id: name }); + + if (pipelineByName[name]) { + return res.conflict({ + body: new Error( + i18n.translate('xpack.ingestPipelines.createRoute.duplicatePipelineIdErrorMessage', { + defaultMessage: "There is already a pipeline with name '{name}'.", + values: { + name, + }, + }) + ), + }); + } + } catch (e) { + // Silently swallow error + } + + try { + const response = await callAsCurrentUser('ingest.putPipeline', { + id: name, + body: { + description, + processors, + version, + on_failure: onFailure, + }, + }); + + return res.ok({ body: response }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts index 28e327e6c2d3c..0d40d17205eed 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts @@ -5,3 +5,7 @@ */ export { registerGetRoutes } from './get'; + +export { registerCreateRoute } from './create'; + +export { registerUpdateRoute } from './update'; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts new file mode 100644 index 0000000000000..4a13c3b15b754 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; + +import { Pipeline } from '../../../common/types'; +import { API_BASE_PATH } from '../../../common/constants'; +import { RouteDependencies } from '../../types'; + +const bodySchema = schema.object({ + description: schema.string(), + processors: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), + version: schema.maybe(schema.number()), + onFailure: schema.maybe(schema.arrayOf(schema.recordOf(schema.string(), schema.any()))), +}); + +const paramsSchema = schema.object({ + name: schema.string(), +}); + +export const registerUpdateRoute = ({ + router, + license, + lib: { isEsError }, +}: RouteDependencies): void => { + router.put( + { + path: `${API_BASE_PATH}/{name}`, + validate: { + body: bodySchema, + params: paramsSchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; + const { name } = req.params; + const pipeline = req.body as Pipeline; + + const { description, processors, version, onFailure } = pipeline; + + try { + // Verify pipeline exists; ES will throw 404 if it doesn't + await callAsCurrentUser('ingest.getPipeline', { id: name }); + + const response = await callAsCurrentUser('ingest.putPipeline', { + id: name, + body: { + description, + processors, + version, + on_failure: onFailure, + }, + }); + + return res.ok({ body: response }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/index.ts b/x-pack/plugins/ingest_pipelines/server/routes/index.ts index b2c940a53f8f2..d217fb937778c 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/index.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/index.ts @@ -6,10 +6,12 @@ import { RouteDependencies } from '../types'; -import { registerGetRoutes } from './api'; +import { registerGetRoutes, registerCreateRoute, registerUpdateRoute } from './api'; export class ApiRoutes { setup(dependencies: RouteDependencies) { registerGetRoutes(dependencies); + registerCreateRoute(dependencies); + registerUpdateRoute(dependencies); } } diff --git a/x-pack/plugins/ingest_pipelines/server/services/license.ts b/x-pack/plugins/ingest_pipelines/server/services/license.ts index 31d3654c51e3e..0a4748bd0ace0 100644 --- a/x-pack/plugins/ingest_pipelines/server/services/license.ts +++ b/x-pack/plugins/ingest_pipelines/server/services/license.ts @@ -53,12 +53,12 @@ export class License { }); } - guardApiRoute(handler: RequestHandler) { + guardApiRoute(handler: RequestHandler) { const license = this; return function licenseCheck( ctx: RequestHandlerContext, - request: KibanaRequest, + request: KibanaRequest, response: KibanaResponseFactory ) { const licenseStatus = license.getStatus(); diff --git a/x-pack/test/api_integration/apis/management/index.js b/x-pack/test/api_integration/apis/management/index.js index 352cd56d0fc9f..cef2caa918620 100644 --- a/x-pack/test/api_integration/apis/management/index.js +++ b/x-pack/test/api_integration/apis/management/index.js @@ -12,5 +12,6 @@ export default function({ loadTestFile }) { loadTestFile(require.resolve('./rollup')); loadTestFile(require.resolve('./index_management')); loadTestFile(require.resolve('./index_lifecycle_management')); + loadTestFile(require.resolve('./ingest_pipelines')); }); } diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/index.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/index.ts new file mode 100644 index 0000000000000..ca222ebc2c1e3 --- /dev/null +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('Ingest Node Pipelines', () => { + loadTestFile(require.resolve('./ingest_pipelines')); + }); +} diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts new file mode 100644 index 0000000000000..2b2a64302d839 --- /dev/null +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { registerEsHelpers } from './lib'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +const API_BASE_PATH = '/api/ingest_pipelines'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + const { createPipeline, deletePipeline } = registerEsHelpers(getService); + + describe('Pipelines', function() { + describe('Create', () => { + const PIPELINE_ID = 'test_create_pipeline'; + after(() => deletePipeline(PIPELINE_ID)); + + it('should create a pipeline', async () => { + const { body } = await supertest + .put(API_BASE_PATH) + .set('kbn-xsrf', 'xxx') + .send({ + name: PIPELINE_ID, + description: 'test pipeline description', + processors: [ + { + script: { + source: 'ctx._type = null', + }, + }, + ], + onFailure: [ + { + set: { + field: 'error.message', + value: '{{ failure_message }}', + }, + }, + ], + version: 1, + }) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + }); + + it('should not allow creation of an existing pipeline', async () => { + const { body } = await supertest + .put(API_BASE_PATH) + .set('kbn-xsrf', 'xxx') + .send({ + name: PIPELINE_ID, + description: 'test pipeline description', + processors: [ + { + script: { + source: 'ctx._type = null', + }, + }, + ], + version: 1, + }) + .expect(409); + + expect(body).to.eql({ + statusCode: 409, + error: 'Conflict', + message: `There is already a pipeline with name '${PIPELINE_ID}'.`, + }); + }); + }); + + describe('Update', () => { + const PIPELINE_ID = 'test_update_pipeline'; + const PIPELINE = { + description: 'test pipeline description', + processors: [ + { + script: { + source: 'ctx._type = null', + }, + }, + ], + version: 1, + }; + + before(() => createPipeline({ body: PIPELINE, id: PIPELINE_ID })); + after(() => deletePipeline(PIPELINE_ID)); + + it('should allow an existing pipeline to be updated', async () => { + const uri = `${API_BASE_PATH}/${PIPELINE_ID}`; + + const { body } = await supertest + .put(uri) + .set('kbn-xsrf', 'xxx') + .send({ + ...PIPELINE, + description: 'updated test pipeline description', + }) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + }); + + it('should not allow a non-existing pipeline to be updated', async () => { + const uri = `${API_BASE_PATH}/pipeline_does_not_exist`; + + const { body } = await supertest + .put(uri) + .set('kbn-xsrf', 'xxx') + .send({ + ...PIPELINE, + description: 'updated test pipeline description', + }) + .expect(404); + + expect(body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/elasticsearch.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/elasticsearch.ts new file mode 100644 index 0000000000000..2f42596a66b54 --- /dev/null +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/elasticsearch.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +interface Processor { + [key: string]: { + [key: string]: unknown; + }; +} + +interface Pipeline { + id: string; + body: { + description: string; + processors: Processor[]; + version?: number; + }; +} + +/** + * Helpers to create and delete pipelines on the Elasticsearch instance + * during our tests. + * @param {ElasticsearchClient} es The Elasticsearch client instance + */ +export const registerEsHelpers = (getService: FtrProviderContext['getService']) => { + const es = getService('legacyEs'); + + const createPipeline = (pipeline: Pipeline) => es.ingest.putPipeline(pipeline); + + const deletePipeline = (pipelineId: string) => es.ingest.deletePipeline({ id: pipelineId }); + + return { + createPipeline, + deletePipeline, + }; +}; diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/index.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/index.ts new file mode 100644 index 0000000000000..66ea0fe40c4ce --- /dev/null +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { registerEsHelpers } from './elasticsearch'; From 699ebefacf49775f621e07341eb8895e63169b0a Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Thu, 9 Apr 2020 16:21:37 +0200 Subject: [PATCH 06/19] List pipelines (#62785) * First iteration of ingest table * Add action placeholders * Refactor list and address PR feedback Refactored the list into smaller pieces and assemble in main.tsx Also addressed feedback on copy, removed unused notifications dep * WiP on flyout Showing name in title * Add reload button * Finish first version of flyout * Slight update to copy * `delete` -> `edit` * Address PR feedback Copy and a11y updates * Add on failure JSON to flyout if it is available * Add details json block file and remove ununsed import Co-authored-by: Elastic Machine --- .../sections/pipelines_list/details.tsx | 130 +++++++++++++++++ .../pipelines_list/details_json_block.tsx | 27 ++++ .../sections/pipelines_list/empty_list.tsx | 32 +++++ .../sections/pipelines_list/index.ts | 2 +- .../sections/pipelines_list/main.tsx | 132 ++++++++++++++++++ .../pipelines_list/pipelines_list.tsx | 73 ---------- .../sections/pipelines_list/table.tsx | 91 ++++++++++++ .../public/application/services/api.ts | 7 +- .../ingest_pipelines/public/shared_imports.ts | 6 + 9 files changed, 423 insertions(+), 77 deletions(-) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_json_block.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx delete mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/pipelines_list.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx new file mode 100644 index 0000000000000..2fa13b5da43e2 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiTitle, + EuiDescriptionList, + EuiSpacer, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, +} from '@elastic/eui'; +import { Pipeline } from '../../../../common/types'; + +import { PipelineDetailsJsonBlock } from './details_json_block'; + +export interface Props { + pipeline: Pipeline; + onEditClick: () => void; + onDeleteClick: () => void; + onClose: () => void; +} + +export const PipelineDetails: FunctionComponent = ({ + pipeline, + onClose, + onEditClick, + onDeleteClick, +}) => { + const descriptionListItems = [ + { + title: i18n.translate('xpack.ingestPipelines.list.pipelineDetails.descriptionTitle', { + defaultMessage: 'Description', + }), + description: pipeline.description ?? '', + }, + ]; + + if (pipeline.version) { + descriptionListItems.push({ + title: i18n.translate('xpack.ingestPipelines.list.pipelineDetails.versionTitle', { + defaultMessage: 'Version', + }), + description: String(pipeline.version), + }); + } + + return ( + + + +

{pipeline.name}

+
+
+ + + + + + + + + {/* On Failure Processor JSON */} + {pipeline.onFailure?.length && ( + <> + + + + )} + {/* End On Failure Processor JSON */} + + + + + + + {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.closeButtonLabel', { + defaultMessage: 'Close', + })} + + + + + + {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.editButtonLabel', { + defaultMessage: 'Edit', + })} + + + + + {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.deleteButtonLabel', { + defaultMessage: 'Delete', + })} + + + + + +
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_json_block.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_json_block.tsx new file mode 100644 index 0000000000000..b648d2445b271 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_json_block.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiCodeBlock, EuiText } from '@elastic/eui'; + +export interface Props { + htmlForId: string; + label: string; + json: Record; +} + +export const PipelineDetailsJsonBlock: FunctionComponent = ({ label, htmlForId, json }) => ( + <> + + + + + {JSON.stringify(json, null, 2)} + + +); diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx new file mode 100644 index 0000000000000..c109334168da9 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; + +interface Props { + onClick: () => void; +} + +export const EmptyList: FunctionComponent = ({ onClick }) => ( + + {i18n.translate('xpack.ingestPipelines.list.table.emptyPromptTitle', { + defaultMessage: 'Create your first pipeline', + })} +

+ } + actions={ + + {i18n.translate('xpack.ingestPipelines.list.table.emptyPrompt.createButtonLabel', { + defaultMessage: 'Create pipeline', + })} + + } + /> +); diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/index.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/index.ts index d750aaba177ba..a541e3bb85fd0 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { PipelinesList } from './pipelines_list'; +export { PipelinesList } from './main'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx new file mode 100644 index 0000000000000..5cd63a61123f3 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + EuiPageBody, + EuiPageContent, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiCallOut, +} from '@elastic/eui'; + +import { EuiSpacer, EuiText } from '@elastic/eui'; + +import { Pipeline } from '../../../../common/types'; +import { useKibana, SectionLoading } from '../../../shared_imports'; +import { UIM_PIPELINES_LIST_LOAD } from '../../constants'; + +import { EmptyList } from './empty_list'; +import { PipelineTable } from './table'; +import { PipelineDetails } from './details'; + +export const PipelinesList: React.FunctionComponent = () => { + const { services } = useKibana(); + + const [selectedPipeline, setSelectedPipeline] = useState(undefined); + + // Track component loaded + useEffect(() => { + services.metric.trackUiMetric(UIM_PIPELINES_LIST_LOAD); + }, [services.metric]); + + const { data, isLoading, error, sendRequest } = services.api.useLoadPipelines(); + + let content: React.ReactNode; + + if (isLoading) { + content = ( + + + + ); + } else if (data?.length) { + content = ( + { + sendRequest(); + }} + onEditPipelineClick={() => {}} + onDeletePipelineClick={() => {}} + onViewPipelineClick={setSelectedPipeline} + pipelines={data} + /> + ); + } else { + content = {}} />; + } + + return ( + <> + + + + + +

+ +

+
+ + + + + +
+
+ + + + + + + + {/* Error call out or pipeline table */} + {error ? ( + + ) : ( + content + )} +
+
+ {selectedPipeline && ( + setSelectedPipeline(undefined)} + onDeleteClick={() => {}} + onEditClick={() => {}} + /> + )} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/pipelines_list.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/pipelines_list.tsx deleted file mode 100644 index d2ea0f77ebcf3..0000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/pipelines_list.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useEffect } from 'react'; - -import { - EuiPageBody, - EuiPageContent, - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiSpacer, EuiText } from '@elastic/eui'; - -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; - -import { UIM_PIPELINES_LIST_LOAD } from '../../constants'; - -export const PipelinesList: React.FunctionComponent = () => { - const { services } = useKibana(); - - // Track component loaded - useEffect(() => { - services.metric.trackUiMetric(UIM_PIPELINES_LIST_LOAD); - }, [services.metric]); - - return ( - - - - - -

- -

-
- - - - - -
-
- - - - - - - - -
-
- ); -}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx new file mode 100644 index 0000000000000..fb79c062c7722 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiInMemoryTable, EuiLink, EuiButton } from '@elastic/eui'; + +import { Pipeline } from '../../../../common/types'; + +export interface Props { + pipelines: Pipeline[]; + onReloadClick: () => void; + onEditPipelineClick: (pipeline: Pipeline) => void; + onDeletePipelineClick: (pipeline: Pipeline) => void; + onViewPipelineClick: (pipeline: Pipeline) => void; +} + +export const PipelineTable: FunctionComponent = ({ + pipelines, + onReloadClick, + onEditPipelineClick, + onDeletePipelineClick, + onViewPipelineClick, +}) => { + return ( + + {i18n.translate('xpack.ingestPipelines.list.table.reloadButtonLabel', { + defaultMessage: 'Reload', + })} + + ), + box: { + incremental: true, + }, + }} + pagination={{ + initialPageSize: 10, + pageSizeOptions: [10, 20, 50], + }} + columns={[ + { + field: 'name', + name: i18n.translate('xpack.ingestPipelines.list.table.nameColumnTitle', { + defaultMessage: 'Name', + }), + render: (name: any, pipeline) => ( + onViewPipelineClick(pipeline)}>{name} + ), + }, + { + name: i18n.translate('xpack.ingestPipelines.list.table.actionColumnTitle', { + defaultMessage: 'Actions', + }), + actions: [ + { + name: i18n.translate('xpack.ingestPipelines.list.table.editActionLabel', { + defaultMessage: 'Edit', + }), + description: i18n.translate( + 'xpack.ingestPipelines.list.table.editActionDescription', + { defaultMessage: 'Edit this pipeline' } + ), + type: 'icon', + icon: 'pencil', + onClick: onEditPipelineClick, + }, + { + name: i18n.translate('xpack.ingestPipelines.list.table.deleteActionLabel', { + defaultMessage: 'Delete', + }), + description: i18n.translate( + 'xpack.ingestPipelines.list.table.deleteActionDescription', + { defaultMessage: 'Delete this pipeline' } + ), + type: 'icon', + icon: 'trash', + color: 'danger', + onClick: onDeletePipelineClick, + }, + ], + }, + ]} + items={pipelines ?? []} + /> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/api.ts b/x-pack/plugins/ingest_pipelines/public/application/services/api.ts index 71ebb4b25d829..98d5c0db7b867 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/services/api.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/services/api.ts @@ -6,6 +6,7 @@ import { HttpSetup } from 'src/core/public'; import { API_BASE_PATH } from '../../../common/constants'; +import { Pipeline } from '../../../common/types'; import { UseRequestConfig, sendRequest as _sendRequest, @@ -15,11 +16,11 @@ import { export class ApiService { private client: HttpSetup | undefined; - private useRequest(config: UseRequestConfig) { + private useRequest(config: UseRequestConfig) { if (!this.client) { throw new Error('Api service has not be initialized.'); } - return _useRequest(this.client, config); + return _useRequest(this.client, config); } public setup(httpClient: HttpSetup): void { @@ -27,7 +28,7 @@ export class ApiService { } public useLoadPipelines() { - return this.useRequest({ + return this.useRequest({ path: API_BASE_PATH, method: 'get', }); diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts index 1a278a04adedf..7bcba3a638a97 100644 --- a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -3,6 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { useKibana as _useKibana } from '../../../../src/plugins/kibana_react/public'; +import { AppServices } from './application'; export { SendRequestConfig, @@ -11,3 +13,7 @@ export { sendRequest, useRequest, } from '../../../../src/plugins/es_ui_shared/public/request/np_ready_request'; + +export { SectionLoading } from '../../../../src/plugins/es_ui_shared/public'; + +export const useKibana = () => _useKibana(); From 92b3f21ac0cfc24fd4e9d7072babbbda4c4f66ed Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Tue, 14 Apr 2020 15:42:30 -0400 Subject: [PATCH 07/19] [Ingest pipelines] Create pipeline UI (#63017) --- .../public/application/app.tsx | 3 +- .../public/application/components/index.ts | 9 + .../components/pipeline_form/index.ts | 7 + .../pipeline_form/pipeline_form.tsx | 294 ++++++++++++++++++ .../components/pipeline_form/schema.tsx | 132 ++++++++ .../application/components/section_error.tsx | 29 ++ .../public/application/constants/index.ts | 2 + .../public/application/index.tsx | 5 +- .../application/mount_management_section.ts | 14 +- .../public/application/sections/index.ts | 2 + .../sections/pipelines_create/index.ts | 7 + .../pipelines_create/pipelines_create.tsx | 60 ++++ .../sections/pipelines_list/empty_list.tsx | 9 +- .../sections/pipelines_list/main.tsx | 5 +- .../sections/pipelines_list/table.tsx | 26 +- .../public/application/services/api.ts | 36 ++- .../application/services/breadcrumbs.ts | 60 ++++ .../application/services/documentation.ts | 8 + .../public/application/services/index.ts | 2 + .../plugins/ingest_pipelines/public/plugin.ts | 2 +- .../ingest_pipelines/public/shared_imports.ts | 29 ++ .../server/routes/api/create.ts | 2 +- .../ingest_pipelines/ingest_pipelines.ts | 4 +- 23 files changed, 714 insertions(+), 33 deletions(-) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/section_error.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/app.tsx b/x-pack/plugins/ingest_pipelines/public/application/app.tsx index 9f994e75256a4..f3c6ccd161f66 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/app.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/app.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { HashRouter, Switch, Route } from 'react-router-dom'; import { BASE_PATH } from '../../common/constants'; -import { PipelinesList } from './sections'; +import { PipelinesList, PipelinesCreate } from './sections'; export const App = () => { return ( @@ -20,5 +20,6 @@ export const App = () => { export const AppWithoutRouter = () => ( + ); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/index.ts new file mode 100644 index 0000000000000..705dbe54618d6 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PipelineForm } from './pipeline_form'; + +export { SectionError } from './section_error'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/index.ts new file mode 100644 index 0000000000000..21a2ee30a84e1 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PipelineForm } from './pipeline_form'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx new file mode 100644 index 0000000000000..59f1f659dadea --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx @@ -0,0 +1,294 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiSwitch, EuiLink } from '@elastic/eui'; + +import { + useForm, + Form, + getUseField, + getFormRow, + Field, + FormConfig, + JsonEditorField, + useKibana, +} from '../../../shared_imports'; +import { Pipeline } from '../../../../common/types'; + +import { SectionError } from '../section_error'; +import { pipelineFormSchema } from './schema'; + +interface Props { + onSave: (pipeline: Pipeline) => void; + isSaving: boolean; + saveError: any; + defaultValue?: Pipeline; +} + +const UseField = getUseField({ component: Field }); +const FormRow = getFormRow({ titleTag: 'h3' }); + +export const PipelineForm: React.FunctionComponent = ({ + defaultValue = { + name: '', + description: '', + processors: '', + onFailure: '', + version: '', + }, + onSave, + isSaving, + saveError, +}) => { + const { services } = useKibana(); + + const [isVersionVisible, setIsVersionVisible] = useState(false); + const [isOnFailureEditorVisible, setIsOnFailureEditorVisible] = useState(false); + + const handleSave: FormConfig['onSubmit'] = (formData, isValid) => { + if (isValid) { + onSave(formData as Pipeline); + } + }; + + const { form } = useForm({ + schema: pipelineFormSchema, + defaultValue, + onSubmit: handleSave, + }); + + return ( + <> + {saveError ? ( + <> + + } + error={saveError} + data-test-subj="savePipelineError" + /> + + + ) : null} + +
+ {/* Name field with optional version field */} + + } + description={ + <> + + + + } + checked={isVersionVisible} + onChange={e => setIsVersionVisible(e.target.checked)} + data-test-subj="versionToggle" + /> + + } + > + + + {isVersionVisible && ( + + )} + + + {/* Description */} + + } + description={ + + } + > + + + + {/* Processors field */} + + } + description={ + + {i18n.translate('xpack.ingestPipelines.form.processorsDocumentionLink', { + defaultMessage: 'Learn more.', + })} + + ), + }} + /> + } + > + + + + {/* On-failure field */} + + } + description={ + <> + + {i18n.translate('xpack.ingestPipelines.form.onFailureDocumentionLink', { + defaultMessage: 'Learn more.', + })} + + ), + }} + /> + + + } + checked={isOnFailureEditorVisible} + onChange={e => setIsOnFailureEditorVisible(e.target.checked)} + data-test-subj="onFailureToggle" + /> + + } + > + {isOnFailureEditorVisible ? ( + + ) : ( + // requires children or a field + // For now, we return an empty
if the editor is not visible +
+ )} + + + + + {/* Form submission */} + + + + + + { + + } + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx new file mode 100644 index 0000000000000..4bc3e6a543206 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +import { + FormSchema, + FIELD_TYPES, + fieldValidators, + fieldFormatters, + isJSON, + isEmptyString, + ValidationFuncArg, +} from '../../../shared_imports'; + +const { emptyField, isJsonField } = fieldValidators; +const { toInt } = fieldFormatters; + +const stringifyJson = (json: { [key: string]: unknown }): string => + Array.isArray(json) ? JSON.stringify(json, null, 2) : '[\n\n]'; + +const parseJson = (jsonString: string): object[] => { + let parsedJSON: any; + + try { + parsedJSON = JSON.parse(jsonString); + + if (!Array.isArray(parsedJSON)) { + // Convert object to array + parsedJSON = [parsedJSON]; + } + } catch { + parsedJSON = []; + } + + return parsedJSON; +}; + +export const pipelineFormSchema: FormSchema = { + name: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.form.nameFieldLabel', { + defaultMessage: 'Name', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.form.pipelineNameRequiredError', { + defaultMessage: 'A pipeline name is required.', + }) + ), + }, + ], + }, + description: { + type: FIELD_TYPES.TEXTAREA, + label: i18n.translate('xpack.ingestPipelines.form.descriptionFieldLabel', { + defaultMessage: 'Description', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.form.pipelineDescriptionRequiredError', { + defaultMessage: 'A pipeline description is required.', + }) + ), + }, + ], + }, + processors: { + label: i18n.translate('xpack.ingestPipelines.form.processorsFieldLabel', { + defaultMessage: 'Processors', + }), + serializer: parseJson, + deserializer: stringifyJson, + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.form.processorsRequiredError', { + defaultMessage: 'Processors are required.', + }) + ), + }, + { + validator: isJsonField( + i18n.translate('xpack.ingestPipelines.form.processorsJsonError', { + defaultMessage: 'The processors JSON is not valid.', + }) + ), + }, + ], + }, + onFailure: { + label: i18n.translate('xpack.ingestPipelines.form.onFailureFieldLabel', { + defaultMessage: 'On-failure processors (optional)', + }), + serializer: parseJson, + deserializer: stringifyJson, + validations: [ + { + validator: ({ value }: ValidationFuncArg) => { + if (isJSON(value)) { + const parsedJSON = JSON.parse(value); + if (!parsedJSON.length) { + return { + message: 'At least one on-failure processor must be defined.', + }; + } + } else { + if (!isEmptyString(value)) { + return { + message: i18n.translate('xpack.ingestPipelines.form.onFailureProcessorsJsonError', { + defaultMessage: 'The on-failure processors JSON is not valid.', + }), + }; + } + } + }, + }, + ], + }, + version: { + type: FIELD_TYPES.NUMBER, + label: i18n.translate('xpack.ingestPipelines.form.versionFieldLabel', { + defaultMessage: 'Version (optional)', + }), + formatters: [toInt], + }, +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/section_error.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/section_error.tsx new file mode 100644 index 0000000000000..317da95e24687 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/section_error.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiCallOut } from '@elastic/eui'; +import React from 'react'; + +export interface Error { + error: string; + message: string; + statusCode: number; +} + +interface Props { + title: React.ReactNode; + error: Error; +} + +export const SectionError: React.FunctionComponent = ({ title, error, ...rest }) => { + const { message } = error; + + return ( + +

{message}

+
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/constants/index.ts b/x-pack/plugins/ingest_pipelines/public/application/constants/index.ts index 122b2fbd9f382..ae2b285c91c53 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/constants/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/constants/index.ts @@ -7,3 +7,5 @@ // UI metric constants export const UIM_APP_NAME = 'ingest_pipelines'; export const UIM_PIPELINES_LIST_LOAD = 'pipelines_list_load'; +export const UIM_PIPELINE_CREATE = 'pipeline_create'; +export const UIM_PIPELINE_UPDATE = 'pipeline_update'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/index.tsx b/x-pack/plugins/ingest_pipelines/public/application/index.tsx index 752d1ef270128..914a4b3c57e70 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/index.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/index.tsx @@ -6,14 +6,13 @@ import React, { ReactNode } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { ChromeBreadcrumb } from 'src/core/public'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { App } from './app'; -import { DocumentationService, UiMetricService, ApiService } from './services'; +import { DocumentationService, UiMetricService, ApiService, BreadcrumbService } from './services'; export interface AppServices { - setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; + breadcrumbs: BreadcrumbService; metric: UiMetricService; documentation: DocumentationService; api: ApiService; diff --git a/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts b/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts index 29db501488e53..51db91295ed42 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts @@ -5,9 +5,8 @@ */ import { CoreSetup } from 'src/core/public'; import { ManagementAppMountParams } from 'src/plugins/management/public'; -import { i18n } from '@kbn/i18n'; -import { documentationService, uiMetricService, apiService } from './services'; +import { documentationService, uiMetricService, apiService, breadcrumbService } from './services'; import { renderApp } from '.'; export async function mountManagementSection( @@ -22,17 +21,10 @@ export async function mountManagementSection( } = coreStart; documentationService.setup(docLinks); - - setBreadcrumbs([ - { - text: i18n.translate('xpack.ingestPipelines.breadcrumbsTitle', { - defaultMessage: 'Ingest Pipelines', - }), - }, - ]); + breadcrumbService.setup(setBreadcrumbs); const services = { - setBreadcrumbs, + breadcrumbs: breadcrumbService, metric: uiMetricService, documentation: documentationService, api: apiService, diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts index d750aaba177ba..30935bdd9c9c4 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts @@ -5,3 +5,5 @@ */ export { PipelinesList } from './pipelines_list'; + +export { PipelinesCreate } from './pipelines_create'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/index.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/index.ts new file mode 100644 index 0000000000000..374defa869916 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PipelinesCreate } from './pipelines_create'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx new file mode 100644 index 0000000000000..6589d57994dbe --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useState, useEffect } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; + +import { BASE_PATH } from '../../../../common/constants'; +import { Pipeline } from '../../../../common/types'; +import { useKibana } from '../../../shared_imports'; +import { PipelineForm } from '../../components'; + +export const PipelinesCreate: React.FunctionComponent = ({ history }) => { + const { services } = useKibana(); + + const [isSaving, setIsSaving] = useState(false); + const [saveError, setSaveError] = useState(null); + + const onSave = async (pipeline: Pipeline) => { + setIsSaving(true); + setSaveError(null); + + const { error } = await services.api.createPipeline(pipeline); + + setIsSaving(false); + + if (error) { + setSaveError(error); + return; + } + + history.push(BASE_PATH); + }; + + useEffect(() => { + services.breadcrumbs.setBreadcrumbs('create'); + }, [services]); + + return ( + + + +

+ +

+
+ + + + +
+
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx index c109334168da9..eacabb08eced3 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx @@ -6,12 +6,9 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { BASE_PATH } from '../../../../common/constants'; -interface Props { - onClick: () => void; -} - -export const EmptyList: FunctionComponent = ({ onClick }) => ( +export const EmptyList: FunctionComponent = () => ( = ({ onClick }) => ( } actions={ - + {i18n.translate('xpack.ingestPipelines.list.table.emptyPrompt.createButtonLabel', { defaultMessage: 'Create pipeline', })} diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx index 5cd63a61123f3..40972aace12e8 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx @@ -36,7 +36,8 @@ export const PipelinesList: React.FunctionComponent = () => { // Track component loaded useEffect(() => { services.metric.trackUiMetric(UIM_PIPELINES_LIST_LOAD); - }, [services.metric]); + services.breadcrumbs.setBreadcrumbs('home'); + }, [services.metric, services.breadcrumbs]); const { data, isLoading, error, sendRequest } = services.api.useLoadPipelines(); @@ -64,7 +65,7 @@ export const PipelinesList: React.FunctionComponent = () => { /> ); } else { - content = {}} />; + content = ; } return ( diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx index fb79c062c7722..12693435f00be 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx @@ -7,6 +7,7 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiInMemoryTable, EuiLink, EuiButton } from '@elastic/eui'; +import { BASE_PATH } from '../../../../common/constants'; import { Pipeline } from '../../../../common/types'; export interface Props { @@ -27,13 +28,30 @@ export const PipelineTable: FunctionComponent = ({ return ( + toolsRight: [ + {i18n.translate('xpack.ingestPipelines.list.table.reloadButtonLabel', { defaultMessage: 'Reload', })} - - ), + , + + {i18n.translate('xpack.ingestPipelines.list.table.createPipelineButtonLabel', { + defaultMessage: 'Create a pipeline', + })} + , + ], box: { incremental: true, }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/api.ts b/x-pack/plugins/ingest_pipelines/public/application/services/api.ts index 98d5c0db7b867..92673109b037e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/services/api.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/services/api.ts @@ -5,16 +5,21 @@ */ import { HttpSetup } from 'src/core/public'; -import { API_BASE_PATH } from '../../../common/constants'; import { Pipeline } from '../../../common/types'; +import { API_BASE_PATH } from '../../../common/constants'; import { UseRequestConfig, + SendRequestConfig, + SendRequestResponse, sendRequest as _sendRequest, useRequest as _useRequest, } from '../../shared_imports'; +import { UiMetricService } from './ui_metric'; +import { UIM_PIPELINE_CREATE } from '../constants'; export class ApiService { private client: HttpSetup | undefined; + private uiMetricService: UiMetricService | undefined; private useRequest(config: UseRequestConfig) { if (!this.client) { @@ -23,8 +28,23 @@ export class ApiService { return _useRequest(this.client, config); } - public setup(httpClient: HttpSetup): void { + private sendRequest(config: SendRequestConfig): Promise { + if (!this.client) { + throw new Error('Api service has not be initialized.'); + } + return _sendRequest(this.client, config); + } + + private trackUiMetric(eventName: string) { + if (!this.uiMetricService) { + throw new Error('UI metric service has not be initialized.'); + } + return this.uiMetricService.trackUiMetric(eventName); + } + + public setup(httpClient: HttpSetup, uiMetricService: UiMetricService): void { this.client = httpClient; + this.uiMetricService = uiMetricService; } public useLoadPipelines() { @@ -33,6 +53,18 @@ export class ApiService { method: 'get', }); } + + public async createPipeline(pipeline: Pipeline) { + const result = await this.sendRequest({ + path: API_BASE_PATH, + method: 'post', + body: JSON.stringify(pipeline), + }); + + this.trackUiMetric(UIM_PIPELINE_CREATE); + + return result; + } } export const apiService = new ApiService(); diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts b/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts new file mode 100644 index 0000000000000..4d3d0d886e999 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +import { BASE_PATH } from '../../../common/constants'; +import { ManagementAppMountParams } from '../../../../../../src/plugins/management/public'; + +type SetBreadcrumbs = ManagementAppMountParams['setBreadcrumbs']; + +const homeBreadcrumbText = i18n.translate('xpack.ingestPipelines.breadcrumb.pipelinesLabel', { + defaultMessage: 'Ingest Pipelines', +}); + +export class BreadcrumbService { + private breadcrumbs: { + [key: string]: Array<{ + text: string; + href?: string; + }>; + } = { + home: [ + { + text: homeBreadcrumbText, + }, + ], + create: [ + { + text: homeBreadcrumbText, + href: `#${BASE_PATH}`, + }, + { + text: i18n.translate('xpack.ingestPipelines.breadcrumb.createPipelineLabel', { + defaultMessage: 'Create pipeline', + }), + }, + ], + }; + + private setBreadcrumbsHandler?: SetBreadcrumbs; + + public setup(setBreadcrumbsHandler: SetBreadcrumbs): void { + this.setBreadcrumbsHandler = setBreadcrumbsHandler; + } + + public setBreadcrumbs(type: 'create' | 'home'): void { + if (!this.setBreadcrumbsHandler) { + throw new Error('Breadcrumb service has not been initialized'); + } + + const newBreadcrumbs = this.breadcrumbs[type] + ? [...this.breadcrumbs[type]] + : [...this.breadcrumbs.home]; + + this.setBreadcrumbsHandler(newBreadcrumbs); + } +} + +export const breadcrumbService = new BreadcrumbService(); diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts b/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts index d968d9762cdf8..78a9764be8e13 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts @@ -18,6 +18,14 @@ export class DocumentationService { public getIngestNodeUrl() { return `${this.esDocBasePath}/ingest.html`; } + + public getProcessorsUrl() { + return `${this.esDocBasePath}/ingest-processors.html`; + } + + public getHandlingFailureUrl() { + return `${this.esDocBasePath}/handling-failure-in-pipelines.html`; + } } export const documentationService = new DocumentationService(); diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/index.ts b/x-pack/plugins/ingest_pipelines/public/application/services/index.ts index 7f69660daefa1..f03a7824f8364 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/services/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/services/index.ts @@ -9,3 +9,5 @@ export { documentationService, DocumentationService } from './documentation'; export { uiMetricService, UiMetricService } from './ui_metric'; export { apiService, ApiService } from './api'; + +export { breadcrumbService, BreadcrumbService } from './breadcrumbs'; diff --git a/x-pack/plugins/ingest_pipelines/public/plugin.ts b/x-pack/plugins/ingest_pipelines/public/plugin.ts index 7d8eb73d35a19..5a166588f6236 100644 --- a/x-pack/plugins/ingest_pipelines/public/plugin.ts +++ b/x-pack/plugins/ingest_pipelines/public/plugin.ts @@ -17,8 +17,8 @@ export class IngestPipelinesPlugin implements Plugin { const { http } = coreSetup; // Initialize services - apiService.setup(http); uiMetricService.setup(usageCollection); + apiService.setup(http, uiMetricService); management.sections.getSection('elasticsearch')!.registerApp({ id: PLUGIN_ID, diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts index 7bcba3a638a97..3067d06174ba7 100644 --- a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -14,6 +14,35 @@ export { useRequest, } from '../../../../src/plugins/es_ui_shared/public/request/np_ready_request'; +export { + FormSchema, + FIELD_TYPES, + VALIDATION_TYPES, + FieldConfig, + FormConfig, + useForm, + Form, + getUseField, + ValidationFuncArg, + FormData, +} from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; + +export { + fieldFormatters, + fieldValidators, + serializers, +} from '../../../../src/plugins/es_ui_shared/static/forms/helpers'; + +export { + getFormRow, + Field, + JsonEditorField, +} from '../../../../src/plugins/es_ui_shared/static/forms/components'; + +export { + isJSON, + isEmptyString, +} from '../../../../src/plugins/es_ui_shared/static/validators/string'; export { SectionLoading } from '../../../../src/plugins/es_ui_shared/public'; export const useKibana = () => _useKibana(); diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts index 013681fa2f4b7..cad29a2fe555d 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts @@ -23,7 +23,7 @@ export const registerCreateRoute = ({ license, lib: { isEsError }, }: RouteDependencies): void => { - router.put( + router.post( { path: API_BASE_PATH, validate: { diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts index 2b2a64302d839..7c5a97f715869 100644 --- a/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts @@ -23,7 +23,7 @@ export default function({ getService }: FtrProviderContext) { it('should create a pipeline', async () => { const { body } = await supertest - .put(API_BASE_PATH) + .post(API_BASE_PATH) .set('kbn-xsrf', 'xxx') .send({ name: PIPELINE_ID, @@ -54,7 +54,7 @@ export default function({ getService }: FtrProviderContext) { it('should not allow creation of an existing pipeline', async () => { const { body } = await supertest - .put(API_BASE_PATH) + .post(API_BASE_PATH) .set('kbn-xsrf', 'xxx') .send({ name: PIPELINE_ID, From cf8b36bb508f177de6394165cf31d9944b1f345c Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Wed, 15 Apr 2020 14:42:40 -0400 Subject: [PATCH 08/19] [Ingest pipelines] Edit pipeline page (#63522) --- .../common/lib/pipeline_serialization.test.ts | 2 +- .../common/lib/pipeline_serialization.ts | 11 +- .../plugins/ingest_pipelines/common/types.ts | 2 +- .../public/application/app.tsx | 3 +- .../public/application/components/index.ts | 2 + .../pipeline_form/pipeline_form.tsx | 117 ++++++++++---- .../components/pipeline_form/schema.tsx | 2 +- .../components/pipeline_request_flyout.tsx | 89 +++++++++++ .../pipeline_request_flyout_provider.tsx | 29 ++++ .../application/components/section_error.tsx | 6 - .../public/application/sections/index.ts | 2 + .../pipelines_create/pipelines_create.tsx | 54 ++++++- .../sections/pipelines_edit/index.ts | 7 + .../pipelines_edit/pipelines_edit.tsx | 144 ++++++++++++++++++ .../sections/pipelines_list/details.tsx | 8 +- .../sections/pipelines_list/empty_list.tsx | 2 +- .../sections/pipelines_list/main.tsx | 18 ++- .../sections/pipelines_list/table.tsx | 4 +- .../public/application/services/api.ts | 28 +++- .../application/services/breadcrumbs.ts | 13 +- .../application/services/documentation.ts | 4 + .../ingest_pipelines/public/shared_imports.ts | 2 + .../server/routes/api/create.ts | 6 +- .../ingest_pipelines/server/routes/api/get.ts | 40 +++++ .../server/routes/api/update.ts | 6 +- .../ingest_pipelines/ingest_pipelines.ts | 56 ++++++- 26 files changed, 576 insertions(+), 81 deletions(-) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_request_flyout.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_request_flyout_provider.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx diff --git a/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.test.ts b/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.test.ts index 2e9147065ea15..65d6b6e30497f 100644 --- a/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.test.ts +++ b/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.test.ts @@ -47,7 +47,7 @@ describe('pipeline_serialization', () => { }, }, ], - onFailure: [ + on_failure: [ { set: { field: 'error.message', diff --git a/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.ts b/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.ts index 9fd41c5695881..572f655076015 100644 --- a/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.ts +++ b/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.ts @@ -10,17 +10,10 @@ export function deserializePipelines(pipelinesByName: PipelinesByName): Pipeline const pipelineNames: string[] = Object.keys(pipelinesByName); const deserializedPipelines = pipelineNames.map((name: string) => { - const { description, version, processors, on_failure } = pipelinesByName[name]; - - const pipeline = { + return { + ...pipelinesByName[name], name, - description, - version, - processors, - onFailure: on_failure, }; - - return pipeline; }); return deserializedPipelines; diff --git a/x-pack/plugins/ingest_pipelines/common/types.ts b/x-pack/plugins/ingest_pipelines/common/types.ts index 6e02922a71018..8d77359a7c3c5 100644 --- a/x-pack/plugins/ingest_pipelines/common/types.ts +++ b/x-pack/plugins/ingest_pipelines/common/types.ts @@ -15,7 +15,7 @@ export interface Pipeline { description: string; version?: number; processors: Processor[]; - onFailure?: Processor[]; + on_failure?: Processor[]; } export interface PipelinesByName { diff --git a/x-pack/plugins/ingest_pipelines/public/application/app.tsx b/x-pack/plugins/ingest_pipelines/public/application/app.tsx index f3c6ccd161f66..87fe55eae91ec 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/app.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/app.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { HashRouter, Switch, Route } from 'react-router-dom'; import { BASE_PATH } from '../../common/constants'; -import { PipelinesList, PipelinesCreate } from './sections'; +import { PipelinesList, PipelinesCreate, PipelinesEdit } from './sections'; export const App = () => { return ( @@ -21,5 +21,6 @@ export const AppWithoutRouter = () => ( + ); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/index.ts index 705dbe54618d6..39a9dc8d89e99 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/index.ts @@ -7,3 +7,5 @@ export { PipelineForm } from './pipeline_form'; export { SectionError } from './section_error'; + +export { PipelineRequestFlyoutProvider as PipelineRequestFlyout } from './pipeline_request_flyout_provider'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx index 59f1f659dadea..10b7c3d4f0931 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx @@ -6,7 +6,15 @@ import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiSwitch, EuiLink } from '@elastic/eui'; +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiSwitch, + EuiLink, +} from '@elastic/eui'; import { useForm, @@ -20,14 +28,16 @@ import { } from '../../../shared_imports'; import { Pipeline } from '../../../../common/types'; -import { SectionError } from '../section_error'; +import { SectionError, PipelineRequestFlyout } from '../'; import { pipelineFormSchema } from './schema'; interface Props { onSave: (pipeline: Pipeline) => void; + onCancel: () => void; isSaving: boolean; saveError: any; defaultValue?: Pipeline; + isEditing?: boolean; } const UseField = getUseField({ component: Field }); @@ -38,17 +48,22 @@ export const PipelineForm: React.FunctionComponent = ({ name: '', description: '', processors: '', - onFailure: '', + on_failure: '', version: '', }, onSave, isSaving, saveError, + isEditing, + onCancel, }) => { const { services } = useKibana(); - const [isVersionVisible, setIsVersionVisible] = useState(false); - const [isOnFailureEditorVisible, setIsOnFailureEditorVisible] = useState(false); + const [isVersionVisible, setIsVersionVisible] = useState(Boolean(defaultValue.version)); + const [isOnFailureEditorVisible, setIsOnFailureEditorVisible] = useState( + Boolean(defaultValue.on_failure) + ); + const [isRequestVisible, setIsRequestVisible] = useState(false); const handleSave: FormConfig['onSubmit'] = (formData, isValid) => { if (isValid) { @@ -62,24 +77,25 @@ export const PipelineForm: React.FunctionComponent = ({ onSubmit: handleSave, }); + const saveButtonLabel = isSaving ? ( + + ) : isEditing ? ( + + ) : ( + + ); + return ( <> - {saveError ? ( - <> - - } - error={saveError} - data-test-subj="savePipelineError" - /> - - - ) : null} -
= ({ path="name" componentProps={{ ['data-test-subj']: 'nameField', + euiFieldProps: { disabled: Boolean(isEditing) }, }} /> @@ -237,7 +254,7 @@ export const PipelineForm: React.FunctionComponent = ({ > {isOnFailureEditorVisible ? ( = ({ + {/* Request error */} + {saveError ? ( + <> + + } + error={saveError} + data-test-subj="savePipelineError" + /> + + + ) : null} + {/* Form submission */} - + = ({ disabled={form.isSubmitted && form.isValid === false} isLoading={isSaving} > - { - - } + {saveButtonLabel} + + + + + + + setIsRequestVisible(prevIsRequestVisible => !prevIsRequestVisible)} + > + {isRequestVisible ? ( + + ) : ( + + )} + + + {isRequestVisible ? ( + setIsRequestVisible(prevIsRequestVisible => !prevIsRequestVisible)} + /> + ) : null} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx index 4bc3e6a543206..55ee62132cf52 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx @@ -93,7 +93,7 @@ export const pipelineFormSchema: FormSchema = { }, ], }, - onFailure: { + on_failure: { label: i18n.translate('xpack.ingestPipelines.form.onFailureFieldLabel', { defaultMessage: 'On-failure processors (optional)', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_request_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_request_flyout.tsx new file mode 100644 index 0000000000000..a5184a20630d5 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_request_flyout.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useRef } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + EuiButtonEmpty, + EuiCodeBlock, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +import { Pipeline } from '../../../common/types'; + +interface Props { + pipeline: Pipeline; + closeFlyout: () => void; +} + +export const PipelineRequestFlyout: React.FunctionComponent = ({ + closeFlyout, + pipeline, +}) => { + const { name, ...pipelineBody } = pipeline; + const endpoint = `PUT _ingest/pipeline/${name || ''}`; + const payload = JSON.stringify(pipelineBody, null, 2); + const request = `${endpoint}\n${payload}`; + // Hack so that copied-to-clipboard value updates as content changes + // Related issue: https://github.com/elastic/eui/issues/3321 + const uuid = useRef(0); + uuid.current++; + + return ( + + + +

+ {name ? ( + + ) : ( + + )} +

+
+
+ + + +

+ +

+
+ + + + {request} + +
+ + + + + + +
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_request_flyout_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_request_flyout_provider.tsx new file mode 100644 index 0000000000000..8f8d89772c964 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_request_flyout_provider.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useEffect } from 'react'; + +import { Pipeline } from '../../../common/types'; +import { useFormContext } from '../../shared_imports'; +import { PipelineRequestFlyout } from './pipeline_request_flyout'; + +export const PipelineRequestFlyoutProvider = ({ closeFlyout }: { closeFlyout: () => void }) => { + const form = useFormContext(); + const [formData, setFormData] = useState({} as Pipeline); + + useEffect(() => { + const subscription = form.subscribe(async ({ isValid, validate, data }) => { + const isFormValid = isValid ?? (await validate()); + if (isFormValid) { + setFormData(data.format() as Pipeline); + } + }); + + return subscription.unsubscribe; + }, [form]); + + return ; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/section_error.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/section_error.tsx index 317da95e24687..f9ae3d588331d 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/section_error.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/section_error.tsx @@ -7,12 +7,6 @@ import { EuiCallOut } from '@elastic/eui'; import React from 'react'; -export interface Error { - error: string; - message: string; - statusCode: number; -} - interface Props { title: React.ReactNode; error: Error; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts index 30935bdd9c9c4..fde6106b508db 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts @@ -7,3 +7,5 @@ export { PipelinesList } from './pipelines_list'; export { PipelinesCreate } from './pipelines_create'; + +export { PipelinesEdit } from './pipelines_edit'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx index 6589d57994dbe..452b0fccde539 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx @@ -6,7 +6,15 @@ import React, { useState, useEffect } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { + EuiPageBody, + EuiPageContent, + EuiSpacer, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, +} from '@elastic/eui'; import { BASE_PATH } from '../../../../common/constants'; import { Pipeline } from '../../../../common/types'; @@ -35,6 +43,10 @@ export const PipelinesCreate: React.FunctionComponent = ({ history.push(BASE_PATH); }; + const onCancel = () => { + history.push(BASE_PATH); + }; + useEffect(() => { services.breadcrumbs.setBreadcrumbs('create'); }, [services]); @@ -43,17 +55,43 @@ export const PipelinesCreate: React.FunctionComponent = ({ -

- -

+ + + +

+ +

+
+
+ + + + + + +
- +
); diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/index.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/index.ts new file mode 100644 index 0000000000000..26458d23fd6d8 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PipelinesEdit } from './pipelines_edit'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx new file mode 100644 index 0000000000000..02eba9c4f620f --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useState, useEffect } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiPageBody, + EuiPageContent, + EuiSpacer, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, +} from '@elastic/eui'; + +import { BASE_PATH } from '../../../../common/constants'; +import { Pipeline } from '../../../../common/types'; +import { useKibana, SectionLoading } from '../../../shared_imports'; +import { PipelineForm, SectionError } from '../../components'; + +interface MatchParams { + name: string; +} + +export const PipelinesEdit: React.FunctionComponent> = ({ + match: { + params: { name }, + }, + history, +}) => { + const { services } = useKibana(); + + const [isSaving, setIsSaving] = useState(false); + const [saveError, setSaveError] = useState(null); + + const decodedPipelineName = decodeURI(decodeURIComponent(name)); + + const { error, data: pipeline, isLoading } = services.api.useLoadPipeline(decodedPipelineName); + + const onSave = async (updatedPipeline: Pipeline) => { + setIsSaving(true); + setSaveError(null); + + const { error: savePipelineError } = await services.api.updatePipeline(updatedPipeline); + + setIsSaving(false); + + if (savePipelineError) { + setSaveError(savePipelineError); + return; + } + + history.push(BASE_PATH); + }; + + const onCancel = () => { + history.push(BASE_PATH); + }; + + useEffect(() => { + services.breadcrumbs.setBreadcrumbs('edit'); + }, [services.breadcrumbs]); + + let content; + + if (isLoading) { + content = ( + + + + ); + } else if (error) { + content = ( + + } + error={error} + data-test-subj="fetchPipelineError" + /> + ); + } else if (pipeline) { + content = ( + + ); + } + + return ( + + + + + + +

+ +

+
+
+ + + + + + +
+
+ + + + {content} +
+
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx index 2fa13b5da43e2..798b9153a1644 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx @@ -24,7 +24,7 @@ import { PipelineDetailsJsonBlock } from './details_json_block'; export interface Props { pipeline: Pipeline; - onEditClick: () => void; + onEditClick: (pipelineName: string) => void; onDeleteClick: () => void; onClose: () => void; } @@ -80,7 +80,7 @@ export const PipelineDetails: FunctionComponent = ({ /> {/* On Failure Processor JSON */} - {pipeline.onFailure?.length && ( + {pipeline.on_failure?.length && ( <> = ({ defaultMessage: 'On failure processors JSON', } )} - json={pipeline.onFailure} + json={pipeline.on_failure} /> )} @@ -109,7 +109,7 @@ export const PipelineDetails: FunctionComponent = ({ - + onEditClick(pipeline.name)}> {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.editButtonLabel', { defaultMessage: 'Edit', })} diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx index eacabb08eced3..45c09a944a74f 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx @@ -21,7 +21,7 @@ export const EmptyList: FunctionComponent = () => ( actions={ {i18n.translate('xpack.ingestPipelines.list.table.emptyPrompt.createButtonLabel', { - defaultMessage: 'Create pipeline', + defaultMessage: 'Create a pipeline', })} } diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx index 40972aace12e8..311c1c9d4c9e7 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx @@ -5,6 +5,7 @@ */ import React, { useEffect, useState } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -21,6 +22,7 @@ import { import { EuiSpacer, EuiText } from '@elastic/eui'; import { Pipeline } from '../../../../common/types'; +import { BASE_PATH } from '../../../../common/constants'; import { useKibana, SectionLoading } from '../../../shared_imports'; import { UIM_PIPELINES_LIST_LOAD } from '../../constants'; @@ -28,7 +30,7 @@ import { EmptyList } from './empty_list'; import { PipelineTable } from './table'; import { PipelineDetails } from './details'; -export const PipelinesList: React.FunctionComponent = () => { +export const PipelinesList: React.FunctionComponent = ({ history }) => { const { services } = useKibana(); const [selectedPipeline, setSelectedPipeline] = useState(undefined); @@ -43,6 +45,10 @@ export const PipelinesList: React.FunctionComponent = () => { let content: React.ReactNode; + const editPipeline = (name: string) => { + history.push(encodeURI(`${BASE_PATH}/edit/${encodeURIComponent(name)}`)); + }; + if (isLoading) { content = ( @@ -55,10 +61,8 @@ export const PipelinesList: React.FunctionComponent = () => { } else if (data?.length) { content = ( { - sendRequest(); - }} - onEditPipelineClick={() => {}} + onReloadClick={sendRequest} + onEditPipelineClick={editPipeline} onDeletePipelineClick={() => {}} onViewPipelineClick={setSelectedPipeline} pipelines={data} @@ -106,7 +110,7 @@ export const PipelinesList: React.FunctionComponent = () => { - {/* Error call out or pipeline table */} + {/* Error call out for pipeline table */} {error ? ( { pipeline={selectedPipeline} onClose={() => setSelectedPipeline(undefined)} onDeleteClick={() => {}} - onEditClick={() => {}} + onEditClick={editPipeline} /> )} diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx index 12693435f00be..45f539007cde3 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx @@ -13,7 +13,7 @@ import { Pipeline } from '../../../../common/types'; export interface Props { pipelines: Pipeline[]; onReloadClick: () => void; - onEditPipelineClick: (pipeline: Pipeline) => void; + onEditPipelineClick: (pipeineName: string) => void; onDeletePipelineClick: (pipeline: Pipeline) => void; onViewPipelineClick: (pipeline: Pipeline) => void; } @@ -85,7 +85,7 @@ export const PipelineTable: FunctionComponent = ({ ), type: 'icon', icon: 'pencil', - onClick: onEditPipelineClick, + onClick: ({ name }) => onEditPipelineClick(name), }, { name: i18n.translate('xpack.ingestPipelines.list.table.deleteActionLabel', { diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/api.ts b/x-pack/plugins/ingest_pipelines/public/application/services/api.ts index 92673109b037e..48b925b02eeb4 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/services/api.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/services/api.ts @@ -15,7 +15,7 @@ import { useRequest as _useRequest, } from '../../shared_imports'; import { UiMetricService } from './ui_metric'; -import { UIM_PIPELINE_CREATE } from '../constants'; +import { UIM_PIPELINE_CREATE, UIM_PIPELINE_UPDATE } from '../constants'; export class ApiService { private client: HttpSetup | undefined; @@ -28,11 +28,13 @@ export class ApiService { return _useRequest(this.client, config); } - private sendRequest(config: SendRequestConfig): Promise { + private sendRequest( + config: SendRequestConfig + ): Promise> { if (!this.client) { throw new Error('Api service has not be initialized.'); } - return _sendRequest(this.client, config); + return _sendRequest(this.client, config); } private trackUiMetric(eventName: string) { @@ -54,6 +56,13 @@ export class ApiService { }); } + public useLoadPipeline(name: string) { + return this.useRequest({ + path: `${API_BASE_PATH}/${encodeURIComponent(name)}`, + method: 'get', + }); + } + public async createPipeline(pipeline: Pipeline) { const result = await this.sendRequest({ path: API_BASE_PATH, @@ -65,6 +74,19 @@ export class ApiService { return result; } + + public async updatePipeline(pipeline: Pipeline) { + const { name, ...body } = pipeline; + const result = await this.sendRequest({ + path: `${API_BASE_PATH}/${encodeURIComponent(name)}`, + method: 'put', + body: JSON.stringify(body), + }); + + this.trackUiMetric(UIM_PIPELINE_UPDATE); + + return result; + } } export const apiService = new ApiService(); diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts b/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts index 4d3d0d886e999..b6856355ddc27 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts @@ -36,6 +36,17 @@ export class BreadcrumbService { }), }, ], + edit: [ + { + text: homeBreadcrumbText, + href: `#${BASE_PATH}`, + }, + { + text: i18n.translate('xpack.ingestPipelines.breadcrumb.editPipelineLabel', { + defaultMessage: 'Edit pipeline', + }), + }, + ], }; private setBreadcrumbsHandler?: SetBreadcrumbs; @@ -44,7 +55,7 @@ export class BreadcrumbService { this.setBreadcrumbsHandler = setBreadcrumbsHandler; } - public setBreadcrumbs(type: 'create' | 'home'): void { + public setBreadcrumbs(type: 'create' | 'home' | 'edit'): void { if (!this.setBreadcrumbsHandler) { throw new Error('Breadcrumb service has not been initialized'); } diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts b/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts index 78a9764be8e13..d443ed83eb388 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts @@ -26,6 +26,10 @@ export class DocumentationService { public getHandlingFailureUrl() { return `${this.esDocBasePath}/handling-failure-in-pipelines.html`; } + + public getPutPipelineApiUrl() { + return `${this.esDocBasePath}/put-pipeline-api.html`; + } } export const documentationService = new DocumentationService(); diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts index 3067d06174ba7..1035a1d8fc864 100644 --- a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -25,6 +25,8 @@ export { getUseField, ValidationFuncArg, FormData, + FormHook, + useFormContext, } from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; export { diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts index cad29a2fe555d..63637eaac765d 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts @@ -15,7 +15,7 @@ const bodySchema = schema.object({ description: schema.string(), processors: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), version: schema.maybe(schema.number()), - onFailure: schema.maybe(schema.arrayOf(schema.recordOf(schema.string(), schema.any()))), + on_failure: schema.maybe(schema.arrayOf(schema.recordOf(schema.string(), schema.any()))), }); export const registerCreateRoute = ({ @@ -34,7 +34,7 @@ export const registerCreateRoute = ({ const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; const pipeline = req.body as Pipeline; - const { name, description, processors, version, onFailure } = pipeline; + const { name, description, processors, version, on_failure } = pipeline; try { // Check that a pipeline with the same name doesn't already exist @@ -63,7 +63,7 @@ export const registerCreateRoute = ({ description, processors, version, - on_failure: onFailure, + on_failure, }, }); diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts index 3c39ac8a81b45..90ead800e5ddf 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts @@ -3,16 +3,22 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { schema } from '@kbn/config-schema'; import { deserializePipelines } from '../../../common/lib'; import { API_BASE_PATH } from '../../../common/constants'; import { RouteDependencies } from '../../types'; +const paramsSchema = schema.object({ + name: schema.string(), +}); + export const registerGetRoutes = ({ router, license, lib: { isEsError }, }: RouteDependencies): void => { + // Get all pipelines router.get( { path: API_BASE_PATH, validate: false }, license.guardApiRoute(async (ctx, req, res) => { @@ -34,4 +40,38 @@ export const registerGetRoutes = ({ } }) ); + + // Get single pipeline + router.get( + { + path: `${API_BASE_PATH}/{name}`, + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; + const { name } = req.params; + + try { + const pipeline = await callAsCurrentUser('ingest.getPipeline', { id: name }); + + return res.ok({ + body: { + ...pipeline[name], + name, + }, + }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); }; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts index 4a13c3b15b754..27a3c9fb97ef8 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts @@ -13,7 +13,7 @@ const bodySchema = schema.object({ description: schema.string(), processors: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), version: schema.maybe(schema.number()), - onFailure: schema.maybe(schema.arrayOf(schema.recordOf(schema.string(), schema.any()))), + on_failure: schema.maybe(schema.arrayOf(schema.recordOf(schema.string(), schema.any()))), }); const paramsSchema = schema.object({ @@ -38,7 +38,7 @@ export const registerUpdateRoute = ({ const { name } = req.params; const pipeline = req.body as Pipeline; - const { description, processors, version, onFailure } = pipeline; + const { description, processors, version, on_failure } = pipeline; try { // Verify pipeline exists; ES will throw 404 if it doesn't @@ -50,7 +50,7 @@ export const registerUpdateRoute = ({ description, processors, version, - on_failure: onFailure, + on_failure, }, }); diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts index 7c5a97f715869..41f285938c003 100644 --- a/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts @@ -35,7 +35,7 @@ export default function({ getService }: FtrProviderContext) { }, }, ], - onFailure: [ + on_failure: [ { set: { field: 'error.message', @@ -131,5 +131,59 @@ export default function({ getService }: FtrProviderContext) { }); }); }); + + describe('Get', () => { + const PIPELINE_ID = 'test_pipeline'; + const PIPELINE = { + description: 'test pipeline description', + processors: [ + { + script: { + source: 'ctx._type = null', + }, + }, + ], + version: 1, + }; + + before(() => createPipeline({ body: PIPELINE, id: PIPELINE_ID })); + after(() => deletePipeline(PIPELINE_ID)); + + describe('all pipelines', () => { + it('should return an array of pipelines', async () => { + const { body } = await supertest + .get(API_BASE_PATH) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(Array.isArray(body)).to.be(true); + + // There are some pipelines created OOTB with ES + // To not be dependent on these, we only confirm the pipeline we created as part of the test exists + const testPipeline = body.find(({ name }: { name: string }) => name === PIPELINE_ID); + + expect(testPipeline).to.eql({ + ...PIPELINE, + name: PIPELINE_ID, + }); + }); + }); + + describe('one pipeline', () => { + it('should return a single pipeline', async () => { + const uri = `${API_BASE_PATH}/${PIPELINE_ID}`; + + const { body } = await supertest + .get(uri) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(body).to.eql({ + ...PIPELINE, + name: PIPELINE_ID, + }); + }); + }); + }); }); } From 7e7d7765a29de40a46a08e4d9cbdf887716c849c Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 20 Apr 2020 09:56:56 -0400 Subject: [PATCH 09/19] [Ingest pipelines] Polish details panel and empty list (#63926) --- .../sections/pipelines_list/details.tsx | 96 ++++++++++--------- .../pipelines_list/details_json_block.tsx | 32 ++++--- .../sections/pipelines_list/empty_list.tsx | 4 +- 3 files changed, 71 insertions(+), 61 deletions(-) diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx index 798b9153a1644..a3e47cb59860d 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx @@ -12,7 +12,8 @@ import { EuiFlyoutBody, EuiTitle, EuiDescriptionList, - EuiSpacer, + EuiDescriptionListTitle, + EuiDescriptionListDescription, EuiFlyoutFooter, EuiFlexGroup, EuiFlexItem, @@ -35,24 +36,6 @@ export const PipelineDetails: FunctionComponent = ({ onEditClick, onDeleteClick, }) => { - const descriptionListItems = [ - { - title: i18n.translate('xpack.ingestPipelines.list.pipelineDetails.descriptionTitle', { - defaultMessage: 'Description', - }), - description: pipeline.description ?? '', - }, - ]; - - if (pipeline.version) { - descriptionListItems.push({ - title: i18n.translate('xpack.ingestPipelines.list.pipelineDetails.versionTitle', { - defaultMessage: 'Version', - }), - description: String(pipeline.version), - }); - } - return ( = ({ - + + {/* Pipeline description */} + + {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.descriptionTitle', { + defaultMessage: 'Description', + })} + + + {pipeline.description ?? ''} + - + {/* Pipeline version */} + {pipeline.version && ( + <> + + {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.versionTitle', { + defaultMessage: 'Version', + })} + + + {String(pipeline.version)} + + + )} - + {/* Processors JSON */} + + {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.processorsTitle', { + defaultMessage: 'Processors JSON', + })} + + + + - {/* On Failure Processor JSON */} - {pipeline.on_failure?.length && ( - <> - - - - )} - {/* End On Failure Processor JSON */} + {/* On Failure Processor JSON */} + {pipeline.on_failure?.length && ( + <> + + {i18n.translate( + 'xpack.ingestPipelines.list.pipelineDetails.failureProcessorsTitle', + { + defaultMessage: 'On failure processors JSON', + } + )} + + + + + + )} + diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_json_block.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_json_block.tsx index b648d2445b271..6c44336c7547d 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_json_block.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_json_block.tsx @@ -4,24 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FunctionComponent } from 'react'; -import { EuiCodeBlock, EuiText } from '@elastic/eui'; +import React, { FunctionComponent, useRef } from 'react'; +import { EuiCodeBlock } from '@elastic/eui'; export interface Props { - htmlForId: string; - label: string; json: Record; } -export const PipelineDetailsJsonBlock: FunctionComponent = ({ label, htmlForId, json }) => ( - <> - - - - +export const PipelineDetailsJsonBlock: FunctionComponent = ({ json }) => { + // Hack so copied-to-clipboard value updates as content changes + // Related issue: https://github.com/elastic/eui/issues/3321 + const uuid = useRef(0); + uuid.current++; + + return ( + 0 ? 300 : undefined} + isCopyable + key={uuid.current} + > {JSON.stringify(json, null, 2)} - -); + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx index 45c09a944a74f..ef64fb33a6a55 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx @@ -14,12 +14,12 @@ export const EmptyList: FunctionComponent = () => ( title={

{i18n.translate('xpack.ingestPipelines.list.table.emptyPromptTitle', { - defaultMessage: 'Create your first pipeline', + defaultMessage: 'Start by creating a pipeline', })}

} actions={ - + {i18n.translate('xpack.ingestPipelines.list.table.emptyPrompt.createButtonLabel', { defaultMessage: 'Create a pipeline', })} From 6ffaeda11ffb5a5956e2be351669bb9039985e53 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 20 Apr 2020 15:03:40 -0400 Subject: [PATCH 10/19] [Ingest pipelines] Delete pipeline (#63635) --- .../public/application/constants/index.ts | 2 + .../public/application/index.tsx | 2 + .../application/mount_management_section.ts | 1 + .../sections/pipelines_list/delete_modal.tsx | 125 ++++++++++++++++++ .../sections/pipelines_list/details.tsx | 4 +- .../sections/pipelines_list/main.tsx | 19 ++- .../sections/pipelines_list/table.tsx | 34 ++++- .../public/application/services/api.ts | 18 ++- .../server/routes/api/delete.ts | 49 +++++++ .../server/routes/api/index.ts | 2 + .../ingest_pipelines/server/routes/index.ts | 8 +- .../ingest_pipelines/ingest_pipelines.ts | 99 ++++++++++++++ 12 files changed, 352 insertions(+), 11 deletions(-) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/delete_modal.tsx create mode 100644 x-pack/plugins/ingest_pipelines/server/routes/api/delete.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/constants/index.ts b/x-pack/plugins/ingest_pipelines/public/application/constants/index.ts index ae2b285c91c53..ed4bd0a42d38e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/constants/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/constants/index.ts @@ -9,3 +9,5 @@ export const UIM_APP_NAME = 'ingest_pipelines'; export const UIM_PIPELINES_LIST_LOAD = 'pipelines_list_load'; export const UIM_PIPELINE_CREATE = 'pipeline_create'; export const UIM_PIPELINE_UPDATE = 'pipeline_update'; +export const UIM_PIPELINE_DELETE = 'pipeline_delete'; +export const UIM_PIPELINE_DELETE_MANY = 'pipeline_delete_many'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/index.tsx b/x-pack/plugins/ingest_pipelines/public/application/index.tsx index 914a4b3c57e70..778ce0c873e66 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/index.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/index.tsx @@ -6,6 +6,7 @@ import React, { ReactNode } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; +import { NotificationsSetup } from 'kibana/public'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { App } from './app'; @@ -16,6 +17,7 @@ export interface AppServices { metric: UiMetricService; documentation: DocumentationService; api: ApiService; + notifications: NotificationsSetup; } export const renderApp = ( diff --git a/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts b/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts index 51db91295ed42..9b950a54096c3 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts @@ -28,6 +28,7 @@ export async function mountManagementSection( metric: uiMetricService, documentation: documentationService, api: apiService, + notifications: coreSetup.notifications, }; return renderApp(element, I18nContext, services); diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/delete_modal.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/delete_modal.tsx new file mode 100644 index 0000000000000..c7736a6c19ba1 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/delete_modal.tsx @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { useKibana } from '../../../shared_imports'; + +export const PipelineDeleteModal = ({ + pipelinesToDelete, + callback, +}: { + pipelinesToDelete: string[]; + callback: (data?: { hasDeletedPipelines: boolean }) => void; +}) => { + const { services } = useKibana(); + + const numPipelinesToDelete = pipelinesToDelete.length; + + const handleDeletePipelines = () => { + services.api + .deletePipelines(pipelinesToDelete) + .then(({ data: { itemsDeleted, errors }, error }) => { + const hasDeletedPipelines = itemsDeleted && itemsDeleted.length; + + if (hasDeletedPipelines) { + const successMessage = + itemsDeleted.length === 1 + ? i18n.translate( + 'xpack.ingestPipelines.deleteModal.successDeleteSingleNotificationMessageText', + { + defaultMessage: "Deleted pipeline '{pipelineName}'", + values: { pipelineName: pipelinesToDelete[0] }, + } + ) + : i18n.translate( + 'xpack.ingestPipelines.deleteModal.successDeleteMultipleNotificationMessageText', + { + defaultMessage: + 'Deleted {numSuccesses, plural, one {# pipeline} other {# pipelines}}', + values: { numSuccesses: itemsDeleted.length }, + } + ); + + callback({ hasDeletedPipelines }); + services.notifications.toasts.addSuccess(successMessage); + } + + if (error || errors?.length) { + const hasMultipleErrors = errors?.length > 1 || (error && pipelinesToDelete.length > 1); + const errorMessage = hasMultipleErrors + ? i18n.translate( + 'xpack.ingestPipelines.deleteModal.multipleErrorsNotificationMessageText', + { + defaultMessage: 'Error deleting {count} pipelines', + values: { + count: errors?.length || pipelinesToDelete.length, + }, + } + ) + : i18n.translate('xpack.ingestPipelines.deleteModal.errorNotificationMessageText', { + defaultMessage: "Error deleting pipeline '{name}'", + values: { name: (errors && errors[0].name) || pipelinesToDelete[0] }, + }); + services.notifications.toasts.addDanger(errorMessage); + } + }); + }; + + const handleOnCancel = () => { + callback(); + }; + + return ( + + + } + onCancel={handleOnCancel} + onConfirm={handleDeletePipelines} + cancelButtonText={ + + } + confirmButtonText={ + + } + > + <> +

+ +

+ +
    + {pipelinesToDelete.map(name => ( +
  • {name}
  • + ))} +
+ +
+
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx index a3e47cb59860d..10720153cd57b 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx @@ -26,7 +26,7 @@ import { PipelineDetailsJsonBlock } from './details_json_block'; export interface Props { pipeline: Pipeline; onEditClick: (pipelineName: string) => void; - onDeleteClick: () => void; + onDeleteClick: (pipelineName: string[]) => void; onClose: () => void; } @@ -122,7 +122,7 @@ export const PipelineDetails: FunctionComponent = ({
- + onDeleteClick([pipeline.name])}> {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.deleteButtonLabel', { defaultMessage: 'Delete', })} diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx index 311c1c9d4c9e7..ca4892fe281c2 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx @@ -29,11 +29,13 @@ import { UIM_PIPELINES_LIST_LOAD } from '../../constants'; import { EmptyList } from './empty_list'; import { PipelineTable } from './table'; import { PipelineDetails } from './details'; +import { PipelineDeleteModal } from './delete_modal'; export const PipelinesList: React.FunctionComponent = ({ history }) => { const { services } = useKibana(); const [selectedPipeline, setSelectedPipeline] = useState(undefined); + const [pipelinesToDelete, setPipelinesToDelete] = useState([]); // Track component loaded useEffect(() => { @@ -63,7 +65,7 @@ export const PipelinesList: React.FunctionComponent = ({ hi {}} + onDeletePipelineClick={setPipelinesToDelete} onViewPipelineClick={setSelectedPipeline} pipelines={data} /> @@ -128,10 +130,23 @@ export const PipelinesList: React.FunctionComponent = ({ hi setSelectedPipeline(undefined)} - onDeleteClick={() => {}} + onDeleteClick={setPipelinesToDelete} onEditClick={editPipeline} /> )} + {pipelinesToDelete?.length > 0 ? ( + { + if (deleteResponse?.hasDeletedPipelines) { + // reload pipelines list + sendRequest(); + } + setPipelinesToDelete([]); + setSelectedPipeline(undefined); + }} + pipelinesToDelete={pipelinesToDelete} + /> + ) : null} ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx index 45f539007cde3..01b05eace3b60 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx @@ -3,8 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { FunctionComponent } from 'react'; +import React, { FunctionComponent, useState } from 'react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiInMemoryTable, EuiLink, EuiButton } from '@elastic/eui'; import { BASE_PATH } from '../../../../common/constants'; @@ -13,8 +14,8 @@ import { Pipeline } from '../../../../common/types'; export interface Props { pipelines: Pipeline[]; onReloadClick: () => void; - onEditPipelineClick: (pipeineName: string) => void; - onDeletePipelineClick: (pipeline: Pipeline) => void; + onEditPipelineClick: (pipelineName: string) => void; + onDeletePipelineClick: (pipelineName: string[]) => void; onViewPipelineClick: (pipeline: Pipeline) => void; } @@ -25,9 +26,32 @@ export const PipelineTable: FunctionComponent = ({ onDeletePipelineClick, onViewPipelineClick, }) => { + const [selection, setSelection] = useState([]); + return ( 0 ? ( + onDeletePipelineClick(selection.map(pipeline => pipeline.name))} + color="danger" + > + + + ) : ( + undefined + ), toolsRight: [ = ({ name: i18n.translate('xpack.ingestPipelines.list.table.nameColumnTitle', { defaultMessage: 'Name', }), - render: (name: any, pipeline) => ( + render: (name: string, pipeline) => ( onViewPipelineClick(pipeline)}>{name} ), }, @@ -98,7 +122,7 @@ export const PipelineTable: FunctionComponent = ({ type: 'icon', icon: 'trash', color: 'danger', - onClick: onDeletePipelineClick, + onClick: ({ name }) => onDeletePipelineClick([name]), }, ], }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/api.ts b/x-pack/plugins/ingest_pipelines/public/application/services/api.ts index 48b925b02eeb4..42a157705baa7 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/services/api.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/services/api.ts @@ -15,7 +15,12 @@ import { useRequest as _useRequest, } from '../../shared_imports'; import { UiMetricService } from './ui_metric'; -import { UIM_PIPELINE_CREATE, UIM_PIPELINE_UPDATE } from '../constants'; +import { + UIM_PIPELINE_CREATE, + UIM_PIPELINE_UPDATE, + UIM_PIPELINE_DELETE, + UIM_PIPELINE_DELETE_MANY, +} from '../constants'; export class ApiService { private client: HttpSetup | undefined; @@ -87,6 +92,17 @@ export class ApiService { return result; } + + public async deletePipelines(names: string[]) { + const result = this.sendRequest({ + path: `${API_BASE_PATH}/${names.map(name => encodeURIComponent(name)).join(',')}`, + method: 'delete', + }); + + this.trackUiMetric(names.length > 1 ? UIM_PIPELINE_DELETE_MANY : UIM_PIPELINE_DELETE); + + return result; + } } export const apiService = new ApiService(); diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/delete.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/delete.ts new file mode 100644 index 0000000000000..4664b49a08a50 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/delete.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; + +import { API_BASE_PATH } from '../../../common/constants'; +import { RouteDependencies } from '../../types'; + +const paramsSchema = schema.object({ + names: schema.string(), +}); + +export const registerDeleteRoute = ({ router, license }: RouteDependencies): void => { + router.delete( + { + path: `${API_BASE_PATH}/{names}`, + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; + const { names } = req.params; + const pipelineNames = names.split(','); + + const response: { itemsDeleted: string[]; errors: any[] } = { + itemsDeleted: [], + errors: [], + }; + + await Promise.all( + pipelineNames.map(pipelineName => { + return callAsCurrentUser('ingest.deletePipeline', { id: pipelineName }) + .then(() => response.itemsDeleted.push(pipelineName)) + .catch(e => + response.errors.push({ + name: pipelineName, + error: e, + }) + ); + }) + ); + + return res.ok({ body: response }); + }) + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts index 0d40d17205eed..9992f56512c01 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts @@ -9,3 +9,5 @@ export { registerGetRoutes } from './get'; export { registerCreateRoute } from './create'; export { registerUpdateRoute } from './update'; + +export { registerDeleteRoute } from './delete'; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/index.ts b/x-pack/plugins/ingest_pipelines/server/routes/index.ts index d217fb937778c..419525816f217 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/index.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/index.ts @@ -6,12 +6,18 @@ import { RouteDependencies } from '../types'; -import { registerGetRoutes, registerCreateRoute, registerUpdateRoute } from './api'; +import { + registerGetRoutes, + registerCreateRoute, + registerUpdateRoute, + registerDeleteRoute, +} from './api'; export class ApiRoutes { setup(dependencies: RouteDependencies) { registerGetRoutes(dependencies); registerCreateRoute(dependencies); registerUpdateRoute(dependencies); + registerDeleteRoute(dependencies); } } diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts index 41f285938c003..a1773a052ede2 100644 --- a/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts @@ -185,5 +185,104 @@ export default function({ getService }: FtrProviderContext) { }); }); }); + + describe('Delete', () => { + const PIPELINE = { + description: 'test pipeline description', + processors: [ + { + script: { + source: 'ctx._type = null', + }, + }, + ], + version: 1, + }; + + it('should delete a pipeline', async () => { + // Create pipeline to be deleted + const PIPELINE_ID = 'test_delete_pipeline'; + createPipeline({ body: PIPELINE, id: PIPELINE_ID }); + + const uri = `${API_BASE_PATH}/${PIPELINE_ID}`; + + const { body } = await supertest + .delete(uri) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(body).to.eql({ + itemsDeleted: [PIPELINE_ID], + errors: [], + }); + }); + + it('should delete multiple pipelines', async () => { + // Create pipelines to be deleted + const PIPELINE_ONE_ID = 'test_delete_pipeline_1'; + const PIPELINE_TWO_ID = 'test_delete_pipeline_2'; + createPipeline({ body: PIPELINE, id: PIPELINE_ONE_ID }); + createPipeline({ body: PIPELINE, id: PIPELINE_TWO_ID }); + + const uri = `${API_BASE_PATH}/${PIPELINE_ONE_ID},${PIPELINE_TWO_ID}`; + + const { + body: { itemsDeleted, errors }, + } = await supertest + .delete(uri) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(errors).to.eql([]); + + // The itemsDeleted array order isn't guaranteed, so we assert against each pipeline name instead + [PIPELINE_ONE_ID, PIPELINE_TWO_ID].forEach(pipelineName => { + expect(itemsDeleted.includes(pipelineName)).to.be(true); + }); + }); + + it('should return an error for any pipelines not sucessfully deleted', async () => { + const PIPELINE_DOES_NOT_EXIST = 'pipeline_does_not_exist'; + + // Create pipeline to be deleted + const PIPELINE_ONE_ID = 'test_delete_pipeline_1'; + createPipeline({ body: PIPELINE, id: PIPELINE_ONE_ID }); + + const uri = `${API_BASE_PATH}/${PIPELINE_ONE_ID},${PIPELINE_DOES_NOT_EXIST}`; + + const { body } = await supertest + .delete(uri) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(body).to.eql({ + itemsDeleted: [PIPELINE_ONE_ID], + errors: [ + { + name: PIPELINE_DOES_NOT_EXIST, + error: { + msg: '[resource_not_found_exception] pipeline [pipeline_does_not_exist] is missing', + path: '/_ingest/pipeline/pipeline_does_not_exist', + query: {}, + statusCode: 404, + response: JSON.stringify({ + error: { + root_cause: [ + { + type: 'resource_not_found_exception', + reason: 'pipeline [pipeline_does_not_exist] is missing', + }, + ], + type: 'resource_not_found_exception', + reason: 'pipeline [pipeline_does_not_exist] is missing', + }, + status: 404, + }), + }, + }, + ], + }); + }); + }); }); } From 34cb91ad5d8d659a1f4ecc3a09fb090f43de2d68 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Wed, 22 Apr 2020 13:03:56 +0200 Subject: [PATCH 11/19] [Ingest Node Pipelines] Clone Pipeline (#64049) * First iteration of clone functionality Wired up for both the list table and the details flyout in the list section. * satisfy eslint * Turn on sorting for the list table * Clean up const declarations * Address PR feedback Sentence-casify and update some other copy. * Mark edit and delete as primary actions in list table * Handle URI encoded chars in pipeline name when cloning --- .../public/application/app.tsx | 3 +- .../public/application/sections/index.ts | 2 + .../sections/pipelines_clone/index.ts | 7 ++ .../pipelines_clone/pipelines_clone.tsx | 59 +++++++++++ .../pipelines_create/pipelines_create.tsx | 13 ++- .../sections/pipelines_list/details.tsx | 100 +++++++++++++++--- .../sections/pipelines_list/main.tsx | 8 +- .../sections/pipelines_list/table.tsx | 18 ++++ 8 files changed, 194 insertions(+), 16 deletions(-) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/app.tsx b/x-pack/plugins/ingest_pipelines/public/application/app.tsx index 87fe55eae91ec..1027d08c133db 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/app.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/app.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { HashRouter, Switch, Route } from 'react-router-dom'; import { BASE_PATH } from '../../common/constants'; -import { PipelinesList, PipelinesCreate, PipelinesEdit } from './sections'; +import { PipelinesList, PipelinesCreate, PipelinesEdit, PipelinesClone } from './sections'; export const App = () => { return ( @@ -20,6 +20,7 @@ export const App = () => { export const AppWithoutRouter = () => ( + diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts index fde6106b508db..b2925666c5768 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts @@ -9,3 +9,5 @@ export { PipelinesList } from './pipelines_list'; export { PipelinesCreate } from './pipelines_create'; export { PipelinesEdit } from './pipelines_edit'; + +export { PipelinesClone } from './pipelines_clone'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/index.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/index.ts new file mode 100644 index 0000000000000..614a3598d407d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PipelinesClone } from './pipelines_clone'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx new file mode 100644 index 0000000000000..b3b1217caf834 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent, useEffect } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { SectionLoading, useKibana } from '../../../shared_imports'; + +import { PipelinesCreate } from '../pipelines_create'; + +export interface ParamProps { + sourceName: string; +} + +/** + * This section is a wrapper around the create section where we receive a pipeline name + * to load and set as the source pipeline for the {@link PipelinesCreate} form. + */ +export const PipelinesClone: FunctionComponent> = props => { + const { sourceName } = props.match.params; + const { services } = useKibana(); + + const { error, data: pipeline, isLoading, isInitialRequest } = services.api.useLoadPipeline( + decodeURIComponent(sourceName) + ); + + useEffect(() => { + if (error && !isLoading) { + services.notifications!.toasts.addError(error, { + title: i18n.translate('xpack.ingestPipelines.clone.loadSourcePipelineErrorTitle', { + defaultMessage: 'Cannot load {name}.', + values: { name: sourceName }, + }), + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [error, isLoading]); + + if (isLoading && isInitialRequest) { + return ( + + + + ); + } else { + // We still show the create form even if we were not able to load the + // latest pipeline data. + const sourcePipeline = pipeline ? { ...pipeline, name: `${pipeline.name}-copy` } : undefined; + return ; + } +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx index 452b0fccde539..2f3e2630adbd1 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx @@ -21,7 +21,17 @@ import { Pipeline } from '../../../../common/types'; import { useKibana } from '../../../shared_imports'; import { PipelineForm } from '../../components'; -export const PipelinesCreate: React.FunctionComponent = ({ history }) => { +interface Props { + /** + * This value may be passed in to prepopulate the creation form + */ + sourcePipeline?: Pipeline; +} + +export const PipelinesCreate: React.FunctionComponent = ({ + history, + sourcePipeline, +}) => { const { services } = useKibana(); const [isSaving, setIsSaving] = useState(false); @@ -87,6 +97,7 @@ export const PipelinesCreate: React.FunctionComponent = ({ void; + onCloneClick: (pipelineName: string) => void; onDeleteClick: (pipelineName: string[]) => void; onClose: () => void; } @@ -34,8 +40,63 @@ export const PipelineDetails: FunctionComponent = ({ pipeline, onClose, onEditClick, + onCloneClick, onDeleteClick, }) => { + const [showPopover, setShowPopover] = useState(false); + const actionMenuItems = [ + /** + * Edit pipeline + */ + { + name: i18n.translate('xpack.ingestPipelines.list.pipelineDetails.editActionLabel', { + defaultMessage: 'Edit', + }), + icon: , + onClick: () => onEditClick(pipeline.name), + }, + /** + * Clone pipeline + */ + { + name: i18n.translate('xpack.ingestPipelines.list.pipelineDetails.cloneActionLabel', { + defaultMessage: 'Clone', + }), + icon: , + onClick: () => onCloneClick(pipeline.name), + }, + /** + * Delete pipeline + */ + { + name: i18n.translate('xpack.ingestPipelines.list.pipelineDetails.deleteActionLabel', { + defaultMessage: 'Delete', + }), + icon: , + onClick: () => onDeleteClick([pipeline.name]), + }, + ]; + + const managePipelineButton = ( + setShowPopover(previousBool => !previousBool)} + iconType="arrowUp" + iconSide="right" + fill + > + {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.managePipelineButtonLabel', { + defaultMessage: 'Manage', + })} + + ); + return ( = ({ - onEditClick(pipeline.name)}> - {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.editButtonLabel', { - defaultMessage: 'Edit', - })} - - - - onDeleteClick([pipeline.name])}> - {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.deleteButtonLabel', { - defaultMessage: 'Delete', - })} - + setShowPopover(false)} + button={managePipelineButton} + panelPaddingSize="none" + withTitle + repositionOnScroll + > + +
diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx index ca4892fe281c2..bd0043e3e74af 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx @@ -51,6 +51,10 @@ export const PipelinesList: React.FunctionComponent = ({ hi history.push(encodeURI(`${BASE_PATH}/edit/${encodeURIComponent(name)}`)); }; + const clonePipeline = (name: string) => { + history.push(encodeURI(`${BASE_PATH}/create/${encodeURIComponent(name)}`)); + }; + if (isLoading) { content = ( @@ -66,6 +70,7 @@ export const PipelinesList: React.FunctionComponent = ({ hi onReloadClick={sendRequest} onEditPipelineClick={editPipeline} onDeletePipelineClick={setPipelinesToDelete} + onClonePipelineClick={clonePipeline} onViewPipelineClick={setSelectedPipeline} pipelines={data} /> @@ -130,8 +135,9 @@ export const PipelinesList: React.FunctionComponent = ({ hi setSelectedPipeline(undefined)} - onDeleteClick={setPipelinesToDelete} onEditClick={editPipeline} + onCloneClick={clonePipeline} + onDeleteClick={setPipelinesToDelete} /> )} {pipelinesToDelete?.length > 0 ? ( diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx index 01b05eace3b60..05488f46c148e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx @@ -15,6 +15,7 @@ export interface Props { pipelines: Pipeline[]; onReloadClick: () => void; onEditPipelineClick: (pipelineName: string) => void; + onClonePipelineClick: (pipelineName: string) => void; onDeletePipelineClick: (pipelineName: string[]) => void; onViewPipelineClick: (pipeline: Pipeline) => void; } @@ -23,6 +24,7 @@ export const PipelineTable: FunctionComponent = ({ pipelines, onReloadClick, onEditPipelineClick, + onClonePipelineClick, onDeletePipelineClick, onViewPipelineClick, }) => { @@ -32,6 +34,7 @@ export const PipelineTable: FunctionComponent = ({ = ({ name: i18n.translate('xpack.ingestPipelines.list.table.nameColumnTitle', { defaultMessage: 'Name', }), + sortable: true, render: (name: string, pipeline) => ( onViewPipelineClick(pipeline)}>{name} ), @@ -100,6 +104,7 @@ export const PipelineTable: FunctionComponent = ({ }), actions: [ { + isPrimary: true, name: i18n.translate('xpack.ingestPipelines.list.table.editActionLabel', { defaultMessage: 'Edit', }), @@ -112,6 +117,19 @@ export const PipelineTable: FunctionComponent = ({ onClick: ({ name }) => onEditPipelineClick(name), }, { + name: i18n.translate('xpack.ingestPipelines.list.table.cloneActionLabel', { + defaultMessage: 'Clone', + }), + description: i18n.translate( + 'xpack.ingestPipelines.list.table.cloneActionDescription', + { defaultMessage: 'Clone this pipeline' } + ), + type: 'icon', + icon: 'copy', + onClick: ({ name }) => onClonePipelineClick(name), + }, + { + isPrimary: true, name: i18n.translate('xpack.ingestPipelines.list.table.deleteActionLabel', { defaultMessage: 'Delete', }), From eba53055a14dca43b68138377b8cc0e0c5b6a170 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Thu, 23 Apr 2020 10:10:06 +0200 Subject: [PATCH 12/19] [Ingest node pipelines] Privileges (#63850) * Create privileges check for ingest pipelines app Also moved the public side logic for checking and rendering privilege related messages to es_ui_shared/public following the new __packages_do_not_import__ convention. * Add , * Fix import paths * Address PR feedback Fix i18n strings (remove reference to snapshot and restore) and fix copy referencing snapshot and restore - all copy-pasta errors. Also remove unused field from missing privileges object. * Fix issue from resolving merge conflicts * Add missing app privilege * Use non-deprecated privilige name --- .../ingest_pipelines/common/constants.ts | 2 + .../public/application/app.tsx | 94 ++++++++++++++++--- .../public/application/index.tsx | 27 ++++-- .../application/mount_management_section.ts | 8 +- .../ingest_pipelines/public/shared_imports.ts | 11 ++- .../server/routes/api/index.ts | 2 + .../server/routes/api/privileges.ts | 62 ++++++++++++ .../ingest_pipelines/server/routes/index.ts | 2 + 8 files changed, 186 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts diff --git a/x-pack/plugins/ingest_pipelines/common/constants.ts b/x-pack/plugins/ingest_pipelines/common/constants.ts index 13cece33838df..edf681c276a84 100644 --- a/x-pack/plugins/ingest_pipelines/common/constants.ts +++ b/x-pack/plugins/ingest_pipelines/common/constants.ts @@ -14,3 +14,5 @@ export const PLUGIN_MIN_LICENSE_TYPE = basicLicense; export const BASE_PATH = '/management/elasticsearch/ingest_pipelines'; export const API_BASE_PATH = '/api/ingest_pipelines'; + +export const APP_CLUSTER_REQUIRED_PRIVILEGES = ['manage_pipeline', 'cluster:monitor/nodes/info']; diff --git a/x-pack/plugins/ingest_pipelines/public/application/app.tsx b/x-pack/plugins/ingest_pipelines/public/application/app.tsx index 1027d08c133db..2ec72267701d7 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/app.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/app.tsx @@ -3,19 +3,22 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiPageContent } from '@elastic/eui'; +import React, { FunctionComponent } from 'react'; import { HashRouter, Switch, Route } from 'react-router-dom'; -import { BASE_PATH } from '../../common/constants'; -import { PipelinesList, PipelinesCreate, PipelinesEdit, PipelinesClone } from './sections'; -export const App = () => { - return ( - - - - ); -}; +import { BASE_PATH, APP_CLUSTER_REQUIRED_PRIVILEGES } from '../../common/constants'; + +import { + SectionError, + useAuthorizationContext, + WithPrivileges, + SectionLoading, + NotAuthorizedSection, +} from '../shared_imports'; + +import { PipelinesList, PipelinesCreate, PipelinesEdit, PipelinesClone } from './sections'; export const AppWithoutRouter = () => ( @@ -25,3 +28,72 @@ export const AppWithoutRouter = () => ( ); + +export const App: FunctionComponent = () => { + const { apiError } = useAuthorizationContext(); + + if (apiError) { + return ( + + } + error={apiError} + /> + ); + } + + return ( + `cluster.${privilege}`)} + > + {({ isLoading, hasPrivileges, privilegesMissing }) => { + if (isLoading) { + return ( + + + + ); + } + + if (!hasPrivileges) { + return ( + + + } + message={ + + } + /> + + ); + } + + return ( + + + + ); + }} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/index.tsx b/x-pack/plugins/ingest_pipelines/public/application/index.tsx index 778ce0c873e66..e43dba4689b44 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/index.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/index.tsx @@ -4,11 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ +import { HttpSetup } from 'kibana/public'; import React, { ReactNode } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { NotificationsSetup } from 'kibana/public'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; +import { API_BASE_PATH } from '../../common/constants'; + +import { AuthorizationProvider } from '../shared_imports'; + import { App } from './app'; import { DocumentationService, UiMetricService, ApiService, BreadcrumbService } from './services'; @@ -20,17 +25,27 @@ export interface AppServices { notifications: NotificationsSetup; } +export interface CoreServices { + http: HttpSetup; +} + export const renderApp = ( element: HTMLElement, I18nContext: ({ children }: { children: ReactNode }) => JSX.Element, - services: AppServices + services: AppServices, + coreServices: CoreServices ) => { render( - - - - - , + + + + + + + , element ); diff --git a/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts b/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts index 9b950a54096c3..e36f27cbf5f62 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts @@ -10,11 +10,11 @@ import { documentationService, uiMetricService, apiService, breadcrumbService } import { renderApp } from '.'; export async function mountManagementSection( - coreSetup: CoreSetup, + { http, getStartServices, notifications }: CoreSetup, params: ManagementAppMountParams ) { const { element, setBreadcrumbs } = params; - const [coreStart] = await coreSetup.getStartServices(); + const [coreStart] = await getStartServices(); const { docLinks, i18n: { Context: I18nContext }, @@ -28,8 +28,8 @@ export async function mountManagementSection( metric: uiMetricService, documentation: documentationService, api: apiService, - notifications: coreSetup.notifications, + notifications, }; - return renderApp(element, I18nContext, services); + return renderApp(element, I18nContext, services, { http }); } diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts index 1035a1d8fc864..5127821f3820c 100644 --- a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -45,6 +45,15 @@ export { isJSON, isEmptyString, } from '../../../../src/plugins/es_ui_shared/static/validators/string'; -export { SectionLoading } from '../../../../src/plugins/es_ui_shared/public'; + +export { + SectionLoading, + WithPrivileges, + AuthorizationProvider, + SectionError, + Error, + useAuthorizationContext, + NotAuthorizedSection, +} from '../../../../src/plugins/es_ui_shared/public'; export const useKibana = () => _useKibana(); diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts index 9992f56512c01..37819b9bf6889 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts @@ -10,4 +10,6 @@ export { registerCreateRoute } from './create'; export { registerUpdateRoute } from './update'; +export { registerPrivilegesRoute } from './privileges'; + export { registerDeleteRoute } from './delete'; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts new file mode 100644 index 0000000000000..2e1c11928959f --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { RouteDependencies } from '../../types'; +import { API_BASE_PATH, APP_CLUSTER_REQUIRED_PRIVILEGES } from '../../../common/constants'; +import { Privileges } from '../../../../../../src/plugins/es_ui_shared/public'; + +const extractMissingPrivileges = (privilegesObject: { [key: string]: boolean } = {}): string[] => + Object.keys(privilegesObject).reduce((privileges: string[], privilegeName: string): string[] => { + if (!privilegesObject[privilegeName]) { + privileges.push(privilegeName); + } + return privileges; + }, []); + +export const registerPrivilegesRoute = ({ license, router }: RouteDependencies) => { + router.get( + { + path: `${API_BASE_PATH}/privileges`, + validate: false, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { + core: { + elasticsearch: { dataClient }, + }, + } = ctx; + + const privilegesResult: Privileges = { + hasAllPrivileges: true, + missingPrivileges: { + cluster: [], + }, + }; + + try { + const { has_all_requested: hasAllPrivileges, cluster } = await dataClient.callAsCurrentUser( + 'transport.request', + { + path: '/_security/user/_has_privileges', + method: 'POST', + body: { + cluster: APP_CLUSTER_REQUIRED_PRIVILEGES, + }, + } + ); + + if (!hasAllPrivileges) { + privilegesResult.missingPrivileges.cluster = extractMissingPrivileges(cluster); + } + + privilegesResult.hasAllPrivileges = hasAllPrivileges; + + return res.ok({ body: privilegesResult }); + } catch (e) { + return res.internalError(e); + } + }) + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/index.ts b/x-pack/plugins/ingest_pipelines/server/routes/index.ts index 419525816f217..8cfcc1054ca4e 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/index.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/index.ts @@ -10,6 +10,7 @@ import { registerGetRoutes, registerCreateRoute, registerUpdateRoute, + registerPrivilegesRoute, registerDeleteRoute, } from './api'; @@ -18,6 +19,7 @@ export class ApiRoutes { registerGetRoutes(dependencies); registerCreateRoute(dependencies); registerUpdateRoute(dependencies); + registerPrivilegesRoute(dependencies); registerDeleteRoute(dependencies); } } From a2b404993b17f89b51861b112494b6d90707dc17 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Sat, 25 Apr 2020 06:50:31 +0200 Subject: [PATCH 13/19] [Ingest Node Pipelines] More lenient treatment of on-failure value (#64411) --- .../components/pipeline_form/schema.tsx | 43 ++++++++----------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx index 55ee62132cf52..e1809069ac11c 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx @@ -6,15 +6,7 @@ import { i18n } from '@kbn/i18n'; -import { - FormSchema, - FIELD_TYPES, - fieldValidators, - fieldFormatters, - isJSON, - isEmptyString, - ValidationFuncArg, -} from '../../../shared_imports'; +import { FormSchema, FIELD_TYPES, fieldValidators, fieldFormatters } from '../../../shared_imports'; const { emptyField, isJsonField } = fieldValidators; const { toInt } = fieldFormatters; @@ -97,27 +89,26 @@ export const pipelineFormSchema: FormSchema = { label: i18n.translate('xpack.ingestPipelines.form.onFailureFieldLabel', { defaultMessage: 'On-failure processors (optional)', }), - serializer: parseJson, + serializer: value => { + const result = parseJson(value); + // If an empty array was passed, strip out this value entirely. + if (!result.length) { + return undefined; + } + return result; + }, deserializer: stringifyJson, validations: [ { - validator: ({ value }: ValidationFuncArg) => { - if (isJSON(value)) { - const parsedJSON = JSON.parse(value); - if (!parsedJSON.length) { - return { - message: 'At least one on-failure processor must be defined.', - }; - } - } else { - if (!isEmptyString(value)) { - return { - message: i18n.translate('xpack.ingestPipelines.form.onFailureProcessorsJsonError', { - defaultMessage: 'The on-failure processors JSON is not valid.', - }), - }; - } + validator: validationArg => { + if (!validationArg.value) { + return; } + return isJsonField( + i18n.translate('xpack.ingestPipelines.form.onFailureProcessorsJsonError', { + defaultMessage: 'The on-failure processors JSON is not valid.', + }) + )(validationArg); }, }, ], From fc0ed646e2bda534e1b281837990842391c64fae Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 27 Apr 2020 15:26:55 -0400 Subject: [PATCH 14/19] [Ingest pipelines] Simulate pipeline (#64223) --- .../public/application/components/index.ts | 2 - .../components/pipeline_form/index.ts | 2 +- .../pipeline_form/pipeline_form.tsx | 257 +++--------------- .../pipeline_form/pipeline_form_error.tsx | 34 +++ .../pipeline_form/pipeline_form_fields.tsx | 223 +++++++++++++++ .../pipeline_form/pipeline_form_provider.tsx | 18 ++ .../pipeline_test_flyout/index.ts | 7 + .../pipeline_test_flyout.tsx | 203 ++++++++++++++ .../pipeline_test_flyout_provider.tsx | 39 +++ .../pipeline_test_flyout/tabs/index.ts | 11 + .../tabs/pipeline_test_tabs.tsx | 60 ++++ .../pipeline_test_flyout/tabs/schema.ts | 62 +++++ .../tabs/tab_documents.tsx | 137 ++++++++++ .../pipeline_test_flyout/tabs/tab_output.tsx | 106 ++++++++ .../components/pipeline_form/schema.tsx | 21 +- .../pipeline_form/test_config_context.tsx | 57 ++++ .../application/components/section_error.tsx | 23 -- .../public/application/constants/index.ts | 1 + .../public/application/lib/index.ts | 7 + .../public/application/lib/utils.test.ts | 37 +++ .../public/application/lib/utils.ts | 25 ++ .../pipelines_create/pipelines_create.tsx | 3 - .../pipelines_edit/pipelines_edit.tsx | 31 ++- .../sections/pipelines_list/main.tsx | 4 +- .../public/application/services/api.ts | 17 ++ .../application/services/breadcrumbs.ts | 2 +- .../plugins/ingest_pipelines/public/plugin.ts | 2 +- .../ingest_pipelines/public/shared_imports.ts | 5 - .../server/routes/api/index.ts | 2 + .../server/routes/api/simulate.ts | 62 +++++ .../ingest_pipelines/server/routes/index.ts | 2 + .../ingest_pipelines/ingest_pipelines.ts | 42 +++ .../apps/ingest_pipelines/ingest_pipelines.ts | 4 +- 33 files changed, 1215 insertions(+), 293 deletions(-) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_provider.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout_provider.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/pipeline_test_tabs.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_documents.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_output.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/test_config_context.tsx delete mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/section_error.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/lib/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/lib/utils.test.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/lib/utils.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/index.ts index 39a9dc8d89e99..ec92d899fd1cd 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/index.ts @@ -6,6 +6,4 @@ export { PipelineForm } from './pipeline_form'; -export { SectionError } from './section_error'; - export { PipelineRequestFlyoutProvider as PipelineRequestFlyout } from './pipeline_request_flyout_provider'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/index.ts index 21a2ee30a84e1..2b007a25667a1 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { PipelineForm } from './pipeline_form'; +export { PipelineFormProvider as PipelineForm } from './pipeline_form_provider'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx index 10b7c3d4f0931..1d080dfc330ba 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx @@ -3,35 +3,21 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { - EuiButton, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiSwitch, - EuiLink, -} from '@elastic/eui'; +import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { - useForm, - Form, - getUseField, - getFormRow, - Field, - FormConfig, - JsonEditorField, - useKibana, -} from '../../../shared_imports'; +import { useForm, Form, FormConfig } from '../../../shared_imports'; import { Pipeline } from '../../../../common/types'; -import { SectionError, PipelineRequestFlyout } from '../'; +import { PipelineRequestFlyout } from '../'; +import { PipelineTestFlyout } from './pipeline_test_flyout'; +import { PipelineFormFields } from './pipeline_form_fields'; +import { PipelineFormError } from './pipeline_form_error'; import { pipelineFormSchema } from './schema'; -interface Props { +export interface PipelineFormProps { onSave: (pipeline: Pipeline) => void; onCancel: () => void; isSaving: boolean; @@ -40,10 +26,7 @@ interface Props { isEditing?: boolean; } -const UseField = getUseField({ component: Field }); -const FormRow = getFormRow({ titleTag: 'h3' }); - -export const PipelineForm: React.FunctionComponent = ({ +export const PipelineForm: React.FunctionComponent = ({ defaultValue = { name: '', description: '', @@ -57,20 +40,20 @@ export const PipelineForm: React.FunctionComponent = ({ isEditing, onCancel, }) => { - const { services } = useKibana(); - - const [isVersionVisible, setIsVersionVisible] = useState(Boolean(defaultValue.version)); - const [isOnFailureEditorVisible, setIsOnFailureEditorVisible] = useState( - Boolean(defaultValue.on_failure) - ); const [isRequestVisible, setIsRequestVisible] = useState(false); + const [isTestingPipeline, setIsTestingPipeline] = useState(false); + const handleSave: FormConfig['onSubmit'] = (formData, isValid) => { if (isValid) { onSave(formData as Pipeline); } }; + const handleTestPipelineClick = () => { + setIsTestingPipeline(true); + }; + const { form } = useForm({ schema: pipelineFormSchema, defaultValue, @@ -102,198 +85,19 @@ export const PipelineForm: React.FunctionComponent = ({ isInvalid={form.isSubmitted && !form.isValid} error={form.getErrors()} > - {/* Name field with optional version field */} - - } - description={ - <> - - - - } - checked={isVersionVisible} - onChange={e => setIsVersionVisible(e.target.checked)} - data-test-subj="versionToggle" - /> - - } - > - - - {isVersionVisible && ( - - )} - - - {/* Description */} - - } - description={ - - } - > - - - - {/* Processors field */} - - } - description={ - - {i18n.translate('xpack.ingestPipelines.form.processorsDocumentionLink', { - defaultMessage: 'Learn more.', - })} - - ), - }} - /> - } - > - - - - {/* On-failure field */} - - } - description={ - <> - - {i18n.translate('xpack.ingestPipelines.form.onFailureDocumentionLink', { - defaultMessage: 'Learn more.', - })} - - ), - }} - /> - - - } - checked={isOnFailureEditorVisible} - onChange={e => setIsOnFailureEditorVisible(e.target.checked)} - data-test-subj="onFailureToggle" - /> - - } - > - {isOnFailureEditorVisible ? ( - - ) : ( - // requires children or a field - // For now, we return an empty
if the editor is not visible -
- )} - - {/* Request error */} - {saveError ? ( - <> - - } - error={saveError} - data-test-subj="savePipelineError" - /> - - - ) : null} + {saveError && } + + {/* All form fields */} + {/* Form submission */} @@ -340,11 +144,22 @@ export const PipelineForm: React.FunctionComponent = ({ + + {/* ES request flyout */} {isRequestVisible ? ( setIsRequestVisible(prevIsRequestVisible => !prevIsRequestVisible)} /> ) : null} + + {/* Test pipeline flyout */} + {isTestingPipeline ? ( + { + setIsTestingPipeline(prevIsTestingPipeline => !prevIsTestingPipeline); + }} + /> + ) : null} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error.tsx new file mode 100644 index 0000000000000..ef0e2737df24d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSpacer, EuiCallOut } from '@elastic/eui'; + +interface Props { + errorMessage: string; +} + +export const PipelineFormError: React.FunctionComponent = ({ errorMessage }) => { + return ( + <> + + } + color="danger" + iconType="alert" + data-test-subj="savePipelineError" + > +

{errorMessage}

+
+ + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx new file mode 100644 index 0000000000000..045afd52204fa --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx @@ -0,0 +1,223 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiSpacer, EuiSwitch, EuiLink } from '@elastic/eui'; + +import { + getUseField, + getFormRow, + Field, + JsonEditorField, + useKibana, +} from '../../../shared_imports'; + +interface Props { + hasVersion: boolean; + hasOnFailure: boolean; + isTestButtonDisabled: boolean; + onTestPipelineClick: () => void; + isEditing?: boolean; +} + +const UseField = getUseField({ component: Field }); +const FormRow = getFormRow({ titleTag: 'h3' }); + +export const PipelineFormFields: React.FunctionComponent = ({ + isEditing, + hasVersion, + hasOnFailure, + isTestButtonDisabled, + onTestPipelineClick, +}) => { + const { services } = useKibana(); + + const [isVersionVisible, setIsVersionVisible] = useState(hasVersion); + const [isOnFailureEditorVisible, setIsOnFailureEditorVisible] = useState(hasOnFailure); + + return ( + <> + {/* Name field with optional version field */} + } + description={ + <> + + + + } + checked={isVersionVisible} + onChange={e => setIsVersionVisible(e.target.checked)} + data-test-subj="versionToggle" + /> + + } + > + + + {isVersionVisible && ( + + )} + + + {/* Description field */} + + } + description={ + + } + > + + + + {/* Processors field */} + + } + description={ + <> + + {i18n.translate('xpack.ingestPipelines.form.processorsDocumentionLink', { + defaultMessage: 'Learn more.', + })} + + ), + }} + /> + + + + + + + + } + > + + + + {/* On-failure field */} + + } + description={ + <> + + {i18n.translate('xpack.ingestPipelines.form.onFailureDocumentionLink', { + defaultMessage: 'Learn more.', + })} + + ), + }} + /> + + + } + checked={isOnFailureEditorVisible} + onChange={e => setIsOnFailureEditorVisible(e.target.checked)} + data-test-subj="onFailureToggle" + /> + + } + > + {isOnFailureEditorVisible ? ( + + ) : ( + // requires children or a field + // For now, we return an empty
if the editor is not visible +
+ )} + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_provider.tsx new file mode 100644 index 0000000000000..57abea2309aa1 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_provider.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { PipelineForm as PipelineFormUI, PipelineFormProps } from './pipeline_form'; +import { TestConfigContextProvider } from './test_config_context'; + +export const PipelineFormProvider: React.FunctionComponent = passThroughProps => { + return ( + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/index.ts new file mode 100644 index 0000000000000..38bbc43b469a5 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PipelineTestFlyoutProvider as PipelineTestFlyout } from './pipeline_test_flyout_provider'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx new file mode 100644 index 0000000000000..c0f4b4a7a0aed --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx @@ -0,0 +1,203 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useEffect, useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiSpacer, + EuiTitle, + EuiCallOut, +} from '@elastic/eui'; + +import { useKibana } from '../../../../shared_imports'; +import { Pipeline } from '../../../../../common/types'; +import { Tabs, Tab, OutputTab, DocumentsTab } from './tabs'; +import { useTestConfigContext } from '../test_config_context'; + +export interface PipelineTestFlyoutProps { + closeFlyout: () => void; + pipeline: Pipeline; + isPipelineValid: boolean; +} + +export const PipelineTestFlyout: React.FunctionComponent = ({ + closeFlyout, + pipeline, + isPipelineValid, +}) => { + const { services } = useKibana(); + + const { testConfig } = useTestConfigContext(); + const { documents: cachedDocuments, verbose: cachedVerbose } = testConfig; + + const initialSelectedTab = cachedDocuments ? 'output' : 'documents'; + const [selectedTab, setSelectedTab] = useState(initialSelectedTab); + + const [shouldExecuteImmediately, setShouldExecuteImmediately] = useState(false); + const [isExecuting, setIsExecuting] = useState(false); + const [executeError, setExecuteError] = useState(null); + const [executeOutput, setExecuteOutput] = useState(undefined); + + const handleExecute = useCallback( + async (documents: object[], verbose?: boolean) => { + const { name: pipelineName, ...pipelineDefinition } = pipeline; + + setIsExecuting(true); + setExecuteError(null); + + const { error, data: output } = await services.api.simulatePipeline({ + documents, + verbose, + pipeline: pipelineDefinition, + }); + + setIsExecuting(false); + + if (error) { + setExecuteError(error); + return; + } + + setExecuteOutput(output); + + services.notifications.toasts.addSuccess( + i18n.translate('xpack.ingestPipelines.testPipelineFlyout.successNotificationText', { + defaultMessage: 'Pipeline executed', + }), + { + toastLifeTimeMs: 1000, + } + ); + + setSelectedTab('output'); + }, + [pipeline, services.api, services.notifications.toasts] + ); + + useEffect(() => { + if (cachedDocuments) { + setShouldExecuteImmediately(true); + } + // We only want to know on initial mount if there are cached documents + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + // If the user has already tested the pipeline once, + // use the cached test config and automatically execute the pipeline + if (shouldExecuteImmediately && Object.entries(pipeline).length > 0) { + setShouldExecuteImmediately(false); + handleExecute(cachedDocuments!, cachedVerbose); + } + }, [ + pipeline, + handleExecute, + cachedDocuments, + cachedVerbose, + isExecuting, + shouldExecuteImmediately, + ]); + + let tabContent; + + if (selectedTab === 'output') { + tabContent = ( + + ); + } else { + // default to "documents" tab + tabContent = ( + + ); + } + + return ( + + + +

+ {pipeline.name ? ( + + ) : ( + + )} +

+
+
+ + + !executeOutput && tabId === 'output'} + /> + + + + {/* Execute error */} + {executeError ? ( + <> + + } + color="danger" + iconType="alert" + > +

{executeError.message}

+
+ + + ) : null} + + {/* Invalid pipeline error */} + {!isPipelineValid ? ( + <> + + } + color="danger" + iconType="alert" + /> + + + ) : null} + + {/* Documents or output tab content */} + {tabContent} +
+
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout_provider.tsx new file mode 100644 index 0000000000000..351478394595a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout_provider.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useEffect } from 'react'; + +import { Pipeline } from '../../../../../common/types'; +import { useFormContext } from '../../../../shared_imports'; +import { PipelineTestFlyout, PipelineTestFlyoutProps } from './pipeline_test_flyout'; + +type Props = Omit; + +export const PipelineTestFlyoutProvider: React.FunctionComponent = ({ closeFlyout }) => { + const form = useFormContext(); + const [formData, setFormData] = useState({} as Pipeline); + const [isFormDataValid, setIsFormDataValid] = useState(false); + + useEffect(() => { + const subscription = form.subscribe(async ({ isValid, validate, data }) => { + const isFormValid = isValid ?? (await validate()); + if (isFormValid) { + setFormData(data.format() as Pipeline); + } + setIsFormDataValid(isFormValid); + }); + + return subscription.unsubscribe; + }, [form]); + + return ( + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/index.ts new file mode 100644 index 0000000000000..ea8fe2cd92350 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { Tabs, Tab } from './pipeline_test_tabs'; + +export { DocumentsTab } from './tab_documents'; + +export { OutputTab } from './tab_output'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/pipeline_test_tabs.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/pipeline_test_tabs.tsx new file mode 100644 index 0000000000000..f720b80122702 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/pipeline_test_tabs.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiTab, EuiTabs } from '@elastic/eui'; + +export type Tab = 'documents' | 'output'; + +interface Props { + onTabChange: (tab: Tab) => void; + selectedTab: Tab; + getIsDisabled: (tab: Tab) => boolean; +} + +export const Tabs: React.FunctionComponent = ({ + onTabChange, + selectedTab, + getIsDisabled, +}) => { + const tabs: Array<{ + id: Tab; + name: React.ReactNode; + }> = [ + { + id: 'documents', + name: ( + + ), + }, + { + id: 'output', + name: ( + + ), + }, + ]; + + return ( + + {tabs.map(tab => ( + onTabChange(tab.id)} + isSelected={tab.id === selectedTab} + key={tab.id} + disabled={getIsDisabled(tab.id)} + data-test-subj={tab.id.toLowerCase() + '_tab'} + > + {tab.name} + + ))} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.ts new file mode 100644 index 0000000000000..21a03a3076248 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; + +import { FormSchema, fieldValidators, ValidationFuncArg } from '../../../../../shared_imports'; +import { parseJson, stringifyJson } from '../../../../lib'; + +const { emptyField, isJsonField } = fieldValidators; + +export const documentsSchema: FormSchema = { + documents: { + label: i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsFieldLabel', + { + defaultMessage: 'Documents', + } + ), + serializer: parseJson, + deserializer: stringifyJson, + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsForm.noDocumentsError', + { + defaultMessage: 'Documents are required.', + } + ) + ), + }, + { + validator: isJsonField( + i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsJsonError', + { + defaultMessage: 'The documents JSON is not valid.', + } + ) + ), + }, + { + validator: ({ value }: ValidationFuncArg) => { + const parsedJSON = JSON.parse(value); + + if (!parsedJSON.length) { + return { + message: i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsForm.oneDocumentRequiredError', + { + defaultMessage: 'At least one document is required.', + } + ), + }; + } + }, + }, + ], + }, +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_documents.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_documents.tsx new file mode 100644 index 0000000000000..79d2031ffa91b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_documents.tsx @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { EuiSpacer, EuiText, EuiButton, EuiHorizontalRule } from '@elastic/eui'; + +import { + getUseField, + Field, + JsonEditorField, + Form, + useForm, + FormConfig, +} from '../../../../../shared_imports'; + +import { documentsSchema } from './schema'; +import { useTestConfigContext, TestConfig } from '../../test_config_context'; + +const UseField = getUseField({ component: Field }); + +interface Props { + handleExecute: (documents: object[], verbose: boolean) => void; + isPipelineValid: boolean; + isExecuting: boolean; +} + +export const DocumentsTab: React.FunctionComponent = ({ + isPipelineValid, + handleExecute, + isExecuting, +}) => { + const { setCurrentTestConfig, testConfig } = useTestConfigContext(); + const { verbose: cachedVerbose, documents: cachedDocuments } = testConfig; + + const executePipeline: FormConfig['onSubmit'] = (formData, isValid) => { + if (!isValid || !isPipelineValid) { + return; + } + + const { documents } = formData as TestConfig; + + // Update context + setCurrentTestConfig({ + ...testConfig, + documents, + }); + + handleExecute(documents!, cachedVerbose); + }; + + const { form } = useForm({ + schema: documentsSchema, + defaultValue: { + documents: cachedDocuments || '', + verbose: cachedVerbose || false, + }, + onSubmit: executePipeline, + }); + + return ( + <> + +

+ +

+
+ + + +
+ {/* Documents editor */} + + + + + +

+ +

+
+ + + + + {isExecuting ? ( + + ) : ( + + )} + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_output.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_output.tsx new file mode 100644 index 0000000000000..aa80f8c86ad8b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_output.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiCodeBlock, + EuiSpacer, + EuiText, + EuiSwitch, + EuiLink, + EuiIcon, + EuiLoadingSpinner, + EuiIconTip, +} from '@elastic/eui'; +import { useTestConfigContext } from '../../test_config_context'; + +interface Props { + executeOutput?: { docs: object[] }; + handleExecute: (documents: object[], verbose: boolean) => void; + isExecuting: boolean; +} + +export const OutputTab: React.FunctionComponent = ({ + executeOutput, + handleExecute, + isExecuting, +}) => { + const { setCurrentTestConfig, testConfig } = useTestConfigContext(); + const { verbose: cachedVerbose, documents: cachedDocuments } = testConfig; + + const onEnableVerbose = (isVerboseEnabled: boolean) => { + setCurrentTestConfig({ + ...testConfig, + verbose: isVerboseEnabled, + }); + + handleExecute(cachedDocuments!, isVerboseEnabled); + }; + + let content: React.ReactNode | undefined; + + if (isExecuting) { + content = ; + } else if (executeOutput) { + content = ( + + {JSON.stringify(executeOutput, null, 2)} + + ); + } + + return ( + <> + +

+ handleExecute(cachedDocuments!, cachedVerbose)}> + {' '} + + + ), + }} + /> +

+
+ + + + + {' '} + + } + /> + + } + checked={cachedVerbose} + onChange={e => onEnableVerbose(e.target.checked)} + /> + + + + {content} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx index e1809069ac11c..d449e1af5f8c8 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx @@ -7,30 +7,11 @@ import { i18n } from '@kbn/i18n'; import { FormSchema, FIELD_TYPES, fieldValidators, fieldFormatters } from '../../../shared_imports'; +import { parseJson, stringifyJson } from '../../lib'; const { emptyField, isJsonField } = fieldValidators; const { toInt } = fieldFormatters; -const stringifyJson = (json: { [key: string]: unknown }): string => - Array.isArray(json) ? JSON.stringify(json, null, 2) : '[\n\n]'; - -const parseJson = (jsonString: string): object[] => { - let parsedJSON: any; - - try { - parsedJSON = JSON.parse(jsonString); - - if (!Array.isArray(parsedJSON)) { - // Convert object to array - parsedJSON = [parsedJSON]; - } - } catch { - parsedJSON = []; - } - - return parsedJSON; -}; - export const pipelineFormSchema: FormSchema = { name: { type: FIELD_TYPES.TEXT, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/test_config_context.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/test_config_context.tsx new file mode 100644 index 0000000000000..6840ebef28796 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/test_config_context.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useCallback, useContext } from 'react'; + +export interface TestConfig { + documents?: object[] | undefined; + verbose: boolean; +} + +interface TestConfigContext { + testConfig: TestConfig; + setCurrentTestConfig: (config: TestConfig) => void; +} + +const TEST_CONFIG_DEFAULT_VALUE = { + testConfig: { + verbose: false, + }, + setCurrentTestConfig: () => {}, +}; + +const TestConfigContext = React.createContext(TEST_CONFIG_DEFAULT_VALUE); + +export const useTestConfigContext = () => { + const ctx = useContext(TestConfigContext); + if (!ctx) { + throw new Error( + '"useTestConfigContext" can only be called inside of TestConfigContext.Provider!' + ); + } + return ctx; +}; + +export const TestConfigContextProvider = ({ children }: { children: React.ReactNode }) => { + const [testConfig, setTestConfig] = useState({ + verbose: false, + }); + + const setCurrentTestConfig = useCallback((currentTestConfig: TestConfig): void => { + setTestConfig(currentTestConfig); + }, []); + + return ( + + {children} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/section_error.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/section_error.tsx deleted file mode 100644 index f9ae3d588331d..0000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/components/section_error.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiCallOut } from '@elastic/eui'; -import React from 'react'; - -interface Props { - title: React.ReactNode; - error: Error; -} - -export const SectionError: React.FunctionComponent = ({ title, error, ...rest }) => { - const { message } = error; - - return ( - -

{message}

-
- ); -}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/constants/index.ts b/x-pack/plugins/ingest_pipelines/public/application/constants/index.ts index ed4bd0a42d38e..776d44c825670 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/constants/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/constants/index.ts @@ -11,3 +11,4 @@ export const UIM_PIPELINE_CREATE = 'pipeline_create'; export const UIM_PIPELINE_UPDATE = 'pipeline_update'; export const UIM_PIPELINE_DELETE = 'pipeline_delete'; export const UIM_PIPELINE_DELETE_MANY = 'pipeline_delete_many'; +export const UIM_PIPELINE_SIMULATE = 'pipeline_simulate'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/lib/index.ts b/x-pack/plugins/ingest_pipelines/public/application/lib/index.ts new file mode 100644 index 0000000000000..1283033267a50 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/lib/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { stringifyJson, parseJson } from './utils'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/lib/utils.test.ts b/x-pack/plugins/ingest_pipelines/public/application/lib/utils.test.ts new file mode 100644 index 0000000000000..e7eff3bd6ca33 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/lib/utils.test.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { stringifyJson, parseJson } from './utils'; + +describe('utils', () => { + describe('stringifyJson()', () => { + it('should stringify a valid JSON array', () => { + expect(stringifyJson([1, 2, 3])).toEqual(`[ + 1, + 2, + 3 +]`); + }); + + it('should return a stringified empty array if the value is not a valid JSON array', () => { + expect(stringifyJson({})).toEqual('[\n\n]'); + }); + }); + + describe('parseJson()', () => { + it('should parse a valid JSON string', () => { + expect(parseJson('[1,2,3]')).toEqual([1, 2, 3]); + expect(parseJson('[{"foo": "bar"}]')).toEqual([{ foo: 'bar' }]); + }); + + it('should convert valid JSON that is not an array to an array', () => { + expect(parseJson('{"foo": "bar"}')).toEqual([{ foo: 'bar' }]); + }); + + it('should return an empty array if invalid JSON string', () => { + expect(parseJson('{invalidJsonString}')).toEqual([]); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/lib/utils.ts b/x-pack/plugins/ingest_pipelines/public/application/lib/utils.ts new file mode 100644 index 0000000000000..fe4e9e65f4b9a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/lib/utils.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const stringifyJson = (json: any): string => + Array.isArray(json) ? JSON.stringify(json, null, 2) : '[\n\n]'; + +export const parseJson = (jsonString: string): object[] => { + let parsedJSON: any; + + try { + parsedJSON = JSON.parse(jsonString); + + if (!Array.isArray(parsedJSON)) { + // Convert object to array + parsedJSON = [parsedJSON]; + } + } catch { + parsedJSON = []; + } + + return parsedJSON; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx index 2f3e2630adbd1..d601e9f9c245b 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx @@ -9,7 +9,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPageBody, EuiPageContent, - EuiSpacer, EuiTitle, EuiFlexGroup, EuiFlexItem, @@ -94,8 +93,6 @@ export const PipelinesCreate: React.FunctionComponent - - - } - error={error} - data-test-subj="fetchPipelineError" - /> + <> + + } + color="danger" + iconType="alert" + data-test-subj="fetchPipelineError" + > +

{error.message}

+
+ + ); } else if (pipeline) { content = ( diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx index bd0043e3e74af..8af76460b75ae 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx @@ -89,7 +89,7 @@ export const PipelinesList: React.FunctionComponent = ({ hi

@@ -101,7 +101,7 @@ export const PipelinesList: React.FunctionComponent = ({ hi > diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/api.ts b/x-pack/plugins/ingest_pipelines/public/application/services/api.ts index 42a157705baa7..13eb96e78adae 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/services/api.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/services/api.ts @@ -20,6 +20,7 @@ import { UIM_PIPELINE_UPDATE, UIM_PIPELINE_DELETE, UIM_PIPELINE_DELETE_MANY, + UIM_PIPELINE_SIMULATE, } from '../constants'; export class ApiService { @@ -103,6 +104,22 @@ export class ApiService { return result; } + + public async simulatePipeline(testConfig: { + documents: object[]; + verbose?: boolean; + pipeline: Omit; + }) { + const result = await this.sendRequest({ + path: `${API_BASE_PATH}/simulate`, + method: 'post', + body: JSON.stringify(testConfig), + }); + + this.trackUiMetric(UIM_PIPELINE_SIMULATE); + + return result; + } } export const apiService = new ApiService(); diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts b/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts index b6856355ddc27..1ccdbbad9b1bb 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts @@ -10,7 +10,7 @@ import { ManagementAppMountParams } from '../../../../../../src/plugins/manageme type SetBreadcrumbs = ManagementAppMountParams['setBreadcrumbs']; const homeBreadcrumbText = i18n.translate('xpack.ingestPipelines.breadcrumb.pipelinesLabel', { - defaultMessage: 'Ingest Pipelines', + defaultMessage: 'Ingest Node Pipelines', }); export class BreadcrumbService { diff --git a/x-pack/plugins/ingest_pipelines/public/plugin.ts b/x-pack/plugins/ingest_pipelines/public/plugin.ts index 5a166588f6236..e9f5fd6c7f57c 100644 --- a/x-pack/plugins/ingest_pipelines/public/plugin.ts +++ b/x-pack/plugins/ingest_pipelines/public/plugin.ts @@ -23,7 +23,7 @@ export class IngestPipelinesPlugin implements Plugin { management.sections.getSection('elasticsearch')!.registerApp({ id: PLUGIN_ID, title: i18n.translate('xpack.ingestPipelines.appTitle', { - defaultMessage: 'Ingest Pipelines', + defaultMessage: 'Ingest Node Pipelines', }), mount: async params => { const { mountManagementSection } = await import('./application/mount_management_section'); diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts index 5127821f3820c..cfa946ff942ec 100644 --- a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -17,22 +17,17 @@ export { export { FormSchema, FIELD_TYPES, - VALIDATION_TYPES, - FieldConfig, FormConfig, useForm, Form, getUseField, ValidationFuncArg, - FormData, - FormHook, useFormContext, } from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; export { fieldFormatters, fieldValidators, - serializers, } from '../../../../src/plugins/es_ui_shared/static/forms/helpers'; export { diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts index 37819b9bf6889..58a4bf5617659 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts @@ -13,3 +13,5 @@ export { registerUpdateRoute } from './update'; export { registerPrivilegesRoute } from './privileges'; export { registerDeleteRoute } from './delete'; + +export { registerSimulateRoute } from './simulate'; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts new file mode 100644 index 0000000000000..78c29d061fe5a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; + +import { API_BASE_PATH } from '../../../common/constants'; +import { RouteDependencies } from '../../types'; + +const bodySchema = schema.object({ + pipeline: schema.object({ + description: schema.string(), + processors: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), + version: schema.maybe(schema.number()), + on_failure: schema.maybe(schema.arrayOf(schema.recordOf(schema.string(), schema.any()))), + }), + documents: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), + verbose: schema.maybe(schema.boolean()), +}); + +export const registerSimulateRoute = ({ + router, + license, + lib: { isEsError }, +}: RouteDependencies): void => { + router.post( + { + path: `${API_BASE_PATH}/simulate`, + validate: { + body: bodySchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; + const reqBody = req.body; + + const { pipeline, documents, verbose } = reqBody; + + try { + const response = await callAsCurrentUser('ingest.simulate', { + verbose, + body: { + pipeline, + docs: documents, + }, + }); + + return res.ok({ body: response }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/index.ts b/x-pack/plugins/ingest_pipelines/server/routes/index.ts index 8cfcc1054ca4e..f703a460143f4 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/index.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/index.ts @@ -12,6 +12,7 @@ import { registerUpdateRoute, registerPrivilegesRoute, registerDeleteRoute, + registerSimulateRoute, } from './api'; export class ApiRoutes { @@ -21,5 +22,6 @@ export class ApiRoutes { registerUpdateRoute(dependencies); registerPrivilegesRoute(dependencies); registerDeleteRoute(dependencies); + registerSimulateRoute(dependencies); } } diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts index a1773a052ede2..88a78d048a3b6 100644 --- a/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts @@ -284,5 +284,47 @@ export default function({ getService }: FtrProviderContext) { }); }); }); + + describe('Simulate', () => { + it('should successfully simulate a pipeline', async () => { + const { body } = await supertest + .post(`${API_BASE_PATH}/simulate`) + .set('kbn-xsrf', 'xxx') + .send({ + pipeline: { + description: 'test simulate pipeline description', + processors: [ + { + set: { + field: 'field2', + value: '_value', + }, + }, + ], + }, + documents: [ + { + _index: 'index', + _id: 'id', + _source: { + foo: 'bar', + }, + }, + { + _index: 'index', + _id: 'id', + _source: { + foo: 'rab', + }, + }, + ], + }) + .expect(200); + + // The simulate ES response is quite long and includes timestamps + // so for now, we just confirm the docs array is returned with the correct length + expect(body.docs?.length).to.eql(2); + }); + }); }); } diff --git a/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts b/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts index c0a0d8595bf88..1b22f8f35d7ad 100644 --- a/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts +++ b/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts @@ -18,10 +18,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('Loads the app', async () => { - await log.debug('Checking for section heading to say Ingest Pipelines.'); + await log.debug('Checking for section heading to say Ingest Node Pipelines.'); const headingText = await pageObjects.ingestPipelines.sectionHeadingText(); - expect(headingText).to.be('Ingest Pipelines'); + expect(headingText).to.be('Ingest Node Pipelines'); }); }); }; From 6dbdf067a996b92996d291e7b0bb2feeae1d8da8 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 28 Apr 2020 15:36:42 +0200 Subject: [PATCH 15/19] [Ingest Node Pipelines] Show flyout after editing or creating a pipeline (#64409) * Show flyout after editing or creating a pipeline * JSX comment * Show not found flyout Copied from CCR * update not found flyout and fix behavior when viewing details from table * Reset pipeline name in URI when closing flyout * Remove encodeURI Already using encodingURIComponent for unsafe string. Co-authored-by: Alison Goryachev --- .../public/application/app.tsx | 2 + .../pipelines_create/pipelines_create.tsx | 2 +- .../pipelines_edit/pipelines_edit.tsx | 2 +- .../{details.tsx => details_flyout.tsx} | 2 +- .../sections/pipelines_list/main.tsx | 89 ++++++++++++++----- .../pipelines_list/not_found_flyout.tsx | 42 +++++++++ .../sections/pipelines_list/table.tsx | 6 +- 7 files changed, 118 insertions(+), 27 deletions(-) rename x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/{details.tsx => details_flyout.tsx} (98%) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/not_found_flyout.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/app.tsx b/x-pack/plugins/ingest_pipelines/public/application/app.tsx index 2ec72267701d7..ba7675b507596 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/app.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/app.tsx @@ -26,6 +26,8 @@ export const AppWithoutRouter = () => ( + {/* Catch all */} + ); diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx index d601e9f9c245b..203ad3d802452 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx @@ -49,7 +49,7 @@ export const PipelinesCreate: React.FunctionComponent { diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx index 02937b5618143..99cd8d7eef97b 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx @@ -54,7 +54,7 @@ export const PipelinesEdit: React.FunctionComponent { diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_flyout.tsx similarity index 98% rename from x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx rename to x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_flyout.tsx index 07fdb1c15e0bf..98243a5149c0d 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_flyout.tsx @@ -36,7 +36,7 @@ export interface Props { onClose: () => void; } -export const PipelineDetails: FunctionComponent = ({ +export const PipelineDetailsFlyout: FunctionComponent = ({ pipeline, onClose, onEditClick, diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx index 8af76460b75ae..c90ac2714a95a 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx @@ -8,6 +8,8 @@ import React, { useEffect, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { Location } from 'history'; +import { parse } from 'query-string'; import { EuiPageBody, @@ -28,33 +30,58 @@ import { UIM_PIPELINES_LIST_LOAD } from '../../constants'; import { EmptyList } from './empty_list'; import { PipelineTable } from './table'; -import { PipelineDetails } from './details'; +import { PipelineDetailsFlyout } from './details_flyout'; +import { PipelineNotFoundFlyout } from './not_found_flyout'; import { PipelineDeleteModal } from './delete_modal'; -export const PipelinesList: React.FunctionComponent = ({ history }) => { +const getPipelineNameFromLocation = (location: Location) => { + const { pipeline } = parse(location.search.substring(1)); + return pipeline; +}; + +export const PipelinesList: React.FunctionComponent = ({ + history, + location, +}) => { const { services } = useKibana(); + const pipelineNameFromLocation = getPipelineNameFromLocation(location); const [selectedPipeline, setSelectedPipeline] = useState(undefined); + const [showFlyout, setShowFlyout] = useState(false); + const [pipelinesToDelete, setPipelinesToDelete] = useState([]); + const { data, isLoading, error, sendRequest } = services.api.useLoadPipelines(); + // Track component loaded useEffect(() => { services.metric.trackUiMetric(UIM_PIPELINES_LIST_LOAD); services.breadcrumbs.setBreadcrumbs('home'); }, [services.metric, services.breadcrumbs]); - const { data, isLoading, error, sendRequest } = services.api.useLoadPipelines(); + useEffect(() => { + if (pipelineNameFromLocation && data?.length) { + const pipeline = data.find(p => p.name === pipelineNameFromLocation); + setSelectedPipeline(pipeline); + setShowFlyout(true); + } + }, [pipelineNameFromLocation, data]); - let content: React.ReactNode; + const goToEditPipeline = (name: string) => { + history.push(`${BASE_PATH}/edit/${encodeURIComponent(name)}`); + }; - const editPipeline = (name: string) => { - history.push(encodeURI(`${BASE_PATH}/edit/${encodeURIComponent(name)}`)); + const goToClonePipeline = (name: string) => { + history.push(`${BASE_PATH}/create/${encodeURIComponent(name)}`); }; - const clonePipeline = (name: string) => { - history.push(encodeURI(`${BASE_PATH}/create/${encodeURIComponent(name)}`)); + const goHome = () => { + setShowFlyout(false); + history.push(BASE_PATH); }; + let content: React.ReactNode; + if (isLoading) { content = ( @@ -68,10 +95,9 @@ export const PipelinesList: React.FunctionComponent = ({ hi content = ( ); @@ -79,6 +105,37 @@ export const PipelinesList: React.FunctionComponent = ({ hi content = ; } + const renderFlyout = (): React.ReactNode => { + if (!showFlyout) { + return; + } + if (selectedPipeline) { + return ( + { + setSelectedPipeline(undefined); + goHome(); + }} + onEditClick={goToEditPipeline} + onCloneClick={goToClonePipeline} + onDeleteClick={setPipelinesToDelete} + /> + ); + } else { + // Somehow we triggered show pipeline details, but do not have a pipeline. + // We assume not found. + return ( + { + goHome(); + }} + pipelineName={pipelineNameFromLocation} + /> + ); + } + }; + return ( <> @@ -131,15 +188,7 @@ export const PipelinesList: React.FunctionComponent = ({ hi )} - {selectedPipeline && ( - setSelectedPipeline(undefined)} - onEditClick={editPipeline} - onCloneClick={clonePipeline} - onDeleteClick={setPipelinesToDelete} - /> - )} + {renderFlyout()} {pipelinesToDelete?.length > 0 ? ( { diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/not_found_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/not_found_flyout.tsx new file mode 100644 index 0000000000000..b967e54187ced --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/not_found_flyout.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlyout, EuiFlyoutBody, EuiCallOut } from '@elastic/eui'; +import { EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; + +interface Props { + onClose: () => void; + pipelineName: string | string[] | null | undefined; +} + +export const PipelineNotFoundFlyout: FunctionComponent = ({ onClose, pipelineName }) => { + return ( + + + {pipelineName && ( + +

{pipelineName}

+
+ )} +
+ + + + } + color="danger" + iconType="alert" + /> + +
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx index 05488f46c148e..1c938a023fc2c 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx @@ -17,7 +17,6 @@ export interface Props { onEditPipelineClick: (pipelineName: string) => void; onClonePipelineClick: (pipelineName: string) => void; onDeletePipelineClick: (pipelineName: string[]) => void; - onViewPipelineClick: (pipeline: Pipeline) => void; } export const PipelineTable: FunctionComponent = ({ @@ -26,7 +25,6 @@ export const PipelineTable: FunctionComponent = ({ onEditPipelineClick, onClonePipelineClick, onDeletePipelineClick, - onViewPipelineClick, }) => { const [selection, setSelection] = useState([]); @@ -94,8 +92,8 @@ export const PipelineTable: FunctionComponent = ({ defaultMessage: 'Name', }), sortable: true, - render: (name: string, pipeline) => ( - onViewPipelineClick(pipeline)}>{name} + render: (name: string) => ( + {name} ), }, { From 564341982c97ade68f4d7691c702f528fd1c1afc Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Wed, 29 Apr 2020 08:49:35 -0400 Subject: [PATCH 16/19] fix TS --- .../sections/pipelines_list/table.tsx | 231 +++++++++--------- 1 file changed, 115 insertions(+), 116 deletions(-) diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx index 1c938a023fc2c..c93285289ff39 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx @@ -6,7 +6,7 @@ import React, { FunctionComponent, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiInMemoryTable, EuiLink, EuiButton } from '@elastic/eui'; +import { EuiInMemoryTable, EuiLink, EuiButton, EuiInMemoryTableProps } from '@elastic/eui'; import { BASE_PATH } from '../../../../common/constants'; import { Pipeline } from '../../../../common/types'; @@ -28,122 +28,121 @@ export const PipelineTable: FunctionComponent = ({ }) => { const [selection, setSelection] = useState([]); - return ( - 0 ? ( - onDeletePipelineClick(selection.map(pipeline => pipeline.name))} - color="danger" - > - - - ) : ( - undefined - ), - toolsRight: [ + const tableProps: EuiInMemoryTableProps = { + itemId: 'name', + isSelectable: true, + sorting: { sort: { field: 'name', direction: 'asc' } }, + selection: { + onSelectionChange: setSelection, + }, + search: { + toolsLeft: + selection.length > 0 ? ( onDeletePipelineClick(selection.map(pipeline => pipeline.name))} + color="danger" > - {i18n.translate('xpack.ingestPipelines.list.table.reloadButtonLabel', { - defaultMessage: 'Reload', - })} - , - - {i18n.translate('xpack.ingestPipelines.list.table.createPipelineButtonLabel', { - defaultMessage: 'Create a pipeline', - })} - , + + + ) : ( + undefined + ), + toolsRight: [ + + {i18n.translate('xpack.ingestPipelines.list.table.reloadButtonLabel', { + defaultMessage: 'Reload', + })} + , + + {i18n.translate('xpack.ingestPipelines.list.table.createPipelineButtonLabel', { + defaultMessage: 'Create a pipeline', + })} + , + ], + box: { + incremental: true, + }, + }, + pagination: { + initialPageSize: 10, + pageSizeOptions: [10, 20, 50], + }, + columns: [ + { + field: 'name', + name: i18n.translate('xpack.ingestPipelines.list.table.nameColumnTitle', { + defaultMessage: 'Name', + }), + sortable: true, + render: (name: string) => {name}, + }, + { + name: ( + + ), + actions: [ + { + isPrimary: true, + name: i18n.translate('xpack.ingestPipelines.list.table.editActionLabel', { + defaultMessage: 'Edit', + }), + description: i18n.translate('xpack.ingestPipelines.list.table.editActionDescription', { + defaultMessage: 'Edit this pipeline', + }), + type: 'icon', + icon: 'pencil', + onClick: ({ name }) => onEditPipelineClick(name), + }, + { + name: i18n.translate('xpack.ingestPipelines.list.table.cloneActionLabel', { + defaultMessage: 'Clone', + }), + description: i18n.translate('xpack.ingestPipelines.list.table.cloneActionDescription', { + defaultMessage: 'Clone this pipeline', + }), + type: 'icon', + icon: 'copy', + onClick: ({ name }) => onClonePipelineClick(name), + }, + { + isPrimary: true, + name: i18n.translate('xpack.ingestPipelines.list.table.deleteActionLabel', { + defaultMessage: 'Delete', + }), + description: i18n.translate( + 'xpack.ingestPipelines.list.table.deleteActionDescription', + { defaultMessage: 'Delete this pipeline' } + ), + type: 'icon', + icon: 'trash', + color: 'danger', + onClick: ({ name }) => onDeletePipelineClick([name]), + }, ], - box: { - incremental: true, - }, - }} - pagination={{ - initialPageSize: 10, - pageSizeOptions: [10, 20, 50], - }} - columns={[ - { - field: 'name', - name: i18n.translate('xpack.ingestPipelines.list.table.nameColumnTitle', { - defaultMessage: 'Name', - }), - sortable: true, - render: (name: string) => ( - {name} - ), - }, - { - name: i18n.translate('xpack.ingestPipelines.list.table.actionColumnTitle', { - defaultMessage: 'Actions', - }), - actions: [ - { - isPrimary: true, - name: i18n.translate('xpack.ingestPipelines.list.table.editActionLabel', { - defaultMessage: 'Edit', - }), - description: i18n.translate( - 'xpack.ingestPipelines.list.table.editActionDescription', - { defaultMessage: 'Edit this pipeline' } - ), - type: 'icon', - icon: 'pencil', - onClick: ({ name }) => onEditPipelineClick(name), - }, - { - name: i18n.translate('xpack.ingestPipelines.list.table.cloneActionLabel', { - defaultMessage: 'Clone', - }), - description: i18n.translate( - 'xpack.ingestPipelines.list.table.cloneActionDescription', - { defaultMessage: 'Clone this pipeline' } - ), - type: 'icon', - icon: 'copy', - onClick: ({ name }) => onClonePipelineClick(name), - }, - { - isPrimary: true, - name: i18n.translate('xpack.ingestPipelines.list.table.deleteActionLabel', { - defaultMessage: 'Delete', - }), - description: i18n.translate( - 'xpack.ingestPipelines.list.table.deleteActionDescription', - { defaultMessage: 'Delete this pipeline' } - ), - type: 'icon', - icon: 'trash', - color: 'danger', - onClick: ({ name }) => onDeletePipelineClick([name]), - }, - ], - }, - ]} - items={pipelines ?? []} - /> - ); + }, + ], + items: pipelines ?? [], + }; + + return ; }; From a4c298d78eb5d6410d79b2aa1f6284a5ee6a7c22 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 30 Apr 2020 08:22:25 -0400 Subject: [PATCH 17/19] [Ingest pipelines] Cleanup (#64794) --- x-pack/plugins/ingest_pipelines/README.md | 21 +++++++-- .../public/application/components/index.ts | 2 - .../pipeline_form/pipeline_form.tsx | 10 ++--- .../pipeline_request_flyout/index.ts | 7 +++ .../pipeline_request_flyout.tsx | 4 +- .../pipeline_request_flyout_provider.tsx | 4 +- .../pipeline_test_flyout.tsx | 2 +- .../tabs/{schema.ts => schema.tsx} | 25 +++++++++++ .../tabs/tab_documents.tsx | 19 +++++++- .../components/pipeline_form/schema.tsx | 43 +++++++++++++++++++ .../pipelines_create/pipelines_create.tsx | 3 ++ .../application/services/documentation.ts | 4 ++ .../ingest_pipelines/public/index.scss | 0 .../plugins/ingest_pipelines/public/index.ts | 2 - .../ingest_pipelines/server/routes/api/get.ts | 6 +++ 15 files changed, 132 insertions(+), 20 deletions(-) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/index.ts rename x-pack/plugins/ingest_pipelines/public/application/components/{ => pipeline_form/pipeline_request_flyout}/pipeline_request_flyout.tsx (95%) rename x-pack/plugins/ingest_pipelines/public/application/components/{ => pipeline_form/pipeline_request_flyout}/pipeline_request_flyout_provider.tsx (89%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/{schema.ts => schema.tsx} (74%) delete mode 100644 x-pack/plugins/ingest_pipelines/public/index.scss diff --git a/x-pack/plugins/ingest_pipelines/README.md b/x-pack/plugins/ingest_pipelines/README.md index 030bba760499d..a469511bdbbd2 100644 --- a/x-pack/plugins/ingest_pipelines/README.md +++ b/x-pack/plugins/ingest_pipelines/README.md @@ -1,9 +1,24 @@ -# ingest_pipelines +# Ingest Node Pipelines UI -> Ingest node pipelines UI +## Summary +The `ingest_pipelines` plugin provides Kibana support for [Elasticsearch's ingest nodes](https://www.elastic.co/guide/en/elasticsearch/reference/master/ingest.html). Please refer to the Elasticsearch documentation for more details. + +This plugin allows Kibana to create, edit, clone and delete ingest node pipelines. It also provides support to simulate a pipeline. + +It requires a Basic license and the following cluster privileges: `manage_pipeline` and `cluster:monitor/nodes/info`. --- ## Development -See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. +A new app called Ingest Node Pipelines is registered in the Management section and follows a typical CRUD UI pattern. The client-side portion of this app lives in [public/application](public/application) and uses endpoints registered in [server/routes/api](server/routes/api). + +See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions on setting up your development environment. + +### Test coverage + +The app has the following test coverage: + +- Complete API integration tests +- Smoke-level functional test +- Client-integration tests diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/index.ts index ec92d899fd1cd..21a2ee30a84e1 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/index.ts @@ -5,5 +5,3 @@ */ export { PipelineForm } from './pipeline_form'; - -export { PipelineRequestFlyoutProvider as PipelineRequestFlyout } from './pipeline_request_flyout_provider'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx index 1d080dfc330ba..9082196a48b39 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx @@ -11,7 +11,7 @@ import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from import { useForm, Form, FormConfig } from '../../../shared_imports'; import { Pipeline } from '../../../../common/types'; -import { PipelineRequestFlyout } from '../'; +import { PipelineRequestFlyout } from './pipeline_request_flyout'; import { PipelineTestFlyout } from './pipeline_test_flyout'; import { PipelineFormFields } from './pipeline_form_fields'; import { PipelineFormError } from './pipeline_form_error'; @@ -85,8 +85,6 @@ export const PipelineForm: React.FunctionComponent = ({ isInvalid={form.isSubmitted && !form.isValid} error={form.getErrors()} > - - {/* Request error */} {saveError && } @@ -101,9 +99,9 @@ export const PipelineForm: React.FunctionComponent = ({ {/* Form submission */} - + - + = ({ {saveButtonLabel} - + = ({ uuid.current++; return ( - +

diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_request_flyout_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout_provider.tsx similarity index 89% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_request_flyout_provider.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout_provider.tsx index 8f8d89772c964..6dcedca6085af 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_request_flyout_provider.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout_provider.tsx @@ -6,8 +6,8 @@ import React, { useState, useEffect } from 'react'; -import { Pipeline } from '../../../common/types'; -import { useFormContext } from '../../shared_imports'; +import { Pipeline } from '../../../../../common/types'; +import { useFormContext } from '../../../../shared_imports'; import { PipelineRequestFlyout } from './pipeline_request_flyout'; export const PipelineRequestFlyoutProvider = ({ closeFlyout }: { closeFlyout: () => void }) => { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx index c0f4b4a7a0aed..16f39b2912c1d 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx @@ -128,7 +128,7 @@ export const PipelineTestFlyout: React.FunctionComponent +

diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.tsx similarity index 74% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.tsx index 21a03a3076248..de9910344bd4b 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.tsx @@ -3,7 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; + +import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { EuiCode } from '@elastic/eui'; import { FormSchema, fieldValidators, ValidationFuncArg } from '../../../../../shared_imports'; import { parseJson, stringifyJson } from '../../../../lib'; @@ -18,6 +22,27 @@ export const documentsSchema: FormSchema = { defaultMessage: 'Documents', } ), + helpText: ( + + {JSON.stringify([ + { + _index: 'index', + _id: 'id', + _source: { + foo: 'bar', + }, + }, + ])} + + ), + }} + /> + ), serializer: parseJson, deserializer: stringifyJson, validations: [ diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_documents.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_documents.tsx index 79d2031ffa91b..97bf03dbdc068 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_documents.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_documents.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EuiSpacer, EuiText, EuiButton, EuiHorizontalRule } from '@elastic/eui'; +import { EuiSpacer, EuiText, EuiButton, EuiHorizontalRule, EuiLink } from '@elastic/eui'; import { getUseField, @@ -17,6 +17,7 @@ import { Form, useForm, FormConfig, + useKibana, } from '../../../../../shared_imports'; import { documentsSchema } from './schema'; @@ -35,6 +36,8 @@ export const DocumentsTab: React.FunctionComponent = ({ handleExecute, isExecuting, }) => { + const { services } = useKibana(); + const { setCurrentTestConfig, testConfig } = useTestConfigContext(); const { verbose: cachedVerbose, documents: cachedDocuments } = testConfig; @@ -69,7 +72,19 @@ export const DocumentsTab: React.FunctionComponent = ({

+ {i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsTab.simulateDocumentionLink', + { + defaultMessage: 'Learn more.', + } + )} + + ), + }} />

diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx index d449e1af5f8c8..2e2689f41527a 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx @@ -3,8 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCode } from '@elastic/eui'; import { FormSchema, FIELD_TYPES, fieldValidators, fieldFormatters } from '../../../shared_imports'; import { parseJson, stringifyJson } from '../../lib'; @@ -47,6 +50,26 @@ export const pipelineFormSchema: FormSchema = { label: i18n.translate('xpack.ingestPipelines.form.processorsFieldLabel', { defaultMessage: 'Processors', }), + helpText: ( + + {JSON.stringify([ + { + set: { + field: 'foo', + value: 'bar', + }, + }, + ])} + + ), + }} + /> + ), serializer: parseJson, deserializer: stringifyJson, validations: [ @@ -70,6 +93,26 @@ export const pipelineFormSchema: FormSchema = { label: i18n.translate('xpack.ingestPipelines.form.onFailureFieldLabel', { defaultMessage: 'On-failure processors (optional)', }), + helpText: ( + + {JSON.stringify([ + { + set: { + field: '_index', + value: 'failed-{{ _index }}', + }, + }, + ])} + + ), + }} + /> + ), serializer: value => { const result = parseJson(value); // If an empty array was passed, strip out this value entirely. diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx index 203ad3d802452..34a362d596d92 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx @@ -13,6 +13,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, + EuiSpacer, } from '@elastic/eui'; import { BASE_PATH } from '../../../../common/constants'; @@ -93,6 +94,8 @@ export const PipelinesCreate: React.FunctionComponent + + Date: Thu, 30 Apr 2020 09:15:59 -0400 Subject: [PATCH 18/19] address review feedback --- x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts | 3 +-- x-pack/plugins/ingest_pipelines/server/routes/api/update.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts index 78c29d061fe5a..ca5fc78d118fd 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts @@ -33,9 +33,8 @@ export const registerSimulateRoute = ({ }, license.guardApiRoute(async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; - const reqBody = req.body; - const { pipeline, documents, verbose } = reqBody; + const { pipeline, documents, verbose } = req.body; try { const response = await callAsCurrentUser('ingest.simulate', { diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts index 27a3c9fb97ef8..dd9d818d0ead7 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts @@ -36,9 +36,8 @@ export const registerUpdateRoute = ({ license.guardApiRoute(async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; const { name } = req.params; - const pipeline = req.body as Pipeline; - const { description, processors, version, on_failure } = pipeline; + const { description, processors, version, on_failure } = req.body; try { // Verify pipeline exists; ES will throw 404 if it doesn't From dd93b1104d16157c699c2fd53eda025bf1a1d4d4 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 30 Apr 2020 10:32:56 -0400 Subject: [PATCH 19/19] remove unused import --- x-pack/plugins/ingest_pipelines/server/routes/api/update.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts index dd9d818d0ead7..a6fdee47f0ecf 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts @@ -5,7 +5,6 @@ */ import { schema } from '@kbn/config-schema'; -import { Pipeline } from '../../../common/types'; import { API_BASE_PATH } from '../../../common/constants'; import { RouteDependencies } from '../../types'; @@ -36,7 +35,6 @@ export const registerUpdateRoute = ({ license.guardApiRoute(async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; const { name } = req.params; - const { description, processors, version, on_failure } = req.body; try {