From 0673dbde1f6f582599c46db7cb5718c24f725181 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 6 Jul 2020 10:23:20 -0400 Subject: [PATCH 01/46] Feature Controls: addressing bugs for enterprise search (#70538) Co-authored-by: Elastic Machine --- .../feature_table/feature_table.test.tsx | 39 +++++++++++++++++++ .../kibana/feature_table/feature_table.tsx | 4 +- .../disable_ui_capabilities.test.ts | 4 +- .../authorization/disable_ui_capabilities.ts | 6 ++- .../capabilities_switcher.test.ts | 4 +- .../capabilities/capabilities_switcher.ts | 6 +++ 6 files changed, 59 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx index 6bc829f766e58..2a0922d614f1d 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx @@ -846,4 +846,43 @@ describe('FeatureTable', () => { }, }); }); + + it('does not render features which lack privileges', () => { + const role = createRole([ + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ]); + + const featureWithoutPrivileges = createFeature({ + id: 'no_privs', + name: 'No Privileges Feature', + privileges: null, + }); + + const { displayedPrivileges } = setup({ + role, + features: [...kibanaFeatures, featureWithoutPrivileges], + privilegeIndex: 0, + calculateDisplayedPrivileges: true, + canCustomizeSubFeaturePrivileges: false, + }); + + expect(displayedPrivileges).toEqual({ + excluded_from_base: { + primaryFeaturePrivilege: 'none', + }, + no_sub_features: { + primaryFeaturePrivilege: 'none', + }, + with_excluded_sub_features: { + primaryFeaturePrivilege: 'none', + }, + with_sub_features: { + primaryFeaturePrivilege: 'none', + }, + }); + }); }); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx index a371a9ec9ba1e..57e24f2838226 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx @@ -63,7 +63,9 @@ export class FeatureTable extends Component { public render() { const { role, kibanaPrivileges } = this.props; - const featurePrivileges = kibanaPrivileges.getSecuredFeatures(); + const featurePrivileges = kibanaPrivileges + .getSecuredFeatures() + .filter((feature) => feature.privileges != null || feature.reserved != null); const items: TableRow[] = featurePrivileges .sort((feature1, feature2) => { diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts index a1bedea9f7deb..45f55b34baf96 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts @@ -50,7 +50,7 @@ describe('usingPrivileges', () => { new Feature({ id: 'fooFeature', name: 'Foo Feature', - app: [], + app: ['fooApp'], navLinkId: 'foo', privileges: null, }), @@ -63,6 +63,7 @@ describe('usingPrivileges', () => { Object.freeze({ navLinks: { foo: true, + fooApp: true, bar: true, }, management: { @@ -85,6 +86,7 @@ describe('usingPrivileges', () => { expect(result).toEqual({ navLinks: { foo: false, + fooApp: false, bar: true, }, management: { diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts index 183ad9169a123..a9b3fa54d3617 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts @@ -18,8 +18,12 @@ export function disableUICapabilitiesFactory( logger: Logger, authz: AuthorizationServiceSetup ) { + // nav links are sourced from two places: + // 1) The `navLinkId` property. This is deprecated and will be removed (https://github.com/elastic/kibana/issues/66217) + // 2) The apps property. The Kibana Platform associates nav links to the app which registers it, in a 1:1 relationship. + // This behavior is replacing the `navLinkId` property above. const featureNavLinkIds = features - .map((feature) => feature.navLinkId) + .flatMap((feature) => [feature.navLinkId, ...feature.app]) .filter((navLinkId) => navLinkId != null); const shouldDisableFeatureUICapability = ( diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts index 1e01e04332f43..babd25dd3ec4b 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts @@ -43,7 +43,7 @@ const features = ([ id: 'feature_3', name: 'Feature 3', navLinkId: 'feature3', - app: [], + app: ['feature3_app'], catalogue: ['feature3Entry'], management: { kibana: ['indices'], @@ -67,6 +67,7 @@ const buildCapabilities = () => feature1: true, feature2: true, feature3: true, + feature3_app: true, unknownFeature: true, }, catalogue: { @@ -241,6 +242,7 @@ describe('capabilitiesSwitcher', () => { expectedCapabilities.feature_2.foo = false; expectedCapabilities.navLinks.feature3 = false; + expectedCapabilities.navLinks.feature3_app = false; expectedCapabilities.catalogue.feature3Entry = false; expectedCapabilities.management.kibana.indices = false; expectedCapabilities.feature_3.bar = false; diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts index a0cdd5ad0e931..05d0429596489 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts @@ -68,6 +68,12 @@ function toggleDisabledFeatures( navLinks[feature.navLinkId] = false; } + feature.app.forEach((app) => { + if (navLinks.hasOwnProperty(app)) { + navLinks[app] = false; + } + }); + // Disable associated catalogue entries const privilegeCatalogueEntries = feature.catalogue || []; privilegeCatalogueEntries.forEach((catalogueEntryId) => { From e0d3022dc9e3018eb85482fb41132483a4bb36fa Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 6 Jul 2020 15:56:01 +0100 Subject: [PATCH 02/46] skip flaky suite (#70727) --- .../security_solution/cypress/integration/alerts.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts index c8c18696359f7..fd52fb6734ef2 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts @@ -30,7 +30,8 @@ import { loginAndWaitForPage } from '../tasks/login'; import { ALERTS_URL } from '../urls/navigation'; -describe('Alerts', () => { +// Flaky: https://github.com/elastic/kibana/issues/70727 +describe.skip('Alerts', () => { context('Closing alerts', () => { beforeEach(() => { esArchiverLoad('alerts'); From 3bddc4aba9ea4e939b7e0140a77a13e0b5c5e8cc Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 6 Jul 2020 16:57:15 +0200 Subject: [PATCH 03/46] [Lens] Improve no data popover styles (#70594) --- .../public/ui/query_string_input/no_data_popover.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/plugins/data/public/ui/query_string_input/no_data_popover.tsx b/src/plugins/data/public/ui/query_string_input/no_data_popover.tsx index 302477a5fff5e..561c33519f96f 100644 --- a/src/plugins/data/public/ui/query_string_input/no_data_popover.tsx +++ b/src/plugins/data/public/ui/query_string_input/no_data_popover.tsx @@ -56,7 +56,7 @@ export function NoDataPopover({

{i18n.translate('data.noDataPopover.content', { defaultMessage: - "This time range doesn't contain any data. Increase or adjust the time range to see more fields and create charts", + "This time range doesn't contain any data. Increase or adjust the time range to see more fields and create charts.", })}

@@ -66,11 +66,13 @@ export function NoDataPopover({ step={1} stepsTotal={1} isStepOpen={noDataPopoverVisible} - subtitle={i18n.translate('data.noDataPopover.title', { defaultMessage: 'Tip' })} - title="" + subtitle={i18n.translate('data.noDataPopover.subtitle', { defaultMessage: 'Tip' })} + title={i18n.translate('data.noDataPopover.title', { defaultMessage: 'Empty dataset' })} footerAction={ { storage.set(NO_DATA_POPOVER_STORAGE_KEY, true); From 81bd66dcfd1c737b12b8968b8bbc27dcbe3e7bf7 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 6 Jul 2020 10:58:54 -0400 Subject: [PATCH 04/46] [Rollups] Make telemetry code more resilient (#70152) --- .../rollup/server/collectors/register.ts | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/rollup/server/collectors/register.ts b/x-pack/plugins/rollup/server/collectors/register.ts index 35c40e42efc19..aa06d3f696d00 100644 --- a/x-pack/plugins/rollup/server/collectors/register.ts +++ b/x-pack/plugins/rollup/server/collectors/register.ts @@ -137,27 +137,30 @@ async function fetchRollupVisualizations( let rollupVisualizationsFromSavedSearches = 0; visualizations.forEach((visualization: any) => { - const { - _source: { - visualization: { - savedSearchRefName, - kibanaSavedObjectMeta: { searchSourceJSON }, - }, - references = [] as any[], - }, - } = visualization; - - const searchSource = JSON.parse(searchSourceJSON); - - if (savedSearchRefName) { + const references: Array<{ name: string; id: string }> | undefined = get( + visualization, + '_source.references' + ); + const savedSearchRefName: string | undefined = get( + visualization, + '_source.visualization.savedSearchRefName' + ); + const searchSourceJSON: string | undefined = get( + visualization, + '_source.visualization.kibanaSavedObjectMeta.searchSourceJSON' + ); + + if (savedSearchRefName && references?.length) { // This visualization depends upon a saved search. - const savedSearch = references.find((ref: any) => ref.name === savedSearchRefName); - if (rollupSavedSearchesToFlagMap[savedSearch.id]) { + const savedSearch = references.find(({ name }) => name === savedSearchRefName); + if (savedSearch && rollupSavedSearchesToFlagMap[savedSearch.id]) { rollupVisualizations++; rollupVisualizationsFromSavedSearches++; } - } else { + } else if (searchSourceJSON) { // This visualization depends upon an index pattern. + const searchSource = JSON.parse(searchSourceJSON); + if (rollupIndexPatternToFlagMap[searchSource.index]) { rollupVisualizations++; } From b172b5b7773977065c64878acb461c49ff807064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez?= Date: Mon, 6 Jul 2020 17:04:23 +0200 Subject: [PATCH 05/46] [Logs UI] move ML job setup UI to a flyout (#68366) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Felix Stürmer Co-authored-by: Elastic Machine --- .../infra/common/log_analysis/log_analysis.ts | 2 +- .../log_analysis_module_status.tsx | 2 +- .../log_entry_categories/page_content.tsx | 34 ++++- .../page_results_content.tsx | 29 +++- .../page_setup_content.tsx | 93 ++++--------- .../log_entry_categories/setup_flyout.tsx | 129 ++++++++++++++++++ .../logs/log_entry_rate/page_content.tsx | 34 ++++- .../log_entry_rate/page_results_content.tsx | 29 +++- .../log_entry_rate/page_setup_content.tsx | 95 ++++--------- .../logs/log_entry_rate/setup_flyout.tsx | 129 ++++++++++++++++++ .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 12 files changed, 411 insertions(+), 169 deletions(-) create mode 100644 x-pack/plugins/infra/public/pages/logs/log_entry_categories/setup_flyout.tsx create mode 100644 x-pack/plugins/infra/public/pages/logs/log_entry_rate/setup_flyout.tsx diff --git a/x-pack/plugins/infra/common/log_analysis/log_analysis.ts b/x-pack/plugins/infra/common/log_analysis/log_analysis.ts index f0aa2067a24c2..b8fba7a14e243 100644 --- a/x-pack/plugins/infra/common/log_analysis/log_analysis.ts +++ b/x-pack/plugins/infra/common/log_analysis/log_analysis.ts @@ -35,7 +35,7 @@ export type SetupStatus = | { type: 'skipped'; newlyCreated?: boolean; - }; // setup is hidden + }; // setup is not necessary /** * Maps a job status to the possibility that results have already been produced diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx index 10205e9684ef2..a0046b630bfe1 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx +++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx @@ -252,7 +252,7 @@ const getSetupStatus = (everyJobStatus: Record(everyJobStatus).reduce((setupStatus, [, jobStatus]) => { if (jobStatus === 'missing') { return { type: 'required', reason: 'missing' }; - } else if (setupStatus.type === 'required') { + } else if (setupStatus.type === 'required' || setupStatus.type === 'succeeded') { return setupStatus; } else if (setupStatus.type === 'skipped' || isJobStatusWithResults(jobStatus)) { return { diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx index 5d9adb8a4f6ec..26633cd190a07 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx @@ -5,8 +5,8 @@ */ import { i18n } from '@kbn/i18n'; -import React, { useEffect } from 'react'; -import { isSetupStatusWithResults } from '../../../../common/log_analysis'; +import React, { useEffect, useState, useCallback } from 'react'; +import { isJobStatusWithResults } from '../../../../common/log_analysis'; import { LoadingPage } from '../../../components/loading_page'; import { LogAnalysisSetupStatusUnknownPrompt, @@ -21,6 +21,7 @@ import { useLogSourceContext } from '../../../containers/logs/log_source'; import { LogEntryCategoriesResultsContent } from './page_results_content'; import { LogEntryCategoriesSetupContent } from './page_setup_content'; import { useLogEntryCategoriesModuleContext } from './use_log_entry_categories_module'; +import { LogEntryCategoriesSetupFlyout } from './setup_flyout'; export const LogEntryCategoriesPageContent = () => { const { @@ -37,7 +38,11 @@ export const LogEntryCategoriesPageContent = () => { hasLogAnalysisSetupCapabilities, } = useLogAnalysisCapabilitiesContext(); - const { fetchJobStatus, setupStatus } = useLogEntryCategoriesModuleContext(); + const { fetchJobStatus, setupStatus, jobStatus } = useLogEntryCategoriesModuleContext(); + + const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); + const openFlyout = useCallback(() => setIsFlyoutOpen(true), []); + const closeFlyout = useCallback(() => setIsFlyoutOpen(false), []); useEffect(() => { if (hasLogAnalysisReadCapabilities) { @@ -45,6 +50,13 @@ export const LogEntryCategoriesPageContent = () => { } }, [fetchJobStatus, hasLogAnalysisReadCapabilities]); + // Open flyout if there are no ML jobs + useEffect(() => { + if (setupStatus.type === 'required' && setupStatus.reason === 'missing') { + openFlyout(); + } + }, [setupStatus, openFlyout]); + if (isLoading || isUninitialized) { return ; } else if (hasFailedLoadingSource) { @@ -63,11 +75,21 @@ export const LogEntryCategoriesPageContent = () => { ); } else if (setupStatus.type === 'unknown') { return ; - } else if (isSetupStatusWithResults(setupStatus)) { - return ; + } else if (isJobStatusWithResults(jobStatus['log-entry-categories-count'])) { + return ( + <> + + + + ); } else if (!hasLogAnalysisSetupCapabilities) { return ; } else { - return ; + return ( + <> + + + + ); } }; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx index a00351551e2d7..b4c044fe1cfcb 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_results_content.tsx @@ -24,7 +24,13 @@ import { const JOB_STATUS_POLLING_INTERVAL = 30000; -export const LogEntryCategoriesResultsContent: React.FunctionComponent = () => { +interface LogEntryCategoriesResultsContentProps { + onOpenSetup: () => void; +} + +export const LogEntryCategoriesResultsContent: React.FunctionComponent = ({ + onOpenSetup, +}) => { useTrackPageview({ app: 'infra_logs', path: 'log_entry_categories_results' }); useTrackPageview({ app: 'infra_logs', path: 'log_entry_categories_results', delay: 15000 }); @@ -123,12 +129,25 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent = () => { [setAutoRefresh] ); + const viewSetupFlyoutForReconfiguration = useCallback(() => { + viewSetupForReconfiguration(); + onOpenSetup(); + }, [onOpenSetup, viewSetupForReconfiguration]); + + const viewSetupFlyoutForUpdate = useCallback(() => { + viewSetupForUpdate(); + onOpenSetup(); + }, [onOpenSetup, viewSetupForUpdate]); + const hasResults = useMemo(() => topLogEntryCategories.length > 0, [ topLogEntryCategories.length, ]); const isFirstUse = useMemo( - () => setupStatus.type === 'skipped' && !!setupStatus.newlyCreated && !hasResults, + () => + ((setupStatus.type === 'skipped' && !!setupStatus.newlyCreated) || + setupStatus.type === 'succeeded') && + !hasResults, [hasResults, setupStatus] ); @@ -184,8 +203,8 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent = () => { hasOutdatedJobDefinitions={hasOutdatedJobDefinitions} hasStoppedJobs={hasStoppedJobs} isFirstUse={isFirstUse} - onRecreateMlJobForReconfiguration={viewSetupForReconfiguration} - onRecreateMlJobForUpdate={viewSetupForUpdate} + onRecreateMlJobForReconfiguration={viewSetupFlyoutForReconfiguration} + onRecreateMlJobForUpdate={viewSetupFlyoutForUpdate} qualityWarnings={categoryQualityWarnings} /> @@ -197,7 +216,7 @@ export const LogEntryCategoriesResultsContent: React.FunctionComponent = () => { isLoadingTopCategories={isLoadingTopLogEntryCategories} jobId={jobIds['log-entry-categories-count']} onChangeDatasetSelection={setCategoryQueryDatasets} - onRequestRecreateMlJob={viewSetupForReconfiguration} + onRequestRecreateMlJob={viewSetupFlyoutForReconfiguration} selectedDatasets={categoryQueryDatasets} sourceId={sourceId} timeRange={categoryQueryTimeRange.timeRange} diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_setup_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_setup_content.tsx index 7ae38234ae221..8d5d8a42200e6 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_setup_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_setup_content.tsx @@ -4,98 +4,51 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiSpacer, EuiSteps, EuiText } from '@elastic/eui'; +import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useMemo } from 'react'; +import { EuiText, EuiButton, EuiSpacer } from '@elastic/eui'; -import { BetaBadge } from '../../../components/beta_badge'; import { - createInitialConfigurationStep, - createProcessStep, LogAnalysisSetupPage, LogAnalysisSetupPageContent, LogAnalysisSetupPageHeader, } from '../../../components/logging/log_analysis_setup'; import { useTrackPageview } from '../../../../../observability/public'; -import { useLogEntryCategoriesSetup } from './use_log_entry_categories_setup'; -export const LogEntryCategoriesSetupContent: React.FunctionComponent = () => { +interface LogEntryCategoriesSetupContentProps { + onOpenSetup: () => void; +} + +export const LogEntryCategoriesSetupContent: React.FunctionComponent = ({ + onOpenSetup, +}) => { useTrackPageview({ app: 'infra_logs', path: 'log_entry_categories_setup' }); useTrackPageview({ app: 'infra_logs', path: 'log_entry_categories_setup', delay: 15000 }); - const { - cleanUpAndSetUp, - endTime, - isValidating, - lastSetupErrorMessages, - setEndTime, - setStartTime, - setValidatedIndices, - setUp, - setupStatus, - startTime, - validatedIndices, - validationErrors, - viewResults, - } = useLogEntryCategoriesSetup(); - - const steps = useMemo( - () => [ - createInitialConfigurationStep({ - setStartTime, - setEndTime, - startTime, - endTime, - isValidating, - validatedIndices, - setupStatus, - setValidatedIndices, - validationErrors, - }), - createProcessStep({ - cleanUpAndSetUp, - errorMessages: lastSetupErrorMessages, - isConfigurationValid: validationErrors.length <= 0 && !isValidating, - setUp, - setupStatus, - viewResults, - }), - ], - [ - cleanUpAndSetUp, - endTime, - isValidating, - lastSetupErrorMessages, - setEndTime, - setStartTime, - setUp, - setValidatedIndices, - setupStatus, - startTime, - validatedIndices, - validationErrors, - viewResults, - ] - ); - return ( {' '} - + defaultMessage="Set up log category analysis" + /> - +

+ +

- + + +
); diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/setup_flyout.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/setup_flyout.tsx new file mode 100644 index 0000000000000..ab5eae1ab3004 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/setup_flyout.tsx @@ -0,0 +1,129 @@ +/* + * 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, { useMemo, useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiTitle, + EuiText, + EuiSpacer, + EuiSteps, +} from '@elastic/eui'; + +import { + createInitialConfigurationStep, + createProcessStep, +} from '../../../components/logging/log_analysis_setup'; +import { useLogEntryCategoriesSetup } from './use_log_entry_categories_setup'; + +interface LogEntryCategoriesSetupFlyoutProps { + isOpen: boolean; + onClose: () => void; +} + +export const LogEntryCategoriesSetupFlyout: React.FC = ({ + isOpen, + onClose, +}) => { + const { + cleanUpAndSetUp, + endTime, + isValidating, + lastSetupErrorMessages, + setEndTime, + setStartTime, + setValidatedIndices, + setUp, + setupStatus, + startTime, + validatedIndices, + validationErrors, + viewResults, + } = useLogEntryCategoriesSetup(); + + const viewResultsAndClose = useCallback(() => { + viewResults(); + onClose(); + }, [viewResults, onClose]); + + const steps = useMemo( + () => [ + createInitialConfigurationStep({ + setStartTime, + setEndTime, + startTime, + endTime, + isValidating, + validatedIndices, + setupStatus, + setValidatedIndices, + validationErrors, + }), + createProcessStep({ + cleanUpAndSetUp, + errorMessages: lastSetupErrorMessages, + isConfigurationValid: validationErrors.length <= 0 && !isValidating, + setUp, + setupStatus, + viewResults: viewResultsAndClose, + }), + ], + [ + cleanUpAndSetUp, + endTime, + isValidating, + lastSetupErrorMessages, + setEndTime, + setStartTime, + setUp, + setValidatedIndices, + setupStatus, + startTime, + validatedIndices, + validationErrors, + viewResultsAndClose, + ] + ); + + if (!isOpen) { + return null; + } + return ( + + + +

+ +

+
+
+ + +

+ +

+
+ + + + + +
+
+ ); +}; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx index 4ec05a9778512..012b694bdbd25 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx @@ -5,8 +5,8 @@ */ import { i18n } from '@kbn/i18n'; -import React, { useEffect } from 'react'; -import { isSetupStatusWithResults } from '../../../../common/log_analysis'; +import React, { useEffect, useState, useCallback } from 'react'; +import { isJobStatusWithResults } from '../../../../common/log_analysis'; import { LoadingPage } from '../../../components/loading_page'; import { LogAnalysisSetupStatusUnknownPrompt, @@ -21,6 +21,7 @@ import { useLogSourceContext } from '../../../containers/logs/log_source'; import { LogEntryRateResultsContent } from './page_results_content'; import { LogEntryRateSetupContent } from './page_setup_content'; import { useLogEntryRateModuleContext } from './use_log_entry_rate_module'; +import { LogEntryRateSetupFlyout } from './setup_flyout'; export const LogEntryRatePageContent = () => { const { @@ -37,7 +38,11 @@ export const LogEntryRatePageContent = () => { hasLogAnalysisSetupCapabilities, } = useLogAnalysisCapabilitiesContext(); - const { fetchJobStatus, setupStatus } = useLogEntryRateModuleContext(); + const { fetchJobStatus, setupStatus, jobStatus } = useLogEntryRateModuleContext(); + + const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); + const openFlyout = useCallback(() => setIsFlyoutOpen(true), []); + const closeFlyout = useCallback(() => setIsFlyoutOpen(false), []); useEffect(() => { if (hasLogAnalysisReadCapabilities) { @@ -45,6 +50,13 @@ export const LogEntryRatePageContent = () => { } }, [fetchJobStatus, hasLogAnalysisReadCapabilities]); + // Open flyout if there are no ML jobs + useEffect(() => { + if (setupStatus.type === 'required' && setupStatus.reason === 'missing') { + openFlyout(); + } + }, [setupStatus, openFlyout]); + if (isLoading || isUninitialized) { return ; } else if (hasFailedLoadingSource) { @@ -63,11 +75,21 @@ export const LogEntryRatePageContent = () => { ); } else if (setupStatus.type === 'unknown') { return ; - } else if (isSetupStatusWithResults(setupStatus)) { - return ; + } else if (isJobStatusWithResults(jobStatus['log-entry-rate'])) { + return ( + <> + + + + ); } else if (!hasLogAnalysisSetupCapabilities) { return ; } else { - return ; + return ( + <> + + + + ); } }; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx index 3c8db3f8246c0..bf4dbcd87cc41 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx @@ -36,7 +36,13 @@ import { const JOB_STATUS_POLLING_INTERVAL = 30000; -export const LogEntryRateResultsContent: React.FunctionComponent = () => { +interface LogEntryRateResultsContentProps { + onOpenSetup: () => void; +} + +export const LogEntryRateResultsContent: React.FunctionComponent = ({ + onOpenSetup, +}) => { useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_results' }); useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_results', delay: 15000 }); @@ -127,13 +133,26 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { [setAutoRefresh] ); + const viewSetupFlyoutForReconfiguration = useCallback(() => { + viewSetupForReconfiguration(); + onOpenSetup(); + }, [viewSetupForReconfiguration, onOpenSetup]); + + const viewSetupFlyoutForUpdate = useCallback(() => { + viewSetupForUpdate(); + onOpenSetup(); + }, [viewSetupForUpdate, onOpenSetup]); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ const hasResults = useMemo(() => (logEntryRate?.histogramBuckets?.length ?? 0) > 0, [ logEntryRate, ]); const isFirstUse = useMemo( - () => setupStatus.type === 'skipped' && !!setupStatus.newlyCreated && !hasResults, + () => + ((setupStatus.type === 'skipped' && !!setupStatus.newlyCreated) || + setupStatus.type === 'succeeded') && + !hasResults, [hasResults, setupStatus] ); @@ -209,8 +228,8 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { hasOutdatedJobDefinitions={hasOutdatedJobDefinitions} hasStoppedJobs={hasStoppedJobs} isFirstUse={isFirstUse} - onRecreateMlJobForReconfiguration={viewSetupForReconfiguration} - onRecreateMlJobForUpdate={viewSetupForUpdate} + onRecreateMlJobForReconfiguration={viewSetupFlyoutForReconfiguration} + onRecreateMlJobForUpdate={viewSetupFlyoutForUpdate} /> @@ -227,7 +246,7 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { { +interface LogEntryRateSetupContentProps { + onOpenSetup: () => void; +} + +export const LogEntryRateSetupContent: React.FunctionComponent = ({ + onOpenSetup, +}) => { useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_setup' }); useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_setup', delay: 15000 }); - const { - cleanUpAndSetUp, - endTime, - isValidating, - lastSetupErrorMessages, - setEndTime, - setStartTime, - setValidatedIndices, - setUp, - setupStatus, - startTime, - validatedIndices, - validationErrors, - viewResults, - } = useLogEntryRateSetup(); - - const steps = useMemo( - () => [ - createInitialConfigurationStep({ - setStartTime, - setEndTime, - startTime, - endTime, - isValidating, - validatedIndices, - setupStatus, - setValidatedIndices, - validationErrors, - }), - createProcessStep({ - cleanUpAndSetUp, - errorMessages: lastSetupErrorMessages, - isConfigurationValid: validationErrors.length <= 0 && !isValidating, - setUp, - setupStatus, - viewResults, - }), - ], - [ - cleanUpAndSetUp, - endTime, - isValidating, - lastSetupErrorMessages, - setEndTime, - setStartTime, - setUp, - setValidatedIndices, - setupStatus, - startTime, - validatedIndices, - validationErrors, - viewResults, - ] - ); - return ( {' '} - + id="xpack.infra.logs.logEntryRate.setupTitle" + defaultMessage="Set up log anomaly analysis" + /> - +

+ +

- + + +
); diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/setup_flyout.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/setup_flyout.tsx new file mode 100644 index 0000000000000..0e9e34432f28b --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/setup_flyout.tsx @@ -0,0 +1,129 @@ +/* + * 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, { useMemo, useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiTitle, + EuiText, + EuiSpacer, + EuiSteps, +} from '@elastic/eui'; + +import { + createInitialConfigurationStep, + createProcessStep, +} from '../../../components/logging/log_analysis_setup'; +import { useLogEntryRateSetup } from './use_log_entry_rate_setup'; + +interface LogEntryRateSetupFlyoutProps { + isOpen: boolean; + onClose: () => void; +} + +export const LogEntryRateSetupFlyout: React.FC = ({ + isOpen, + onClose, +}) => { + const { + cleanUpAndSetUp, + endTime, + isValidating, + lastSetupErrorMessages, + setEndTime, + setStartTime, + setValidatedIndices, + setUp, + setupStatus, + startTime, + validatedIndices, + validationErrors, + viewResults, + } = useLogEntryRateSetup(); + + const viewResultsAndClose = useCallback(() => { + viewResults(); + onClose(); + }, [viewResults, onClose]); + + const steps = useMemo( + () => [ + createInitialConfigurationStep({ + setStartTime, + setEndTime, + startTime, + endTime, + isValidating, + validatedIndices, + setupStatus, + setValidatedIndices, + validationErrors, + }), + createProcessStep({ + cleanUpAndSetUp, + errorMessages: lastSetupErrorMessages, + isConfigurationValid: validationErrors.length <= 0 && !isValidating, + setUp, + setupStatus, + viewResults: viewResultsAndClose, + }), + ], + [ + cleanUpAndSetUp, + endTime, + isValidating, + lastSetupErrorMessages, + setEndTime, + setStartTime, + setUp, + setValidatedIndices, + setupStatus, + startTime, + validatedIndices, + validationErrors, + viewResultsAndClose, + ] + ); + + if (!isOpen) { + return null; + } + return ( + + + +

+ +

+
+
+ + +

+ +

+
+ + + + + +
+
+ ); +}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 683d83dde4e0f..51c6b33579f50 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7316,8 +7316,6 @@ "xpack.infra.alerting.logs.manageAlerts": "アラートを管理", "xpack.infra.alerting.manageAlerts": "アラートを管理", "xpack.infra.analysisSetup.actionStepTitle": "MLジョブを作成", - "xpack.infra.analysisSetup.analysisSetupDescription": "機械学習を使用して自動的に異常ログレートカウントを検出します。", - "xpack.infra.analysisSetup.analysisSetupTitle": "機械学習分析を有効にする", "xpack.infra.analysisSetup.configurationStepTitle": "構成", "xpack.infra.analysisSetup.createMlJobButton": "ML ジョブを作成", "xpack.infra.analysisSetup.deleteAnalysisResultsWarning": "これにより以前検出された異常が削除されます。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ca065c9523637..8121df6d05090 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7320,8 +7320,6 @@ "xpack.infra.alerting.logs.manageAlerts": "管理告警", "xpack.infra.alerting.manageAlerts": "管理告警", "xpack.infra.analysisSetup.actionStepTitle": "创建 ML 作业", - "xpack.infra.analysisSetup.analysisSetupDescription": "使用 Machine Learning 自动检测异常日志速率计数。", - "xpack.infra.analysisSetup.analysisSetupTitle": "启用 Machine Learning 分析", "xpack.infra.analysisSetup.configurationStepTitle": "配置", "xpack.infra.analysisSetup.createMlJobButton": "创建 ML 作业", "xpack.infra.analysisSetup.deleteAnalysisResultsWarning": "这将移除以前检测到的异常。", From 353f33297b12b6f89effce7926102d3c2b4fb546 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 6 Jul 2020 17:09:30 +0200 Subject: [PATCH 06/46] [Uptime] Fix detail page down monitor location badge text color (#70778) --- .../availability_reporting.test.tsx.snap | 21 ++++--- .../location_status_tags.test.tsx.snap | 57 ++++++++++--------- .../__snapshots__/tag_label.test.tsx.snap | 12 ++-- .../__tests__/availability_reporting.test.tsx | 10 +++- .../__tests__/tag_label.test.tsx | 4 +- .../availability_reporting.tsx | 2 +- .../location_status_tags.tsx | 6 +- .../availability_reporting/tag_label.tsx | 14 ++--- .../location_map/embeddables/map_tool_tip.tsx | 4 +- 9 files changed, 71 insertions(+), 59 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/availability_reporting.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/availability_reporting.test.tsx.snap index 9496274a69171..823346db3518a 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/availability_reporting.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/availability_reporting.test.tsx.snap @@ -111,13 +111,13 @@ Array [ -

au-heartbeat

-
+
@@ -188,13 +188,13 @@ Array [ -

nyc-heartbeat

-
+
@@ -265,13 +265,13 @@ Array [ -

spa-heartbeat

-
+
@@ -356,18 +356,21 @@ exports[`AvailabilityReporting component shallow renders correctly against snaps "availability": 100, "color": "#d3dae6", "label": "au-heartbeat", + "status": "up", "timestamp": "36m ago", }, Object { "availability": 100, "color": "#d3dae6", "label": "nyc-heartbeat", + "status": "down", "timestamp": "36m ago", }, Object { "availability": 100, "color": "#d3dae6", "label": "spa-heartbeat", + "status": "down", "timestamp": "36m ago", }, ] diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/location_status_tags.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/location_status_tags.test.tsx.snap index 05e0b50a86f35..4d3e85ba18ebe 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/location_status_tags.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/location_status_tags.test.tsx.snap @@ -10,18 +10,21 @@ exports[`LocationStatusTags component renders properly against props 1`] = ` "availability": 100, "color": "#d3dae6", "label": "Berlin", + "status": "up", "timestamp": "1 Mon ago", }, Object { "availability": 100, "color": "#bd271e", "label": "Berlin", + "status": "down", "timestamp": "1 Mon ago", }, Object { "availability": 100, "color": "#d3dae6", "label": "Islamabad", + "status": "up", "timestamp": "1 Mon ago", }, ] @@ -145,13 +148,13 @@ exports[`LocationStatusTags component renders when all locations are down 1`] = -

Berlin

-
+
@@ -222,13 +225,13 @@ exports[`LocationStatusTags component renders when all locations are down 1`] = -

Islamabad

-
+
@@ -393,13 +396,13 @@ exports[`LocationStatusTags component renders when all locations are up 1`] = ` -

Berlin

-
+
@@ -470,13 +473,13 @@ exports[`LocationStatusTags component renders when all locations are up 1`] = ` -

Islamabad

-
+
@@ -641,13 +644,13 @@ exports[`LocationStatusTags component renders when there are many location 1`] = -

Berlin

-
+
@@ -718,13 +721,13 @@ exports[`LocationStatusTags component renders when there are many location 1`] = -

Islamabad

-
+
@@ -795,13 +798,13 @@ exports[`LocationStatusTags component renders when there are many location 1`] = -

New York

-
+
@@ -872,13 +875,13 @@ exports[`LocationStatusTags component renders when there are many location 1`] = -

Paris

-
+
@@ -949,13 +952,13 @@ exports[`LocationStatusTags component renders when there are many location 1`] = -

Sydney

-
+
diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/tag_label.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/tag_label.test.tsx.snap index 3381efa62286b..28f1f433648c8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/tag_label.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/__snapshots__/tag_label.test.tsx.snap @@ -26,13 +26,13 @@ exports[`TagLabel component renders correctly against snapshot 1`] = ` -

US-East

-
+
@@ -44,13 +44,13 @@ exports[`TagLabel component shallow render correctly against snapshot 1`] = ` -

US-East

-
+
`; diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/availability_reporting.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/availability_reporting.test.tsx index de9f6b0d3b30f..b5fe5d17312c6 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/availability_reporting.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/availability_reporting.test.tsx @@ -19,14 +19,22 @@ describe('AvailabilityReporting component', () => { timestamp: '36m ago', color: '#d3dae6', availability: 100, + status: 'up', }, { label: 'nyc-heartbeat', timestamp: '36m ago', color: '#d3dae6', availability: 100, + status: 'down', + }, + { + label: 'spa-heartbeat', + timestamp: '36m ago', + color: '#d3dae6', + availability: 100, + status: 'down', }, - { label: 'spa-heartbeat', timestamp: '36m ago', color: '#d3dae6', availability: 100 }, ]; }); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/tag_label.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/tag_label.test.tsx index 3560784122298..8e46196ec3ab7 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/tag_label.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/__tests__/tag_label.test.tsx @@ -10,12 +10,12 @@ import { TagLabel } from '../tag_label'; describe('TagLabel component', () => { it('shallow render correctly against snapshot', () => { - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); it('renders correctly against snapshot', () => { - const component = renderWithIntl(); + const component = renderWithIntl(); expect(component).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/availability_reporting.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/availability_reporting.tsx index 8fed5db5e0271..ccf7d41642bfb 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/availability_reporting.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/availability_reporting.tsx @@ -30,7 +30,7 @@ export const AvailabilityReporting: React.FC = ({ allLocations }) => { name: LocationLabel, truncateText: true, render: (val: string, item: StatusTag) => { - return ; + return ; }, }, { diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/location_status_tags.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/location_status_tags.tsx index 6096499213a10..b48252d4208d2 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/location_status_tags.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/location_status_tags.tsx @@ -24,9 +24,10 @@ interface Props { export interface StatusTag { label: string; - timestamp: string; + timestamp?: string; color: string; - availability: number; + availability?: number; + status: 'up' | 'down'; } export const LocationStatusTags = ({ locations }: Props) => { @@ -48,6 +49,7 @@ export const LocationStatusTags = ({ locations }: Props) => { timestamp: moment(new Date(item.timestamp).valueOf()).fromNow(), color: item.summary.down === 0 ? gray : danger, availability: (item.up_history / (item.up_history + item.down_history)) * 100, + status: item.summary.down === 0 ? 'up' : 'down', }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/tag_label.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/tag_label.tsx index dbd73fc7d440b..ec5718415595d 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/tag_label.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/availability_reporting/tag_label.tsx @@ -6,7 +6,8 @@ import React from 'react'; import styled from 'styled-components'; -import { EuiBadge, EuiText } from '@elastic/eui'; +import { EuiBadge, EuiTextColor } from '@elastic/eui'; +import { StatusTag } from './location_status_tags'; const BadgeItem = styled.div` white-space: nowrap; @@ -17,18 +18,13 @@ const BadgeItem = styled.div` } `; -interface Props { - color: string; - label: string; -} - -export const TagLabel: React.FC = ({ color, label }) => { +export const TagLabel: React.FC = ({ color, label, status }) => { return ( - +

{label}

-
+
); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx index bf403846dcec4..8c66b57de58ac 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/location_map/embeddables/map_tool_tip.tsx @@ -64,9 +64,9 @@ export const MapToolTipComponent = ({ closeTooltip, features = [] }: MapToolTipP <> {layerId === 'up_points' ? ( - + ) : ( - + )} From 3250816d022f547b8fdb33dfcaa44df94846c80a Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 6 Jul 2020 16:13:37 +0100 Subject: [PATCH 07/46] skip flaky suite (#70818) --- .../test/functional/apps/security/doc_level_security_roles.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/security/doc_level_security_roles.js b/x-pack/test/functional/apps/security/doc_level_security_roles.js index 72f463be48fd5..d8a3e40ccc010 100644 --- a/x-pack/test/functional/apps/security/doc_level_security_roles.js +++ b/x-pack/test/functional/apps/security/doc_level_security_roles.js @@ -15,7 +15,8 @@ export default function ({ getService, getPageObjects }) { const screenshot = getService('screenshots'); const PageObjects = getPageObjects(['security', 'common', 'header', 'discover', 'settings']); - describe('dls', function () { + // Skipped as failing on ES Promotion: https://github.com/elastic/kibana/issues/70818 + describe.skip('dls', function () { before('initialize tests', async () => { await esArchiver.load('empty_kibana'); await esArchiver.loadIfNeeded('security/dlstest'); From 2399780d99a812a3d9a0c1957ea47582e0107072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Mon, 6 Jul 2020 17:37:28 +0200 Subject: [PATCH 08/46] [Logs + Metrics UI] Add index names for the new indexing strategy (#70245) This add support for the new index name patterns `logs-*` and `metrics-*` of the new indexing strategy to the Logs and Metrics UI source configurations in the form of a migration and changed defaults. --- .../add_log_column_popover.tsx | 161 --------- .../fields_configuration_panel.tsx | 328 +++++++----------- .../indices_configuration_panel.tsx | 128 ++----- .../log_columns_configuration_panel.tsx | 279 --------------- .../source_configuration_settings.tsx | 26 -- .../settings/indices_configuration_panel.tsx | 2 +- .../infra/public/pages/metrics/settings.tsx | 1 - .../infra/server/lib/sources/defaults.ts | 4 +- ..._new_indexing_strategy_index_names.test.ts | 131 +++++++ ...0_add_new_indexing_strategy_index_names.ts | 36 ++ .../server/lib/sources/saved_object_type.ts | 5 +- .../apis/metrics_ui/log_sources.ts | 6 +- .../apis/metrics_ui/sources.ts | 10 +- 13 files changed, 354 insertions(+), 763 deletions(-) delete mode 100644 x-pack/plugins/infra/public/components/source_configuration/add_log_column_popover.tsx delete mode 100644 x-pack/plugins/infra/public/components/source_configuration/log_columns_configuration_panel.tsx create mode 100644 x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.test.ts create mode 100644 x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.ts diff --git a/x-pack/plugins/infra/public/components/source_configuration/add_log_column_popover.tsx b/x-pack/plugins/infra/public/components/source_configuration/add_log_column_popover.tsx deleted file mode 100644 index 9f55126a1440a..0000000000000 --- a/x-pack/plugins/infra/public/components/source_configuration/add_log_column_popover.tsx +++ /dev/null @@ -1,161 +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 { EuiBadge, EuiButton, EuiPopover, EuiPopoverTitle, EuiSelectable } from '@elastic/eui'; -import { EuiSelectableOption } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useCallback, useMemo } from 'react'; -import { v4 as uuidv4 } from 'uuid'; - -import { LogColumnConfiguration } from '../../utils/source_configuration'; -import { useVisibilityState } from '../../utils/use_visibility_state'; -import { euiStyled } from '../../../../observability/public'; - -interface SelectableColumnOption { - optionProps: EuiSelectableOption; - columnConfiguration: LogColumnConfiguration; -} - -export const AddLogColumnButtonAndPopover: React.FunctionComponent<{ - addLogColumn: (logColumnConfiguration: LogColumnConfiguration) => void; - availableFields: string[]; - isDisabled?: boolean; -}> = ({ addLogColumn, availableFields, isDisabled }) => { - const { isVisible: isOpen, show: openPopover, hide: closePopover } = useVisibilityState(false); - - const availableColumnOptions = useMemo( - () => [ - { - optionProps: { - append: , - 'data-test-subj': 'addTimestampLogColumn', - // this key works around EuiSelectable using a lowercased label as - // key, which leads to conflicts with field names - key: 'timestamp', - label: 'Timestamp', - }, - columnConfiguration: { - timestampColumn: { - id: uuidv4(), - }, - }, - }, - { - optionProps: { - 'data-test-subj': 'addMessageLogColumn', - append: , - // this key works around EuiSelectable using a lowercased label as - // key, which leads to conflicts with field names - key: 'message', - label: 'Message', - }, - columnConfiguration: { - messageColumn: { - id: uuidv4(), - }, - }, - }, - ...availableFields.map((field) => ({ - optionProps: { - 'data-test-subj': `addFieldLogColumn addFieldLogColumn:${field}`, - // this key works around EuiSelectable using a lowercased label as - // key, which leads to conflicts with fields that only differ in the - // case (e.g. the metricbeat mongodb module) - key: `field-${field}`, - label: field, - }, - columnConfiguration: { - fieldColumn: { - id: uuidv4(), - field, - }, - }, - })), - ], - [availableFields] - ); - - const availableOptions = useMemo( - () => availableColumnOptions.map((availableColumnOption) => availableColumnOption.optionProps), - [availableColumnOptions] - ); - - const handleColumnSelection = useCallback( - (selectedOptions: EuiSelectableOption[]) => { - closePopover(); - - const selectedOptionIndex = selectedOptions.findIndex( - (selectedOption) => selectedOption.checked === 'on' - ); - const selectedOption = availableColumnOptions[selectedOptionIndex]; - - addLogColumn(selectedOption.columnConfiguration); - }, - [addLogColumn, availableColumnOptions, closePopover] - ); - - return ( - - - - } - closePopover={closePopover} - id="addLogColumn" - isOpen={isOpen} - ownFocus - panelPaddingSize="none" - > - - {(list, search) => ( - - {search} - {list} - - )} - - - ); -}; - -const searchProps = { - 'data-test-subj': 'fieldSearchInput', -}; - -const selectableListProps = { - showIcons: false, -}; - -const SystemColumnBadge: React.FunctionComponent = () => ( - - - -); - -const SelectableContent = euiStyled.div` - width: 400px; -`; diff --git a/x-pack/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx b/x-pack/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx index 369f07be67bf4..5ad05deafd69d 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx @@ -27,9 +27,7 @@ interface FieldsConfigurationPanelProps { isLoading: boolean; readOnly: boolean; podFieldProps: InputFieldProps; - tiebreakerFieldProps: InputFieldProps; timestampFieldProps: InputFieldProps; - displaySettings: 'metrics' | 'logs'; } export const FieldsConfigurationPanel = ({ @@ -38,15 +36,12 @@ export const FieldsConfigurationPanel = ({ isLoading, readOnly, podFieldProps, - tiebreakerFieldProps, timestampFieldProps, - displaySettings, }: FieldsConfigurationPanelProps) => { const isHostValueDefault = hostFieldProps.value === 'host.name'; const isContainerValueDefault = containerFieldProps.value === 'container.id'; const isPodValueDefault = podFieldProps.value === 'kubernetes.pod.uid'; const isTimestampValueDefault = timestampFieldProps.value === '@timestamp'; - const isTiebreakerValueDefault = tiebreakerFieldProps.value === '_doc'; return ( @@ -139,194 +134,141 @@ export const FieldsConfigurationPanel = ({ /> - {displaySettings === 'logs' && ( - <> - - - - } - description={ - - } - > - _doc, - }} - /> - } - isInvalid={tiebreakerFieldProps.isInvalid} - label={ - - } - > - - - - - )} - {displaySettings === 'metrics' && ( - <> - - - - } - description={ - - } - > - container.id, - }} - /> - } - isInvalid={containerFieldProps.isInvalid} - label={ - - } - > - - - - - - - } - description={ - - } - > - host.name, - }} - /> - } - isInvalid={hostFieldProps.isInvalid} - label={ - - } - > - - - - - - - } - description={ - - } - > - kubernetes.pod.uid, - }} - /> - } - isInvalid={podFieldProps.isInvalid} - label={ - - } - > - - - - - )} + + + + } + description={ + + } + > + container.id, + }} + /> + } + isInvalid={containerFieldProps.isInvalid} + label={ + + } + > + + + + + + + } + description={ + + } + > + host.name, + }} + /> + } + isInvalid={hostFieldProps.isInvalid} + label={ + + } + > + + + + + + + } + description={ + + } + > + kubernetes.pod.uid, + }} + /> + } + isInvalid={podFieldProps.isInvalid} + label={ + + } + > + + + ); }; diff --git a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx b/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx index 1d634b781bd34..e9817331ace93 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx @@ -21,17 +21,13 @@ import { InputFieldProps } from './input_fields'; interface IndicesConfigurationPanelProps { isLoading: boolean; readOnly: boolean; - logAliasFieldProps: InputFieldProps; metricAliasFieldProps: InputFieldProps; - displaySettings: 'metrics' | 'logs'; } export const IndicesConfigurationPanel = ({ isLoading, readOnly, - logAliasFieldProps, metricAliasFieldProps, - displaySettings, }: IndicesConfigurationPanelProps) => ( @@ -43,101 +39,51 @@ export const IndicesConfigurationPanel = ({ - {displaySettings === 'metrics' && ( - - - - } - description={ + - } - > - metricbeat-*, - }} - /> - } - isInvalid={metricAliasFieldProps.isInvalid} - label={ - - } - > - + } + description={ + + } + > + metrics-*,metricbeat-*, + }} /> - - - )} - {displaySettings === 'logs' && ( - - - } - description={ + isInvalid={metricAliasFieldProps.isInvalid} + label={ } > - filebeat-*, - }} - /> - } - isInvalid={logAliasFieldProps.isInvalid} - label={ - - } - > - - - - )} + disabled={isLoading} + readOnly={readOnly} + isLoading={isLoading} + {...metricAliasFieldProps} + /> + + ); diff --git a/x-pack/plugins/infra/public/components/source_configuration/log_columns_configuration_panel.tsx b/x-pack/plugins/infra/public/components/source_configuration/log_columns_configuration_panel.tsx deleted file mode 100644 index 46ab1e65c29d1..0000000000000 --- a/x-pack/plugins/infra/public/components/source_configuration/log_columns_configuration_panel.tsx +++ /dev/null @@ -1,279 +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 { - EuiButtonIcon, - EuiEmptyPrompt, - EuiForm, - EuiPanel, - EuiSpacer, - EuiText, - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiDragDropContext, - EuiDraggable, - EuiDroppable, - EuiIcon, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useCallback } from 'react'; -import { DragHandleProps, DropResult } from '../../../../observability/public'; - -import { AddLogColumnButtonAndPopover } from './add_log_column_popover'; -import { - FieldLogColumnConfigurationProps, - LogColumnConfigurationProps, -} from './log_columns_configuration_form_state'; -import { LogColumnConfiguration } from '../../utils/source_configuration'; - -interface LogColumnsConfigurationPanelProps { - availableFields: string[]; - isLoading: boolean; - logColumnConfiguration: LogColumnConfigurationProps[]; - addLogColumn: (logColumn: LogColumnConfiguration) => void; - moveLogColumn: (sourceIndex: number, destinationIndex: number) => void; -} - -export const LogColumnsConfigurationPanel: React.FunctionComponent = ({ - addLogColumn, - moveLogColumn, - availableFields, - isLoading, - logColumnConfiguration, -}) => { - const onDragEnd = useCallback( - ({ source, destination }: DropResult) => - destination && moveLogColumn(source.index, destination.index), - [moveLogColumn] - ); - - return ( - - - - -

- -

-
-
- - - -
- {logColumnConfiguration.length > 0 ? ( - - - <> - {/* Fragment here necessary for typechecking */} - {logColumnConfiguration.map((column, index) => ( - - {(provided) => ( - - )} - - ))} - - - - ) : ( - - )} -
- ); -}; - -interface LogColumnConfigurationPanelProps { - logColumnConfigurationProps: LogColumnConfigurationProps; - dragHandleProps: DragHandleProps; -} - -const LogColumnConfigurationPanel: React.FunctionComponent = ( - props -) => ( - <> - - {props.logColumnConfigurationProps.type === 'timestamp' ? ( - - ) : props.logColumnConfigurationProps.type === 'message' ? ( - - ) : ( - - )} - -); - -const TimestampLogColumnConfigurationPanel: React.FunctionComponent = ({ - logColumnConfigurationProps, - dragHandleProps, -}) => ( - timestamp, - }} - /> - } - removeColumn={logColumnConfigurationProps.remove} - dragHandleProps={dragHandleProps} - /> -); - -const MessageLogColumnConfigurationPanel: React.FunctionComponent = ({ - logColumnConfigurationProps, - dragHandleProps, -}) => ( - - } - removeColumn={logColumnConfigurationProps.remove} - dragHandleProps={dragHandleProps} - /> -); - -const FieldLogColumnConfigurationPanel: React.FunctionComponent<{ - logColumnConfigurationProps: FieldLogColumnConfigurationProps; - dragHandleProps: DragHandleProps; -}> = ({ - logColumnConfigurationProps: { - logColumnConfiguration: { field }, - remove, - }, - dragHandleProps, -}) => { - const fieldLogColumnTitle = i18n.translate( - 'xpack.infra.sourceConfiguration.fieldLogColumnTitle', - { - defaultMessage: 'Field', - } - ); - return ( - - - -
- -
-
- {fieldLogColumnTitle} - - {field} - - - - -
-
- ); -}; - -const ExplainedLogColumnConfigurationPanel: React.FunctionComponent<{ - fieldName: React.ReactNode; - helpText: React.ReactNode; - removeColumn: () => void; - dragHandleProps: DragHandleProps; -}> = ({ fieldName, helpText, removeColumn, dragHandleProps }) => ( - - - -
- -
-
- {fieldName} - - - {helpText} - - - - - -
-
-); - -const RemoveLogColumnButton: React.FunctionComponent<{ - onClick?: () => void; - columnDescription: string; -}> = ({ onClick, columnDescription }) => { - const removeColumnLabel = i18n.translate( - 'xpack.infra.sourceConfiguration.removeLogColumnButtonLabel', - { - defaultMessage: 'Remove {columnDescription} column', - values: { columnDescription }, - } - ); - - return ( - - ); -}; - -const LogColumnConfigurationEmptyPrompt: React.FunctionComponent = () => ( - - - - } - body={ -

- -

- } - /> -); diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx index 43bdc1f4cedcc..53b62f8dda04c 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx @@ -22,19 +22,16 @@ import { Source } from '../../containers/source'; import { FieldsConfigurationPanel } from './fields_configuration_panel'; import { IndicesConfigurationPanel } from './indices_configuration_panel'; import { NameConfigurationPanel } from './name_configuration_panel'; -import { LogColumnsConfigurationPanel } from './log_columns_configuration_panel'; import { useSourceConfigurationFormState } from './source_configuration_form_state'; import { SourceLoadingPage } from '../source_loading_page'; import { Prompt } from '../../utils/navigation_warning_prompt'; interface SourceConfigurationSettingsProps { shouldAllowEdit: boolean; - displaySettings: 'metrics' | 'logs'; } export const SourceConfigurationSettings = ({ shouldAllowEdit, - displaySettings, }: SourceConfigurationSettingsProps) => { const { createSourceConfiguration, @@ -45,16 +42,8 @@ export const SourceConfigurationSettings = ({ updateSourceConfiguration, } = useContext(Source.Context); - const availableFields = useMemo( - () => (source && source.status ? source.status.indexFields.map((field) => field.name) : []), - [source] - ); - const { - addLogColumn, - moveLogColumn, indicesConfigurationProps, - logColumnConfigurationProps, errors, resetForm, isFormDirty, @@ -119,10 +108,8 @@ export const SourceConfigurationSettings = ({ @@ -133,23 +120,10 @@ export const SourceConfigurationSettings = ({ isLoading={isLoading} podFieldProps={indicesConfigurationProps.podField} readOnly={!isWriteable} - tiebreakerFieldProps={indicesConfigurationProps.tiebreakerField} timestampFieldProps={indicesConfigurationProps.timestampField} - displaySettings={displaySettings} />
- {displaySettings === 'logs' && ( - - - - )} {errors.length > 0 ? ( <> diff --git a/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx index 83effaa3d51a5..b1dc55fe5c184 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx @@ -62,7 +62,7 @@ export const IndicesConfigurationPanel = ({ id="xpack.infra.sourceConfiguration.logIndicesRecommendedValue" defaultMessage="The recommended value is {defaultValue}" values={{ - defaultValue: filebeat-*, + defaultValue: logs-*,filebeat-*, }} /> } diff --git a/x-pack/plugins/infra/public/pages/metrics/settings.tsx b/x-pack/plugins/infra/public/pages/metrics/settings.tsx index 7d4f35b19da7d..b0aa67b5f0816 100644 --- a/x-pack/plugins/infra/public/pages/metrics/settings.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/settings.tsx @@ -15,7 +15,6 @@ export const MetricsSettingsPage = () => { ); diff --git a/x-pack/plugins/infra/server/lib/sources/defaults.ts b/x-pack/plugins/infra/server/lib/sources/defaults.ts index ba22b4db62d61..b096bed84fa9a 100644 --- a/x-pack/plugins/infra/server/lib/sources/defaults.ts +++ b/x-pack/plugins/infra/server/lib/sources/defaults.ts @@ -9,8 +9,8 @@ import { InfraSourceConfiguration } from '../../../common/http_api/source_api'; export const defaultSourceConfiguration: InfraSourceConfiguration = { name: 'Default', description: '', - metricAlias: 'metricbeat-*', - logAlias: 'filebeat-*,kibana_sample_data_logs*', + metricAlias: 'metrics-*,metricbeat-*', + logAlias: 'logs-*,filebeat-*,kibana_sample_data_logs*', fields: { container: 'container.id', host: 'host.name', diff --git a/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.test.ts b/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.test.ts new file mode 100644 index 0000000000000..59a22d33de858 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.test.ts @@ -0,0 +1,131 @@ +/* + * 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 { migrationMocks } from 'src/core/server/mocks'; +import { addNewIndexingStrategyIndexNames } from './7_9_0_add_new_indexing_strategy_index_names'; +import { infraSourceConfigurationSavedObjectName } from '../saved_object_type'; + +describe('infra source configuration migration function for 7.9.0', () => { + test('adds "logs-*" when the logAlias contains "filebeat-*"', () => { + const unmigratedConfiguration = createTestSourceConfiguration( + 'filebeat-*,custom-log-index-*', + 'custom-metric-index-*' + ); + + const migratedConfiguration = addNewIndexingStrategyIndexNames( + unmigratedConfiguration, + migrationMocks.createContext() + ); + + expect(migratedConfiguration).toStrictEqual( + createTestSourceConfiguration('filebeat-*,custom-log-index-*,logs-*', 'custom-metric-index-*') + ); + }); + + test('doesn\'t add "logs-*" when the logAlias doesn\'t contain "filebeat-*"', () => { + const unmigratedConfiguration = createTestSourceConfiguration( + 'custom-log-index-*', + 'custom-metric-index-*' + ); + + const migratedConfiguration = addNewIndexingStrategyIndexNames( + unmigratedConfiguration, + migrationMocks.createContext() + ); + + expect(migratedConfiguration).toStrictEqual(unmigratedConfiguration); + }); + + test('doesn\'t add "logs-*" when the logAlias already contains it', () => { + const unmigratedConfiguration = createTestSourceConfiguration( + 'filebeat-*,logs-*,custom-log-index-*', + 'custom-metric-index-*' + ); + + const migratedConfiguration = addNewIndexingStrategyIndexNames( + unmigratedConfiguration, + migrationMocks.createContext() + ); + + expect(migratedConfiguration).toStrictEqual(unmigratedConfiguration); + }); + + test('adds "metrics-*" when the logAlias contains "metricbeat-*"', () => { + const unmigratedConfiguration = createTestSourceConfiguration( + 'custom-log-index-*', + 'metricbeat-*,custom-metric-index-*' + ); + + const migratedConfiguration = addNewIndexingStrategyIndexNames( + unmigratedConfiguration, + migrationMocks.createContext() + ); + + expect(migratedConfiguration).toStrictEqual( + createTestSourceConfiguration( + 'custom-log-index-*', + 'metricbeat-*,custom-metric-index-*,metrics-*' + ) + ); + }); + + test('doesn\'t add "metrics-*" when the logAlias doesn\'t contain "metricbeat-*"', () => { + const unmigratedConfiguration = createTestSourceConfiguration( + 'custom-log-index-*', + 'custom-metric-index-*' + ); + + const migratedConfiguration = addNewIndexingStrategyIndexNames( + unmigratedConfiguration, + migrationMocks.createContext() + ); + + expect(migratedConfiguration).toStrictEqual(unmigratedConfiguration); + }); + + test('doesn\'t add "metrics-*" when the metricAlias already contains it', () => { + const unmigratedConfiguration = createTestSourceConfiguration( + 'custom-log-index-*', + 'metrics-*,metricbeat-*,custom-metric-index-*' + ); + + const migratedConfiguration = addNewIndexingStrategyIndexNames( + unmigratedConfiguration, + migrationMocks.createContext() + ); + + expect(migratedConfiguration).toStrictEqual(unmigratedConfiguration); + }); +}); + +const createTestSourceConfiguration = (logAlias: string, metricAlias: string) => ({ + attributes: { + name: 'TEST CONFIGURATION', + description: '', + fields: { + pod: 'TEST POD FIELD', + host: 'TEST HOST FIELD', + message: ['TEST MESSAGE FIELD'], + container: 'TEST CONTAINER FIELD', + timestamp: 'TEST TIMESTAMP FIELD', + tiebreaker: 'TEST TIEBREAKER FIELD', + }, + inventoryDefaultView: '0', + metricsExplorerDefaultView: '0', + logColumns: [ + { + fieldColumn: { + id: 'TEST FIELD COLUMN ID', + field: 'TEST FIELD COLUMN FIELD', + }, + }, + ], + logAlias, + metricAlias, + }, + id: 'TEST_ID', + type: infraSourceConfigurationSavedObjectName, +}); diff --git a/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.ts b/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.ts new file mode 100644 index 0000000000000..0d5563191d1b9 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.ts @@ -0,0 +1,36 @@ +/* + * 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 { SavedObjectMigrationFn } from 'src/core/server'; +import { InfraSourceConfiguration } from '../../../../common/http_api/source_api'; + +export const addNewIndexingStrategyIndexNames: SavedObjectMigrationFn< + InfraSourceConfiguration, + InfraSourceConfiguration +> = (sourceConfigurationDocument) => { + const oldLogAliasSegments = sourceConfigurationDocument.attributes.logAlias.split(','); + const oldMetricAliasSegments = sourceConfigurationDocument.attributes.metricAlias.split(','); + + const newLogAliasSegment = 'logs-*'; + const newMetricAliasSegment = 'metrics-*'; + + return { + ...sourceConfigurationDocument, + attributes: { + ...sourceConfigurationDocument.attributes, + logAlias: + oldLogAliasSegments.includes('filebeat-*') && + !oldLogAliasSegments.includes(newLogAliasSegment) + ? [...oldLogAliasSegments, newLogAliasSegment].join(',') + : sourceConfigurationDocument.attributes.logAlias, + metricAlias: + oldMetricAliasSegments.includes('metricbeat-*') && + !oldMetricAliasSegments.includes(newMetricAliasSegment) + ? [...oldMetricAliasSegments, newMetricAliasSegment].join(',') + : sourceConfigurationDocument.attributes.metricAlias, + }, + }; +}; diff --git a/x-pack/plugins/infra/server/lib/sources/saved_object_type.ts b/x-pack/plugins/infra/server/lib/sources/saved_object_type.ts index a36ef8d1a8921..11db18d6bf799 100644 --- a/x-pack/plugins/infra/server/lib/sources/saved_object_type.ts +++ b/x-pack/plugins/infra/server/lib/sources/saved_object_type.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { SavedObjectsType } from 'src/core/server'; +import { addNewIndexingStrategyIndexNames } from './migrations/7_9_0_add_new_indexing_strategy_index_names'; export const infraSourceConfigurationSavedObjectName = 'infrastructure-ui-source'; @@ -86,4 +86,7 @@ export const infraSourceConfigurationSavedObjectType: SavedObjectsType = { }, }, }, + migrations: { + '7.9.0': addNewIndexingStrategyIndexNames, + }, }; diff --git a/x-pack/test/api_integration/apis/metrics_ui/log_sources.ts b/x-pack/test/api_integration/apis/metrics_ui/log_sources.ts index d4cdd7316b3fd..00af3f8c25105 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/log_sources.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/log_sources.ts @@ -35,7 +35,7 @@ export default function ({ getService }: FtrProviderContext) { expect(origin).to.be('fallback'); expect(configuration.name).to.be('Default'); - expect(configuration.logAlias).to.be('filebeat-*,kibana_sample_data_logs*'); + expect(configuration.logAlias).to.be('logs-*,filebeat-*,kibana_sample_data_logs*'); expect(configuration.fields.timestamp).to.be('@timestamp'); expect(configuration.fields.tiebreaker).to.be('_doc'); expect(configuration.logColumns[0]).to.have.key('timestampColumn'); @@ -97,7 +97,7 @@ export default function ({ getService }: FtrProviderContext) { expect(configuration.name).to.be('Default'); expect(origin).to.be('stored'); - expect(configuration.logAlias).to.be('filebeat-*,kibana_sample_data_logs*'); + expect(configuration.logAlias).to.be('logs-*,filebeat-*,kibana_sample_data_logs*'); expect(configuration.fields.timestamp).to.be('@timestamp'); expect(configuration.fields.tiebreaker).to.be('_doc'); expect(configuration.logColumns).to.have.length(3); @@ -166,7 +166,7 @@ export default function ({ getService }: FtrProviderContext) { expect(configuration.name).to.be('NAME'); expect(origin).to.be('stored'); - expect(configuration.logAlias).to.be('filebeat-*,kibana_sample_data_logs*'); + expect(configuration.logAlias).to.be('logs-*,filebeat-*,kibana_sample_data_logs*'); expect(configuration.fields.timestamp).to.be('@timestamp'); expect(configuration.fields.tiebreaker).to.be('_doc'); expect(configuration.logColumns).to.have.length(3); diff --git a/x-pack/test/api_integration/apis/metrics_ui/sources.ts b/x-pack/test/api_integration/apis/metrics_ui/sources.ts index 5ed038776625c..5908523af2496 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/sources.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/sources.ts @@ -40,8 +40,8 @@ export default function ({ getService }: FtrProviderContext) { // shipped default values expect(sourceConfiguration.name).to.be('Default'); - expect(sourceConfiguration.metricAlias).to.be('metricbeat-*'); - expect(sourceConfiguration.logAlias).to.be('filebeat-*,kibana_sample_data_logs*'); + expect(sourceConfiguration.metricAlias).to.be('metrics-*,metricbeat-*'); + expect(sourceConfiguration.logAlias).to.be('logs-*,filebeat-*,kibana_sample_data_logs*'); expect(sourceConfiguration.fields.container).to.be('container.id'); expect(sourceConfiguration.fields.host).to.be('host.name'); expect(sourceConfiguration.fields.pod).to.be('kubernetes.pod.uid'); @@ -125,8 +125,8 @@ export default function ({ getService }: FtrProviderContext) { expect(updatedAt).to.be.greaterThan(0); expect(configuration.name).to.be('NAME'); expect(configuration.description).to.be(''); - expect(configuration.metricAlias).to.be('metricbeat-*'); - expect(configuration.logAlias).to.be('filebeat-*,kibana_sample_data_logs*'); + expect(configuration.metricAlias).to.be('metrics-*,metricbeat-*'); + expect(configuration.logAlias).to.be('logs-*,filebeat-*,kibana_sample_data_logs*'); expect(configuration.fields.container).to.be('container.id'); expect(configuration.fields.host).to.be('host.name'); expect(configuration.fields.pod).to.be('kubernetes.pod.uid'); @@ -283,7 +283,7 @@ export default function ({ getService }: FtrProviderContext) { expect(version).to.not.be(initialVersion); expect(updatedAt).to.be.greaterThan(createdAt); expect(configuration.metricAlias).to.be('metricbeat-**'); - expect(configuration.logAlias).to.be('filebeat-*,kibana_sample_data_logs*'); + expect(configuration.logAlias).to.be('logs-*,filebeat-*,kibana_sample_data_logs*'); expect(status.logIndicesExist).to.be(true); expect(status.metricIndicesExist).to.be(true); }); From f28d4e920e7c86f24eec3f4c899c271087c6fe57 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Mon, 6 Jul 2020 09:40:07 -0700 Subject: [PATCH 09/46] Clean-up expression "run" methods (#70795) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 🤖 remove legacy "interpret" methods in expressions * fix: 🐛 correctly use the moved "interpret" method * feat: 🎸 pass through execution context to sub-expressions * chore: 🤖 remove not used imports --- .../expressions/common/execution/execution.ts | 29 +++++++++++++++-- .../expressions/common/executor/executor.ts | 31 +------------------ 2 files changed, 28 insertions(+), 32 deletions(-) diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index 7bfb14b8bfa1c..8df9f08e9c40b 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -18,7 +18,7 @@ */ import { keys, last, mapValues, reduce, zipObject } from 'lodash'; -import { Executor } from '../executor'; +import { Executor, ExpressionExecOptions } from '../executor'; import { createExecutionContainer, ExecutionContainer } from './container'; import { createError } from '../util'; import { Defer, now } from '../../../kibana_utils/common'; @@ -31,6 +31,7 @@ import { parse, formatExpression, parseExpression, + ExpressionAstNode, } from '../ast'; import { ExecutionContext, DefaultInspectorAdapters } from './types'; import { getType, ExpressionValue } from '../expression_types'; @@ -382,7 +383,7 @@ export class Execution< const resolveArgFns = mapValues(argAstsWithDefaults, (asts, argName) => { return asts.map((item: ExpressionAstExpression) => { return async (subInput = input) => { - const output = await this.params.executor.interpret(item, subInput, { + const output = await this.interpret(item, subInput, { debug: this.params.debug, }); if (isExpressionValueError(output)) throw output.error; @@ -415,4 +416,28 @@ export class Execution< // function which would be treated as a promise return { resolvedArgs }; } + + public async interpret( + ast: ExpressionAstNode, + input: T, + options?: ExpressionExecOptions + ): Promise { + switch (getType(ast)) { + case 'expression': + const execution = this.params.executor.createExecution( + ast as ExpressionAstExpression, + this.context, + options + ); + execution.start(input); + return await execution.result; + case 'string': + case 'number': + case 'null': + case 'boolean': + return ast; + default: + throw new Error(`Unknown AST object: ${JSON.stringify(ast)}`); + } + } } diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts index 2ecbc5f75a9e8..2b5f9f2556d89 100644 --- a/src/plugins/expressions/common/executor/executor.ts +++ b/src/plugins/expressions/common/executor/executor.ts @@ -26,8 +26,7 @@ import { Execution, ExecutionParams } from '../execution/execution'; import { IRegistry } from '../types'; import { ExpressionType } from '../expression_types/expression_type'; import { AnyExpressionTypeDefinition } from '../expression_types/types'; -import { getType } from '../expression_types'; -import { ExpressionAstExpression, ExpressionAstNode } from '../ast'; +import { ExpressionAstExpression } from '../ast'; import { typeSpecs } from '../expression_types/specs'; import { functionSpecs } from '../expression_functions/specs'; @@ -154,34 +153,6 @@ export class Executor = Record( - ast: ExpressionAstNode, - input: T, - options?: ExpressionExecOptions - ): Promise { - switch (getType(ast)) { - case 'expression': - return await this.interpretExpression(ast as ExpressionAstExpression, input, options); - case 'string': - case 'number': - case 'null': - case 'boolean': - return ast; - default: - throw new Error(`Unknown AST object: ${JSON.stringify(ast)}`); - } - } - - public async interpretExpression( - ast: string | ExpressionAstExpression, - input: T, - options?: ExpressionExecOptions - ): Promise { - const execution = this.createExecution(ast, undefined, options); - execution.start(input); - return await execution.result; - } - /** * Execute expression and return result. * From cbd39d98a60feb9007fb8602fca1a584ae1729f2 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Mon, 6 Jul 2020 09:45:51 -0700 Subject: [PATCH 10/46] [Ingest Manager] Implement concurrency control for package configs (#70680) * Send SO version field as part of package configs, enforce it during package config update * Fix typings, extend response error to include optional status code * Revert unnecessary version fields in tests, fix schema Co-authored-by: Elastic Machine --- .../common/types/models/package_config.ts | 7 +++- .../common/types/rest_spec/package_config.ts | 4 +- .../hooks/use_request/use_request.ts | 22 +++++++---- .../edit_package_config_page/index.tsx | 37 +++++++++++++++---- .../ingest_manager/types/index.ts | 1 + .../server/routes/package_config/handlers.ts | 2 +- .../server/services/package_config.ts | 32 +++++++++++----- .../ingest_manager/server/types/index.tsx | 1 + .../server/types/models/package_config.ts | 6 +++ .../server/types/rest_spec/package_config.ts | 4 +- 10 files changed, 86 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/ingest_manager/common/types/models/package_config.ts b/x-pack/plugins/ingest_manager/common/types/models/package_config.ts index e9595bab0174e..0ff56e6d05d37 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/package_config.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/package_config.ts @@ -55,9 +55,14 @@ export interface NewPackageConfig { inputs: NewPackageConfigInput[]; } +export interface UpdatePackageConfig extends NewPackageConfig { + version?: string; +} + export interface PackageConfig extends Omit { id: string; inputs: PackageConfigInput[]; + version?: string; revision: number; updated_at: string; updated_by: string; @@ -65,4 +70,4 @@ export interface PackageConfig extends Omit { created_by: string; } -export type PackageConfigSOAttributes = Omit; +export type PackageConfigSOAttributes = Omit; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/package_config.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/package_config.ts index 4b8abbde47d5b..e62645debb748 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/package_config.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/package_config.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { PackageConfig, NewPackageConfig } from '../models'; +import { PackageConfig, NewPackageConfig, UpdatePackageConfig } from '../models'; export interface GetPackageConfigsRequest { query: { @@ -42,7 +42,7 @@ export interface CreatePackageConfigResponse { } export type UpdatePackageConfigRequest = GetOnePackageConfigRequest & { - body: NewPackageConfig; + body: UpdatePackageConfig; }; export type UpdatePackageConfigResponse = CreatePackageConfigResponse; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/use_request.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/use_request.ts index fbbc482fb96af..1486c2e50b7af 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/use_request.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/use_request.ts @@ -17,33 +17,39 @@ let httpClient: HttpSetup; export type UseRequestConfig = _UseRequestConfig; +interface RequestError extends Error { + statusCode?: number; +} + export const setHttpClient = (client: HttpSetup) => { httpClient = client; }; -export const sendRequest = ( +export const sendRequest = ( config: SendRequestConfig -): Promise> => { +): Promise> => { if (!httpClient) { throw new Error('sendRequest has no http client set'); } - return _sendRequest(httpClient, config); + return _sendRequest(httpClient, config); }; -export const useRequest = (config: UseRequestConfig) => { +export const useRequest = (config: UseRequestConfig) => { if (!httpClient) { throw new Error('sendRequest has no http client set'); } - return _useRequest(httpClient, config); + return _useRequest(httpClient, config); }; export type SendConditionalRequestConfig = | (SendRequestConfig & { shouldSendRequest: true }) | (Partial & { shouldSendRequest: false }); -export const useConditionalRequest = (config: SendConditionalRequestConfig) => { +export const useConditionalRequest = ( + config: SendConditionalRequestConfig +) => { const [state, setState] = useState<{ - error: Error | null; + error: RequestError | null; data: D | null; isLoading: boolean; }>({ @@ -70,7 +76,7 @@ export const useConditionalRequest = (config: SendConditionalRequestCon isLoading: true, error: null, }); - const res = await sendRequest({ + const res = await sendRequest({ method: config.method, path: config.path, query: config.query, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_package_config_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_package_config_page/index.tsx index 7fbcdbb9738cb..52fd95d663671 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_package_config_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_package_config_page/index.tsx @@ -16,7 +16,7 @@ import { EuiFlexItem, EuiSpacer, } from '@elastic/eui'; -import { AgentConfig, PackageInfo, NewPackageConfig } from '../../../types'; +import { AgentConfig, PackageInfo, UpdatePackageConfig } from '../../../types'; import { useLink, useBreadcrumbs, @@ -72,7 +72,7 @@ export const EditPackageConfigPage: React.FunctionComponent = () => { const [loadingError, setLoadingError] = useState(); const [agentConfig, setAgentConfig] = useState(); const [packageInfo, setPackageInfo] = useState(); - const [packageConfig, setPackageConfig] = useState({ + const [packageConfig, setPackageConfig] = useState({ name: '', description: '', namespace: '', @@ -80,6 +80,7 @@ export const EditPackageConfigPage: React.FunctionComponent = () => { enabled: true, output_id: '', inputs: [], + version: '', }); // Retrieve agent config, package, and package config info @@ -160,7 +161,7 @@ export const EditPackageConfigPage: React.FunctionComponent = () => { const hasErrors = validationResults ? validationHasErrors(validationResults) : false; // Update package config method - const updatePackageConfig = (updatedFields: Partial) => { + const updatePackageConfig = (updatedFields: Partial) => { const newPackageConfig = { ...packageConfig, ...updatedFields, @@ -178,7 +179,7 @@ export const EditPackageConfigPage: React.FunctionComponent = () => { } }; - const updatePackageConfigValidation = (newPackageConfig?: NewPackageConfig) => { + const updatePackageConfigValidation = (newPackageConfig?: UpdatePackageConfig) => { if (packageInfo) { const newValidationResult = validatePackageConfig( newPackageConfig || packageConfig, @@ -234,9 +235,31 @@ export const EditPackageConfigPage: React.FunctionComponent = () => { : undefined, }); } else { - notifications.toasts.addError(error, { - title: 'Error', - }); + if (error.statusCode === 409) { + notifications.toasts.addError(error, { + title: i18n.translate('xpack.ingestManager.editPackageConfig.failedNotificationTitle', { + defaultMessage: `Error updating '{packageConfigName}'`, + values: { + packageConfigName: packageConfig.name, + }, + }), + toastMessage: i18n.translate( + 'xpack.ingestManager.editPackageConfig.failedConflictNotificationMessage', + { + defaultMessage: `Data is out of date. Refresh the page to get the latest configuration.`, + } + ), + }); + } else { + notifications.toasts.addError(error, { + title: i18n.translate('xpack.ingestManager.editPackageConfig.failedNotificationTitle', { + defaultMessage: `Error updating '{packageConfigName}'`, + values: { + packageConfigName: packageConfig.name, + }, + }), + }); + } setFormState('VALID'); } }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts index 43ec2f6d1a74d..e28d76cae9955 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts @@ -15,6 +15,7 @@ export { EnrollmentAPIKey, PackageConfig, NewPackageConfig, + UpdatePackageConfig, PackageConfigInput, PackageConfigInputStream, PackageConfigConfigRecordEntry, diff --git a/x-pack/plugins/ingest_manager/server/routes/package_config/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/package_config/handlers.ts index e212c861ce770..f11275c92bb68 100644 --- a/x-pack/plugins/ingest_manager/server/routes/package_config/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/package_config/handlers.ts @@ -178,7 +178,7 @@ export const updatePackageConfigHandler: RequestHandler< }); } catch (e) { return response.customError({ - statusCode: 500, + statusCode: e.statusCode || 500, body: { message: e.message }, }); } diff --git a/x-pack/plugins/ingest_manager/server/services/package_config.ts b/x-pack/plugins/ingest_manager/server/services/package_config.ts index 5a7546bfee2e0..9fa51d025ad2b 100644 --- a/x-pack/plugins/ingest_manager/server/services/package_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/package_config.ts @@ -15,6 +15,7 @@ import { import { PACKAGE_CONFIG_SAVED_OBJECT_TYPE } from '../constants'; import { NewPackageConfig, + UpdatePackageConfig, PackageConfig, ListWithKuery, PackageConfigSOAttributes, @@ -60,6 +61,7 @@ class PackageConfigService { return { id: newSo.id, + version: newSo.version, ...newSo.attributes, }; } @@ -71,7 +73,7 @@ class PackageConfigService { options?: { user?: AuthenticatedUser } ): Promise { const isoDate = new Date().toISOString(); - const { saved_objects: newSos } = await soClient.bulkCreate>( + const { saved_objects: newSos } = await soClient.bulkCreate( packageConfigs.map((packageConfig) => ({ type: SAVED_OBJECT_TYPE, attributes: { @@ -98,6 +100,7 @@ class PackageConfigService { return newSos.map((newSo) => ({ id: newSo.id, + version: newSo.version, ...newSo.attributes, })); } @@ -117,6 +120,7 @@ class PackageConfigService { return { id: packageConfigSO.id, + version: packageConfigSO.version, ...packageConfigSO.attributes, }; } @@ -137,6 +141,7 @@ class PackageConfigService { return packageConfigSO.saved_objects.map((so) => ({ id: so.id, + version: so.version, ...so.attributes, })); } @@ -163,8 +168,9 @@ class PackageConfigService { }); return { - items: packageConfigs.saved_objects.map((packageConfigSO) => ({ + items: packageConfigs.saved_objects.map((packageConfigSO) => ({ id: packageConfigSO.id, + version: packageConfigSO.version, ...packageConfigSO.attributes, })), total: packageConfigs.total, @@ -176,21 +182,29 @@ class PackageConfigService { public async update( soClient: SavedObjectsClientContract, id: string, - packageConfig: NewPackageConfig, + packageConfig: UpdatePackageConfig, options?: { user?: AuthenticatedUser } ): Promise { const oldPackageConfig = await this.get(soClient, id); + const { version, ...restOfPackageConfig } = packageConfig; if (!oldPackageConfig) { throw new Error('Package config not found'); } - await soClient.update(SAVED_OBJECT_TYPE, id, { - ...packageConfig, - revision: oldPackageConfig.revision + 1, - updated_at: new Date().toISOString(), - updated_by: options?.user?.username ?? 'system', - }); + await soClient.update( + SAVED_OBJECT_TYPE, + id, + { + ...restOfPackageConfig, + revision: oldPackageConfig.revision + 1, + updated_at: new Date().toISOString(), + updated_by: options?.user?.username ?? 'system', + }, + { + version, + } + ); // Bump revision of associated agent config await agentConfigService.bumpRevision(soClient, packageConfig.config_id, { diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx index 179474d31bc18..8239302a97832 100644 --- a/x-pack/plugins/ingest_manager/server/types/index.tsx +++ b/x-pack/plugins/ingest_manager/server/types/index.tsx @@ -21,6 +21,7 @@ export { PackageConfigInput, PackageConfigInputStream, NewPackageConfig, + UpdatePackageConfig, PackageConfigSOAttributes, FullAgentConfigInput, FullAgentConfig, diff --git a/x-pack/plugins/ingest_manager/server/types/models/package_config.ts b/x-pack/plugins/ingest_manager/server/types/models/package_config.ts index 4b9718dfbe165..0823ccd85a32b 100644 --- a/x-pack/plugins/ingest_manager/server/types/models/package_config.ts +++ b/x-pack/plugins/ingest_manager/server/types/models/package_config.ts @@ -66,7 +66,13 @@ export const NewPackageConfigSchema = schema.object({ ...PackageConfigBaseSchema, }); +export const UpdatePackageConfigSchema = schema.object({ + ...PackageConfigBaseSchema, + version: schema.maybe(schema.string()), +}); + export const PackageConfigSchema = schema.object({ ...PackageConfigBaseSchema, id: schema.string(), + version: schema.maybe(schema.string()), }); diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/package_config.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/package_config.ts index 7b7ae1957c15e..630fb55f2654d 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/package_config.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/package_config.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { schema } from '@kbn/config-schema'; -import { NewPackageConfigSchema } from '../models'; +import { NewPackageConfigSchema, UpdatePackageConfigSchema } from '../models'; import { ListWithKuerySchema } from './index'; export const GetPackageConfigsRequestSchema = { @@ -23,7 +23,7 @@ export const CreatePackageConfigRequestSchema = { export const UpdatePackageConfigRequestSchema = { ...GetOnePackageConfigRequestSchema, - body: NewPackageConfigSchema, + body: UpdatePackageConfigSchema, }; export const DeletePackageConfigsRequestSchema = { From a4485c86c1613bfd560f9d88c6d3c02614f3e83c Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 6 Jul 2020 12:53:27 -0400 Subject: [PATCH 11/46] [Ingest Manager] Fix agent version check to work with SNAPSHOT versions (#70796) --- .../server/services/agents/enroll.test.ts | 66 +++++++++++++++++++ .../server/services/agents/enroll.ts | 28 ++++++-- 2 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/server/services/agents/enroll.test.ts diff --git a/x-pack/plugins/ingest_manager/server/services/agents/enroll.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/enroll.test.ts new file mode 100644 index 0000000000000..764564cfa49f5 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/agents/enroll.test.ts @@ -0,0 +1,66 @@ +/* + * 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 { validateAgentVersion } from './enroll'; +import { appContextService } from '../app_context'; +import { IngestManagerAppContext } from '../../plugin'; + +describe('validateAgentVersion', () => { + it('should throw with agent > kibana version', () => { + appContextService.start(({ + kibanaVersion: '8.0.0', + } as unknown) as IngestManagerAppContext); + expect(() => + validateAgentVersion({ + local: { elastic: { agent: { version: '8.8.0' } } }, + userProvided: {}, + }) + ).toThrowError(/Agent version is not compatible with kibana version/); + }); + it('should work with agent < kibana version', () => { + appContextService.start(({ + kibanaVersion: '8.0.0', + } as unknown) as IngestManagerAppContext); + validateAgentVersion({ local: { elastic: { agent: { version: '7.8.0' } } }, userProvided: {} }); + }); + + it('should work with agent = kibana version', () => { + appContextService.start(({ + kibanaVersion: '8.0.0', + } as unknown) as IngestManagerAppContext); + validateAgentVersion({ local: { elastic: { agent: { version: '8.0.0' } } }, userProvided: {} }); + }); + + it('should work with SNAPSHOT version', () => { + appContextService.start(({ + kibanaVersion: '8.0.0-SNAPSHOT', + } as unknown) as IngestManagerAppContext); + validateAgentVersion({ + local: { elastic: { agent: { version: '8.0.0-SNAPSHOT' } } }, + userProvided: {}, + }); + }); + + it('should work with a agent using SNAPSHOT version', () => { + appContextService.start(({ + kibanaVersion: '7.8.0', + } as unknown) as IngestManagerAppContext); + validateAgentVersion({ + local: { elastic: { agent: { version: '7.8.0-SNAPSHOT' } } }, + userProvided: {}, + }); + }); + + it('should work with a kibana using SNAPSHOT version', () => { + appContextService.start(({ + kibanaVersion: '7.8.0-SNAPSHOT', + } as unknown) as IngestManagerAppContext); + validateAgentVersion({ + local: { elastic: { agent: { version: '7.8.0' } } }, + userProvided: {}, + }); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts b/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts index bf15815e6ae41..b63b1c13e4df9 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/enroll.ts @@ -20,11 +20,7 @@ export async function enroll( metadata?: { local: any; userProvided: any }, sharedId?: string ): Promise { - const kibanaVersion = appContextService.getKibanaVersion(); - const version: string | undefined = metadata?.local?.elastic?.agent?.version; - if (!version || semver.compare(version, kibanaVersion) === 1) { - throw Boom.badRequest('Agent version is not compatible with kibana version'); - } + validateAgentVersion(metadata); const existingAgent = sharedId ? await getAgentBySharedId(soClient, sharedId) : null; @@ -92,3 +88,25 @@ async function getAgentBySharedId(soClient: SavedObjectsClientContract, sharedId return null; } + +export function validateAgentVersion(metadata?: { local: any; userProvided: any }) { + const kibanaVersion = semver.parse(appContextService.getKibanaVersion()); + if (!kibanaVersion) { + throw Boom.badRequest('Kibana version is not set'); + } + const version = semver.parse(metadata?.local?.elastic?.agent?.version); + if (!version) { + throw Boom.badRequest('Agent version not provided in metadata.'); + } + + if (!version || !semver.lte(formatVersion(version), formatVersion(kibanaVersion))) { + throw Boom.badRequest('Agent version is not compatible with kibana version'); + } +} + +/** + * used to remove prelease from version as includePrerelease in not working as expected + */ +function formatVersion(version: semver.SemVer) { + return `${version.major}.${version.minor}.${version.patch}`; +} From 89dcdbbbee08b2b464cf64164b001c7a82945a7e Mon Sep 17 00:00:00 2001 From: Sonja Krause-Harder Date: Mon, 6 Jul 2020 18:57:18 +0200 Subject: [PATCH 12/46] [Ingest Manager] Update package registry docker image for CI. (#70716) * Update package registry docker image for CI. * Adapt to new registry filesystem layout. * Adjust tests to changed registry behavior. Co-authored-by: Elastic Machine --- .../test/ingest_manager_api_integration/apis/file.ts | 6 +++--- .../apis/fixtures/package_registry_config.yml | 4 ++-- x-pack/test/ingest_manager_api_integration/config.ts | 10 +++++++--- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/x-pack/test/ingest_manager_api_integration/apis/file.ts b/x-pack/test/ingest_manager_api_integration/apis/file.ts index 33eeda1ee274d..a7462ac51ecc1 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/file.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/file.ts @@ -47,7 +47,7 @@ export default function ({ getService }: FtrProviderContext) { '/api/ingest_manager/epm/packages/filetest/0.1.0/kibana/visualization/sample_visualization.json' ) .set('kbn-xsrf', 'xxx') - .expect('Content-Type', 'application/json; charset=utf-8') + .expect('Content-Type', 'text/plain; charset=utf-8') .expect(200); } else { warnAndSkipTest(this, log); @@ -61,7 +61,7 @@ export default function ({ getService }: FtrProviderContext) { '/api/ingest_manager/epm/packages/filetest/0.1.0/kibana/dashboard/sample_dashboard.json' ) .set('kbn-xsrf', 'xxx') - .expect('Content-Type', 'application/json; charset=utf-8') + .expect('Content-Type', 'text/plain; charset=utf-8') .expect(200); } else { warnAndSkipTest(this, log); @@ -73,7 +73,7 @@ export default function ({ getService }: FtrProviderContext) { await supertest .get('/api/ingest_manager/epm/packages/filetest/0.1.0/kibana/search/sample_search.json') .set('kbn-xsrf', 'xxx') - .expect('Content-Type', 'application/json; charset=utf-8') + .expect('Content-Type', 'text/plain; charset=utf-8') .expect(200); } else { warnAndSkipTest(this, log); diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/package_registry_config.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/package_registry_config.yml index 0060e247827da..5bfbf78e25ed8 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fixtures/package_registry_config.yml +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/package_registry_config.yml @@ -1,3 +1,3 @@ package_paths: - - /registry/packages/package-storage - - /registry/packages/test-packages \ No newline at end of file + - /packages/production + - /packages/test-packages \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/config.ts b/x-pack/test/ingest_manager_api_integration/config.ts index bbef12463ed08..88ec8d53c1cde 100644 --- a/x-pack/test/ingest_manager_api_integration/config.ts +++ b/x-pack/test/ingest_manager_api_integration/config.ts @@ -21,21 +21,25 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { `${path.join( path.dirname(__filename), './apis/fixtures/package_registry_config.yml' - )}:/registry/config.yml`, + )}:/package-registry/config.yml`, '-v', `${path.join( path.dirname(__filename), './apis/fixtures/test_packages' - )}:/registry/packages/test-packages`, + )}:/packages/test-packages`, ]; + // Docker image to use for Ingest Manager API integration tests. + const dockerImage = + 'docker.elastic.co/package-registry/distribution:184b85f19e8fd14363e36150173d338ff9659f01'; + return { testFiles: [require.resolve('./apis')], servers: xPackAPITestsConfig.get('servers'), dockerServers: defineDockerServersConfig({ registry: { enabled: !!registryPort, - image: 'docker.elastic.co/package-registry/package-registry:kibana-testing-1', + image: dockerImage, portInContainer: 8080, port: registryPort, args: dockerArgs, From 31abd6dc28d723d71378920bd1d01940444148be Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 6 Jul 2020 13:10:01 -0400 Subject: [PATCH 13/46] [ML] DF Analytics creation: switch to includes table (#70009) * update modelMemoryLimit when hyperParams change * update functional clone tests * switch excludes table to includes table * Job configuration details update * fix jest tests and types * fix translations and validate includes fields * fix functional test * handle empty includes selection * switch filter to field_value_toggle_group * update clone functional test --- .../custom_selection_table.js | 13 +- .../advanced_step/advanced_step.tsx | 15 +- .../advanced_step/advanced_step_form.tsx | 11 +- .../analysis_fields_table.tsx | 278 ++++++++++-------- .../configuration_step/configuration_step.tsx | 18 +- .../configuration_step_details.tsx | 15 +- .../configuration_step_form.tsx | 61 ++-- .../configuration_step/job_type.tsx | 2 +- .../components/create_step/create_step.tsx | 6 +- .../components/details_step/details_step.tsx | 15 +- .../pages/analytics_creation/page.tsx | 3 - .../analytics_list/action_clone.test.ts | 40 ++- .../analytics_list/action_clone.tsx | 2 +- .../use_create_analytics_form/reducer.ts | 19 +- .../use_create_analytics_form/state.test.ts | 54 +++- .../hooks/use_create_analytics_form/state.ts | 77 +---- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - .../classification_creation.ts | 9 + .../apps/ml/data_frame_analytics/cloning.ts | 10 +- .../apps/ml/data_frame_analytics/index.ts | 2 +- .../outlier_detection_creation.ts | 9 + .../regression_creation.ts | 9 + .../ml/data_frame_analytics_creation.ts | 75 ++++- 24 files changed, 461 insertions(+), 286 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/custom_selection_table/custom_selection_table.js b/x-pack/plugins/ml/public/application/components/custom_selection_table/custom_selection_table.js index c86b716b2f49b..274a5ff0ffbb4 100644 --- a/x-pack/plugins/ml/public/application/components/custom_selection_table/custom_selection_table.js +++ b/x-pack/plugins/ml/public/application/components/custom_selection_table/custom_selection_table.js @@ -45,6 +45,7 @@ function getError(error) { export function CustomSelectionTable({ checkboxDisabledCheck, columns, + currentPage = 0, filterDefaultFields, filters, items, @@ -52,6 +53,7 @@ export function CustomSelectionTable({ onTableChange, radioDisabledCheck, selectedIds, + setCurrentPaginationData, singleSelection, sortableProperties, tableItemId = 'id', @@ -80,7 +82,7 @@ export function CustomSelectionTable({ }, [selectedIds]); // eslint-disable-line useEffect(() => { - const tablePager = new Pager(currentItems.length, itemsPerPage); + const tablePager = new Pager(currentItems.length, itemsPerPage, currentPage); setPagerSettings({ itemsPerPage: itemsPerPage, firstItemIndex: tablePager.getFirstItemIndex(), @@ -124,6 +126,13 @@ export function CustomSelectionTable({ } } + if (setCurrentPaginationData) { + setCurrentPaginationData({ + pageIndex: pager.getCurrentPageIndex(), + itemsPerPage: pagerSettings.itemsPerPage, + }); + } + onTableChange(currentSelected); } @@ -389,6 +398,7 @@ export function CustomSelectionTable({ CustomSelectionTable.propTypes = { checkboxDisabledCheck: PropTypes.func, columns: PropTypes.array.isRequired, + currentPage: PropTypes.number, filterDefaultFields: PropTypes.array, filters: PropTypes.array, items: PropTypes.array.isRequired, @@ -396,6 +406,7 @@ CustomSelectionTable.propTypes = { onTableChange: PropTypes.func.isRequired, radioDisabledCheck: PropTypes.func, selectedId: PropTypes.array, + setCurrentPaginationData: PropTypes.func, singleSelection: PropTypes.bool, sortableProperties: PropTypes.object, tableItemId: PropTypes.string, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step.tsx index f957dcab2e87e..b16300a448a7c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step.tsx @@ -19,14 +19,19 @@ export const AdvancedStep: FC = ({ setCurrentStep, stepActivated, }) => { + const showForm = step === ANALYTICS_STEPS.ADVANCED; + const showDetails = step !== ANALYTICS_STEPS.ADVANCED && stepActivated === true; + + const dataTestSubj = `mlAnalyticsCreateJobWizardAdvancedStep${showForm ? ' active' : ''}${ + showDetails ? ' summary' : '' + }`; + return ( - - {step === ANALYTICS_STEPS.ADVANCED && ( + + {showForm && ( )} - {step !== ANALYTICS_STEPS.ADVANCED && stepActivated === true && ( - - )} + {showDetails && } ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx index bc9bb0cce5ae8..21b0d3d7dd89e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx @@ -47,7 +47,7 @@ export const AdvancedStepForm: FC = ({ const [advancedParamErrors, setAdvancedParamErrors] = useState({}); const [fetchingAdvancedParamErrors, setFetchingAdvancedParamErrors] = useState(false); - const { setFormState } = actions; + const { setEstimatedModelMemoryLimit, setFormState } = actions; const { form, isJobCreated } = state; const { computeFeatureInfluence, @@ -87,10 +87,15 @@ export const AdvancedStepForm: FC = ({ useEffect(() => { setFetchingAdvancedParamErrors(true); (async function () { - const { success, errorMessage } = await fetchExplainData(form); + const { success, errorMessage, expectedMemory } = await fetchExplainData(form); const paramErrors: AdvancedParamErrors = {}; - if (!success) { + if (success) { + if (modelMemoryLimit !== expectedMemory) { + setEstimatedModelMemoryLimit(expectedMemory); + setFormState({ modelMemoryLimit: expectedMemory }); + } + } else { // Check which field is invalid Object.values(ANALYSIS_ADVANCED_FIELDS).forEach((param) => { if (errorMessage.includes(`[${param}]`)) { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx index c71e7e73b13d9..def6acdae14e3 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, memo, useEffect, useState } from 'react'; +import React, { FC, Fragment, useEffect, useState } from 'react'; import { EuiCallOut, EuiFormRow, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; // @ts-ignore no declaration import { LEFT_ALIGNMENT, CENTER_ALIGNMENT, SortableProperties } from '@elastic/eui/lib/services'; @@ -14,6 +14,13 @@ import { FieldSelectionItem } from '../../../../common/analytics'; // @ts-ignore could not find declaration file import { CustomSelectionTable } from '../../../../../components/custom_selection_table'; +const minimumFieldsMessage = i18n.translate( + 'xpack.ml.dataframe.analytics.create.analysisFieldsTable.minimumFieldsMessage', + { + defaultMessage: 'At least one field must be selected.', + } +); + const columns = [ { id: 'checkbox', @@ -22,9 +29,12 @@ const columns = [ width: '32px', }, { - label: i18n.translate('xpack.ml.dataframe.analytics.create.analyticsTable.fieldNameColumn', { - defaultMessage: 'Field name', - }), + label: i18n.translate( + 'xpack.ml.dataframe.analytics.create.analysisFieldsTable.fieldNameColumn', + { + defaultMessage: 'Field name', + } + ), id: 'name', isSortable: true, alignment: LEFT_ALIGNMENT, @@ -68,140 +78,154 @@ const columns = [ ]; const checkboxDisabledCheck = (item: FieldSelectionItem) => - (item.is_included === false && !item.reason?.includes('in excludes list')) || - item.is_required === true; + item.is_required === true || (item.reason && item.reason.includes('unsupported type')); -export const MemoizedAnalysisFieldsTable: FC<{ - excludes: string[]; +export const AnalysisFieldsTable: FC<{ + dependentVariable?: string; + includes: string[]; loadingItems: boolean; - setFormState: any; + setFormState: React.Dispatch>; tableItems: FieldSelectionItem[]; -}> = memo( - ({ excludes, loadingItems, setFormState, tableItems }) => { - const [sortableProperties, setSortableProperties] = useState(); - const [currentSelection, setCurrentSelection] = useState([]); +}> = ({ dependentVariable, includes, loadingItems, setFormState, tableItems }) => { + const [sortableProperties, setSortableProperties] = useState(); + const [currentPaginationData, setCurrentPaginationData] = useState<{ + pageIndex: number; + itemsPerPage: number; + }>({ pageIndex: 0, itemsPerPage: 5 }); + const [minimumFieldsRequiredMessage, setMinimumFieldsRequiredMessage] = useState< + undefined | string + >(undefined); - useEffect(() => { - if (excludes.length > 0) { - setCurrentSelection(excludes); - } - }, [tableItems]); + useEffect(() => { + if (includes.length === 0 && tableItems.length > 0) { + const includedFields: string[] = []; + tableItems.forEach((field) => { + if (field.is_included === true) { + includedFields.push(field.name); + } + }); + setFormState({ includes: includedFields }); + } else if (includes.length > 0) { + setFormState({ includes }); + } + setMinimumFieldsRequiredMessage(undefined); + }, [tableItems]); - // Only set form state on unmount to prevent re-renders due to props changing if exludes was updated on each selection - useEffect(() => { - return () => { - setFormState({ excludes: currentSelection }); - }; - }, [currentSelection]); + useEffect(() => { + let sortablePropertyItems = []; + const defaultSortProperty = 'name'; - useEffect(() => { - let sortablePropertyItems = []; - const defaultSortProperty = 'name'; + sortablePropertyItems = [ + { + name: 'name', + getValue: (item: any) => item.name.toLowerCase(), + isAscending: true, + }, + { + name: 'is_included', + getValue: (item: any) => item.is_included, + isAscending: true, + }, + { + name: 'is_required', + getValue: (item: any) => item.is_required, + isAscending: true, + }, + ]; + const sortableProps = new SortableProperties(sortablePropertyItems, defaultSortProperty); - sortablePropertyItems = [ - { - name: 'name', - getValue: (item: any) => item.name.toLowerCase(), - isAscending: true, - }, + setSortableProperties(sortableProps); + }, []); + + const filters = [ + { + type: 'field_value_toggle_group', + field: 'is_included', + items: [ { - name: 'is_included', - getValue: (item: any) => item.is_included, - isAscending: true, + value: true, + name: i18n.translate('xpack.ml.dataframe.analytics.create.isIncludedOption', { + defaultMessage: 'Is included', + }), }, { - name: 'is_required', - getValue: (item: any) => item.is_required, - isAscending: true, + value: false, + name: i18n.translate('xpack.ml.dataframe.analytics.create.isNotIncludedOption', { + defaultMessage: 'Is not included', + }), }, - ]; - const sortableProps = new SortableProperties(sortablePropertyItems, defaultSortProperty); - - setSortableProperties(sortableProps); - }, []); - - const filters = [ - { - type: 'field_value_selection', - field: 'is_included', - name: i18n.translate('xpack.ml.dataframe.analytics.create.excludedFilterLabel', { - defaultMessage: 'Is included', - }), - multiSelect: false, - options: [ - { - value: true, - view: ( - - {i18n.translate('xpack.ml.dataframe.analytics.create.isIncludedOption', { - defaultMessage: 'Yes', - })} - - ), - }, - { - value: false, - view: ( - - {i18n.translate('xpack.ml.dataframe.analytics.create.isNotIncludedOption', { - defaultMessage: 'No', - })} - - ), - }, - ], - }, - ]; + ], + }, + ]; - return ( - - + + + + {tableItems.length > 0 && minimumFieldsRequiredMessage === undefined && ( + + {i18n.translate('xpack.ml.dataframe.analytics.create.includedFieldsCount', { + defaultMessage: + '{numFields, plural, one {# field} other {# fields}} included in the analysis', + values: { numFields: includes.length }, + })} + + )} + {tableItems.length === 0 && ( + - - - {tableItems.length === 0 && ( - - - - )} - {tableItems.length > 0 && ( - - { - setCurrentSelection(selection); - }} - selectedIds={currentSelection} - singleSelection={false} - sortableProperties={sortableProperties} - tableItemId={'name'} - /> - - )} - - - ); - }, - (prevProps, nextProps) => prevProps.tableItems.length === nextProps.tableItems.length -); + + + )} + {tableItems.length > 0 && ( + + { + // dependent variable must always be in includes + if ( + dependentVariable !== undefined && + dependentVariable !== '' && + selection.length === 0 + ) { + selection = [dependentVariable]; + } + // If nothing selected show minimum fields required message and don't update form yet + if (selection.length === 0) { + setMinimumFieldsRequiredMessage(minimumFieldsMessage); + } else { + setMinimumFieldsRequiredMessage(undefined); + setFormState({ includes: selection }); + } + }} + selectedIds={includes} + setCurrentPaginationData={setCurrentPaginationData} + singleSelection={false} + sortableProperties={sortableProperties} + tableItemId={'name'} + /> + + )} + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step.tsx index 220910535aafe..d818117c9d784 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step.tsx @@ -19,17 +19,19 @@ export const ConfigurationStep: FC = ({ step, stepActivated, }) => { + const showForm = step === ANALYTICS_STEPS.CONFIGURATION; + const showDetails = step !== ANALYTICS_STEPS.CONFIGURATION && stepActivated === true; + + const dataTestSubj = `mlAnalyticsCreateJobWizardConfigurationStep${showForm ? ' active' : ''}${ + showDetails ? ' summary' : '' + }`; + return ( - - {step === ANALYTICS_STEPS.CONFIGURATION && ( + + {showForm && ( )} - {step !== ANALYTICS_STEPS.CONFIGURATION && stepActivated === true && ( - - )} + {showDetails && } ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_details.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_details.tsx index 6603af9aa302e..193d7dcce7f5e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_details.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_details.tsx @@ -21,6 +21,8 @@ import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics'; import { useMlContext } from '../../../../../contexts/ml'; import { ANALYTICS_STEPS } from '../../page'; +const MAX_INCLUDES_LENGTH = 5; + interface Props { setCurrentStep: React.Dispatch>; state: State; @@ -30,7 +32,7 @@ export const ConfigurationStepDetails: FC = ({ setCurrentStep, state }) = const mlContext = useMlContext(); const { currentIndexPattern } = mlContext; const { form, isJobCreated } = state; - const { dependentVariable, excludes, jobConfigQueryString, jobType, trainingPercent } = form; + const { dependentVariable, includes, jobConfigQueryString, jobType, trainingPercent } = form; const isJobTypeWithDepVar = jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION; @@ -61,10 +63,15 @@ export const ConfigurationStepDetails: FC = ({ setCurrentStep, state }) = const detailsThirdCol = [ { - title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.excludedFields', { - defaultMessage: 'Excluded fields', + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.includedFields', { + defaultMessage: 'Included fields', }), - description: excludes.length > 0 ? excludes.join(', ') : UNSET_CONFIG_ITEM, + description: + includes.length > MAX_INCLUDES_LENGTH + ? `${includes.slice(0, MAX_INCLUDES_LENGTH).join(', ')} ... (and ${ + includes.length - MAX_INCLUDES_LENGTH + } more)` + : includes.join(', '), }, ]; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx index 76378dc372f15..b83dd2e4329e0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx @@ -39,7 +39,7 @@ import { ANALYTICS_STEPS } from '../../page'; import { ContinueButton } from '../continue_button'; import { JobType } from './job_type'; import { SupportedFieldsMessage } from './supported_fields_message'; -import { MemoizedAnalysisFieldsTable } from './analysis_fields_table'; +import { AnalysisFieldsTable } from './analysis_fields_table'; import { DataGrid } from '../../../../../components/data_grid'; import { fetchExplainData } from '../shared'; import { useIndexData } from '../../hooks'; @@ -49,7 +49,8 @@ import { useSavedSearch } from './use_saved_search'; const requiredFieldsErrorText = i18n.translate( 'xpack.ml.dataframe.analytics.createWizard.requiredFieldsErrorMessage', { - defaultMessage: 'At least one field must be included in the analysis.', + defaultMessage: + 'At least one field must be included in the analysis in addition to the dependent variable.', } ); @@ -69,17 +70,20 @@ export const ConfigurationStepForm: FC = ({ const [dependentVariableOptions, setDependentVariableOptions] = useState< EuiComboBoxOptionOption[] >([]); - const [excludesTableItems, setExcludesTableItems] = useState([]); + const [includesTableItems, setIncludesTableItems] = useState([]); const [maxDistinctValuesError, setMaxDistinctValuesError] = useState( undefined ); + const [unsupportedFieldsError, setUnsupportedFieldsError] = useState( + undefined + ); const { setEstimatedModelMemoryLimit, setFormState } = actions; const { estimatedModelMemoryLimit, form, isJobCreated, requestMessages } = state; const firstUpdate = useRef(true); const { dependentVariable, - excludes, + includes, jobConfigQuery, jobConfigQueryString, jobType, @@ -117,7 +121,8 @@ export const ConfigurationStepForm: FC = ({ dependentVariableEmpty || jobType === undefined || maxDistinctValuesError !== undefined || - requiredFieldsError !== undefined; + requiredFieldsError !== undefined || + unsupportedFieldsError !== undefined; const loadDepVarOptions = async (formState: State['form']) => { setLoadingDepVarOptions(true); @@ -187,7 +192,8 @@ export const ConfigurationStepForm: FC = ({ setLoadingFieldOptions(false); setFieldOptionsFetchFail(false); setMaxDistinctValuesError(undefined); - setExcludesTableItems(fieldSelection ? fieldSelection : []); + setUnsupportedFieldsError(undefined); + setIncludesTableItems(fieldSelection ? fieldSelection : []); setFormState({ ...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: expectedMemory } : {}), requiredFieldsError: !hasRequiredFields ? requiredFieldsErrorText : undefined, @@ -200,6 +206,7 @@ export const ConfigurationStepForm: FC = ({ } } else { let maxDistinctValuesErrorMessage; + let unsupportedFieldsErrorMessage; if ( jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && errorMessage.includes('status_exception') && @@ -208,6 +215,10 @@ export const ConfigurationStepForm: FC = ({ maxDistinctValuesErrorMessage = errorMessage; } + if (errorMessage.includes('status_exception') && errorMessage.includes('unsupported type')) { + unsupportedFieldsErrorMessage = errorMessage; + } + if ( errorMessage.includes('status_exception') && errorMessage.includes('Unable to estimate memory usage as no documents') @@ -231,6 +242,7 @@ export const ConfigurationStepForm: FC = ({ setLoadingFieldOptions(false); setFieldOptionsFetchFail(true); setMaxDistinctValuesError(maxDistinctValuesErrorMessage); + setUnsupportedFieldsError(unsupportedFieldsErrorMessage); setFormState({ ...(shouldUpdateModelMemoryLimit ? { modelMemoryLimit: fallbackModelMemoryLimit } : {}), }); @@ -267,7 +279,7 @@ export const ConfigurationStepForm: FC = ({ return () => { debouncedGetExplainData.cancel(); }; - }, [jobType, dependentVariable, trainingPercent, JSON.stringify(excludes), jobConfigQueryString]); + }, [jobType, dependentVariable, trainingPercent, JSON.stringify(includes), jobConfigQueryString]); return ( @@ -392,21 +404,32 @@ export const ConfigurationStepForm: FC = ({ )} - diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx index f31c9cd28f65a..da547ee6255a1 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx @@ -71,7 +71,7 @@ export const JobType: FC = ({ type, setFormState }) => { setFormState({ previousJobType: type, jobType: value, - excludes: [], + includes: [], requiredFieldsError: undefined, }); }} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx index 0d1690cf17946..8ad49b84134cb 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/create_step.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useState } from 'react'; +import React, { FC, useState } from 'react'; import { EuiButton, EuiCheckbox, @@ -45,7 +45,7 @@ export const CreateStep: FC = ({ actions, state, step }) => { }; return ( - +
{!isJobCreated && !isJobStarted && ( @@ -88,6 +88,6 @@ export const CreateStep: FC = ({ actions, state, step }) => { {isJobCreated === true && showProgress && } {isJobCreated === true && } - +
); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step.tsx index a40813ed2fc3e..2e027b7b67e50 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step.tsx @@ -19,14 +19,19 @@ export const DetailsStep: FC = ({ step, stepActivated, }) => { + const showForm = step === ANALYTICS_STEPS.DETAILS; + const showDetails = step !== ANALYTICS_STEPS.DETAILS && stepActivated === true; + + const dataTestSubj = `mlAnalyticsCreateJobWizardDetailsStep${showForm ? ' active' : ''}${ + showDetails ? ' summary' : '' + }`; + return ( - - {step === ANALYTICS_STEPS.DETAILS && ( + + {showForm && ( )} - {step !== ANALYTICS_STEPS.DETAILS && stepActivated === true && ( - - )} + {showDetails && } ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx index e821428890046..04dd25896d443 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx @@ -109,7 +109,6 @@ export const Page: FC = ({ jobId }) => { /> ), status: currentStep >= ANALYTICS_STEPS.ADVANCED ? undefined : ('incomplete' as EuiStepStatus), - 'data-test-subj': 'mlAnalyticsCreateJobWizardAdvancedStep', }, { title: i18n.translate('xpack.ml.dataframe.analytics.creation.detailsStepTitle', { @@ -124,7 +123,6 @@ export const Page: FC = ({ jobId }) => { /> ), status: currentStep >= ANALYTICS_STEPS.DETAILS ? undefined : ('incomplete' as EuiStepStatus), - 'data-test-subj': 'mlAnalyticsCreateJobWizardDetailsStep', }, { title: i18n.translate('xpack.ml.dataframe.analytics.creation.createStepTitle', { @@ -132,7 +130,6 @@ export const Page: FC = ({ jobId }) => { }), children: , status: currentStep >= ANALYTICS_STEPS.CREATE ? undefined : ('incomplete' as EuiStepStatus), - 'data-test-subj': 'mlAnalyticsCreateJobWizardCreateStep', }, ]; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts index 01d92d8e192c1..4227c19fec5af 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.test.ts @@ -64,7 +64,7 @@ describe('Analytics job clone action', () => { }, analyzed_fields: { includes: [], - excludes: ['id', 'outlier'], + excludes: [], }, model_memory_limit: '1mb', allow_lazy_start: false, @@ -96,7 +96,7 @@ describe('Analytics job clone action', () => { }, }, analyzed_fields: { - includes: [], + includes: ['included_field', 'other_included_field'], excludes: [], }, model_memory_limit: '150mb', @@ -140,6 +140,40 @@ describe('Analytics job clone action', () => { expect(isAdvancedConfig(advancedClassificationJob)).toBe(true); }); + test('should detect advanced classification job with excludes set', () => { + const advancedClassificationJob = { + description: "Classification job with 'bank-marketing' dataset", + source: { + index: ['bank-marketing'], + query: { + match_all: {}, + }, + }, + dest: { + index: 'dest_bank_1', + results_field: 'ml', + }, + analysis: { + classification: { + dependent_variable: 'y', + num_top_classes: 2, + num_top_feature_importance_values: 4, + prediction_field_name: 'y_prediction', + training_percent: 2, + randomize_seed: 6233212276062807000, + }, + }, + analyzed_fields: { + includes: [], + excludes: ['excluded_field', 'other_excluded_field'], + }, + model_memory_limit: '350mb', + allow_lazy_start: false, + }; + + expect(isAdvancedConfig(advancedClassificationJob)).toBe(true); + }); + test('should detect advanced regression job', () => { const advancedRegressionJob = { description: "Outlier detection job with 'glass' dataset", @@ -161,7 +195,7 @@ describe('Analytics job clone action', () => { }, analyzed_fields: { includes: [], - excludes: ['id', 'outlier'], + excludes: [], }, model_memory_limit: '1mb', allow_lazy_start: false, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx index f184c7c5d874e..bff54bc283296 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx @@ -217,11 +217,11 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo analyzed_fields: { excludes: { optional: true, - formKey: 'excludes', defaultValue: [], }, includes: { optional: true, + formKey: 'includes', defaultValue: [], }, }, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index 8bace7b4f5952..81d35679443b8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -116,7 +116,7 @@ export const validateNumTopFeatureImportanceValues = ( }; export const validateAdvancedEditor = (state: State): State => { - const { jobIdEmpty, jobIdValid, jobIdExists, jobType, createIndexPattern, excludes } = state.form; + const { jobIdEmpty, jobIdValid, jobIdExists, jobType, createIndexPattern, includes } = state.form; const { jobConfig } = state; state.advancedEditorMessages = []; @@ -152,7 +152,7 @@ export const validateAdvancedEditor = (state: State): State => { } let dependentVariableEmpty = false; - let excludesValid = true; + let includesValid = true; let trainingPercentValid = true; let numTopFeatureImportanceValuesValid = true; @@ -170,14 +170,19 @@ export const validateAdvancedEditor = (state: State): State => { const dependentVariableName = getDependentVar(jobConfig.analysis) || ''; dependentVariableEmpty = dependentVariableName === ''; - if (!dependentVariableEmpty && excludes.includes(dependentVariableName)) { - excludesValid = false; + if ( + !dependentVariableEmpty && + includes !== undefined && + includes.length > 0 && + !includes.includes(dependentVariableName) + ) { + includesValid = false; state.advancedEditorMessages.push({ error: i18n.translate( - 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.excludesInvalid', + 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.includesInvalid', { - defaultMessage: 'The dependent variable cannot be excluded.', + defaultMessage: 'The dependent variable must be included.', } ), message: '', @@ -321,7 +326,7 @@ export const validateAdvancedEditor = (state: State): State => { state.form.destinationIndexPatternTitleExists = destinationIndexPatternTitleExists; state.isValid = - excludesValid && + includesValid && trainingPercentValid && state.form.modelMemoryLimitUnitValid && !jobIdEmpty && diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts index b9a9caadcebd0..d397dfc315da4 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.test.ts @@ -42,6 +42,37 @@ const regJobConfig = { allow_lazy_start: false, }; +const outlierJobConfig = { + id: 'outlier-test-01', + description: 'outlier test job description', + source: { + index: ['outlier-test-index'], + query: { + match_all: {}, + }, + }, + dest: { + index: 'outlier-test-01-index', + results_field: 'ml', + }, + analysis: { + outlier_detection: { + feature_influence_threshold: 0.01, + outlier_fraction: 0.05, + compute_feature_influence: false, + method: 'lof', + }, + }, + analyzed_fields: { + includes: ['field', 'other_field'], + excludes: [], + }, + model_memory_limit: '22mb', + create_time: 1590514291395, + version: '8.0.0', + allow_lazy_start: false, +}; + describe('useCreateAnalyticsForm', () => { test('state: getJobConfigFromFormState()', () => { const state = getInitialState(); @@ -53,8 +84,8 @@ describe('useCreateAnalyticsForm', () => { expect(jobConfig?.dest?.index).toBe('the-destination-index'); expect(jobConfig?.source?.index).toBe('the-source-index'); - expect(jobConfig?.analyzed_fields?.excludes).toStrictEqual([]); - expect(typeof jobConfig?.analyzed_fields?.includes).toBe('undefined'); + expect(jobConfig?.analyzed_fields?.includes).toStrictEqual([]); + expect(typeof jobConfig?.analyzed_fields?.excludes).toBe('undefined'); // test the conversion of comma-separated Kibana index patterns to ES array based index patterns state.form.sourceIndex = 'the-source-index-1,the-source-index-2'; @@ -65,11 +96,11 @@ describe('useCreateAnalyticsForm', () => { ]); }); - test('state: getCloneFormStateFromJobConfig()', () => { + test('state: getCloneFormStateFromJobConfig() regression', () => { const clonedState = getCloneFormStateFromJobConfig(regJobConfig); expect(clonedState?.sourceIndex).toBe('reg-test-index'); - expect(clonedState?.excludes).toStrictEqual([]); + expect(clonedState?.includes).toStrictEqual([]); expect(clonedState?.dependentVariable).toBe('price'); expect(clonedState?.numTopFeatureImportanceValues).toBe(2); expect(clonedState?.predictionFieldName).toBe('airbnb_test'); @@ -80,4 +111,19 @@ describe('useCreateAnalyticsForm', () => { expect(clonedState?.destinationIndex).toBe(undefined); expect(clonedState?.jobId).toBe(undefined); }); + + test('state: getCloneFormStateFromJobConfig() outlier detection', () => { + const clonedState = getCloneFormStateFromJobConfig(outlierJobConfig); + + expect(clonedState?.sourceIndex).toBe('outlier-test-index'); + expect(clonedState?.includes).toStrictEqual(['field', 'other_field']); + expect(clonedState?.featureInfluenceThreshold).toBe(0.01); + expect(clonedState?.outlierFraction).toBe(0.05); + expect(clonedState?.computeFeatureInfluence).toBe(false); + expect(clonedState?.method).toBe('lof'); + expect(clonedState?.modelMemoryLimit).toBe('22mb'); + // destination index and job id should be undefined + expect(clonedState?.destinationIndex).toBe(undefined); + expect(clonedState?.jobId).toBe(undefined); + }); }); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 241866b56c5c8..da6e2e440a26e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -7,11 +7,8 @@ import { DeepPartial, DeepReadonly } from '../../../../../../../common/types/common'; import { checkPermission } from '../../../../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../../../../ml_nodes_check'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; import { - isClassificationAnalysis, - isRegressionAnalysis, DataFrameAnalyticsId, DataFrameAnalyticsConfig, ANALYSIS_CONFIG_TYPE, @@ -57,10 +54,10 @@ export interface State { destinationIndexNameValid: boolean; destinationIndexPatternTitleExists: boolean; eta: undefined | number; - excludes: string[]; featureBagFraction: undefined | number; featureInfluenceThreshold: undefined | number; gamma: undefined | number; + includes: string[]; jobId: DataFrameAnalyticsId; jobIdExists: boolean; jobIdEmpty: boolean; @@ -122,10 +119,10 @@ export const getInitialState = (): State => ({ destinationIndexNameValid: false, destinationIndexPatternTitleExists: false, eta: undefined, - excludes: [], featureBagFraction: undefined, featureInfluenceThreshold: undefined, gamma: undefined, + includes: [], jobId: '', jobIdExists: false, jobIdEmpty: true, @@ -175,55 +172,6 @@ export const getInitialState = (): State => ({ estimatedModelMemoryLimit: '', }); -const getExcludesFields = (excluded: string[]) => { - const { fields } = newJobCapsService; - const updatedExcluded: string[] = []; - // Loop through excluded fields to check for multiple types of same field - for (let i = 0; i < excluded.length; i++) { - const fieldName = excluded[i]; - let mainField; - - // No dot in fieldName - it is the main field - if (fieldName.includes('.') === false) { - mainField = fieldName; - } else { - // Dot in fieldName - check if there's a field whose name equals the fieldName with the last dot suffix removed - const regex = /\.[^.]*$/; - const suffixRemovedField = fieldName.replace(regex, ''); - const fieldMatch = newJobCapsService.getFieldById(suffixRemovedField); - - // There's a match - set as the main field - if (fieldMatch !== null) { - mainField = suffixRemovedField; - } else { - // No main field to be found - add the fieldName to updatedExcluded array if it's not already there - if (updatedExcluded.includes(fieldName) === false) { - updatedExcluded.push(fieldName); - } - } - } - - if (mainField !== undefined) { - // Add the main field to the updatedExcluded array if it's not already there - if (updatedExcluded.includes(mainField) === false) { - updatedExcluded.push(mainField); - } - // Create regex to find all other fields whose names begin with main field followed by a dot - const regex = new RegExp(`${mainField}\\..+`); - - // Loop through fields and add fields matching the pattern to updatedExcluded array - for (let j = 0; j < fields.length; j++) { - const field = fields[j].name; - if (updatedExcluded.includes(field) === false && field.match(regex) !== null) { - updatedExcluded.push(field); - } - } - } - } - - return updatedExcluded; -}; - export const getJobConfigFromFormState = ( formState: State['form'] ): DeepPartial => { @@ -242,7 +190,7 @@ export const getJobConfigFromFormState = ( index: formState.destinationIndex, }, analyzed_fields: { - excludes: getExcludesFields(formState.excludes), + includes: formState.includes, }, analysis: { outlier_detection: {}, @@ -333,21 +281,16 @@ export function getCloneFormStateFromJobConfig( ? analyticsJobConfig.source.index.join(',') : analyticsJobConfig.source.index, modelMemoryLimit: analyticsJobConfig.model_memory_limit, - excludes: analyticsJobConfig.analyzed_fields.excludes, + includes: analyticsJobConfig.analyzed_fields.includes, }; - if ( - isRegressionAnalysis(analyticsJobConfig.analysis) || - isClassificationAnalysis(analyticsJobConfig.analysis) - ) { - const analysisConfig = analyticsJobConfig.analysis[jobType]; + const analysisConfig = analyticsJobConfig.analysis[jobType]; - for (const key in analysisConfig) { - if (analysisConfig.hasOwnProperty(key)) { - const camelCased = toCamelCase(key); - // @ts-ignore - resultState[camelCased] = analysisConfig[key]; - } + for (const key in analysisConfig) { + if (analysisConfig.hasOwnProperty(key)) { + const camelCased = toCamelCase(key); + // @ts-ignore + resultState[camelCased] = analysisConfig[key]; } } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 51c6b33579f50..d6f9cd383ae93 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9564,7 +9564,6 @@ "xpack.ml.dataframe.analytics.create.advancedEditorMessage.destinationIndexNameEmpty": "デスティネーションインデックス名は未入力のままにできません。", "xpack.ml.dataframe.analytics.create.advancedEditorMessage.destinationIndexNameExistsWarn": "この対象インデックス名のインデックスは既に存在します。この分析ジョブを実行すると、デスティネーションインデックスが変更されます。", "xpack.ml.dataframe.analytics.create.advancedEditorMessage.destinationIndexNameValid": "無効なデスティネーションインデックス名。", - "xpack.ml.dataframe.analytics.create.advancedEditorMessage.excludesInvalid": "従属変数を除外できません。", "xpack.ml.dataframe.analytics.create.advancedEditorMessage.modelMemoryLimitEmpty": "モデルメモリー制限フィールドを空にすることはできません。", "xpack.ml.dataframe.analytics.create.advancedEditorMessage.numTopFeatureImportanceValuesInvalid": "num_top_feature_importance_valuesの値は整数の{min}以上でなければなりません。", "xpack.ml.dataframe.analytics.create.advancedEditorMessage.sourceIndexNameEmpty": "ソースインデックス名は未入力のままにできません。", @@ -9591,7 +9590,6 @@ "xpack.ml.dataframe.analytics.create.errorGettingDataFrameIndexNames": "既存のインデックス名の取得中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.errorGettingIndexPatternTitles": "既存のインデックスパターンのタイトルの取得中にエラーが発生しました。", "xpack.ml.dataframe.analytics.create.errorStartingDataFrameAnalyticsJob": "データフレーム分析ジョブの開始中にエラーが発生しました。", - "xpack.ml.dataframe.analytics.create.excludedFieldsLabel": "除外されたフィールド", "xpack.ml.dataframe.analytics.create.indexPatternAlreadyExistsError": "このタイトルのインデックスパターンが既に存在します。", "xpack.ml.dataframe.analytics.create.indexPatternExistsError": "このタイトルのインデックスパターンが既に存在します。", "xpack.ml.dataframe.analytics.create.jobDescription.helpText": "オプションの説明テキストです", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8121df6d05090..235f8203608d4 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9568,7 +9568,6 @@ "xpack.ml.dataframe.analytics.create.advancedEditorMessage.destinationIndexNameEmpty": "目标索引名称不得为空。", "xpack.ml.dataframe.analytics.create.advancedEditorMessage.destinationIndexNameExistsWarn": "具有此目标索引名称的索引已存在。请注意,运行此分析作业将会修改此目标索引。", "xpack.ml.dataframe.analytics.create.advancedEditorMessage.destinationIndexNameValid": "目标索引名称无效。", - "xpack.ml.dataframe.analytics.create.advancedEditorMessage.excludesInvalid": "无法排除依赖变量。", "xpack.ml.dataframe.analytics.create.advancedEditorMessage.modelMemoryLimitEmpty": "模型内存限制字段不得为空。", "xpack.ml.dataframe.analytics.create.advancedEditorMessage.numTopFeatureImportanceValuesInvalid": "num_top_feature_importance_values 的值必须是 {min} 或更高的整数。", "xpack.ml.dataframe.analytics.create.advancedEditorMessage.sourceIndexNameEmpty": "源索引名称不得为空。", @@ -9595,7 +9594,6 @@ "xpack.ml.dataframe.analytics.create.errorGettingDataFrameIndexNames": "获取现有索引名称时发生错误:", "xpack.ml.dataframe.analytics.create.errorGettingIndexPatternTitles": "获取现有索引模式标题时发生错误:", "xpack.ml.dataframe.analytics.create.errorStartingDataFrameAnalyticsJob": "启动数据帧分析作业时发生错误:", - "xpack.ml.dataframe.analytics.create.excludedFieldsLabel": "排除的字段", "xpack.ml.dataframe.analytics.create.indexPatternAlreadyExistsError": "具有此名称的索引模式已存在。", "xpack.ml.dataframe.analytics.create.indexPatternExistsError": "具有此名称的索引模式已存在。", "xpack.ml.dataframe.analytics.create.jobDescription.helpText": "可选的描述文本", diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts index 63b4ad3a8668b..4a79610cadbde 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts @@ -66,6 +66,7 @@ export default function ({ getService }: FtrProviderContext) { it('selects the source data and loads the job wizard page', async () => { await ml.jobSourceSelection.selectSourceForAnalyticsJob(testData.source); + await ml.dataFrameAnalyticsCreation.assertConfigurationStepActive(); }); it('selects the job type', async () => { @@ -83,6 +84,14 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsCreation.setTrainingPercent(testData.trainingPercent); }); + it('displays the source data preview', async () => { + await ml.dataFrameAnalyticsCreation.assertSourceDataPreviewExists(); + }); + + it('displays the include fields selection', async () => { + await ml.dataFrameAnalyticsCreation.assertIncludeFieldsSelectionExists(); + }); + it('continues to the additional options step', async () => { await ml.dataFrameAnalyticsCreation.continueToAdditionalOptionsStep(); }); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts index 525e25d0158bf..068ef48b095e1 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts @@ -45,6 +45,7 @@ export default function ({ getService }: FtrProviderContext) { }, analysis: { classification: { + prediction_field_name: 'test', dependent_variable: 'y', training_percent: 20, }, @@ -107,6 +108,7 @@ export default function ({ getService }: FtrProviderContext) { }, analysis: { regression: { + prediction_field_name: 'test', dependent_variable: 'stab', training_percent: 20, }, @@ -157,9 +159,9 @@ export default function ({ getService }: FtrProviderContext) { }); it('should open the wizard with a proper header', async () => { - expect(await ml.dataFrameAnalyticsCreation.getHeaderText()).to.match( - /Clone analytics job/ - ); + const headerText = await ml.dataFrameAnalyticsCreation.getHeaderText(); + expect(headerText).to.match(/Clone job/); + await ml.dataFrameAnalyticsCreation.assertConfigurationStepActive(); }); it('should have correct init form values for config step', async () => { @@ -174,7 +176,7 @@ export default function ({ getService }: FtrProviderContext) { it('should have correct init form values for additional options step', async () => { await ml.dataFrameAnalyticsCreation.assertInitialCloneJobAdditionalOptionsStep( - testData.job as DataFrameAnalyticsConfig + testData.job.analysis as DataFrameAnalyticsConfig['analysis'] ); }); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts index cff59fa42abb0..0202c8431ce34 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts @@ -12,6 +12,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./outlier_detection_creation')); loadTestFile(require.resolve('./regression_creation')); loadTestFile(require.resolve('./classification_creation')); - // loadTestFile(require.resolve('./cloning')); + loadTestFile(require.resolve('./cloning')); }); } diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts index 582b19f5e18a8..500825f7d9d31 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts @@ -64,6 +64,7 @@ export default function ({ getService }: FtrProviderContext) { it('selects the source data and loads the job wizard page', async () => { await ml.jobSourceSelection.selectSourceForAnalyticsJob(testData.source); + await ml.dataFrameAnalyticsCreation.assertConfigurationStepActive(); }); it('selects the job type', async () => { @@ -79,6 +80,14 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsCreation.assertTrainingPercentInputMissing(); }); + it('displays the source data preview', async () => { + await ml.dataFrameAnalyticsCreation.assertSourceDataPreviewExists(); + }); + + it('displays the include fields selection', async () => { + await ml.dataFrameAnalyticsCreation.assertIncludeFieldsSelectionExists(); + }); + it('continues to the additional options step', async () => { await ml.dataFrameAnalyticsCreation.continueToAdditionalOptionsStep(); }); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts index c8a6e1c96c219..33f0ee9cd99ac 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts @@ -66,6 +66,7 @@ export default function ({ getService }: FtrProviderContext) { it('selects the source data and loads the job wizard page', async () => { await ml.jobSourceSelection.selectSourceForAnalyticsJob(testData.source); + await ml.dataFrameAnalyticsCreation.assertConfigurationStepActive(); }); it('selects the job type', async () => { @@ -83,6 +84,14 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsCreation.setTrainingPercent(testData.trainingPercent); }); + it('displays the source data preview', async () => { + await ml.dataFrameAnalyticsCreation.assertSourceDataPreviewExists(); + }); + + it('displays the include fields selection', async () => { + await ml.dataFrameAnalyticsCreation.assertIncludeFieldsSelectionExists(); + }); + it('continues to the additional options step', async () => { await ml.dataFrameAnalyticsCreation.continueToAdditionalOptionsStep(); }); diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts index f67ea583e25cd..918c982de02ed 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts @@ -124,13 +124,21 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( await this.assertJobDescriptionValue(jobDescription); }, - // async assertExcludedFieldsSelection(expectedSelection: string[]) { - // const actualSelection = await comboBox.getComboBoxSelectedOptions( - // 'mlAnalyticsCreateJobWizardExcludesSelect' - // ); + async assertSourceDataPreviewExists() { + await testSubjects.existOrFail('mlAnalyticsCreationDataGrid loaded', { timeout: 5000 }); + }, + + async assertIncludeFieldsSelectionExists() { + await testSubjects.existOrFail('mlAnalyticsCreateJobWizardIncludesSelect', { timeout: 5000 }); + }, + + // async assertIncludedFieldsSelection(expectedSelection: string[]) { + // const includesTable = await testSubjects.find('mlAnalyticsCreateJobWizardIncludesSelect'); + // const actualSelection = await includesTable.findByClassName('euiTableRow-isSelected'); + // expect(actualSelection).to.eql( // expectedSelection, - // `Excluded fields should be '${expectedSelection}' (got '${actualSelection}')` + // `Included fields should be '${expectedSelection}' (got '${actualSelection}')` // ); // }, @@ -252,19 +260,35 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( await this.assertTrainingPercentValue(trainingPercent); }, + async assertConfigurationStepActive() { + await testSubjects.existOrFail('mlAnalyticsCreateJobWizardConfigurationStep active'); + }, + + async assertAdditionalOptionsStepActive() { + await testSubjects.existOrFail('mlAnalyticsCreateJobWizardAdvancedStep active'); + }, + + async assertDetailsStepActive() { + await testSubjects.existOrFail('mlAnalyticsCreateJobWizardDetailsStep active'); + }, + + async assertCreateStepActive() { + await testSubjects.existOrFail('mlAnalyticsCreateJobWizardCreateStep active'); + }, + async continueToAdditionalOptionsStep() { - await testSubjects.click('mlAnalyticsCreateJobWizardContinueButton'); - await testSubjects.existOrFail('mlAnalyticsCreateJobWizardAdvancedStep'); + await testSubjects.clickWhenNotDisabled('mlAnalyticsCreateJobWizardContinueButton'); + await this.assertAdditionalOptionsStepActive(); }, async continueToDetailsStep() { - await testSubjects.click('mlAnalyticsCreateJobWizardContinueButton'); - await testSubjects.existOrFail('mlAnalyticsCreateJobWizardDetailsStep'); + await testSubjects.clickWhenNotDisabled('mlAnalyticsCreateJobWizardContinueButton'); + await this.assertDetailsStepActive(); }, async continueToCreateStep() { - await testSubjects.click('mlAnalyticsCreateJobWizardContinueButton'); - await testSubjects.existOrFail('mlAnalyticsCreateJobWizardCreateStep'); + await testSubjects.clickWhenNotDisabled('mlAnalyticsCreateJobWizardContinueButton'); + await this.assertCreateStepActive(); }, async assertModelMemoryInputExists() { @@ -282,6 +306,17 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( ); }, + async assertPredictionFieldNameValue(expectedValue: string) { + const actualPredictedFieldName = await testSubjects.getAttribute( + 'mlAnalyticsCreateJobWizardPredictionFieldNameInput', + 'value' + ); + expect(actualPredictedFieldName).to.eql( + expectedValue, + `Prediction field name should be '${expectedValue}' (got '${actualPredictedFieldName}')` + ); + }, + async setModelMemory(modelMemory: string) { await retry.tryForTime(15 * 1000, async () => { await mlCommon.setValueWithChecks( @@ -372,11 +407,19 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( await this.assertDependentVariableSelection([job.analysis[jobType].dependent_variable]); await this.assertTrainingPercentValue(String(job.analysis[jobType].training_percent)); } - // await this.assertExcludedFieldsSelection(job.analyzed_fields.excludes); - }, - - async assertInitialCloneJobAdditionalOptionsStep(job: DataFrameAnalyticsConfig) { - await this.assertModelMemoryValue(job.model_memory_limit); + await this.assertSourceDataPreviewExists(); + await this.assertIncludeFieldsSelectionExists(); + // await this.assertIncludedFieldsSelection(job.analyzed_fields.includes); + }, + + async assertInitialCloneJobAdditionalOptionsStep( + analysis: DataFrameAnalyticsConfig['analysis'] + ) { + const jobType = Object.keys(analysis)[0]; + if (isClassificationAnalysis(analysis) || isRegressionAnalysis(analysis)) { + // @ts-ignore + await this.assertPredictionFieldNameValue(analysis[jobType].prediction_field_name); + } }, async assertInitialCloneJobDetailsStep(job: DataFrameAnalyticsConfig) { From 93bae2284ce5b9ce108230f34bf9b66dc8a06cc3 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 6 Jul 2020 13:23:28 -0400 Subject: [PATCH 14/46] [ML] DF Analytics: adds prompt for destination index pattern creation (#70651) * add warning if create index not selected * create indexPrompt component and set needsDestIndexPattern * translation for prompt text and link * create indexPattern text to warning color --- .../common/use_results_view_config.ts | 3 ++ .../details_step/details_step_form.tsx | 43 +++++++++++++---- .../exploration_page_wrapper.tsx | 4 +- .../exploration_results_table.tsx | 12 ++++- .../components/index_pattern_prompt/index.ts | 7 +++ .../index_pattern_prompt.tsx | 48 +++++++++++++++++++ .../outlier_exploration.tsx | 6 ++- 7 files changed, 111 insertions(+), 12 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/index_pattern_prompt/index.ts create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/index_pattern_prompt/index_pattern_prompt.tsx diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts index 2570dd20416be..fde1b26106508 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts @@ -24,6 +24,7 @@ export const useResultsViewConfig = (jobId: string) => { const mlContext = useMlContext(); const [indexPattern, setIndexPattern] = useState(undefined); const [isInitialized, setIsInitialized] = useState(false); + const [needsDestIndexPattern, setNeedsDestIndexPattern] = useState(false); const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false); const [jobConfig, setJobConfig] = useState(undefined); const [jobCapsServiceErrorMessage, setJobCapsServiceErrorMessage] = useState( @@ -68,6 +69,7 @@ export const useResultsViewConfig = (jobId: string) => { } if (indexP === undefined) { + setNeedsDestIndexPattern(true); const sourceIndex = jobConfigUpdate.source.index[0]; const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); @@ -100,5 +102,6 @@ export const useResultsViewConfig = (jobId: string) => { jobConfig, jobConfigErrorMessage, jobStatus, + needsDestIndexPattern, }; }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx index 67f8472e7ad14..d846ae95c2c7e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx @@ -5,7 +5,15 @@ */ import React, { FC, Fragment, useRef } from 'react'; -import { EuiFieldText, EuiFormRow, EuiLink, EuiSpacer, EuiSwitch, EuiTextArea } from '@elastic/eui'; +import { + EuiFieldText, + EuiFormRow, + EuiLink, + EuiSpacer, + EuiSwitch, + EuiText, + EuiTextArea, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useMlKibana } from '../../../../../contexts/kibana'; @@ -188,15 +196,32 @@ export const DetailsStepForm: FC = ({ /> + {i18n.translate( + 'xpack.ml.dataframe.analytics.create.shouldCreateIndexPatternMessage', + { + defaultMessage: + 'You may not be able to view job results if an index pattern is not created for the destination index.', + } + )} + , + ] + : []), + ]} > = ({ jobId, title, EvaluatePanel jobConfig, jobConfigErrorMessage, jobStatus, + needsDestIndexPattern, } = useResultsViewConfig(jobId); const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); @@ -64,9 +65,10 @@ export const ExplorationPageWrapper: FC = ({ jobId, title, EvaluatePanel indexPattern !== undefined && isInitialized === true && ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx index 941fbefd78084..755bac699ce40 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx @@ -33,6 +33,7 @@ import { getTaskStateBadge } from '../../../analytics_management/components/anal import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; import { ExplorationTitle } from '../exploration_title'; import { ExplorationQueryBar } from '../exploration_query_bar'; +import { IndexPatternPrompt } from '../index_pattern_prompt'; import { useExplorationResults } from './use_exploration_results'; @@ -55,12 +56,20 @@ interface Props { indexPattern: IndexPattern; jobConfig: DataFrameAnalyticsConfig; jobStatus?: DATA_FRAME_TASK_STATE; + needsDestIndexPattern: boolean; setEvaluateSearchQuery: React.Dispatch>; title: string; } export const ExplorationResultsTable: FC = React.memo( - ({ indexPattern, jobConfig, jobStatus, setEvaluateSearchQuery, title }) => { + ({ + indexPattern, + jobConfig, + jobStatus, + needsDestIndexPattern, + setEvaluateSearchQuery, + title, + }) => { const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); useEffect(() => { @@ -119,6 +128,7 @@ export const ExplorationResultsTable: FC = React.memo( id="mlDataFrameAnalyticsTableResultsPanel" data-test-subj="mlDFAnalyticsExplorationTablePanel" > + {needsDestIndexPattern && } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/index_pattern_prompt/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/index_pattern_prompt/index.ts new file mode 100644 index 0000000000000..0b012794c9420 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/index_pattern_prompt/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 { IndexPatternPrompt } from './index_pattern_prompt'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/index_pattern_prompt/index_pattern_prompt.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/index_pattern_prompt/index_pattern_prompt.tsx new file mode 100644 index 0000000000000..f478dc639da2f --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/index_pattern_prompt/index_pattern_prompt.tsx @@ -0,0 +1,48 @@ +/* + * 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, { FC } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; +import { useMlKibana } from '../../../../../contexts/kibana'; + +interface Props { + destIndex: string; +} + +export const IndexPatternPrompt: FC = ({ destIndex }) => { + const { + services: { + http: { basePath }, + }, + } = useMlKibana(); + + return ( + <> + + + + + ), + }} + /> + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx index 0b29b7f43bfc8..9afb50c11fad7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx @@ -33,6 +33,7 @@ import { getTaskStateBadge } from '../../../analytics_management/components/anal import { ExplorationQueryBar } from '../exploration_query_bar'; import { ExplorationTitle } from '../exploration_title'; +import { IndexPatternPrompt } from '../index_pattern_prompt'; import { getFeatureCount } from './common'; import { useOutlierData } from './use_outlier_data'; @@ -49,7 +50,7 @@ export const OutlierExploration: FC = React.memo(({ jobId }) = values: { jobId }, }); - const { indexPattern, jobConfig, jobStatus } = useResultsViewConfig(jobId); + const { indexPattern, jobConfig, jobStatus, needsDestIndexPattern } = useResultsViewConfig(jobId); const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); const outlierData = useOutlierData(indexPattern, jobConfig, searchQuery); @@ -82,6 +83,9 @@ export const OutlierExploration: FC = React.memo(({ jobId }) = return ( + {jobConfig !== undefined && needsDestIndexPattern && ( + + )} Date: Mon, 6 Jul 2020 10:36:44 -0700 Subject: [PATCH 15/46] docs: add annotation user docs (#70265) --- docs/apm/api.asciidoc | 1 + docs/apm/apm-app-users.asciidoc | 50 ++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/docs/apm/api.asciidoc b/docs/apm/api.asciidoc index 54159b642dd1a..2fbeea0534fc0 100644 --- a/docs/apm/api.asciidoc +++ b/docs/apm/api.asciidoc @@ -355,6 +355,7 @@ allowing you to easily see how these events are impacting the performance of you By default, annotations are stored in a newly created `observability-annotations` index. The name of this index can be changed in your `config.yml` by editing `xpack.observability.annotations.index`. +If you change the default index name, you'll also need to <> accordingly. The following APIs are available: diff --git a/docs/apm/apm-app-users.asciidoc b/docs/apm/apm-app-users.asciidoc index 442a07d279725..d766c866f87e4 100644 --- a/docs/apm/apm-app-users.asciidoc +++ b/docs/apm/apm-app-users.asciidoc @@ -4,7 +4,7 @@ :beat_default_index_prefix: apm :beat_kib_app: APM app -:annotation_index: `observability-annotations` +:annotation_index: observability-annotations ++++ Users and privileges @@ -102,6 +102,54 @@ Here are two examples: *********************************** *********************************** //// +[role="xpack"] +[[apm-app-annotation-user-create]] +=== APM app annotation user + +++++ +Create an annotation user +++++ + +NOTE: By default, the `apm_user` built-in role provides access to Observability annotations. +You only need to create an annotation user if the default annotation index +defined in <> has been customized. + +[[apm-app-annotation-user]] +==== Annotation user + +View deployment annotations in the APM app. + +. Create a new role, named something like `annotation_user`, +and assign the following privileges: ++ +[options="header"] +|==== +|Type | Privilege | Purpose + +|Index +|`read` on +\{ANNOTATION_INDEX\}+^1^ +|Read-only access to the observability annotation index + +|Index +|`view_index_metadata` on +\{ANNOTATION_INDEX\}+^1^ +|Read-only access to observability annotation index metadata +|==== ++ +^1^ +\{ANNOTATION_INDEX\}+ should be the index name you've defined in +<>. + +. Assign the `annotation_user` created previously, and the built-in roles necessary to create +a <> or <> APM reader to any users that need to view annotations in the APM app + +[[apm-app-annotation-api]] +==== Annotation API + +See <>. + +//// +*********************************** *********************************** +//// + [role="xpack"] [[apm-app-central-config-user]] === APM app central config user From 21af99c9b986fc5625471d9e862cb8addcbefb16 Mon Sep 17 00:00:00 2001 From: Octavio Ranieri <60898133+octavioranieri@users.noreply.github.com> Date: Mon, 6 Jul 2020 14:49:56 -0300 Subject: [PATCH 16/46] [Canvas] Fix falsey/null value bug for dropdown choices (#69290) * Fixed falsey/null value bug for dropdown choices * Filter only null and undefined values Co-authored-by: Elastic Machine --- .../canvas_plugin_src/functions/common/dropdownControl.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdownControl.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdownControl.ts index 7231f01671e02..74a9061b5df2d 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdownControl.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdownControl.ts @@ -52,8 +52,12 @@ export function dropdownControl(): ExpressionFunctionDefinition< fn: (input, { valueColumn, filterColumn, filterGroup }) => { let choices = []; - if (input.rows[0][valueColumn]) { - choices = uniq(input.rows.map((row) => row[valueColumn])).sort(); + const filteredRows = input.rows.filter( + (row) => row[valueColumn] !== null && row[valueColumn] !== undefined + ); + + if (filteredRows.length > 0) { + choices = uniq(filteredRows.map((row) => row[valueColumn])).sort(); } const column = filterColumn || valueColumn; From bd952721a4791da8cd7c8d9cfdfa1e6660cf9fa8 Mon Sep 17 00:00:00 2001 From: Diana Derevyankina <54894989+DziyanaDzeraviankina@users.noreply.github.com> Date: Mon, 6 Jul 2020 21:02:26 +0300 Subject: [PATCH 17/46] Convert tag cloud tests to jest (#70066) * Convert tag cloud tests to jest * Add mocks to test_utils and remove tests from legacy * Revert changes made by accident * Update tag_cloud_visualization.test.js * Update tag_cloud.test.js * Update jsdom_svg_mocks.ts * Add restoring previous value to window.SVGElement.prototype.transform * Get rid of some deep imports * Reimport jsdom_svg_mocks functions from test_utils/public * Get rid of ExprVis by inlining some of its params to vis object Co-authored-by: Elastic Machine Co-authored-by: Alexey Antonov --- package.json | 3 +- .../vis_type_tagcloud/afterparamchange.png | Bin 11622 -> 0 bytes .../vis_type_tagcloud/afterresize.png | Bin 9012 -> 0 bytes .../__tests__/vis_type_tagcloud/basicdraw.png | Bin 12964 -> 0 bytes .../vis_type_tagcloud/simpleload.png | Bin 10359 -> 0 bytes .../tag_cloud_visualization.js | 202 --------------- .../__snapshots__/tag_cloud.test.js.snap | 3 + .../tag_cloud_visualization.test.js.snap | 7 + .../public/components/tag_cloud.test.js} | 231 ++++++++---------- .../tag_cloud_visualization.test.js | 176 +++++++++++++ src/test_utils/public/helpers/index.ts | 2 + .../public/helpers/jsdom_svg_mocks.ts | 57 +++++ src/test_utils/public/index.ts | 20 ++ yarn.lock | 25 ++ 14 files changed, 395 insertions(+), 331 deletions(-) delete mode 100644 src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/afterparamchange.png delete mode 100644 src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/afterresize.png delete mode 100644 src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/basicdraw.png delete mode 100644 src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/simpleload.png delete mode 100644 src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud_visualization.js create mode 100644 src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap create mode 100644 src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap rename src/{legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud.js => plugins/vis_type_tagcloud/public/components/tag_cloud.test.js} (72%) create mode 100644 src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.test.js create mode 100644 src/test_utils/public/helpers/jsdom_svg_mocks.ts create mode 100644 src/test_utils/public/index.ts diff --git a/package.json b/package.json index 8e51f9207eaf1..2f6b643b02601 100644 --- a/package.json +++ b/package.json @@ -455,9 +455,10 @@ "is-path-inside": "^2.1.0", "istanbul-instrumenter-loader": "3.0.1", "jest": "^25.5.4", - "jest-environment-jsdom-thirteen": "^1.0.1", + "jest-canvas-mock": "^2.2.0", "jest-circus": "^25.5.4", "jest-cli": "^25.5.4", + "jest-environment-jsdom-thirteen": "^1.0.1", "jest-raw-loader": "^1.0.1", "jimp": "^0.9.6", "json5": "^1.0.1", diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/afterparamchange.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/afterparamchange.png deleted file mode 100644 index bc41213edc7b60126bad788f5af94d995fa1a4cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11622 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 O8cW?999lx{14?i>yU_n~ok=k&Gcnr$v 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 diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/basicdraw.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/basicdraw.png deleted file mode 100644 index 3716867865e4430d4080d00f0c0e16b2b67ebc91..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12964 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 diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/simpleload.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/simpleload.png deleted file mode 100644 index 6ea090562d46e774fcbd12682cccd36f39bb791f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10359 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 Z>~6C!wyQT20TvVjmn`iO73a|p{u5H(=8pgX diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud_visualization.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud_visualization.js deleted file mode 100644 index 4a6e9e7765213..0000000000000 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud_visualization.js +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import { ImageComparator } from 'test_utils/image_comparator'; -import basicdrawPng from './basicdraw.png'; -import afterresizePng from './afterresize.png'; -import afterparamChange from './afterparamchange.png'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ExprVis } from '../../../../../../plugins/visualizations/public/expressions/vis'; - -// Replace with mock when converting to jest tests -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { seedColors } from '../../../../../../plugins/charts/public/services/colors/seed_colors'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { BaseVisType } from '../../../../../../plugins/visualizations/public/vis_types/base_vis_type'; -// Will be replaced with new path when tests are moved -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { createTagCloudVisTypeDefinition } from '../../../../../../plugins/vis_type_tagcloud/public/tag_cloud_type'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { createTagCloudVisualization } from '../../../../../../plugins/vis_type_tagcloud/public/components/tag_cloud_visualization'; -import { npStart } from 'ui/new_platform'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { setFormatService } from '../../../../../../plugins/vis_type_tagcloud/public/services'; - -const THRESHOLD = 0.65; -const PIXEL_DIFF = 64; -describe('TagCloudVisualizationTest', function () { - let domNode; - let vis; - let imageComparator; - - const dummyTableGroup = { - columns: [ - { - id: 'col-0', - title: 'geo.dest: Descending', - }, - { - id: 'col-1', - title: 'Count', - }, - ], - rows: [ - { 'col-0': 'CN', 'col-1': 26 }, - { 'col-0': 'IN', 'col-1': 17 }, - { 'col-0': 'US', 'col-1': 6 }, - { 'col-0': 'DE', 'col-1': 4 }, - { 'col-0': 'BR', 'col-1': 3 }, - ], - }; - const TagCloudVisualization = createTagCloudVisualization({ - colors: { - seedColors, - }, - }); - - before(() => setFormatService(npStart.plugins.data.fieldFormats)); - - beforeEach(ngMock.module('kibana')); - - describe('TagCloudVisualization - basics', function () { - beforeEach(async function () { - const visType = new BaseVisType(createTagCloudVisTypeDefinition({ colors: seedColors })); - setupDOM('512px', '512px'); - imageComparator = new ImageComparator(); - vis = new ExprVis({ - type: visType, - params: { - bucket: { accessor: 0, format: {} }, - metric: { accessor: 0, format: {} }, - }, - data: {}, - }); - }); - - afterEach(function () { - teardownDOM(); - imageComparator.destroy(); - }); - - it('simple draw', async function () { - const tagcloudVisualization = new TagCloudVisualization(domNode, vis); - - await tagcloudVisualization.render(dummyTableGroup, vis.params, { - resize: false, - params: true, - aggs: true, - data: true, - uiState: false, - }); - - const svgNode = domNode.querySelector('svg'); - const mismatchedPixels = await imageComparator.compareDOMContents( - svgNode.outerHTML, - 512, - 512, - basicdrawPng, - THRESHOLD - ); - expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF); - }); - - it('with resize', async function () { - const tagcloudVisualization = new TagCloudVisualization(domNode, vis); - await tagcloudVisualization.render(dummyTableGroup, vis.params, { - resize: false, - params: true, - aggs: true, - data: true, - uiState: false, - }); - - domNode.style.width = '256px'; - domNode.style.height = '368px'; - await tagcloudVisualization.render(dummyTableGroup, vis.params, { - resize: true, - params: false, - aggs: false, - data: false, - uiState: false, - }); - - const svgNode = domNode.querySelector('svg'); - const mismatchedPixels = await imageComparator.compareDOMContents( - svgNode.outerHTML, - 256, - 368, - afterresizePng, - THRESHOLD - ); - expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF); - }); - - it('with param change', async function () { - const tagcloudVisualization = new TagCloudVisualization(domNode, vis); - await tagcloudVisualization.render(dummyTableGroup, vis.params, { - resize: false, - params: true, - aggs: true, - data: true, - uiState: false, - }); - - domNode.style.width = '256px'; - domNode.style.height = '368px'; - vis.params.orientation = 'right angled'; - vis.params.minFontSize = 70; - await tagcloudVisualization.render(dummyTableGroup, vis.params, { - resize: true, - params: true, - aggs: false, - data: false, - uiState: false, - }); - - const svgNode = domNode.querySelector('svg'); - const mismatchedPixels = await imageComparator.compareDOMContents( - svgNode.outerHTML, - 256, - 368, - afterparamChange, - THRESHOLD - ); - expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF); - }); - }); - - function setupDOM(width, height) { - domNode = document.createElement('div'); - domNode.style.top = '0'; - domNode.style.left = '0'; - domNode.style.width = width; - domNode.style.height = height; - domNode.style.position = 'fixed'; - domNode.style.border = '1px solid blue'; - domNode.style['pointer-events'] = 'none'; - document.body.appendChild(domNode); - } - - function teardownDOM() { - domNode.innerHTML = ''; - document.body.removeChild(domNode); - } -}); diff --git a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap new file mode 100644 index 0000000000000..e32425a095429 --- /dev/null +++ b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`tag cloud tests tagcloudscreenshot should render simple image 1`] = `"foobarfoobar"`; diff --git a/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap new file mode 100644 index 0000000000000..dbc3dd1202cbd --- /dev/null +++ b/src/plugins/vis_type_tagcloud/public/components/__snapshots__/tag_cloud_visualization.test.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TagCloudVisualizationTest TagCloudVisualization - basics simple draw 1`] = `"CNINUSDEBR"`; + +exports[`TagCloudVisualizationTest TagCloudVisualization - basics with param change 1`] = `"CNINUSDEBR"`; + +exports[`TagCloudVisualizationTest TagCloudVisualization - basics with resize 1`] = `"CNINUSDEBR"`; diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.test.js similarity index 72% rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud.js rename to src/plugins/vis_type_tagcloud/public/components/tag_cloud.test.js index 35c7b77687b94..89a6a67bcb2fb 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud.js +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.test.js @@ -17,22 +17,27 @@ * under the License. */ -import expect from '@kbn/expect'; import _ from 'lodash'; import d3 from 'd3'; +import 'jest-canvas-mock'; import { fromNode, delay } from 'bluebird'; -import { ImageComparator } from 'test_utils/image_comparator'; -import simpleloadPng from './simpleload.png'; +import { TagCloud } from './tag_cloud'; +import { setHTMLElementOffset, setSVGElementGetBBox } from '../../../../test_utils/public'; -// Replace with mock when converting to jest tests -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { seedColors } from '../../../../../../plugins/charts/public/services/colors/seed_colors'; -// Will be replaced with new path when tests are moved -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TagCloud } from '../../../../../../plugins/vis_type_tagcloud/public/components/tag_cloud'; +describe('tag cloud tests', () => { + let SVGElementGetBBoxSpyInstance; + let HTMLElementOffsetMockInstance; + + beforeEach(() => { + setupDOM(); + }); + + afterEach(() => { + SVGElementGetBBoxSpyInstance.mockRestore(); + HTMLElementOffsetMockInstance.mockRestore(); + }); -describe('tag cloud tests', function () { const minValue = 1; const maxValue = 9; const midValue = (minValue + maxValue) / 2; @@ -100,16 +105,15 @@ describe('tag cloud tests', function () { let domNode; let tagCloud; - const colorScale = d3.scale.ordinal().range(seedColors); + const colorScale = d3.scale + .ordinal() + .range(['#00a69b', '#57c17b', '#6f87d8', '#663db8', '#bc52bc', '#9e3533', '#daa05d']); function setupDOM() { domNode = document.createElement('div'); - domNode.style.top = '0'; - domNode.style.left = '0'; - domNode.style.width = '512px'; - domNode.style.height = '512px'; - domNode.style.position = 'fixed'; - domNode.style['pointer-events'] = 'none'; + SVGElementGetBBoxSpyInstance = setSVGElementGetBBox(); + HTMLElementOffsetMockInstance = setHTMLElementOffset(512, 512); + document.body.appendChild(domNode); } @@ -126,42 +130,39 @@ describe('tag cloud tests', function () { sqrtScaleTest, biggerFontTest, trimDataTest, - ].forEach(function (test) { + ].forEach(function (currentTest) { describe(`should position elements correctly for options: ${JSON.stringify( - test.options - )}`, function () { - beforeEach(async function () { - setupDOM(); + currentTest.options + )}`, () => { + beforeEach(async () => { tagCloud = new TagCloud(domNode, colorScale); - tagCloud.setData(test.data); - tagCloud.setOptions(test.options); + tagCloud.setData(currentTest.data); + tagCloud.setOptions(currentTest.options); await fromNode((cb) => tagCloud.once('renderComplete', cb)); }); afterEach(teardownDOM); - it( + test( 'completeness should be ok', - handleExpectedBlip(function () { - expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.COMPLETE); + handleExpectedBlip(() => { + expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); }) ); - it( + test( 'positions should be ok', - handleExpectedBlip(function () { + handleExpectedBlip(() => { const textElements = domNode.querySelectorAll('text'); - verifyTagProperties(test.expected, textElements, tagCloud); + verifyTagProperties(currentTest.expected, textElements, tagCloud); }) ); }); }); - [5, 100, 200, 300, 500].forEach(function (timeout) { - describe(`should only send single renderComplete event at the very end, using ${timeout}ms timeout`, function () { - beforeEach(async function () { - setupDOM(); - + [5, 100, 200, 300, 500].forEach((timeout) => { + describe(`should only send single renderComplete event at the very end, using ${timeout}ms timeout`, () => { + beforeEach(async () => { //TagCloud takes at least 600ms to complete (due to d3 animation) //renderComplete should only notify at the last one tagCloud = new TagCloud(domNode, colorScale); @@ -176,16 +177,16 @@ describe('tag cloud tests', function () { afterEach(teardownDOM); - it( + test( 'completeness should be ok', - handleExpectedBlip(function () { - expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.COMPLETE); + handleExpectedBlip(() => { + expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); }) ); - it( + test( 'positions should be ok', - handleExpectedBlip(function () { + handleExpectedBlip(() => { const textElements = domNode.querySelectorAll('text'); verifyTagProperties(logScaleTest.expected, textElements, tagCloud); }) @@ -193,9 +194,8 @@ describe('tag cloud tests', function () { }); }); - describe('should use the latest state before notifying (when modifying options multiple times)', function () { - beforeEach(async function () { - setupDOM(); + describe('should use the latest state before notifying (when modifying options multiple times)', () => { + beforeEach(async () => { tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(baseTest.data); tagCloud.setOptions(baseTest.options); @@ -205,53 +205,53 @@ describe('tag cloud tests', function () { afterEach(teardownDOM); - it( + test( 'completeness should be ok', - handleExpectedBlip(function () { - expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.COMPLETE); + handleExpectedBlip(() => { + expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); }) ); - it( + test( 'positions should be ok', - handleExpectedBlip(function () { + handleExpectedBlip(() => { const textElements = domNode.querySelectorAll('text'); verifyTagProperties(logScaleTest.expected, textElements, tagCloud); }) ); }); - describe('should use the latest state before notifying (when modifying data multiple times)', function () { - beforeEach(async function () { - setupDOM(); + describe('should use the latest state before notifying (when modifying data multiple times)', () => { + beforeEach(async () => { tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(baseTest.data); tagCloud.setOptions(baseTest.options); tagCloud.setData(trimDataTest.data); + await fromNode((cb) => tagCloud.once('renderComplete', cb)); }); afterEach(teardownDOM); - it( + test( 'completeness should be ok', - handleExpectedBlip(function () { - expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.COMPLETE); + handleExpectedBlip(() => { + expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); }) ); - it( + test( 'positions should be ok', - handleExpectedBlip(function () { + handleExpectedBlip(() => { const textElements = domNode.querySelectorAll('text'); verifyTagProperties(trimDataTest.expected, textElements, tagCloud); }) ); }); - describe('should not get multiple render-events', function () { + describe('should not get multiple render-events', () => { let counter; - beforeEach(function () { + beforeEach(() => { counter = 0; - setupDOM(); + return new Promise((resolve, reject) => { tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(baseTest.data); @@ -281,31 +281,32 @@ describe('tag cloud tests', function () { afterEach(teardownDOM); - it( + test( 'completeness should be ok', - handleExpectedBlip(function () { - expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.COMPLETE); + handleExpectedBlip(() => { + expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); }) ); - it( + test( 'positions should be ok', - handleExpectedBlip(function () { + handleExpectedBlip(() => { const textElements = domNode.querySelectorAll('text'); verifyTagProperties(logScaleTest.expected, textElements, tagCloud); }) ); }); - describe('should show correct data when state-updates are interleaved with resize event', function () { - beforeEach(async function () { - setupDOM(); + describe('should show correct data when state-updates are interleaved with resize event', () => { + beforeEach(async () => { tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(logScaleTest.data); tagCloud.setOptions(logScaleTest.options); await delay(1000); //let layout run - domNode.style.width = '600px'; - domNode.style.height = '600px'; + + SVGElementGetBBoxSpyInstance.mockRestore(); + SVGElementGetBBoxSpyInstance = setSVGElementGetBBox(600, 600); + tagCloud.resize(); //triggers new layout setTimeout(() => { //change the options at the very end too @@ -317,26 +318,23 @@ describe('tag cloud tests', function () { afterEach(teardownDOM); - it( + test( 'completeness should be ok', - handleExpectedBlip(function () { - expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.COMPLETE); + handleExpectedBlip(() => { + expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); }) ); - it( + test( 'positions should be ok', - handleExpectedBlip(function () { + handleExpectedBlip(() => { const textElements = domNode.querySelectorAll('text'); verifyTagProperties(baseTest.expected, textElements, tagCloud); }) ); }); - describe(`should not put elements in view when container is too small`, function () { - beforeEach(async function () { - setupDOM(); - domNode.style.width = '1px'; - domNode.style.height = '1px'; + describe(`should not put elements in view when container is too small`, () => { + beforeEach(async () => { tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(baseTest.data); tagCloud.setOptions(baseTest.options); @@ -345,10 +343,10 @@ describe('tag cloud tests', function () { afterEach(teardownDOM); - it('completeness should not be ok', function () { - expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.INCOMPLETE); + test('completeness should not be ok', () => { + expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.INCOMPLETE); }); - it('positions should not be ok', function () { + test('positions should not be ok', () => { const textElements = domNode.querySelectorAll('text'); for (let i = 0; i < textElements; i++) { const bbox = textElements[i].getBoundingClientRect(); @@ -357,96 +355,73 @@ describe('tag cloud tests', function () { }); }); - describe(`tags should fit after making container bigger`, function () { - beforeEach(async function () { - setupDOM(); - domNode.style.width = '1px'; - domNode.style.height = '1px'; - + describe(`tags should fit after making container bigger`, () => { + beforeEach(async () => { tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(baseTest.data); tagCloud.setOptions(baseTest.options); await fromNode((cb) => tagCloud.once('renderComplete', cb)); //make bigger - domNode.style.width = '512px'; - domNode.style.height = '512px'; + tagCloud._size = [600, 600]; tagCloud.resize(); await fromNode((cb) => tagCloud.once('renderComplete', cb)); }); afterEach(teardownDOM); - it( + test( 'completeness should be ok', - handleExpectedBlip(function () { - expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.COMPLETE); + handleExpectedBlip(() => { + expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.COMPLETE); }) ); }); - describe(`tags should no longer fit after making container smaller`, function () { - beforeEach(async function () { - setupDOM(); + describe(`tags should no longer fit after making container smaller`, () => { + beforeEach(async () => { tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(baseTest.data); tagCloud.setOptions(baseTest.options); await fromNode((cb) => tagCloud.once('renderComplete', cb)); //make smaller - domNode.style.width = '1px'; - domNode.style.height = '1px'; + tagCloud._size = []; tagCloud.resize(); await fromNode((cb) => tagCloud.once('renderComplete', cb)); }); afterEach(teardownDOM); - it('completeness should not be ok', function () { - expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.INCOMPLETE); + test('completeness should not be ok', () => { + expect(tagCloud.getStatus()).toEqual(TagCloud.STATUS.INCOMPLETE); }); }); - describe('tagcloudscreenshot', function () { - let imageComparator; - beforeEach(async function () { - setupDOM(); - imageComparator = new ImageComparator(); - }); - - afterEach(() => { - imageComparator.destroy(); - teardownDOM(); - }); + describe('tagcloudscreenshot', () => { + afterEach(teardownDOM); - it('should render simple image', async function () { + test('should render simple image', async () => { tagCloud = new TagCloud(domNode, colorScale); tagCloud.setData(baseTest.data); tagCloud.setOptions(baseTest.options); await fromNode((cb) => tagCloud.once('renderComplete', cb)); - const mismatchedPixels = await imageComparator.compareDOMContents( - domNode.innerHTML, - 512, - 512, - simpleloadPng, - 0.5 - ); - expect(mismatchedPixels).to.be.lessThan(64); + expect(domNode.innerHTML).toMatchSnapshot(); }); }); function verifyTagProperties(expectedValues, actualElements, tagCloud) { - expect(actualElements.length).to.equal(expectedValues.length); + expect(actualElements.length).toEqual(expectedValues.length); expectedValues.forEach((test, index) => { try { - expect(actualElements[index].style.fontSize).to.equal(test.fontSize); + expect(actualElements[index].style.fontSize).toEqual(test.fontSize); } catch (e) { throw new Error('fontsize is not correct: ' + e.message); } try { - expect(actualElements[index].innerHTML).to.equal(test.text); + expect(actualElements[index].innerHTML).toEqual(test.text); } catch (e) { throw new Error('fontsize is not correct: ' + e.message); } @@ -470,14 +445,14 @@ describe('tag cloud tests', function () { debugInfo: ${JSON.stringify(tagCloud.getDebugInfo())}`; try { - expect(bbox.top >= 0 && bbox.top <= domNode.offsetHeight).to.be(shouldBeInside); + expect(bbox.top >= 0 && bbox.top <= domNode.offsetHeight).toBe(shouldBeInside); } catch (e) { throw new Error( 'top boundary of tag should have been ' + (shouldBeInside ? 'inside' : 'outside') + message ); } try { - expect(bbox.bottom >= 0 && bbox.bottom <= domNode.offsetHeight).to.be(shouldBeInside); + expect(bbox.bottom >= 0 && bbox.bottom <= domNode.offsetHeight).toBe(shouldBeInside); } catch (e) { throw new Error( 'bottom boundary of tag should have been ' + @@ -486,14 +461,14 @@ describe('tag cloud tests', function () { ); } try { - expect(bbox.left >= 0 && bbox.left <= domNode.offsetWidth).to.be(shouldBeInside); + expect(bbox.left >= 0 && bbox.left <= domNode.offsetWidth).toBe(shouldBeInside); } catch (e) { throw new Error( 'left boundary of tag should have been ' + (shouldBeInside ? 'inside' : 'outside') + message ); } try { - expect(bbox.right >= 0 && bbox.right <= domNode.offsetWidth).to.be(shouldBeInside); + expect(bbox.right >= 0 && bbox.right <= domNode.offsetWidth).toBe(shouldBeInside); } catch (e) { throw new Error( 'right boundary of tag should have been ' + @@ -532,7 +507,7 @@ describe('tag cloud tests', function () { } function handleExpectedBlip(assertion) { - return function () { + return () => { if (!shouldAssert()) { return; } diff --git a/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.test.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.test.js new file mode 100644 index 0000000000000..7f96066c16076 --- /dev/null +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.test.js @@ -0,0 +1,176 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import 'jest-canvas-mock'; + +import { createTagCloudVisTypeDefinition } from '../tag_cloud_type'; +import { createTagCloudVisualization } from './tag_cloud_visualization'; +import { setFormatService } from '../services'; +import { dataPluginMock } from '../../../data/public/mocks'; +import { setHTMLElementOffset, setSVGElementGetBBox } from '../../../../test_utils/public'; + +const seedColors = ['#00a69b', '#57c17b', '#6f87d8', '#663db8', '#bc52bc', '#9e3533', '#daa05d']; + +describe('TagCloudVisualizationTest', () => { + let domNode; + let vis; + let SVGElementGetBBoxSpyInstance; + let HTMLElementOffsetMockInstance; + + const dummyTableGroup = { + columns: [ + { + id: 'col-0', + title: 'geo.dest: Descending', + }, + { + id: 'col-1', + title: 'Count', + }, + ], + rows: [ + { 'col-0': 'CN', 'col-1': 26 }, + { 'col-0': 'IN', 'col-1': 17 }, + { 'col-0': 'US', 'col-1': 6 }, + { 'col-0': 'DE', 'col-1': 4 }, + { 'col-0': 'BR', 'col-1': 3 }, + ], + }; + const TagCloudVisualization = createTagCloudVisualization({ + colors: { + seedColors, + }, + }); + + const originTransformSVGElement = window.SVGElement.prototype.transform; + + beforeAll(() => { + setFormatService(dataPluginMock.createStartContract().fieldFormats); + Object.defineProperties(window.SVGElement.prototype, { + transform: { + get: () => ({ + baseVal: { + consolidate: () => {}, + }, + }), + configurable: true, + }, + }); + }); + + afterAll(() => { + SVGElementGetBBoxSpyInstance.mockRestore(); + HTMLElementOffsetMockInstance.mockRestore(); + window.SVGElement.prototype.transform = originTransformSVGElement; + }); + + describe('TagCloudVisualization - basics', () => { + beforeEach(async () => { + const visType = createTagCloudVisTypeDefinition({ colors: seedColors }); + setupDOM(512, 512); + + vis = { + type: visType, + params: { + bucket: { accessor: 0, format: {} }, + metric: { accessor: 0, format: {} }, + scale: 'linear', + orientation: 'single', + }, + data: {}, + }; + }); + + test('simple draw', async () => { + const tagcloudVisualization = new TagCloudVisualization(domNode, vis); + + await tagcloudVisualization.render(dummyTableGroup, vis.params, { + resize: false, + params: true, + aggs: true, + data: true, + uiState: false, + }); + + const svgNode = domNode.querySelector('svg'); + expect(svgNode.outerHTML).toMatchSnapshot(); + }); + + test('with resize', async () => { + const tagcloudVisualization = new TagCloudVisualization(domNode, vis); + await tagcloudVisualization.render(dummyTableGroup, vis.params, { + resize: false, + params: true, + aggs: true, + data: true, + uiState: false, + }); + + domNode.style.width = '256px'; + domNode.style.height = '368px'; + await tagcloudVisualization.render(dummyTableGroup, vis.params, { + resize: true, + params: false, + aggs: false, + data: false, + uiState: false, + }); + + const svgNode = domNode.querySelector('svg'); + expect(svgNode.outerHTML).toMatchSnapshot(); + }); + + test('with param change', async function () { + const tagcloudVisualization = new TagCloudVisualization(domNode, vis); + await tagcloudVisualization.render(dummyTableGroup, vis.params, { + resize: false, + params: true, + aggs: true, + data: true, + uiState: false, + }); + + SVGElementGetBBoxSpyInstance.mockRestore(); + SVGElementGetBBoxSpyInstance = setSVGElementGetBBox(256, 368); + + HTMLElementOffsetMockInstance.mockRestore(); + HTMLElementOffsetMockInstance = setHTMLElementOffset(256, 386); + + vis.params.orientation = 'right angled'; + vis.params.minFontSize = 70; + await tagcloudVisualization.render(dummyTableGroup, vis.params, { + resize: true, + params: true, + aggs: false, + data: false, + uiState: false, + }); + + const svgNode = domNode.querySelector('svg'); + expect(svgNode.outerHTML).toMatchSnapshot(); + }); + }); + + function setupDOM(width, height) { + domNode = document.createElement('div'); + + HTMLElementOffsetMockInstance = setHTMLElementOffset(width, height); + SVGElementGetBBoxSpyInstance = setSVGElementGetBBox(width, height); + } +}); diff --git a/src/test_utils/public/helpers/index.ts b/src/test_utils/public/helpers/index.ts index 79dc29e83bc3b..c8447743ee287 100644 --- a/src/test_utils/public/helpers/index.ts +++ b/src/test_utils/public/helpers/index.ts @@ -24,3 +24,5 @@ export { WithStore } from './redux_helpers'; export { WithMemoryRouter, WithRoute, reactRouterMock } from './router_helpers'; export * from './utils'; + +export { setSVGElementGetBBox, setHTMLElementOffset } from './jsdom_svg_mocks'; diff --git a/src/test_utils/public/helpers/jsdom_svg_mocks.ts b/src/test_utils/public/helpers/jsdom_svg_mocks.ts new file mode 100644 index 0000000000000..dbc8266f663f1 --- /dev/null +++ b/src/test_utils/public/helpers/jsdom_svg_mocks.ts @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const setSVGElementGetBBox = ( + width: number, + height: number, + x: number = 0, + y: number = 0 +) => { + const SVGElementPrototype = SVGElement.prototype as any; + const originalGetBBox = SVGElementPrototype.getBBox; + + // getBBox is not in the SVGElement.prototype object by default, so we cannot use jest.spyOn for that case + SVGElementPrototype.getBBox = jest.fn(() => ({ + x, + y, + width, + height, + })); + + return { + mockRestore: () => { + SVGElementPrototype.getBBox = originalGetBBox; + }, + }; +}; + +export const setHTMLElementOffset = (width: number, height: number) => { + const offsetWidthSpy = jest.spyOn(window.HTMLElement.prototype, 'offsetWidth', 'get'); + offsetWidthSpy.mockReturnValue(width); + + const offsetHeightSpy = jest.spyOn(window.HTMLElement.prototype, 'offsetHeight', 'get'); + offsetHeightSpy.mockReturnValue(height); + + return { + mockRestore: () => { + offsetWidthSpy.mockRestore(); + offsetHeightSpy.mockRestore(); + }, + }; +}; diff --git a/src/test_utils/public/index.ts b/src/test_utils/public/index.ts new file mode 100644 index 0000000000000..4f46dfe1578db --- /dev/null +++ b/src/test_utils/public/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { setSVGElementGetBBox, setHTMLElementOffset } from './helpers'; diff --git a/yarn.lock b/yarn.lock index 7e44780389531..eb1943c5cd00c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10379,6 +10379,11 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" +color-convert@~0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd" + integrity sha1-vbbGnOZg+t/+CwAHzER+G59ygr0= + color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" @@ -11444,6 +11449,11 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== +cssfontparser@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/cssfontparser/-/cssfontparser-1.2.1.tgz#f4022fc8f9700c68029d542084afbaf425a3f3e3" + integrity sha1-9AIvyPlwDGgCnVQghK+69CWj8+M= + csso@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/csso/-/csso-4.0.2.tgz#e5f81ab3a56b8eefb7f0092ce7279329f454de3d" @@ -18922,6 +18932,14 @@ iterall@^1.2.2: resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea" integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg== +jest-canvas-mock@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/jest-canvas-mock/-/jest-canvas-mock-2.2.0.tgz#45fbc58589c6ce9df50dc90bd8adce747cbdada7" + integrity sha512-DcJdchb7eWFZkt6pvyceWWnu3lsp5QWbUeXiKgEMhwB3sMm5qHM1GQhDajvJgBeiYpgKcojbzZ53d/nz6tXvJw== + dependencies: + cssfontparser "^1.2.1" + parse-color "^1.0.0" + jest-changed-files@^25.5.0: version "25.5.0" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-25.5.0.tgz#141cc23567ceb3f534526f8614ba39421383634c" @@ -23755,6 +23773,13 @@ parse-bmfont-xml@^1.1.4: xml-parse-from-string "^1.0.0" xml2js "^0.4.5" +parse-color@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-color/-/parse-color-1.0.0.tgz#7b748b95a83f03f16a94f535e52d7f3d94658619" + integrity sha1-e3SLlag/A/FqlPU15S1/PZRlhhk= + dependencies: + color-convert "~0.5.0" + parse-entities@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.1.1.tgz#8112d88471319f27abae4d64964b122fe4e1b890" From 44925311fc8fe38bbed5269cbea6388d0263437e Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 6 Jul 2020 11:13:17 -0700 Subject: [PATCH 18/46] Fix kbn/optimizer tests (#70827) Co-authored-by: spalger --- .../basic_optimization.test.ts.snap | 66 +++++++++++++++++++ .../basic_optimization.test.ts | 14 ++-- 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 1466865df8d98..211cfac3806ad 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -1,5 +1,71 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`builds expected bundles, saves bundle counts to metadata: OptimizerConfig 1`] = ` +OptimizerConfig { + "bundles": Array [ + Bundle { + "cache": BundleCache { + "path": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/target/public/.kbn-optimizer-cache, + "state": undefined, + }, + "contextDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar, + "id": "bar", + "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/target/public, + "publicDirNames": Array [ + "public", + ], + "sourceRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, + "type": "plugin", + }, + Bundle { + "cache": BundleCache { + "path": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/target/public/.kbn-optimizer-cache, + "state": undefined, + }, + "contextDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo, + "id": "foo", + "outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/target/public, + "publicDirNames": Array [ + "public", + ], + "sourceRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, + "type": "plugin", + }, + ], + "cache": true, + "dist": false, + "inspectWorkers": false, + "maxWorkerCount": 1, + "plugins": Array [ + Object { + "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar, + "extraPublicDirs": Array [], + "id": "bar", + "isUiPlugin": true, + }, + Object { + "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo, + "extraPublicDirs": Array [], + "id": "foo", + "isUiPlugin": true, + }, + Object { + "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/nested/baz, + "extraPublicDirs": Array [], + "id": "baz", + "isUiPlugin": false, + }, + ], + "profileWebpack": false, + "repoRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, + "themeTags": Array [ + "v7dark", + "v7light", + ], + "watch": false, +} +`; + exports[`prepares assets for distribution: bar bundle 1`] = `"(function(modules){var installedModules={};function __webpack_require__(moduleId){if(installedModules[moduleId]){return installedModules[moduleId].exports}var module=installedModules[moduleId]={i:moduleId,l:false,exports:{}};modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);module.l=true;return module.exports}__webpack_require__.m=modules;__webpack_require__.c=installedModules;__webpack_require__.d=function(exports,name,getter){if(!__webpack_require__.o(exports,name)){Object.defineProperty(exports,name,{enumerable:true,get:getter})}};__webpack_require__.r=function(exports){if(typeof Symbol!==\\"undefined\\"&&Symbol.toStringTag){Object.defineProperty(exports,Symbol.toStringTag,{value:\\"Module\\"})}Object.defineProperty(exports,\\"__esModule\\",{value:true})};__webpack_require__.t=function(value,mode){if(mode&1)value=__webpack_require__(value);if(mode&8)return value;if(mode&4&&typeof value===\\"object\\"&&value&&value.__esModule)return value;var ns=Object.create(null);__webpack_require__.r(ns);Object.defineProperty(ns,\\"default\\",{enumerable:true,value:value});if(mode&2&&typeof value!=\\"string\\")for(var key in value)__webpack_require__.d(ns,key,function(key){return value[key]}.bind(null,key));return ns};__webpack_require__.n=function(module){var getter=module&&module.__esModule?function getDefault(){return module[\\"default\\"]}:function getModuleExports(){return module};__webpack_require__.d(getter,\\"a\\",getter);return getter};__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)};__webpack_require__.p=\\"\\";return __webpack_require__(__webpack_require__.s=5)})([function(module,exports,__webpack_require__){\\"use strict\\";var isOldIE=function isOldIE(){var memo;return function memorize(){if(typeof memo===\\"undefined\\"){memo=Boolean(window&&document&&document.all&&!window.atob)}return memo}}();var getTarget=function getTarget(){var memo={};return function memorize(target){if(typeof memo[target]===\\"undefined\\"){var styleTarget=document.querySelector(target);if(window.HTMLIFrameElement&&styleTarget instanceof window.HTMLIFrameElement){try{styleTarget=styleTarget.contentDocument.head}catch(e){styleTarget=null}}memo[target]=styleTarget}return memo[target]}}();var stylesInDom=[];function getIndexByIdentifier(identifier){var result=-1;for(var i=0;i { await del(TMP_DIR); }); -// FLAKY: https://github.com/elastic/kibana/issues/70762 -it.skip('builds expected bundles, saves bundle counts to metadata', async () => { +it('builds expected bundles, saves bundle counts to metadata', async () => { const config = OptimizerConfig.create({ repoRoot: MOCK_REPO_DIR, pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], @@ -75,7 +74,11 @@ it.skip('builds expected bundles, saves bundle counts to metadata', async () => expect(config).toMatchSnapshot('OptimizerConfig'); const msgs = await runOptimizer(config) - .pipe(logOptimizerState(log, config), toArray()) + .pipe( + logOptimizerState(log, config), + filter((x) => x.event?.type !== 'worker stdio'), + toArray() + ) .toPromise(); const assert = (statement: string, truth: boolean, altStates?: OptimizerUpdate[]) => { @@ -168,8 +171,7 @@ it.skip('builds expected bundles, saves bundle counts to metadata', async () => `); }); -// FLAKY: https://github.com/elastic/kibana/issues/70764 -it.skip('uses cache on second run and exist cleanly', async () => { +it('uses cache on second run and exist cleanly', async () => { const config = OptimizerConfig.create({ repoRoot: MOCK_REPO_DIR, pluginScanDirs: [Path.resolve(MOCK_REPO_DIR, 'plugins')], From da602fc783b5a8d444eb5651314802862502662c Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 6 Jul 2020 14:25:56 -0400 Subject: [PATCH 19/46] fix nav link to be hidden and update access tag (#70607) --- .../security_solution/common/constants.ts | 10 +++++++++ .../security_solution/public/app/types.ts | 10 +-------- .../timeline/routes/create_timelines_route.ts | 2 +- .../timeline/routes/update_timelines_route.ts | 2 +- .../security_solution/server/plugin.ts | 21 ++++++++++++++----- 5 files changed, 29 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index f547bc8185d02..d32d9f01d61ae 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -35,6 +35,16 @@ export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges'; export const NO_ALERT_INDEX = 'no-alert-index-049FC71A-4C2C-446F-9901-37XMC5024C51'; export const ENDPOINT_METADATA_INDEX = 'metrics-endpoint.metadata-*'; +export enum SecurityPageName { + alerts = 'alerts', + overview = 'overview', + hosts = 'hosts', + network = 'network', + timelines = 'timelines', + case = 'case', + management = 'management', +} + export const APP_OVERVIEW_PATH = `${APP_PATH}/overview`; export const APP_ALERTS_PATH = `${APP_PATH}/alerts`; export const APP_HOSTS_PATH = `${APP_PATH}/hosts`; diff --git a/x-pack/plugins/security_solution/public/app/types.ts b/x-pack/plugins/security_solution/public/app/types.ts index 4bd888e87bbdc..4590f05e12631 100644 --- a/x-pack/plugins/security_solution/public/app/types.ts +++ b/x-pack/plugins/security_solution/public/app/types.ts @@ -18,16 +18,8 @@ import { State, SubPluginsInitReducer } from '../common/store'; import { Immutable } from '../../common/endpoint/types'; import { AppAction } from '../common/store/actions'; import { TimelineState } from '../timelines/store/timeline/types'; +export { SecurityPageName } from '../../common/constants'; -export enum SecurityPageName { - alerts = 'alerts', - overview = 'overview', - hosts = 'hosts', - network = 'network', - timelines = 'timelines', - case = 'case', - management = 'management', -} export interface SecuritySubPluginStore { initialState: Record; reducer: Record>; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts index 60ddaea367aed..5bc4bec45dfb2 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/create_timelines_route.ts @@ -33,7 +33,7 @@ export const createTimelinesRoute = ( body: buildRouteValidation(createTimelineSchema), }, options: { - tags: ['access:siem'], + tags: ['access:securitySolution'], }, }, async (context, request, response) => { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts index f59df151b6955..a622ee9b15706 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/update_timelines_route.ts @@ -31,7 +31,7 @@ export const updateTimelinesRoute = ( body: buildRouteValidation(updateTimelineSchema), }, options: { - tags: ['access:siem'], + tags: ['access:securitySolution'], }, }, // eslint-disable-next-line complexity diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index a97f1eee56342..356b6fca7e8ce 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -39,7 +39,7 @@ import { initSavedObjects, savedObjectTypes } from './saved_objects'; import { AppClientFactory } from './client'; import { createConfig$, ConfigType } from './config'; import { initUiSettings } from './ui_settings'; -import { APP_ID, APP_ICON, SERVER_APP_ID } from '../common/constants'; +import { APP_ID, APP_ICON, SERVER_APP_ID, SecurityPageName } from '../common/constants'; import { registerEndpointRoutes } from './endpoint/routes/metadata'; import { registerResolverRoutes } from './endpoint/routes/resolver'; import { registerPolicyRoutes } from './endpoint/routes/policy'; @@ -70,6 +70,17 @@ export interface PluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PluginStart {} +const securitySubPlugins = [ + APP_ID, + `${APP_ID}:${SecurityPageName.overview}`, + `${APP_ID}:${SecurityPageName.alerts}`, + `${APP_ID}:${SecurityPageName.hosts}`, + `${APP_ID}:${SecurityPageName.network}`, + `${APP_ID}:${SecurityPageName.timelines}`, + `${APP_ID}:${SecurityPageName.case}`, + `${APP_ID}:${SecurityPageName.management}`, +]; + export class Plugin implements IPlugin { private readonly logger: Logger; private readonly config$: Observable; @@ -144,12 +155,12 @@ export class Plugin implements IPlugin Date: Mon, 6 Jul 2020 15:09:35 -0400 Subject: [PATCH 20/46] [EPM][Security Solution] Implementing dataset component templates (#70517) * Implementing dataset component templates * Fixing test * Temporary fix to include timestamp with any component template created * Update package registry docker image for CI. * Adapt to new registry filesystem layout. * Adjust tests to changed registry behavior. * Adding a test for mappings and settings overrides * Wrap all the tests in the docker check Co-authored-by: Elastic Machine Co-authored-by: Sonja Krause-Harder --- .../ingest_manager/common/types/models/epm.ts | 7 ++ .../__snapshots__/template.test.ts.snap | 3 + .../epm/elasticsearch/template/install.ts | 109 +++++++++++++++++- .../elasticsearch/template/template.test.ts | 30 +++++ .../epm/elasticsearch/template/template.ts | 8 +- .../ingest_manager/server/types/index.tsx | 1 + .../0.1.0/dataset/test/fields/fields.yml | 16 +++ .../overrides/0.1.0/dataset/test/manifest.yml | 9 ++ .../overrides/0.1.0/docs/README.md | 3 + .../0.1.0/img/logo_overrides_64_color.svg | 7 ++ .../overrides/0.1.0/manifest.yml | 20 ++++ .../apis/index.js | 1 + .../apis/install.ts | 85 ++++++++++++++ .../apis/list.ts | 2 +- .../apis/template.ts | 1 + 15 files changed, 298 insertions(+), 4 deletions(-) create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/dataset/test/fields/fields.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/dataset/test/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/docs/README.md create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/img/logo_overrides_64_color.svg create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/manifest.yml create mode 100644 x-pack/test/ingest_manager_api_integration/apis/install.ts diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index 3ee3039e9e1c4..0d2825f0aa80d 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -175,6 +175,12 @@ export interface Dataset { package: string; path: string; ingest_pipeline: string; + elasticsearch?: RegistryElasticsearch; +} + +export interface RegistryElasticsearch { + 'index_template.settings'?: object; + 'index_template.mappings'?: object; } // EPR types this as `[]map[string]interface{}` @@ -272,6 +278,7 @@ export interface IndexTemplate { data_stream: { timestamp_field: string; }; + composed_of: string[]; _meta: object; } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap index f5fec020bf5b4..848e65b7931eb 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap @@ -94,6 +94,7 @@ exports[`tests loading base.yml: base.yml 1`] = ` "data_stream": { "timestamp_field": "@timestamp" }, + "composed_of": [], "_meta": { "package": { "name": "nginx" @@ -197,6 +198,7 @@ exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = ` "data_stream": { "timestamp_field": "@timestamp" }, + "composed_of": [], "_meta": { "package": { "name": "coredns" @@ -1684,6 +1686,7 @@ exports[`tests loading system.yml: system.yml 1`] = ` "data_stream": { "timestamp_field": "@timestamp" }, + "composed_of": [], "_meta": { "package": { "name": "system" diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts index a318aecf347d6..e14645bbbf5fb 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts @@ -5,7 +5,13 @@ */ import Boom from 'boom'; -import { Dataset, RegistryPackage, ElasticsearchAssetType, TemplateRef } from '../../../../types'; +import { + Dataset, + RegistryPackage, + ElasticsearchAssetType, + TemplateRef, + RegistryElasticsearch, +} from '../../../../types'; import { CallESAsCurrentUser } from '../../../../types'; import { Field, loadFieldsFromYaml, processFields } from '../../fields/field'; import { getPipelineNameForInstallation } from '../ingest_pipeline/install'; @@ -157,6 +163,98 @@ export async function installTemplateForDataset({ }); } +function putComponentTemplate( + body: object | undefined, + name: string, + callCluster: CallESAsCurrentUser +): { clusterPromise: Promise; name: string } | undefined { + if (body) { + const callClusterParams: { + method: string; + path: string; + ignore: number[]; + body: any; + } = { + method: 'PUT', + path: `/_component_template/${name}`, + ignore: [404], + body, + }; + + return { clusterPromise: callCluster('transport.request', callClusterParams), name }; + } +} + +function buildComponentTemplates(registryElasticsearch: RegistryElasticsearch | undefined) { + let mappingsTemplate; + let settingsTemplate; + + if (registryElasticsearch && registryElasticsearch['index_template.mappings']) { + mappingsTemplate = { + template: { + mappings: { + ...registryElasticsearch['index_template.mappings'], + // temporary change until https://github.com/elastic/elasticsearch/issues/58956 is resolved + // hopefully we'll be able to remove the entire properties section once that issue is resolved + properties: { + // if the timestamp_field changes here: https://github.com/elastic/kibana/blob/master/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts#L309 + // we'll need to update this as well + '@timestamp': { + type: 'date', + }, + }, + }, + }, + }; + } + + if (registryElasticsearch && registryElasticsearch['index_template.settings']) { + settingsTemplate = { + template: { + settings: registryElasticsearch['index_template.settings'], + }, + }; + } + return { settingsTemplate, mappingsTemplate }; +} + +async function installDatasetComponentTemplates( + templateName: string, + registryElasticsearch: RegistryElasticsearch | undefined, + callCluster: CallESAsCurrentUser +) { + const templates: string[] = []; + const componentPromises: Array> = []; + + const compTemplates = buildComponentTemplates(registryElasticsearch); + + const mappings = putComponentTemplate( + compTemplates.mappingsTemplate, + `${templateName}-mappings`, + callCluster + ); + + const settings = putComponentTemplate( + compTemplates.settingsTemplate, + `${templateName}-settings`, + callCluster + ); + + if (mappings) { + templates.push(mappings.name); + componentPromises.push(mappings.clusterPromise); + } + + if (settings) { + templates.push(settings.name); + componentPromises.push(settings.clusterPromise); + } + + // TODO: Check return values for errors + await Promise.all(componentPromises); + return templates; +} + export async function installTemplate({ callCluster, fields, @@ -180,13 +278,22 @@ export async function installTemplate({ packageVersion, }); } + + const composedOfTemplates = await installDatasetComponentTemplates( + templateName, + dataset.elasticsearch, + callCluster + ); + const template = getTemplate({ type: dataset.type, templateName, mappings, pipelineName, packageName, + composedOfTemplates, }); + // TODO: Check return values for errors const callClusterParams: { method: string; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts index 73a6767f6b947..99e568bf771f8 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.test.ts @@ -29,10 +29,37 @@ test('get template', () => { templateName, packageName: 'nginx', mappings: { properties: {} }, + composedOfTemplates: [], }); expect(template.index_patterns).toStrictEqual([`${templateName}-*`]); }); +test('adds composed_of correctly', () => { + const composedOfTemplates = ['component1', 'component2']; + + const template = getTemplate({ + type: 'logs', + templateName: 'name', + packageName: 'nginx', + mappings: { properties: {} }, + composedOfTemplates, + }); + expect(template.composed_of).toStrictEqual(composedOfTemplates); +}); + +test('adds empty composed_of correctly', () => { + const composedOfTemplates: string[] = []; + + const template = getTemplate({ + type: 'logs', + templateName: 'name', + packageName: 'nginx', + mappings: { properties: {} }, + composedOfTemplates, + }); + expect(template.composed_of).toStrictEqual(composedOfTemplates); +}); + test('tests loading base.yml', () => { const ymlPath = path.join(__dirname, '../../fields/tests/base.yml'); const fieldsYML = readFileSync(ymlPath, 'utf-8'); @@ -45,6 +72,7 @@ test('tests loading base.yml', () => { templateName: 'foo', packageName: 'nginx', mappings, + composedOfTemplates: [], }); expect(template).toMatchSnapshot(path.basename(ymlPath)); @@ -62,6 +90,7 @@ test('tests loading coredns.logs.yml', () => { templateName: 'foo', packageName: 'coredns', mappings, + composedOfTemplates: [], }); expect(template).toMatchSnapshot(path.basename(ymlPath)); @@ -79,6 +108,7 @@ test('tests loading system.yml', () => { templateName: 'whatsthis', packageName: 'system', mappings, + composedOfTemplates: [], }); expect(template).toMatchSnapshot(path.basename(ymlPath)); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts index 2de378f717534..e7867532ed176 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts @@ -43,14 +43,16 @@ export function getTemplate({ mappings, pipelineName, packageName, + composedOfTemplates, }: { type: string; templateName: string; mappings: IndexTemplateMappings; pipelineName?: string | undefined; packageName: string; + composedOfTemplates: string[]; }): IndexTemplate { - const template = getBaseTemplate(type, templateName, mappings, packageName); + const template = getBaseTemplate(type, templateName, mappings, packageName, composedOfTemplates); if (pipelineName) { template.template.settings.index.default_pipeline = pipelineName; } @@ -244,7 +246,8 @@ function getBaseTemplate( type: string, templateName: string, mappings: IndexTemplateMappings, - packageName: string + packageName: string, + composedOfTemplates: string[] ): IndexTemplate { return { // This takes precedence over all index templates installed by ES by default (logs-*-* and metrics-*-*) @@ -308,6 +311,7 @@ function getBaseTemplate( data_stream: { timestamp_field: '@timestamp', }, + composed_of: composedOfTemplates, _meta: { package: { name: packageName, diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx index 8239302a97832..a559ca18cfede 100644 --- a/x-pack/plugins/ingest_manager/server/types/index.tsx +++ b/x-pack/plugins/ingest_manager/server/types/index.tsx @@ -41,6 +41,7 @@ export { PackageInfo, RegistryVarsEntry, Dataset, + RegistryElasticsearch, AssetReference, ElasticsearchAssetType, IngestAssetType, diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/dataset/test/fields/fields.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/dataset/test/fields/fields.yml new file mode 100644 index 0000000000000..12a9a03c1337b --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/dataset/test/fields/fields.yml @@ -0,0 +1,16 @@ +- name: dataset.type + type: constant_keyword + description: > + Dataset type. +- name: dataset.name + type: constant_keyword + description: > + Dataset name. +- name: dataset.namespace + type: constant_keyword + description: > + Dataset namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/dataset/test/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/dataset/test/manifest.yml new file mode 100644 index 0000000000000..9ac3c68a0be9e --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/dataset/test/manifest.yml @@ -0,0 +1,9 @@ +title: Test Dataset + +type: logs + +elasticsearch: + index_template.mappings: + dynamic: false + index_template.settings: + index.lifecycle.name: reference diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/docs/README.md b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/docs/README.md new file mode 100644 index 0000000000000..17fb41ceae242 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +For testing the that the settings and mappings section get used diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/img/logo_overrides_64_color.svg b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/img/logo_overrides_64_color.svg new file mode 100644 index 0000000000000..b03007a76ffcc --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/img/logo_overrides_64_color.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/manifest.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/manifest.yml new file mode 100644 index 0000000000000..ba9fd0fada006 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/overrides/0.1.0/manifest.yml @@ -0,0 +1,20 @@ +format_version: 1.0.0 +name: overrides +title: Mappings Settings Test +description: This is a test package for testing that the mappings and settings sections in the dataset manifest are applied. +version: 0.1.0 +categories: ['security'] +release: beta +type: integration +license: basic + +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' + +icons: + - src: '/img/logo_overrides_64_color.svg' + size: '16x16' + type: 'image/svg+xml' diff --git a/x-pack/test/ingest_manager_api_integration/apis/index.js b/x-pack/test/ingest_manager_api_integration/apis/index.js index ef8880f86078b..3f8df8379e743 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/index.js @@ -11,5 +11,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./file')); //loadTestFile(require.resolve('./template')); loadTestFile(require.resolve('./ilm')); + loadTestFile(require.resolve('./install')); }); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/install.ts b/x-pack/test/ingest_manager_api_integration/apis/install.ts new file mode 100644 index 0000000000000..92078c25419df --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/install.ts @@ -0,0 +1,85 @@ +/* + * 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 '../../api_integration/ftr_provider_context'; +import { warnAndSkipTest } from '../helpers'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('es'); + const dockerServers = getService('dockerServers'); + const log = getService('log'); + + const deletePackage = async (pkgkey: string) => { + await supertest.delete(`/api/ingest_manager/epm/packages/${pkgkey}`).set('kbn-xsrf', 'xxxx'); + }; + + const mappingsPackage = 'overrides-0.1.0'; + const server = dockerServers.get('registry'); + + describe('installs packages that include settings and mappings overrides', async () => { + after(async () => { + if (server.enabled) { + // remove the package just in case it being installed will affect other tests + await deletePackage(mappingsPackage); + } + }); + + it('should install the overrides package correctly', async function () { + if (server.enabled) { + let { body } = await supertest + .post(`/api/ingest_manager/epm/packages/${mappingsPackage}`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + + const templateName = body.response[0].id; + + ({ body } = await es.transport.request({ + method: 'GET', + path: `/_index_template/${templateName}`, + })); + + // make sure it has the right composed_of array, the contents should be the component templates + // that were installed + expect(body.index_templates[0].index_template.composed_of).to.contain( + `${templateName}-mappings` + ); + expect(body.index_templates[0].index_template.composed_of).to.contain( + `${templateName}-settings` + ); + + ({ body } = await es.transport.request({ + method: 'GET', + path: `/_component_template/${templateName}-mappings`, + })); + + // Make sure that the `dynamic` field exists and is set to false (as it is in the package) + expect(body.component_templates[0].component_template.template.mappings.dynamic).to.be( + false + ); + // Make sure that the `@timestamp` field exists and is set to date + // this can be removed once https://github.com/elastic/elasticsearch/issues/58956 is resolved + expect( + body.component_templates[0].component_template.template.mappings.properties['@timestamp'] + .type + ).to.be('date'); + + ({ body } = await es.transport.request({ + method: 'GET', + path: `/_component_template/${templateName}-settings`, + })); + + // Make sure that the lifecycle name gets set correct in the settings + expect( + body.component_templates[0].component_template.template.settings.index.lifecycle.name + ).to.be('reference'); + } else { + warnAndSkipTest(this, log); + } + }); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/list.ts b/x-pack/test/ingest_manager_api_integration/apis/list.ts index 200358cb6f8f0..abed9a7b85959 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/list.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/list.ts @@ -29,7 +29,7 @@ export default function ({ getService }: FtrProviderContext) { return response.body; }; const listResponse = await fetchPackageList(); - expect(listResponse.response.length).to.be(11); + expect(listResponse.response.length).to.be(12); } else { warnAndSkipTest(this, log); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/template.ts b/x-pack/test/ingest_manager_api_integration/apis/template.ts index 8911dd28dc243..f7e5a894b83ff 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/template.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/template.ts @@ -27,6 +27,7 @@ export default function ({ getService }: FtrProviderContext) { templateName, mappings, packageName: 'system', + composedOfTemplates: [], }); // This test is not an API integration test with Kibana From a4340f0ecebbc46d77045a83fcc9d1d5cf8fef8b Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 6 Jul 2020 15:10:01 -0400 Subject: [PATCH 21/46] [ML] DF Analytics: add ability to edit job for fields supported by API (#70489) * wip: add edit action to dfanalytics table * add update endpoint and edit flyout * show success and error toasts. close flyout and refresh on success * show permission message in edit action * update types * disable update button if mml not valid * show error in toast, init values are config values * fix undefined check for allow lazy start * prevent update if mml is empty --- x-pack/plugins/ml/common/util/validators.ts | 2 + .../data_frame_analytics/common/analytics.ts | 7 +- .../data_frame_analytics/common/index.ts | 1 + .../components/analytics_list/action_edit.tsx | 66 +++++ .../components/analytics_list/actions.tsx | 6 + .../analytics_list/edit_analytics_flyout.tsx | 270 ++++++++++++++++++ .../ml_api_service/data_frame_analytics.ts | 13 +- .../ml/server/client/elasticsearch_ml.ts | 15 + .../ml/server/routes/data_frame_analytics.ts | 40 +++ .../routes/schemas/data_analytics_schema.ts | 6 + 10 files changed, 424 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_edit.tsx create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/edit_analytics_flyout.tsx diff --git a/x-pack/plugins/ml/common/util/validators.ts b/x-pack/plugins/ml/common/util/validators.ts index 5dcdec0553106..c14c20917a136 100644 --- a/x-pack/plugins/ml/common/util/validators.ts +++ b/x-pack/plugins/ml/common/util/validators.ts @@ -67,6 +67,8 @@ export function requiredValidator() { export type ValidationResult = object | null; +export type MemoryInputValidatorResult = { invalidUnits: { allowedUnits: string } } | null; + export function memoryInputValidator(allowedUnits = ALLOWED_DATA_UNITS) { return (value: any) => { if (typeof value !== 'string' || value === '') { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 5715687402bcb..aa637f71db1cc 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -327,9 +327,14 @@ export const isClassificationEvaluateResponse = ( ); }; +export interface UpdateDataFrameAnalyticsConfig { + allow_lazy_start?: string; + description?: string; + model_memory_limit?: string; +} + export interface DataFrameAnalyticsConfig { id: DataFrameAnalyticsId; - // Description attribute is not supported yet description?: string; dest: { index: IndexName; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts index 58343e26153cc..65531009e4436 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts @@ -13,6 +13,7 @@ export { useRefreshAnalyticsList, DataFrameAnalyticsId, DataFrameAnalyticsConfig, + UpdateDataFrameAnalyticsConfig, IndexName, IndexPattern, REFRESH_ANALYTICS_LIST_STATE, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_edit.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_edit.tsx new file mode 100644 index 0000000000000..041b52d0322c4 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_edit.tsx @@ -0,0 +1,66 @@ +/* + * 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, FC } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; + +import { checkPermission } from '../../../../../capabilities/check_capabilities'; +import { DataFrameAnalyticsListRow } from './common'; + +import { EditAnalyticsFlyout } from './edit_analytics_flyout'; + +interface EditActionProps { + item: DataFrameAnalyticsListRow; +} + +export const EditAction: FC = ({ item }) => { + const canCreateDataFrameAnalytics: boolean = checkPermission('canCreateDataFrameAnalytics'); + + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const closeFlyout = () => setIsFlyoutVisible(false); + const showFlyout = () => setIsFlyoutVisible(true); + + const buttonEditText = i18n.translate('xpack.ml.dataframe.analyticsList.editActionName', { + defaultMessage: 'Edit', + }); + + const editButton = ( + + {buttonEditText} + + ); + + if (!canCreateDataFrameAnalytics) { + return ( + + {editButton} + + ); + } + + return ( + <> + {editButton} + {isFlyoutVisible && } + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx index b47b23f668530..b03a3a4c4edb2 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx @@ -27,6 +27,7 @@ import { getResultsUrl, isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } import { stopAnalytics } from '../../services/analytics_service'; import { StartAction } from './action_start'; +import { EditAction } from './action_edit'; import { DeleteAction } from './action_delete'; interface Props { @@ -133,6 +134,11 @@ export const getActions = ( return stopButton; }, }, + { + render: (item: DataFrameAnalyticsListRow) => { + return ; + }, + }, { render: (item: DataFrameAnalyticsListRow) => { return ; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/edit_analytics_flyout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/edit_analytics_flyout.tsx new file mode 100644 index 0000000000000..b6aed9321e4e3 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/edit_analytics_flyout.tsx @@ -0,0 +1,270 @@ +/* + * 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, { FC, useEffect, useState } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { + EuiButton, + EuiButtonEmpty, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiForm, + EuiFormRow, + EuiOverlayMask, + EuiSelect, + EuiTitle, +} from '@elastic/eui'; + +import { useMlKibana } from '../../../../../contexts/kibana'; +import { ml } from '../../../../../services/ml_api_service'; +import { + memoryInputValidator, + MemoryInputValidatorResult, +} from '../../../../../../../common/util/validators'; +import { extractErrorMessage } from '../../../../../../../common/util/errors'; +import { DataFrameAnalyticsListRow, DATA_FRAME_TASK_STATE } from './common'; +import { + useRefreshAnalyticsList, + UpdateDataFrameAnalyticsConfig, +} from '../../../../common/analytics'; + +interface EditAnalyticsJobFlyoutProps { + closeFlyout: () => void; + item: DataFrameAnalyticsListRow; +} + +let mmLValidator: (value: any) => MemoryInputValidatorResult; + +export const EditAnalyticsFlyout: FC = ({ closeFlyout, item }) => { + const { id: jobId, config } = item; + const { state } = item.stats; + const initialAllowLazyStart = + config.allow_lazy_start !== undefined ? String(config.allow_lazy_start) : ''; + + const [allowLazyStart, setAllowLazyStart] = useState(initialAllowLazyStart); + const [description, setDescription] = useState(config.description || ''); + const [modelMemoryLimit, setModelMemoryLimit] = useState(config.model_memory_limit); + const [mmlValidationError, setMmlValidationError] = useState(); + + const { + services: { notifications }, + } = useMlKibana(); + const { refresh } = useRefreshAnalyticsList(); + + // Disable if mml is not valid + const updateButtonDisabled = mmlValidationError !== undefined; + + useEffect(() => { + if (mmLValidator === undefined) { + mmLValidator = memoryInputValidator(); + } + // validate mml and create validation message + if (modelMemoryLimit !== '') { + const validationResult = mmLValidator(modelMemoryLimit); + if (validationResult !== null && validationResult.invalidUnits) { + setMmlValidationError( + i18n.translate('xpack.ml.dataframe.analytics.create.modelMemoryUnitsInvalidError', { + defaultMessage: 'Model memory limit data unit unrecognized. It must be {str}', + values: { str: validationResult.invalidUnits.allowedUnits }, + }) + ); + } else { + setMmlValidationError(undefined); + } + } else { + setMmlValidationError( + i18n.translate('xpack.ml.dataframe.analytics.create.modelMemoryEmptyError', { + defaultMessage: 'Model memory limit must not be empty', + }) + ); + } + }, [modelMemoryLimit]); + + const onSubmit = async () => { + const updateConfig: UpdateDataFrameAnalyticsConfig = Object.assign( + { + allow_lazy_start: allowLazyStart, + description, + }, + modelMemoryLimit && { model_memory_limit: modelMemoryLimit } + ); + + try { + await ml.dataFrameAnalytics.updateDataFrameAnalytics(jobId, updateConfig); + notifications.toasts.addSuccess( + i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutSuccessMessage', { + defaultMessage: 'Analytics job {jobId} has been updated.', + values: { jobId }, + }) + ); + refresh(); + closeFlyout(); + } catch (e) { + // eslint-disable-next-line + console.error(e); + + notifications.toasts.addDanger({ + title: i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutErrorMessage', { + defaultMessage: 'Could not save changes to analytics job {jobId}', + values: { + jobId, + }, + }), + text: extractErrorMessage(e), + }); + } + }; + + return ( + + + + +

+ {i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutTitle', { + defaultMessage: 'Edit {jobId}', + values: { + jobId, + }, + })} +

+
+
+ + + + ) => + setAllowLazyStart(e.target.value) + } + /> + + + setDescription(e.target.value)} + aria-label={i18n.translate( + 'xpack.ml.dataframe.analyticsList.editFlyout.descriptionAriaLabel', + { + defaultMessage: 'Update the job description.', + } + )} + /> + + + setModelMemoryLimit(e.target.value)} + aria-label={i18n.translate( + 'xpack.ml.dataframe.analyticsList.editFlyout.modelMemoryLimitAriaLabel', + { + defaultMessage: 'Update the model memory limit.', + } + )} + /> + + + + + + + + {i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutCancelButtonText', { + defaultMessage: 'Cancel', + })} + + + + + {i18n.translate('xpack.ml.dataframe.analyticsList.editFlyoutUpdateButtonText', { + defaultMessage: 'Update', + })} + + + + +
+
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts index 7cdd5478e3983..7de39d91047ef 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts @@ -8,7 +8,10 @@ import { http } from '../http_service'; import { basePath } from './index'; import { DataFrameAnalyticsStats } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; -import { DataFrameAnalyticsConfig } from '../../data_frame_analytics/common'; +import { + DataFrameAnalyticsConfig, + UpdateDataFrameAnalyticsConfig, +} from '../../data_frame_analytics/common'; import { DeepPartial } from '../../../../common/types/common'; import { DeleteDataFrameAnalyticsWithIndexStatus } from '../../../../common/types/data_frame_analytics'; @@ -72,6 +75,14 @@ export const dataFrameAnalytics = { body, }); }, + updateDataFrameAnalytics(analyticsId: string, updateConfig: UpdateDataFrameAnalyticsConfig) { + const body = JSON.stringify(updateConfig); + return http({ + path: `${basePath()}/data_frame/analytics/${analyticsId}/_update`, + method: 'POST', + body, + }); + }, evaluateDataFrameAnalytics(evaluateConfig: any) { const body = JSON.stringify(evaluateConfig); return http({ diff --git a/x-pack/plugins/ml/server/client/elasticsearch_ml.ts b/x-pack/plugins/ml/server/client/elasticsearch_ml.ts index 07159534e1e2c..24c80c450f61a 100644 --- a/x-pack/plugins/ml/server/client/elasticsearch_ml.ts +++ b/x-pack/plugins/ml/server/client/elasticsearch_ml.ts @@ -223,6 +223,21 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any) method: 'POST', }); + ml.updateDataFrameAnalytics = ca({ + urls: [ + { + fmt: '/_ml/data_frame/analytics/<%=analyticsId%>/_update', + req: { + analyticsId: { + type: 'string', + }, + }, + }, + ], + needBody: true, + method: 'POST', + }); + ml.deleteJob = ca({ urls: [ { diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index e2601c7ad6a2e..24be23332e4cf 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -10,6 +10,7 @@ import { analyticsAuditMessagesProvider } from '../models/data_frame_analytics/a import { RouteInitialization } from '../types'; import { dataAnalyticsJobConfigSchema, + dataAnalyticsJobUpdateSchema, dataAnalyticsEvaluateSchema, dataAnalyticsExplainSchema, analyticsIdSchema, @@ -483,6 +484,45 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {post} /api/ml/data_frame/analytics/:analyticsId/_update Update specified analytics job + * @apiName UpdateDataFrameAnalyticsJob + * @apiDescription Updates a data frame analytics job. + * + * @apiSchema (params) analyticsIdSchema + */ + router.post( + { + path: '/api/ml/data_frame/analytics/{analyticsId}/_update', + validate: { + params: analyticsIdSchema, + body: dataAnalyticsJobUpdateSchema, + }, + options: { + tags: ['access:ml:canCreateDataFrameAnalytics'], + }, + }, + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { + try { + const { analyticsId } = request.params; + const results = await context.ml!.mlClient.callAsCurrentUser( + 'ml.updateDataFrameAnalytics', + { + body: request.body, + analyticsId, + } + ); + return response.ok({ + body: results, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + /** * @apiGroup DataFrameAnalytics * diff --git a/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts index e6b4e4ccf8582..5469c2fefdf33 100644 --- a/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/data_analytics_schema.ts @@ -69,6 +69,12 @@ export const deleteDataFrameAnalyticsJobSchema = schema.object({ deleteDestIndexPattern: schema.maybe(schema.boolean()), }); +export const dataAnalyticsJobUpdateSchema = schema.object({ + description: schema.maybe(schema.string()), + model_memory_limit: schema.maybe(schema.string()), + allow_lazy_start: schema.maybe(schema.boolean()), +}); + export const stopsDataFrameAnalyticsJobQuerySchema = schema.object({ force: schema.maybe(schema.boolean()), }); From 7b0e9dfe9a50b27bc724d9645585aee49fc1a719 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Mon, 6 Jul 2020 21:25:52 +0200 Subject: [PATCH 22/46] [SIEM] Unskips and fixes 'Detection rules, custom' test (#70693) * unskips and fixes 'Detection rules, custom' test * deletes comment Co-authored-by: Elastic Machine --- .../alerts_detection_rules_custom.spec.ts | 7 +- .../security_solution/cypress/objects/rule.ts | 6 +- .../custom_rule_with_timeline/data.json.gz | Bin 67934 -> 74563 bytes .../custom_rule_with_timeline/mappings.json | 2599 ++++------------- 4 files changed, 524 insertions(+), 2088 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts index 2a1a2d2c8e194..51c29c15a8097 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { newRule, totalNumberOfPrebuiltRulesInEsArchive } from '../objects/rule'; +import { newRule, totalNumberOfPrebuiltRulesInEsArchiveCustomRule } from '../objects/rule'; import { CUSTOM_RULES_BTN, @@ -64,8 +64,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { ALERTS_URL } from '../urls/navigation'; -// // Skipped as was causing failures on master -describe.skip('Detection rules, custom', () => { +describe('Detection rules, custom', () => { before(() => { esArchiverLoad('custom_rule_with_timeline'); }); @@ -90,7 +89,7 @@ describe.skip('Detection rules, custom', () => { changeToThreeHundredRowsPerPage(); waitForRulesToBeLoaded(); - const expectedNumberOfRules = totalNumberOfPrebuiltRulesInEsArchive + 1; + const expectedNumberOfRules = totalNumberOfPrebuiltRulesInEsArchiveCustomRule + 1; cy.get(RULES_TABLE).then(($table) => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); }); diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index d750fe212002d..c9d3af57e5e59 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -11,6 +11,8 @@ export const totalNumberOfPrebuiltRules = rawRules.length; export const totalNumberOfPrebuiltRulesInEsArchive = 127; +export const totalNumberOfPrebuiltRulesInEsArchiveCustomRule = 145; + interface Mitre { tactic: string; techniques: string[]; @@ -57,7 +59,7 @@ const mitre2: Mitre = { }; export const newRule: CustomRule = { - customQuery: 'host.name: *', + customQuery: 'host.name: * ', name: 'New Rule Test', description: 'The new rule description.', severity: 'High', @@ -67,7 +69,7 @@ export const newRule: CustomRule = { falsePositivesExamples: ['False1', 'False2'], mitre: [mitre1, mitre2], note: '# test markdown', - timelineId: '352c6110-9ffb-11ea-b3d8-857d6042d9bd', + timelineId: '3270f530-bc84-11ea-b73f-89980a6a1ce7', }; export const machineLearningRule: MachineLearningRule = { diff --git a/x-pack/test/security_solution_cypress/es_archives/custom_rule_with_timeline/data.json.gz b/x-pack/test/security_solution_cypress/es_archives/custom_rule_with_timeline/data.json.gz index 3d50451cee39fe8c8f8b49da585473585a812c1d..b3a94c77c11846e2e6d34e2908022b2e943f551b 100644 GIT binary patch literal 74563 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 Z58M!_J$|-*jy<8I@O~(EH^S|5MR~?{>6Tp8 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 diff --git a/x-pack/test/security_solution_cypress/es_archives/custom_rule_with_timeline/mappings.json b/x-pack/test/security_solution_cypress/es_archives/custom_rule_with_timeline/mappings.json index 693878a88f899..01a768351e483 100644 --- a/x-pack/test/security_solution_cypress/es_archives/custom_rule_with_timeline/mappings.json +++ b/x-pack/test/security_solution_cypress/es_archives/custom_rule_with_timeline/mappings.json @@ -14,36 +14,39 @@ "alert": "7b44fba6773e37c806ce290ea9b7024e", "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", "apm-telemetry": "3525d7c22c42bc80f5e6e9cb3f2b26a2", - "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", - "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "43b8830d5d0df85a6823d290885fc9fd", "canvas-element": "7390014e1091044523666d97247392fc", "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", "cases": "32aa96a6d3855ddda53010ae2048ac22", "cases-comments": "c2061fb929f585df57425102fa928b4b", "cases-configure": "42711cbb311976c0687853f4c1354572", "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", - "config": "ae24d22d5986d04124cc6568f771066f", + "config": "c63748b75f39d0c54de12d12c1ccbc20", "dashboard": "d00f614b29a80360e1190193fd333bab", - "epm-packages": "92b4b1899b887b090d01c033f3118a85", + "endpoint:exceptions-artifact": "053713a6b91811c7de078ead17384914", + "endpoint:exceptions-manifest": "67c28185da541c1404e7852d30498cd6", + "epm-packages": "04696e7dba1b9597f7d6ed78a4a76658", "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", - "fleet-agent-actions": "e520c855577170c24481be05c3ae14ec", + "fleet-agent-actions": "00fe5651ed2da16b7f8159bbf0f7d910", "fleet-agent-events": "3231653fafe4ef3196fe3b32ab774bf2", - "fleet-agents": "864760267df6c970f629bd4458506c53", - "fleet-enrollment-api-keys": "28b91e20b105b6f928e2012600085d8f", + "fleet-agents": "578bbfa81650206927683ebde0c85409", + "fleet-enrollment-api-keys": "451e5c329b3ae9722dc7bc8f5921e05d", "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", "index-pattern": "66eccb05066c5a89924f48a9e9736499", - "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", - "ingest-agent-configs": "d9a5cbdce8e937f674a7b376c47a34a1", - "ingest-package-configs": "c0fe6347b0eebcbf421841669e3acd31", - "ingest-outputs": "0e57221778a7153c8292edf154099036", + "infrastructure-ui-source": "2b2809653635caf490c93f090502d04c", + "ingest-agent-configs": "f1e09bc73462386a8c07e9d1997d0688", + "ingest-outputs": "87da6a0e27b3a61ad389fb7a7e2da293", + "ingest-package-configs": "48e8bd97e488008e21c0b5a2367b83ad", "ingest_manager_settings": "c5b0749b4ab03c582efd4c14cb8f132c", "inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2", "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", - "lens": "21c3ea0763beb1ecb0162529706b88c5", + "lens": "d33c68a69ff1e78c9888dedd2164ac22", "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", - "map": "23d7aa4a720d4938ccde3983f87bd58d", - "maps-telemetry": "bfd39d88aadadb4be597ea984d433dbe", - "metrics-explorer-view": "428e319af3e822c80a84cf87123ca35c", + "map": "4a05b35c3a3a58fbc72dd0202dc3487f", + "maps-telemetry": "5ef305b18111b77789afefbd36b66171", + "metrics-explorer-view": "a8df1d270ee48c969d22d23812d08187", "migrationVersion": "4a1746014a75ade3a714e1db5763276f", "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", "namespace": "2f4316de49999235636386fe51dc06c1", @@ -67,7 +70,7 @@ "upgrade-assistant-reindex-operation": "296a89039fc4260292be36b1b005d8f2", "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", "uptime-dynamic-settings": "fcdb453a30092f022f2642db29523d80", - "url": "b675c3be8d76ecf029294d51dc7ec65d", + "url": "c7f66a0df8b1b52f17c28c4adb111105", "visualization": "52d7a13ad68a150c4525b292d23e12cc" } }, @@ -109,145 +112,6 @@ } } }, - "agent_actions": { - "properties": { - "agent_id": { - "type": "keyword" - }, - "created_at": { - "type": "date" - }, - "data": { - "type": "flattened" - }, - "sent_at": { - "type": "date" - }, - "type": { - "type": "keyword" - } - } - }, - "agent_configs": { - "properties": { - "datasources": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "is_default": { - "type": "boolean" - }, - "name": { - "type": "text" - }, - "namespace": { - "type": "keyword" - }, - "revision": { - "type": "integer" - }, - "status": { - "type": "keyword" - }, - "updated_by": { - "type": "keyword" - }, - "updated_on": { - "type": "keyword" - } - } - }, - "agent_events": { - "properties": { - "action_id": { - "type": "keyword" - }, - "agent_id": { - "type": "keyword" - }, - "config_id": { - "type": "keyword" - }, - "data": { - "type": "text" - }, - "message": { - "type": "text" - }, - "payload": { - "type": "text" - }, - "stream_id": { - "type": "keyword" - }, - "subtype": { - "type": "keyword" - }, - "timestamp": { - "type": "date" - }, - "type": { - "type": "keyword" - } - } - }, - "agents": { - "properties": { - "access_api_key_id": { - "type": "keyword" - }, - "active": { - "type": "boolean" - }, - "config_id": { - "type": "keyword" - }, - "config_newest_revision": { - "type": "integer" - }, - "config_revision": { - "type": "integer" - }, - "current_error_events": { - "type": "text" - }, - "default_api_key": { - "type": "keyword" - }, - "enrolled_at": { - "type": "date" - }, - "last_checkin": { - "type": "date" - }, - "last_updated": { - "type": "date" - }, - "local_metadata": { - "type": "text" - }, - "shared_id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "user_provided_metadata": { - "type": "text" - }, - "version": { - "type": "keyword" - } - } - }, "alert": { "properties": { "actions": { @@ -1264,29 +1128,12 @@ } }, "application_usage_totals": { - "properties": { - "appId": { - "type": "keyword" - }, - "minutesOnScreen": { - "type": "float" - }, - "numberOfClicks": { - "type": "long" - } - } + "dynamic": "false", + "type": "object" }, "application_usage_transactional": { + "dynamic": "false", "properties": { - "appId": { - "type": "keyword" - }, - "minutesOnScreen": { - "type": "float" - }, - "numberOfClicks": { - "type": "long" - }, "timestamp": { "type": "date" } @@ -1339,6 +1186,38 @@ } } }, + "canvas-workpad-template": { + "dynamic": "false", + "properties": { + "help": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "tags": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "template_key": { + "type": "keyword" + } + } + }, "cases": { "properties": { "closed_at": { @@ -1574,7 +1453,7 @@ } }, "config": { - "dynamic": "true", + "dynamic": "false", "properties": { "buildNum": { "type": "keyword" @@ -1635,163 +1514,70 @@ } } }, - "datasources": { + "endpoint:exceptions-artifact": { "properties": { - "config_id": { + "body": { + "type": "binary" + }, + "created": { + "index": false, + "type": "date" + }, + "encoding": { + "index": false, "type": "keyword" }, - "description": { - "type": "text" + "identifier": { + "type": "keyword" }, - "enabled": { - "type": "boolean" + "sha256": { + "type": "keyword" }, - "inputs": { + "size": { + "index": false, + "type": "long" + } + } + }, + "endpoint:exceptions-manifest": { + "properties": { + "created": { + "index": false, + "type": "date" + }, + "ids": { + "index": false, + "type": "keyword" + } + } + }, + "epm-packages": { + "properties": { + "es_index_patterns": { + "enabled": false, + "type": "object" + }, + "installed": { "properties": { - "config": { - "type": "flattened" - }, - "enabled": { - "type": "boolean" - }, - "processors": { + "id": { "type": "keyword" }, - "streams": { - "properties": { - "config": { - "type": "flattened" - }, - "dataset": { - "type": "keyword" - }, - "enabled": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "processors": { - "type": "keyword" - } - }, - "type": "nested" - }, "type": { "type": "keyword" } }, "type": "nested" }, + "internal": { + "type": "boolean" + }, "name": { "type": "keyword" }, - "namespace": { - "type": "keyword" + "removable": { + "type": "boolean" }, - "output_id": { - "type": "keyword" - }, - "package": { - "properties": { - "name": { - "type": "keyword" - }, - "title": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "revision": { - "type": "integer" - } - } - }, - "enrollment_api_keys": { - "properties": { - "active": { - "type": "boolean" - }, - "api_key": { - "type": "binary" - }, - "api_key_id": { - "type": "keyword" - }, - "config_id": { - "type": "keyword" - }, - "created_at": { - "type": "date" - }, - "expire_at": { - "type": "date" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - } - } - }, - "epm-package": { - "properties": { - "installed": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "internal": { - "type": "boolean" - }, - "name": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "epm-packages": { - "properties": { - "es_index_patterns": { - "dynamic": "false", - "type": "object" - }, - "installed": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - }, - "type": "nested" - }, - "internal": { - "type": "boolean" - }, - "name": { - "type": "keyword" - }, - "removable": { - "type": "boolean" - }, - "version": { + "version": { "type": "keyword" } } @@ -1874,10 +1660,11 @@ "type": "integer" }, "current_error_events": { + "index": false, "type": "text" }, "default_api_key": { - "type": "keyword" + "type": "binary" }, "default_api_key_id": { "type": "keyword" @@ -1894,6 +1681,9 @@ "local_metadata": { "type": "flattened" }, + "packages": { + "type": "keyword" + }, "shared_id": { "type": "keyword" }, @@ -2026,6 +1816,9 @@ } } }, + "inventoryDefaultView": { + "type": "keyword" + }, "logAlias": { "type": "keyword" }, @@ -2061,6 +1854,9 @@ "metricAlias": { "type": "keyword" }, + "metricsExplorerDefaultView": { + "type": "keyword" + }, "name": { "type": "text" } @@ -2068,9 +1864,6 @@ }, "ingest-agent-configs": { "properties": { - "datasources": { - "type": "keyword" - }, "description": { "type": "text" }, @@ -2081,6 +1874,7 @@ "type": "boolean" }, "monitoring_enabled": { + "index": false, "type": "keyword" }, "name": { @@ -2089,6 +1883,9 @@ "namespace": { "type": "keyword" }, + "package_configs": { + "type": "keyword" + }, "revision": { "type": "integer" }, @@ -2103,6 +1900,35 @@ } } }, + "ingest-outputs": { + "properties": { + "ca_sha256": { + "index": false, + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, "ingest-package-configs": { "properties": { "config_id": { @@ -2121,6 +1947,7 @@ "type": "boolean" }, "inputs": { + "enabled": false, "properties": { "config": { "type": "flattened" @@ -2128,19 +1955,23 @@ "enabled": { "type": "boolean" }, - "processors": { - "type": "keyword" - }, "streams": { "properties": { - "agent_stream": { + "compiled_stream": { "type": "flattened" }, "config": { "type": "flattened" }, "dataset": { - "type": "keyword" + "properties": { + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } }, "enabled": { "type": "boolean" @@ -2148,9 +1979,6 @@ "id": { "type": "keyword" }, - "processors": { - "type": "keyword" - }, "vars": { "type": "flattened" } @@ -2199,34 +2027,6 @@ } } }, - "ingest-outputs": { - "properties": { - "ca_sha256": { - "type": "keyword" - }, - "config": { - "type": "flattened" - }, - "fleet_enroll_password": { - "type": "binary" - }, - "fleet_enroll_username": { - "type": "binary" - }, - "hosts": { - "type": "keyword" - }, - "is_default": { - "type": "boolean" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, "ingest_manager_settings": { "properties": { "agent_auto_upgrade": { @@ -2387,6 +2187,9 @@ }, "lens": { "properties": { + "description": { + "type": "text" + }, "expression": { "index": false, "type": "keyword" @@ -2420,9 +2223,6 @@ }, "map": { "properties": { - "bounds": { - "type": "geo_shape" - }, "description": { "type": "text" }, @@ -2444,68 +2244,8 @@ } }, "maps-telemetry": { - "properties": { - "attributesPerMap": { - "properties": { - "dataSourcesCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - }, - "emsVectorLayersCount": { - "dynamic": "true", - "type": "object" - }, - "layerTypesCount": { - "dynamic": "true", - "type": "object" - }, - "layersCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - } - } - }, - "indexPatternsWithGeoFieldCount": { - "type": "long" - }, - "indexPatternsWithGeoPointFieldCount": { - "type": "long" - }, - "indexPatternsWithGeoShapeFieldCount": { - "type": "long" - }, - "mapsTotalCount": { - "type": "long" - }, - "settings": { - "properties": { - "showMapVisualizationTypes": { - "type": "boolean" - } - } - }, - "timeCaptured": { - "type": "date" - } - } + "enabled": false, + "type": "object" }, "metrics-explorer-view": { "properties": { @@ -2571,6 +2311,9 @@ } }, "type": "nested" + }, + "source": { + "type": "keyword" } } } @@ -2579,7 +2322,7 @@ "migrationVersion": { "dynamic": "true", "properties": { - "dashboard": { + "alert": { "fields": { "keyword": { "ignore_above": 256, @@ -2588,7 +2331,7 @@ }, "type": "text" }, - "index-pattern": { + "config": { "fields": { "keyword": { "ignore_above": 256, @@ -2597,7 +2340,7 @@ }, "type": "text" }, - "ingest-agent-configs": { + "dashboard": { "fields": { "keyword": { "ignore_above": 256, @@ -2606,7 +2349,7 @@ }, "type": "text" }, - "ingest-package-configs": { + "index-pattern": { "fields": { "keyword": { "ignore_above": 256, @@ -2670,45 +2413,14 @@ "namespaces": { "type": "keyword" }, - "outputs": { + "query": { "properties": { - "api_key": { - "type": "keyword" - }, - "ca_sha256": { - "type": "keyword" - }, - "config": { - "type": "flattened" - }, - "fleet_enroll_password": { - "type": "binary" + "description": { + "type": "text" }, - "fleet_enroll_username": { - "type": "binary" - }, - "hosts": { - "type": "keyword" - }, - "is_default": { - "type": "boolean" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "query": { - "properties": { - "description": { - "type": "text" - }, - "filters": { - "enabled": false, - "type": "object" + "filters": { + "enabled": false, + "type": "object" }, "query": { "properties": { @@ -2784,6 +2496,7 @@ } }, "server": { + "dynamic": "strict", "properties": { "uuid": { "type": "keyword" @@ -3208,6 +2921,9 @@ } } }, + "spaceId": { + "type": "keyword" + }, "telemetry": { "properties": { "allowChangingOptInStatus": { @@ -3424,6 +3140,7 @@ "url": { "fields": { "keyword": { + "ignore_above": 2048, "type": "keyword" } }, @@ -3489,14 +3206,6 @@ }, "agent": { "properties": { - "build": { - "properties": { - "original": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, "ephemeral_id": { "ignore_above": 1024, "type": "keyword" @@ -3519,6 +3228,27 @@ } } }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, "client": { "properties": { "address": { @@ -3684,10 +3414,6 @@ "id": { "ignore_above": 1024, "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" } } }, @@ -3715,18 +3441,6 @@ } } }, - "project": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, "provider": { "ignore_above": 1024, "type": "keyword" @@ -3737,6 +3451,27 @@ } } }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, "container": { "properties": { "id": { @@ -3949,9 +3684,6 @@ } } }, - "compile_time": { - "type": "date" - }, "hash": { "properties": { "md5": { @@ -3972,53 +3704,6 @@ } } }, - "malware_classification": { - "properties": { - "features": { - "properties": { - "data": { - "properties": { - "buffer": { - "ignore_above": 1024, - "type": "keyword" - }, - "decompressed_size": { - "type": "integer" - }, - "encoding": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "identifier": { - "ignore_above": 1024, - "type": "keyword" - }, - "score": { - "type": "double" - }, - "threshold": { - "type": "double" - }, - "upx_packed": { - "type": "boolean" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "mapped_address": { - "ignore_above": 1024, - "type": "keyword" - }, - "mapped_size": { - "type": "long" - }, "name": { "ignore_above": 1024, "type": "keyword" @@ -4029,10 +3714,6 @@ }, "pe": { "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, "company": { "ignore_above": 1024, "type": "keyword" @@ -4045,10 +3726,6 @@ "ignore_above": 1024, "type": "keyword" }, - "imphash": { - "ignore_above": 1024, - "type": "keyword" - }, "original_file_name": { "ignore_above": 1024, "type": "keyword" @@ -4147,46 +3824,6 @@ } } }, - "endpoint": { - "properties": { - "artifact": { - "properties": { - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "event": { - "properties": { - "process": { - "properties": { - "ancestry": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "policy": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, "error": { "properties": { "code": { @@ -4360,9 +3997,6 @@ "ignore_above": 1, "type": "keyword" }, - "entry_modified": { - "type": "double" - }, "extension": { "ignore_above": 1024, "type": "keyword" @@ -4399,352 +4033,114 @@ "ignore_above": 1024, "type": "keyword" }, - "macro": { + "mime_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { "properties": { - "code_page": { - "type": "long" - }, - "collection": { - "properties": { - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } + "company": { + "ignore_above": 1024, + "type": "keyword" }, - "errors": { - "properties": { - "count": { - "type": "long" - }, - "error_type": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" + "description": { + "ignore_above": 1024, + "type": "keyword" }, - "file_extension": { + "file_version": { "ignore_above": 1024, "type": "keyword" }, - "project_file": { - "properties": { - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" }, - "stream": { - "properties": { - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "raw_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "raw_code_size": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - } - } - }, - "malware_classification": { - "properties": { - "features": { - "properties": { - "data": { - "properties": { - "buffer": { - "ignore_above": 1024, - "type": "keyword" - }, - "decompressed_size": { - "type": "integer" - }, - "encoding": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "identifier": { - "ignore_above": 1024, - "type": "keyword" - }, - "score": { - "type": "double" - }, - "threshold": { - "type": "double" - }, - "upx_packed": { - "type": "boolean" - }, - "version": { + "product": { "ignore_above": 1024, "type": "keyword" } } }, - "mime_type": { + "size": { + "type": "long" + }, + "target_path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, "ignore_above": 1024, "type": "keyword" }, - "mode": { + "type": { "ignore_above": 1024, "type": "keyword" }, - "mtime": { - "type": "date" - }, - "name": { + "uid": { "ignore_above": 1024, "type": "keyword" - }, - "owner": { + } + } + }, + "geo": { + "properties": { + "city_name": { "ignore_above": 1024, "type": "keyword" }, - "path": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, + "continent_name": { "ignore_above": 1024, "type": "keyword" }, - "pe": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "imphash": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "quarantine_path": { + "country_iso_code": { "ignore_above": 1024, "type": "keyword" }, - "quarantine_result": { - "type": "boolean" - }, - "size": { - "type": "long" - }, - "target_path": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, + "country_name": { "ignore_above": 1024, "type": "keyword" }, - "temp_file_path": { + "location": { + "type": "geo_point" + }, + "name": { "ignore_above": 1024, "type": "keyword" }, - "type": { + "region_iso_code": { "ignore_above": 1024, "type": "keyword" }, - "uid": { + "region_name": { "ignore_above": 1024, "type": "keyword" - }, - "x509": { - "properties": { - "alternative_names": { - "ignore_above": 1024, - "type": "keyword" - }, - "issuer": { - "properties": { - "common_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country": { - "ignore_above": 1024, - "type": "keyword" - }, - "distinguished_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "locality": { - "ignore_above": 1024, - "type": "keyword" - }, - "organization": { - "ignore_above": 1024, - "type": "keyword" - }, - "organizational_unit": { - "ignore_above": 1024, - "type": "keyword" - }, - "state_or_province": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "not_after": { - "type": "date" - }, - "not_before": { - "type": "date" - }, - "public_key_algorithm": { - "ignore_above": 1024, - "type": "keyword" - }, - "public_key_curve": { - "ignore_above": 1024, - "type": "keyword" - }, - "public_key_exponent": { - "doc_values": false, - "index": false, - "type": "long" - }, - "public_key_size": { - "type": "long" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "signature_algorithm": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject": { - "properties": { - "common_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country": { - "ignore_above": 1024, - "type": "keyword" - }, - "distinguished_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "locality": { - "ignore_above": 1024, - "type": "keyword" - }, - "organization": { - "ignore_above": 1024, - "type": "keyword" - }, - "organizational_unit": { - "ignore_above": 1024, - "type": "keyword" - }, - "state_or_province": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "version_number": { - "ignore_above": 1024, - "type": "keyword" - } - } } } }, @@ -4764,6 +4160,26 @@ } } }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, "host": { "properties": { "architecture": { @@ -4862,10 +4278,6 @@ "ignore_above": 1024, "type": "keyword" }, - "variant": { - "ignore_above": 1024, - "type": "keyword" - }, "version": { "ignore_above": 1024, "type": "keyword" @@ -4995,10 +4407,6 @@ }, "status_code": { "type": "long" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" } } }, @@ -5008,19 +4416,27 @@ } } }, + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, "labels": { "type": "object" }, "log": { "properties": { - "file": { - "properties": { - "path": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, "level": { "ignore_above": 1024, "type": "keyword" @@ -5320,10 +4736,6 @@ "ignore_above": 1024, "type": "keyword" }, - "variant": { - "ignore_above": 1024, - "type": "keyword" - }, "version": { "ignore_above": 1024, "type": "keyword" @@ -5370,21 +4782,61 @@ } } }, - "package": { + "os": { "properties": { - "architecture": { + "family": { "ignore_above": 1024, "type": "keyword" }, - "build_version": { + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, "ignore_above": 1024, "type": "keyword" }, - "checksum": { + "kernel": { "ignore_above": 1024, "type": "keyword" }, - "description": { + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "package": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "build_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "checksum": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { "ignore_above": 1024, "type": "keyword" }, @@ -5424,6 +4876,30 @@ } } }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, "process": { "properties": { "args": { @@ -5501,46 +4977,6 @@ } } }, - "malware_classification": { - "properties": { - "features": { - "properties": { - "data": { - "properties": { - "buffer": { - "ignore_above": 1024, - "type": "keyword" - }, - "decompressed_size": { - "type": "integer" - }, - "encoding": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "identifier": { - "ignore_above": 1024, - "type": "keyword" - }, - "score": { - "type": "double" - }, - "threshold": { - "type": "double" - }, - "upx_packed": { - "type": "boolean" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, "name": { "fields": { "text": { @@ -5688,10 +5124,6 @@ }, "pe": { "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, "company": { "ignore_above": 1024, "type": "keyword" @@ -5704,10 +5136,6 @@ "ignore_above": 1024, "type": "keyword" }, - "imphash": { - "ignore_above": 1024, - "type": "keyword" - }, "original_file_name": { "ignore_above": 1024, "type": "keyword" @@ -5727,132 +5155,17 @@ "ppid": { "type": "long" }, - "services": { - "ignore_above": 1024, - "type": "keyword" - }, "start": { "type": "date" }, "thread": { "properties": { - "call_stack": { - "properties": { - "instruction_pointer": { - "ignore_above": 1024, - "type": "keyword" - }, - "memory_section": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "protection": { - "ignore_above": 1024, - "type": "keyword" - }, - "size": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "module_path": { - "ignore_above": 1024, - "type": "keyword" - }, - "rva": { - "ignore_above": 1024, - "type": "keyword" - }, - "symbol_info": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, "id": { "type": "long" }, "name": { "ignore_above": 1024, "type": "keyword" - }, - "service": { - "ignore_above": 1024, - "type": "keyword" - }, - "start": { - "type": "date" - }, - "start_address": { - "ignore_above": 1024, - "type": "keyword" - }, - "start_address_module": { - "ignore_above": 1024, - "type": "keyword" - }, - "token": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "elevation": { - "type": "boolean" - }, - "elevation_type": { - "ignore_above": 1024, - "type": "keyword" - }, - "impersonation_level": { - "ignore_above": 1024, - "type": "keyword" - }, - "integrity_level": { - "type": "long" - }, - "integrity_level_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "is_appcontainer": { - "type": "boolean" - }, - "privileges": { - "properties": { - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "enabled": { - "type": "boolean" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "sid": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "uptime": { - "type": "long" } } }, @@ -5866,70 +5179,9 @@ "ignore_above": 1024, "type": "keyword" }, - "token": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "elevation": { - "type": "boolean" - }, - "elevation_type": { - "ignore_above": 1024, - "type": "keyword" - }, - "impersonation_level": { - "ignore_above": 1024, - "type": "keyword" - }, - "integrity_level": { - "type": "long" - }, - "integrity_level_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "is_appcontainer": { - "type": "boolean" - }, - "privileges": { - "properties": { - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "enabled": { - "type": "boolean" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "sid": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, "uptime": { "type": "long" }, - "user": { - "ignore_above": 1024, - "type": "keyword" - }, "working_directory": { "fields": { "text": { @@ -6342,6 +5594,12 @@ }, "rule": { "properties": { + "author": { + "type": "keyword" + }, + "building_block_type": { + "type": "keyword" + }, "created_at": { "type": "date" }, @@ -6378,6 +5636,9 @@ "language": { "type": "keyword" }, + "license": { + "type": "keyword" + }, "max_signals": { "type": "keyword" }, @@ -6399,28 +5660,60 @@ "risk_score": { "type": "keyword" }, - "rule_id": { - "type": "keyword" + "risk_score_mapping": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "value": { + "type": "keyword" + } + } }, - "saved_id": { + "rule_id": { "type": "keyword" }, - "severity": { + "rule_name_override": { "type": "keyword" }, - "size": { + "saved_id": { "type": "keyword" }, - "tags": { + "severity": { "type": "keyword" }, - "threat": { + "severity_mapping": { "properties": { - "framework": { + "field": { "type": "keyword" }, - "tactic": { - "properties": { + "operator": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + }, + "value": { + "type": "keyword" + } + } + }, + "size": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "threat": { + "properties": { + "framework": { + "type": "keyword" + }, + "tactic": { + "properties": { "id": { "type": "keyword" }, @@ -6453,6 +5746,9 @@ "timeline_title": { "type": "keyword" }, + "timestamp_override": { + "type": "keyword" + }, "to": { "type": "keyword" }, @@ -6539,674 +5835,53 @@ "type": "keyword" }, "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "tags": { - "ignore_above": 1024, - "type": "keyword" - }, - "target": { - "properties": { - "dll": { - "properties": { - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "compile_time": { - "type": "date" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "malware_classification": { - "properties": { - "features": { - "properties": { - "data": { - "properties": { - "buffer": { - "ignore_above": 1024, - "type": "keyword" - }, - "decompressed_size": { - "type": "integer" - }, - "encoding": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "identifier": { - "ignore_above": 1024, - "type": "keyword" - }, - "score": { - "type": "double" - }, - "threshold": { - "type": "double" - }, - "upx_packed": { - "type": "boolean" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "mapped_address": { - "ignore_above": 1024, - "type": "keyword" - }, - "mapped_size": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "pe": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "imphash": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "process": { - "properties": { - "args": { - "ignore_above": 1024, - "type": "keyword" - }, - "args_count": { - "type": "long" - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "command_line": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "entity_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "executable": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "exit_code": { - "type": "long" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "malware_classification": { - "properties": { - "features": { - "properties": { - "data": { - "properties": { - "buffer": { - "ignore_above": 1024, - "type": "keyword" - }, - "decompressed_size": { - "type": "integer" - }, - "encoding": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "identifier": { - "ignore_above": 1024, - "type": "keyword" - }, - "score": { - "type": "double" - }, - "threshold": { - "type": "double" - }, - "upx_packed": { - "type": "boolean" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "parent": { - "properties": { - "args": { - "ignore_above": 1024, - "type": "keyword" - }, - "args_count": { - "type": "long" - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "command_line": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "entity_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "executable": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "exit_code": { - "type": "long" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "pgid": { - "type": "long" - }, - "pid": { - "type": "long" - }, - "ppid": { - "type": "long" - }, - "start": { - "type": "date" - }, - "thread": { - "properties": { - "id": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "title": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "uptime": { - "type": "long" - }, - "working_directory": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pe": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "imphash": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pgid": { - "type": "long" - }, - "pid": { - "type": "long" - }, - "ppid": { - "type": "long" - }, - "services": { - "ignore_above": 1024, - "type": "keyword" - }, - "start": { - "type": "date" - }, - "thread": { - "properties": { - "call_stack": { - "properties": { - "instruction_pointer": { - "ignore_above": 1024, - "type": "keyword" - }, - "memory_section": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "protection": { - "ignore_above": 1024, - "type": "keyword" - }, - "size": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "module_path": { - "ignore_above": 1024, - "type": "keyword" - }, - "rva": { - "ignore_above": 1024, - "type": "keyword" - }, - "symbol_info": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "id": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "service": { - "ignore_above": 1024, - "type": "keyword" - }, - "start": { - "type": "date" - }, - "start_address": { - "ignore_above": 1024, - "type": "keyword" - }, - "start_address_module": { - "ignore_above": 1024, - "type": "keyword" - }, - "token": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "elevation": { - "type": "boolean" - }, - "elevation_type": { - "ignore_above": 1024, - "type": "keyword" - }, - "impersonation_level": { - "ignore_above": 1024, - "type": "keyword" - }, - "integrity_level": { - "type": "long" - }, - "integrity_level_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "is_appcontainer": { - "type": "boolean" - }, - "privileges": { - "properties": { - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "enabled": { - "type": "boolean" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "sid": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "uptime": { - "type": "long" - } - } + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "nat": { + "properties": { + "ip": { + "type": "ip" }, - "title": { + "port": { + "type": "long" + } + } + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { "fields": { "text": { "norms": false, @@ -7216,71 +5891,31 @@ "ignore_above": 1024, "type": "keyword" }, - "token": { + "group": { "properties": { "domain": { "ignore_above": 1024, "type": "keyword" }, - "elevation": { - "type": "boolean" - }, - "elevation_type": { - "ignore_above": 1024, - "type": "keyword" - }, - "impersonation_level": { - "ignore_above": 1024, - "type": "keyword" - }, - "integrity_level": { - "type": "long" - }, - "integrity_level_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "is_appcontainer": { - "type": "boolean" - }, - "privileges": { - "properties": { - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "enabled": { - "type": "boolean" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - }, - "type": "nested" - }, - "sid": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { + "id": { "ignore_above": 1024, "type": "keyword" }, - "user": { + "name": { "ignore_above": 1024, "type": "keyword" } } }, - "uptime": { - "type": "long" + "hash": { + "ignore_above": 1024, + "type": "keyword" }, - "user": { + "id": { "ignore_above": 1024, "type": "keyword" }, - "working_directory": { + "name": { "fields": { "text": { "norms": false, @@ -7294,6 +5929,10 @@ } } }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, "threat": { "properties": { "framework": { @@ -7397,112 +6036,6 @@ "supported_ciphers": { "ignore_above": 1024, "type": "keyword" - }, - "x509": { - "properties": { - "alternative_names": { - "ignore_above": 1024, - "type": "keyword" - }, - "issuer": { - "properties": { - "common_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country": { - "ignore_above": 1024, - "type": "keyword" - }, - "distinguished_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "locality": { - "ignore_above": 1024, - "type": "keyword" - }, - "organization": { - "ignore_above": 1024, - "type": "keyword" - }, - "organizational_unit": { - "ignore_above": 1024, - "type": "keyword" - }, - "state_or_province": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "not_after": { - "type": "date" - }, - "not_before": { - "type": "date" - }, - "public_key_algorithm": { - "ignore_above": 1024, - "type": "keyword" - }, - "public_key_curve": { - "ignore_above": 1024, - "type": "keyword" - }, - "public_key_exponent": { - "doc_values": false, - "index": false, - "type": "long" - }, - "public_key_size": { - "type": "long" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "signature_algorithm": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject": { - "properties": { - "common_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country": { - "ignore_above": 1024, - "type": "keyword" - }, - "distinguished_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "locality": { - "ignore_above": 1024, - "type": "keyword" - }, - "organization": { - "ignore_above": 1024, - "type": "keyword" - }, - "organizational_unit": { - "ignore_above": 1024, - "type": "keyword" - }, - "state_or_province": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "version_number": { - "ignore_above": 1024, - "type": "keyword" - } - } } } }, @@ -7563,112 +6096,6 @@ "subject": { "ignore_above": 1024, "type": "keyword" - }, - "x509": { - "properties": { - "alternative_names": { - "ignore_above": 1024, - "type": "keyword" - }, - "issuer": { - "properties": { - "common_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country": { - "ignore_above": 1024, - "type": "keyword" - }, - "distinguished_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "locality": { - "ignore_above": 1024, - "type": "keyword" - }, - "organization": { - "ignore_above": 1024, - "type": "keyword" - }, - "organizational_unit": { - "ignore_above": 1024, - "type": "keyword" - }, - "state_or_province": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "not_after": { - "type": "date" - }, - "not_before": { - "type": "date" - }, - "public_key_algorithm": { - "ignore_above": 1024, - "type": "keyword" - }, - "public_key_curve": { - "ignore_above": 1024, - "type": "keyword" - }, - "public_key_exponent": { - "doc_values": false, - "index": false, - "type": "long" - }, - "public_key_size": { - "type": "long" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "signature_algorithm": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject": { - "properties": { - "common_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country": { - "ignore_above": 1024, - "type": "keyword" - }, - "distinguished_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "locality": { - "ignore_above": 1024, - "type": "keyword" - }, - "organization": { - "ignore_above": 1024, - "type": "keyword" - }, - "organizational_unit": { - "ignore_above": 1024, - "type": "keyword" - }, - "state_or_province": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "version_number": { - "ignore_above": 1024, - "type": "keyword" - } - } } } }, @@ -7879,10 +6306,6 @@ "ignore_above": 1024, "type": "keyword" }, - "variant": { - "ignore_above": 1024, - "type": "keyword" - }, "version": { "ignore_above": 1024, "type": "keyword" @@ -7895,6 +6318,18 @@ } } }, + "vlan": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, "vulnerability": { "properties": { "category": { From cd508994931d26b0c7266b404b287df8a6eb6b98 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Mon, 6 Jul 2020 21:26:34 +0200 Subject: [PATCH 23/46] fixes and unskips 'export rule' test (#70699) Co-authored-by: Elastic Machine --- .../alerts_detection_rules_export.spec.ts | 7 +- .../test_files/expected_rules_export.ndjson | 2 +- .../es_archives/export_rule/data.json.gz | Bin 0 -> 28233 bytes .../es_archives/export_rule/mappings.json | 6415 +++++++++++++++++ 4 files changed, 6419 insertions(+), 5 deletions(-) create mode 100644 x-pack/test/security_solution_cypress/es_archives/export_rule/data.json.gz create mode 100644 x-pack/test/security_solution_cypress/es_archives/export_rule/mappings.json diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts index 06e9228de4f49..fdab3016de8de 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts @@ -17,10 +17,9 @@ import { ALERTS_URL } from '../urls/navigation'; const EXPECTED_EXPORTED_RULE_FILE_PATH = 'cypress/test_files/expected_rules_export.ndjson'; -// Skipped as was causing failures on master -describe.skip('Export rules', () => { +describe('Export rules', () => { before(() => { - esArchiverLoad('custom_rules'); + esArchiverLoad('export_rule'); cy.server(); cy.route( 'POST', @@ -29,7 +28,7 @@ describe.skip('Export rules', () => { }); after(() => { - esArchiverUnload('custom_rules'); + esArchiverUnload('export_rule'); }); it('Exports a custom rule', () => { diff --git a/x-pack/plugins/security_solution/cypress/test_files/expected_rules_export.ndjson b/x-pack/plugins/security_solution/cypress/test_files/expected_rules_export.ndjson index dcbfa9d0dd16e..7baa59fb3d8c0 100644 --- a/x-pack/plugins/security_solution/cypress/test_files/expected_rules_export.ndjson +++ b/x-pack/plugins/security_solution/cypress/test_files/expected_rules_export.ndjson @@ -1,2 +1,2 @@ -{"actions":[],"created_at":"2020-03-26T10:09:07.569Z","updated_at":"2020-03-26T10:09:08.021Z","created_by":"elastic","description":"Rule 1","enabled":true,"false_positives":[],"filters":[],"from":"now-360s","id":"49db5bd1-bdd5-4821-be26-bb70a815dedb","immutable":false,"index":["apm-*-transaction*","auditbeat-*","endgame-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"0cea4194-03f2-4072-b281-d31b72221d9d","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":50,"name":"Rule 1","query":"host.name:*","references":[],"meta":{"from":"1m","throttle":"no_actions"},"severity":"low","updated_by":"elastic","tags":["rule1"],"to":"now","type":"query","threat":[],"throttle":"no_actions","version":1,"exceptions_list":[]} +{"author":[],"actions":[],"created_at":"2020-07-03T10:44:10.567Z","updated_at":"2020-07-03T10:44:10.941Z","created_by":"elastic","description":"Export rule","enabled":true,"false_positives":[],"filters":[],"from":"now-360s","id":"ad65b1b6-be18-4e41-9d0a-89d8576053d8","immutable":false,"index":["apm-*-transaction*","auditbeat-*","endgame-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"50a3776b-144d-4cff-9f1f-1173e0d5d4a4","language":"kuery","license":"","output_index":".siem-signals-default","max_signals":100,"risk_score":50,"risk_score_mapping":[],"rule_name_override":"","name":"Export rule","query":"host.name: * ","references":[],"meta":{"from":"1m","kibana_siem_app_url":"http://localhost:5620/app/security"},"severity":"low","severity_mapping":[],"updated_by":"elastic","tags":[],"to":"now","type":"query","threat":[],"throttle":"no_actions","timestamp_override":"","version":1,"exceptions_list":[]} {"exported_count":1,"missing_rules":[],"missing_rules_count":0} diff --git a/x-pack/test/security_solution_cypress/es_archives/export_rule/data.json.gz b/x-pack/test/security_solution_cypress/es_archives/export_rule/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..9501cf5e0586e96a97076fd0d71b5db6485eb6ea GIT binary patch literal 28233 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 Date: Mon, 6 Jul 2020 15:38:21 -0400 Subject: [PATCH 24/46] change user facing text Data streams to datasets (#70840) --- .../ingest_manager/constants/page_paths.ts | 4 ++-- .../ingest_manager/hooks/use_breadcrumbs.tsx | 2 +- .../applications/ingest_manager/layouts/default.tsx | 2 +- .../sections/data_stream/list_page/index.tsx | 10 +++++----- .../overview/components/datastream_section.tsx | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts index 9881d5e40d8ab..9f1088a94aa94 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts @@ -53,7 +53,7 @@ export const PAGE_ROUTING_PATHS = { fleet_agent_details_events: '/fleet/agents/:agentId', fleet_agent_details_details: '/fleet/agents/:agentId/details', fleet_enrollment_tokens: '/fleet/enrollment-tokens', - data_streams: '/data-streams', + data_streams: '/datasets', }; export const pagePathGetters: { @@ -80,5 +80,5 @@ export const pagePathGetters: { fleet_agent_details: ({ agentId, tabId }) => `/fleet/agents/${agentId}${tabId ? `/${tabId}` : ''}`, fleet_enrollment_tokens: () => '/fleet/enrollment-tokens', - data_streams: () => '/data-streams', + data_streams: () => '/datasets', }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx index 2b92987963ef6..293638cff50bf 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx @@ -207,7 +207,7 @@ const breadcrumbGetters: { BASE_BREADCRUMB, { text: i18n.translate('xpack.ingestManager.breadcrumbs.datastreamsPageTitle', { - defaultMessage: 'Data streams', + defaultMessage: 'Datasets', }), }, ], diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx index 5e0cba7383e9c..1f356301b714a 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx @@ -103,7 +103,7 @@ export const DefaultLayout: React.FunctionComponent = ({ diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx index e1583d2e426bc..a6e458a4615cd 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx @@ -32,7 +32,7 @@ const DataStreamListPageLayout: React.FunctionComponent = ({ children }) => (

@@ -177,7 +177,7 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => {

} @@ -220,14 +220,14 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { isLoading ? ( ) : dataStreamsData && !dataStreamsData.data_streams.length ? ( emptyPrompt ) : ( ) } @@ -257,7 +257,7 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { placeholder: i18n.translate( 'xpack.ingestManager.dataStreamList.searchPlaceholderTitle', { - defaultMessage: 'Filter data streams', + defaultMessage: 'Filter datasets', } ), incremental: true, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx index 87906afb4122a..eab6cf087e127 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/datastream_section.tsx @@ -51,14 +51,14 @@ export const OverviewDatastreamSection: React.FC = () => {

@@ -70,7 +70,7 @@ export const OverviewDatastreamSection: React.FC = () => { From 984ea0700ee8b84e69f626792e1dd913607307c9 Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Mon, 6 Jul 2020 15:46:30 -0400 Subject: [PATCH 25/46] [Ingest Manager ] prepend kibana asset ids with package name (#70502) * prepend asset ids with package name * fix type * cleanup Co-authored-by: Elastic Machine --- .../services/epm/kibana/assets/install.ts | 119 ++++++++++++++++ .../tests/__snapshots__/install.test.ts.snap | 133 ++++++++++++++++++ .../epm/kibana/assets/tests/dashboard.json | 129 +++++++++++++++++ .../epm/kibana/assets/tests/install.test.ts | 35 +++++ .../services/epm/packages/get_objects.ts | 32 ----- .../server/services/epm/packages/index.ts | 2 +- .../server/services/epm/packages/install.ts | 58 +------- 7 files changed, 419 insertions(+), 89 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts create mode 100644 x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/__snapshots__/install.test.ts.snap create mode 100644 x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/dashboard.json create mode 100644 x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/install.test.ts delete mode 100644 x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts new file mode 100644 index 0000000000000..ae6493d4716e8 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts @@ -0,0 +1,119 @@ +/* + * 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 { + SavedObject, + SavedObjectsBulkCreateObject, + SavedObjectsClientContract, +} from 'src/core/server'; +import * as Registry from '../../registry'; +import { AssetType, KibanaAssetType, AssetReference } from '../../../../types'; + +type SavedObjectToBe = Required & { type: AssetType }; +export type ArchiveAsset = Pick< + SavedObject, + 'id' | 'attributes' | 'migrationVersion' | 'references' +> & { + type: AssetType; +}; + +export async function getKibanaAsset(key: string) { + const buffer = Registry.getAsset(key); + + // cache values are buffers. convert to string / JSON + return JSON.parse(buffer.toString('utf8')); +} + +export function createSavedObjectKibanaAsset( + jsonAsset: ArchiveAsset, + pkgName: string +): SavedObjectToBe { + // convert that to an object + const asset = changeAssetIds(jsonAsset, pkgName); + + return { + type: asset.type, + id: asset.id, + attributes: asset.attributes, + references: asset.references || [], + migrationVersion: asset.migrationVersion || {}, + }; +} + +// modifies id property and the id property of references objects (not index-pattern) +// to be prepended with the package name to distinguish assets from Beats modules' assets +export const changeAssetIds = (asset: ArchiveAsset, pkgName: string): ArchiveAsset => { + const references = asset.references.map((ref) => { + if (ref.type === KibanaAssetType.indexPattern) return ref; + const id = getAssetId(ref.id, pkgName); + return { ...ref, id }; + }); + return { + ...asset, + id: getAssetId(asset.id, pkgName), + references, + }; +}; + +export const getAssetId = (id: string, pkgName: string) => { + return `${pkgName}-${id}`; +}; + +// TODO: make it an exhaustive list +// e.g. switch statement with cases for each enum key returning `never` for default case +export async function installKibanaAssets(options: { + savedObjectsClient: SavedObjectsClientContract; + pkgName: string; + paths: string[]; +}) { + const { savedObjectsClient, paths, pkgName } = options; + + // Only install Kibana assets during package installation. + const kibanaAssetTypes = Object.values(KibanaAssetType); + const installationPromises = kibanaAssetTypes.map((assetType) => + installKibanaSavedObjects({ savedObjectsClient, assetType, paths, pkgName }) + ); + + // installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][] + // call .flat to flatten into one dimensional array + return Promise.all(installationPromises).then((results) => results.flat()); +} + +async function installKibanaSavedObjects({ + savedObjectsClient, + assetType, + paths, + pkgName, +}: { + savedObjectsClient: SavedObjectsClientContract; + assetType: KibanaAssetType; + paths: string[]; + pkgName: string; +}) { + const isSameType = (path: string) => assetType === Registry.pathParts(path).type; + const pathsOfType = paths.filter((path) => isSameType(path)); + const kibanaAssets = await Promise.all(pathsOfType.map((path) => getKibanaAsset(path))); + const toBeSavedObjects = await Promise.all( + kibanaAssets.map((asset) => createSavedObjectKibanaAsset(asset, pkgName)) + ); + + if (toBeSavedObjects.length === 0) { + return []; + } else { + const createResults = await savedObjectsClient.bulkCreate(toBeSavedObjects, { + overwrite: true, + }); + const createdObjects = createResults.saved_objects; + const installed = createdObjects.map(toAssetReference); + return installed; + } +} + +function toAssetReference({ id, type }: SavedObject) { + const reference: AssetReference = { id, type: type as KibanaAssetType }; + + return reference; +} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/__snapshots__/install.test.ts.snap b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/__snapshots__/install.test.ts.snap new file mode 100644 index 0000000000000..638ed4b6118c9 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/__snapshots__/install.test.ts.snap @@ -0,0 +1,133 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`a kibana asset id and its reference ids are appended with package name changeAssetIds output matches snapshot: dashboard.json 1`] = ` +{ + "attributes": { + "description": "Overview dashboard for the Nginx integration in Metrics", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": { + "filter": [], + "highlightAll": true, + "query": { + "language": "kuery", + "query": "" + }, + "version": true + } + }, + "optionsJSON": { + "darkTheme": false, + "hidePanelTitles": false, + "useMargins": true + }, + "panelsJSON": [ + { + "embeddableConfig": {}, + "gridData": { + "h": 12, + "i": "1", + "w": 24, + "x": 24, + "y": 0 + }, + "panelIndex": "1", + "panelRefName": "panel_0", + "version": "7.3.0" + }, + { + "embeddableConfig": {}, + "gridData": { + "h": 12, + "i": "2", + "w": 24, + "x": 24, + "y": 12 + }, + "panelIndex": "2", + "panelRefName": "panel_1", + "version": "7.3.0" + }, + { + "embeddableConfig": {}, + "gridData": { + "h": 12, + "i": "3", + "w": 24, + "x": 0, + "y": 12 + }, + "panelIndex": "3", + "panelRefName": "panel_2", + "version": "7.3.0" + }, + { + "embeddableConfig": {}, + "gridData": { + "h": 12, + "i": "4", + "w": 24, + "x": 0, + "y": 0 + }, + "panelIndex": "4", + "panelRefName": "panel_3", + "version": "7.3.0" + }, + { + "embeddableConfig": {}, + "gridData": { + "h": 12, + "i": "5", + "w": 48, + "x": 0, + "y": 24 + }, + "panelIndex": "5", + "panelRefName": "panel_4", + "version": "7.3.0" + } + ], + "timeRestore": false, + "title": "[Metrics Nginx] Overview ECS", + "version": 1 + }, + "id": "nginx-023d2930-f1a5-11e7-a9ef-93c69af7b129-ecs", + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + { + "id": "metrics-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + }, + { + "id": "nginx-555df8a0-f1a1-11e7-a9ef-93c69af7b129-ecs", + "name": "panel_0", + "type": "search" + }, + { + "id": "nginx-a1d92240-f1a1-11e7-a9ef-93c69af7b129-ecs", + "name": "panel_1", + "type": "map" + }, + { + "id": "nginx-d763a570-f1a1-11e7-a9ef-93c69af7b129-ecs", + "name": "panel_2", + "type": "dashboard" + }, + { + "id": "nginx-47a8e0f0-f1a4-11e7-a9ef-93c69af7b129-ecs", + "name": "panel_3", + "type": "visualization" + }, + { + "id": "nginx-dcbffe30-f1a4-11e7-a9ef-93c69af7b129-ecs", + "name": "panel_4", + "type": "visualization" + } + ], + "type": "dashboard" +} +`; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/dashboard.json b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/dashboard.json new file mode 100644 index 0000000000000..e28a61ae5e18c --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/dashboard.json @@ -0,0 +1,129 @@ +{ + "attributes": { + "description": "Overview dashboard for the Nginx integration in Metrics", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": { + "filter": [], + "highlightAll": true, + "query": { + "language": "kuery", + "query": "" + }, + "version": true + } + }, + "optionsJSON": { + "darkTheme": false, + "hidePanelTitles": false, + "useMargins": true + }, + "panelsJSON": [ + { + "embeddableConfig": {}, + "gridData": { + "h": 12, + "i": "1", + "w": 24, + "x": 24, + "y": 0 + }, + "panelIndex": "1", + "panelRefName": "panel_0", + "version": "7.3.0" + }, + { + "embeddableConfig": {}, + "gridData": { + "h": 12, + "i": "2", + "w": 24, + "x": 24, + "y": 12 + }, + "panelIndex": "2", + "panelRefName": "panel_1", + "version": "7.3.0" + }, + { + "embeddableConfig": {}, + "gridData": { + "h": 12, + "i": "3", + "w": 24, + "x": 0, + "y": 12 + }, + "panelIndex": "3", + "panelRefName": "panel_2", + "version": "7.3.0" + }, + { + "embeddableConfig": {}, + "gridData": { + "h": 12, + "i": "4", + "w": 24, + "x": 0, + "y": 0 + }, + "panelIndex": "4", + "panelRefName": "panel_3", + "version": "7.3.0" + }, + { + "embeddableConfig": {}, + "gridData": { + "h": 12, + "i": "5", + "w": 48, + "x": 0, + "y": 24 + }, + "panelIndex": "5", + "panelRefName": "panel_4", + "version": "7.3.0" + } + ], + "timeRestore": false, + "title": "[Metrics Nginx] Overview ECS", + "version": 1 + }, + "id": "023d2930-f1a5-11e7-a9ef-93c69af7b129-ecs", + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + { + "id": "metrics-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + }, + { + "id": "555df8a0-f1a1-11e7-a9ef-93c69af7b129-ecs", + "name": "panel_0", + "type": "search" + }, + { + "id": "a1d92240-f1a1-11e7-a9ef-93c69af7b129-ecs", + "name": "panel_1", + "type": "map" + }, + { + "id": "d763a570-f1a1-11e7-a9ef-93c69af7b129-ecs", + "name": "panel_2", + "type": "dashboard" + }, + { + "id": "47a8e0f0-f1a4-11e7-a9ef-93c69af7b129-ecs", + "name": "panel_3", + "type": "visualization" + }, + { + "id": "dcbffe30-f1a4-11e7-a9ef-93c69af7b129-ecs", + "name": "panel_4", + "type": "visualization" + } + ], + "type": "dashboard" +} \ No newline at end of file diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/install.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/install.test.ts new file mode 100644 index 0000000000000..f9bc4cdbf203f --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/install.test.ts @@ -0,0 +1,35 @@ +/* + * 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 { readFileSync } from 'fs'; +import path from 'path'; +import { getAssetId, changeAssetIds } from '../install'; + +expect.addSnapshotSerializer({ + print(val) { + return JSON.stringify(val, null, 2); + }, + + test(val) { + return val; + }, +}); + +describe('a kibana asset id and its reference ids are appended with package name', () => { + const assetPath = path.join(__dirname, './dashboard.json'); + const kibanaAsset = JSON.parse(readFileSync(assetPath, 'utf-8')); + const pkgName = 'nginx'; + const modifiedAssetObject = changeAssetIds(kibanaAsset, pkgName); + + test('changeAssetIds output matches snapshot', () => { + expect(modifiedAssetObject).toMatchSnapshot(path.basename(assetPath)); + }); + + test('getAssetId', () => { + const id = '47a8e0f0-f1a4-11e7-a9ef-93c69af7b129-ecs'; + expect(getAssetId(id, pkgName)).toBe(`${pkgName}-${id}`); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts deleted file mode 100644 index b623295c5e060..0000000000000 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts +++ /dev/null @@ -1,32 +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 { SavedObject, SavedObjectsBulkCreateObject } from 'src/core/server'; -import { AssetType } from '../../../types'; -import * as Registry from '../registry'; - -type ArchiveAsset = Pick; -type SavedObjectToBe = Required & { type: AssetType }; - -export async function getObject(key: string) { - const buffer = Registry.getAsset(key); - - // cache values are buffers. convert to string / JSON - const json = buffer.toString('utf8'); - // convert that to an object - const asset: ArchiveAsset = JSON.parse(json); - - const { type, file } = Registry.pathParts(key); - const savedObject: SavedObjectToBe = { - type, - id: file.replace('.json', ''), - attributes: asset.attributes, - references: asset.references || [], - migrationVersion: asset.migrationVersion || {}, - }; - - return savedObject; -} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts index b79f9178ad6af..53ffd5c6e7032 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts @@ -23,7 +23,7 @@ export { SearchParams, } from './get'; -export { installKibanaAssets, installPackage, ensureInstalledPackage } from './install'; +export { installPackage, ensureInstalledPackage } from './install'; export { removeInstallation } from './remove'; type RequiredPackage = 'system' | 'endpoint'; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 910283549abdf..8f73bc9a02765 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -4,13 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; +import { SavedObjectsClientContract } from 'src/core/server'; import Boom from 'boom'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { AssetReference, Installation, - KibanaAssetType, CallESAsCurrentUser, DefaultPackages, ElasticsearchAssetType, @@ -18,7 +17,7 @@ import { } from '../../../types'; import { installIndexPatterns } from '../kibana/index_pattern/install'; import * as Registry from '../registry'; -import { getObject } from './get_objects'; +import { installKibanaAssets } from '../kibana/assets/install'; import { getInstallation, getInstallationObject, isRequiredPackage } from './index'; import { installTemplates } from '../elasticsearch/template/install'; import { generateESIndexPatterns } from '../elasticsearch/template/template'; @@ -121,7 +120,6 @@ export async function installPackage(options: { installKibanaAssets({ savedObjectsClient, pkgName, - pkgVersion, paths, }), installPipelines(registryPackageInfo, paths, callCluster), @@ -185,27 +183,6 @@ export async function installPackage(options: { }); } -// TODO: make it an exhaustive list -// e.g. switch statement with cases for each enum key returning `never` for default case -export async function installKibanaAssets(options: { - savedObjectsClient: SavedObjectsClientContract; - pkgName: string; - pkgVersion: string; - paths: string[]; -}) { - const { savedObjectsClient, paths } = options; - - // Only install Kibana assets during package installation. - const kibanaAssetTypes = Object.values(KibanaAssetType); - const installationPromises = kibanaAssetTypes.map(async (assetType) => - installKibanaSavedObjects({ savedObjectsClient, assetType, paths }) - ); - - // installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][] - // call .flat to flatten into one dimensional array - return Promise.all(installationPromises).then((results) => results.flat()); -} - export async function saveInstallationReferences(options: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; @@ -240,34 +217,3 @@ export async function saveInstallationReferences(options: { return toSaveAssetRefs; } - -async function installKibanaSavedObjects({ - savedObjectsClient, - assetType, - paths, -}: { - savedObjectsClient: SavedObjectsClientContract; - assetType: KibanaAssetType; - paths: string[]; -}) { - const isSameType = (path: string) => assetType === Registry.pathParts(path).type; - const pathsOfType = paths.filter((path) => isSameType(path)); - const toBeSavedObjects = await Promise.all(pathsOfType.map(getObject)); - - if (toBeSavedObjects.length === 0) { - return []; - } else { - const createResults = await savedObjectsClient.bulkCreate(toBeSavedObjects, { - overwrite: true, - }); - const createdObjects = createResults.saved_objects; - const installed = createdObjects.map(toAssetReference); - return installed; - } -} - -function toAssetReference({ id, type }: SavedObject) { - const reference: AssetReference = { id, type: type as KibanaAssetType }; - - return reference; -} From ee0653658d17a62242c56adfac5c59fede2f4d66 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Mon, 6 Jul 2020 15:49:14 -0400 Subject: [PATCH 26/46] Remove the legacy Ingest Manager plugin. (#65534) The last thing we were using from it was configuring a static assets directory (which is only use for the EPM Integrations header graphic). This is now provided by platform and is not configurable https://github.com/elastic/kibana/blob/da28df5b154bd8223124b1814f5b350b842c309d/src/core/MIGRATION.md#L1344 Moved the header assets to the new directory & updated the `toAssets` helper --- x-pack/index.js | 10 +--------- x-pack/legacy/plugins/ingest_manager/index.ts | 14 -------------- .../sections/epm/hooks/use_links.tsx | 5 +---- .../assets/illustration_integrations_darkmode.svg | 0 .../assets/illustration_integrations_lightmode.svg | 0 5 files changed, 2 insertions(+), 27 deletions(-) delete mode 100644 x-pack/legacy/plugins/ingest_manager/index.ts rename x-pack/plugins/ingest_manager/public/{applications/ingest_manager/sections/epm => }/assets/illustration_integrations_darkmode.svg (100%) rename x-pack/plugins/ingest_manager/public/{applications/ingest_manager/sections/epm => }/assets/illustration_integrations_lightmode.svg (100%) diff --git a/x-pack/index.js b/x-pack/index.js index 2d2e42650cfa7..66fe05e8f035e 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -9,15 +9,7 @@ import { monitoring } from './legacy/plugins/monitoring'; import { security } from './legacy/plugins/security'; import { beats } from './legacy/plugins/beats_management'; import { spaces } from './legacy/plugins/spaces'; -import { ingestManager } from './legacy/plugins/ingest_manager'; module.exports = function (kibana) { - return [ - xpackMain(kibana), - monitoring(kibana), - spaces(kibana), - security(kibana), - ingestManager(kibana), - beats(kibana), - ]; + return [xpackMain(kibana), monitoring(kibana), spaces(kibana), security(kibana), beats(kibana)]; }; diff --git a/x-pack/legacy/plugins/ingest_manager/index.ts b/x-pack/legacy/plugins/ingest_manager/index.ts deleted file mode 100644 index 2b20bf16f2400..0000000000000 --- a/x-pack/legacy/plugins/ingest_manager/index.ts +++ /dev/null @@ -1,14 +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 { resolve } from 'path'; - -export function ingestManager(kibana: any) { - return new kibana.Plugin({ - id: 'ingestManager', - require: ['kibana', 'elasticsearch', 'xpack_main'], - publicDir: resolve(__dirname, '../../../plugins/ingest_manager/public'), - }); -} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_links.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_links.tsx index 436163bafcfe4..a453a7f2e28cb 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_links.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_links.tsx @@ -13,10 +13,7 @@ const removeRelativePath = (relativePath: string): string => export function useLinks() { const { http } = useCore(); return { - toAssets: (path: string) => - http.basePath.prepend( - `/plugins/${PLUGIN_ID}/applications/ingest_manager/sections/epm/assets/${path}` - ), + toAssets: (path: string) => http.basePath.prepend(`/plugins/${PLUGIN_ID}/assets/${path}`), toImage: (path: string) => http.basePath.prepend(epmRouteService.getFilePath(path)), toRelativeImage: ({ path, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/assets/illustration_integrations_darkmode.svg b/x-pack/plugins/ingest_manager/public/assets/illustration_integrations_darkmode.svg similarity index 100% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/assets/illustration_integrations_darkmode.svg rename to x-pack/plugins/ingest_manager/public/assets/illustration_integrations_darkmode.svg diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/assets/illustration_integrations_lightmode.svg b/x-pack/plugins/ingest_manager/public/assets/illustration_integrations_lightmode.svg similarity index 100% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/assets/illustration_integrations_lightmode.svg rename to x-pack/plugins/ingest_manager/public/assets/illustration_integrations_lightmode.svg From eb84503d8abff3d80a0c8762b405dd815d350914 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 6 Jul 2020 12:56:26 -0700 Subject: [PATCH 27/46] upgrade caniuse-lite database (#70833) Co-authored-by: spalger --- yarn.lock | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/yarn.lock b/yarn.lock index eb1943c5cd00c..5efea82e84c68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9550,20 +9550,10 @@ can-use-dom@^0.1.0: resolved "https://registry.yarnpkg.com/can-use-dom/-/can-use-dom-0.1.0.tgz#22cc4a34a0abc43950f42c6411024a3f6366b45a" integrity sha1-IsxKNKCrxDlQ9CxkEQJKP2NmtFo= -caniuse-lite@^1.0.30000984, caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001022: - version "1.0.30001022" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001022.tgz#9eeffe580c3a8f110b7b1742dcf06a395885e4c6" - integrity sha512-FjwPPtt/I07KyLPkBQ0g7/XuZg6oUkYBVnPHNj3VHJbOjmmJ/GdSo/GUY6MwINEQvjhP6WZVbX8Tvms8xh0D5A== - -caniuse-lite@^1.0.30001035: - version "1.0.30001036" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001036.tgz#930ea5272010d8bf190d859159d757c0b398caf0" - integrity sha512-jU8CIFIj2oR7r4W+5AKcsvWNVIb6Q6OZE3UsrXrZBHFtreT4YgTeOJtTucp+zSedEpTi3L5wASSP0LYIE3if6w== - -caniuse-lite@^1.0.30001043: - version "1.0.30001079" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001079.tgz#ed3e5225cd9a6850984fdd88bf24ce45d69b9c22" - integrity sha512-2KaYheg0iOY+CMmDuAB3DHehrXhhb4OZU4KBVGDr/YKyYAcpudaiUQ9PJ9rxrPlKEoJ3ATasQ5AN48MqpwS43Q== +caniuse-lite@^1.0.30000984, caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001022, caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001043: + version "1.0.30001094" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001094.tgz#0b11d02e1cdc201348dbd8e3e57bd9b6ce82b175" + integrity sha512-ufHZNtMaDEuRBpTbqD93tIQnngmJ+oBknjvr0IbFympSdtFpAUFmNv4mVKbb53qltxFx0nK3iy32S9AqkLzUNA== canvas@^2.6.1: version "2.6.1" From 11cfe80020d2fba1ab02ef8517e896744c85e35e Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Mon, 6 Jul 2020 15:33:27 -0500 Subject: [PATCH 28/46] [Metrics UI] Fix a bug in Metric Threshold query filter construction (#70672) Co-authored-by: Elastic Machine --- .../metric_threshold/lib/metric_query.test.ts | 59 +++++++++++++++++++ .../metric_threshold/lib/metric_query.ts | 13 ++-- 2 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts new file mode 100644 index 0000000000000..3ad1031f574e2 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.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 { MetricExpressionParams } from '../types'; +import { getElasticsearchMetricQuery } from './metric_query'; + +describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => { + const expressionParams = { + metric: 'system.is.a.good.puppy.dog', + aggType: 'avg', + timeUnit: 'm', + timeSize: 1, + } as MetricExpressionParams; + + const timefield = '@timestamp'; + const groupBy = 'host.doggoname'; + + describe('when passed no filterQuery', () => { + const searchBody = getElasticsearchMetricQuery(expressionParams, timefield, groupBy); + test('includes a range filter', () => { + expect( + searchBody.query.bool.filter.find((filter) => filter.hasOwnProperty('range')) + ).toBeTruthy(); + }); + + test('includes a metric field filter', () => { + expect(searchBody.query.bool.filter).toMatchObject( + expect.arrayContaining([{ exists: { field: 'system.is.a.good.puppy.dog' } }]) + ); + }); + }); + + describe('when passed a filterQuery', () => { + const filterQuery = + // This is adapted from a real-world query that previously broke alerts + // We want to make sure it doesn't override any existing filters + '{"bool":{"filter":[{"bool":{"filter":[{"bool":{"must_not":[{"bool":{"should":[{"query_string":{"query":"bark*","fields":["host.name^1.0"],"type":"best_fields","default_operator":"or","max_determinized_states":10000,"enable_position_increments":true,"fuzziness":"AUTO","fuzzy_prefix_length":0,"fuzzy_max_expansions":50,"phrase_slop":0,"escape":false,"auto_generate_synonyms_phrase_query":true,"fuzzy_transpositions":true,"boost":1}}],"adjust_pure_negative":true,"minimum_should_match":"1","boost":1}}],"adjust_pure_negative":true,"boost":1}},{"bool":{"must_not":[{"bool":{"should":[{"query_string":{"query":"woof*","fields":["host.name^1.0"],"type":"best_fields","default_operator":"or","max_determinized_states":10000,"enable_position_increments":true,"fuzziness":"AUTO","fuzzy_prefix_length":0,"fuzzy_max_expansions":50,"phrase_slop":0,"escape":false,"auto_generate_synonyms_phrase_query":true,"fuzzy_transpositions":true,"boost":1}}],"adjust_pure_negative":true,"minimum_should_match":"1","boost":1}}],"adjust_pure_negative":true,"boost":1}}],"adjust_pure_negative":true,"boost":1}}],"adjust_pure_negative":true,"boost":1}}'; + + const searchBody = getElasticsearchMetricQuery( + expressionParams, + timefield, + groupBy, + filterQuery + ); + test('includes a range filter', () => { + expect( + searchBody.query.bool.filter.find((filter) => filter.hasOwnProperty('range')) + ).toBeTruthy(); + }); + + test('includes a metric field filter', () => { + expect(searchBody.query.bool.filter).toMatchObject( + expect.arrayContaining([{ exists: { field: 'system.is.a.good.puppy.dog' } }]) + ); + }); + }); +}); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts index 5680035d9d609..15506a30529c4 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts @@ -11,11 +11,11 @@ import { createPercentileAggregation } from './create_percentile_aggregation'; const MINIMUM_BUCKETS = 5; -const getParsedFilterQuery: ( - filterQuery: string | undefined -) => Record | Array> = (filterQuery) => { - if (!filterQuery) return {}; - return JSON.parse(filterQuery).bool; +const getParsedFilterQuery: (filterQuery: string | undefined) => Record | null = ( + filterQuery +) => { + if (!filterQuery) return null; + return JSON.parse(filterQuery); }; export const getElasticsearchMetricQuery = ( @@ -129,9 +129,8 @@ export const getElasticsearchMetricQuery = ( filter: [ ...rangeFilters, ...metricFieldFilters, - ...(Array.isArray(parsedFilterQuery) ? parsedFilterQuery : []), + ...(parsedFilterQuery ? [parsedFilterQuery] : []), ], - ...(!Array.isArray(parsedFilterQuery) ? parsedFilterQuery : {}), }, }, size: 0, From ad20a17bc6c287e4edba4ef57762818f8996988c Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 6 Jul 2020 22:09:19 +0100 Subject: [PATCH 29/46] skip flaky suite (#70880) --- x-pack/test/functional/apps/security/field_level_security.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/security/field_level_security.js b/x-pack/test/functional/apps/security/field_level_security.js index 7b22d72885c9d..20b13ad935f93 100644 --- a/x-pack/test/functional/apps/security/field_level_security.js +++ b/x-pack/test/functional/apps/security/field_level_security.js @@ -14,7 +14,8 @@ export default function ({ getService, getPageObjects }) { const log = getService('log'); const PageObjects = getPageObjects(['security', 'settings', 'common', 'discover', 'header']); - describe('field_level_security', () => { + // Skipped as it was failing on ES Promotion: https://github.com/elastic/kibana/issues/70880 + describe.skip('field_level_security', () => { before('initialize tests', async () => { await esArchiver.loadIfNeeded('security/flstest/data'); //( data) await esArchiver.load('security/flstest/kibana'); //(savedobject) From 7debf4dd9f8818cb232df6dc2fbf57a8b6bd1bb8 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Mon, 6 Jul 2020 14:12:15 -0700 Subject: [PATCH 30/46] [Ingest Manager] Support limiting integrations on an agent config (#70542) * Add API endpoint and hook for retrieving restricted packages * Filter out restricted packages already in use from list of integrations available for an agent config * Allow list agent configs to optionally return expanded package configs, re * Filter out agent configs which already use the restricted package already from list of agent configs available for an integration * Allow more than 20 agent configs to be shown * Rename restricted to limited; add some common methods to DRY * Add limited package check on server side * Adjust copy wording * Fix typings * Add some package config api integration tests, update es archive mappings * Move test to dockerized integation tests directory; move existing epm tests to their own directory * Remove extra assignPackageConfigs() - already handled in packageConfigService.create() * Review fixes * Fix type, reenabled skipped test * Move new EPM integration test file --- .../ingest_manager/common/constants/routes.ts | 1 + .../ingest_manager/common/services/index.ts | 5 +- .../common/services/limited_package.ts | 23 ++++ .../ingest_manager/common/services/routes.ts | 4 + .../ingest_manager/common/types/models/epm.ts | 1 + .../common/types/rest_spec/agent_config.ts | 4 +- .../common/types/rest_spec/common.ts | 3 +- .../common/types/rest_spec/epm.ts | 5 + .../hooks/use_request/agent_config.ts | 4 +- .../ingest_manager/hooks/use_request/epm.ts | 8 ++ .../step_select_config.tsx | 25 +++- .../step_select_package.tsx | 28 +++- .../ingest_manager/services/index.ts | 4 +- .../ingest_manager/types/index.ts | 2 + .../server/routes/agent_config/handlers.ts | 15 +- .../server/routes/epm/handlers.ts | 35 ++++- .../ingest_manager/server/routes/epm/index.ts | 10 ++ .../routes/package_config/handlers.test.ts | 6 +- .../server/routes/package_config/handlers.ts | 24 +--- .../server/services/agent_config.ts | 44 ++++-- .../server/services/epm/packages/get.ts | 23 ++++ .../server/services/epm/packages/index.ts | 4 +- .../server/services/package_config.ts | 39 +++++- .../ingest_manager/server/services/setup.ts | 8 +- .../server/types/rest_spec/agent_config.ts | 4 +- .../es_archives/fleet/agents/mappings.json | 30 ++-- .../apis/{ => epm}/file.ts | 6 +- .../apis/{ => epm}/ilm.ts | 4 +- .../apis/{ => epm}/install.ts | 4 +- .../apis/{ => epm}/list.ts | 6 +- .../apis/{ => epm}/template.ts | 6 +- .../apis/index.js | 17 ++- .../apis/package_config/create.ts | 130 ++++++++++++++++++ 33 files changed, 429 insertions(+), 103 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/common/services/limited_package.ts rename x-pack/test/ingest_manager_api_integration/apis/{ => epm}/file.ts (94%) rename x-pack/test/ingest_manager_api_integration/apis/{ => epm}/ilm.ts (89%) rename x-pack/test/ingest_manager_api_integration/apis/{ => epm}/install.ts (95%) rename x-pack/test/ingest_manager_api_integration/apis/{ => epm}/list.ts (87%) rename x-pack/test/ingest_manager_api_integration/apis/{ => epm}/template.ts (88%) create mode 100644 x-pack/test/ingest_manager_api_integration/apis/package_config/create.ts diff --git a/x-pack/plugins/ingest_manager/common/constants/routes.ts b/x-pack/plugins/ingest_manager/common/constants/routes.ts index dad3cdce1a497..7c3b5a198571c 100644 --- a/x-pack/plugins/ingest_manager/common/constants/routes.ts +++ b/x-pack/plugins/ingest_manager/common/constants/routes.ts @@ -17,6 +17,7 @@ const EPM_PACKAGES_ONE = `${EPM_PACKAGES_MANY}/{pkgkey}`; const EPM_PACKAGES_FILE = `${EPM_PACKAGES_MANY}/{pkgName}/{pkgVersion}`; export const EPM_API_ROUTES = { LIST_PATTERN: EPM_PACKAGES_MANY, + LIMITED_LIST_PATTERN: `${EPM_PACKAGES_MANY}/limited`, INFO_PATTERN: EPM_PACKAGES_ONE, INSTALL_PATTERN: EPM_PACKAGES_ONE, DELETE_PATTERN: EPM_PACKAGES_ONE, diff --git a/x-pack/plugins/ingest_manager/common/services/index.ts b/x-pack/plugins/ingest_manager/common/services/index.ts index a0db7c20747e2..0c91dbbe10354 100644 --- a/x-pack/plugins/ingest_manager/common/services/index.ts +++ b/x-pack/plugins/ingest_manager/common/services/index.ts @@ -3,11 +3,10 @@ * 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 AgentStatusKueryHelper from './agent_status'; - export * from './routes'; +export * as AgentStatusKueryHelper from './agent_status'; export { packageToPackageConfigInputs, packageToPackageConfig } from './package_to_config'; export { storedPackageConfigsToAgentInputs } from './package_configs_to_agent_inputs'; export { configToYaml } from './config_to_yaml'; -export { AgentStatusKueryHelper }; +export { isPackageLimited, doesAgentConfigAlreadyIncludePackage } from './limited_package'; export { decodeCloudId } from './decode_cloud_id'; diff --git a/x-pack/plugins/ingest_manager/common/services/limited_package.ts b/x-pack/plugins/ingest_manager/common/services/limited_package.ts new file mode 100644 index 0000000000000..7ef445d55063c --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/services/limited_package.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 { PackageInfo, AgentConfig, PackageConfig } from '../types'; + +// Assume packages only ever include 1 config template for now +export const isPackageLimited = (packageInfo: PackageInfo): boolean => { + return packageInfo.config_templates?.[0]?.multiple === false; +}; + +export const doesAgentConfigAlreadyIncludePackage = ( + agentConfig: AgentConfig, + packageName: string +): boolean => { + if (agentConfig.package_configs.length && typeof agentConfig.package_configs[0] === 'string') { + throw new Error('Unable to read full package config information'); + } + return (agentConfig.package_configs as PackageConfig[]) + .map((packageConfig) => packageConfig.package?.name || '') + .includes(packageName); +}; diff --git a/x-pack/plugins/ingest_manager/common/services/routes.ts b/x-pack/plugins/ingest_manager/common/services/routes.ts index 463a18887174c..49de9a4d8fd85 100644 --- a/x-pack/plugins/ingest_manager/common/services/routes.ts +++ b/x-pack/plugins/ingest_manager/common/services/routes.ts @@ -27,6 +27,10 @@ export const epmRouteService = { return EPM_API_ROUTES.LIST_PATTERN; }, + getListLimitedPath: () => { + return EPM_API_ROUTES.LIMITED_LIST_PATTERN; + }, + getInfoPath: (pkgkey: string) => { return EPM_API_ROUTES.INFO_PATTERN.replace('{pkgkey}', pkgkey); }, diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index 0d2825f0aa80d..23e31227cbf3c 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -79,6 +79,7 @@ export interface RegistryConfigTemplate { title: string; description: string; inputs: RegistryInput[]; + multiple?: boolean; } export interface RegistryInput { diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_config.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_config.ts index 86020cb5235ae..4e1612d144ede 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_config.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_config.ts @@ -7,7 +7,9 @@ import { AgentConfig, NewAgentConfig, FullAgentConfig } from '../models'; import { ListWithKuery } from './common'; export interface GetAgentConfigsRequest { - query: ListWithKuery; + query: ListWithKuery & { + full?: boolean; + }; } export type GetAgentConfigsResponseItem = AgentConfig & { agents?: number }; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/common.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/common.ts index 0d1f72afa16f1..a454e39c203ed 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/common.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/common.ts @@ -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 { HttpFetchQuery } from 'src/core/public'; -export interface ListWithKuery { +export interface ListWithKuery extends HttpFetchQuery { page?: number; perPage?: number; sortField?: string; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts index 5ac7fe9e2779b..c5035d2d44432 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts @@ -34,6 +34,11 @@ export interface GetPackagesResponse { success: boolean; } +export interface GetLimitedPackagesResponse { + response: string[]; + success: boolean; +} + export interface GetFileRequest { params: { pkgkey: string; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts index c81303de3d7c3..56b78c6faa93a 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { HttpFetchQuery } from 'src/core/public'; import { useRequest, sendRequest, @@ -12,6 +11,7 @@ import { } from './use_request'; import { agentConfigRouteService } from '../../services'; import { + GetAgentConfigsRequest, GetAgentConfigsResponse, GetOneAgentConfigResponse, GetFullAgentConfigResponse, @@ -25,7 +25,7 @@ import { DeleteAgentConfigResponse, } from '../../types'; -export const useGetAgentConfigs = (query: HttpFetchQuery = {}) => { +export const useGetAgentConfigs = (query?: GetAgentConfigsRequest['query']) => { return useRequest({ path: agentConfigRouteService.getListPath(), method: 'get', diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/epm.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/epm.ts index 128ef8de68aae..64bee1763b08b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/epm.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/epm.ts @@ -10,6 +10,7 @@ import { epmRouteService } from '../../services'; import { GetCategoriesResponse, GetPackagesResponse, + GetLimitedPackagesResponse, GetInfoResponse, InstallPackageResponse, DeletePackageResponse, @@ -30,6 +31,13 @@ export const useGetPackages = (query: HttpFetchQuery = {}) => { }); }; +export const useGetLimitedPackages = () => { + return useRequest({ + path: epmRouteService.getListLimitedPath(), + method: 'get', + }); +}; + export const useGetPackageInfoByKey = (pkgkey: string) => { return useRequest({ path: epmRouteService.getInfoPath(pkgkey), diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx index 849d7bfc63f34..f6391cf1fa456 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx @@ -9,6 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiSelectable, EuiSpacer, EuiTextColor } from '@elastic/eui'; import { Error } from '../../../components'; import { AgentConfig, PackageInfo, GetAgentConfigsResponseItem } from '../../../types'; +import { isPackageLimited, doesAgentConfigAlreadyIncludePackage } from '../../../services'; import { useGetPackageInfoByKey, useGetAgentConfigs, sendGetOneAgentConfig } from '../../../hooks'; export const StepSelectConfig: React.FunctionComponent<{ @@ -24,7 +25,12 @@ export const StepSelectConfig: React.FunctionComponent<{ const [selectedConfigError, setSelectedConfigError] = useState(); // Fetch package info - const { data: packageInfoData, error: packageInfoError } = useGetPackageInfoByKey(pkgkey); + const { + data: packageInfoData, + error: packageInfoError, + isLoading: packageInfoLoading, + } = useGetPackageInfoByKey(pkgkey); + const isLimitedPackage = (packageInfoData && isPackageLimited(packageInfoData.response)) || false; // Fetch agent configs info const { @@ -36,6 +42,7 @@ export const StepSelectConfig: React.FunctionComponent<{ perPage: 1000, sortField: 'name', sortOrder: 'asc', + full: true, }); const agentConfigs = agentConfigsData?.items || []; const agentConfigsById = agentConfigs.reduce( @@ -112,12 +119,18 @@ export const StepSelectConfig: React.FunctionComponent<{ searchable allowExclusions={false} singleSelection={true} - isLoading={isAgentConfigsLoading} - options={agentConfigs.map(({ id, name, description }) => { + isLoading={isAgentConfigsLoading || packageInfoLoading} + options={agentConfigs.map((agentConf) => { + const alreadyHasLimitedPackage = + (isLimitedPackage && + packageInfoData && + doesAgentConfigAlreadyIncludePackage(agentConf, packageInfoData.response.name)) || + false; return { - label: name, - key: id, - checked: selectedConfigId === id ? 'on' : undefined, + label: agentConf.name, + key: agentConf.id, + checked: selectedConfigId === agentConf.id ? 'on' : undefined, + disabled: alreadyHasLimitedPackage, 'data-test-subj': 'agentConfigItem', }; })} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_package.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_package.tsx index e4f4c976688b1..204b862bd4dc4 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_package.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_package.tsx @@ -8,8 +8,13 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiSelectable, EuiSpacer } from '@elastic/eui'; import { Error } from '../../../components'; -import { AgentConfig, PackageInfo } from '../../../types'; -import { useGetOneAgentConfig, useGetPackages, sendGetPackageInfoByKey } from '../../../hooks'; +import { AgentConfig, PackageInfo, PackageConfig, GetPackagesResponse } from '../../../types'; +import { + useGetOneAgentConfig, + useGetPackages, + useGetLimitedPackages, + sendGetPackageInfoByKey, +} from '../../../hooks'; import { PackageIcon } from '../../../components/package_icon'; export const StepSelectPackage: React.FunctionComponent<{ @@ -28,12 +33,27 @@ export const StepSelectPackage: React.FunctionComponent<{ const { data: agentConfigData, error: agentConfigError } = useGetOneAgentConfig(agentConfigId); // Fetch packages info + // Filter out limited packages already part of selected agent config + const [packages, setPackages] = useState([]); const { data: packagesData, error: packagesError, isLoading: isPackagesLoading, } = useGetPackages(); - const packages = packagesData?.response || []; + const { + data: limitedPackagesData, + isLoading: isLimitedPackagesLoading, + } = useGetLimitedPackages(); + useEffect(() => { + if (packagesData?.response && limitedPackagesData?.response && agentConfigData?.item) { + const allPackages = packagesData.response; + const limitedPackages = limitedPackagesData.response; + const usedLimitedPackages = (agentConfigData.item.package_configs as PackageConfig[]) + .map((packageConfig) => packageConfig.package?.name || '') + .filter((pkgName) => limitedPackages.includes(pkgName)); + setPackages(allPackages.filter((pkg) => !usedLimitedPackages.includes(pkg.name))); + } + }, [packagesData, limitedPackagesData, agentConfigData]); // Update parent agent config state useEffect(() => { @@ -101,7 +121,7 @@ export const StepSelectPackage: React.FunctionComponent<{ searchable allowExclusions={false} singleSelection={true} - isLoading={isPackagesLoading} + isLoading={isPackagesLoading || isLimitedPackagesLoading} options={packages.map(({ title, name, version, icons }) => { const pkgkey = `${name}-${version}`; return { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts index 5dc9026aebdee..9c3b84d0835b8 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts @@ -7,6 +7,7 @@ export { getFlattenedObject } from '../../../../../../../src/core/public'; export { + AgentStatusKueryHelper, agentConfigRouteService, packageConfigRouteService, dataStreamRouteService, @@ -21,5 +22,6 @@ export { packageToPackageConfigInputs, storedPackageConfigsToAgentInputs, configToYaml, - AgentStatusKueryHelper, + isPackageLimited, + doesAgentConfigAlreadyIncludePackage, } from '../../../../common'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts index e28d76cae9955..9cd8a75642296 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts @@ -24,6 +24,7 @@ export { // API schema - misc setup, status GetFleetStatusResponse, // API schemas - Agent Config + GetAgentConfigsRequest, GetAgentConfigsResponse, GetAgentConfigsResponseItem, GetOneAgentConfigResponse, @@ -92,6 +93,7 @@ export { ServiceName, GetCategoriesResponse, GetPackagesResponse, + GetLimitedPackagesResponse, GetInfoResponse, InstallPackageResponse, DeletePackageResponse, diff --git a/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts index 7b12a076ff041..110f6b9950829 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts @@ -38,8 +38,12 @@ export const getAgentConfigsHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const { full: withPackageConfigs = false, ...restOfQuery } = request.query; try { - const { items, total, page, perPage } = await agentConfigService.list(soClient, request.query); + const { items, total, page, perPage } = await agentConfigService.list(soClient, { + withPackageConfigs, + ...restOfQuery, + }); const body: GetAgentConfigsResponse = { items, total, @@ -103,6 +107,7 @@ export const createAgentConfigHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; const withSysMonitoring = request.query.sys_monitoring ?? false; try { @@ -128,15 +133,9 @@ export const createAgentConfigHandler: RequestHandler< if (withSysMonitoring && newSysPackageConfig !== undefined && agentConfig !== undefined) { newSysPackageConfig.config_id = agentConfig.id; newSysPackageConfig.namespace = agentConfig.namespace; - const sysPackageConfig = await packageConfigService.create(soClient, newSysPackageConfig, { + await packageConfigService.create(soClient, callCluster, newSysPackageConfig, { user, }); - - if (sysPackageConfig) { - agentConfig = await agentConfigService.assignPackageConfigs(soClient, agentConfig.id, [ - sysPackageConfig.id, - ]); - } } const body: CreateAgentConfigResponse = { diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts index eaf0e1a104b3e..a50b3b13faeab 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -5,20 +5,21 @@ */ import { TypeOf } from '@kbn/config-schema'; import { RequestHandler, CustomHttpResponseOptions } from 'src/core/server'; -import { - GetPackagesRequestSchema, - GetFileRequestSchema, - GetInfoRequestSchema, - InstallPackageRequestSchema, - DeletePackageRequestSchema, -} from '../../types'; import { GetInfoResponse, InstallPackageResponse, DeletePackageResponse, GetCategoriesResponse, GetPackagesResponse, + GetLimitedPackagesResponse, } from '../../../common'; +import { + GetPackagesRequestSchema, + GetFileRequestSchema, + GetInfoRequestSchema, + InstallPackageRequestSchema, + DeletePackageRequestSchema, +} from '../../types'; import { getCategories, getPackages, @@ -26,6 +27,7 @@ import { getPackageInfo, installPackage, removeInstallation, + getLimitedPackages, } from '../../services/epm/packages'; export const getCategoriesHandler: RequestHandler = async (context, request, response) => { @@ -69,6 +71,25 @@ export const getListHandler: RequestHandler< } }; +export const getLimitedListHandler: RequestHandler = async (context, request, response) => { + try { + const savedObjectsClient = context.core.savedObjects.client; + const res = await getLimitedPackages({ savedObjectsClient }); + const body: GetLimitedPackagesResponse = { + response: res, + success: true, + }; + return response.ok({ + body, + }); + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; + export const getFileHandler: RequestHandler> = async ( context, request, diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/index.ts b/x-pack/plugins/ingest_manager/server/routes/epm/index.ts index fcf81f9894d5e..ffaf0ce46c89a 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/index.ts @@ -8,6 +8,7 @@ import { PLUGIN_ID, EPM_API_ROUTES } from '../../constants'; import { getCategoriesHandler, getListHandler, + getLimitedListHandler, getFileHandler, getInfoHandler, installPackageHandler, @@ -40,6 +41,15 @@ export const registerRoutes = (router: IRouter) => { getListHandler ); + router.get( + { + path: EPM_API_ROUTES.LIMITED_LIST_PATTERN, + validate: false, + options: { tags: [`access:${PLUGIN_ID}`] }, + }, + getLimitedListHandler + ); + router.get( { path: EPM_API_ROUTES.FILEPATH_PATTERN, diff --git a/x-pack/plugins/ingest_manager/server/routes/package_config/handlers.test.ts b/x-pack/plugins/ingest_manager/server/routes/package_config/handlers.test.ts index 6d712ce063290..85ecc5027d64d 100644 --- a/x-pack/plugins/ingest_manager/server/routes/package_config/handlers.test.ts +++ b/x-pack/plugins/ingest_manager/server/routes/package_config/handlers.test.ts @@ -25,7 +25,7 @@ jest.mock('../../services/package_config', (): { assignPackageStream: jest.fn((packageInfo, dataInputs) => Promise.resolve(dataInputs)), buildPackageConfigFromPackage: jest.fn(), bulkCreate: jest.fn(), - create: jest.fn((soClient, newData) => + create: jest.fn((soClient, callCluster, newData) => Promise.resolve({ ...newData, id: '1', @@ -213,7 +213,7 @@ describe('When calling package config', () => { const request = getCreateKibanaRequest(); await routeHandler(context, request, response); expect(response.ok).toHaveBeenCalled(); - expect(packageConfigServiceMock.create.mock.calls[0][1]).toEqual({ + expect(packageConfigServiceMock.create.mock.calls[0][2]).toEqual({ config_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c', description: '', enabled: true, @@ -294,7 +294,7 @@ describe('When calling package config', () => { const request = getCreateKibanaRequest(); await routeHandler(context, request, response); expect(response.ok).toHaveBeenCalled(); - expect(packageConfigServiceMock.create.mock.calls[0][1]).toEqual({ + expect(packageConfigServiceMock.create.mock.calls[0][2]).toEqual({ config_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c', description: '', enabled: true, diff --git a/x-pack/plugins/ingest_manager/server/routes/package_config/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/package_config/handlers.ts index f11275c92bb68..6b0c2fe9c2ff7 100644 --- a/x-pack/plugins/ingest_manager/server/routes/package_config/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/package_config/handlers.ts @@ -7,7 +7,7 @@ import { TypeOf } from '@kbn/config-schema'; import Boom from 'boom'; import { RequestHandler } from 'src/core/server'; import { appContextService, packageConfigService } from '../../services'; -import { ensureInstalledPackage, getPackageInfo } from '../../services/epm/packages'; +import { getPackageInfo } from '../../services/epm/packages'; import { GetPackageConfigsRequestSchema, GetOnePackageConfigRequestSchema, @@ -106,26 +106,10 @@ export const createPackageConfigHandler: RequestHandler< newData = updatedNewData; } - // Make sure the associated package is installed - if (newData.package?.name) { - await ensureInstalledPackage({ - savedObjectsClient: soClient, - pkgName: newData.package.name, - callCluster, - }); - const pkgInfo = await getPackageInfo({ - savedObjectsClient: soClient, - pkgName: newData.package.name, - pkgVersion: newData.package.version, - }); - newData.inputs = (await packageConfigService.assignPackageStream( - pkgInfo, - newData.inputs - )) as TypeOf['inputs']; - } - // Create package config - const packageConfig = await packageConfigService.create(soClient, newData, { user }); + const packageConfig = await packageConfigService.create(soClient, callCluster, newData, { + user, + }); const body: CreatePackageConfigResponse = { item: packageConfig, success: true }; return response.ok({ body, diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.ts index bd00727714c33..fe247d5b91db0 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts @@ -141,11 +141,20 @@ class AgentConfigService { public async list( soClient: SavedObjectsClientContract, - options: ListWithKuery + options: ListWithKuery & { + withPackageConfigs?: boolean; + } ): Promise<{ items: AgentConfig[]; total: number; page: number; perPage: number }> { - const { page = 1, perPage = 20, sortField = 'updated_at', sortOrder = 'desc', kuery } = options; - - const agentConfigs = await soClient.find({ + const { + page = 1, + perPage = 20, + sortField = 'updated_at', + sortOrder = 'desc', + kuery, + withPackageConfigs = false, + } = options; + + const agentConfigsSO = await soClient.find({ type: SAVED_OBJECT_TYPE, sortField, sortOrder, @@ -160,12 +169,29 @@ class AgentConfigService { : undefined, }); + const agentConfigs = await Promise.all( + agentConfigsSO.saved_objects.map(async (agentConfigSO) => { + const agentConfig = { + id: agentConfigSO.id, + ...agentConfigSO.attributes, + }; + if (withPackageConfigs) { + const agentConfigWithPackageConfigs = await this.get( + soClient, + agentConfigSO.id, + withPackageConfigs + ); + if (agentConfigWithPackageConfigs) { + agentConfig.package_configs = agentConfigWithPackageConfigs.package_configs; + } + } + return agentConfig; + }) + ); + return { - items: agentConfigs.saved_objects.map((agentConfigSO) => ({ - id: agentConfigSO.id, - ...agentConfigSO.attributes, - })), - total: agentConfigs.total, + items: agentConfigs, + total: agentConfigsSO.total, page, perPage, }; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts index a261eec899d7c..ad9635cc02e06 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts @@ -5,6 +5,7 @@ */ import { SavedObjectsClientContract } from 'src/core/server'; +import { isPackageLimited } from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { Installation, InstallationStatus, PackageInfo, KibanaAssetType } from '../../../types'; import * as Registry from '../registry'; @@ -49,6 +50,28 @@ export async function getPackages( return packageList; } +// Get package names for packages which cannot have more than one package config on an agent config +// Assume packages only export one config template for now +export async function getLimitedPackages(options: { + savedObjectsClient: SavedObjectsClientContract; +}): Promise { + const { savedObjectsClient } = options; + const allPackages = await getPackages({ savedObjectsClient }); + const installedPackages = allPackages.filter( + (pkg) => (pkg.status = InstallationStatus.installed) + ); + const installedPackagesInfo = await Promise.all( + installedPackages.map((pkgInstall) => { + return getPackageInfo({ + savedObjectsClient, + pkgName: pkgInstall.name, + pkgVersion: pkgInstall.version, + }); + }) + ); + return installedPackagesInfo.filter((pkgInfo) => isPackageLimited).map((pkgInfo) => pkgInfo.name); +} + export async function getPackageSavedObjects(savedObjectsClient: SavedObjectsClientContract) { return savedObjectsClient.find({ type: PACKAGES_SAVED_OBJECT_TYPE, diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts index 53ffd5c6e7032..57c4f77432455 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts @@ -11,8 +11,7 @@ import { Installation, InstallationStatus, KibanaAssetType, -} from '../../../../common/types/models/epm'; - +} from '../../../types'; export { getCategories, getFile, @@ -20,6 +19,7 @@ export { getInstallation, getPackageInfo, getPackages, + getLimitedPackages, SearchParams, } from './get'; diff --git a/x-pack/plugins/ingest_manager/server/services/package_config.ts b/x-pack/plugins/ingest_manager/server/services/package_config.ts index 9fa51d025ad2b..9433a81e74b07 100644 --- a/x-pack/plugins/ingest_manager/server/services/package_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/package_config.ts @@ -7,24 +7,27 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { AuthenticatedUser } from '../../../security/server'; import { DeletePackageConfigsResponse, - packageToPackageConfig, PackageConfigInput, PackageConfigInputStream, PackageInfo, + ListWithKuery, + packageToPackageConfig, + isPackageLimited, + doesAgentConfigAlreadyIncludePackage, } from '../../common'; import { PACKAGE_CONFIG_SAVED_OBJECT_TYPE } from '../constants'; import { NewPackageConfig, UpdatePackageConfig, PackageConfig, - ListWithKuery, PackageConfigSOAttributes, RegistryPackage, + CallESAsCurrentUser, } from '../types'; import { agentConfigService } from './agent_config'; import { outputService } from './output'; import * as Registry from './epm/registry'; -import { getPackageInfo, getInstallation } from './epm/packages'; +import { getPackageInfo, getInstallation, ensureInstalledPackage } from './epm/packages'; import { getAssetsData } from './epm/packages/assets'; import { createStream } from './epm/agent/agent'; @@ -37,9 +40,39 @@ function getDataset(st: string) { class PackageConfigService { public async create( soClient: SavedObjectsClientContract, + callCluster: CallESAsCurrentUser, packageConfig: NewPackageConfig, options?: { id?: string; user?: AuthenticatedUser } ): Promise { + // Make sure the associated package is installed + if (packageConfig.package?.name) { + const [, pkgInfo] = await Promise.all([ + ensureInstalledPackage({ + savedObjectsClient: soClient, + pkgName: packageConfig.package.name, + callCluster, + }), + getPackageInfo({ + savedObjectsClient: soClient, + pkgName: packageConfig.package.name, + pkgVersion: packageConfig.package.version, + }), + ]); + + // Check if it is a limited package, and if so, check that the corresponding agent config does not + // already contain a package config for this package + if (isPackageLimited(pkgInfo)) { + const agentConfig = await agentConfigService.get(soClient, packageConfig.config_id, true); + if (agentConfig && doesAgentConfigAlreadyIncludePackage(agentConfig, pkgInfo.name)) { + throw new Error( + `Unable to create package config. Package '${pkgInfo.name}' already exists on this agent config.` + ); + } + } + + packageConfig.inputs = await this.assignPackageStream(pkgInfo, packageConfig.inputs); + } + const isoDate = new Date().toISOString(); const newSo = await soClient.create( SAVED_OBJECT_TYPE, diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index 61e1d0ad94db8..e5ed5c589389c 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -113,6 +113,7 @@ export async function setupIngestManager( if (!isInstalled) { await addPackageToConfig( soClient, + callCluster, installedPackage, configWithPackageConfigs, defaultOutput @@ -192,6 +193,7 @@ function generateRandomPassword() { async function addPackageToConfig( soClient: SavedObjectsClientContract, + callCluster: CallESAsCurrentUser, packageToInstall: Installation, config: AgentConfig, defaultOutput: Output @@ -208,10 +210,6 @@ async function addPackageToConfig( defaultOutput.id, config.namespace ); - newPackageConfig.inputs = await packageConfigService.assignPackageStream( - packageInfo, - newPackageConfig.inputs - ); - await packageConfigService.create(soClient, newPackageConfig); + await packageConfigService.create(soClient, callCluster, newPackageConfig); } diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts index 306aefb0d51ff..d076a803f4b53 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts @@ -8,7 +8,9 @@ import { NewAgentConfigSchema } from '../models'; import { ListWithKuerySchema } from './index'; export const GetAgentConfigsRequestSchema = { - query: ListWithKuerySchema, + query: ListWithKuerySchema.extends({ + full: schema.maybe(schema.boolean()), + }), }; export const GetOneAgentConfigRequestSchema = { diff --git a/x-pack/test/functional/es_archives/fleet/agents/mappings.json b/x-pack/test/functional/es_archives/fleet/agents/mappings.json index 0b84514de23f2..1f0aa2f24d6df 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/mappings.json +++ b/x-pack/test/functional/es_archives/fleet/agents/mappings.json @@ -1839,6 +1839,12 @@ "config_id": { "type": "keyword" }, + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, "description": { "type": "text" }, @@ -1847,6 +1853,7 @@ }, "inputs": { "type": "nested", + "enabled": false, "properties": { "config": { "type": "flattened" @@ -1854,20 +1861,24 @@ "enabled": { "type": "boolean" }, - "processors": { - "type": "keyword" - }, "streams": { "type": "nested", "properties": { - "agent_stream": { + "compiled_stream": { "type": "flattened" }, "config": { "type": "flattened" }, "dataset": { - "type": "keyword" + "properties": { + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } }, "enabled": { "type": "boolean" @@ -1875,9 +1886,6 @@ "id": { "type": "keyword" }, - "processors": { - "type": "keyword" - }, "vars": { "type": "flattened" } @@ -1915,6 +1923,12 @@ }, "revision": { "type": "integer" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" } } }, diff --git a/x-pack/test/ingest_manager_api_integration/apis/file.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/file.ts similarity index 94% rename from x-pack/test/ingest_manager_api_integration/apis/file.ts rename to x-pack/test/ingest_manager_api_integration/apis/epm/file.ts index a7462ac51ecc1..733b8d4fd9bd6 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/file.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/file.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FtrProviderContext } from '../../api_integration/ftr_provider_context'; -import { warnAndSkipTest } from '../helpers'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { warnAndSkipTest } from '../../helpers'; export default function ({ getService }: FtrProviderContext) { const log = getService('log'); @@ -13,7 +13,7 @@ export default function ({ getService }: FtrProviderContext) { const dockerServers = getService('dockerServers'); const server = dockerServers.get('registry'); - describe('package file', () => { + describe('EPM - package file', () => { it('fetches a .png screenshot image', async function () { if (server.enabled) { await supertest diff --git a/x-pack/test/ingest_manager_api_integration/apis/ilm.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/ilm.ts similarity index 89% rename from x-pack/test/ingest_manager_api_integration/apis/ilm.ts rename to x-pack/test/ingest_manager_api_integration/apis/epm/ilm.ts index b73a9da5fad59..8a801d59eb5b2 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/ilm.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/ilm.ts @@ -5,10 +5,10 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../api_integration/ftr_provider_context'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { - describe('ilm', () => { + describe('EPM - ilm', () => { it('setup policy', async () => { const policyName = 'foo'; const es = getService('es'); diff --git a/x-pack/test/ingest_manager_api_integration/apis/install.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install.ts similarity index 95% rename from x-pack/test/ingest_manager_api_integration/apis/install.ts rename to x-pack/test/ingest_manager_api_integration/apis/epm/install.ts index 92078c25419df..f73ba56c172c4 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/install.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install.ts @@ -5,8 +5,8 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../api_integration/ftr_provider_context'; -import { warnAndSkipTest } from '../helpers'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { warnAndSkipTest } from '../../helpers'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/ingest_manager_api_integration/apis/list.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts similarity index 87% rename from x-pack/test/ingest_manager_api_integration/apis/list.ts rename to x-pack/test/ingest_manager_api_integration/apis/epm/list.ts index abed9a7b85959..1ac1474e03700 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/list.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/list.ts @@ -5,8 +5,8 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../api_integration/ftr_provider_context'; -import { warnAndSkipTest } from '../helpers'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { warnAndSkipTest } from '../../helpers'; export default function ({ getService }: FtrProviderContext) { const log = getService('log'); @@ -18,7 +18,7 @@ export default function ({ getService }: FtrProviderContext) { // because `this` has to point to the Mocha context // see https://mochajs.org/#arrow-functions - describe('list', async function () { + describe('EPM - list', async function () { it('lists all packages from the registry', async function () { if (server.enabled) { const fetchPackageList = async () => { diff --git a/x-pack/test/ingest_manager_api_integration/apis/template.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/template.ts similarity index 88% rename from x-pack/test/ingest_manager_api_integration/apis/template.ts rename to x-pack/test/ingest_manager_api_integration/apis/epm/template.ts index f7e5a894b83ff..c92dac3334de3 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/template.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/template.ts @@ -5,8 +5,8 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../api_integration/ftr_provider_context'; -import { getTemplate } from '../../../plugins/ingest_manager/server/services/epm/elasticsearch/template/template'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { getTemplate } from '../../../../plugins/ingest_manager/server/services/epm/elasticsearch/template/template'; export default function ({ getService }: FtrProviderContext) { const indexPattern = 'foo'; @@ -20,7 +20,7 @@ export default function ({ getService }: FtrProviderContext) { }, }; // This test was inspired by https://github.com/elastic/kibana/blob/master/x-pack/test/api_integration/apis/monitoring/common/mappings_exist.js - describe('template', async () => { + describe('EPM - template', async () => { it('can be loaded', async () => { const template = getTemplate({ type: 'logs', diff --git a/x-pack/test/ingest_manager_api_integration/apis/index.js b/x-pack/test/ingest_manager_api_integration/apis/index.js index 3f8df8379e743..30c49140c6e2a 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/index.js @@ -5,12 +5,17 @@ */ export default function ({ loadTestFile }) { - describe('EPM Endpoints', function () { + describe('Ingest Manager Endpoints', function () { this.tags('ciGroup7'); - loadTestFile(require.resolve('./list')); - loadTestFile(require.resolve('./file')); - //loadTestFile(require.resolve('./template')); - loadTestFile(require.resolve('./ilm')); - loadTestFile(require.resolve('./install')); + + // EPM + loadTestFile(require.resolve('./epm/list')); + loadTestFile(require.resolve('./epm/file')); + //loadTestFile(require.resolve('./epm/template')); + loadTestFile(require.resolve('./epm/ilm')); + loadTestFile(require.resolve('./epm/install')); + + // Package configs + loadTestFile(require.resolve('./package_config/create')); }); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/package_config/create.ts b/x-pack/test/ingest_manager_api_integration/apis/package_config/create.ts new file mode 100644 index 0000000000000..c7748ab255f43 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/package_config/create.ts @@ -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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { warnAndSkipTest } from '../../helpers'; + +export default function ({ getService }: FtrProviderContext) { + const log = getService('log'); + const supertest = getService('supertest'); + const dockerServers = getService('dockerServers'); + + const server = dockerServers.get('registry'); + // use function () {} and not () => {} here + // because `this` has to point to the Mocha context + // see https://mochajs.org/#arrow-functions + + describe('Package Config - create', async function () { + let agentConfigId: string; + + before(async function () { + const { body: agentConfigResponse } = await supertest + .post(`/api/ingest_manager/agent_configs`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Test config', + namespace: 'default', + }); + agentConfigId = agentConfigResponse.item.id; + }); + + it('should work with valid values', async function () { + if (server.enabled) { + const { body: apiResponse } = await supertest + .post(`/api/ingest_manager/package_configs`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'filetest-1', + description: '', + namespace: 'default', + config_id: agentConfigId, + enabled: true, + output_id: '', + inputs: [], + package: { + name: 'filetest', + title: 'For File Tests', + version: '0.1.0', + }, + }) + .expect(200); + + expect(apiResponse.success).to.be(true); + } else { + warnAndSkipTest(this, log); + } + }); + + it('should return a 400 with an invalid namespace', async function () { + if (server.enabled) { + await supertest + .post(`/api/ingest_manager/package_configs`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'filetest-1', + description: '', + namespace: '', + config_id: agentConfigId, + enabled: true, + output_id: '', + inputs: [], + package: { + name: 'filetest', + title: 'For File Tests', + version: '0.1.0', + }, + }) + .expect(400); + } else { + warnAndSkipTest(this, log); + } + }); + + it('should not allow multiple limited packages on the same agent config', async function () { + if (server.enabled) { + await supertest + .post(`/api/ingest_manager/package_configs`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'endpoint-1', + description: '', + namespace: 'default', + config_id: agentConfigId, + enabled: true, + output_id: '', + inputs: [], + package: { + name: 'endpoint', + title: 'Endpoint', + version: '0.8.0', + }, + }) + .expect(200); + await supertest + .post(`/api/ingest_manager/package_configs`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'endpoint-2', + description: '', + namespace: 'default', + config_id: agentConfigId, + enabled: true, + output_id: '', + inputs: [], + package: { + name: 'endpoint', + title: 'Endpoint', + version: '0.8.0', + }, + }) + .expect(500); + } else { + warnAndSkipTest(this, log); + } + }); + }); +} From 94a18fda5d3a70dd7ee670dfd93105319935746f Mon Sep 17 00:00:00 2001 From: Bhavya RM Date: Mon, 6 Jul 2020 17:51:27 -0400 Subject: [PATCH 31/46] Adding test user to maps functional tests - PR 1 (#70649) adding test user to pr 1 of maps functional tests. --- x-pack/test/functional/apps/maps/discover.js | 12 ++++++ x-pack/test/functional/apps/maps/index.js | 2 +- .../apps/maps/visualize_create_menu.js | 13 +++++- x-pack/test/functional/config.js | 41 +++++++++++++++++++ 4 files changed, 66 insertions(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/maps/discover.js b/x-pack/test/functional/apps/maps/discover.js index 5f488d917c182..8dbd98ed3af2f 100644 --- a/x-pack/test/functional/apps/maps/discover.js +++ b/x-pack/test/functional/apps/maps/discover.js @@ -9,12 +9,24 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { const queryBar = getService('queryBar'); const PageObjects = getPageObjects(['common', 'discover', 'header', 'maps', 'timePicker']); + const security = getService('security'); describe('discover visualize button', () => { beforeEach(async () => { + await security.testUser.setRoles([ + 'test_logstash_reader', + 'global_maps_all', + 'geoshape_data_reader', + 'global_discover_read', + 'global_visualize_read', + ]); await PageObjects.common.navigateToApp('discover'); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should link geo_shape fields to Maps application', async () => { await PageObjects.discover.selectIndexPattern('geo_shapes*'); await PageObjects.discover.clickFieldListItemVisualize('geometry'); diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index 94c7587decf15..15928170972d9 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -17,7 +17,7 @@ export default function ({ loadTestFile, getService }) { await esArchiver.load('maps/data'); await esArchiver.load('maps/kibana'); await kibanaServer.uiSettings.replace({ - defaultIndex: 'logstash-*', + defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', }); await browser.setWindowSize(1600, 1000); }); diff --git a/x-pack/test/functional/apps/maps/visualize_create_menu.js b/x-pack/test/functional/apps/maps/visualize_create_menu.js index 5a53d3d8b571d..ef39771d6be07 100644 --- a/x-pack/test/functional/apps/maps/visualize_create_menu.js +++ b/x-pack/test/functional/apps/maps/visualize_create_menu.js @@ -6,14 +6,25 @@ import expect from '@kbn/expect'; -export default function ({ getPageObjects }) { +export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['visualize', 'header', 'maps']); + const security = getService('security'); + describe('visualize create menu', () => { before(async () => { + await security.testUser.setRoles( + ['test_logstash_reader', 'global_maps_all', 'geoshape_data_reader', 'global_visualize_all'], + false + ); + await PageObjects.visualize.navigateToNewVisualization(); }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should show maps application in create menu', async () => { const hasMapsApp = await PageObjects.visualize.hasMapsApp(); expect(hasMapsApp).to.equal(true); diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 3eef95b42cb7d..ad65f82d6dfe1 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -230,6 +230,47 @@ export default async function ({ readConfigFile }) { }, ], }, + global_visualize_read: { + kibana: [ + { + feature: { + visualize: ['read'], + }, + spaces: ['*'], + }, + ], + }, + global_visualize_all: { + kibana: [ + { + feature: { + visualize: ['all'], + }, + spaces: ['*'], + }, + ], + }, + global_maps_all: { + kibana: [ + { + feature: { + maps: ['all'], + }, + spaces: ['*'], + }, + ], + }, + + geoshape_data_reader: { + elasticsearch: { + indices: [ + { + names: ['geo_shapes*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + }, global_devtools_read: { kibana: [ From 2eb0896415122264fe23a76945d969ac74330b52 Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Mon, 6 Jul 2020 18:07:29 -0400 Subject: [PATCH 32/46] [Ingest Manager] Copy changes (#70828) * update overview page * remove streams column from config table * fleet name chanegs * remove unused component * update translations --- .../create_package_config_page/index.tsx | 2 +- .../package_configs/package_configs_table.tsx | 38 ------------------- .../sections/fleet/agent_list_page/index.tsx | 4 +- .../enrollment_token_list_page/index.tsx | 2 +- .../components/configuration_section.tsx | 6 +-- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 7 files changed, 7 insertions(+), 47 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/index.tsx index a81fb232ceaa0..b446e6bf97e7b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/index.tsx @@ -314,7 +314,7 @@ export const CreatePackageConfigPage: React.FunctionComponent = () => { title: i18n.translate( 'xpack.ingestManager.createPackageConfig.stepDefinePackageConfigTitle', { - defaultMessage: 'Define your integration', + defaultMessage: 'Configure integration', } ), status: !packageInfo || !agentConfig ? 'disabled' : undefined, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/package_configs_table.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/package_configs_table.tsx index 42d1075e2ee1f..4da4e2cc68c9d 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/package_configs_table.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/package_configs_table.tsx @@ -10,7 +10,6 @@ import { EuiInMemoryTable, EuiInMemoryTableProps, EuiBadge, - EuiTextColor, EuiContextMenuItem, EuiButton, EuiFlexGroup, @@ -23,7 +22,6 @@ import { useCapabilities, useLink } from '../../../../../hooks'; import { useConfigRefresh } from '../../hooks'; interface InMemoryPackageConfig extends PackageConfig { - streams: { total: number; enabled: number }; inputTypes: string[]; packageName?: string; packageTitle?: string; @@ -72,30 +70,11 @@ export const PackageConfigsTable: React.FunctionComponent = ({ } const dsInputTypes: string[] = []; - const streams = packageConfig.inputs.reduce( - (streamSummary, input) => { - if (!inputTypesValues.includes(input.type)) { - inputTypesValues.push(input.type); - } - if (!dsInputTypes.includes(input.type)) { - dsInputTypes.push(input.type); - } - - streamSummary.total += input.streams.length; - streamSummary.enabled += input.enabled - ? input.streams.filter((stream) => stream.enabled).length - : 0; - - return streamSummary; - }, - { total: 0, enabled: 0 } - ); dsInputTypes.sort(stringSortAscending); return { ...packageConfig, - streams, inputTypes: dsInputTypes, packageName: packageConfig.package?.name ?? '', packageTitle: packageConfig.package?.title ?? '', @@ -175,23 +154,6 @@ export const PackageConfigsTable: React.FunctionComponent = ({ return namespace ? {namespace} : ''; }, }, - { - field: 'streams', - name: i18n.translate( - 'xpack.ingestManager.configDetails.packageConfigsTable.streamsCountColumnTitle', - { - defaultMessage: 'Streams', - } - ), - render: (streams: InMemoryPackageConfig['streams']) => { - return ( - <> - {streams.enabled} -  / {streams.total} - - ); - }, - }, { name: i18n.translate( 'xpack.ingestManager.configDetails.packageConfigsTable.actionsColumnTitle', diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx index 6d04f63702c64..ec58789becb72 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx @@ -245,7 +245,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { { field: 'config_id', name: i18n.translate('xpack.ingestManager.agentList.configColumnTitle', { - defaultMessage: 'Configuration', + defaultMessage: 'Agent config', }), render: (configId: string, agent: Agent) => { const configName = agentConfigs.find((p) => p.id === configId)?.name; @@ -445,7 +445,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { > } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx index 800d4abfd45ed..df0862be9a141 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx @@ -175,7 +175,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { { field: 'config_id', name: i18n.translate('xpack.ingestManager.enrollmentTokensList.configTitle', { - defaultMessage: 'Config', + defaultMessage: 'Agent config', }), render: (configId: string) => { const config = agentConfigs.find((c) => c.id === configId); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/configuration_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/configuration_section.tsx index ed4b3fc8e6a5d..5a5e901d629b5 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/configuration_section.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/configuration_section.tsx @@ -36,7 +36,7 @@ export const OverviewConfigurationSection: React.FC<{ agentConfigs: AgentConfig[

@@ -55,7 +55,7 @@ export const OverviewConfigurationSection: React.FC<{ agentConfigs: AgentConfig[ @@ -64,7 +64,7 @@ export const OverviewConfigurationSection: React.FC<{ agentConfigs: AgentConfig[ diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d6f9cd383ae93..c12b1366746b0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8154,7 +8154,6 @@ "xpack.ingestManager.configDetails.packageConfigsTable.nameColumnTitle": "データソース", "xpack.ingestManager.configDetails.packageConfigsTable.namespaceColumnTitle": "名前空間", "xpack.ingestManager.configDetails.packageConfigsTable.packageNameColumnTitle": "統合", - "xpack.ingestManager.configDetails.packageConfigsTable.streamsCountColumnTitle": "ストリーム", "xpack.ingestManager.configDetails.subTabs.packageConfigsTabText": "データソース", "xpack.ingestManager.configDetails.subTabs.settingsTabText": "設定", "xpack.ingestManager.configDetails.summary.lastUpdated": "最終更新日:", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 235f8203608d4..f68a245acbc31 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8158,7 +8158,6 @@ "xpack.ingestManager.configDetails.packageConfigsTable.nameColumnTitle": "数据源", "xpack.ingestManager.configDetails.packageConfigsTable.namespaceColumnTitle": "命名空间", "xpack.ingestManager.configDetails.packageConfigsTable.packageNameColumnTitle": "集成", - "xpack.ingestManager.configDetails.packageConfigsTable.streamsCountColumnTitle": "流计数", "xpack.ingestManager.configDetails.subTabs.packageConfigsTabText": "数据源", "xpack.ingestManager.configDetails.subTabs.settingsTabText": "设置", "xpack.ingestManager.configDetails.summary.lastUpdated": "最后更新时间", From e35a42aa07b302f89c838f3259c29779486133a1 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 6 Jul 2020 18:14:59 -0400 Subject: [PATCH 33/46] [Component templates] Form wizard (#69732) --- .../component_template_serialization.test.ts | 203 +++++++++++----- .../lib/component_template_serialization.ts | 13 + .../index_management/common/lib/index.ts | 1 + .../public/application/app.tsx | 12 + .../public/application/app_context.tsx | 4 +- .../component_template_create.test.tsx | 218 +++++++++++++++++ .../component_template_edit.test.tsx | 123 ++++++++++ .../component_template_create.helpers.ts | 38 +++ .../component_template_edit.helpers.ts | 38 +++ .../component_template_form.helpers.ts | 159 ++++++++++++ .../helpers/http_requests.ts | 21 +- .../helpers/setup_environment.tsx | 1 + .../component_template_details.tsx | 7 +- .../tab_summary.tsx | 2 +- .../component_template_list.tsx | 45 +++- .../component_template_list/empty_prompt.tsx | 21 +- .../component_template_list/table.tsx | 54 ++++- .../component_template_clone.tsx | 61 +++++ .../component_template_clone/index.ts | 7 + .../component_template_create.tsx | 83 +++++++ .../component_template_create/index.ts | 7 + .../component_template_edit.tsx | 121 +++++++++ .../component_template_edit/index.ts | 7 + .../component_template_form.tsx | 209 ++++++++++++++++ .../component_template_form/index.ts | 7 + .../component_template_form/steps/index.ts | 8 + .../steps/step_logistics.tsx | 229 ++++++++++++++++++ .../steps/step_logistics_container.tsx | 22 ++ .../steps/step_logistics_schema.tsx | 102 ++++++++ .../steps/step_review.tsx | 212 ++++++++++++++++ .../steps/step_review_container.tsx | 24 ++ .../component_template_wizard/index.ts | 11 + .../component_templates_context.tsx | 10 +- .../component_templates/constants.ts | 2 + .../components/component_templates/index.ts | 6 + .../components/component_templates/lib/api.ts | 41 +++- .../component_templates/lib/breadcrumbs.ts | 61 +++++ .../component_templates/lib/documentation.ts | 2 + .../component_templates/lib/index.ts | 4 + .../component_templates/lib/utils.ts | 18 ++ .../component_templates/shared_imports.ts | 36 ++- .../public/application/index.tsx | 3 +- .../application/mount_management_section.ts | 1 + .../routes/api/component_templates/create.ts | 15 +- .../component_templates/schema_validation.ts | 8 +- .../routes/api/component_templates/update.ts | 4 +- .../index_management/component_templates.ts | 17 ++ 47 files changed, 2195 insertions(+), 103 deletions(-) create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_create.helpers.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_edit.helpers.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/component_template_form.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_container.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_schema.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review_container.tsx create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/index.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/lib/breadcrumbs.ts create mode 100644 x-pack/plugins/index_management/public/application/components/component_templates/lib/utils.ts diff --git a/x-pack/plugins/index_management/common/lib/component_template_serialization.test.ts b/x-pack/plugins/index_management/common/lib/component_template_serialization.test.ts index eaa7f24017a2f..83682f45918e3 100644 --- a/x-pack/plugins/index_management/common/lib/component_template_serialization.test.ts +++ b/x-pack/plugins/index_management/common/lib/component_template_serialization.test.ts @@ -4,91 +4,164 @@ * you may not use this file except in compliance with the Elastic License. */ -import { deserializeComponentTemplate } from './component_template_serialization'; +import { + deserializeComponentTemplate, + serializeComponentTemplate, +} from './component_template_serialization'; -describe('deserializeComponentTemplate', () => { - test('deserializes a component template', () => { - expect( - deserializeComponentTemplate( - { - name: 'my_component_template', - component_template: { - version: 1, - _meta: { - serialization: { - id: 10, - class: 'MyComponentTemplate', - }, - description: 'set number of shards to one', - }, - template: { - settings: { - number_of_shards: 1, +describe('Component template serialization', () => { + describe('deserializeComponentTemplate()', () => { + test('deserializes a component template', () => { + expect( + deserializeComponentTemplate( + { + name: 'my_component_template', + component_template: { + version: 1, + _meta: { + serialization: { + id: 10, + class: 'MyComponentTemplate', + }, + description: 'set number of shards to one', }, - mappings: { - _source: { - enabled: false, + template: { + settings: { + number_of_shards: 1, }, - properties: { - host_name: { - type: 'keyword', + mappings: { + _source: { + enabled: false, }, - created_at: { - type: 'date', - format: 'EEE MMM dd HH:mm:ss Z yyyy', + properties: { + host_name: { + type: 'keyword', + }, + created_at: { + type: 'date', + format: 'EEE MMM dd HH:mm:ss Z yyyy', + }, }, }, }, }, }, - }, - [ - { - name: 'my_index_template', - index_template: { - index_patterns: ['foo'], - template: { - settings: { - number_of_replicas: 2, + [ + { + name: 'my_index_template', + index_template: { + index_patterns: ['foo'], + template: { + settings: { + number_of_replicas: 2, + }, }, + composed_of: ['my_component_template'], + }, + }, + ] + ) + ).toEqual({ + name: 'my_component_template', + version: 1, + _meta: { + serialization: { + id: 10, + class: 'MyComponentTemplate', + }, + description: 'set number of shards to one', + }, + template: { + settings: { + number_of_shards: 1, + }, + mappings: { + _source: { + enabled: false, + }, + properties: { + host_name: { + type: 'keyword', + }, + created_at: { + type: 'date', + format: 'EEE MMM dd HH:mm:ss Z yyyy', }, - composed_of: ['my_component_template'], }, }, - ] - ) - ).toEqual({ - name: 'my_component_template', - version: 1, - _meta: { - serialization: { - id: 10, - class: 'MyComponentTemplate', }, - description: 'set number of shards to one', - }, - template: { - settings: { - number_of_shards: 1, + _kbnMeta: { + usedBy: ['my_index_template'], }, - mappings: { - _source: { - enabled: false, + }); + }); + }); + + describe('serializeComponentTemplate()', () => { + test('serialize a component template', () => { + expect( + serializeComponentTemplate({ + name: 'my_component_template', + version: 1, + _kbnMeta: { + usedBy: [], + }, + _meta: { + serialization: { + id: 10, + class: 'MyComponentTemplate', + }, + description: 'set number of shards to one', + }, + template: { + settings: { + number_of_shards: 1, + }, + mappings: { + _source: { + enabled: false, + }, + properties: { + host_name: { + type: 'keyword', + }, + created_at: { + type: 'date', + format: 'EEE MMM dd HH:mm:ss Z yyyy', + }, + }, + }, + }, + }) + ).toEqual({ + version: 1, + _meta: { + serialization: { + id: 10, + class: 'MyComponentTemplate', }, - properties: { - host_name: { - type: 'keyword', + description: 'set number of shards to one', + }, + template: { + settings: { + number_of_shards: 1, + }, + mappings: { + _source: { + enabled: false, }, - created_at: { - type: 'date', - format: 'EEE MMM dd HH:mm:ss Z yyyy', + properties: { + host_name: { + type: 'keyword', + }, + created_at: { + type: 'date', + format: 'EEE MMM dd HH:mm:ss Z yyyy', + }, }, }, }, - }, - _kbnMeta: { - usedBy: ['my_index_template'], - }, + }); }); }); }); diff --git a/x-pack/plugins/index_management/common/lib/component_template_serialization.ts b/x-pack/plugins/index_management/common/lib/component_template_serialization.ts index 0db81bf81d300..672b8140f79fb 100644 --- a/x-pack/plugins/index_management/common/lib/component_template_serialization.ts +++ b/x-pack/plugins/index_management/common/lib/component_template_serialization.ts @@ -8,6 +8,7 @@ import { ComponentTemplateFromEs, ComponentTemplateDeserialized, ComponentTemplateListItem, + ComponentTemplateSerialized, } from '../types'; const hasEntries = (data: object = {}) => Object.entries(data).length > 0; @@ -84,3 +85,15 @@ export function deserializeComponenTemplateList( return componentTemplateListItem; } + +export function serializeComponentTemplate( + componentTemplateDeserialized: ComponentTemplateDeserialized +): ComponentTemplateSerialized { + const { version, template, _meta } = componentTemplateDeserialized; + + return { + version, + template, + _meta, + }; +} diff --git a/x-pack/plugins/index_management/common/lib/index.ts b/x-pack/plugins/index_management/common/lib/index.ts index 6b1005b4faa05..f39cc063ba731 100644 --- a/x-pack/plugins/index_management/common/lib/index.ts +++ b/x-pack/plugins/index_management/common/lib/index.ts @@ -20,4 +20,5 @@ export { getTemplateParameter } from './utils'; export { deserializeComponentTemplate, deserializeComponenTemplateList, + serializeComponentTemplate, } from './component_template_serialization'; diff --git a/x-pack/plugins/index_management/public/application/app.tsx b/x-pack/plugins/index_management/public/application/app.tsx index 92197bee30c88..8d78995a94e2f 100644 --- a/x-pack/plugins/index_management/public/application/app.tsx +++ b/x-pack/plugins/index_management/public/application/app.tsx @@ -16,6 +16,11 @@ import { TemplateClone } from './sections/template_clone'; import { TemplateEdit } from './sections/template_edit'; import { useServices } from './app_context'; +import { + ComponentTemplateCreate, + ComponentTemplateEdit, + ComponentTemplateClone, +} from './components'; export const App = ({ history }: { history: ScopedHistory }) => { const { uiMetricService } = useServices(); @@ -34,6 +39,13 @@ export const AppWithoutRouter = () => ( + + + diff --git a/x-pack/plugins/index_management/public/application/app_context.tsx b/x-pack/plugins/index_management/public/application/app_context.tsx index c821907120373..6fbe177d24e06 100644 --- a/x-pack/plugins/index_management/public/application/app_context.tsx +++ b/x-pack/plugins/index_management/public/application/app_context.tsx @@ -6,9 +6,10 @@ import React, { createContext, useContext } from 'react'; import { ScopedHistory } from 'kibana/public'; +import { ManagementAppMountParams } from 'src/plugins/management/public'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; - import { CoreStart } from '../../../../../src/core/public'; + import { IngestManagerSetup } from '../../../ingest_manager/public'; import { IndexMgmtMetricsType } from '../types'; import { UiMetricService, NotificationService, HttpService } from './services'; @@ -32,6 +33,7 @@ export interface AppDependencies { notificationService: NotificationService; }; history: ScopedHistory; + setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; } export const AppContextProvider = ({ diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx new file mode 100644 index 0000000000000..6c8da4684f019 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx @@ -0,0 +1,218 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { setupEnvironment } from './helpers'; +import { setup, ComponentTemplateCreateTestBed } from './helpers/component_template_create.helpers'; + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + // Mocking EuiComboBox, as it utilizes "react-virtualized" for rendering search suggestions, + // which does not produce a valid component wrapper + EuiComboBox: (props: any) => ( + { + props.onChange([syntheticEvent['0']]); + }} + /> + ), + // Mocking EuiCodeEditor, which uses React Ace under the hood + EuiCodeEditor: (props: any) => ( + { + props.onChange(syntheticEvent.jsonString); + }} + /> + ), + }; +}); + +describe('', () => { + let testBed: ComponentTemplateCreateTestBed; + + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + afterAll(() => { + server.restore(); + }); + + describe('On component mount', () => { + beforeEach(async () => { + await act(async () => { + testBed = await setup(); + }); + + testBed.component.update(); + }); + + test('should set the correct page header', async () => { + const { exists, find } = testBed; + + // Verify page title + expect(exists('pageTitle')).toBe(true); + expect(find('pageTitle').text()).toEqual('Create component template'); + + // Verify documentation link + expect(exists('documentationLink')).toBe(true); + expect(find('documentationLink').text()).toBe('Component Templates docs'); + }); + + describe('Step: Logistics', () => { + test('should toggle the metadata field', async () => { + const { exists, component, actions } = testBed; + + // Meta editor should be hidden by default + // Since the editor itself is mocked, we checked for the mocked element + expect(exists('mockCodeEditor')).toBe(false); + + await act(async () => { + actions.toggleMetaSwitch(); + }); + + component.update(); + + expect(exists('mockCodeEditor')).toBe(true); + }); + + describe('Validation', () => { + test('should require a name', async () => { + const { form, actions, component, find } = testBed; + + await act(async () => { + // Submit logistics step without any values + actions.clickNextButton(); + }); + + component.update(); + + // Verify name is required + expect(form.getErrorsMessages()).toEqual(['A component template name is required.']); + expect(find('nextButton').props().disabled).toEqual(true); + }); + }); + }); + + describe('Step: Review and submit', () => { + const COMPONENT_TEMPLATE_NAME = 'comp-1'; + const SETTINGS = { number_of_shards: 1 }; + const ALIASES = { my_alias: {} }; + + const BOOLEAN_MAPPING_FIELD = { + name: 'boolean_datatype', + type: 'boolean', + }; + + beforeEach(async () => { + await act(async () => { + testBed = await setup(); + }); + + const { actions, component } = testBed; + + component.update(); + + // Complete step 1 (logistics) + await actions.completeStepLogistics({ name: COMPONENT_TEMPLATE_NAME }); + + // Complete step 2 (index settings) + await actions.completeStepSettings(SETTINGS); + + // Complete step 3 (mappings) + await actions.completeStepMappings([BOOLEAN_MAPPING_FIELD]); + + // Complete step 4 (aliases) + await actions.completeStepAliases(ALIASES); + }); + + test('should render the review content', () => { + const { find, exists, actions } = testBed; + // Verify page header + expect(exists('stepReview')).toBe(true); + expect(find('stepReview.title').text()).toEqual( + `Review details for '${COMPONENT_TEMPLATE_NAME}'` + ); + + // Verify 2 tabs exist + expect(find('stepReview.content').find('.euiTab').length).toBe(2); + expect( + find('stepReview.content') + .find('.euiTab') + .map((t) => t.text()) + ).toEqual(['Summary', 'Request']); + + // Summary tab should render by default + expect(exists('stepReview.summaryTab')).toBe(true); + expect(exists('stepReview.requestTab')).toBe(false); + + // Navigate to request tab and verify content + actions.selectReviewTab('request'); + + expect(exists('stepReview.summaryTab')).toBe(false); + expect(exists('stepReview.requestTab')).toBe(true); + }); + + test('should send the correct payload when submitting the form', async () => { + const { actions, component } = testBed; + + await act(async () => { + actions.clickNextButton(); + }); + + component.update(); + + const latestRequest = server.requests[server.requests.length - 1]; + + const expected = { + name: COMPONENT_TEMPLATE_NAME, + template: { + settings: SETTINGS, + mappings: { + _source: {}, + _meta: {}, + properties: { + [BOOLEAN_MAPPING_FIELD.name]: { + type: BOOLEAN_MAPPING_FIELD.type, + }, + }, + }, + aliases: ALIASES, + }, + _kbnMeta: { usedBy: [] }, + }; + + expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected); + }); + + test('should surface API errors if the request is unsuccessful', async () => { + const { component, actions, find, exists } = testBed; + + const error = { + status: 409, + error: 'Conflict', + message: `There is already a template with name '${COMPONENT_TEMPLATE_NAME}'`, + }; + + httpRequestsMockHelpers.setCreateComponentTemplateResponse(undefined, { body: error }); + + await act(async () => { + actions.clickNextButton(); + }); + + component.update(); + + expect(exists('saveComponentTemplateError')).toBe(true); + expect(find('saveComponentTemplateError').text()).toContain(error.message); + }); + }); + }); +}); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx new file mode 100644 index 0000000000000..f237605756d5c --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx @@ -0,0 +1,123 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { setupEnvironment } from './helpers'; +import { setup, ComponentTemplateEditTestBed } from './helpers/component_template_edit.helpers'; + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + // Mocking EuiComboBox, as it utilizes "react-virtualized" for rendering search suggestions, + // which does not produce a valid component wrapper + EuiComboBox: (props: any) => ( + { + props.onChange([syntheticEvent['0']]); + }} + /> + ), + // Mocking EuiCodeEditor, which uses React Ace under the hood + EuiCodeEditor: (props: any) => ( + { + props.onChange(syntheticEvent.jsonString); + }} + /> + ), + }; +}); + +describe('', () => { + let testBed: ComponentTemplateEditTestBed; + + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + afterAll(() => { + server.restore(); + }); + + const COMPONENT_TEMPLATE_NAME = 'comp-1'; + const COMPONENT_TEMPLATE_TO_EDIT = { + name: COMPONENT_TEMPLATE_NAME, + template: { + settings: { number_of_shards: 1 }, + }, + _kbnMeta: { usedBy: [] }, + }; + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadComponentTemplateResponse(COMPONENT_TEMPLATE_TO_EDIT); + + await act(async () => { + testBed = await setup(); + }); + + testBed.component.update(); + }); + + test('should set the correct page title', () => { + const { exists, find } = testBed; + + expect(exists('pageTitle')).toBe(true); + expect(find('pageTitle').text()).toEqual( + `Edit component template '${COMPONENT_TEMPLATE_NAME}'` + ); + }); + + it('should set the name field to read only', () => { + const { find } = testBed; + + const nameInput = find('nameField.input'); + expect(nameInput.props().disabled).toEqual(true); + }); + + describe('form payload', () => { + it('should send the correct payload with changed values', async () => { + const { actions, component, form } = testBed; + + await act(async () => { + form.setInputValue('versionField.input', '1'); + actions.clickNextButton(); + }); + + component.update(); + + await actions.completeStepSettings(); + await actions.completeStepMappings(); + await actions.completeStepAliases(); + + await act(async () => { + actions.clickNextButton(); + }); + + component.update(); + + const latestRequest = server.requests[server.requests.length - 1]; + + const expected = { + version: 1, + ...COMPONENT_TEMPLATE_TO_EDIT, + template: { + ...COMPONENT_TEMPLATE_TO_EDIT.template, + mappings: { + _meta: {}, + _source: {}, + properties: {}, + }, + }, + }; + + expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected); + }); + }); +}); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_create.helpers.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_create.helpers.ts new file mode 100644 index 0000000000000..e6ced2fcc309a --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_create.helpers.ts @@ -0,0 +1,38 @@ +/* + * 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 { registerTestBed, TestBed, TestBedConfig } from '../../../../../../../../../test_utils'; +import { BASE_PATH } from '../../../../../../../common'; +import { ComponentTemplateCreate } from '../../../component_template_wizard'; + +import { WithAppDependencies } from './setup_environment'; +import { + getFormActions, + ComponentTemplateFormTestSubjects, +} from './component_template_form.helpers'; + +export type ComponentTemplateCreateTestBed = TestBed & { + actions: ReturnType; +}; + +const testBedConfig: TestBedConfig = { + memoryRouter: { + initialEntries: [`${BASE_PATH}/create_component_template`], + componentRoutePath: `${BASE_PATH}/create_component_template`, + }, + doMountAsync: true, +}; + +const initTestBed = registerTestBed(WithAppDependencies(ComponentTemplateCreate), testBedConfig); + +export const setup = async (): Promise => { + const testBed = await initTestBed(); + + return { + ...testBed, + actions: getFormActions(testBed), + }; +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_edit.helpers.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_edit.helpers.ts new file mode 100644 index 0000000000000..3c0cbb19577a9 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_edit.helpers.ts @@ -0,0 +1,38 @@ +/* + * 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 { registerTestBed, TestBed, TestBedConfig } from '../../../../../../../../../test_utils'; +import { BASE_PATH } from '../../../../../../../common'; +import { ComponentTemplateEdit } from '../../../component_template_wizard'; + +import { WithAppDependencies } from './setup_environment'; +import { + getFormActions, + ComponentTemplateFormTestSubjects, +} from './component_template_form.helpers'; + +export type ComponentTemplateEditTestBed = TestBed & { + actions: ReturnType; +}; + +const testBedConfig: TestBedConfig = { + memoryRouter: { + initialEntries: [`${BASE_PATH}/edit_component_template/comp-1`], + componentRoutePath: `${BASE_PATH}/edit_component_template/:name`, + }, + doMountAsync: true, +}; + +const initTestBed = registerTestBed(WithAppDependencies(ComponentTemplateEdit), testBedConfig); + +export const setup = async (): Promise => { + const testBed = await initTestBed(); + + return { + ...testBed, + actions: getFormActions(testBed), + }; +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts new file mode 100644 index 0000000000000..f92f46d71e7c7 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/component_template_form.helpers.ts @@ -0,0 +1,159 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { TestBed } from '../../../../../../../../../test_utils'; + +interface MappingField { + name: string; + type: string; +} + +export const getFormActions = (testBed: TestBed) => { + // User actions + const toggleVersionSwitch = () => { + testBed.form.toggleEuiSwitch('versionToggle'); + }; + + const toggleMetaSwitch = () => { + testBed.form.toggleEuiSwitch('metaToggle'); + }; + + const clickNextButton = () => { + testBed.find('nextButton').simulate('click'); + }; + + const clickBackButton = () => { + testBed.find('backButton').simulate('click'); + }; + + const clickSubmitButton = () => { + testBed.find('submitButton').simulate('click'); + }; + + const setMetaField = (jsonString: string) => { + testBed.find('mockCodeEditor').simulate('change', { + jsonString, + }); + }; + + const selectReviewTab = (tab: 'summary' | 'request') => { + const tabs = ['summary', 'request']; + + testBed.find('stepReview.content').find('.euiTab').at(tabs.indexOf(tab)).simulate('click'); + }; + + const completeStepLogistics = async ({ name }: { name: string }) => { + const { form, component } = testBed; + // Add name field + form.setInputValue('nameField.input', name); + + await act(async () => { + clickNextButton(); + }); + + component.update(); + }; + + const completeStepSettings = async (settings?: { [key: string]: any }) => { + const { find, component } = testBed; + + await act(async () => { + if (settings) { + find('mockCodeEditor').simulate('change', { + jsonString: JSON.stringify(settings), + }); // Using mocked EuiCodeEditor + } + + clickNextButton(); + }); + + component.update(); + }; + + const addMappingField = async (name: string, type: string) => { + const { find, form, component } = testBed; + + await act(async () => { + form.setInputValue('nameParameterInput', name); + find('createFieldForm.mockComboBox').simulate('change', [ + { + label: type, + value: type, + }, + ]); + find('createFieldForm.addButton').simulate('click'); + }); + + component.update(); + }; + + const completeStepMappings = async (mappingFields?: MappingField[]) => { + const { component } = testBed; + + if (mappingFields) { + for (const field of mappingFields) { + const { name, type } = field; + await addMappingField(name, type); + } + } + + await act(async () => { + clickNextButton(); + }); + + component.update(); + }; + + const completeStepAliases = async (aliases?: { [key: string]: any }) => { + const { find, component } = testBed; + + await act(async () => { + if (aliases) { + find('mockCodeEditor').simulate('change', { + jsonString: JSON.stringify(aliases), + }); // Using mocked EuiCodeEditor + } + + clickNextButton(); + }); + + component.update(); + }; + + return { + toggleVersionSwitch, + toggleMetaSwitch, + clickNextButton, + clickBackButton, + clickSubmitButton, + setMetaField, + selectReviewTab, + completeStepSettings, + completeStepAliases, + completeStepLogistics, + completeStepMappings, + }; +}; + +export type ComponentTemplateFormTestSubjects = + | 'backButton' + | 'documentationLink' + | 'metaToggle' + | 'metaEditor' + | 'mockCodeEditor' + | 'nameField.input' + | 'nextButton' + | 'pageTitle' + | 'saveComponentTemplateError' + | 'submitButton' + | 'stepReview' + | 'stepReview.title' + | 'stepReview.content' + | 'stepReview.summaryTab' + | 'stepReview.requestTab' + | 'versionField' + | 'versionField.input'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts index b7b674292dd98..a4e532ba5d3d3 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/http_requests.ts @@ -5,7 +5,11 @@ */ import sinon, { SinonFakeServer } from 'sinon'; -import { ComponentTemplateListItem, ComponentTemplateDeserialized } from '../../../shared_imports'; +import { + ComponentTemplateListItem, + ComponentTemplateDeserialized, + ComponentTemplateSerialized, +} from '../../../shared_imports'; import { API_BASE_PATH } from './constants'; // Register helpers to mock HTTP Requests @@ -46,10 +50,25 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { ]); }; + const setCreateComponentTemplateResponse = ( + response?: ComponentTemplateSerialized, + error?: any + ) => { + const status = error ? error.body.status || 400 : 200; + const body = error ? JSON.stringify(error.body) : JSON.stringify(response); + + server.respondWith('POST', `${API_BASE_PATH}/component_templates`, [ + status, + { 'Content-Type': 'application/json' }, + body, + ]); + }; + return { setLoadComponentTemplatesResponse, setDeleteComponentTemplateResponse, setLoadComponentTemplateResponse, + setCreateComponentTemplateResponse, }; }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx index a2194bbfa0186..70634a226c67b 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/helpers/setup_environment.tsx @@ -27,6 +27,7 @@ const appDependencies = { trackMetric: () => {}, docLinks: docLinksServiceMock.createStartContract(), toasts: notificationServiceMock.createSetupContract().toasts, + setBreadcrumbs: () => {}, }; export const setupEnvironment = () => { diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx index a8007c6363584..f94c5c38f23dd 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/component_template_details.tsx @@ -24,6 +24,7 @@ import { useComponentTemplatesContext } from '../component_templates_context'; import { TabSummary } from './tab_summary'; import { ComponentTemplateTabs, TabType } from './tabs'; import { ManageButton, ManageAction } from './manage_button'; +import { attemptToDecodeURI } from '../lib'; interface Props { componentTemplateName: string; @@ -39,8 +40,10 @@ export const ComponentTemplateDetailsFlyout: React.FunctionComponent = ({ }) => { const { api } = useComponentTemplatesContext(); + const decodedComponentTemplateName = attemptToDecodeURI(componentTemplateName); + const { data: componentTemplateDetails, isLoading, error } = api.useLoadComponentTemplate( - componentTemplateName + decodedComponentTemplateName ); const [activeTab, setActiveTab] = useState('summary'); @@ -108,7 +111,7 @@ export const ComponentTemplateDetailsFlyout: React.FunctionComponent = ({

- {componentTemplateName} + {decodedComponentTemplateName}

diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx index 401186f6c962e..80f28f23c9f91 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_details/tab_summary.tsx @@ -74,7 +74,7 @@ export const TabSummary: React.FunctionComponent = ({ componentTemplateDe )} {/* Version (optional) */} - {version && ( + {typeof version !== 'undefined' && ( <> = ({ const [componentTemplatesToDelete, setComponentTemplatesToDelete] = useState([]); - const goToList = () => { - return history.push('component_templates'); + const goToComponentTemplateList = () => { + return history.push({ + pathname: 'component_templates', + }); + }; + + const goToEditComponentTemplate = (name: string) => { + return history.push({ + pathname: encodeURI(`edit_component_template/${encodeURIComponent(name)}`), + }); + }; + + const goToCloneComponentTemplate = (name: string) => { + return history.push({ + pathname: encodeURI(`create_component_template/${encodeURIComponent(name)}`), + }); }; // Track component loaded @@ -60,11 +75,13 @@ export const ComponentTemplateList: React.FunctionComponent = ({ componentTemplates={data} onReloadClick={sendRequest} onDeleteClick={setComponentTemplatesToDelete} + onEditClick={goToEditComponentTemplate} + onCloneClick={goToCloneComponentTemplate} history={history as ScopedHistory} /> ); } else if (data && data.length === 0) { - content = ; + content = ; } else if (error) { content = ; } @@ -81,7 +98,7 @@ export const ComponentTemplateList: React.FunctionComponent = ({ // refetch the component templates sendRequest(); // go back to list view (if deleted from details flyout) - goToList(); + goToComponentTemplateList(); } setComponentTemplatesToDelete([]); }} @@ -92,9 +109,25 @@ export const ComponentTemplateList: React.FunctionComponent = ({ {/* details flyout */} {componentTemplateName && ( + goToEditComponentTemplate(attemptToDecodeURI(componentTemplateName)), + }, + { + name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.cloneActionLabel', { + defaultMessage: 'Clone', + }), + icon: 'copy', + handleActionClick: () => + goToCloneComponentTemplate(attemptToDecodeURI(componentTemplateName)), + }, { name: i18n.translate('xpack.idxMgmt.componentTemplateDetails.deleteButtonLabel', { defaultMessage: 'Delete', @@ -104,7 +137,7 @@ export const ComponentTemplateList: React.FunctionComponent = ({ details._kbnMeta.usedBy.length > 0, closePopoverOnClick: true, handleActionClick: () => { - setComponentTemplatesToDelete([componentTemplateName]); + setComponentTemplatesToDelete([attemptToDecodeURI(componentTemplateName)]); }, }, ]} diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/empty_prompt.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/empty_prompt.tsx index edd9f77cbf635..fbb1968491ff6 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/empty_prompt.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/empty_prompt.tsx @@ -6,11 +6,17 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiEmptyPrompt, EuiLink } from '@elastic/eui'; +import { RouteComponentProps } from 'react-router-dom'; +import { EuiEmptyPrompt, EuiLink, EuiButton } from '@elastic/eui'; +import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; import { useComponentTemplatesContext } from '../component_templates_context'; -export const EmptyPrompt: FunctionComponent = () => { +interface Props { + history: RouteComponentProps['history']; +} + +export const EmptyPrompt: FunctionComponent = ({ history }) => { const { documentation } = useComponentTemplatesContext(); return ( @@ -38,6 +44,17 @@ export const EmptyPrompt: FunctionComponent = () => {

} + actions={ + + {i18n.translate('xpack.idxMgmt.home.componentTemplates.emptyPromptButtonLabel', { + defaultMessage: 'Create a component template', + })} + + } /> ); }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx index b67a249ae6976..089c2f889e726 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/table.tsx @@ -25,6 +25,8 @@ export interface Props { componentTemplates: ComponentTemplateListItem[]; onReloadClick: () => void; onDeleteClick: (componentTemplateName: string[]) => void; + onEditClick: (componentTemplateName: string) => void; + onCloneClick: (componentTemplateName: string) => void; history: ScopedHistory; } @@ -32,6 +34,8 @@ export const ComponentTable: FunctionComponent = ({ componentTemplates, onReloadClick, onDeleteClick, + onEditClick, + onCloneClick, history, }) => { const { trackMetric } = useComponentTemplatesContext(); @@ -85,6 +89,17 @@ export const ComponentTable: FunctionComponent = ({ defaultMessage: 'Reload', })} , + + {i18n.translate('xpack.idxMgmt.componentTemplatesList.table.createButtonLabel', { + defaultMessage: 'Create a component template', + })} + , ], box: { incremental: true, @@ -135,7 +150,7 @@ export const ComponentTable: FunctionComponent = ({ {...reactRouterNavigate( history, { - pathname: `/component_templates/${name}`, + pathname: encodeURI(`/component_templates/${encodeURIComponent(name)}`), }, () => trackMetric('click', UIM_COMPONENT_TEMPLATE_DETAILS) )} @@ -204,8 +219,37 @@ export const ComponentTable: FunctionComponent = ({ ), actions: [ { - 'data-test-subj': 'deleteComponentTemplateButton', + name: i18n.translate('xpack.idxMgmt.componentTemplatesList.table.actionEditText', { + defaultMessage: 'Edit', + }), + description: i18n.translate( + 'xpack.idxMgmt.componentTemplatesList.table.actionEditDecription', + { + defaultMessage: 'Edit this component template', + } + ), + onClick: ({ name }: ComponentTemplateListItem) => onEditClick(name), isPrimary: true, + icon: 'pencil', + type: 'icon', + 'data-test-subj': 'editComponentTemplateButton', + }, + { + name: i18n.translate('xpack.idxMgmt.componentTemplatesList.table.actionCloneText', { + defaultMessage: 'Clone', + }), + description: i18n.translate( + 'xpack.idxMgmt.componentTemplatesList.table.actionCloneDecription', + { + defaultMessage: 'Clone this component template', + } + ), + onClick: ({ name }: ComponentTemplateListItem) => onCloneClick(name), + icon: 'copy', + type: 'icon', + 'data-test-subj': 'cloneComponentTemplateButton', + }, + { name: i18n.translate('xpack.idxMgmt.componentTemplatesList.table.deleteActionLabel', { defaultMessage: 'Delete', }), @@ -213,11 +257,13 @@ export const ComponentTable: FunctionComponent = ({ 'xpack.idxMgmt.componentTemplatesList.table.deleteActionDescription', { defaultMessage: 'Delete this component template' } ), + onClick: ({ name }) => onDeleteClick([name]), + enabled: ({ usedBy }) => usedBy.length === 0, + isPrimary: true, type: 'icon', icon: 'trash', color: 'danger', - onClick: ({ name }) => onDeleteClick([name]), - enabled: ({ usedBy }) => usedBy.length === 0, + 'data-test-subj': 'deleteComponentTemplateButton', }, ], }, diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx new file mode 100644 index 0000000000000..94db623f313c7 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/component_template_clone.tsx @@ -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 React, { FunctionComponent, useEffect } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { SectionLoading } from '../../shared_imports'; +import { useComponentTemplatesContext } from '../../component_templates_context'; +import { attemptToDecodeURI } from '../../lib'; +import { ComponentTemplateCreate } from '../component_template_create'; + +export interface Params { + sourceComponentTemplateName: string; +} + +export const ComponentTemplateClone: FunctionComponent> = (props) => { + const { sourceComponentTemplateName } = props.match.params; + const decodedSourceName = attemptToDecodeURI(sourceComponentTemplateName); + + const { toasts, api } = useComponentTemplatesContext(); + + const { error, data: componentTemplateToClone, isLoading } = api.useLoadComponentTemplate( + decodedSourceName + ); + + useEffect(() => { + if (error && !isLoading) { + toasts.addError(error, { + title: i18n.translate('xpack.idxMgmt.componentTemplateClone.loadComponentTemplateTitle', { + defaultMessage: `Error loading component template '{sourceComponentTemplateName}'.`, + values: { sourceComponentTemplateName }, + }), + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [error, isLoading]); + + if (isLoading) { + return ( + + + + ); + } else { + // We still show the create form (unpopulated) even if we were not able to load the + // selected component template data. + const sourceComponentTemplate = componentTemplateToClone + ? { ...componentTemplateToClone, name: `${componentTemplateToClone.name}-copy` } + : undefined; + + return ; + } +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_clone/index.ts new file mode 100644 index 0000000000000..b7165919644f4 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_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 { ComponentTemplateClone } from './component_template_clone'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx new file mode 100644 index 0000000000000..94afadaed37f1 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx @@ -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 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 { ComponentTemplateDeserialized } from '../../shared_imports'; +import { useComponentTemplatesContext } from '../../component_templates_context'; +import { ComponentTemplateForm } from '../component_template_form'; + +interface Props { + /** + * This value may be passed in to prepopulate the creation form (e.g., to clone a template) + */ + sourceComponentTemplate?: any; +} + +export const ComponentTemplateCreate: React.FunctionComponent = ({ + history, + sourceComponentTemplate, +}) => { + const [isSaving, setIsSaving] = useState(false); + const [saveError, setSaveError] = useState(null); + + const { api, breadcrumbs } = useComponentTemplatesContext(); + + const onSave = async (componentTemplate: ComponentTemplateDeserialized) => { + const { name } = componentTemplate; + + setIsSaving(true); + setSaveError(null); + + const { error } = await api.createComponentTemplate(componentTemplate); + + setIsSaving(false); + + if (error) { + setSaveError(error); + return; + } + + history.push({ + pathname: encodeURI(`/component_templates/${encodeURIComponent(name)}`), + }); + }; + + const clearSaveError = () => { + setSaveError(null); + }; + + useEffect(() => { + breadcrumbs.setCreateBreadcrumbs(); + }, [breadcrumbs]); + + return ( + + + +

+ +

+
+ + + + +
+
+ ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/index.ts new file mode 100644 index 0000000000000..6b0e02317888b --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_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 { ComponentTemplateCreate } from './component_template_create'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx new file mode 100644 index 0000000000000..2bd3dfb34acb9 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx @@ -0,0 +1,121 @@ +/* + * 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, EuiTitle, EuiSpacer, EuiCallOut } from '@elastic/eui'; + +import { useComponentTemplatesContext } from '../../component_templates_context'; +import { ComponentTemplateDeserialized, SectionLoading } from '../../shared_imports'; +import { attemptToDecodeURI } from '../../lib'; +import { ComponentTemplateForm } from '../component_template_form'; + +interface MatchParams { + name: string; +} + +export const ComponentTemplateEdit: React.FunctionComponent> = ({ + match: { + params: { name }, + }, + history, +}) => { + const { api, breadcrumbs } = useComponentTemplatesContext(); + + const [isSaving, setIsSaving] = useState(false); + const [saveError, setSaveError] = useState(null); + + const decodedName = attemptToDecodeURI(name); + + const { error, data: componentTemplate, isLoading } = api.useLoadComponentTemplate(decodedName); + + useEffect(() => { + breadcrumbs.setEditBreadcrumbs(); + }, [breadcrumbs]); + + const onSave = async (updatedComponentTemplate: ComponentTemplateDeserialized) => { + setIsSaving(true); + setSaveError(null); + + const { error: saveErrorObject } = await api.updateComponentTemplate(updatedComponentTemplate); + + setIsSaving(false); + + if (saveErrorObject) { + setSaveError(saveErrorObject); + return; + } + + history.push({ + pathname: encodeURI(`/component_templates/${encodeURIComponent(name)}`), + }); + }; + + const clearSaveError = () => { + setSaveError(null); + }; + + let content; + + if (isLoading) { + content = ( + + + + ); + } else if (error) { + content = ( + <> + + } + color="danger" + iconType="alert" + data-test-subj="loadComponentTemplateError" + > +
{error.message}
+
+ + + ); + } else if (componentTemplate) { + content = ( + + ); + } + + return ( + + + +

+ +

+
+ + {content} +
+
+ ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/index.ts new file mode 100644 index 0000000000000..1f877bdae24f0 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_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 { ComponentTemplateEdit } from './component_template_edit'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/component_template_form.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/component_template_form.tsx new file mode 100644 index 0000000000000..6e35fbad31d4e --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/component_template_form.tsx @@ -0,0 +1,209 @@ +/* + * 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, { useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSpacer, EuiCallOut } from '@elastic/eui'; + +import { + serializers, + Forms, + ComponentTemplateDeserialized, + CommonWizardSteps, + StepSettingsContainer, + StepMappingsContainer, + StepAliasesContainer, +} from '../../shared_imports'; +import { useComponentTemplatesContext } from '../../component_templates_context'; +import { StepLogisticsContainer, StepReviewContainer } from './steps'; + +const { stripEmptyFields } = serializers; +const { FormWizard, FormWizardStep } = Forms; + +interface Props { + onSave: (componentTemplate: ComponentTemplateDeserialized) => void; + clearSaveError: () => void; + isSaving: boolean; + saveError: any; + defaultValue?: ComponentTemplateDeserialized; + isEditing?: boolean; +} + +export interface WizardContent extends CommonWizardSteps { + logistics: Omit; +} + +export type WizardSection = keyof WizardContent | 'review'; + +const wizardSections: { [id: string]: { id: WizardSection; label: string } } = { + logistics: { + id: 'logistics', + label: i18n.translate('xpack.idxMgmt.componentTemplateForm.steps.logisticsStepName', { + defaultMessage: 'Logistics', + }), + }, + settings: { + id: 'settings', + label: i18n.translate('xpack.idxMgmt.componentTemplateForm.steps.settingsStepName', { + defaultMessage: 'Index settings', + }), + }, + mappings: { + id: 'mappings', + label: i18n.translate('xpack.idxMgmt.componentTemplateForm.steps.mappingsStepName', { + defaultMessage: 'Mappings', + }), + }, + aliases: { + id: 'aliases', + label: i18n.translate('xpack.idxMgmt.componentTemplateForm.steps.aliasesStepName', { + defaultMessage: 'Aliases', + }), + }, + review: { + id: 'review', + label: i18n.translate('xpack.idxMgmt.componentTemplateForm.steps.summaryStepName', { + defaultMessage: 'Review', + }), + }, +}; + +export const ComponentTemplateForm = ({ + defaultValue = { + name: '', + template: { + settings: {}, + mappings: {}, + aliases: {}, + }, + _meta: {}, + _kbnMeta: { + usedBy: [], + }, + }, + isEditing, + isSaving, + saveError, + clearSaveError, + onSave, +}: Props) => { + const { + template: { settings, mappings, aliases }, + ...logistics + } = defaultValue; + + const { documentation } = useComponentTemplatesContext(); + + const wizardDefaultValue: WizardContent = { + logistics, + settings, + mappings, + aliases, + }; + + const i18nTexts = { + save: isEditing ? ( + + ) : ( + + ), + }; + + const apiError = saveError ? ( + <> + + } + color="danger" + iconType="alert" + data-test-subj="saveComponentTemplateError" + > +
{saveError.message || saveError.statusText}
+
+ + + ) : null; + + const buildComponentTemplateObject = (initialTemplate: ComponentTemplateDeserialized) => ( + wizardData: WizardContent + ): ComponentTemplateDeserialized => { + const componentTemplate = { + ...initialTemplate, + name: wizardData.logistics.name, + version: wizardData.logistics.version, + _meta: wizardData.logistics._meta, + template: { + settings: wizardData.settings, + mappings: wizardData.mappings, + aliases: wizardData.aliases, + }, + }; + return componentTemplate; + }; + + const onSaveComponentTemplate = useCallback( + async (wizardData: WizardContent) => { + const componentTemplate = buildComponentTemplateObject(defaultValue)(wizardData); + + // This will strip an empty string if "version" is not set, as well as an empty "_meta" object + onSave( + stripEmptyFields(componentTemplate, { + types: ['string', 'object'], + }) as ComponentTemplateDeserialized + ); + + clearSaveError(); + }, + [defaultValue, onSave, clearSaveError] + ); + + return ( + + defaultValue={wizardDefaultValue} + onSave={onSaveComponentTemplate} + isEditing={isEditing} + isSaving={isSaving} + apiError={apiError} + texts={i18nTexts} + > + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/index.ts new file mode 100644 index 0000000000000..84d9a2795ee2c --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_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 { ComponentTemplateForm } from './component_template_form'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/index.ts new file mode 100644 index 0000000000000..b7e3e36e61814 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { StepLogisticsContainer } from './step_logistics_container'; +export { StepReviewContainer } from './step_review_container'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx new file mode 100644 index 0000000000000..8762eae9d2297 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx @@ -0,0 +1,229 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiButtonEmpty, + EuiSpacer, + EuiSwitch, + EuiLink, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + useForm, + Form, + getUseField, + getFormRow, + Field, + Forms, + JsonEditorField, +} from '../../../shared_imports'; +import { useComponentTemplatesContext } from '../../../component_templates_context'; +import { logisticsFormSchema } from './step_logistics_schema'; + +const UseField = getUseField({ component: Field }); +const FormRow = getFormRow({ titleTag: 'h3' }); + +interface Props { + defaultValue: { [key: string]: any }; + onChange: (content: Forms.Content) => void; + isEditing?: boolean; +} + +export const StepLogistics: React.FunctionComponent = React.memo( + ({ defaultValue, isEditing, onChange }) => { + const { form } = useForm({ + schema: logisticsFormSchema, + defaultValue, + options: { stripEmptyFields: false }, + }); + + const { documentation } = useComponentTemplatesContext(); + + const [isMetaVisible, setIsMetaVisible] = useState( + Boolean(defaultValue._meta && Object.keys(defaultValue._meta).length) + ); + + const validate = async () => { + return (await form.submit()).isValid; + }; + + useEffect(() => { + onChange({ + isValid: form.isValid, + validate, + getData: form.getFormData, + }); + }, [form.isValid, onChange]); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + const subscription = form.subscribe(({ data, isValid }) => { + onChange({ + isValid, + validate, + getData: data.format, + }); + }); + return subscription.unsubscribe; + }, [onChange]); // eslint-disable-line react-hooks/exhaustive-deps + + return ( +
+ + + +

+ +

+
+
+ + + + + + +
+ + + + {/* Name field */} + + } + description={ + + } + > + + + + {/* version field */} + + } + description={ + + } + > + + + + {/* _meta field */} + + } + description={ + <> + + {i18n.translate( + 'xpack.idxMgmt.componentTemplateForm.stepLogistics.metaDocumentionLink', + { + defaultMessage: 'Learn more', + } + )} + + ), + }} + /> + + + + + } + checked={isMetaVisible} + onChange={(e) => setIsMetaVisible(e.target.checked)} + data-test-subj="metaToggle" + /> + + } + > + {isMetaVisible ? ( + + ) : ( + // requires children or a field + // For now, we return an empty
if the editor is not visible +
+ )} + + + ); + } +); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_container.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_container.tsx new file mode 100644 index 0000000000000..d71e36c0d997f --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_container.tsx @@ -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 React from 'react'; + +import { Forms } from '../../../shared_imports'; +import { WizardContent } from '../component_template_form'; +import { StepLogistics } from './step_logistics'; + +interface Props { + isEditing?: boolean; +} + +export const StepLogisticsContainer = ({ isEditing = false }: Props) => { + const { defaultValue, updateContent } = Forms.useContent('logistics'); + + return ( + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_schema.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_schema.tsx new file mode 100644 index 0000000000000..0c52037abde45 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_schema.tsx @@ -0,0 +1,102 @@ +/* + * 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 { EuiCode } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { FIELD_TYPES, fieldValidators, fieldFormatters, FormSchema } from '../../../shared_imports'; + +const { emptyField, containsCharsField, isJsonField } = fieldValidators; +const { toInt } = fieldFormatters; + +const stringifyJson = (json: { [key: string]: any }): string => + Object.keys(json).length ? JSON.stringify(json, null, 2) : '{\n\n}'; + +const parseJson = (jsonString: string): object => { + let parsedJSON: any; + + try { + parsedJSON = JSON.parse(jsonString); + } catch { + parsedJSON = {}; + } + + return parsedJSON; +}; + +export const logisticsFormSchema: FormSchema = { + name: { + defaultValue: undefined, + label: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepLogistics.nameFieldLabel', { + defaultMessage: 'Name', + }), + type: FIELD_TYPES.TEXT, + validations: [ + { + validator: emptyField( + i18n.translate('xpack.idxMgmt.componentTemplateForm.validation.nameRequiredError', { + defaultMessage: 'A component template name is required.', + }) + ), + }, + { + validator: containsCharsField({ + chars: ' ', + message: i18n.translate( + 'xpack.idxMgmt.componentTemplateForm.stepLogistics.validation.nameSpacesError', + { + defaultMessage: 'Spaces are not allowed in a component template name.', + } + ), + }), + }, + ], + }, + version: { + type: FIELD_TYPES.NUMBER, + label: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepLogistics.versionFieldLabel', { + defaultMessage: 'Version (optional)', + }), + formatters: [toInt], + }, + _meta: { + label: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepLogistics.metaFieldLabel', { + defaultMessage: 'Metadata (optional)', + }), + helpText: ( + {JSON.stringify({ arbitrary_data: 'anything_goes' })}, + }} + /> + ), + serializer: (value) => { + const result = parseJson(value); + // If an empty object was passed, strip out this value entirely. + if (!Object.keys(result).length) { + return undefined; + } + return result; + }, + deserializer: stringifyJson, + validations: [ + { + validator: isJsonField( + i18n.translate( + 'xpack.idxMgmt.componentTemplateForm.stepLogistics.validation.metaJsonError', + { + defaultMessage: 'The input is not valid.', + } + ), + { allowEmptyString: true } + ), + }, + ], + }, +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx new file mode 100644 index 0000000000000..ce85854dc79ab --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review.tsx @@ -0,0 +1,212 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { + EuiFlexGroup, + EuiTitle, + EuiFlexItem, + EuiSpacer, + EuiTabbedContent, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiText, + EuiCodeBlock, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + ComponentTemplateDeserialized, + serializers, + serializeComponentTemplate, +} from '../../../shared_imports'; + +const { stripEmptyFields } = serializers; + +const getDescriptionText = (data: any) => { + const hasEntries = data && Object.entries(data).length > 0; + + return hasEntries ? ( + + ) : ( + + ); +}; + +interface Props { + componentTemplate: ComponentTemplateDeserialized; +} + +export const StepReview: React.FunctionComponent = React.memo(({ componentTemplate }) => { + const { name } = componentTemplate; + + const serializedComponentTemplate = serializeComponentTemplate( + stripEmptyFields(componentTemplate, { + types: ['string', 'object'], + }) as ComponentTemplateDeserialized + ); + + const { + template: { + mappings: serializedMappings, + settings: serializedSettings, + aliases: serializedAliases, + }, + _meta: serializedMeta, + version: serializedVersion, + } = serializedComponentTemplate; + + const SummaryTab = () => ( +
+ + + + + + {/* Version */} + {typeof serializedVersion !== 'undefined' && ( + <> + + + + {serializedVersion} + + )} + + {/* Index settings */} + + + + + {getDescriptionText(serializedSettings)} + + + {/* Mappings */} + + + + + {getDescriptionText(serializedMappings)} + + + {/* Aliases */} + + + + + {getDescriptionText(serializedAliases)} + + + + + + {/* Metadata */} + {serializedMeta && ( + + + + + + + {JSON.stringify(serializedMeta, null, 2)} + + + + )} + + +
+ ); + + const RequestTab = () => { + const endpoint = `PUT _component_template/${name || ''}`; + const templateString = JSON.stringify(serializedComponentTemplate, null, 2); + const request = `${endpoint}\n${templateString}`; + + // Beyond a certain point, highlighting the syntax will bog down performance to unacceptable + // levels. This way we prevent that happening for very large requests. + const language = request.length < 60000 ? 'json' : undefined; + + return ( +
+ + + +

+ +

+
+ + + + + {request} + +
+ ); + }; + + return ( +
+ +

+ +

+
+ + + + , + }, + { + id: 'request', + name: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepReview.requestTabTitle', { + defaultMessage: 'Request', + }), + content: , + }, + ]} + /> +
+ ); +}); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review_container.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review_container.tsx new file mode 100644 index 0000000000000..10698afc5bc23 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_review_container.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 { Forms, ComponentTemplateDeserialized } from '../../../shared_imports'; +import { WizardContent } from '../component_template_form'; +import { StepReview } from './step_review'; + +interface Props { + getComponentTemplateData: (wizardContent: WizardContent) => ComponentTemplateDeserialized; +} + +export const StepReviewContainer = React.memo(({ getComponentTemplateData }: Props) => { + const { getData } = Forms.useMultiContentContext(); + + const wizardContent = getData(); + // Build the final template object, providing the wizard content data + const componentTemplate = getComponentTemplateData(wizardContent); + + return ; +}); diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/index.ts new file mode 100644 index 0000000000000..59168785b77b2 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/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 { ComponentTemplateCreate } from './component_template_create'; + +export { ComponentTemplateEdit } from './component_template_edit'; + +export { ComponentTemplateClone } from './component_template_clone'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx index bfea8d39e1203..ce9e28d0feefe 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_templates_context.tsx @@ -7,7 +7,8 @@ import React, { createContext, useContext } from 'react'; import { HttpSetup, DocLinksStart, NotificationsSetup } from 'src/core/public'; -import { getApi, getUseRequest, getSendRequest, getDocumentation } from './lib'; +import { ManagementAppMountParams } from 'src/plugins/management/public'; +import { getApi, getUseRequest, getSendRequest, getDocumentation, getBreadcrumbs } from './lib'; const ComponentTemplatesContext = createContext(undefined); @@ -17,6 +18,7 @@ interface Props { trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void; docLinks: DocLinksStart; toasts: NotificationsSetup['toasts']; + setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; } interface Context { @@ -24,6 +26,7 @@ interface Context { apiBasePath: string; api: ReturnType; documentation: ReturnType; + breadcrumbs: ReturnType; trackMetric: (type: 'loaded' | 'click' | 'count', eventName: string) => void; toasts: NotificationsSetup['toasts']; } @@ -35,17 +38,18 @@ export const ComponentTemplatesProvider = ({ value: Props; children: React.ReactNode; }) => { - const { httpClient, apiBasePath, trackMetric, docLinks, toasts } = value; + const { httpClient, apiBasePath, trackMetric, docLinks, toasts, setBreadcrumbs } = value; const useRequest = getUseRequest(httpClient); const sendRequest = getSendRequest(httpClient); const api = getApi(useRequest, sendRequest, apiBasePath, trackMetric); const documentation = getDocumentation(docLinks); + const breadcrumbs = getBreadcrumbs(setBreadcrumbs); return ( {children} diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts b/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts index e9acfa8dcc56d..897440feedf70 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/constants.ts @@ -9,6 +9,8 @@ export const UIM_COMPONENT_TEMPLATE_LIST_LOAD = 'component_template_list_load'; export const UIM_COMPONENT_TEMPLATE_DELETE = 'component_template_delete'; export const UIM_COMPONENT_TEMPLATE_DELETE_MANY = 'component_template_delete_many'; export const UIM_COMPONENT_TEMPLATE_DETAILS = 'component_template_details'; +export const UIM_COMPONENT_TEMPLATE_CREATE = 'component_template_create'; +export const UIM_COMPONENT_TEMPLATE_UPDATE = 'component_template_update'; // privileges export const APP_CLUSTER_REQUIRED_PRIVILEGES = ['manage_index_templates']; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/index.ts index 52235502e33df..7b40435464f2b 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/index.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/index.ts @@ -10,4 +10,10 @@ export { ComponentTemplateList } from './component_template_list'; export { ComponentTemplateDetailsFlyout } from './component_template_details'; +export { + ComponentTemplateCreate, + ComponentTemplateEdit, + ComponentTemplateClone, +} from './component_template_wizard'; + export * from './component_template_selector'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts index 63fe127c6b2d7..87f6767f14d5c 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/api.ts @@ -4,8 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ComponentTemplateListItem, ComponentTemplateDeserialized, Error } from '../shared_imports'; -import { UIM_COMPONENT_TEMPLATE_DELETE_MANY, UIM_COMPONENT_TEMPLATE_DELETE } from '../constants'; +import { + ComponentTemplateListItem, + ComponentTemplateDeserialized, + ComponentTemplateSerialized, + Error, +} from '../shared_imports'; +import { + UIM_COMPONENT_TEMPLATE_DELETE_MANY, + UIM_COMPONENT_TEMPLATE_DELETE, + UIM_COMPONENT_TEMPLATE_CREATE, + UIM_COMPONENT_TEMPLATE_UPDATE, +} from '../constants'; import { UseRequestHook, SendRequestHook } from './request'; export const getApi = ( @@ -44,9 +54,36 @@ export const getApi = ( }); } + async function createComponentTemplate(componentTemplate: ComponentTemplateSerialized) { + const result = await sendRequest({ + path: `${apiBasePath}/component_templates`, + method: 'post', + body: JSON.stringify(componentTemplate), + }); + + trackMetric('count', UIM_COMPONENT_TEMPLATE_CREATE); + + return result; + } + + async function updateComponentTemplate(componentTemplate: ComponentTemplateDeserialized) { + const { name } = componentTemplate; + const result = await sendRequest({ + path: `${apiBasePath}/component_templates/${encodeURIComponent(name)}`, + method: 'put', + body: JSON.stringify(componentTemplate), + }); + + trackMetric('count', UIM_COMPONENT_TEMPLATE_UPDATE); + + return result; + } + return { useLoadComponentTemplates, deleteComponentTemplates, useLoadComponentTemplate, + createComponentTemplate, + updateComponentTemplate, }; }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/breadcrumbs.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/breadcrumbs.ts new file mode 100644 index 0000000000000..033df5a9562ed --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/breadcrumbs.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 { ManagementAppMountParams } from 'src/plugins/management/public'; + +export const getBreadcrumbs = (setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']) => { + const baseBreadcrumbs = [ + { + text: i18n.translate('xpack.idxMgmt.componentTemplate.breadcrumb.homeLabel', { + defaultMessage: 'Index Management', + }), + href: '/', + }, + { + text: i18n.translate('xpack.idxMgmt.componentTemplate.breadcrumb.componentTemplatesLabel', { + defaultMessage: 'Component templates', + }), + href: '/component_templates', + }, + ]; + + const setCreateBreadcrumbs = () => { + const createBreadcrumbs = [ + ...baseBreadcrumbs, + { + text: i18n.translate( + 'xpack.idxMgmt.componentTemplate.breadcrumb.createComponentTemplateLabel', + { + defaultMessage: 'Create component template', + } + ), + }, + ]; + + return setBreadcrumbs(createBreadcrumbs); + }; + + const setEditBreadcrumbs = () => { + const editBreadcrumbs = [ + ...baseBreadcrumbs, + { + text: i18n.translate( + 'xpack.idxMgmt.componentTemplate.breadcrumb.editComponentTemplateLabel', + { + defaultMessage: 'Edit component template', + } + ), + }, + ]; + + return setBreadcrumbs(editBreadcrumbs); + }; + + return { + setCreateBreadcrumbs, + setEditBreadcrumbs, + }; +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/documentation.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/documentation.ts index 9d20ae9d2ec76..db06877d6e81a 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/documentation.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/documentation.ts @@ -11,6 +11,8 @@ export const getDocumentation = ({ ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }: DocL const esDocsBase = `${docsBase}/elasticsearch/reference/${DOC_LINK_VERSION}`; return { + esDocsBase, componentTemplates: `${esDocsBase}/indices-component-template.html`, + componentTemplatesMetadata: `${esDocsBase}/indices-component-template.html#component-templates-metadata`, }; }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/index.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/index.ts index 9a91312f83294..29273bd946e10 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/lib/index.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/index.ts @@ -9,3 +9,7 @@ export * from './api'; export * from './request'; export * from './documentation'; + +export * from './breadcrumbs'; + +export { attemptToDecodeURI } from './utils'; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/lib/utils.ts b/x-pack/plugins/index_management/public/application/components/component_templates/lib/utils.ts new file mode 100644 index 0000000000000..48a6d843c4fa7 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/component_templates/lib/utils.ts @@ -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. + */ + +export const attemptToDecodeURI = (value: string) => { + let result: string; + + try { + result = decodeURI(value); + result = decodeURIComponent(result); + } catch (e) { + result = decodeURIComponent(value); + } + + return result; +}; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts index bd19c2004894c..80e222f4f7706 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts +++ b/x-pack/plugins/index_management/public/application/components/component_templates/shared_imports.ts @@ -21,10 +21,44 @@ export { Forms, } from '../../../../../../../src/plugins/es_ui_shared/public'; -export { TabMappings, TabSettings, TabAliases } from '../shared'; +export { + serializers, + fieldValidators, + fieldFormatters, +} from '../../../../../../../src/plugins/es_ui_shared/static/forms/helpers'; + +export { + FormSchema, + FIELD_TYPES, + VALIDATION_TYPES, + FieldConfig, + useForm, + Form, + getUseField, +} from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; + +export { + getFormRow, + Field, + JsonEditorField, +} from '../../../../../../../src/plugins/es_ui_shared/static/forms/components'; + +export { isJSON } from '../../../../../../../src/plugins/es_ui_shared/static/validators/string'; + +export { + TabMappings, + TabSettings, + TabAliases, + CommonWizardSteps, + StepSettingsContainer, + StepMappingsContainer, + StepAliasesContainer, +} from '../shared'; export { ComponentTemplateSerialized, ComponentTemplateDeserialized, ComponentTemplateListItem, } from '../../../../common'; + +export { serializeComponentTemplate } from '../../../../common/lib'; diff --git a/x-pack/plugins/index_management/public/application/index.tsx b/x-pack/plugins/index_management/public/application/index.tsx index ff54b4b1bfe35..7b053a15b26d0 100644 --- a/x-pack/plugins/index_management/public/application/index.tsx +++ b/x-pack/plugins/index_management/public/application/index.tsx @@ -27,7 +27,7 @@ export const renderApp = ( const { i18n, docLinks, notifications } = core; const { Context: I18nContext } = i18n; - const { services, history } = dependencies; + const { services, history, setBreadcrumbs } = dependencies; const componentTemplateProviderValues = { httpClient: services.httpService.httpClient, @@ -35,6 +35,7 @@ export const renderApp = ( trackMetric: services.uiMetricService.trackMetric.bind(services.uiMetricService), docLinks, toasts: notifications.toasts, + setBreadcrumbs, }; render( diff --git a/x-pack/plugins/index_management/public/application/mount_management_section.ts b/x-pack/plugins/index_management/public/application/mount_management_section.ts index 258f32865720a..6145ea410b0e8 100644 --- a/x-pack/plugins/index_management/public/application/mount_management_section.ts +++ b/x-pack/plugins/index_management/public/application/mount_management_section.ts @@ -50,6 +50,7 @@ export async function mountManagementSection( }, services, history, + setBreadcrumbs, }; return renderApp(element, { core, dependencies: appDependencies }); diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts index 175254ca16e3d..56ee9640d3d07 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts @@ -4,17 +4,12 @@ * 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 { serializeComponentTemplate } from '../../../../common/lib'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; import { componentTemplateSchema } from './schema_validation'; -const bodySchema = schema.object({ - name: schema.string(), - ...componentTemplateSchema, -}); - export const registerCreateRoute = ({ router, license, @@ -24,13 +19,15 @@ export const registerCreateRoute = ({ { path: addBasePath('/component_templates'), validate: { - body: bodySchema, + body: componentTemplateSchema, }, }, license.guardApiRoute(async (ctx, req, res) => { const { callAsCurrentUser } = ctx.dataManagement!.client; - const { name, ...componentTemplateDefinition } = req.body; + const serializedComponentTemplate = serializeComponentTemplate(req.body); + + const { name } = req.body; try { // Check that a component template with the same name doesn't already exist @@ -60,7 +57,7 @@ export const registerCreateRoute = ({ try { const response = await callAsCurrentUser('dataManagement.saveComponentTemplate', { name, - body: componentTemplateDefinition, + body: serializedComponentTemplate, }); return res.ok({ body: response }); diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts index 7d32637c6b977..a1fc258127229 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/schema_validation.ts @@ -5,7 +5,8 @@ */ import { schema } from '@kbn/config-schema'; -export const componentTemplateSchema = { +export const componentTemplateSchema = schema.object({ + name: schema.string(), template: schema.object({ settings: schema.maybe(schema.object({}, { unknowns: 'allow' })), aliases: schema.maybe(schema.object({}, { unknowns: 'allow' })), @@ -13,4 +14,7 @@ export const componentTemplateSchema = { }), version: schema.maybe(schema.number()), _meta: schema.maybe(schema.object({}, { unknowns: 'allow' })), -}; + _kbnMeta: schema.object({ + usedBy: schema.arrayOf(schema.string()), + }), +}); diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts index 7e447bb110c67..47834a2cf499d 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts @@ -9,8 +9,6 @@ import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; import { componentTemplateSchema } from './schema_validation'; -const bodySchema = schema.object(componentTemplateSchema); - const paramsSchema = schema.object({ name: schema.string(), }); @@ -24,7 +22,7 @@ export const registerUpdateRoute = ({ { path: addBasePath('/component_templates/{name}'), validate: { - body: bodySchema, + body: componentTemplateSchema, params: paramsSchema, }, }, diff --git a/x-pack/test/api_integration/apis/management/index_management/component_templates.ts b/x-pack/test/api_integration/apis/management/index_management/component_templates.ts index 56b4ec45b42b7..1a00eaba35aa1 100644 --- a/x-pack/test/api_integration/apis/management/index_management/component_templates.ts +++ b/x-pack/test/api_integration/apis/management/index_management/component_templates.ts @@ -146,6 +146,9 @@ export default function ({ getService }: FtrProviderContext) { id: 10, }, }, + _kbnMeta: { + usedBy: [], + }, }) .expect(200); @@ -162,6 +165,9 @@ export default function ({ getService }: FtrProviderContext) { .send({ name: REQUIRED_FIELDS_COMPONENT_NAME, template: {}, + _kbnMeta: { + usedBy: [], + }, }) .expect(200); @@ -177,6 +183,9 @@ export default function ({ getService }: FtrProviderContext) { .send({ name: COMPONENT_NAME, template: {}, + _kbnMeta: { + usedBy: [], + }, }) .expect(409); @@ -233,7 +242,11 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxx') .send({ ...COMPONENT, + name: COMPONENT_NAME, version: 1, + _kbnMeta: { + usedBy: [], + }, }) .expect(200); @@ -250,7 +263,11 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxx') .send({ ...COMPONENT, + name: 'component_does_not_exist', version: 1, + _kbnMeta: { + usedBy: [], + }, }) .expect(404); From a9b543d9bca049bda4fe03977f23de9561765873 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 6 Jul 2020 18:40:12 -0400 Subject: [PATCH 34/46] reenable regression and classification functional tests (#70661) --- .../apps/ml/data_frame_analytics/classification_creation.ts | 4 ++-- .../apps/ml/data_frame_analytics/regression_creation.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts index 4a79610cadbde..5c750e119063e 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts @@ -10,8 +10,8 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - // flaky test https://github.com/elastic/kibana/issues/70455 - describe.skip('classification creation', function () { + + describe('classification creation', function () { before(async () => { await esArchiver.loadIfNeeded('ml/bm_classification'); await ml.testResources.createIndexPatternIfNeeded('ft_bank_marketing', '@timestamp'); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts index 33f0ee9cd99ac..9ddf2dfc48269 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts @@ -10,8 +10,8 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - // flaky test https://github.com/elastic/kibana/issues/70455 - describe.skip('regression creation', function () { + + describe('regression creation', function () { before(async () => { await esArchiver.loadIfNeeded('ml/egs_regression'); await ml.testResources.createIndexPatternIfNeeded('ft_egs_regression', '@timestamp'); From 59924243127bb314acb5d921bff882079ce926e2 Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Mon, 6 Jul 2020 18:52:00 -0400 Subject: [PATCH 35/46] add button link to ingest (#70142) update security solution empty page --- .../sections/epm/screens/home/index.tsx | 15 ++++- .../detection_engine/detection_engine.tsx | 4 +- .../detection_engine_empty_page.test.tsx | 19 ------ .../detection_engine_empty_page.tsx | 28 -------- .../detection_engine/rules/details/index.tsx | 4 +- .../__snapshots__/index.test.tsx.snap | 2 +- .../common/components/empty_page/index.tsx | 15 +++-- .../common/hooks/endpoint/ingest_enabled.ts | 34 ++++++++++ .../public/common/translations.ts | 14 ++-- .../public/hosts/pages/details/index.tsx | 4 +- .../public/hosts/pages/hosts.tsx | 4 +- .../public/hosts/pages/hosts_empty_page.tsx | 34 ---------- .../pages/endpoint_hosts/view/hooks.ts | 6 +- .../__snapshots__/index.test.tsx.snap | 2 +- .../public/network/pages/ip_details/index.tsx | 4 +- .../public/network/pages/network.tsx | 4 +- .../network/pages/network_empty_page.tsx | 34 ---------- .../components/overview_empty/index.tsx | 54 +++++++++++++-- .../public/overview/pages/overview.test.tsx | 66 ++++++++++++++----- .../source_status/elasticsearch_adapter.ts | 3 + 20 files changed, 180 insertions(+), 170 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_empty_page.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_empty_page.tsx create mode 100644 x-pack/plugins/security_solution/public/common/hooks/endpoint/ingest_enabled.ts delete mode 100644 x-pack/plugins/security_solution/public/hosts/pages/hosts_empty_page.tsx delete mode 100644 x-pack/plugins/security_solution/public/network/pages/network_empty_page.tsx diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx index e00b63e29019e..c68833c1b2d95 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx @@ -5,7 +5,7 @@ */ import React, { useState } from 'react'; -import { useRouteMatch, Switch, Route } from 'react-router-dom'; +import { useRouteMatch, Switch, Route, useLocation, useHistory } from 'react-router-dom'; import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab'; import { i18n } from '@kbn/i18n'; import { PAGE_ROUTING_PATHS } from '../../../../constants'; @@ -114,7 +114,10 @@ function InstalledPackages() { function AvailablePackages() { useBreadcrumbs('integrations_all'); - const [selectedCategory, setSelectedCategory] = useState(''); + const history = useHistory(); + const queryParams = new URLSearchParams(useLocation().search); + const initialCategory = queryParams.get('category') || ''; + const [selectedCategory, setSelectedCategory] = useState(initialCategory); const { data: categoryPackagesRes, isLoading: isLoadingPackages } = useGetPackages({ category: selectedCategory, }); @@ -141,7 +144,13 @@ function AvailablePackages() { isLoading={isLoadingCategories} categories={categories} selectedCategory={selectedCategory} - onCategoryChange={({ id }: CategorySummaryItem) => setSelectedCategory(id)} + onCategoryChange={({ id }: CategorySummaryItem) => { + // clear category query param in the url + if (queryParams.get('category')) { + history.push({}); + } + setSelectedCategory(id); + }} /> ) : null; diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx index 5c525a8553477..b39d51e2de95f 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx @@ -31,7 +31,7 @@ import { NoWriteAlertsCallOut } from '../../components/no_write_alerts_callout'; import { AlertsHistogramPanel } from '../../components/alerts_histogram_panel'; import { alertsHistogramOptions } from '../../components/alerts_histogram_panel/config'; import { useUserInfo } from '../../components/user_info'; -import { DetectionEngineEmptyPage } from './detection_engine_empty_page'; +import { OverviewEmpty } from '../../../overview/components/overview_empty'; import { DetectionEngineNoIndex } from './detection_engine_no_signal_index'; import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page'; import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unauthenticated'; @@ -159,7 +159,7 @@ export const DetectionEnginePageComponent: React.FC = ({ ) : ( - + )} diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_empty_page.test.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_empty_page.test.tsx deleted file mode 100644 index 039c878b121a0..0000000000000 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_empty_page.test.tsx +++ /dev/null @@ -1,19 +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 from 'react'; -import { shallow } from 'enzyme'; - -import { DetectionEngineEmptyPage } from './detection_engine_empty_page'; -jest.mock('../../../common/lib/kibana'); - -describe('DetectionEngineEmptyPage', () => { - it('renders correctly', () => { - const wrapper = shallow(); - - expect(wrapper.find('EmptyPage')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_empty_page.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_empty_page.tsx deleted file mode 100644 index 0c58f5620964b..0000000000000 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine_empty_page.tsx +++ /dev/null @@ -1,28 +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 from 'react'; - -import { useKibana } from '../../../common/lib/kibana'; -import { EmptyPage } from '../../../common/components/empty_page'; -import * as i18n from '../../../common/translations'; -import { ADD_DATA_PATH } from '../../../../common/constants'; - -export const DetectionEngineEmptyPage = React.memo(() => ( - -)); -DetectionEngineEmptyPage.displayName = 'DetectionEngineEmptyPage'; diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx index b937e95c0a57e..c73613842a872 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx @@ -43,7 +43,7 @@ import { DetectionEngineHeaderPage } from '../../../../components/detection_engi import { AlertsHistogramPanel } from '../../../../components/alerts_histogram_panel'; import { AlertsTable } from '../../../../components/alerts_table'; import { useUserInfo } from '../../../../components/user_info'; -import { DetectionEngineEmptyPage } from '../../detection_engine_empty_page'; +import { OverviewEmpty } from '../../../../../overview/components/overview_empty'; import { useAlertInfo } from '../../../../components/alerts_info'; import { StepDefineRule } from '../../../../components/rules/step_define_rule'; import { StepScheduleRule } from '../../../../components/rules/step_schedule_rule'; @@ -426,7 +426,7 @@ export const RuleDetailsPageComponent: FC = ({ - + )} diff --git a/x-pack/plugins/security_solution/public/common/components/empty_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/empty_page/__snapshots__/index.test.tsx.snap index 65893f84f5e56..623b15aa76d12 100644 --- a/x-pack/plugins/security_solution/public/common/components/empty_page/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/empty_page/__snapshots__/index.test.tsx.snap @@ -18,7 +18,7 @@ exports[`renders correctly 1`] = ` } - iconType="securityAnalyticsApp" + iconType="logoSecurity" title={

My Super Title diff --git a/x-pack/plugins/security_solution/public/common/components/empty_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/empty_page/index.tsx index a067c1d28f87f..f6d6752729b6d 100644 --- a/x-pack/plugins/security_solution/public/common/components/empty_page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/empty_page/index.tsx @@ -5,7 +5,7 @@ */ import { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, IconType } from '@elastic/eui'; -import React from 'react'; +import React, { MouseEventHandler, ReactNode } from 'react'; import styled from 'styled-components'; const EmptyPrompt = styled(EuiEmptyPrompt)` @@ -19,12 +19,14 @@ interface EmptyPageProps { actionPrimaryLabel: string; actionPrimaryTarget?: string; actionPrimaryUrl: string; + actionPrimaryFill?: boolean; actionSecondaryIcon?: IconType; actionSecondaryLabel?: string; actionSecondaryTarget?: string; actionSecondaryUrl?: string; + actionSecondaryOnClick?: MouseEventHandler; 'data-test-subj'?: string; - message?: string; + message?: ReactNode; title: string; } @@ -34,23 +36,25 @@ export const EmptyPage = React.memo( actionPrimaryLabel, actionPrimaryTarget, actionPrimaryUrl, + actionPrimaryFill = true, actionSecondaryIcon, actionSecondaryLabel, actionSecondaryTarget, actionSecondaryUrl, + actionSecondaryOnClick, message, title, ...rest }) => ( {title}

} body={message &&

{message}

} actions={ ( {actionSecondaryLabel && actionSecondaryUrl && ( + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} {actionSecondaryLabel} diff --git a/x-pack/plugins/security_solution/public/common/hooks/endpoint/ingest_enabled.ts b/x-pack/plugins/security_solution/public/common/hooks/endpoint/ingest_enabled.ts new file mode 100644 index 0000000000000..c201d85a270c0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/endpoint/ingest_enabled.ts @@ -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 { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; + +/** + * Returns an object which ingest permissions are allowed + */ +export const useIngestEnabledCheck = (): { + allEnabled: boolean; + show: boolean; + write: boolean; + read: boolean; +} => { + const { services } = useKibana(); + + // Check if Ingest Manager is present in the configuration + const show = services.application.capabilities.ingestManager?.show ?? false; + const write = services.application.capabilities.ingestManager?.write ?? false; + const read = services.application.capabilities.ingestManager?.read ?? false; + + // Check if all Ingest Manager permissions are enabled + const allEnabled = show && read && write ? true : false; + + return { + allEnabled, + show, + write, + read, + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/translations.ts b/x-pack/plugins/security_solution/public/common/translations.ts index 677543ec0dba6..413119fb40f14 100644 --- a/x-pack/plugins/security_solution/public/common/translations.ts +++ b/x-pack/plugins/security_solution/public/common/translations.ts @@ -10,11 +10,6 @@ export const EMPTY_TITLE = i18n.translate('xpack.securitySolution.pages.common.e defaultMessage: 'Welcome to Security Solution. Let’s get you started.', }); -export const EMPTY_MESSAGE = i18n.translate('xpack.securitySolution.pages.common.emptyMessage', { - defaultMessage: - 'To begin using security information and event management (Security Solution), you’ll need to add security solution related data, in Elastic Common Schema (ECS) format, to the Elastic Stack. An easy way to get started is by installing and configuring our data shippers, called Beats. Let’s do that now!', -}); - export const EMPTY_ACTION_PRIMARY = i18n.translate( 'xpack.securitySolution.pages.common.emptyActionPrimary', { @@ -25,6 +20,13 @@ export const EMPTY_ACTION_PRIMARY = i18n.translate( export const EMPTY_ACTION_SECONDARY = i18n.translate( 'xpack.securitySolution.pages.common.emptyActionSecondary', { - defaultMessage: 'View getting started guide', + defaultMessage: 'getting started guide.', + } +); + +export const EMPTY_ACTION_ENDPOINT = i18n.translate( + 'xpack.securitySolution.pages.common.emptyActionEndpoint', + { + defaultMessage: 'Add data with Elastic Agent (Beta)', } ); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index 46823f037b61c..bb0317f0482b0 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -38,7 +38,7 @@ import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '. import { SpyRoute } from '../../../common/utils/route/spy_routes'; import { esQuery, Filter } from '../../../../../../../src/plugins/data/public'; -import { HostsEmptyPage } from '../hosts_empty_page'; +import { OverviewEmpty } from '../../../overview/components/overview_empty'; import { HostDetailsTabs } from './details_tabs'; import { navTabsHostDetails } from './nav_tabs'; import { HostDetailsProps } from './types'; @@ -194,7 +194,7 @@ const HostDetailsComponent = React.memo( - + )} diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index 90438aec7c27e..a2f83bf0965f3 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -33,7 +33,7 @@ import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from import { SpyRoute } from '../../common/utils/route/spy_routes'; import { esQuery } from '../../../../../../src/plugins/data/public'; import { useMlCapabilities } from '../../common/components/ml_popover/hooks/use_ml_capabilities'; -import { HostsEmptyPage } from './hosts_empty_page'; +import { OverviewEmpty } from '../../overview/components/overview_empty'; import { HostsTabs } from './hosts_tabs'; import { navTabsHosts } from './nav_tabs'; import * as i18n from './translations'; @@ -141,7 +141,7 @@ export const HostsComponent = React.memo( - + )} diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts_empty_page.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts_empty_page.tsx deleted file mode 100644 index a01e249561e5c..0000000000000 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts_empty_page.tsx +++ /dev/null @@ -1,34 +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 from 'react'; - -import { EmptyPage } from '../../common/components/empty_page'; -import { useKibana } from '../../common/lib/kibana'; -import * as i18n from '../../common/translations'; -import { ADD_DATA_PATH } from '../../../common/constants'; - -export const HostsEmptyPage = React.memo(() => { - const { http, docLinks } = useKibana().services; - const basePath = http.basePath.get(); - - return ( - - ); -}); - -HostsEmptyPage.displayName = 'HostsEmptyPage'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts index 68198b691da40..b048a8f69b5d2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts @@ -24,16 +24,16 @@ export function useHostSelector(selector: (state: HostState) => TSele /** * Returns an object that contains Ingest app and URL information */ -export const useHostIngestUrl = (): { url: string; appId: string; appPath: string } => { +export const useIngestUrl = (subpath: string): { url: string; appId: string; appPath: string } => { const { services } = useKibana(); return useMemo(() => { - const appPath = `#/fleet`; + const appPath = `#/${subpath}`; return { url: `${services.application.getUrlForApp('ingestManager')}${appPath}`, appId: 'ingestManager', appPath, }; - }, [services.application]); + }, [services.application, subpath]); }; /** diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/pages/ip_details/__snapshots__/index.test.tsx.snap index d7af8d6910f45..93dafeff34ce9 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/__snapshots__/index.test.tsx.snap @@ -9,7 +9,7 @@ exports[`Ip Details it matches the snapshot 1`] = ` border={true} title="123.456.78.90" /> - + - + )} diff --git a/x-pack/plugins/security_solution/public/network/pages/network.tsx b/x-pack/plugins/security_solution/public/network/pages/network.tsx index bdaac1ac049e5..5767951f9f6b3 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.tsx @@ -34,7 +34,7 @@ import { SpyRoute } from '../../common/utils/route/spy_routes'; import { networkModel } from '../store'; import { navTabsNetwork, NetworkRoutes, NetworkRoutesLoading } from './navigation'; import { filterNetworkData } from './navigation/alerts_query_tab_body'; -import { NetworkEmptyPage } from './network_empty_page'; +import { OverviewEmpty } from '../../overview/components/overview_empty'; import * as i18n from './translations'; import { NetworkComponentProps } from './types'; import { NetworkRouteType } from './navigation/types'; @@ -164,7 +164,7 @@ const NetworkComponent = React.memo( ) : ( - + )} diff --git a/x-pack/plugins/security_solution/public/network/pages/network_empty_page.tsx b/x-pack/plugins/security_solution/public/network/pages/network_empty_page.tsx deleted file mode 100644 index dce3f85797f12..0000000000000 --- a/x-pack/plugins/security_solution/public/network/pages/network_empty_page.tsx +++ /dev/null @@ -1,34 +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 from 'react'; - -import { useKibana } from '../../common/lib/kibana'; -import { EmptyPage } from '../../common/components/empty_page'; -import * as i18n from '../../common/translations'; -import { ADD_DATA_PATH } from '../../../common/constants'; - -export const NetworkEmptyPage = React.memo(() => { - const { http, docLinks } = useKibana().services; - const basePath = http.basePath.get(); - - return ( - - ); -}); - -NetworkEmptyPage.displayName = 'NetworkEmptyPage'; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx index 00db437bce11e..33413be10079e 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx @@ -5,27 +5,67 @@ */ import React from 'react'; - +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiLink } from '@elastic/eui'; import * as i18nCommon from '../../../common/translations'; import { EmptyPage } from '../../../common/components/empty_page'; import { useKibana } from '../../../common/lib/kibana'; import { ADD_DATA_PATH } from '../../../../common/constants'; +import { useIngestUrl } from '../../../management/pages/endpoint_hosts/view/hooks'; +import { useNavigateToAppEventHandler } from '../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; +import { useIngestEnabledCheck } from '../../../common/hooks/endpoint/ingest_enabled'; const OverviewEmptyComponent: React.FC = () => { const { http, docLinks } = useKibana().services; const basePath = http.basePath.get(); + const { appId: ingestAppId, appPath: ingestPath, url: ingestUrl } = useIngestUrl( + 'integrations?category=security' + ); + const handleOnClick = useNavigateToAppEventHandler(ingestAppId, { path: ingestPath }); + const { allEnabled: isIngestEnabled } = useIngestEnabledCheck(); - return ( + return isIngestEnabled === true ? ( + + + + {i18nCommon.EMPTY_ACTION_SECONDARY} + + + } + title={i18nCommon.EMPTY_TITLE} + /> + ) : ( + + + {i18nCommon.EMPTY_ACTION_SECONDARY} + + + } title={i18nCommon.EMPTY_TITLE} /> ); diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx index 9613a1e7210a3..6f13f64ca1bff 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx @@ -16,6 +16,7 @@ import { UseMessagesStorage, } from '../../common/containers/local_storage/use_messages_storage'; import { Overview } from './index'; +import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled'; jest.mock('../../common/lib/kibana'); jest.mock('../../common/containers/source'); @@ -33,6 +34,7 @@ jest.mock('../../common/components/search_bar', () => ({ jest.mock('../../common/components/query_bar', () => ({ QueryBar: () => null, })); +jest.mock('../../common/hooks/endpoint/ingest_enabled'); jest.mock('../../common/containers/local_storage/use_messages_storage'); const endpointNoticeMessage = (hasMessageValue: boolean) => { @@ -47,26 +49,54 @@ const endpointNoticeMessage = (hasMessageValue: boolean) => { describe('Overview', () => { describe('rendering', () => { - test('it renders the Setup Instructions text when no index is available', async () => { - (useWithSource as jest.Mock).mockReturnValue({ - indicesExist: false, + describe('when no index is available', () => { + beforeEach(() => { + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: false, + }); + (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: false }); + const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock< + UseMessagesStorage + >; + mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); }); - const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; - mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); + it('renders the Setup Instructions text', () => { + const wrapper = mount( + + + + + + ); + expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(true); + }); - const wrapper = mount( - - - - - - ); + it('does not show Endpoint get ready button when ingest is not enabled', () => { + const wrapper = mount( + + + + + + ); + expect(wrapper.find('[data-test-subj="empty-page-secondary-action"]').exists()).toBe(false); + }); - expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(true); + it('shows Endpoint get ready button when ingest is enabled', () => { + (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true }); + const wrapper = mount( + + + + + + ); + expect(wrapper.find('[data-test-subj="empty-page-secondary-action"]').exists()).toBe(true); + }); }); - test('it DOES NOT render the Getting started text when an index is available', async () => { + it('it DOES NOT render the Getting started text when an index is available', () => { (useWithSource as jest.Mock).mockReturnValue({ indicesExist: true, indexPattern: {}, @@ -85,7 +115,7 @@ describe('Overview', () => { expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(false); }); - test('it DOES render the Endpoint banner when the endpoint index is NOT available AND storage is NOT set', async () => { + test('it DOES render the Endpoint banner when the endpoint index is NOT available AND storage is NOT set', () => { (useWithSource as jest.Mock).mockReturnValueOnce({ indicesExist: true, indexPattern: {}, @@ -109,7 +139,7 @@ describe('Overview', () => { expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(true); }); - test('it does NOT render the Endpoint banner when the endpoint index is NOT available but storage is set', async () => { + test('it does NOT render the Endpoint banner when the endpoint index is NOT available but storage is set', () => { (useWithSource as jest.Mock).mockReturnValueOnce({ indicesExist: true, indexPattern: {}, @@ -133,7 +163,7 @@ describe('Overview', () => { expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(false); }); - test('it does NOT render the Endpoint banner when the endpoint index is available AND storage is set', async () => { + test('it does NOT render the Endpoint banner when the endpoint index is available AND storage is set', () => { (useWithSource as jest.Mock).mockReturnValue({ indicesExist: true, indexPattern: {}, @@ -152,7 +182,7 @@ describe('Overview', () => { expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(false); }); - test('it does NOT render the Endpoint banner when an index IS available but storage is NOT set', async () => { + test('it does NOT render the Endpoint banner when an index IS available but storage is NOT set', () => { (useWithSource as jest.Mock).mockReturnValue({ indicesExist: true, indexPattern: {}, diff --git a/x-pack/plugins/security_solution/server/lib/source_status/elasticsearch_adapter.ts b/x-pack/plugins/security_solution/server/lib/source_status/elasticsearch_adapter.ts index 8872d347da826..ab491f54854e4 100644 --- a/x-pack/plugins/security_solution/server/lib/source_status/elasticsearch_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/source_status/elasticsearch_adapter.ts @@ -8,6 +8,7 @@ import { FrameworkAdapter, FrameworkRequest } from '../framework'; import { SourceStatusAdapter } from './index'; import { buildQuery } from './query.dsl'; import { ApmServiceNameAgg } from './types'; +import { ENDPOINT_METADATA_INDEX } from '../../../common/constants'; const APM_INDEX_NAME = 'apm-*-transaction*'; @@ -18,6 +19,8 @@ export class ElasticsearchSourceStatusAdapter implements SourceStatusAdapter { // Intended flow to determine app-empty state is to first check siem indices (as this is a quick shard count), and // if no shards exist only then perform the heavier APM query. This optimizes for normal use when siem data exists try { + // Add endpoint metadata index to indices to check + indexNames.push(ENDPOINT_METADATA_INDEX); // Remove APM index if exists, and only query if length > 0 in case it's the only index provided const nonApmIndexNames = indexNames.filter((name) => name !== APM_INDEX_NAME); const indexCheckResponse = await (nonApmIndexNames.length > 0 From 57915e164169df6026e766bcfe6a754cfa6228a2 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Mon, 6 Jul 2020 16:38:45 -0700 Subject: [PATCH 36/46] ServiceNow push to Incident generic implementation (supporting both Case specific and generic Alerts) (#68464) * Draft ServiceNow generic implementation * simple working servicenow incident per alert * fixed running times * rely on externalId for update incident on the next execution * Added consumer to the action type to be able to split ServiceNow for Cases and Alerts * Added subActions support for ServiceNow action form * Basic version for Alerts part for ServiceNow * Keep Case ServiceNow functionality working * Revert changes on app_router * Fixed type checks * Fixed language check issues * Fixed actions unit tests * Fixed functional tests * Fixed jest tests * fixed tests * Copied case mappings to alerting plugin * made consumer optional * Cleanup tests * more cleanup * Fixed jest tests and type checks * fixed tests * fixed servicenow validation tests * Added ServiceNow unit tests * Removed consumer for actions * fixed client side isCaseOwned support * fixed failing tests * fixed jest tests * Fixed URL validation * fixed due to comments * fixed tests * fixed jest tests * Fixed due to comments. Moved ServiceNow filtering in case plugin to server side * fixed mock for ServiceNow * fixed consumer config * fixed test * fixed type check * Fixed jest test * fixed type check --- .../pre-configured-connectors.asciidoc | 4 +- .../plugins/actions/server/actions_client.ts | 2 +- .../server/builtin_action_types/case/api.ts | 2 +- .../builtin_action_types/case/schema.ts | 2 +- .../server/builtin_action_types/case/types.ts | 2 +- .../builtin_action_types/case/utils.test.ts | 139 +--------- .../server/builtin_action_types/case/utils.ts | 54 +--- .../server/builtin_action_types/index.ts | 2 +- .../server/builtin_action_types/jira/mocks.ts | 2 +- .../builtin_action_types/jira/service.test.ts | 6 +- .../builtin_action_types/jira/service.ts | 2 +- .../lib/axios_utils.test.ts | 105 +++++++ .../builtin_action_types/lib/axios_utils.ts | 60 ++++ .../servicenow/api.test.ts | 257 +++++++++++------ .../builtin_action_types/servicenow/api.ts | 142 +++++++++- .../servicenow/case_shema.ts | 36 +++ .../servicenow/case_types.ts | 64 +++++ .../builtin_action_types/servicenow/index.ts | 107 +++++-- .../builtin_action_types/servicenow/mocks.ts | 31 +-- .../builtin_action_types/servicenow/schema.ts | 70 +++++ .../servicenow/service.test.ts | 56 +--- .../servicenow/service.ts | 62 ++--- .../servicenow/translations.ts | 18 +- .../builtin_action_types/servicenow/types.ts | 97 ++++++- .../servicenow/validators.ts | 34 ++- x-pack/plugins/case/common/api/cases/case.ts | 2 +- x-pack/plugins/case/common/constants.ts | 1 + .../routes/api/__mocks__/request_responses.ts | 3 +- .../api/cases/configure/get_connectors.ts | 9 +- .../components/configure_cases/index.test.tsx | 10 +- .../components/configure_cases/index.tsx | 1 + .../public/cases/containers/configure/mock.ts | 6 +- .../public/cases/containers/mock.ts | 2 +- .../use_post_push_to_service.test.tsx | 2 +- .../containers/use_post_push_to_service.tsx | 2 +- .../public/common/lib/connectors/config.ts | 5 +- .../public/common/lib/connectors/index.ts | 1 - .../lib/connectors/servicenow/flyout.tsx | 87 ------ .../lib/connectors/servicenow/index.tsx | 47 ---- .../lib/connectors/servicenow/translations.ts | 30 -- .../common/lib/connectors/servicenow/types.ts | 22 -- .../security_solution/public/plugin.tsx | 3 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - x-pack/plugins/triggers_actions_ui/README.md | 7 +- .../components/builtin_action_types/index.ts | 2 + .../case_mappings/field_mapping.tsx | 141 ++++++++++ .../case_mappings/field_mapping_row.tsx | 78 ++++++ .../servicenow/case_mappings/translations.ts | 190 +++++++++++++ .../servicenow/case_mappings/types.ts} | 16 +- .../servicenow/case_mappings/utils.ts | 38 +++ .../servicenow/config.ts | 3 +- .../builtin_action_types/servicenow/index.ts | 7 + .../builtin_action_types}/servicenow/logo.svg | 0 .../servicenow/servicenow.test.tsx | 97 +++++++ .../servicenow/servicenow.tsx | 67 +++++ .../servicenow/servicenow_connectors.test.tsx | 83 ++++++ .../servicenow/servicenow_connectors.tsx | 182 ++++++++++++ .../servicenow/servicenow_params.test.tsx | 43 +++ .../servicenow/servicenow_params.tsx | 262 ++++++++++++++++++ .../servicenow/translations.ts | 133 +++++++++ .../builtin_action_types/servicenow/types.ts | 46 +++ .../slack/slack_connectors.tsx | 2 +- .../context/actions_connectors_context.tsx | 1 + .../application/lib/value_validators.test.ts | 16 +- .../application/lib/value_validators.ts | 12 + .../action_connector_form.tsx | 3 + .../action_form.test.tsx | 10 + .../action_connector_form/action_form.tsx | 10 +- .../connector_add_flyout.tsx | 2 + .../connector_add_modal.tsx | 3 + .../connector_edit_flyout.tsx | 2 + .../actions_connectors_list.test.tsx | 130 +++++---- .../components/actions_connectors_list.tsx | 10 +- .../public/common/index.ts | 2 + .../triggers_actions_ui/public/types.ts | 1 + .../builtin_action_types/servicenow.ts | 21 +- .../server/servicenow_simulation.ts | 4 + .../actions/builtin_action_types/jira.ts | 18 +- .../builtin_action_types/servicenow.ts | 123 +++----- 80 files changed, 2559 insertions(+), 797 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/case_shema.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/case_types.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts delete mode 100644 x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/flyout.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/translations.ts delete mode 100644 x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/types.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping_row.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/translations.ts rename x-pack/plugins/{actions/server/builtin_action_types/servicenow/config.ts => triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/types.ts} (50%) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/utils.ts rename x-pack/plugins/{security_solution/public/common/lib/connectors => triggers_actions_ui/public/application/components/builtin_action_types}/servicenow/config.ts (91%) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/index.ts rename x-pack/plugins/{security_solution/public/common/lib/connectors => triggers_actions_ui/public/application/components/builtin_action_types}/servicenow/logo.svg (100%) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts diff --git a/docs/user/alerting/action-types/pre-configured-connectors.asciidoc b/docs/user/alerting/action-types/pre-configured-connectors.asciidoc index b1cf2d650e576..e3f1703f08e88 100644 --- a/docs/user/alerting/action-types/pre-configured-connectors.asciidoc +++ b/docs/user/alerting/action-types/pre-configured-connectors.asciidoc @@ -28,12 +28,12 @@ two out-of-the box connectors: <> and < actionTypeId: .slack <2> name: 'Slack #xyz' <3> - secrets: <4> + secrets: webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz' webhook-service: actionTypeId: .webhook name: 'Email service' - config: + config: <4> url: 'https://email-alert-service.elastic.co' method: post headers: diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index a512d314fb7e2..44f9cfd5c9e61 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -148,7 +148,7 @@ export class ActionsClient { this.actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); - const result = await this.savedObjectsClient.update('action', id, { + const result = await this.savedObjectsClient.update('action', id, { actionTypeId, name, config: validatedActionTypeConfig as SavedObjectAttributes, diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/api.ts b/x-pack/plugins/actions/server/builtin_action_types/case/api.ts index 6dc8a9cc9af6a..de4b7edaed3da 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/api.ts @@ -41,7 +41,7 @@ const pushToServiceHandler = async ({ } const fields = prepareFieldsForTransformation({ - params, + externalCase: params.externalCase, mapping, defaultPipes, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts index 33b2ad6d18684..f47686c911ff0 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts @@ -67,7 +67,7 @@ export const ExecutorSubActionSchema = schema.oneOf([ ]); export const ExecutorSubActionPushParamsSchema = schema.object({ - caseId: schema.string(), + savedObjectId: schema.string(), title: schema.string(), description: schema.nullable(schema.string()), comments: schema.nullable(schema.arrayOf(CommentSchema)), diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/types.ts b/x-pack/plugins/actions/server/builtin_action_types/case/types.ts index 992b2cb16fb06..de96864d0b295 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/types.ts @@ -144,7 +144,7 @@ export interface PipedField { } export interface PrepareFieldsForTransformArgs { - params: PushToServiceApiParams; + externalCase: Record; mapping: Map; defaultPipes?: string[]; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts index 017fc73efae20..dbb18fa5c695c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import axios from 'axios'; - import { normalizeMapping, buildMap, @@ -13,19 +11,11 @@ import { prepareFieldsForTransformation, transformFields, transformComments, - addTimeZoneToDate, - throwIfNotAlive, - request, - patch, - getErrorMessage, } from './utils'; import { SUPPORTED_SOURCE_FIELDS } from './constants'; import { Comment, MapRecord, PushToServiceApiParams } from './types'; -jest.mock('axios'); -const axiosMock = (axios as unknown) as jest.Mock; - const mapping: MapRecord[] = [ { source: 'title', target: 'short_description', actionType: 'overwrite' }, { source: 'description', target: 'description', actionType: 'append' }, @@ -63,7 +53,7 @@ const maliciousMapping: MapRecord[] = [ ]; const fullParams: PushToServiceApiParams = { - caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', + savedObjectId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', title: 'a title', description: 'a description', createdAt: '2020-03-13T08:34:53.450Z', @@ -132,7 +122,7 @@ describe('buildMap', () => { describe('mapParams', () => { test('maps params correctly', () => { const params = { - caseId: '123', + savedObjectId: '123', incidentId: '456', title: 'Incident title', description: 'Incident description', @@ -148,7 +138,7 @@ describe('mapParams', () => { test('do not add fields not in mapping', () => { const params = { - caseId: '123', + savedObjectId: '123', incidentId: '456', title: 'Incident title', description: 'Incident description', @@ -164,7 +154,7 @@ describe('mapParams', () => { describe('prepareFieldsForTransformation', () => { test('prepare fields with defaults', () => { const res = prepareFieldsForTransformation({ - params: fullParams, + externalCase: fullParams.externalCase, mapping: finalMapping, }); expect(res).toEqual([ @@ -185,7 +175,7 @@ describe('prepareFieldsForTransformation', () => { test('prepare fields with default pipes', () => { const res = prepareFieldsForTransformation({ - params: fullParams, + externalCase: fullParams.externalCase, mapping: finalMapping, defaultPipes: ['myTestPipe'], }); @@ -209,7 +199,7 @@ describe('prepareFieldsForTransformation', () => { describe('transformFields', () => { test('transform fields for creation correctly', () => { const fields = prepareFieldsForTransformation({ - params: fullParams, + externalCase: fullParams.externalCase, mapping: finalMapping, }); @@ -226,14 +216,7 @@ describe('transformFields', () => { test('transform fields for update correctly', () => { const fields = prepareFieldsForTransformation({ - params: { - ...fullParams, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { - username: 'anotherUser', - fullName: 'Another User', - }, - }, + externalCase: fullParams.externalCase, mapping: finalMapping, defaultPipes: ['informationUpdated'], }); @@ -262,7 +245,7 @@ describe('transformFields', () => { test('add newline character to descripton', () => { const fields = prepareFieldsForTransformation({ - params: fullParams, + externalCase: fullParams.externalCase, mapping: finalMapping, defaultPipes: ['informationUpdated'], }); @@ -280,7 +263,7 @@ describe('transformFields', () => { test('append username if fullname is undefined when create', () => { const fields = prepareFieldsForTransformation({ - params: fullParams, + externalCase: fullParams.externalCase, mapping: finalMapping, }); @@ -300,14 +283,7 @@ describe('transformFields', () => { test('append username if fullname is undefined when update', () => { const fields = prepareFieldsForTransformation({ - params: { - ...fullParams, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { - username: 'anotherUser', - fullName: 'Another User', - }, - }, + externalCase: fullParams.externalCase, mapping: finalMapping, defaultPipes: ['informationUpdated'], }); @@ -479,98 +455,3 @@ describe('transformComments', () => { ]); }); }); - -describe('addTimeZoneToDate', () => { - test('adds timezone with default', () => { - const date = addTimeZoneToDate('2020-04-14T15:01:55.456Z'); - expect(date).toBe('2020-04-14T15:01:55.456Z GMT'); - }); - - test('adds timezone correctly', () => { - const date = addTimeZoneToDate('2020-04-14T15:01:55.456Z', 'PST'); - expect(date).toBe('2020-04-14T15:01:55.456Z PST'); - }); -}); - -describe('throwIfNotAlive ', () => { - test('throws correctly when status is invalid', async () => { - expect(() => { - throwIfNotAlive(404, 'application/json'); - }).toThrow('Instance is not alive.'); - }); - - test('throws correctly when content is invalid', () => { - expect(() => { - throwIfNotAlive(200, 'application/html'); - }).toThrow('Instance is not alive.'); - }); - - test('do NOT throws with custom validStatusCodes', async () => { - expect(() => { - throwIfNotAlive(404, 'application/json', [404]); - }).not.toThrow('Instance is not alive.'); - }); -}); - -describe('request', () => { - beforeEach(() => { - axiosMock.mockImplementation(() => ({ - status: 200, - headers: { 'content-type': 'application/json' }, - data: { incidentId: '123' }, - })); - }); - - test('it fetch correctly with defaults', async () => { - const res = await request({ axios, url: '/test' }); - - expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'get', data: {} }); - expect(res).toEqual({ - status: 200, - headers: { 'content-type': 'application/json' }, - data: { incidentId: '123' }, - }); - }); - - test('it fetch correctly', async () => { - const res = await request({ axios, url: '/test', method: 'post', data: { id: '123' } }); - - expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'post', data: { id: '123' } }); - expect(res).toEqual({ - status: 200, - headers: { 'content-type': 'application/json' }, - data: { incidentId: '123' }, - }); - }); - - test('it throws correctly', async () => { - axiosMock.mockImplementation(() => ({ - status: 404, - headers: { 'content-type': 'application/json' }, - data: { incidentId: '123' }, - })); - - await expect(request({ axios, url: '/test' })).rejects.toThrow(); - }); -}); - -describe('patch', () => { - beforeEach(() => { - axiosMock.mockImplementation(() => ({ - status: 200, - headers: { 'content-type': 'application/json' }, - })); - }); - - test('it fetch correctly', async () => { - await patch({ axios, url: '/test', data: { id: '123' } }); - expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'patch', data: { id: '123' } }); - }); -}); - -describe('getErrorMessage', () => { - test('it returns the correct error message', () => { - const msg = getErrorMessage('My connector name', 'An error has occurred'); - expect(msg).toBe('[Action][My connector name]: An error has occurred'); - }); -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts index 2d81c2bf4e15f..676a4776d0055 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts @@ -6,7 +6,6 @@ import { curry, flow, get } from 'lodash'; import { schema } from '@kbn/config-schema'; -import { AxiosInstance, Method, AxiosResponse } from 'axios'; import { ActionTypeExecutorOptions, ActionTypeExecutorResult, ActionType } from '../../types'; @@ -134,65 +133,18 @@ export const createConnector = ({ }); }; -export const throwIfNotAlive = ( - status: number, - contentType: string, - validStatusCodes: number[] = [200, 201, 204] -) => { - if (!validStatusCodes.includes(status) || !contentType.includes('application/json')) { - throw new Error('Instance is not alive.'); - } -}; - -export const request = async ({ - axios, - url, - method = 'get', - data, -}: { - axios: AxiosInstance; - url: string; - method?: Method; - data?: T; -}): Promise => { - const res = await axios(url, { method, data: data ?? {} }); - throwIfNotAlive(res.status, res.headers['content-type']); - return res; -}; - -export const patch = async ({ - axios, - url, - data, -}: { - axios: AxiosInstance; - url: string; - data: T; -}): Promise => { - return request({ - axios, - url, - method: 'patch', - data, - }); -}; - -export const addTimeZoneToDate = (date: string, timezone = 'GMT'): string => { - return `${date} ${timezone}`; -}; - export const prepareFieldsForTransformation = ({ - params, + externalCase, mapping, defaultPipes = ['informationCreated'], }: PrepareFieldsForTransformArgs): PipedField[] => { - return Object.keys(params.externalCase) + return Object.keys(externalCase) .filter((p) => mapping.get(p)?.actionType != null && mapping.get(p)?.actionType !== 'nothing') .map((p) => { const actionType = mapping.get(p)?.actionType ?? 'nothing'; return { key: p, - value: params.externalCase[p], + value: externalCase[p], actionType, pipes: actionType === 'append' ? [...defaultPipes, 'append'] : defaultPipes, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.ts b/x-pack/plugins/actions/server/builtin_action_types/index.ts index 6ba4d7cfc7de0..0020161789d71 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.ts @@ -32,6 +32,6 @@ export function registerBuiltInActionTypes({ actionTypeRegistry.register(getServerLogActionType({ logger })); actionTypeRegistry.register(getSlackActionType({ configurationUtilities })); actionTypeRegistry.register(getWebhookActionType({ logger, configurationUtilities })); - actionTypeRegistry.register(getServiceNowActionType({ configurationUtilities })); + actionTypeRegistry.register(getServiceNowActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getJiraActionType({ configurationUtilities })); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts index 3ae0e9db36de0..709d490a5227f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts @@ -88,7 +88,7 @@ mapping.set('summary', { }); const executorParams: ExecutorSubActionPushParams = { - caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', + savedObjectId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', externalId: 'incident-3', createdAt: '2020-04-27T10:59:46.202Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts index b9225b043d526..3de3926b7d821 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts @@ -7,12 +7,12 @@ import axios from 'axios'; import { createExternalService } from './service'; -import * as utils from '../case/utils'; +import * as utils from '../lib/axios_utils'; import { ExternalService } from '../case/types'; jest.mock('axios'); -jest.mock('../case/utils', () => { - const originalUtils = jest.requireActual('../case/utils'); +jest.mock('../lib/axios_utils', () => { + const originalUtils = jest.requireActual('../lib/axios_utils'); return { ...originalUtils, request: jest.fn(), diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index ff22b8368e7dd..240b645c3a7dc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -16,7 +16,7 @@ import { } from './types'; import * as i18n from './translations'; -import { getErrorMessage, request } from '../case/utils'; +import { request, getErrorMessage } from '../lib/axios_utils'; const VERSION = '2'; const BASE_URL = `rest/api/${VERSION}`; diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts new file mode 100644 index 0000000000000..4a52ae60bcdda --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts @@ -0,0 +1,105 @@ +/* + * 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 axios from 'axios'; +import { addTimeZoneToDate, throwIfNotAlive, request, patch, getErrorMessage } from './axios_utils'; +jest.mock('axios'); +const axiosMock = (axios as unknown) as jest.Mock; + +describe('addTimeZoneToDate', () => { + test('adds timezone with default', () => { + const date = addTimeZoneToDate('2020-04-14T15:01:55.456Z'); + expect(date).toBe('2020-04-14T15:01:55.456Z GMT'); + }); + + test('adds timezone correctly', () => { + const date = addTimeZoneToDate('2020-04-14T15:01:55.456Z', 'PST'); + expect(date).toBe('2020-04-14T15:01:55.456Z PST'); + }); +}); + +describe('throwIfNotAlive ', () => { + test('throws correctly when status is invalid', async () => { + expect(() => { + throwIfNotAlive(404, 'application/json'); + }).toThrow('Instance is not alive.'); + }); + + test('throws correctly when content is invalid', () => { + expect(() => { + throwIfNotAlive(200, 'application/html'); + }).toThrow('Instance is not alive.'); + }); + + test('do NOT throws with custom validStatusCodes', async () => { + expect(() => { + throwIfNotAlive(404, 'application/json', [404]); + }).not.toThrow('Instance is not alive.'); + }); +}); + +describe('request', () => { + beforeEach(() => { + axiosMock.mockImplementation(() => ({ + status: 200, + headers: { 'content-type': 'application/json' }, + data: { incidentId: '123' }, + })); + }); + + test('it fetch correctly with defaults', async () => { + const res = await request({ axios, url: '/test' }); + + expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'get', data: {} }); + expect(res).toEqual({ + status: 200, + headers: { 'content-type': 'application/json' }, + data: { incidentId: '123' }, + }); + }); + + test('it fetch correctly', async () => { + const res = await request({ axios, url: '/test', method: 'post', data: { id: '123' } }); + + expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'post', data: { id: '123' } }); + expect(res).toEqual({ + status: 200, + headers: { 'content-type': 'application/json' }, + data: { incidentId: '123' }, + }); + }); + + test('it throws correctly', async () => { + axiosMock.mockImplementation(() => ({ + status: 404, + headers: { 'content-type': 'application/json' }, + data: { incidentId: '123' }, + })); + + await expect(request({ axios, url: '/test' })).rejects.toThrow(); + }); +}); + +describe('patch', () => { + beforeEach(() => { + axiosMock.mockImplementation(() => ({ + status: 200, + headers: { 'content-type': 'application/json' }, + })); + }); + + test('it fetch correctly', async () => { + await patch({ axios, url: '/test', data: { id: '123' } }); + expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'patch', data: { id: '123' } }); + }); +}); + +describe('getErrorMessage', () => { + test('it returns the correct error message', () => { + const msg = getErrorMessage('My connector name', 'An error has occurred'); + expect(msg).toBe('[Action][My connector name]: An error has occurred'); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts new file mode 100644 index 0000000000000..d527cf632bace --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.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 { AxiosInstance, Method, AxiosResponse } from 'axios'; + +export const throwIfNotAlive = ( + status: number, + contentType: string, + validStatusCodes: number[] = [200, 201, 204] +) => { + if (!validStatusCodes.includes(status) || !contentType.includes('application/json')) { + throw new Error('Instance is not alive.'); + } +}; + +export const request = async ({ + axios, + url, + method = 'get', + data, + params, +}: { + axios: AxiosInstance; + url: string; + method?: Method; + data?: T; + params?: unknown; +}): Promise => { + const res = await axios(url, { method, data: data ?? {}, params }); + throwIfNotAlive(res.status, res.headers['content-type']); + return res; +}; + +export const patch = async ({ + axios, + url, + data, +}: { + axios: AxiosInstance; + url: string; + data: T; +}): Promise => { + return request({ + axios, + url, + method: 'patch', + data, + }); +}; + +export const addTimeZoneToDate = (date: string, timezone = 'GMT'): string => { + return `${date} ${timezone}`; +}; + +export const getErrorMessage = (connector: string, msg: string) => { + return `[Action][${connector}]: ${msg}`; +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts index 86a8318841271..7daf14e99f254 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts @@ -4,9 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { api } from '../case/api'; +import { Logger } from '../../../../../../src/core/server'; import { externalServiceMock, mapping, apiParams } from './mocks'; -import { ExternalService } from '../case/types'; +import { ExternalService } from './types'; +import { api } from './api'; +let mockedLogger: jest.Mocked; describe('api', () => { let externalService: jest.Mocked; @@ -24,7 +26,13 @@ describe('api', () => { describe('create incident', () => { test('it creates an incident', async () => { const params = { ...apiParams, externalId: null }; - const res = await api.pushToService({ externalService, mapping, params }); + const res = await api.pushToService({ + externalService, + mapping, + params, + secrets: {}, + logger: mockedLogger, + }); expect(res).toEqual({ id: 'incident-1', @@ -46,7 +54,13 @@ describe('api', () => { test('it creates an incident without comments', async () => { const params = { ...apiParams, externalId: null, comments: [] }; - const res = await api.pushToService({ externalService, mapping, params }); + const res = await api.pushToService({ + externalService, + mapping, + params, + secrets: {}, + logger: mockedLogger, + }); expect(res).toEqual({ id: 'incident-1', @@ -57,8 +71,14 @@ describe('api', () => { }); test('it calls createIncident correctly', async () => { - const params = { ...apiParams, externalId: null }; - await api.pushToService({ externalService, mapping, params }); + const params = { ...apiParams, externalId: null, comments: undefined }; + await api.pushToService({ + externalService, + mapping, + params, + secrets: {}, + logger: mockedLogger, + }); expect(externalService.createIncident).toHaveBeenCalledWith({ incident: { @@ -71,53 +91,49 @@ describe('api', () => { expect(externalService.updateIncident).not.toHaveBeenCalled(); }); - test('it calls createComment correctly', async () => { + test('it calls updateIncident correctly', async () => { const params = { ...apiParams, externalId: null }; - await api.pushToService({ externalService, mapping, params }); - expect(externalService.createComment).toHaveBeenCalledTimes(2); - expect(externalService.createComment).toHaveBeenNthCalledWith(1, { - incidentId: 'incident-1', - comment: { - commentId: 'case-comment-1', - comment: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, + await api.pushToService({ + externalService, + mapping, + params, + secrets: {}, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledTimes(2); + expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, { + incident: { + comments: 'A comment', + description: + 'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: + 'Incident title (created at 2020-03-13T08:34:53.450Z by Elastic User)', }, - field: 'comments', + incidentId: 'incident-1', }); - expect(externalService.createComment).toHaveBeenNthCalledWith(2, { - incidentId: 'incident-1', - comment: { - commentId: 'case-comment-2', - comment: 'Another comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, + expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, { + incident: { + comments: 'Another comment', + description: + 'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: + 'Incident title (created at 2020-03-13T08:34:53.450Z by Elastic User)', }, - field: 'comments', + incidentId: 'incident-1', }); }); }); describe('update incident', () => { test('it updates an incident', async () => { - const res = await api.pushToService({ externalService, mapping, params: apiParams }); + const res = await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); expect(res).toEqual({ id: 'incident-2', @@ -139,7 +155,13 @@ describe('api', () => { test('it updates an incident without comments', async () => { const params = { ...apiParams, comments: [] }; - const res = await api.pushToService({ externalService, mapping, params }); + const res = await api.pushToService({ + externalService, + mapping, + params, + secrets: {}, + logger: mockedLogger, + }); expect(res).toEqual({ id: 'incident-2', @@ -151,7 +173,13 @@ describe('api', () => { test('it calls updateIncident correctly', async () => { const params = { ...apiParams }; - await api.pushToService({ externalService, mapping, params }); + await api.pushToService({ + externalService, + mapping, + params, + secrets: {}, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', @@ -165,46 +193,35 @@ describe('api', () => { expect(externalService.createIncident).not.toHaveBeenCalled(); }); - test('it calls createComment correctly', async () => { + test('it calls updateIncident to create a comments correctly', async () => { const params = { ...apiParams }; - await api.pushToService({ externalService, mapping, params }); - expect(externalService.createComment).toHaveBeenCalledTimes(2); - expect(externalService.createComment).toHaveBeenNthCalledWith(1, { - incidentId: 'incident-2', - comment: { - commentId: 'case-comment-1', - comment: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, + await api.pushToService({ + externalService, + mapping, + params, + secrets: {}, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledTimes(3); + expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, { + incident: { + description: + 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: + 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', }, - field: 'comments', + incidentId: 'incident-3', }); - expect(externalService.createComment).toHaveBeenNthCalledWith(2, { - incidentId: 'incident-2', - comment: { - commentId: 'case-comment-2', - comment: 'Another comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, + expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, { + incident: { + comments: 'A comment', + description: + 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: + 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', }, - field: 'comments', + incidentId: 'incident-2', }); }); }); @@ -231,7 +248,13 @@ describe('api', () => { actionType: 'overwrite', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -264,7 +287,13 @@ describe('api', () => { actionType: 'nothing', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -295,7 +324,13 @@ describe('api', () => { actionType: 'append', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -328,7 +363,13 @@ describe('api', () => { actionType: 'nothing', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: {}, @@ -356,7 +397,13 @@ describe('api', () => { actionType: 'overwrite', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -387,7 +434,13 @@ describe('api', () => { actionType: 'overwrite', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -420,7 +473,13 @@ describe('api', () => { actionType: 'nothing', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -451,7 +510,13 @@ describe('api', () => { actionType: 'append', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -484,7 +549,13 @@ describe('api', () => { actionType: 'append', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -515,8 +586,14 @@ describe('api', () => { actionType: 'overwrite', }); - await api.pushToService({ externalService, mapping, params: apiParams }); - expect(externalService.createComment).not.toHaveBeenCalled(); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + secrets: {}, + logger: mockedLogger, + }); + expect(externalService.updateIncident).toHaveBeenCalledTimes(1); }); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts index 3db66e5884af4..bd6f88f5efaa9 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts @@ -3,5 +3,145 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { flow } from 'lodash'; +import { + ExternalServiceParams, + PushToServiceApiHandlerArgs, + HandshakeApiHandlerArgs, + GetIncidentApiHandlerArgs, + ExternalServiceApi, +} from './types'; -export { api } from '../case/api'; +// TODO: to remove, need to support Case +import { transformers } from '../case/transformers'; +import { PushToServiceResponse, TransformFieldsArgs } from './case_types'; +import { prepareFieldsForTransformation } from '../case/utils'; + +const handshakeHandler = async ({ + externalService, + mapping, + params, +}: HandshakeApiHandlerArgs) => {}; +const getIncidentHandler = async ({ + externalService, + mapping, + params, +}: GetIncidentApiHandlerArgs) => {}; + +const pushToServiceHandler = async ({ + externalService, + mapping, + params, + secrets, + logger, +}: PushToServiceApiHandlerArgs): Promise => { + const { externalId, comments } = params; + const updateIncident = externalId ? true : false; + const defaultPipes = updateIncident ? ['informationUpdated'] : ['informationCreated']; + let currentIncident: ExternalServiceParams | undefined; + let res: PushToServiceResponse; + + if (externalId) { + try { + currentIncident = await externalService.getIncident(externalId); + } catch (ex) { + logger.debug( + `Retrieving Incident by id ${externalId} from ServiceNow was failed with exception: ${ex}` + ); + } + } + + let incident = {}; + // TODO: should be removed later but currently keep it for the Case implementation support + if (mapping) { + const fields = prepareFieldsForTransformation({ + externalCase: params.externalObject, + mapping, + defaultPipes, + }); + + incident = transformFields({ + params, + fields, + currentIncident, + }); + } else { + incident = { ...params, short_description: params.title, comments: params.comment }; + } + + if (updateIncident) { + res = await externalService.updateIncident({ + incidentId: externalId, + incident, + }); + } else { + res = await externalService.createIncident({ + incident: { + ...incident, + caller_id: secrets.username, + }, + }); + } + + // TODO: should temporary keep comments for a Case usage + if ( + comments && + Array.isArray(comments) && + comments.length > 0 && + mapping && + mapping.get('comments')?.actionType !== 'nothing' + ) { + res.comments = []; + + const fieldsKey = mapping.get('comments')?.target ?? 'comments'; + for (const currentComment of comments) { + await externalService.updateIncident({ + incidentId: res.id, + incident: { + ...incident, + [fieldsKey]: currentComment.comment, + }, + }); + res.comments = [ + ...(res.comments ?? []), + { + commentId: currentComment.commentId, + pushedDate: res.pushedDate, + }, + ]; + } + } + return res; +}; + +export const transformFields = ({ + params, + fields, + currentIncident, +}: TransformFieldsArgs): Record => { + return fields.reduce((prev, cur) => { + const transform = flow(...cur.pipes.map((p) => transformers[p])); + return { + ...prev, + [cur.key]: transform({ + value: cur.value, + date: params.updatedAt ?? params.createdAt, + user: + (params.updatedBy != null + ? params.updatedBy.fullName + ? params.updatedBy.fullName + : params.updatedBy.username + : params.createdBy.fullName + ? params.createdBy.fullName + : params.createdBy.username) ?? '', + previousValue: currentIncident ? currentIncident[cur.key] : '', + }).value, + }; + }, {}); +}; + +export const api: ExternalServiceApi = { + handshake: handshakeHandler, + pushToService: pushToServiceHandler, + getIncident: getIncidentHandler, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_shema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_shema.ts new file mode 100644 index 0000000000000..2df8c8156cde8 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_shema.ts @@ -0,0 +1,36 @@ +/* + * 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'; + +export const MappingActionType = schema.oneOf([ + schema.literal('nothing'), + schema.literal('overwrite'), + schema.literal('append'), +]); + +export const MapRecordSchema = schema.object({ + source: schema.string(), + target: schema.string(), + actionType: MappingActionType, +}); + +export const IncidentConfigurationSchema = schema.object({ + mapping: schema.arrayOf(MapRecordSchema), +}); + +export const EntityInformation = { + createdAt: schema.maybe(schema.string()), + createdBy: schema.maybe(schema.any()), + updatedAt: schema.nullable(schema.string()), + updatedBy: schema.nullable(schema.any()), +}; + +export const CommentSchema = schema.object({ + commentId: schema.string(), + comment: schema.string(), + ...EntityInformation, +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_types.ts new file mode 100644 index 0000000000000..7e659125af7b2 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_types.ts @@ -0,0 +1,64 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { TypeOf } from '@kbn/config-schema'; +import { + ExecutorSubActionGetIncidentParamsSchema, + ExecutorSubActionHandshakeParamsSchema, +} from './schema'; +import { IncidentConfigurationSchema, MapRecordSchema } from './case_shema'; +import { + PushToServiceApiParams, + ExternalServiceIncidentResponse, + ExternalServiceParams, +} from './types'; + +export interface CreateCommentRequest { + [key: string]: string; +} + +export type IncidentConfiguration = TypeOf; +export type MapRecord = TypeOf; + +export interface ExternalServiceCommentResponse { + commentId: string; + pushedDate: string; + externalCommentId?: string; +} + +export type ExecutorSubActionGetIncidentParams = TypeOf< + typeof ExecutorSubActionGetIncidentParamsSchema +>; + +export type ExecutorSubActionHandshakeParams = TypeOf< + typeof ExecutorSubActionHandshakeParamsSchema +>; + +export interface PushToServiceResponse extends ExternalServiceIncidentResponse { + comments?: ExternalServiceCommentResponse[]; +} + +export interface PipedField { + key: string; + value: string; + actionType: string; + pipes: string[]; +} + +export interface TransformFieldsArgs { + params: PushToServiceApiParams; + fields: PipedField[]; + currentIncident?: ExternalServiceParams; +} + +export interface TransformerArgs { + value: string; + date?: string; + user?: string; + previousValue?: string; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index dbb536d2fa53d..e62ca465f30f8 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -4,24 +4,99 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createConnector } from '../case/utils'; +import { curry } from 'lodash'; +import { schema } from '@kbn/config-schema'; -import { api } from './api'; -import { config } from './config'; import { validate } from './validators'; -import { createExternalService } from './service'; import { ExternalIncidentServiceConfiguration, ExternalIncidentServiceSecretConfiguration, -} from '../case/schema'; - -export const getActionType = createConnector({ - api, - config, - validate, - createExternalService, - validationSchema: { - config: ExternalIncidentServiceConfiguration, - secrets: ExternalIncidentServiceSecretConfiguration, - }, -}); + ExecutorParamsSchema, +} from './schema'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types'; +import { createExternalService } from './service'; +import { api } from './api'; +import { ExecutorParams, ExecutorSubActionPushParams } from './types'; +import * as i18n from './translations'; +import { Logger } from '../../../../../../src/core/server'; + +// TODO: to remove, need to support Case +import { buildMap, mapParams } from '../case/utils'; +import { PushToServiceResponse } from './case_types'; + +interface GetActionTypeParams { + logger: Logger; + configurationUtilities: ActionsConfigurationUtilities; +} + +// action type definition +export function getActionType(params: GetActionTypeParams): ActionType { + const { logger, configurationUtilities } = params; + return { + id: '.servicenow', + minimumLicenseRequired: 'platinum', + name: i18n.NAME, + validate: { + config: schema.object(ExternalIncidentServiceConfiguration, { + validate: curry(validate.config)(configurationUtilities), + }), + secrets: schema.object(ExternalIncidentServiceSecretConfiguration, { + validate: curry(validate.secrets)(configurationUtilities), + }), + params: ExecutorParamsSchema, + }, + executor: curry(executor)({ logger }), + }; +} + +// action executor + +async function executor( + { logger }: { logger: Logger }, + execOptions: ActionTypeExecutorOptions +): Promise { + const { actionId, config, params, secrets } = execOptions; + const { subAction, subActionParams } = params as ExecutorParams; + let data: PushToServiceResponse | null = null; + + const externalService = createExternalService({ + config, + secrets, + }); + + if (!api[subAction]) { + const errorMessage = `[Action][ExternalService] Unsupported subAction type ${subAction}.`; + logger.error(errorMessage); + throw new Error(errorMessage); + } + + if (subAction !== 'pushToService') { + const errorMessage = `[Action][ExternalService] subAction ${subAction} not implemented.`; + logger.error(errorMessage); + throw new Error(errorMessage); + } + + if (subAction === 'pushToService') { + const pushToServiceParams = subActionParams as ExecutorSubActionPushParams; + + const { comments, externalId, ...restParams } = pushToServiceParams; + const mapping = config.incidentConfiguration + ? buildMap(config.incidentConfiguration.mapping) + : null; + const externalObject = + config.incidentConfiguration && mapping ? mapParams(restParams, mapping) : {}; + + data = await api.pushToService({ + externalService, + mapping, + params: { ...pushToServiceParams, externalObject }, + secrets, + logger, + }); + + logger.debug(`response push to service for incident id: ${data.id}`); + } + + return { status: 'ok', data: data ?? {}, actionId }; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts index 37228380910b3..5f22fcd4fdc85 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts @@ -4,12 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - ExternalService, - PushToServiceApiParams, - ExecutorSubActionPushParams, - MapRecord, -} from '../case/types'; +import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types'; +import { MapRecord } from './case_types'; const createMock = (): jest.Mocked => { const service = { @@ -35,22 +31,9 @@ const createMock = (): jest.Mocked => { url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', }) ), - createComment: jest.fn(), + findIncidents: jest.fn(), }; - service.createComment.mockImplementationOnce(() => - Promise.resolve({ - commentId: 'case-comment-1', - pushedDate: '2020-03-10T12:24:20.000Z', - }) - ); - - service.createComment.mockImplementationOnce(() => - Promise.resolve({ - commentId: 'case-comment-2', - pushedDate: '2020-03-10T12:24:20.000Z', - }) - ); return service; }; @@ -81,7 +64,7 @@ mapping.set('short_description', { }); const executorParams: ExecutorSubActionPushParams = { - caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', + savedObjectId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', externalId: 'incident-3', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, @@ -89,6 +72,10 @@ const executorParams: ExecutorSubActionPushParams = { updatedBy: { fullName: 'Elastic User', username: 'elastic' }, title: 'Incident title', description: 'Incident description', + comment: 'test-alert comment', + severity: '1', + urgency: '2', + impact: '1', comments: [ { commentId: 'case-comment-1', @@ -111,7 +98,7 @@ const executorParams: ExecutorSubActionPushParams = { const apiParams: PushToServiceApiParams = { ...executorParams, - externalCase: { short_description: 'Incident title', description: 'Incident description' }, + externalObject: { short_description: 'Incident title', description: 'Incident description' }, }; export { externalServiceMock, mapping, executorParams, apiParams }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts new file mode 100644 index 0000000000000..82afebaaee445 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.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 { CommentSchema, EntityInformation, IncidentConfigurationSchema } from './case_shema'; + +export const ExternalIncidentServiceConfiguration = { + apiUrl: schema.string(), + // TODO: to remove - set it optional for the current stage to support Case ServiceNow implementation + incidentConfiguration: schema.nullable(IncidentConfigurationSchema), + isCaseOwned: schema.maybe(schema.boolean()), +}; + +export const ExternalIncidentServiceConfigurationSchema = schema.object( + ExternalIncidentServiceConfiguration +); + +export const ExternalIncidentServiceSecretConfiguration = { + password: schema.string(), + username: schema.string(), +}; + +export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( + ExternalIncidentServiceSecretConfiguration +); + +export const ExecutorSubActionSchema = schema.oneOf([ + schema.literal('getIncident'), + schema.literal('pushToService'), + schema.literal('handshake'), +]); + +export const ExecutorSubActionPushParamsSchema = schema.object({ + savedObjectId: schema.string(), + title: schema.string(), + description: schema.nullable(schema.string()), + comment: schema.nullable(schema.string()), + externalId: schema.nullable(schema.string()), + severity: schema.nullable(schema.string()), + urgency: schema.nullable(schema.string()), + impact: schema.nullable(schema.string()), + // TODO: remove later - need for support Case push multiple comments + comments: schema.maybe(schema.arrayOf(CommentSchema)), + ...EntityInformation, +}); + +export const ExecutorSubActionGetIncidentParamsSchema = schema.object({ + externalId: schema.string(), +}); + +// Reserved for future implementation +export const ExecutorSubActionHandshakeParamsSchema = schema.object({}); + +export const ExecutorParamsSchema = schema.oneOf([ + schema.object({ + subAction: schema.literal('getIncident'), + subActionParams: ExecutorSubActionGetIncidentParamsSchema, + }), + schema.object({ + subAction: schema.literal('handshake'), + subActionParams: ExecutorSubActionHandshakeParamsSchema, + }), + schema.object({ + subAction: schema.literal('pushToService'), + subActionParams: ExecutorSubActionPushParamsSchema, + }), +]); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts index f65cd5430560e..07d60ec9f7a05 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts @@ -7,12 +7,12 @@ import axios from 'axios'; import { createExternalService } from './service'; -import * as utils from '../case/utils'; -import { ExternalService } from '../case/types'; +import * as utils from '../lib/axios_utils'; +import { ExternalService } from './types'; jest.mock('axios'); -jest.mock('../case/utils', () => { - const originalUtils = jest.requireActual('../case/utils'); +jest.mock('../lib/axios_utils', () => { + const originalUtils = jest.requireActual('../lib/axios_utils'); return { ...originalUtils, request: jest.fn(), @@ -198,58 +198,22 @@ describe('ServiceNow service', () => { '[Action][ServiceNow]: Unable to update incident with id 1. Error: An error has occurred' ); }); - }); - - describe('createComment', () => { test('it creates the comment correctly', async () => { patchMock.mockImplementation(() => ({ - data: { result: { sys_id: '1', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } }, + data: { result: { sys_id: '11', number: 'INC011', sys_updated_on: '2020-03-10 12:24:20' } }, })); - const res = await service.createComment({ + const res = await service.updateIncident({ incidentId: '1', - comment: { comment: 'comment', commentId: 'comment-1' }, - field: 'comments', + comment: 'comment-1', }); expect(res).toEqual({ - commentId: 'comment-1', + title: 'INC011', + id: '11', pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://dev102283.service-now.com/nav_to.do?uri=incident.do?sys_id=11', }); }); - - test('it should call request with correct arguments', async () => { - patchMock.mockImplementation(() => ({ - data: { result: { sys_id: '1', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } }, - })); - - await service.createComment({ - incidentId: '1', - comment: { comment: 'comment', commentId: 'comment-1' }, - field: 'my_field', - }); - - expect(patchMock).toHaveBeenCalledWith({ - axios, - url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1', - data: { my_field: 'comment' }, - }); - }); - - test('it should throw an error', async () => { - patchMock.mockImplementation(() => { - throw new Error('An error has occurred'); - }); - - expect( - service.createComment({ - incidentId: '1', - comment: { comment: 'comment', commentId: 'comment-1' }, - field: 'comments', - }) - ).rejects.toThrow( - '[Action][ServiceNow]: Unable to create comment at incident with id 1. Error: An error has occurred' - ); - }); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts index 541fefce2f2ff..2b5204af2eb7d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts @@ -6,21 +6,14 @@ import axios from 'axios'; -import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types'; -import { addTimeZoneToDate, patch, request, getErrorMessage } from '../case/utils'; +import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from './types'; import * as i18n from './translations'; -import { - ServiceNowPublicConfigurationType, - ServiceNowSecretConfigurationType, - CreateIncidentRequest, - UpdateIncidentRequest, - CreateCommentRequest, -} from './types'; +import { ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType } from './types'; +import { request, getErrorMessage, addTimeZoneToDate, patch } from '../lib/axios_utils'; const API_VERSION = 'v2'; const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; -const COMMENT_URL = `api/now/${API_VERSION}/table/incident`; // Based on: https://docs.servicenow.com/bundle/orlando-platform-user-interface/page/use/navigation/reference/r_NavigatingByURLExamples.html const VIEW_INCIDENT_URL = `nav_to.do?uri=incident.do?sys_id=`; @@ -37,7 +30,6 @@ export const createExternalService = ({ } const incidentUrl = `${url}/${INCIDENT_URL}`; - const commentUrl = `${url}/${COMMENT_URL}`; const axiosInstance = axios.create({ auth: { username, password }, }); @@ -61,13 +53,29 @@ export const createExternalService = ({ } }; + const findIncidents = async (params?: Record) => { + try { + const res = await request({ + axios: axiosInstance, + url: incidentUrl, + params, + }); + + return res.data.result.length > 0 ? { ...res.data.result } : undefined; + } catch (error) { + throw new Error( + getErrorMessage(i18n.NAME, `Unable to find incidents by query. Error: ${error.message}`) + ); + } + }; + const createIncident = async ({ incident }: ExternalServiceParams) => { try { - const res = await request({ + const res = await request({ axios: axiosInstance, url: `${incidentUrl}`, method: 'post', - data: { ...incident }, + data: { ...(incident as Record) }, }); return { @@ -85,10 +93,10 @@ export const createExternalService = ({ const updateIncident = async ({ incidentId, incident }: ExternalServiceParams) => { try { - const res = await patch({ + const res = await patch({ axios: axiosInstance, url: `${incidentUrl}/${incidentId}`, - data: { ...incident }, + data: { ...(incident as Record) }, }); return { @@ -107,32 +115,10 @@ export const createExternalService = ({ } }; - const createComment = async ({ incidentId, comment, field }: ExternalServiceParams) => { - try { - const res = await patch({ - axios: axiosInstance, - url: `${commentUrl}/${incidentId}`, - data: { [field]: comment.comment }, - }); - - return { - commentId: comment.commentId, - pushedDate: new Date(addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), - }; - } catch (error) { - throw new Error( - getErrorMessage( - i18n.NAME, - `Unable to create comment at incident with id ${incidentId}. Error: ${error.message}` - ) - ); - } - }; - return { getIncident, createIncident, updateIncident, - createComment, + findIncidents, }; }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts index 3d6138169c4cc..05c7d805a1852 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts @@ -6,6 +6,22 @@ import { i18n } from '@kbn/i18n'; -export const NAME = i18n.translate('xpack.actions.builtin.case.servicenowTitle', { +export const NAME = i18n.translate('xpack.actions.builtin.servicenowTitle', { defaultMessage: 'ServiceNow', }); + +export const WHITE_LISTED_ERROR = (message: string) => + i18n.translate('xpack.actions.builtin.configuration.apiWhitelistError', { + defaultMessage: 'error configuring connector action: {message}', + values: { + message, + }, + }); + +// TODO: remove when Case mappings will be removed +export const MAPPING_EMPTY = i18n.translate( + 'xpack.actions.builtin.servicenow.configuration.emptyMapping', + { + defaultMessage: '[incidentConfiguration.mapping]: expected non-empty but got empty', + } +); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index d8476b7dca54a..0db9b6642ea5c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -4,18 +4,97 @@ * you may not use this file except in compliance with the Elastic License. */ -export { - ExternalIncidentServiceConfiguration as ServiceNowPublicConfigurationType, - ExternalIncidentServiceSecretConfiguration as ServiceNowSecretConfigurationType, -} from '../case/types'; +/* eslint-disable @typescript-eslint/no-explicit-any */ -export interface CreateIncidentRequest { - summary: string; - description: string; -} +import { TypeOf } from '@kbn/config-schema'; +import { + ExternalIncidentServiceConfigurationSchema, + ExternalIncidentServiceSecretConfigurationSchema, + ExecutorParamsSchema, + ExecutorSubActionPushParamsSchema, + ExecutorSubActionGetIncidentParamsSchema, + ExecutorSubActionHandshakeParamsSchema, +} from './schema'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { IncidentConfigurationSchema } from './case_shema'; +import { PushToServiceResponse } from './case_types'; +import { Logger } from '../../../../../../src/core/server'; -export type UpdateIncidentRequest = Partial; +export type ServiceNowPublicConfigurationType = TypeOf< + typeof ExternalIncidentServiceConfigurationSchema +>; +export type ServiceNowSecretConfigurationType = TypeOf< + typeof ExternalIncidentServiceSecretConfigurationSchema +>; export interface CreateCommentRequest { [key: string]: string; } + +export type ExecutorParams = TypeOf; +export type ExecutorSubActionPushParams = TypeOf; + +export type IncidentConfiguration = TypeOf; + +export interface ExternalServiceCredentials { + config: Record; + secrets: Record; +} + +export interface ExternalServiceValidation { + config: (configurationUtilities: ActionsConfigurationUtilities, configObject: any) => void; + secrets: (configurationUtilities: ActionsConfigurationUtilities, secrets: any) => void; +} + +export interface ExternalServiceIncidentResponse { + id: string; + title: string; + url: string; + pushedDate: string; +} + +export type ExternalServiceParams = Record; + +export interface ExternalService { + getIncident: (id: string) => Promise; + createIncident: (params: ExternalServiceParams) => Promise; + updateIncident: (params: ExternalServiceParams) => Promise; + findIncidents: (params?: Record) => Promise; +} + +export interface PushToServiceApiParams extends ExecutorSubActionPushParams { + externalObject: Record; +} + +export interface ExternalServiceApiHandlerArgs { + externalService: ExternalService; + mapping: Map | null; +} + +export type ExecutorSubActionGetIncidentParams = TypeOf< + typeof ExecutorSubActionGetIncidentParamsSchema +>; + +export type ExecutorSubActionHandshakeParams = TypeOf< + typeof ExecutorSubActionHandshakeParamsSchema +>; + +export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: PushToServiceApiParams; + secrets: Record; + logger: Logger; +} + +export interface GetIncidentApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: ExecutorSubActionGetIncidentParams; +} + +export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: ExecutorSubActionHandshakeParams; +} + +export interface ExternalServiceApi { + handshake: (args: HandshakeApiHandlerArgs) => Promise; + pushToService: (args: PushToServiceApiHandlerArgs) => Promise; + getIncident: (args: GetIncidentApiHandlerArgs) => Promise; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts index 7226071392bc6..65bbe9aea8119 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts @@ -4,8 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import { validateCommonConfig, validateCommonSecrets } from '../case/validators'; -import { ExternalServiceValidation } from '../case/types'; +import { isEmpty } from 'lodash'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, + ExternalServiceValidation, +} from './types'; + +import * as i18n from './translations'; + +export const validateCommonConfig = ( + configurationUtilities: ActionsConfigurationUtilities, + configObject: ServiceNowPublicConfigurationType +) => { + if ( + configObject.incidentConfiguration !== null && + isEmpty(configObject.incidentConfiguration.mapping) + ) { + return i18n.MAPPING_EMPTY; + } + + try { + configurationUtilities.ensureWhitelistedUri(configObject.apiUrl); + } catch (whitelistError) { + return i18n.WHITE_LISTED_ERROR(whitelistError.message); + } +}; + +export const validateCommonSecrets = ( + configurationUtilities: ActionsConfigurationUtilities, + secrets: ServiceNowSecretConfigurationType +) => {}; export const validate: ExternalServiceValidation = { config: validateCommonConfig, diff --git a/x-pack/plugins/case/common/api/cases/case.ts b/x-pack/plugins/case/common/api/cases/case.ts index 283196373fe9f..67b296d2ba197 100644 --- a/x-pack/plugins/case/common/api/cases/case.ts +++ b/x-pack/plugins/case/common/api/cases/case.ts @@ -130,7 +130,7 @@ export const ServiceConnectorCommentParamsRt = rt.type({ }); export const ServiceConnectorCaseParamsRt = rt.type({ - caseId: rt.string, + savedObjectId: rt.string, createdAt: rt.string, createdBy: ServiceConnectorUserParams, externalId: rt.union([rt.string, rt.null]), diff --git a/x-pack/plugins/case/common/constants.ts b/x-pack/plugins/case/common/constants.ts index 819d4110e168d..e912c661439b2 100644 --- a/x-pack/plugins/case/common/constants.ts +++ b/x-pack/plugins/case/common/constants.ts @@ -27,5 +27,6 @@ export const CASE_USER_ACTIONS_URL = `${CASE_DETAILS_URL}/user_actions`; export const ACTION_URL = '/api/actions'; export const ACTION_TYPES_URL = '/api/actions/list_action_types'; +export const SERVICENOW_ACTION_TYPE_ID = '.servicenow'; export const SUPPORTED_CONNECTORS = ['.servicenow', '.jira']; diff --git a/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts index 4aa6725159043..b02f53bcd174a 100644 --- a/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts +++ b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts @@ -31,7 +31,7 @@ export const getActions = (): FindActionResult[] => [ actionTypeId: '.servicenow', name: 'ServiceNow', config: { - casesConfiguration: { + incidentConfiguration: { mapping: [ { source: 'title', @@ -51,6 +51,7 @@ export const getActions = (): FindActionResult[] => [ ], }, apiUrl: 'https://dev102283.service-now.com', + isCaseOwned: true, }, isPreconfigured: false, referencedByCount: 0, diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts index d86e1777e920d..28e75dd2f8c32 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts @@ -11,6 +11,7 @@ import { wrapError } from '../../utils'; import { CASE_CONFIGURE_CONNECTORS_URL, SUPPORTED_CONNECTORS, + SERVICENOW_ACTION_TYPE_ID, } from '../../../../../common/constants'; /* @@ -31,8 +32,12 @@ export function initCaseConfigureGetActionConnector({ caseService, router }: Rou throw Boom.notFound('Action client have not been found'); } - const results = (await actionsClient.getAll()).filter((action) => - SUPPORTED_CONNECTORS.includes(action.actionTypeId) + const results = (await actionsClient.getAll()).filter( + (action) => + SUPPORTED_CONNECTORS.includes(action.actionTypeId) && + // Need this filtering temporary to display only Case owned ServiceNow connectors + (action.actionTypeId !== SERVICENOW_ACTION_TYPE_ID || + (action.actionTypeId === SERVICENOW_ACTION_TYPE_ID && action.config!.isCaseOwned)) ); return response.ok({ body: results }); } catch (error) { diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx index f070431a34f21..91a5aa5c88beb 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx @@ -125,7 +125,7 @@ describe('ConfigureCases', () => { jest.resetAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping: connectors[0].config.casesConfiguration.mapping, + mapping: connectors[0].config.incidentConfiguration.mapping, closureType: 'close-by-user', connectorId: 'servicenow-1', connectorName: 'unchanged', @@ -213,7 +213,7 @@ describe('ConfigureCases', () => { jest.clearAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, + mapping: connectors[1].config.incidentConfiguration.mapping, closureType: 'close-by-user', connectorId: 'servicenow-2', connectorName: 'unchanged', @@ -332,7 +332,7 @@ describe('ConfigureCases', () => { jest.resetAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping: connectors[0].config.casesConfiguration.mapping, + mapping: connectors[0].config.incidentConfiguration.mapping, closureType: 'close-by-user', connectorId: 'servicenow-1', connectorName: 'My connector', @@ -399,7 +399,7 @@ describe('closure options', () => { jest.resetAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping: connectors[0].config.casesConfiguration.mapping, + mapping: connectors[0].config.incidentConfiguration.mapping, closureType: 'close-by-user', connectorId: 'servicenow-1', connectorName: 'My connector', @@ -435,7 +435,7 @@ describe('user interactions', () => { jest.resetAllMocks(); useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, + mapping: connectors[1].config.incidentConfiguration.mapping, closureType: 'close-by-user', connectorId: 'servicenow-2', connectorName: 'unchanged', diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx index 256c8893be941..43922462cd092 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx @@ -198,6 +198,7 @@ const ConfigureCasesComponent: React.FC = ({ userC capabilities: application.capabilities, reloadConnectors, docLinks, + consumer: 'case', }} > { updateCase, }; const sampleServiceRequestData = { - caseId: pushedCase.id, + savedObjectId: pushedCase.id, createdAt: pushedCase.createdAt, createdBy: serviceConnectorUser, comments: [ diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx index 0d8a4c04ca7cd..346390bd2a49f 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx @@ -171,7 +171,7 @@ export const formatServiceRequestData = ( const actualExternalService = caseServices[connectorId] ?? null; return { - caseId, + savedObjectId: caseId, createdAt, createdBy: { fullName: createdBy.fullName ?? null, diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts index d8b55665f7768..0b19e4177f5c2 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connector as serviceNowConnectorConfig } from './servicenow/config'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ServiceNowConnectorConfiguration } from '../../../../../triggers_actions_ui/public/common'; import { connector as jiraConnectorConfig } from './jira/config'; import { ConnectorConfiguration } from './types'; export const connectorsConfiguration: Record = { - '.servicenow': serviceNowConnectorConfig, + '.servicenow': ServiceNowConnectorConfiguration as ConnectorConfiguration, '.jira': jiraConnectorConfig, }; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts index 2ce61bef49c5e..83b07a2905ef0 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { getActionType as serviceNowActionType } from './servicenow'; export { getActionType as jiraActionType } from './jira'; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/flyout.tsx b/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/flyout.tsx deleted file mode 100644 index 1e5abbab46a06..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/flyout.tsx +++ /dev/null @@ -1,87 +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 from 'react'; -import { - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiFieldPassword, - EuiSpacer, -} from '@elastic/eui'; - -import * as i18n from './translations'; -import { ConnectorFlyoutFormProps } from '../types'; -import { ServiceNowActionConnector } from './types'; -import { withConnectorFlyout } from '../components/connector_flyout'; - -const ServiceNowConnectorForm: React.FC> = ({ - errors, - action, - onChangeSecret, - onBlurSecret, -}) => { - const { username, password } = action.secrets; - const isUsernameInvalid: boolean = errors.username.length > 0 && username != null; - const isPasswordInvalid: boolean = errors.password.length > 0 && password != null; - - return ( - <> - - - - onChangeSecret('username', evt.target.value)} - onBlur={() => onBlurSecret('username')} - /> - - - - - - - - onChangeSecret('password', evt.target.value)} - onBlur={() => onBlurSecret('password')} - /> - - - - - ); -}; - -export const ServiceNowConnectorFlyout = withConnectorFlyout({ - ConnectorFormComponent: ServiceNowConnectorForm, - secretKeys: ['username', 'password'], - connectorActionTypeId: '.servicenow', -}); - -// eslint-disable-next-line import/no-default-export -export { ServiceNowConnectorFlyout as default }; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/index.tsx b/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/index.tsx deleted file mode 100644 index c9c5298365e81..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/index.tsx +++ /dev/null @@ -1,47 +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 { lazy } from 'react'; -import { - ValidationResult, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../triggers_actions_ui/public/types'; -import { connector } from './config'; -import { createActionType } from '../utils'; -import logo from './logo.svg'; -import { ServiceNowActionConnector } from './types'; -import * as i18n from './translations'; - -interface Errors { - username: string[]; - password: string[]; -} - -const validateConnector = (action: ServiceNowActionConnector): ValidationResult => { - const errors: Errors = { - username: [], - password: [], - }; - - if (!action.secrets.username) { - errors.username = [...errors.username, i18n.USERNAME_REQUIRED]; - } - - if (!action.secrets.password) { - errors.password = [...errors.password, i18n.PASSWORD_REQUIRED]; - } - - return { errors }; -}; - -export const getActionType = createActionType({ - id: connector.id, - iconClass: logo, - selectMessage: i18n.SERVICENOW_DESC, - actionTypeTitle: connector.name, - validateConnector, - actionConnectorFields: lazy(() => import('./flyout')), -}); diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/translations.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/translations.ts deleted file mode 100644 index b3e58dcd5b6be..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/translations.ts +++ /dev/null @@ -1,30 +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 { i18n } from '@kbn/i18n'; - -export * from '../translations'; - -export const SERVICENOW_DESC = i18n.translate( - 'xpack.securitySolution.case.connectors.servicenow.selectMessageText', - { - defaultMessage: 'Push or update Security case data to a new incident in ServiceNow', - } -); - -export const SERVICENOW_TITLE = i18n.translate( - 'xpack.securitySolution.case.connectors.servicenow.actionTypeTitle', - { - defaultMessage: 'ServiceNow', - } -); - -export const MAPPING_FIELD_SHORT_DESC = i18n.translate( - 'xpack.securitySolution.case.configureCases.mappingFieldShortDescription', - { - defaultMessage: 'Short Description', - } -); diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/types.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/types.ts deleted file mode 100644 index b4a80e28c8d15..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/types.ts +++ /dev/null @@ -1,22 +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. - */ - -/* eslint-disable no-restricted-imports */ -/* eslint-disable @kbn/eslint/no-restricted-paths */ - -import { - ServiceNowPublicConfigurationType, - ServiceNowSecretConfigurationType, -} from '../../../../../../actions/server/builtin_action_types/servicenow/types'; - -export { ServiceNowFieldsType } from '../../../../../../case/common/api/connectors'; - -export * from '../types'; - -export interface ServiceNowActionConnector { - config: ServiceNowPublicConfigurationType; - secrets: ServiceNowSecretConfigurationType; -} diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 65121327b40b9..18072c25e6dde 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -22,7 +22,7 @@ import { Storage } from '../../../../src/plugins/kibana_utils/public'; import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; import { initTelemetry } from './common/lib/telemetry'; import { KibanaServices } from './common/lib/kibana/services'; -import { serviceNowActionType, jiraActionType } from './common/lib/connectors'; +import { jiraActionType } from './common/lib/connectors'; import { PluginSetup, PluginStart, @@ -74,7 +74,6 @@ export class Plugin implements IPlugin { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c12b1366746b0..d97e5ec2ced60 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3940,7 +3940,6 @@ "xpack.actions.builtin.case.configuration.emptyMapping": "[casesConfiguration.mapping]:空以外の値が必要ですが空でした", "xpack.actions.builtin.case.connectorApiNullError": "コネクター[apiUrl]が必要です", "xpack.actions.builtin.case.jiraTitle": "Jira", - "xpack.actions.builtin.case.servicenowTitle": "ServiceNow", "xpack.actions.builtin.email.errorSendingErrorMessage": "エラー送信メールアドレス", "xpack.actions.builtin.emailTitle": "メール", "xpack.actions.builtin.esIndex.errorIndexingErrorMessage": "エラーインデックス作成ドキュメント", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f68a245acbc31..9a3bd8f615a47 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3943,7 +3943,6 @@ "xpack.actions.builtin.case.configuration.emptyMapping": "[casesConfiguration.mapping]:应为非空,但却为空", "xpack.actions.builtin.case.connectorApiNullError": "需要指定连接器 [apiUrl]", "xpack.actions.builtin.case.jiraTitle": "Jira", - "xpack.actions.builtin.case.servicenowTitle": "ServiceNow", "xpack.actions.builtin.email.errorSendingErrorMessage": "发送电子邮件时出错", "xpack.actions.builtin.emailTitle": "电子邮件", "xpack.actions.builtin.esIndex.errorIndexingErrorMessage": "索引文档时出错", diff --git a/x-pack/plugins/triggers_actions_ui/README.md b/x-pack/plugins/triggers_actions_ui/README.md index 5a25f7b94050e..4b6e596b8d657 100644 --- a/x-pack/plugins/triggers_actions_ui/README.md +++ b/x-pack/plugins/triggers_actions_ui/README.md @@ -1295,6 +1295,7 @@ Then this dependencies will be used to embed Actions form or register your own a defaultActionMessage={'Alert [{{ctx.metadata.name}}] has exceeded the threshold'} actionTypes={ALOWED_BY_PLUGIN_ACTION_TYPES} toastNotifications={toastNotifications} + consumer={initialAlert.consumer} /> ); }; @@ -1317,6 +1318,7 @@ interface ActionAccordionFormProps { actionTypes?: ActionType[]; messageVariables?: string[]; defaultActionMessage?: string; + consumer: string; } ``` @@ -1334,6 +1336,7 @@ interface ActionAccordionFormProps { |actionTypes|Optional property, which allowes to define a list of available actions specific for a current plugin.| |actionTypes|Optional property, which allowes to define a list of variables for action 'message' property.| |defaultActionMessage|Optional property, which allowes to define a message value for action with 'message' property.| +|consumer|Name of the plugin that creates an action.| AlertsContextProvider value options: @@ -1425,7 +1428,7 @@ const connector = { toastNotifications: toastNotifications, actionTypeRegistry: triggers_actions_ui.actionTypeRegistry, capabilities: capabilities, - docLinks, + docLinks, }} > Promise; + consumer: string; } ``` @@ -1479,6 +1483,7 @@ export interface ActionsConnectorsContextValue { |capabilities|Property, which is defining action current user usage capabilities like canSave or canDelete.| |toastNotifications|Toast messages.| |reloadConnectors|Optional function, which will be executed if connector was saved sucsessfuly, like reload list of connecotrs.| +|consumer|Optional name of the plugin that creates an action.| ## Embed the Edit Connector flyout within any Kibana plugin diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts index 8f49fa46dd54e..c241997e99dd7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts @@ -12,6 +12,7 @@ import { getPagerDutyActionType } from './pagerduty'; import { getWebhookActionType } from './webhook'; import { TypeRegistry } from '../../type_registry'; import { ActionTypeModel } from '../../../types'; +import { getServiceNowActionType } from './servicenow'; export function registerBuiltInActionTypes({ actionTypeRegistry, @@ -24,4 +25,5 @@ export function registerBuiltInActionTypes({ actionTypeRegistry.register(getIndexActionType()); actionTypeRegistry.register(getPagerDutyActionType()); actionTypeRegistry.register(getWebhookActionType()); + actionTypeRegistry.register(getServiceNowActionType()); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping.tsx new file mode 100644 index 0000000000000..52b881a1eb75f --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping.tsx @@ -0,0 +1,141 @@ +/* + * 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, { useCallback, useMemo } from 'react'; +import { EuiFormRow, EuiFlexItem, EuiFlexGroup, EuiSuperSelectOption } from '@elastic/eui'; +import styled from 'styled-components'; + +import { FieldMappingRow } from './field_mapping_row'; +import * as i18n from './translations'; + +import { setActionTypeToMapping, setThirdPartyToMapping } from './utils'; +import { ThirdPartyField as ConnectorConfigurationThirdPartyField } from './types'; +import { CasesConfigurationMapping } from '../types'; +import { connectorConfiguration } from '../config'; +import { createDefaultMapping } from '../servicenow_connectors'; + +const FieldRowWrapper = styled.div` + margin-top: 8px; + font-size: 14px; +`; + +const actionTypeOptions: Array> = [ + { + value: 'nothing', + inputDisplay: <>{i18n.FIELD_MAPPING_EDIT_NOTHING}, + 'data-test-subj': 'edit-update-option-nothing', + }, + { + value: 'overwrite', + inputDisplay: <>{i18n.FIELD_MAPPING_EDIT_OVERWRITE}, + 'data-test-subj': 'edit-update-option-overwrite', + }, + { + value: 'append', + inputDisplay: <>{i18n.FIELD_MAPPING_EDIT_APPEND}, + 'data-test-subj': 'edit-update-option-append', + }, +]; + +const getThirdPartyOptions = ( + caseField: string, + thirdPartyFields: Record +): Array> => + (Object.keys(thirdPartyFields) as string[]).reduce>>( + (acc, key) => { + if (thirdPartyFields[key].validSourceFields.includes(caseField)) { + return [ + ...acc, + { + value: key, + inputDisplay: {thirdPartyFields[key].label}, + 'data-test-subj': `dropdown-mapping-${key}`, + }, + ]; + } + return acc; + }, + [ + { + value: 'not_mapped', + inputDisplay: i18n.MAPPING_FIELD_NOT_MAPPED, + 'data-test-subj': 'dropdown-mapping-not_mapped', + }, + ] + ); + +export interface FieldMappingProps { + disabled: boolean; + mapping: CasesConfigurationMapping[] | null; + connectorActionTypeId: string; + onChangeMapping: (newMapping: CasesConfigurationMapping[]) => void; +} + +const FieldMappingComponent: React.FC = ({ + disabled, + mapping, + onChangeMapping, + connectorActionTypeId, +}) => { + const onChangeActionType = useCallback( + (caseField: string, newActionType: string) => { + const myMapping = mapping ?? defaultMapping; + onChangeMapping(setActionTypeToMapping(caseField, newActionType, myMapping)); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [mapping] + ); + + const onChangeThirdParty = useCallback( + (caseField: string, newThirdPartyField: string) => { + const myMapping = mapping ?? defaultMapping; + onChangeMapping(setThirdPartyToMapping(caseField, newThirdPartyField, myMapping)); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [mapping] + ); + + const selectedConnector = connectorConfiguration ?? { fields: {} }; + const defaultMapping = useMemo(() => createDefaultMapping(selectedConnector.fields), [ + selectedConnector.fields, + ]); + + return ( + <> + + + + {i18n.FIELD_MAPPING_FIRST_COL} + + + {i18n.FIELD_MAPPING_SECOND_COL} + + + {i18n.FIELD_MAPPING_THIRD_COL} + + + + + {(mapping ?? defaultMapping).map((item) => ( + + ))} + + + ); +}; + +export const FieldMapping = React.memo(FieldMappingComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping_row.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping_row.tsx new file mode 100644 index 0000000000000..beca8f1fbbc77 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping_row.tsx @@ -0,0 +1,78 @@ +/* + * 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, { useMemo } from 'react'; +import { + EuiFlexItem, + EuiFlexGroup, + EuiSuperSelect, + EuiIcon, + EuiSuperSelectOption, +} from '@elastic/eui'; + +import { capitalize } from 'lodash'; + +export interface RowProps { + id: string; + disabled: boolean; + securitySolutionField: string; + thirdPartyOptions: Array>; + actionTypeOptions: Array>; + onChangeActionType: (caseField: string, newActionType: string) => void; + onChangeThirdParty: (caseField: string, newThirdPartyField: string) => void; + selectedActionType: string; + selectedThirdParty: string; +} + +const FieldMappingRowComponent: React.FC = ({ + id, + disabled, + securitySolutionField, + thirdPartyOptions, + actionTypeOptions, + onChangeActionType, + onChangeThirdParty, + selectedActionType, + selectedThirdParty, +}) => { + const securitySolutionFieldCapitalized = useMemo(() => capitalize(securitySolutionField), [ + securitySolutionField, + ]); + return ( + + + + + {securitySolutionFieldCapitalized} + + + + + + + + + + + + + + ); +}; + +export const FieldMappingRow = React.memo(FieldMappingRowComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/translations.ts new file mode 100644 index 0000000000000..665ccbcfa114d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/translations.ts @@ -0,0 +1,190 @@ +/* + * 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'; + +export const INCIDENT_MANAGEMENT_SYSTEM_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.incidentManagementSystemTitle', + { + defaultMessage: 'Connect to external incident management system', + } +); + +export const INCIDENT_MANAGEMENT_SYSTEM_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.incidentManagementSystemDesc', + { + defaultMessage: + 'You may optionally connect Security cases to an external incident management system of your choosing. This will allow you to push case data as an incident in your chosen third-party system.', + } +); + +export const INCIDENT_MANAGEMENT_SYSTEM_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.incidentManagementSystemLabel', + { + defaultMessage: 'Incident management system', + } +); + +export const NO_CONNECTOR = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.noConnector', + { + defaultMessage: 'No connector selected', + } +); + +export const ADD_NEW_CONNECTOR = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.addNewConnector', + { + defaultMessage: 'Add new connector', + } +); + +export const CASE_CLOSURE_OPTIONS_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsTitle', + { + defaultMessage: 'Case Closures', + } +); + +export const CASE_CLOSURE_OPTIONS_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsDesc', + { + defaultMessage: + 'Define how you wish Security cases to be closed. Automated case closures require an established connection to an external incident management system.', + } +); + +export const CASE_CLOSURE_OPTIONS_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsLabel', + { + defaultMessage: 'Case closure options', + } +); + +export const CASE_CLOSURE_OPTIONS_MANUAL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsManual', + { + defaultMessage: 'Manually close Security cases', + } +); + +export const CASE_CLOSURE_OPTIONS_NEW_INCIDENT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsNewIncident', + { + defaultMessage: + 'Automatically close Security cases when pushing new incident to external system', + } +); + +export const CASE_CLOSURE_OPTIONS_CLOSED_INCIDENT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.caseClosureOptionsClosedIncident', + { + defaultMessage: 'Automatically close Security cases when incident is closed in external system', + } +); + +export const FIELD_MAPPING_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingTitle', + { + defaultMessage: 'Field mappings', + } +); + +export const FIELD_MAPPING_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingDesc', + { + defaultMessage: + 'Map Security case fields when pushing data to a third-party. Field mappings require an established connection to an external incident management system.', + } +); + +export const FIELD_MAPPING_FIRST_COL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingFirstCol', + { + defaultMessage: 'Security case field', + } +); + +export const FIELD_MAPPING_SECOND_COL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingSecondCol', + { + defaultMessage: 'External incident field', + } +); + +export const FIELD_MAPPING_THIRD_COL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingThirdCol', + { + defaultMessage: 'On edit and update', + } +); + +export const FIELD_MAPPING_EDIT_NOTHING = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingEditNothing', + { + defaultMessage: 'Nothing', + } +); + +export const FIELD_MAPPING_EDIT_OVERWRITE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingEditOverwrite', + { + defaultMessage: 'Overwrite', + } +); + +export const FIELD_MAPPING_EDIT_APPEND = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.fieldMappingEditAppend', + { + defaultMessage: 'Append', + } +); + +export const CANCEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.cancelButton', + { + defaultMessage: 'Cancel', + } +); + +export const WARNING_NO_CONNECTOR_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.warningTitle', + { + defaultMessage: 'Warning', + } +); + +export const WARNING_NO_CONNECTOR_MESSAGE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.warningMessage', + { + defaultMessage: + 'The selected connector has been deleted. Either select a different connector or create a new one.', + } +); + +export const MAPPING_FIELD_NOT_MAPPED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.mappingFieldNotMapped', + { + defaultMessage: 'Not mapped', + } +); + +export const UPDATE_CONNECTOR = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.updateConnector', + { + defaultMessage: 'Update connector', + } +); + +export const UPDATE_SELECTED_CONNECTOR = (connectorName: string): string => { + return i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.updateSelectedConnector', + { + values: { connectorName }, + defaultMessage: 'Update { connectorName }', + } + ); +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/types.ts similarity index 50% rename from x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/types.ts index 70d53ab79f631..6cd2200e1dc74 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/types.ts @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExternalServiceConfiguration } from '../case/types'; -import * as i18n from './translations'; +import { ActionType } from '../../../../../types'; -export const config: ExternalServiceConfiguration = { - id: '.servicenow', - name: i18n.NAME, - minimumLicenseRequired: 'platinum', -}; +export { ActionType }; + +export interface ThirdPartyField { + label: string; + validSourceFields: string[]; + defaultSourceField: string; + defaultActionType: string; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/utils.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/utils.ts new file mode 100644 index 0000000000000..a173d90515302 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/utils.ts @@ -0,0 +1,38 @@ +/* + * 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 { CasesConfigurationMapping } from '../types'; + +export const setActionTypeToMapping = ( + caseField: string, + newActionType: string, + mapping: CasesConfigurationMapping[] +): CasesConfigurationMapping[] => { + const findItemIndex = mapping.findIndex((item) => item.source === caseField); + + if (findItemIndex >= 0) { + return [ + ...mapping.slice(0, findItemIndex), + { ...mapping[findItemIndex], actionType: newActionType }, + ...mapping.slice(findItemIndex + 1), + ]; + } + + return [...mapping]; +}; + +export const setThirdPartyToMapping = ( + caseField: string, + newThirdPartyField: string, + mapping: CasesConfigurationMapping[] +): CasesConfigurationMapping[] => + mapping.map((item) => { + if (item.source !== caseField && item.target === newThirdPartyField) { + return { ...item, target: 'not_mapped' }; + } else if (item.source === caseField) { + return { ...item, target: newThirdPartyField }; + } + return item; + }); diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/config.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/config.ts similarity index 91% rename from x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/config.ts rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/config.ts index 35c677c9574e3..7f810cf5eb38f 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/config.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/config.ts @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ConnectorConfiguration } from './types'; import * as i18n from './translations'; import logo from './logo.svg'; -export const connector: ConnectorConfiguration = { +export const connectorConfiguration = { id: '.servicenow', name: i18n.SERVICENOW_TITLE, logo, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/index.ts new file mode 100644 index 0000000000000..65bb3ae4f5a37 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/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 { getActionType as getServiceNowActionType } from './servicenow'; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/logo.svg b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/logo.svg similarity index 100% rename from x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/logo.svg rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/logo.svg diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx new file mode 100644 index 0000000000000..5e70bc20f5c51 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx @@ -0,0 +1,97 @@ +/* + * 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 { TypeRegistry } from '../../../type_registry'; +import { registerBuiltInActionTypes } from '.././index'; +import { ActionTypeModel } from '../../../../types'; +import { ServiceNowActionConnector } from './types'; + +const ACTION_TYPE_ID = '.servicenow'; +let actionTypeModel: ActionTypeModel; + +beforeAll(() => { + const actionTypeRegistry = new TypeRegistry(); + registerBuiltInActionTypes({ actionTypeRegistry }); + const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); + if (getResult !== null) { + actionTypeModel = getResult; + } +}); + +describe('actionTypeRegistry.get() works', () => { + test('action type static data is as expected', () => { + expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); + }); +}); + +describe('servicenow connector validation', () => { + test('connector validation succeeds when connector config is valid', () => { + const actionConnector = { + secrets: { + username: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.servicenow', + name: 'ServiceNow', + isPreconfigured: false, + config: { + apiUrl: 'https://dev94428.service-now.com/', + }, + } as ServiceNowActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + apiUrl: [], + username: [], + password: [], + }, + }); + }); + + test('connector validation fails when connector config is not valid', () => { + const actionConnector = ({ + secrets: { + username: 'user', + }, + id: '.servicenow', + actionTypeId: '.servicenow', + name: 'servicenow', + config: {}, + } as unknown) as ServiceNowActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + apiUrl: ['URL is required.'], + username: [], + password: ['Password is required.'], + }, + }); + }); +}); + +describe('servicenow action params validation', () => { + test('action params validation succeeds when action params is valid', () => { + const actionParams = { + subActionParams: { title: 'some title {{test}}' }, + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { title: [] }, + }); + }); + + test('params validation fails when body is not valid', () => { + const actionParams = { + subActionParams: { title: '' }, + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + title: ['Title is required.'], + }, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx new file mode 100644 index 0000000000000..0f7b83ed84fb4 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx @@ -0,0 +1,67 @@ +/* + * 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 { lazy } from 'react'; +import { ValidationResult, ActionTypeModel } from '../../../../types'; +import { connectorConfiguration } from './config'; +import logo from './logo.svg'; +import { ServiceNowActionConnector, ServiceNowActionParams } from './types'; +import * as i18n from './translations'; +import { isValidUrl } from '../../../lib/value_validators'; + +const validateConnector = (action: ServiceNowActionConnector): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + apiUrl: new Array(), + username: new Array(), + password: new Array(), + }; + validationResult.errors = errors; + + if (!action.config.apiUrl) { + errors.apiUrl = [...errors.apiUrl, i18n.API_URL_REQUIRED]; + } + + if (action.config.apiUrl && !isValidUrl(action.config.apiUrl, 'https:')) { + errors.apiUrl = [...errors.apiUrl, i18n.API_URL_INVALID]; + } + + if (!action.secrets.username) { + errors.username = [...errors.username, i18n.USERNAME_REQUIRED]; + } + + if (!action.secrets.password) { + errors.password = [...errors.password, i18n.PASSWORD_REQUIRED]; + } + + return validationResult; +}; + +export function getActionType(): ActionTypeModel< + ServiceNowActionConnector, + ServiceNowActionParams +> { + return { + id: connectorConfiguration.id, + iconClass: logo, + selectMessage: i18n.SERVICENOW_DESC, + actionTypeTitle: connectorConfiguration.name, + validateConnector, + actionConnectorFields: lazy(() => import('./servicenow_connectors')), + validateParams: (actionParams: ServiceNowActionParams): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + title: new Array(), + }; + validationResult.errors = errors; + if (actionParams.subActionParams && !actionParams.subActionParams.title?.length) { + errors.title.push(i18n.TITLE_REQUIRED); + } + return validationResult; + }, + actionParamsFields: lazy(() => import('./servicenow_params')), + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx new file mode 100644 index 0000000000000..452d9c288926e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx @@ -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 React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { DocLinksStart } from 'kibana/public'; +import ServiceNowConnectorFields from './servicenow_connectors'; +import { ServiceNowActionConnector } from './types'; + +describe('ServiceNowActionConnectorFields renders', () => { + test('alerting servicenow connector fields is rendered', () => { + const actionConnector = { + secrets: { + username: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.webhook', + isPreconfigured: false, + name: 'webhook', + config: { + apiUrl: 'https://test/', + }, + } as ServiceNowActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + /> + ); + expect( + wrapper.find('[data-test-subj="connector-servicenow-username-form-input"]').length > 0 + ).toBeTruthy(); + + expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="connector-servicenow-password-form-input"]').length > 0 + ).toBeTruthy(); + }); + + test('case specific servicenow connector fields is rendered', () => { + const actionConnector = { + secrets: { + username: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.servicenow', + isPreconfigured: false, + name: 'servicenow', + config: { + apiUrl: 'https://test/', + incidentConfiguration: { mapping: [] }, + isCaseOwned: true, + }, + } as ServiceNowActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + /> + ); + expect(wrapper.find('[data-test-subj="case-servicenow-mappings"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="connector-servicenow-password-form-input"]').length > 0 + ).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx new file mode 100644 index 0000000000000..a5c4849cb63d9 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx @@ -0,0 +1,182 @@ +/* + * 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, { useCallback } from 'react'; + +import { + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiFieldPassword, + EuiSpacer, +} from '@elastic/eui'; + +import { isEmpty } from 'lodash'; +import { ActionConnectorFieldsProps } from '../../../../types'; +import * as i18n from './translations'; +import { ServiceNowActionConnector, CasesConfigurationMapping } from './types'; +import { connectorConfiguration } from './config'; +import { FieldMapping } from './case_mappings/field_mapping'; + +const ServiceNowConnectorFields: React.FC> = ({ action, editActionSecrets, editActionConfig, errors, consumer }) => { + // TODO: remove incidentConfiguration later, when Case ServiceNow will move their fields to the level of action execution + const { apiUrl, incidentConfiguration, isCaseOwned } = action.config; + const mapping = incidentConfiguration ? incidentConfiguration.mapping : []; + + const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl != null; + + const { username, password } = action.secrets; + + const isUsernameInvalid: boolean = errors.username.length > 0 && username != null; + const isPasswordInvalid: boolean = errors.password.length > 0 && password != null; + + // TODO: remove this block later, when Case ServiceNow will move their fields to the level of action execution + if (consumer === 'case') { + if (isEmpty(mapping)) { + editActionConfig('incidentConfiguration', { + mapping: createDefaultMapping(connectorConfiguration.fields as any), + }); + } + if (!isCaseOwned) { + editActionConfig('isCaseOwned', true); + } + } + + const handleOnChangeActionConfig = useCallback( + (key: string, value: string) => editActionConfig(key, value), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + const handleOnChangeSecretConfig = useCallback( + (key: string, value: string) => editActionSecrets(key, value), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + const handleOnChangeMappingConfig = useCallback( + (newMapping: CasesConfigurationMapping[]) => + editActionConfig('incidentConfiguration', { + ...action.config.incidentConfiguration, + mapping: newMapping, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [action.config] + ); + + return ( + <> + + + + handleOnChangeActionConfig('apiUrl', evt.target.value)} + onBlur={() => { + if (!apiUrl) { + editActionConfig('apiUrl', ''); + } + }} + /> + + + + + + + + handleOnChangeSecretConfig('username', evt.target.value)} + onBlur={() => { + if (!username) { + editActionSecrets('username', ''); + } + }} + /> + + + + + + + + handleOnChangeSecretConfig('password', evt.target.value)} + onBlur={() => { + if (!password) { + editActionSecrets('password', ''); + } + }} + /> + + + + {isCaseOwned && ( // TODO: remove this block later, when Case ServiceNow will move their fields to the level of action execution + <> + + + + + + + + )} + + ); +}; + +export const createDefaultMapping = (fields: Record): CasesConfigurationMapping[] => + Object.keys(fields).map( + (key) => + ({ + source: fields[key].defaultSourceField, + target: key, + actionType: fields[key].defaultActionType, + } as CasesConfigurationMapping) + ); + +// eslint-disable-next-line import/no-default-export +export { ServiceNowConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx new file mode 100644 index 0000000000000..57d50cf7e5bdd --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx @@ -0,0 +1,43 @@ +/* + * 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 { mountWithIntl } from 'test_utils/enzyme_helpers'; +import ServiceNowParamsFields from './servicenow_params'; + +describe('ServiceNowParamsFields renders', () => { + test('all params fields is rendered', () => { + const actionParams = { + subAction: 'pushToService', + subActionParams: { + title: 'sn title', + description: 'some description', + comment: 'comment for sn', + severity: '1', + urgency: '2', + impact: '3', + savedObjectId: '123', + externalId: null, + }, + }; + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[]} + /> + ); + expect(wrapper.find('[data-test-subj="urgencySelect"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('value')).toStrictEqual( + '1' + ); + expect(wrapper.find('[data-test-subj="impactSelect"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="titleInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="incidentDescriptionTextArea"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="incidentCommentTextArea"]').length > 0).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx new file mode 100644 index 0000000000000..67070b6dc8907 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.tsx @@ -0,0 +1,262 @@ +/* + * 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, { Fragment, useEffect } from 'react'; +import { EuiFormRow, EuiTextArea } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiSelect } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiFieldText } from '@elastic/eui'; +import { EuiSpacer } from '@elastic/eui'; +import { EuiTitle } from '@elastic/eui'; +import { ActionParamsProps } from '../../../../types'; +import { AddMessageVariables } from '../../add_message_variables'; +import { ServiceNowActionParams } from './types'; + +const ServiceNowParamsFields: React.FunctionComponent> = ({ actionParams, editAction, index, errors, messageVariables }) => { + const { title, description, comment, severity, urgency, impact, savedObjectId } = + actionParams.subActionParams || {}; + const selectOptions = [ + { + value: '1', + text: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectHighOptionLabel', + { + defaultMessage: 'High', + } + ), + }, + { + value: '2', + text: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectMediumOptionLabel', + { + defaultMessage: 'Medium', + } + ), + }, + { + value: '3', + text: i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severitySelectLawOptionLabel', + { + defaultMessage: 'Low', + } + ), + }, + ]; + + const editSubActionProperty = (key: string, value: {}) => { + const newProps = { ...actionParams.subActionParams, [key]: value }; + editAction('subActionParams', newProps, index); + }; + + useEffect(() => { + if (!actionParams.subAction) { + editAction('subAction', 'pushToService', index); + } + if (!savedObjectId && messageVariables?.find((variable) => variable === 'alertId')) { + editSubActionProperty('savedObjectId', '{{alertId}}'); + } + if (!urgency) { + editSubActionProperty('urgency', '3'); + } + if (!impact) { + editSubActionProperty('impact', '3'); + } + if (!severity) { + editSubActionProperty('severity', '3'); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [title, description, comment, severity, impact, urgency]); + + const onSelectMessageVariable = (paramsProperty: string, variable: string) => { + editSubActionProperty( + paramsProperty, + ((actionParams as any).subActionParams[paramsProperty] ?? '').concat(` {{${variable}}}`) + ); + }; + + return ( + + +

Incident

+
+ + + { + editSubActionProperty('urgency', e.target.value); + }} + /> + + + + + + { + editSubActionProperty('severity', e.target.value); + }} + /> + + + + + { + editSubActionProperty('impact', e.target.value); + }} + /> + + + + + 0 && title !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.titleFieldLabel', + { + defaultMessage: 'Short description', + } + )} + labelAppend={ + onSelectMessageVariable('title', variable)} + paramsProperty="title" + /> + } + > + 0 && title !== undefined} + value={title || ''} + onChange={(e: React.ChangeEvent) => { + editSubActionProperty('title', e.target.value); + }} + onBlur={() => { + if (!title) { + editSubActionProperty('title', ''); + } + }} + /> + + + onSelectMessageVariable('description', variable) + } + paramsProperty="description" + /> + } + > + { + editSubActionProperty('description', e.target.value); + }} + onBlur={() => { + if (!description) { + editSubActionProperty('description', ''); + } + }} + /> + + + onSelectMessageVariable('comment', variable) + } + paramsProperty="comment" + /> + } + > + { + editSubActionProperty('comment', e.target.value); + }} + onBlur={() => { + if (!comment) { + editSubActionProperty('comment', ''); + } + }} + /> + +
+ ); +}; + +// eslint-disable-next-line import/no-default-export +export { ServiceNowParamsFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts new file mode 100644 index 0000000000000..f5670f432d4d4 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts @@ -0,0 +1,133 @@ +/* + * 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'; + +export const SERVICENOW_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.selectMessageText', + { + defaultMessage: 'Push or update data to a new incident in ServiceNow.', + } +); + +export const SERVICENOW_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.actionTypeTitle', + { + defaultMessage: 'ServiceNow', + } +); + +export const API_URL_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiUrlTextFieldLabel', + { + defaultMessage: 'URL', + } +); + +export const API_URL_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredApiUrlTextField', + { + defaultMessage: 'URL is required.', + } +); + +export const API_URL_INVALID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.invalidApiUrlTextField', + { + defaultMessage: 'URL is invalid.', + } +); + +export const USERNAME_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.usernameTextFieldLabel', + { + defaultMessage: 'Username', + } +); + +export const USERNAME_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredUsernameTextField', + { + defaultMessage: 'Username is required.', + } +); + +export const PASSWORD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.passwordTextFieldLabel', + { + defaultMessage: 'Password', + } +); + +export const PASSWORD_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredPasswordTextField', + { + defaultMessage: 'Password is required.', + } +); + +export const API_TOKEN_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiTokenTextFieldLabel', + { + defaultMessage: 'Api token', + } +); + +export const API_TOKEN_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredApiTokenTextField', + { + defaultMessage: 'Api token is required.', + } +); + +export const EMAIL_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.emailTextFieldLabel', + { + defaultMessage: 'Email', + } +); + +export const EMAIL_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredEmailTextField', + { + defaultMessage: 'Email is required.', + } +); + +export const MAPPING_FIELD_SHORT_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.mappingFieldShortDescription', + { + defaultMessage: 'Short Description', + } +); + +export const MAPPING_FIELD_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.mappingFieldDescription', + { + defaultMessage: 'Description', + } +); + +export const MAPPING_FIELD_COMMENTS = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.mappingFieldComments', + { + defaultMessage: 'Comments', + } +); + +export const DESCRIPTION_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.common.requiredDescriptionTextField', + { + defaultMessage: 'Description is required.', + } +); + +export const TITLE_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.common.requiredTitleTextField', + { + defaultMessage: 'Title is required.', + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts new file mode 100644 index 0000000000000..92252efc3a41c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts @@ -0,0 +1,46 @@ +/* + * 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 interface ServiceNowActionConnector { + config: ServiceNowConfig; + secrets: ServiceNowSecrets; +} + +export interface ServiceNowActionParams { + subAction: string; + subActionParams: { + savedObjectId: string; + title: string; + description: string; + comment: string; + externalId: string | null; + severity: string; + urgency: string; + impact: string; + }; +} + +interface IncidentConfiguration { + mapping: CasesConfigurationMapping[]; +} + +interface ServiceNowConfig { + apiUrl: string; + incidentConfiguration?: IncidentConfiguration; + isCaseOwned?: boolean; +} + +interface ServiceNowSecrets { + username: string; + password: string; +} + +// to remove +export interface CasesConfigurationMapping { + source: string; + target: string; + actionType: string; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx index 11934d3af3ceb..311ae587bbe13 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx @@ -12,7 +12,7 @@ import { SlackActionConnector } from '../types'; const SlackActionFields: React.FunctionComponent> = ({ action, editActionSecrets, errors, docLinks }) => { +>> = ({ action, editActionSecrets, errors }) => { const { webhookUrl } = action.secrets; return ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx b/x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx index f8a9085a88656..d78930344a673 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx @@ -19,6 +19,7 @@ export interface ActionsConnectorsContextValue { capabilities: ApplicationStart['capabilities']; reloadConnectors?: () => Promise; docLinks: DocLinksStart; + consumer?: string; } const ActionsConnectorsContext = createContext(null as any); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.test.ts index 9d628adc1db6b..e954fb5c7617b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.test.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { throwIfAbsent, throwIfIsntContained } from './value_validators'; +import { throwIfAbsent, throwIfIsntContained, isValidUrl } from './value_validators'; import uuid from 'uuid'; describe('throwIfAbsent', () => { @@ -79,3 +79,17 @@ describe('throwIfIsntContained', () => { ).toEqual(values); }); }); + +describe('isValidUrl', () => { + test('verifies invalid url', () => { + expect(isValidUrl('this is not a url')).toBeFalsy(); + }); + + test('verifies valid url any protocol', () => { + expect(isValidUrl('https://www.elastic.co/')).toBeTruthy(); + }); + + test('verifies valid url with specific protocol', () => { + expect(isValidUrl('https://www.elastic.co/', 'https:')).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.ts index 7ee7359086406..4942e6328097d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/value_validators.ts @@ -31,3 +31,15 @@ export function throwIfIsntContained( return values; }; } + +export const isValidUrl = (urlString: string, protocol?: string) => { + try { + const urlObject = new URL(urlString); + if (protocol === undefined || urlObject.protocol === protocol) { + return true; + } + return false; + } catch (err) { + return false; + } +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx index 489cdf167b283..813f3598a748d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx @@ -53,6 +53,7 @@ interface ActionConnectorProps { http: HttpSetup; actionTypeRegistry: TypeRegistry; docLinks: DocLinksStart; + consumer?: string; } export const ActionConnectorForm = ({ @@ -64,6 +65,7 @@ export const ActionConnectorForm = ({ http, actionTypeRegistry, docLinks, + consumer, }: ActionConnectorProps) => { const setActionProperty = (key: string, value: any) => { dispatch({ command: { type: 'setProperty' }, payload: { key, value } }); @@ -170,6 +172,7 @@ export const ActionConnectorForm = ({ editActionSecrets={setActionSecretsProperty} http={http} docLinks={docLinks} + consumer={consumer} /> ) : null} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 7db6b5145f895..c21cce4cc4b62 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -149,6 +149,16 @@ describe('action_form', () => { config: {}, isPreconfigured: false, }, + { + secrets: {}, + id: '.servicenow', + actionTypeId: '.servicenow', + name: 'Non consumer connector', + config: { + isCaseOwned: true, + }, + isPreconfigured: false, + }, ]); const mocks = coreMock.createSetup(); const [ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 201852ddeee48..7f400ee9a5db1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -45,6 +45,7 @@ import { TypeRegistry } from '../../type_registry'; import { actionTypeCompare } from '../../lib/action_type_compare'; import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled'; import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; +import { ServiceNowConnectorConfiguration } from '../../../common'; interface ActionAccordionFormProps { actions: AlertAction[]; @@ -131,7 +132,14 @@ export const ActionForm = ({ try { setIsLoadingConnectors(true); const loadedConnectors = await loadConnectors({ http }); - setConnectors(loadedConnectors); + setConnectors( + loadedConnectors.filter( + (action) => + action.actionTypeId !== ServiceNowConnectorConfiguration.id || + (action.actionTypeId === ServiceNowConnectorConfiguration.id && + !action.config.isCaseOwned) + ) + ); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx index 2dd1f83749372..60ec0cfa6955e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx @@ -52,6 +52,7 @@ export const ConnectorAddFlyout = ({ actionTypeRegistry, reloadConnectors, docLinks, + consumer, } = useActionsConnectorsContext(); const [actionType, setActionType] = useState(undefined); const [hasActionsUpgradeableByTrial, setHasActionsUpgradeableByTrial] = useState(false); @@ -117,6 +118,7 @@ export const ConnectorAddFlyout = ({ actionTypeRegistry={actionTypeRegistry} http={http} docLinks={docLinks} + consumer={consumer} /> ); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx index 1d19f436950c7..67c836fc12cf7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx @@ -40,6 +40,7 @@ interface ConnectorAddModalProps { >; capabilities: ApplicationStart['capabilities']; docLinks: DocLinksStart; + consumer?: string; } export const ConnectorAddModal = ({ @@ -52,6 +53,7 @@ export const ConnectorAddModal = ({ actionTypeRegistry, capabilities, docLinks, + consumer, }: ConnectorAddModalProps) => { let hasErrors = false; const initialConnector = { @@ -164,6 +166,7 @@ export const ConnectorAddModal = ({ actionTypeRegistry={actionTypeRegistry} docLinks={docLinks} http={http} + consumer={consumer} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx index cbbbbfaea7ea3..68fd8b65f1a41 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx @@ -48,6 +48,7 @@ export const ConnectorEditFlyout = ({ actionTypeRegistry, reloadConnectors, docLinks, + consumer, } = useActionsConnectorsContext(); const canSave = hasSaveActionsCapability(capabilities); const closeFlyout = useCallback(() => setEditFlyoutVisibility(false), [setEditFlyoutVisibility]); @@ -185,6 +186,7 @@ export const ConnectorEditFlyout = ({ actionTypeRegistry={actionTypeRegistry} http={http} docLinks={docLinks} + consumer={consumer} /> ) : ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx index 09d94e2418cb8..40505ac3fe76c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import * as React from 'react'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { ScopedHistory } from 'kibana/public'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ActionsConnectorsList } from './actions_connectors_list'; import { coreMock, scopedHistoryMock } from '../../../../../../../../src/core/public/mocks'; import { ReactWrapper } from 'enzyme'; @@ -27,7 +27,7 @@ const actionTypeRegistry = actionTypeRegistryMock.create(); describe('actions_connectors_list component empty', () => { let wrapper: ReactWrapper; - beforeAll(async () => { + async function setup() { const { loadAllActions, loadActionTypes } = jest.requireMock( '../../../lib/action_connector_api' ); @@ -75,25 +75,29 @@ describe('actions_connectors_list component empty', () => { }; actionTypeRegistry.has.mockReturnValue(true); + wrapper = mountWithIntl( + + + + ); + + // Wait for active space to resolve before requesting the component to update await act(async () => { - wrapper = mountWithIntl( - - - - ); + await nextTick(); + wrapper.update(); }); + } - await waitForRender(wrapper); - }); - - it('renders empty prompt', () => { + it('renders empty prompt', async () => { + await setup(); expect(wrapper.find('EuiEmptyPrompt')).toHaveLength(1); expect( wrapper.find('[data-test-subj="createFirstActionButton"]').find('EuiButton') ).toHaveLength(1); }); - test('if click create button should render ConnectorAddFlyout', () => { + test('if click create button should render ConnectorAddFlyout', async () => { + await setup(); wrapper.find('[data-test-subj="createFirstActionButton"]').first().simulate('click'); expect(wrapper.find('ConnectorAddFlyout')).toHaveLength(1); }); @@ -102,7 +106,7 @@ describe('actions_connectors_list component empty', () => { describe('actions_connectors_list component with items', () => { let wrapper: ReactWrapper; - beforeAll(async () => { + async function setup() { const { loadAllActions, loadActionTypes } = jest.requireMock( '../../../lib/action_connector_api' ); @@ -181,29 +185,34 @@ describe('actions_connectors_list component with items', () => { alertTypeRegistry: {} as any, }; + wrapper = mountWithIntl( + + + + ); + + // Wait for active space to resolve before requesting the component to update await act(async () => { - wrapper = mountWithIntl( - - - - ); + await nextTick(); + wrapper.update(); }); - await waitForRender(wrapper); - expect(loadAllActions).toHaveBeenCalled(); - }); + } - it('renders table of connectors', () => { + it('renders table of connectors', async () => { + await setup(); expect(wrapper.find('EuiInMemoryTable')).toHaveLength(1); expect(wrapper.find('EuiTableRow')).toHaveLength(3); }); - it('renders table with preconfigured connectors', () => { + it('renders table with preconfigured connectors', async () => { + await setup(); expect(wrapper.find('[data-test-subj="preConfiguredTitleMessage"]')).toHaveLength(2); }); test('if select item for edit should render ConnectorEditFlyout', async () => { + await setup(); await wrapper.find('[data-test-subj="edit1"]').first().simulate('click'); expect(wrapper.find('ConnectorEditFlyout')).toHaveLength(1); @@ -213,7 +222,7 @@ describe('actions_connectors_list component with items', () => { describe('actions_connectors_list component empty with show only capability', () => { let wrapper: ReactWrapper; - beforeAll(async () => { + async function setup() { const { loadAllActions, loadActionTypes } = jest.requireMock( '../../../lib/action_connector_api' ); @@ -264,18 +273,21 @@ describe('actions_connectors_list component empty with show only capability', () alertTypeRegistry: {} as any, }; + wrapper = mountWithIntl( + + + + ); + + // Wait for active space to resolve before requesting the component to update await act(async () => { - wrapper = mountWithIntl( - - - - ); + await nextTick(); + wrapper.update(); }); + } - await waitForRender(wrapper); - }); - - it('renders no permissions to create connector', () => { + it('renders no permissions to create connector', async () => { + await setup(); expect(wrapper.find('[defaultMessage="No permissions to create connector"]')).toHaveLength(1); expect(wrapper.find('[data-test-subj="createActionButton"]')).toHaveLength(0); }); @@ -284,7 +296,7 @@ describe('actions_connectors_list component empty with show only capability', () describe('actions_connectors_list with show only capability', () => { let wrapper: ReactWrapper; - beforeAll(async () => { + async function setup() { const { loadAllActions, loadActionTypes } = jest.requireMock( '../../../lib/action_connector_api' ); @@ -350,18 +362,21 @@ describe('actions_connectors_list with show only capability', () => { alertTypeRegistry: {} as any, }; + wrapper = mountWithIntl( + + + + ); + + // Wait for active space to resolve before requesting the component to update await act(async () => { - wrapper = mountWithIntl( - - - - ); + await nextTick(); + wrapper.update(); }); + } - await waitForRender(wrapper); - }); - - it('renders table of connectors with delete button disabled', () => { + it('renders table of connectors with delete button disabled', async () => { + await setup(); expect(wrapper.find('EuiInMemoryTable')).toHaveLength(1); expect(wrapper.find('EuiTableRow')).toHaveLength(2); wrapper.find('EuiTableRow').forEach((elem) => { @@ -375,7 +390,7 @@ describe('actions_connectors_list with show only capability', () => { describe('actions_connectors_list component with disabled items', () => { let wrapper: ReactWrapper; - beforeAll(async () => { + async function setup() { const { loadAllActions, loadActionTypes } = jest.requireMock( '../../../lib/action_connector_api' ); @@ -448,20 +463,23 @@ describe('actions_connectors_list component with disabled items', () => { alertTypeRegistry: {} as any, }; + wrapper = mountWithIntl( + + + + ); + + // Wait for active space to resolve before requesting the component to update await act(async () => { - wrapper = mountWithIntl( - - - - ); + await nextTick(); + wrapper.update(); }); - await waitForRender(wrapper); - expect(loadAllActions).toHaveBeenCalled(); - }); + } - it('renders table of connectors', () => { + it('renders table of connectors', async () => { + await setup(); expect(wrapper.find('EuiInMemoryTable')).toHaveLength(1); expect(wrapper.find('EuiTableRow')).toHaveLength(2); expect(wrapper.find('EuiTableRow').at(0).prop('className')).toEqual( @@ -472,9 +490,3 @@ describe('actions_connectors_list component with disabled items', () => { ); }); }); - -async function waitForRender(wrapper: ReactWrapper) { - await Promise.resolve(); - await Promise.resolve(); - wrapper.update(); -} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index 5d52896cc628f..0e0691960729d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -20,6 +20,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { ServiceNowConnectorConfiguration } from '../../../../common'; import { useAppDependencies } from '../../../app_context'; import { loadAllActions, loadActionTypes, deleteActions } from '../../../lib/action_connector_api'; import ConnectorAddFlyout from '../../action_connector_form/connector_add_flyout'; @@ -118,7 +119,14 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { setIsLoadingActions(true); try { const actionsResponse = await loadAllActions({ http }); - setActions(actionsResponse); + setActions( + actionsResponse.filter( + (action) => + action.actionTypeId !== ServiceNowConnectorConfiguration.id || + (action.actionTypeId === ServiceNowConnectorConfiguration.id && + !action.config.isCaseOwned) + ) + ); } catch (e) { toastNotifications.addDanger({ title: i18n.translate( diff --git a/x-pack/plugins/triggers_actions_ui/public/common/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/index.ts index 94089a274e79d..9dd3fd787f860 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/index.ts @@ -5,3 +5,5 @@ */ export * from './expression_items'; + +export { connectorConfiguration as ServiceNowConnectorConfiguration } from '../application/components/builtin_action_types/servicenow/config'; diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 52179dd35767c..a4a13d7ec849c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -32,6 +32,7 @@ export interface ActionConnectorFieldsProps { errors: IErrorObject; docLinks: DocLinksStart; http?: HttpSetup; + consumer?: string; } export interface ActionParamsProps { diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts index 3356b3e3d5828..a451edea76d83 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts @@ -38,15 +38,27 @@ export default function servicenowTest({ getService }: FtrProviderContext) { const mockServiceNow = { config: { apiUrl: 'www.servicenowisinkibanaactions.com', - casesConfiguration: { mapping: [...mapping] }, + incidentConfiguration: { mapping: [...mapping] }, + isCaseOwned: true, }, secrets: { password: 'elastic', username: 'changeme', }, params: { - comments: 'hello cool service now incident', - short_description: 'this is a cool service now incident', + savedObjectId: '123', + title: 'a title', + description: 'a description', + comment: 'test-alert comment', + severity: '1', + urgency: '2', + impact: '1', + comments: [ + { + commentId: '456', + comment: 'first comment', + }, + ], }, }; describe('servicenow', () => { @@ -68,7 +80,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, + incidentConfiguration: { ...mockServiceNow.config.incidentConfiguration }, + isCaseOwned: true, }, secrets: mockServiceNow.secrets, }) diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/servicenow_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/servicenow_simulation.ts index 8a675ec10aa8c..e2f31da1c8064 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/servicenow_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/servicenow_simulation.ts @@ -25,6 +25,10 @@ export function initPlugin(router: IRouter, path: string) { short_description: schema.string(), description: schema.maybe(schema.string()), comments: schema.maybe(schema.string()), + caller_id: schema.string(), + severity: schema.string({ defaultValue: '1' }), + urgency: schema.string({ defaultValue: '1' }), + impact: schema.string({ defaultValue: '1' }), }), }, }, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index 093f09c24bad3..19206ce681000 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -50,7 +50,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { params: { subAction: 'pushToService', subActionParams: { - caseId: '123', + savedObjectId: '123', title: 'a title', description: 'a description', createdAt: '2020-03-13T08:34:53.450Z', @@ -361,12 +361,12 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.caseId]: expected value of type [string] but got [undefined]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]', }); }); }); - it('should handle failing with a simulated success without caseId', async () => { + it('should handle failing with a simulated success without savedObjectId', async () => { await supertest .post(`/api/actions/action/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') @@ -379,7 +379,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.caseId]: expected value of type [string] but got [undefined]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]', }); }); }); @@ -392,7 +392,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { params: { ...mockJira.params, subActionParams: { - caseId: 'success', + savedObjectId: 'success', }, }, }) @@ -415,7 +415,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { params: { ...mockJira.params, subActionParams: { - caseId: 'success', + savedObjectId: 'success', title: 'success', }, }, @@ -440,7 +440,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { ...mockJira.params, subActionParams: { ...mockJira.params.subActionParams, - caseId: 'success', + savedObjectId: 'success', title: 'success', createdAt: 'success', createdBy: { username: 'elastic' }, @@ -468,7 +468,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { ...mockJira.params, subActionParams: { ...mockJira.params.subActionParams, - caseId: 'success', + savedObjectId: 'success', title: 'success', createdAt: 'success', createdBy: { username: 'elastic' }, @@ -496,7 +496,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { ...mockJira.params, subActionParams: { ...mockJira.params.subActionParams, - caseId: 'success', + savedObjectId: 'success', title: 'success', createdAt: 'success', createdBy: { username: 'elastic' }, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index 531a362fa2bab..8205b75cabed5 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -40,7 +40,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { const mockServiceNow = { config: { apiUrl: 'www.servicenowisinkibanaactions.com', - casesConfiguration: { mapping }, + incidentConfiguration: { mapping }, + isCaseOwned: true, }, secrets: { password: 'elastic', @@ -49,18 +50,12 @@ export default function servicenowTest({ getService }: FtrProviderContext) { params: { subAction: 'pushToService', subActionParams: { - caseId: '123', - title: 'a title', - description: 'a description', + savedObjectId: '123', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - externalId: null, comments: [ { commentId: '456', - version: 'WzU3LDFd', comment: 'first comment', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, @@ -68,6 +63,11 @@ export default function servicenowTest({ getService }: FtrProviderContext) { updatedBy: null, }, ], + description: 'a description', + externalId: null, + title: 'a title', + updatedAt: '2020-06-17T04:37:45.147Z', + updatedBy: { fullName: null, username: 'elastic' }, }, }, }; @@ -93,7 +93,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - casesConfiguration: mockServiceNow.config.casesConfiguration, + incidentConfiguration: mockServiceNow.config.incidentConfiguration, + isCaseOwned: true, }, secrets: mockServiceNow.secrets, }) @@ -106,7 +107,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - casesConfiguration: mockServiceNow.config.casesConfiguration, + incidentConfiguration: mockServiceNow.config.incidentConfiguration, + isCaseOwned: true, }, }); @@ -121,7 +123,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - casesConfiguration: mockServiceNow.config.casesConfiguration, + incidentConfiguration: mockServiceNow.config.incidentConfiguration, + isCaseOwned: true, }, }); }); @@ -155,7 +158,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: 'http://servicenow.mynonexistent.com', - casesConfiguration: mockServiceNow.config.casesConfiguration, + incidentConfiguration: mockServiceNow.config.incidentConfiguration, + isCaseOwned: true, }, secrets: mockServiceNow.secrets, }) @@ -179,7 +183,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - casesConfiguration: mockServiceNow.config.casesConfiguration, + incidentConfiguration: mockServiceNow.config.incidentConfiguration, + isCaseOwned: true, }, }) .expect(400) @@ -193,7 +198,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }); }); - it('should respond with a 400 Bad Request when creating a servicenow action without casesConfiguration', async () => { + it('should create a servicenow action without incidentConfiguration', async () => { await supertest .post('/api/actions/action') .set('kbn-xsrf', 'foo') @@ -202,18 +207,11 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, + isCaseOwned: true, }, secrets: mockServiceNow.secrets, }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type config: [casesConfiguration.mapping]: expected value of type [array] but got [undefined]', - }); - }); + .expect(200); }); it('should respond with a 400 Bad Request when creating a servicenow action with empty mapping', async () => { @@ -225,7 +223,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - casesConfiguration: { mapping: [] }, + incidentConfiguration: { mapping: [] }, + isCaseOwned: true, }, secrets: mockServiceNow.secrets, }) @@ -235,7 +234,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'error validating action type config: [casesConfiguration.mapping]: expected non-empty but got empty', + 'error validating action type config: [incidentConfiguration.mapping]: expected non-empty but got empty', }); }); }); @@ -249,7 +248,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - casesConfiguration: { + incidentConfiguration: { mapping: [ { source: 'title', @@ -258,6 +257,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }, ], }, + isCaseOwned: true, }, secrets: mockServiceNow.secrets, }) @@ -276,7 +276,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - casesConfiguration: mockServiceNow.config.casesConfiguration, + incidentConfiguration: mockServiceNow.config.incidentConfiguration, + isCaseOwned: true, }, secrets: mockServiceNow.secrets, }); @@ -332,12 +333,12 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.caseId]: expected value of type [string] but got [undefined]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]', }); }); }); - it('should handle failing with a simulated success without caseId', async () => { + it('should handle failing with a simulated success without savedObjectId', async () => { await supertest .post(`/api/actions/action/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') @@ -350,7 +351,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.caseId]: expected value of type [string] but got [undefined]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]', }); }); }); @@ -363,7 +364,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { params: { ...mockServiceNow.params, subActionParams: { - caseId: 'success', + savedObjectId: 'success', }, }, }) @@ -378,30 +379,6 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }); }); - it('should handle failing with a simulated success without createdAt', async () => { - await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...mockServiceNow.params, - subActionParams: { - caseId: 'success', - title: 'success', - }, - }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.createdAt]: expected value of type [string] but got [undefined]', - }); - }); - }); - it('should handle failing with a simulated success without commentId', async () => { await supertest .post(`/api/actions/action/${simulatedActionId}/_execute`) @@ -411,7 +388,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { ...mockServiceNow.params, subActionParams: { ...mockServiceNow.params.subActionParams, - caseId: 'success', + savedObjectId: 'success', title: 'success', createdAt: 'success', createdBy: { username: 'elastic' }, @@ -425,7 +402,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.commentId]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments.0.commentId]: expected value of type [string] but got [undefined]', }); }); }); @@ -439,7 +416,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { ...mockServiceNow.params, subActionParams: { ...mockServiceNow.params.subActionParams, - caseId: 'success', + savedObjectId: 'success', title: 'success', createdAt: 'success', createdBy: { username: 'elastic' }, @@ -453,35 +430,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.comment]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', - }); - }); - }); - - it('should handle failing with a simulated success without comment.createdAt', async () => { - await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...mockServiceNow.params, - subActionParams: { - ...mockServiceNow.params.subActionParams, - caseId: 'success', - title: 'success', - createdAt: 'success', - createdBy: { username: 'elastic' }, - comments: [{ commentId: 'success', comment: 'success' }], - }, - }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.createdAt]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments.0.comment]: expected value of type [string] but got [undefined]', }); }); }); From 610bff1269df5546261231f5acde686957061fea Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Mon, 6 Jul 2020 19:52:58 -0400 Subject: [PATCH 37/46] [Security Solution] Change default index pattern (#70797) * [Security Solution] Change default index pattern Add `logs-*` to the Security Solution default index pattern. This should allow the app to recognize events from the Elastic Endpoint. --- x-pack/plugins/security_solution/common/constants.ts | 1 + .../detection_engine/rules/fetch_index_patterns.test.tsx | 6 +++++- .../__snapshots__/drag_drop_context_wrapper.test.tsx.snap | 1 + .../event_details/__snapshots__/event_details.test.tsx.snap | 2 ++ .../public/common/containers/source/index.test.tsx | 6 ++++-- .../public/overview/components/overview_host/index.test.tsx | 1 + .../overview/components/overview_network/index.test.tsx | 1 + .../timeline/__snapshots__/timeline.test.tsx.snap | 1 + .../body/column_headers/__snapshots__/index.test.tsx.snap | 1 + .../__snapshots__/suricata_row_renderer.test.tsx.snap | 1 + .../renderers/zeek/__snapshots__/zeek_details.test.tsx.snap | 1 + .../zeek/__snapshots__/zeek_row_renderer.test.tsx.snap | 1 + 12 files changed, 20 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index d32d9f01d61ae..a34a76361f799 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -61,6 +61,7 @@ export const DEFAULT_INDEX_PATTERN = [ 'filebeat-*', 'packetbeat-*', 'winlogbeat-*', + 'logs-*', ]; /** This Kibana Advanced Setting enables the `Security news` feed widget */ diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/fetch_index_patterns.test.tsx b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/fetch_index_patterns.test.tsx index 79d5886f8845f..c282a204f19a5 100644 --- a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/fetch_index_patterns.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/fetch_index_patterns.test.tsx @@ -354,6 +354,7 @@ describe('useFetchIndexPatterns', () => { 'filebeat-*', 'packetbeat-*', 'winlogbeat-*', + 'logs-*', ], name: 'event.end', searchable: true, @@ -370,6 +371,7 @@ describe('useFetchIndexPatterns', () => { 'filebeat-*', 'packetbeat-*', 'winlogbeat-*', + 'logs-*', ], indicesExists: true, indexPatterns: { @@ -415,7 +417,8 @@ describe('useFetchIndexPatterns', () => { { name: 'source.port', searchable: true, type: 'long', aggregatable: true }, { name: 'event.end', searchable: true, type: 'date', aggregatable: true }, ], - title: 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,packetbeat-*,winlogbeat-*', + title: + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,packetbeat-*,winlogbeat-*,logs-*', }, }, result.current[1], @@ -449,6 +452,7 @@ describe('useFetchIndexPatterns', () => { 'filebeat-*', 'packetbeat-*', 'winlogbeat-*', + 'logs-*', ], indicesExists: false, isLoading: false, diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap index 07cbd6dfe0370..0c96d0320d198 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap @@ -371,6 +371,7 @@ exports[`DragDropContextWrapper rendering it renders against the snapshot 1`] = "filebeat-*", "packetbeat-*", "winlogbeat-*", + "logs-*", ], "name": "event.end", "searchable": true, diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap index 33ed6a8c87b5f..408a4c74e930f 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap @@ -379,6 +379,7 @@ exports[`EventDetails rendering should match snapshot 1`] = ` "filebeat-*", "packetbeat-*", "winlogbeat-*", + "logs-*", ], "name": "event.end", "searchable": true, @@ -1071,6 +1072,7 @@ In other use cases the message field can be used to concatenate different values "filebeat-*", "packetbeat-*", "winlogbeat-*", + "logs-*", ], "name": "event.end", "searchable": true, diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx index 69e4ac615ebf2..b9daba9a40941 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx @@ -28,7 +28,8 @@ describe('Index Fields & Browser Fields', () => { errorMessage: null, indexPattern: { fields: [], - title: 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,packetbeat-*,winlogbeat-*', + title: + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,packetbeat-*,winlogbeat-*,logs-*', }, indicesExist: true, loading: true, @@ -57,7 +58,8 @@ describe('Index Fields & Browser Fields', () => { browserFields: mockBrowserFields, indexPattern: { fields: mockIndexFields, - title: 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,packetbeat-*,winlogbeat-*', + title: + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,packetbeat-*,winlogbeat-*,logs-*', }, loading: false, errorMessage: null, diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx index 2b21385004a73..bb9fd73d2df8e 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx @@ -60,6 +60,7 @@ const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'filebeat-*', 'packetbeat-*', 'winlogbeat-*', + 'logs-*', ], inspect: false, }, diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx index 42c80b6b115bd..0f6fce1486ee7 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx @@ -75,6 +75,7 @@ const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'filebeat-*', 'packetbeat-*', 'winlogbeat-*', + 'logs-*', ], inspect: false, }, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap index 012cfd66317de..7baefaa6ab951 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap @@ -476,6 +476,7 @@ exports[`Timeline rendering renders correctly against snapshot 1`] = ` "filebeat-*", "packetbeat-*", "winlogbeat-*", + "logs-*", ], "name": "event.end", "searchable": true, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap index 9508e3f18a348..efd99e781d827 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap @@ -384,6 +384,7 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` "filebeat-*", "packetbeat-*", "winlogbeat-*", + "logs-*", ], "name": "event.end", "searchable": true, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap index 93b3046b57ed6..cba4b9aa72a25 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap @@ -373,6 +373,7 @@ exports[`suricata_row_renderer renders correctly against snapshot 1`] = ` "filebeat-*", "packetbeat-*", "winlogbeat-*", + "logs-*", ], "name": "event.end", "searchable": true, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap index 0a60c8facff9c..e1000637147a8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap @@ -371,6 +371,7 @@ exports[`ZeekDetails rendering it renders the default ZeekDetails 1`] = ` "filebeat-*", "packetbeat-*", "winlogbeat-*", + "logs-*", ], "name": "event.end", "searchable": true, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap index 460ad35b47678..d4c80441e6037 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap @@ -373,6 +373,7 @@ exports[`zeek_row_renderer renders correctly against snapshot 1`] = ` "filebeat-*", "packetbeat-*", "winlogbeat-*", + "logs-*", ], "name": "event.end", "searchable": true, From 438e905800feecd0b76dff18c75305a4355294d5 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Mon, 6 Jul 2020 17:35:47 -0700 Subject: [PATCH 38/46] Added UI validation when creating a Webhook connector with invalid URL (#70025) * Added UI validation when creating a Webhook connector with invalid URL * fixed tests * Fixed due to comments * fixed type check and extended error message for invalid URL * Fixed whitelisting of URL * fixed failing tests * fixed str --- .../builtin_action_types/webhook.test.ts | 11 ++++++++ .../server/builtin_action_types/webhook.ts | 14 +++++++++- .../webhook/webhook.test.tsx | 27 ++++++++++++++++++- .../builtin_action_types/webhook/webhook.tsx | 12 +++++++++ .../webhook/webhook_connectors.tsx | 1 + 5 files changed, 63 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts index 6daf15208f4d9..53b17f58d6e18 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -114,6 +114,17 @@ describe('config validation', () => { }); }); + test('config validation failed when a url is invalid', () => { + const config: Record = { + url: 'example.com/do-something', + }; + expect(() => { + validateConfig(actionType, config); + }).toThrowErrorMatchingInlineSnapshot( + '"error validating action type config: error configuring webhook action: unable to parse url: TypeError: Invalid URL: example.com/do-something"' + ); + }); + test('config validation passes when valid headers are provided', () => { // any for testing // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts index 4a34fea762164..0b8b27b278928 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts @@ -85,8 +85,20 @@ function validateActionTypeConfig( configurationUtilities: ActionsConfigurationUtilities, configObject: ActionTypeConfigType ) { + let url: URL; try { - configurationUtilities.ensureWhitelistedUri(configObject.url); + url = new URL(configObject.url); + } catch (err) { + return i18n.translate('xpack.actions.builtin.webhook.webhookConfigurationErrorNoHostname', { + defaultMessage: 'error configuring webhook action: unable to parse url: {err}', + values: { + err, + }, + }); + } + + try { + configurationUtilities.ensureWhitelistedUri(url.toString()); } catch (whitelistError) { return i18n.translate('xpack.actions.builtin.webhook.webhookConfigurationError', { defaultMessage: 'error configuring webhook action: {message}', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx index 3413465d70d93..337c1f0f18a93 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.test.tsx @@ -40,7 +40,7 @@ describe('webhook connector validation', () => { isPreconfigured: false, config: { method: 'PUT', - url: 'http:\\test', + url: 'http://test.com', headers: { 'content-type': 'text' }, }, } as WebhookActionConnector; @@ -77,6 +77,31 @@ describe('webhook connector validation', () => { }, }); }); + + test('connector validation fails when url in config is not valid', () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.webhook', + name: 'webhook', + config: { + method: 'PUT', + url: 'invalid.url', + }, + } as WebhookActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + url: ['URL is invalid.'], + method: [], + user: [], + password: [], + }, + }); + }); }); describe('webhook action params validation', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.tsx index 9f33e4491233a..2c51b21d70034 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook.tsx @@ -7,6 +7,7 @@ import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; import { ActionTypeModel, ValidationResult } from '../../../../types'; import { WebhookActionParams, WebhookActionConnector } from '../types'; +import { isValidUrl } from '../../../lib/value_validators'; export function getActionType(): ActionTypeModel { return { @@ -43,6 +44,17 @@ export function getActionType(): ActionTypeModel 0 && url !== undefined} fullWidth value={url || ''} + placeholder="https:// or http://" data-test-subj="webhookUrlText" onChange={(e) => { editActionConfig('url', e.target.value); From c5eab1021fe5f5502faa8c9e99a120df1dcc2351 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Mon, 6 Jul 2020 23:09:26 -0400 Subject: [PATCH 39/46] Revert "reenable regression and classification functional tests (#70661)" (#70908) This reverts commit a9b543d9bca049bda4fe03977f23de9561765873. --- .../apps/ml/data_frame_analytics/classification_creation.ts | 4 ++-- .../apps/ml/data_frame_analytics/regression_creation.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts index 5c750e119063e..4a79610cadbde 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts @@ -10,8 +10,8 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - - describe('classification creation', function () { + // flaky test https://github.com/elastic/kibana/issues/70455 + describe.skip('classification creation', function () { before(async () => { await esArchiver.loadIfNeeded('ml/bm_classification'); await ml.testResources.createIndexPatternIfNeeded('ft_bank_marketing', '@timestamp'); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts index 9ddf2dfc48269..33f0ee9cd99ac 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts @@ -10,8 +10,8 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - - describe('regression creation', function () { + // flaky test https://github.com/elastic/kibana/issues/70455 + describe.skip('regression creation', function () { before(async () => { await esArchiver.loadIfNeeded('ml/egs_regression'); await ml.testResources.createIndexPatternIfNeeded('ft_egs_regression', '@timestamp'); From 79e5a07bdc8333fa0a02d5605f957561746f6628 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 7 Jul 2020 05:16:58 +0100 Subject: [PATCH 40/46] skip flaky suite (#70906) --- .../apps/ml/data_frame_analytics/outlier_detection_creation.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts index 500825f7d9d31..65e6dc9b4ea74 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts @@ -11,7 +11,8 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - describe('outlier detection creation', function () { + // Flaky: https://github.com/elastic/kibana/issues/70906 + describe.skip('outlier detection creation', function () { before(async () => { await esArchiver.loadIfNeeded('ml/ihp_outlier'); await ml.testResources.createIndexPatternIfNeeded('ft_ihp_outlier', '@timestamp'); From 468201acf3bd4c8704c126cc9ddc84eece189f5a Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 7 Jul 2020 05:22:48 +0100 Subject: [PATCH 41/46] skip flaky suite (#67814) --- .../cypress/integration/alerts_detection_rules_custom.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts index 51c29c15a8097..684570450aa05 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts @@ -64,7 +64,8 @@ import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { ALERTS_URL } from '../urls/navigation'; -describe('Detection rules, custom', () => { +// Flaky: https://github.com/elastic/kibana/issues/67814 +describe.skip('Detection rules, custom', () => { before(() => { esArchiverLoad('custom_rule_with_timeline'); }); From f62f3e372786b1dde84e9b3b7972458da4501f22 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 7 Jul 2020 05:34:02 +0100 Subject: [PATCH 42/46] skip flaky suite (#70885) --- x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts index 068ef48b095e1..e2f7960f9d856 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts @@ -13,7 +13,8 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - describe('jobs cloning supported by UI form', function () { + // Flaky: https://github.com/elastic/kibana/issues/70885 + describe.skip('jobs cloning supported by UI form', function () { const testDataList: Array<{ suiteTitle: string; archive: string; From 4257afad1b65dc8ff715d99e7a325fa55c2d3e53 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Tue, 7 Jul 2020 07:27:12 +0200 Subject: [PATCH 43/46] Adapt expected response of advanced settings feature control for cloud tests (#70793) --- .../advanced_settings/feature_controls.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/x-pack/test/api_integration/apis/management/advanced_settings/feature_controls.ts b/x-pack/test/api_integration/apis/management/advanced_settings/feature_controls.ts index 77e23bd74cc22..7a0d0fe2f5d48 100644 --- a/x-pack/test/api_integration/apis/management/advanced_settings/feature_controls.ts +++ b/x-pack/test/api_integration/apis/management/advanced_settings/feature_controls.ts @@ -21,9 +21,16 @@ export default function featureControlsTests({ getService }: FtrProviderContext) }; const expectResponse = (result: any) => { - expect(result.error).to.be(undefined); - expect(result.response).not.to.be(undefined); - expect(result.response).to.have.property('statusCode', 200); + if (result.response && result.response.statusCode === 400) { + // expect a change of telemetry settings to fail in cloud environment + expect(result.response.body.message).to.be( + '{"error":"Not allowed to change Opt-in Status."}' + ); + } else { + expect(result.error).to.be(undefined); + expect(result.response).not.to.be(undefined); + expect(result.response).to.have.property('statusCode', 200); + } }; async function saveAdvancedSetting(username: string, password: string, spaceId?: string) { From dfeb60b5ee8c116564d8fcd796b73b36e724a9fa Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Tue, 7 Jul 2020 09:21:00 +0200 Subject: [PATCH 44/46] moving indexPattern.delete() to indexPatterns.delete(indexPattern) (#70430) --- ...lugins-data-public.indexpattern.destroy.md | 15 ----------- ...plugin-plugins-data-public.indexpattern.md | 1 - .../index_patterns/index_pattern.ts | 26 +++---------------- .../index_patterns/index_patterns.test.ts | 10 +++++++ .../index_patterns/index_patterns.ts | 9 +++++++ src/plugins/data/public/public.api.md | 2 -- .../edit_index_pattern/edit_index_pattern.tsx | 20 +++++++++----- .../plugins/index_patterns/server/plugin.ts | 3 +-- 8 files changed, 36 insertions(+), 50 deletions(-) delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.destroy.md diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.destroy.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.destroy.md deleted file mode 100644 index 3a8e1b9dae5a6..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.destroy.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [destroy](./kibana-plugin-plugins-data-public.indexpattern.destroy.md) - -## IndexPattern.destroy() method - -Signature: - -```typescript -destroy(): Promise<{}> | undefined; -``` -Returns: - -`Promise<{}> | undefined` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index bc999a3bb48e3..a37f115358922 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -39,7 +39,6 @@ export declare class IndexPattern implements IIndexPattern | [\_fetchFields()](./kibana-plugin-plugins-data-public.indexpattern._fetchfields.md) | | | | [addScriptedField(name, script, fieldType, lang)](./kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md) | | | | [create(allowOverride)](./kibana-plugin-plugins-data-public.indexpattern.create.md) | | | -| [destroy()](./kibana-plugin-plugins-data-public.indexpattern.destroy.md) | | | | [getAggregationRestrictions()](./kibana-plugin-plugins-data-public.indexpattern.getaggregationrestrictions.md) | | | | [getComputedFields()](./kibana-plugin-plugins-data-public.indexpattern.getcomputedfields.md) | | | | [getFieldByName(name)](./kibana-plugin-plugins-data-public.indexpattern.getfieldbyname.md) | | | diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index dab11ad0ce29a..2acb9d5f767ad 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -224,7 +224,7 @@ export class IndexPattern implements IIndexPattern { this.sourceFilters = spec.sourceFilters; // ignoring this because the same thing happens elsewhere but via _.assign - // @ts-ignore + // @ts-expect-error this.fields = spec.fields || []; this.typeMeta = spec.typeMeta; this.fieldFormatMap = _.mapValues(fieldFormatMap, (mapping) => { @@ -473,21 +473,8 @@ export class IndexPattern implements IIndexPattern { async create(allowOverride: boolean = false) { const _create = async (duplicateId?: string) => { if (duplicateId) { - const duplicatePattern = new IndexPattern(duplicateId, { - getConfig: this.getConfig, - savedObjectsClient: this.savedObjectsClient, - apiClient: this.apiClient, - patternCache: this.patternCache, - fieldFormats: this.fieldFormats, - onNotification: this.onNotification, - onError: this.onError, - uiSettingsValues: { - shortDotsEnable: this.shortDotsEnable, - metaFields: this.metaFields, - }, - }); - - await duplicatePattern.destroy(); + this.patternCache.clear(duplicateId); + await this.savedObjectsClient.delete(savedObjectType, duplicateId); } const body = this.prepBody(); @@ -634,11 +621,4 @@ export class IndexPattern implements IIndexPattern { toString() { return '' + this.toJSON(); } - - destroy() { - if (this.id) { - this.patternCache.clear(this.id); - return this.savedObjectsClient.delete(savedObjectType, this.id); - } - } } diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts index 2eb9744fc16b3..a1842d31479c0 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts @@ -53,6 +53,7 @@ describe('IndexPatterns', () => { Array> > ); + savedObjectsClient.delete = jest.fn(() => Promise.resolve({}) as Promise); indexPatterns = new IndexPatternsService({ uiSettings: ({ @@ -98,4 +99,13 @@ describe('IndexPatterns', () => { await indexPatterns.getFields(['id', 'title'], true); expect(savedObjectsClient.find).toHaveBeenCalledTimes(3); }); + + test('deletes the index pattern', async () => { + const id = '1'; + const indexPattern = await indexPatterns.get(id); + + expect(indexPattern).toBeDefined(); + await indexPatterns.delete(id); + expect(indexPattern).not.toBe(await indexPatterns.get(id)); + }); }); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index ef03ca8fe2d14..a07ffaf92aea5 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -228,6 +228,15 @@ export class IndexPatternsService { return indexPattern.init(); } + + /** + * Deletes an index pattern from .kibana index + * @param indexPatternId: Id of kibana Index Pattern to delete + */ + async delete(indexPatternId: string) { + indexPatternCache.clear(indexPatternId); + return this.savedObjectsClient.delete('index-pattern', indexPatternId); + } } export type IndexPatternsContract = PublicMethodsOf; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 670b40e7d9472..2b18584bcd781 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -988,8 +988,6 @@ export class IndexPattern implements IIndexPattern { // (undocumented) create(allowOverride?: boolean): Promise; // (undocumented) - destroy(): Promise<{}> | undefined; - // (undocumented) _fetchFields(): Promise; // (undocumented) fieldFormatMap: any; diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx index eab8b2c231c9c..090c72d319f8c 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -83,9 +83,14 @@ const confirmModalOptionsDelete = { export const EditIndexPattern = withRouter( ({ indexPattern, history, location }: EditIndexPatternProps) => { - const { uiSettings, indexPatternManagementStart, overlays, savedObjects, chrome } = useKibana< - IndexPatternManagmentContext - >().services; + const { + uiSettings, + indexPatternManagementStart, + overlays, + savedObjects, + chrome, + data, + } = useKibana().services; const [fields, setFields] = useState(indexPattern.getNonScriptedFields()); const [conflictedFields, setConflictedFields] = useState( indexPattern.fields.filter((field) => field.type === 'conflict') @@ -138,10 +143,11 @@ export const EditIndexPattern = withRouter( uiSettings.set('defaultIndex', otherPatterns[0].id); } } - - Promise.resolve(indexPattern.destroy()).then(function () { - history.push(''); - }); + if (indexPattern.id) { + Promise.resolve(data.indexPatterns.delete(indexPattern.id)).then(function () { + history.push(''); + }); + } } overlays.openConfirm('', confirmModalOptionsDelete).then((isConfirmed) => { diff --git a/test/plugin_functional/plugins/index_patterns/server/plugin.ts b/test/plugin_functional/plugins/index_patterns/server/plugin.ts index ffc70136ccffa..d6a4fdd67b0a1 100644 --- a/test/plugin_functional/plugins/index_patterns/server/plugin.ts +++ b/test/plugin_functional/plugins/index_patterns/server/plugin.ts @@ -96,8 +96,7 @@ export class IndexPatternsTestPlugin const [, { data }] = await core.getStartServices(); const id = (req.params as Record).id; const service = await data.indexPatterns.indexPatternsServiceFactory(req); - const ip = await service.get(id); - await ip.destroy(); + await service.delete(id); return res.ok(); } ); From 77e40199b80254896766fe284ca074a8ef80742e Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 7 Jul 2020 09:22:09 +0200 Subject: [PATCH 45/46] [Uptime] Ping list body scroll (#70781) --- .../uptime/public/components/monitor/ping_list/expanded_row.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx index e8ce3465f6fd8..67bef3e72929e 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx @@ -47,7 +47,7 @@ const BodyDescription = ({ body }: { body: HttpResponseBody }) => { }; const BodyExcerpt = ({ content }: { content: string }) => - content ? {content} : null; + content ? {content} : null; export const PingListExpandedRowComponent = ({ ping }: Props) => { const listItems = []; From 053b922b7cacc264c2d6ec9fdb909dd893266261 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Tue, 7 Jul 2020 09:58:00 +0200 Subject: [PATCH 46/46] [Composable template] Details panel + delete functionality (#70814) --- .../home/index_templates_tab.helpers.ts | 16 +- .../home/index_templates_tab.test.ts | 187 ++++++++-- .../common/lib/template_serialization.ts | 22 +- .../common/types/templates.ts | 6 + .../components/template_content_indicator.tsx | 12 +- .../template_form/template_form.tsx | 2 + .../template_details/index.ts | 7 - .../template_details/template_details.tsx | 330 ------------------ .../template_table/template_table.tsx | 21 +- .../template_details/tabs/tab_summary.tsx | 244 +++++++++---- .../template_details/template_details.tsx | 18 +- .../template_details_content.tsx | 324 +++++++++++++++++ .../home/template_list/template_list.tsx | 9 +- .../template_table/template_table.tsx | 166 +++++++-- .../sections/template_edit/template_edit.tsx | 4 +- .../server/lib/get_managed_templates.ts | 2 +- .../api/templates/register_delete_route.ts | 15 +- .../api/templates/register_get_routes.ts | 14 +- .../routes/api/templates/validate_schemas.ts | 2 + .../test/fixtures/template.ts | 6 + .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - .../management/index_management/templates.js | 32 ++ 23 files changed, 921 insertions(+), 522 deletions(-) delete mode 100644 x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/index.ts delete mode 100644 x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx create mode 100644 x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts index 5eb4eaf6e2ca1..0047e4c0294cb 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts @@ -51,12 +51,15 @@ const createActions = (testBed: TestBed) => { find('reloadButton').simulate('click'); }; - const clickActionMenu = async (templateName: TemplateDeserialized['name']) => { + const clickActionMenu = (templateName: TemplateDeserialized['name']) => { const { component } = testBed; // When a table has > 2 actions, EUI displays an overflow menu with an id "-actions" // The template name may contain a period (.) so we use bracket syntax for selector - component.find(`div[id="${templateName}-actions"] button`).simulate('click'); + act(() => { + component.find(`div[id="${templateName}-actions"] button`).simulate('click'); + }); + component.update(); }; const clickTemplateAction = ( @@ -68,12 +71,15 @@ const createActions = (testBed: TestBed) => { clickActionMenu(templateName); - component.find('.euiContextMenuItem').at(actions.indexOf(action)).simulate('click'); + act(() => { + component.find('.euiContextMenuItem').at(actions.indexOf(action)).simulate('click'); + }); + component.update(); }; - const clickTemplateAt = async (index: number) => { + const clickTemplateAt = async (index: number, isLegacy = false) => { const { component, table, router } = testBed; - const { rows } = table.getMetaData('legacyTemplateTable'); + const { rows } = table.getMetaData(isLegacy ? 'legacyTemplateTable' : 'templateTable'); const templateLink = findTestSubject(rows[index].reactWrapper, 'templateDetailsLink'); const { href } = templateLink.props(); diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts index fb3e16e5345cb..1ec29f1c5b894 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts @@ -63,6 +63,7 @@ describe('Index Templates tab', () => { }, }, }); + (template1 as any).hasSettings = true; const template2 = fixtures.getTemplate({ name: `b${getRandomString()}`, @@ -122,20 +123,22 @@ describe('Index Templates tab', () => { // Test composable table content tableCellsValues.forEach((row, i) => { - const template = templates[i]; - const { name, indexPatterns, priority, ilmPolicy, composedOf } = template; + const indexTemplate = templates[i]; + const { name, indexPatterns, priority, ilmPolicy, composedOf, template } = indexTemplate; + const hasContent = !!template.settings || !!template.mappings || !!template.aliases; const ilmPolicyName = ilmPolicy && ilmPolicy.name ? ilmPolicy.name : ''; const composedOfString = composedOf ? composedOf.join(',') : ''; const priorityFormatted = priority ? priority.toString() : ''; expect(removeWhiteSpaceOnArrayValues(row)).toEqual([ + '', // Checkbox to select row name, indexPatterns.join(', '), ilmPolicyName, composedOfString, priorityFormatted, - 'M S A', // Mappings Settings Aliases badges + hasContent ? 'M S A' : 'None', // M S A -> Mappings Settings Aliases badges '', // Column of actions ]); }); @@ -202,52 +205,101 @@ describe('Index Templates tab', () => { }); test('each row should have a link to the template details panel', async () => { - const { find, exists, actions } = testBed; + const { find, exists, actions, component } = testBed; + // Composable templates await actions.clickTemplateAt(0); + expect(exists('templateList')).toBe(true); + expect(exists('templateDetails')).toBe(true); + expect(find('templateDetails.title').text()).toBe(templates[0].name); + + // Close flyout + await act(async () => { + actions.clickCloseDetailsButton(); + }); + component.update(); + + await actions.clickTemplateAt(0, true); expect(exists('templateList')).toBe(true); expect(exists('templateDetails')).toBe(true); expect(find('templateDetails.title').text()).toBe(legacyTemplates[0].name); }); - test('template actions column should have an option to delete', () => { - const { actions, findAction } = testBed; - const [{ name: templateName }] = legacyTemplates; + describe('table row actions', () => { + describe('composable templates', () => { + test('should have an option to delete', () => { + const { actions, findAction } = testBed; + const [{ name: templateName }] = templates; - actions.clickActionMenu(templateName); + actions.clickActionMenu(templateName); - const deleteAction = findAction('delete'); + const deleteAction = findAction('delete'); + expect(deleteAction.text()).toEqual('Delete'); + }); - expect(deleteAction.text()).toEqual('Delete'); - }); + test('should have an option to clone', () => { + const { actions, findAction } = testBed; + const [{ name: templateName }] = templates; - test('template actions column should have an option to clone', () => { - const { actions, findAction } = testBed; - const [{ name: templateName }] = legacyTemplates; + actions.clickActionMenu(templateName); - actions.clickActionMenu(templateName); + const cloneAction = findAction('clone'); - const cloneAction = findAction('clone'); + expect(cloneAction.text()).toEqual('Clone'); + }); - expect(cloneAction.text()).toEqual('Clone'); - }); + test('should have an option to edit', () => { + const { actions, findAction } = testBed; + const [{ name: templateName }] = templates; + + actions.clickActionMenu(templateName); - test('template actions column should have an option to edit', () => { - const { actions, findAction } = testBed; - const [{ name: templateName }] = legacyTemplates; + const editAction = findAction('edit'); + + expect(editAction.text()).toEqual('Edit'); + }); + }); + + describe('legacy templates', () => { + test('should have an option to delete', () => { + const { actions, findAction } = testBed; + const [{ name: legacyTemplateName }] = legacyTemplates; + + actions.clickActionMenu(legacyTemplateName); + + const deleteAction = findAction('delete'); + expect(deleteAction.text()).toEqual('Delete'); + }); + + test('should have an option to clone', () => { + const { actions, findAction } = testBed; + const [{ name: templateName }] = legacyTemplates; + + actions.clickActionMenu(templateName); + + const cloneAction = findAction('clone'); + + expect(cloneAction.text()).toEqual('Clone'); + }); - actions.clickActionMenu(templateName); + test('should have an option to edit', () => { + const { actions, findAction } = testBed; + const [{ name: templateName }] = legacyTemplates; - const editAction = findAction('edit'); + actions.clickActionMenu(templateName); - expect(editAction.text()).toEqual('Edit'); + const editAction = findAction('edit'); + + expect(editAction.text()).toEqual('Edit'); + }); + }); }); describe('delete index template', () => { test('should show a confirmation when clicking the delete template button', async () => { const { actions } = testBed; - const [{ name: templateName }] = legacyTemplates; + const [{ name: templateName }] = templates; await actions.clickTemplateAction(templateName, 'delete'); @@ -267,24 +319,29 @@ describe('Index Templates tab', () => { actions.toggleViewItem('system'); - const { name: systemTemplateName } = legacyTemplates[2]; + const { name: systemTemplateName } = templates[2]; await actions.clickTemplateAction(systemTemplateName, 'delete'); expect(exists('deleteSystemTemplateCallOut')).toBe(true); }); test('should send the correct HTTP request to delete an index template', async () => { - const { actions, table } = testBed; - const { rows } = table.getMetaData('legacyTemplateTable'); - - const templateId = rows[0].columns[2].value; + const { actions } = testBed; const [ { name: templateName, _kbnMeta: { isLegacy }, }, - ] = legacyTemplates; + ] = templates; + + httpRequestsMockHelpers.setDeleteTemplateResponse({ + results: { + successes: [templateName], + errors: [], + }, + }); + await actions.clickTemplateAction(templateName, 'delete'); const modal = document.body.querySelector('[data-test-subj="deleteTemplatesConfirmation"]'); @@ -292,13 +349,68 @@ describe('Index Templates tab', () => { '[data-test-subj="confirmModalConfirmButton"]' ); + await act(async () => { + confirmButton!.click(); + }); + + const latestRequest = server.requests[server.requests.length - 1]; + + expect(latestRequest.method).toBe('POST'); + expect(latestRequest.url).toBe(`${API_BASE_PATH}/delete_index_templates`); + expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual({ + templates: [{ name: templates[0].name, isLegacy }], + }); + }); + }); + + describe('delete legacy index template', () => { + test('should show a confirmation when clicking the delete template button', async () => { + const { actions } = testBed; + const [{ name: templateName }] = legacyTemplates; + + await actions.clickTemplateAction(templateName, 'delete'); + + // We need to read the document "body" as the modal is added there and not inside + // the component DOM tree. + expect( + document.body.querySelector('[data-test-subj="deleteTemplatesConfirmation"]') + ).not.toBe(null); + + expect( + document.body.querySelector('[data-test-subj="deleteTemplatesConfirmation"]')!.textContent + ).toContain('Delete template'); + }); + + test('should show a warning message when attempting to delete a system template', async () => { + const { exists, actions } = testBed; + + actions.toggleViewItem('system'); + + const { name: systemTemplateName } = legacyTemplates[2]; + await actions.clickTemplateAction(systemTemplateName, 'delete'); + + expect(exists('deleteSystemTemplateCallOut')).toBe(true); + }); + + test('should send the correct HTTP request to delete an index template', async () => { + const { actions } = testBed; + + const [{ name: templateName }] = legacyTemplates; + httpRequestsMockHelpers.setDeleteTemplateResponse({ results: { - successes: [templateId], + successes: [templateName], errors: [], }, }); + await actions.clickTemplateAction(templateName, 'delete'); + + const modal = document.body.querySelector('[data-test-subj="deleteTemplatesConfirmation"]'); + const confirmButton: HTMLButtonElement | null = modal!.querySelector( + '[data-test-subj="confirmModalConfirmButton"]' + ); + await act(async () => { confirmButton!.click(); }); @@ -307,9 +419,12 @@ describe('Index Templates tab', () => { expect(latestRequest.method).toBe('POST'); expect(latestRequest.url).toBe(`${API_BASE_PATH}/delete_index_templates`); - expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual({ - templates: [{ name: legacyTemplates[0].name, isLegacy }], - }); + + // Commenting as I don't find a way to make it work. + // It keeps on returning the composable template instead of the legacy one + // expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual({ + // templates: [{ name: templateName, isLegacy }], + // }); }); }); @@ -343,7 +458,7 @@ describe('Index Templates tab', () => { test('should set the correct title', async () => { const { find } = testBed; - const [{ name }] = legacyTemplates; + const [{ name }] = templates; expect(find('templateDetails.title').text()).toEqual(name); }); diff --git a/x-pack/plugins/index_management/common/lib/template_serialization.ts b/x-pack/plugins/index_management/common/lib/template_serialization.ts index 608a8b8aca294..5c55860bda81b 100644 --- a/x-pack/plugins/index_management/common/lib/template_serialization.ts +++ b/x-pack/plugins/index_management/common/lib/template_serialization.ts @@ -27,7 +27,7 @@ export function serializeTemplate(templateDeserialized: TemplateDeserialized): T export function deserializeTemplate( templateEs: TemplateSerialized & { name: string }, - managedTemplatePrefix?: string + cloudManagedTemplatePrefix?: string ): TemplateDeserialized { const { name, @@ -37,6 +37,7 @@ export function deserializeTemplate( priority, _meta, composed_of: composedOf, + data_stream: dataStream, } = templateEs; const { settings } = template; @@ -48,9 +49,14 @@ export function deserializeTemplate( template, ilmPolicy: settings?.index?.lifecycle, composedOf, + dataStream, _meta, _kbnMeta: { - isManaged: Boolean(managedTemplatePrefix && name.startsWith(managedTemplatePrefix)), + isManaged: Boolean(_meta?.managed === true), + isCloudManaged: Boolean( + cloudManagedTemplatePrefix && name.startsWith(cloudManagedTemplatePrefix) + ), + hasDatastream: Boolean(dataStream), }, }; @@ -59,13 +65,13 @@ export function deserializeTemplate( export function deserializeTemplateList( indexTemplates: Array<{ name: string; index_template: TemplateSerialized }>, - managedTemplatePrefix?: string + cloudManagedTemplatePrefix?: string ): TemplateListItem[] { return indexTemplates.map(({ name, index_template: templateSerialized }) => { const { template: { mappings, settings, aliases }, ...deserializedTemplate - } = deserializeTemplate({ name, ...templateSerialized }, managedTemplatePrefix); + } = deserializeTemplate({ name, ...templateSerialized }, cloudManagedTemplatePrefix); return { ...deserializedTemplate, @@ -102,13 +108,13 @@ export function serializeLegacyTemplate(template: TemplateDeserialized): LegacyT export function deserializeLegacyTemplate( templateEs: LegacyTemplateSerialized & { name: string }, - managedTemplatePrefix?: string + cloudManagedTemplatePrefix?: string ): TemplateDeserialized { const { settings, aliases, mappings, ...rest } = templateEs; const deserializedTemplate = deserializeTemplate( { ...rest, template: { aliases, settings, mappings } }, - managedTemplatePrefix + cloudManagedTemplatePrefix ); return { @@ -123,13 +129,13 @@ export function deserializeLegacyTemplate( export function deserializeLegacyTemplateList( indexTemplatesByName: { [key: string]: LegacyTemplateSerialized }, - managedTemplatePrefix?: string + cloudManagedTemplatePrefix?: string ): TemplateListItem[] { return Object.entries(indexTemplatesByName).map(([name, templateSerialized]) => { const { template: { mappings, settings, aliases }, ...deserializedTemplate - } = deserializeLegacyTemplate({ name, ...templateSerialized }, managedTemplatePrefix); + } = deserializeLegacyTemplate({ name, ...templateSerialized }, cloudManagedTemplatePrefix); return { ...deserializedTemplate, diff --git a/x-pack/plugins/index_management/common/types/templates.ts b/x-pack/plugins/index_management/common/types/templates.ts index 14318b5fa2a8d..fdcac40ca596f 100644 --- a/x-pack/plugins/index_management/common/types/templates.ts +++ b/x-pack/plugins/index_management/common/types/templates.ts @@ -22,6 +22,7 @@ export interface TemplateSerialized { version?: number; priority?: number; _meta?: { [key: string]: any }; + data_stream?: { timestamp_field: string }; } /** @@ -45,8 +46,11 @@ export interface TemplateDeserialized { name: string; }; _meta?: { [key: string]: any }; + dataStream?: { timestamp_field: string }; _kbnMeta: { isManaged: boolean; + isCloudManaged: boolean; + hasDatastream: boolean; isLegacy?: boolean; }; } @@ -75,6 +79,8 @@ export interface TemplateListItem { }; _kbnMeta: { isManaged: boolean; + isCloudManaged: boolean; + hasDatastream: boolean; isLegacy?: boolean; }; } diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/template_content_indicator.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/template_content_indicator.tsx index 78e33d7940bd4..20cbff7047810 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/template_content_indicator.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/template_content_indicator.tsx @@ -12,6 +12,7 @@ interface Props { mappings: boolean; settings: boolean; aliases: boolean; + contentWhenEmpty?: JSX.Element | null; } const texts = { @@ -26,9 +27,18 @@ const texts = { }), }; -export const TemplateContentIndicator = ({ mappings, settings, aliases }: Props) => { +export const TemplateContentIndicator = ({ + mappings, + settings, + aliases, + contentWhenEmpty = null, +}: Props) => { const getColor = (flag: boolean) => (flag ? 'primary' : 'hollow'); + if (!mappings && !settings && !aliases) { + return contentWhenEmpty; + } + return ( <> diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx index 269ad94251074..6310ac09488e5 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx @@ -99,6 +99,8 @@ export const TemplateForm = ({ }, _kbnMeta: { isManaged: false, + isCloudManaged: false, + hasDatastream: false, isLegacy, }, }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/index.ts b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/index.ts deleted file mode 100644 index 519120b559e7b..0000000000000 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { LegacyTemplateDetails } from './template_details'; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx deleted file mode 100644 index f85b14ea0d2d5..0000000000000 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_details/template_details.tsx +++ /dev/null @@ -1,330 +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, { Fragment, useState } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { - EuiCallOut, - EuiFlyout, - EuiFlyoutHeader, - EuiTitle, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiTab, - EuiTabs, - EuiSpacer, - EuiPopover, - EuiButton, - EuiContextMenu, -} from '@elastic/eui'; -import { - UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, - UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, - UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, - UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, -} from '../../../../../../../common/constants'; -import { - TemplateDeleteModal, - SectionLoading, - SectionError, - Error, -} from '../../../../../components'; -import { useLoadIndexTemplate } from '../../../../../services/api'; -import { decodePathFromReactRouter } from '../../../../../services/routing'; -import { SendRequestResponse } from '../../../../../../shared_imports'; -import { useServices } from '../../../../../app_context'; -import { TabAliases, TabMappings, TabSettings } from '../../../../../components/shared'; -import { TabSummary } from '../../template_details/tabs'; - -interface Props { - template: { name: string; isLegacy?: boolean }; - onClose: () => void; - editTemplate: (name: string, isLegacy: boolean) => void; - cloneTemplate: (name: string, isLegacy?: boolean) => void; - reload: () => Promise; -} - -const SUMMARY_TAB_ID = 'summary'; -const MAPPINGS_TAB_ID = 'mappings'; -const ALIASES_TAB_ID = 'aliases'; -const SETTINGS_TAB_ID = 'settings'; - -const TABS = [ - { - id: SUMMARY_TAB_ID, - name: i18n.translate('xpack.idxMgmt.legacyTemplateDetails.summaryTabTitle', { - defaultMessage: 'Summary', - }), - }, - { - id: SETTINGS_TAB_ID, - name: i18n.translate('xpack.idxMgmt.legacyTemplateDetails.settingsTabTitle', { - defaultMessage: 'Settings', - }), - }, - { - id: MAPPINGS_TAB_ID, - name: i18n.translate('xpack.idxMgmt.legacyTemplateDetails.mappingsTabTitle', { - defaultMessage: 'Mappings', - }), - }, - { - id: ALIASES_TAB_ID, - name: i18n.translate('xpack.idxMgmt.legacyTemplateDetails.aliasesTabTitle', { - defaultMessage: 'Aliases', - }), - }, -]; - -const tabToUiMetricMap: { [key: string]: string } = { - [SUMMARY_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, - [SETTINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, - [MAPPINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, - [ALIASES_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, -}; - -export const LegacyTemplateDetails: React.FunctionComponent = ({ - template: { name: templateName, isLegacy }, - onClose, - editTemplate, - cloneTemplate, - reload, -}) => { - const { uiMetricService } = useServices(); - const decodedTemplateName = decodePathFromReactRouter(templateName); - const { error, data: templateDetails, isLoading } = useLoadIndexTemplate( - decodedTemplateName, - isLegacy - ); - const isManaged = templateDetails?._kbnMeta.isManaged ?? false; - const [templateToDelete, setTemplateToDelete] = useState< - Array<{ name: string; isLegacy?: boolean }> - >([]); - const [activeTab, setActiveTab] = useState(SUMMARY_TAB_ID); - const [isPopoverOpen, setIsPopOverOpen] = useState(false); - - let content; - - if (isLoading) { - content = ( - - - - ); - } else if (error) { - content = ( - - } - error={error as Error} - data-test-subj="sectionError" - /> - ); - } else if (templateDetails) { - const { - template: { settings, mappings, aliases }, - } = templateDetails; - - const tabToComponentMap: Record = { - [SUMMARY_TAB_ID]: , - [SETTINGS_TAB_ID]: , - [MAPPINGS_TAB_ID]: , - [ALIASES_TAB_ID]: , - }; - - const tabContent = tabToComponentMap[activeTab]; - - const managedTemplateCallout = isManaged ? ( - - - } - color="primary" - size="s" - > - - - - - ) : null; - - content = ( - - {managedTemplateCallout} - - - {TABS.map((tab) => ( - { - uiMetricService.trackMetric('click', tabToUiMetricMap[tab.id]); - setActiveTab(tab.id); - }} - isSelected={tab.id === activeTab} - key={tab.id} - data-test-subj="tab" - > - {tab.name} - - ))} - - - - - {tabContent} - - ); - } - - return ( - - {templateToDelete && templateToDelete.length > 0 ? ( - { - if (data && data.hasDeletedTemplates) { - reload(); - } else { - setTemplateToDelete([]); - } - onClose(); - }} - templatesToDelete={templateToDelete} - /> - ) : null} - - - - -

- {decodedTemplateName} -

-
-
- - {content} - - - - - - - - - {templateDetails && ( - - {/* Manage templates context menu */} - setIsPopOverOpen((prev) => !prev)} - > - -
- } - isOpen={isPopoverOpen} - closePopover={() => setIsPopOverOpen(false)} - panelPaddingSize="none" - withTitle - anchorPosition="rightUp" - repositionOnScroll - > - editTemplate(templateName, true), - disabled: isManaged, - }, - { - name: i18n.translate( - 'xpack.idxMgmt.legacyTemplateDetails.cloneButtonLabel', - { - defaultMessage: 'Clone', - } - ), - icon: 'copy', - onClick: () => cloneTemplate(templateName, isLegacy), - }, - { - name: i18n.translate( - 'xpack.idxMgmt.legacyTemplateDetails.deleteButtonLabel', - { - defaultMessage: 'Delete', - } - ), - icon: 'trash', - onClick: () => - setTemplateToDelete([{ name: decodedTemplateName, isLegacy }]), - disabled: isManaged, - }, - ], - }, - ]} - /> - -
- )} -
- - - - ); -}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx index 99915c2b70e2a..b470bcfd7660e 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx @@ -19,7 +19,7 @@ import { useServices } from '../../../../../app_context'; interface Props { templates: TemplateListItem[]; reload: () => Promise; - editTemplate: (name: string, isLegacy: boolean) => void; + editTemplate: (name: string, isLegacy?: boolean) => void; cloneTemplate: (name: string, isLegacy?: boolean) => void; history: ScopedHistory; } @@ -153,7 +153,7 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ onClick: ({ name }: TemplateListItem) => { editTemplate(name, true); }, - enabled: ({ _kbnMeta: { isManaged } }: TemplateListItem) => !isManaged, + enabled: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged, }, { type: 'icon', @@ -167,8 +167,8 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ } ), icon: 'copy', - onClick: ({ name, _kbnMeta: { isLegacy } }: TemplateListItem) => { - cloneTemplate(name, isLegacy); + onClick: ({ name }: TemplateListItem) => { + cloneTemplate(name, true); }, }, { @@ -188,7 +188,7 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ setTemplatesToDelete([{ name, isLegacy }]); }, isPrimary: true, - enabled: ({ _kbnMeta: { isManaged } }: TemplateListItem) => !isManaged, + enabled: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged, }, ], }, @@ -208,7 +208,7 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ const selectionConfig = { onSelectionChange: setSelection, - selectable: ({ _kbnMeta: { isManaged } }: TemplateListItem) => !isManaged, + selectable: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged, selectableMessage: (selectable: boolean) => { if (!selectable) { return i18n.translate( @@ -265,6 +265,10 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ ], }; + const goToList = () => { + return history.push('templates'); + }; + return ( {templatesToDelete && templatesToDelete.length > 0 ? ( @@ -272,9 +276,10 @@ export const LegacyTemplateTable: React.FunctionComponent = ({ callback={(data) => { if (data && data.hasDeletedTemplates) { reload(); - } else { - setTemplatesToDelete([]); + // Close the flyout if it is opened + goToList(); } + setTemplatesToDelete([]); }} templatesToDelete={templatesToDelete} /> diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx index 9ce29ab746a2f..fe6c9ad3d8e07 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx @@ -5,6 +5,7 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiDescriptionList, @@ -13,6 +14,9 @@ import { EuiLink, EuiText, EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiCodeBlock, } from '@elastic/eui'; import { TemplateDeserialized } from '../../../../../../../common'; import { getILMPolicyPath } from '../../../../../services/navigation'; @@ -21,84 +25,184 @@ interface Props { templateDetails: TemplateDeserialized; } -const NoneDescriptionText = () => ( - -); +const i18nTexts = { + yes: i18n.translate('xpack.idxMgmt.templateDetails.summaryTab.yesDescriptionText', { + defaultMessage: 'Yes', + }), + no: i18n.translate('xpack.idxMgmt.templateDetails.summaryTab.noDescriptionText', { + defaultMessage: 'No', + }), + none: i18n.translate('xpack.idxMgmt.templateDetails.summaryTab.noneDescriptionText', { + defaultMessage: 'None', + }), +}; export const TabSummary: React.FunctionComponent = ({ templateDetails }) => { - const { version, order, indexPatterns = [], ilmPolicy } = templateDetails; + const { + version, + priority, + composedOf, + order, + indexPatterns = [], + ilmPolicy, + _meta, + _kbnMeta: { isLegacy, hasDatastream }, + } = templateDetails; const numIndexPatterns = indexPatterns.length; return ( - - {/* Index patterns */} - - - - - {numIndexPatterns > 1 ? ( - -
    - {indexPatterns.map((indexName: string, i: number) => { - return ( -
  • - - {indexName} - -
  • - ); - })} -
-
- ) : ( - indexPatterns.toString() - )} -
+ + + + {/* Index patterns */} + + + + + {numIndexPatterns > 1 ? ( + +
    + {indexPatterns.map((indexName: string, i: number) => { + return ( +
  • + + {indexName} + +
  • + ); + })} +
+
+ ) : ( + indexPatterns.toString() + )} +
+ + {/* Priority / Order */} + {isLegacy !== true ? ( + <> + + + + + {priority || priority === 0 ? priority : i18nTexts.none} + + + ) : ( + <> + + + + + {order || order === 0 ? order : i18nTexts.none} + + + )} + + {/* Components */} + {isLegacy !== true && ( + <> + + + + + {composedOf && composedOf.length > 0 ? ( +
    + {composedOf.map((component) => ( +
  • + + {component} + +
  • + ))} +
+ ) : ( + i18nTexts.none + )} +
+ + )} +
+
+ + + + {/* ILM Policy (only for legacy as composable template could have ILM policy + inside one of their components) */} + {isLegacy && ( + <> + + + + + {ilmPolicy && ilmPolicy.name ? ( + {ilmPolicy.name} + ) : ( + i18nTexts.none + )} + + + )} - {/* // ILM Policy */} - - - - - {ilmPolicy && ilmPolicy.name ? ( - {ilmPolicy.name} - ) : ( - - )} - + {/* Has data stream? (only for composable template) */} + {isLegacy !== true && ( + <> + + + + + {hasDatastream ? i18nTexts.yes : i18nTexts.no} + + + )} - {/* // Order */} - - - - - {order || order === 0 ? order : } - + {/* Version */} + + + + + {version || version === 0 ? version : i18nTexts.none} + - {/* // Version */} - - - - - {version || version === 0 ? version : } - - + {/* Metadata (optional) */} + {isLegacy !== true && _meta && ( + <> + + + + + {JSON.stringify(_meta, null, 2)} + + + )} +
+ + ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx index 9f51f114176fb..faeca2f2487a8 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details.tsx @@ -5,8 +5,20 @@ */ import React from 'react'; +import { EuiFlyout } from '@elastic/eui'; -export const TemplateDetails: React.FunctionComponent = () => { - // TODO new (V2) templatte details - return null; +import { TemplateDetailsContent, Props } from './template_details_content'; + +export const TemplateDetails = (props: Props) => { + return ( + + + + ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx new file mode 100644 index 0000000000000..34e90aef51701 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx @@ -0,0 +1,324 @@ +/* + * 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 { + EuiCallOut, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiTab, + EuiTabs, + EuiSpacer, + EuiPopover, + EuiButton, + EuiContextMenu, +} from '@elastic/eui'; + +import { + UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, + UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, + UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, + UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, +} from '../../../../../../common/constants'; +import { SendRequestResponse } from '../../../../../shared_imports'; +import { TemplateDeleteModal, SectionLoading, SectionError, Error } from '../../../../components'; +import { useLoadIndexTemplate } from '../../../../services/api'; +import { decodePathFromReactRouter } from '../../../../services/routing'; +import { useServices } from '../../../../app_context'; +import { TabAliases, TabMappings, TabSettings } from '../../../../components/shared'; +import { TabSummary } from './tabs'; + +const SUMMARY_TAB_ID = 'summary'; +const MAPPINGS_TAB_ID = 'mappings'; +const ALIASES_TAB_ID = 'aliases'; +const SETTINGS_TAB_ID = 'settings'; + +const TABS = [ + { + id: SUMMARY_TAB_ID, + name: i18n.translate('xpack.idxMgmt.templateDetails.summaryTabTitle', { + defaultMessage: 'Summary', + }), + }, + { + id: SETTINGS_TAB_ID, + name: i18n.translate('xpack.idxMgmt.templateDetails.settingsTabTitle', { + defaultMessage: 'Settings', + }), + }, + { + id: MAPPINGS_TAB_ID, + name: i18n.translate('xpack.idxMgmt.templateDetails.mappingsTabTitle', { + defaultMessage: 'Mappings', + }), + }, + { + id: ALIASES_TAB_ID, + name: i18n.translate('xpack.idxMgmt.templateDetails.aliasesTabTitle', { + defaultMessage: 'Aliases', + }), + }, +]; + +const tabToUiMetricMap: { [key: string]: string } = { + [SUMMARY_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SUMMARY_TAB, + [SETTINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_SETTINGS_TAB, + [MAPPINGS_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_MAPPINGS_TAB, + [ALIASES_TAB_ID]: UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, +}; + +export interface Props { + template: { name: string; isLegacy?: boolean }; + onClose: () => void; + editTemplate: (name: string, isLegacy?: boolean) => void; + cloneTemplate: (name: string, isLegacy?: boolean) => void; + reload: () => Promise; +} + +export const TemplateDetailsContent = ({ + template: { name: templateName, isLegacy }, + onClose, + editTemplate, + cloneTemplate, + reload, +}: Props) => { + const { uiMetricService } = useServices(); + const decodedTemplateName = decodePathFromReactRouter(templateName); + const { error, data: templateDetails, isLoading } = useLoadIndexTemplate( + decodedTemplateName, + isLegacy + ); + const isCloudManaged = templateDetails?._kbnMeta.isCloudManaged ?? false; + const [templateToDelete, setTemplateToDelete] = useState< + Array<{ name: string; isLegacy?: boolean }> + >([]); + const [activeTab, setActiveTab] = useState(SUMMARY_TAB_ID); + const [isPopoverOpen, setIsPopOverOpen] = useState(false); + + const renderHeader = () => { + return ( + + +

+ {decodedTemplateName} +

+
+
+ ); + }; + + const renderBody = () => { + if (isLoading) { + return ( + + + + ); + } + + if (error) { + return ( + + } + error={error as Error} + data-test-subj="sectionError" + /> + ); + } + + if (templateDetails) { + const { + template: { settings, mappings, aliases }, + } = templateDetails; + + const tabToComponentMap: Record = { + [SUMMARY_TAB_ID]: , + [SETTINGS_TAB_ID]: , + [MAPPINGS_TAB_ID]: , + [ALIASES_TAB_ID]: , + }; + + const tabContent = tabToComponentMap[activeTab]; + + const managedTemplateCallout = isCloudManaged && ( + <> + + } + color="primary" + size="s" + > + + + + + ); + + return ( + <> + {managedTemplateCallout} + + + {TABS.map((tab) => ( + { + uiMetricService.trackMetric('click', tabToUiMetricMap[tab.id]); + setActiveTab(tab.id); + }} + isSelected={tab.id === activeTab} + key={tab.id} + data-test-subj="tab" + > + {tab.name} + + ))} + + + + + {tabContent} + + ); + } + }; + + const renderFooter = () => { + return ( + + + + + + + + {templateDetails && ( + + {/* Manage templates context menu */} + setIsPopOverOpen((prev) => !prev)} + > + + + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopOverOpen(false)} + panelPaddingSize="none" + withTitle + anchorPosition="rightUp" + repositionOnScroll + > + editTemplate(templateName, isLegacy), + disabled: isCloudManaged, + }, + { + name: i18n.translate('xpack.idxMgmt.templateDetails.cloneButtonLabel', { + defaultMessage: 'Clone', + }), + icon: 'copy', + onClick: () => cloneTemplate(templateName, isLegacy), + }, + { + name: i18n.translate('xpack.idxMgmt.templateDetails.deleteButtonLabel', { + defaultMessage: 'Delete', + }), + icon: 'trash', + onClick: () => + setTemplateToDelete([{ name: decodedTemplateName, isLegacy }]), + disabled: isCloudManaged, + }, + ], + }, + ]} + /> + + + )} + + + ); + }; + + return ( + <> + {renderHeader()} + + {renderBody()} + + {renderFooter()} + + {templateToDelete && templateToDelete.length > 0 ? ( + { + if (data && data.hasDeletedTemplates) { + reload(); + } else { + setTemplateToDelete([]); + } + onClose(); + }} + templatesToDelete={templateToDelete} + /> + ) : null} + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx index 956b0481dceed..afa8fa5b4ee04 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx @@ -31,8 +31,8 @@ import { } from '../../../services/routing'; import { getIsLegacyFromQueryParams } from '../../../lib/index_templates'; import { TemplateTable } from './template_table'; +import { TemplateDetails } from './template_details'; import { LegacyTemplateTable } from './legacy_templates/template_table'; -import { LegacyTemplateDetails } from './legacy_templates/template_details'; import { FilterListButton, Filters } from './components'; type FilterName = 'composable' | 'system'; @@ -90,7 +90,7 @@ export const TemplateList: React.FunctionComponent 0 || allTemplates.templates.length > 0); @@ -146,6 +146,7 @@ export const TemplateList: React.FunctionComponent @@ -235,8 +236,8 @@ export const TemplateList: React.FunctionComponent {renderContent()} - {isLegacyTemplateDetailsVisible && ( - Promise; editTemplate: (name: string) => void; + cloneTemplate: (name: string) => void; history: ScopedHistory; } export const TemplateTable: React.FunctionComponent = ({ templates, reload, - history, editTemplate, + cloneTemplate, + history, }) => { + const { uiMetricService } = useServices(); + const [selection, setSelection] = useState([]); const [templatesToDelete, setTemplatesToDelete] = useState< Array<{ name: string; isLegacy?: boolean }> >([]); @@ -40,6 +54,32 @@ export const TemplateTable: React.FunctionComponent = ({ }), truncateText: true, sortable: true, + render: (name: TemplateListItem['name'], item: TemplateListItem) => { + return ( + <> + uiMetricService.trackMetric('click', UIM_TEMPLATE_SHOW_DETAILS_CLICK) + )} + data-test-subj="templateDetailsLink" + > + {name} + +   + {item._kbnMeta.isManaged ? ( + + Managed + + ) : ( + '' + )} + + ); + }, }, { field: 'indexPatterns', @@ -50,27 +90,6 @@ export const TemplateTable: React.FunctionComponent = ({ sortable: true, render: (indexPatterns: string[]) => {indexPatterns.join(', ')}, }, - { - field: 'ilmPolicy', - name: i18n.translate('xpack.idxMgmt.templateList.table.ilmPolicyColumnTitle', { - defaultMessage: 'ILM policy', - }), - truncateText: true, - sortable: true, - render: (ilmPolicy: { name: string }) => - ilmPolicy && ilmPolicy.name ? ( - - {ilmPolicy.name} - - ) : null, - }, { field: 'composedOf', name: i18n.translate('xpack.idxMgmt.templateList.table.componentsColumnTitle', { @@ -89,8 +108,16 @@ export const TemplateTable: React.FunctionComponent = ({ sortable: true, }, { - name: i18n.translate('xpack.idxMgmt.templateList.table.overridesColumnTitle', { - defaultMessage: 'Overrides', + name: i18n.translate('xpack.idxMgmt.templateList.table.dataStreamColumnTitle', { + defaultMessage: 'Data stream', + }), + truncateText: true, + render: (template: TemplateListItem) => + template._kbnMeta.hasDatastream ? : null, + }, + { + name: i18n.translate('xpack.idxMgmt.templateList.table.contentColumnTitle', { + defaultMessage: 'Content', }), truncateText: true, render: (item: TemplateListItem) => ( @@ -98,6 +125,13 @@ export const TemplateTable: React.FunctionComponent = ({ mappings={item.hasMappings} settings={item.hasSettings} aliases={item.hasAliases} + contentWhenEmpty={ + + {i18n.translate('xpack.idxMgmt.templateList.table.noneDescriptionText', { + defaultMessage: 'None', + })} + + } /> ), }, @@ -119,7 +153,36 @@ export const TemplateTable: React.FunctionComponent = ({ onClick: ({ name }: TemplateListItem) => { editTemplate(name); }, - enabled: ({ _kbnMeta: { isManaged } }: TemplateListItem) => !isManaged, + enabled: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged, + }, + { + type: 'icon', + name: i18n.translate('xpack.idxMgmt.templateList.table.actionCloneTitle', { + defaultMessage: 'Clone', + }), + description: i18n.translate('xpack.idxMgmt.templateList.table.actionCloneDescription', { + defaultMessage: 'Clone this template', + }), + icon: 'copy', + onClick: ({ name }: TemplateListItem) => { + cloneTemplate(name); + }, + }, + { + name: i18n.translate('xpack.idxMgmt.templateList.table.actionDeleteText', { + defaultMessage: 'Delete', + }), + description: i18n.translate('xpack.idxMgmt.templateList.table.actionDeleteDecription', { + defaultMessage: 'Delete this template', + }), + icon: 'trash', + color: 'danger', + type: 'icon', + onClick: ({ name, _kbnMeta: { isLegacy } }: TemplateListItem) => { + setTemplatesToDelete([{ name, isLegacy }]); + }, + isPrimary: true, + enabled: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged, }, ], }, @@ -137,10 +200,47 @@ export const TemplateTable: React.FunctionComponent = ({ }, } as const; + const selectionConfig = { + onSelectionChange: setSelection, + selectable: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged, + selectableMessage: (selectable: boolean) => { + if (!selectable) { + return i18n.translate( + 'xpack.idxMgmt.templateList.legacyTable.deleteManagedTemplateTooltip', + { + defaultMessage: 'You cannot delete a managed template.', + } + ); + } + return ''; + }, + }; + const searchConfig = { box: { incremental: true, }, + toolsLeft: + selection.length > 0 ? ( + + setTemplatesToDelete( + selection.map(({ name, _kbnMeta: { isLegacy } }: TemplateListItem) => ({ + name, + isLegacy, + })) + ) + } + color="danger" + > + + + ) : undefined, toolsRight: [ = ({ ], }; + const goToList = () => { + return history.push('templates'); + }; + return ( {templatesToDelete && templatesToDelete.length > 0 ? ( @@ -164,9 +268,10 @@ export const TemplateTable: React.FunctionComponent = ({ callback={(data) => { if (data && data.hasDeletedTemplates) { reload(); - } else { - setTemplatesToDelete([]); + // Close the flyout if it is opened + goToList(); } + setTemplatesToDelete([]); }} templatesToDelete={templatesToDelete} /> @@ -177,7 +282,8 @@ export const TemplateTable: React.FunctionComponent = ({ columns={columns} search={searchConfig} sorting={sorting} - isSelectable={false} + isSelectable={true} + selection={selectionConfig} pagination={pagination} rowProps={() => ({ 'data-test-subj': 'row', diff --git a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx index 7cacb5ee97a60..6ecefe18b1a61 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx @@ -85,11 +85,11 @@ export const TemplateEdit: React.FunctionComponent => { try { diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts index 1527af12a92a4..ba7803a5fc228 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_delete_route.ts @@ -28,6 +28,7 @@ export function registerDeleteRoute({ router, license }: RouteDependencies) { validate: { body: bodySchema }, }, license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.dataManagement!.client; const { templates } = req.body as TypeOf; const response: { templatesDeleted: Array; errors: any[] } = { templatesDeleted: [], @@ -37,14 +38,16 @@ export function registerDeleteRoute({ router, license }: RouteDependencies) { await Promise.all( templates.map(async ({ name, isLegacy }) => { try { - if (!isLegacy) { - return res.badRequest({ body: 'Only legacy index template can be deleted.' }); + if (isLegacy) { + await callAsCurrentUser('indices.deleteTemplate', { + name, + }); + } else { + await callAsCurrentUser('dataManagement.deleteComposableIndexTemplate', { + name, + }); } - await ctx.core.elasticsearch.legacy.client.callAsCurrentUser('indices.deleteTemplate', { - name, - }); - return response.templatesDeleted.push(name); } catch (e) { return response.errors.push({ diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts index 1d8645268dc25..2f4df724cdbb4 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts @@ -11,7 +11,7 @@ import { deserializeLegacyTemplate, deserializeLegacyTemplateList, } from '../../../../common/lib'; -import { getManagedTemplatePrefix } from '../../../lib/get_managed_templates'; +import { getCloudManagedTemplatePrefix } from '../../../lib/get_managed_templates'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../index'; @@ -20,7 +20,7 @@ export function registerGetAllRoute({ router, license }: RouteDependencies) { { path: addBasePath('/index_templates'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { const { callAsCurrentUser } = ctx.dataManagement!.client; - const managedTemplatePrefix = await getManagedTemplatePrefix(callAsCurrentUser); + const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(callAsCurrentUser); const legacyTemplatesEs = await callAsCurrentUser('indices.getTemplate'); const { index_templates: templatesEs } = await callAsCurrentUser( @@ -29,9 +29,9 @@ export function registerGetAllRoute({ router, license }: RouteDependencies) { const legacyTemplates = deserializeLegacyTemplateList( legacyTemplatesEs, - managedTemplatePrefix + cloudManagedTemplatePrefix ); - const templates = deserializeTemplateList(templatesEs, managedTemplatePrefix); + const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix); const body = { templates, @@ -65,7 +65,7 @@ export function registerGetOneRoute({ router, license, lib }: RouteDependencies) const isLegacy = (req.query as TypeOf).legacy === 'true'; try { - const managedTemplatePrefix = await getManagedTemplatePrefix(callAsCurrentUser); + const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(callAsCurrentUser); if (isLegacy) { const indexTemplateByName = await callAsCurrentUser('indices.getTemplate', { name }); @@ -74,7 +74,7 @@ export function registerGetOneRoute({ router, license, lib }: RouteDependencies) return res.ok({ body: deserializeLegacyTemplate( { ...indexTemplateByName[name], name }, - managedTemplatePrefix + cloudManagedTemplatePrefix ), }); } @@ -87,7 +87,7 @@ export function registerGetOneRoute({ router, license, lib }: RouteDependencies) return res.ok({ body: deserializeTemplate( { ...indexTemplates[0].index_template, name }, - managedTemplatePrefix + cloudManagedTemplatePrefix ), }); } diff --git a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts index f82ea8f3cf152..c905f92d70541 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts @@ -29,6 +29,8 @@ export const templateSchema = schema.object({ ), _kbnMeta: schema.object({ isManaged: schema.maybe(schema.boolean()), + isCloudManaged: schema.maybe(schema.boolean()), + hasDatastream: schema.maybe(schema.boolean()), isLegacy: schema.maybe(schema.boolean()), }), }); diff --git a/x-pack/plugins/index_management/test/fixtures/template.ts b/x-pack/plugins/index_management/test/fixtures/template.ts index e2e93bfb365d4..1a44ac0f71f20 100644 --- a/x-pack/plugins/index_management/test/fixtures/template.ts +++ b/x-pack/plugins/index_management/test/fixtures/template.ts @@ -14,11 +14,15 @@ export const getTemplate = ({ indexPatterns = [], template: { settings, aliases, mappings } = {}, isManaged = false, + isCloudManaged = false, + hasDatastream = false, isLegacy = false, }: Partial< TemplateDeserialized & { isLegacy?: boolean; isManaged: boolean; + isCloudManaged: boolean; + hasDatastream: boolean; } > = {}): TemplateDeserialized => ({ name, @@ -32,6 +36,8 @@ export const getTemplate = ({ }, _kbnMeta: { isManaged, + isCloudManaged, + hasDatastream, isLegacy, }, }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d97e5ec2ced60..3200240e9089a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7079,8 +7079,6 @@ "xpack.idxMgmt.templateForm.steps.mappingsStepName": "マッピング", "xpack.idxMgmt.templateForm.steps.settingsStepName": "インデックス設定", "xpack.idxMgmt.templateForm.steps.summaryStepName": "テンプレートのレビュー", - "xpack.idxMgmt.templateList.table.ilmPolicyColumnDescription": "インデックスライフサイクルポリシー「{policyName}」", - "xpack.idxMgmt.templateList.table.ilmPolicyColumnTitle": "ILM ポリシー", "xpack.idxMgmt.templateList.table.indexPatternsColumnTitle": "インデックスパターン", "xpack.idxMgmt.templateList.table.nameColumnTitle": "名前", "xpack.idxMgmt.templateList.table.noIndexTemplatesMessage": "インデックステンプレートが見つかりません", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9a3bd8f615a47..9758893732540 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7083,8 +7083,6 @@ "xpack.idxMgmt.templateForm.steps.mappingsStepName": "映射", "xpack.idxMgmt.templateForm.steps.settingsStepName": "索引设置", "xpack.idxMgmt.templateForm.steps.summaryStepName": "复查模板", - "xpack.idxMgmt.templateList.table.ilmPolicyColumnDescription": "“{policyName}”索引生命周期策略", - "xpack.idxMgmt.templateList.table.ilmPolicyColumnTitle": "ILM 策略", "xpack.idxMgmt.templateList.table.indexPatternsColumnTitle": "索引模式", "xpack.idxMgmt.templateList.table.nameColumnTitle": "名称", "xpack.idxMgmt.templateList.table.noIndexTemplatesMessage": "未找到任何索引模板", diff --git a/x-pack/test/api_integration/apis/management/index_management/templates.js b/x-pack/test/api_integration/apis/management/index_management/templates.js index 3a3d73ab68412..8d491e6a135ea 100644 --- a/x-pack/test/api_integration/apis/management/index_management/templates.js +++ b/x-pack/test/api_integration/apis/management/index_management/templates.js @@ -252,6 +252,38 @@ export default function ({ getService }) { describe('delete', () => { it('should delete an index template', async () => { + const templateName = `template-${getRandomString()}`; + const payload = getTemplatePayload(templateName, [getRandomString()]); + + const { status: createStatus, body: createBody } = await createTemplate(payload); + if (createStatus !== 200) { + throw new Error(`Error creating template: ${createStatus} ${createBody.message}`); + } + + let catTemplateResponse = await catTemplate(templateName); + + expect( + catTemplateResponse.find((template) => template.name === payload.name).name + ).to.equal(templateName); + + const { status: deleteStatus, body: deleteBody } = await deleteTemplates([ + { name: templateName }, + ]); + if (deleteStatus !== 200) { + throw new Error(`Error deleting template: ${deleteBody.message}`); + } + + expect(deleteBody.errors).to.be.empty; + expect(deleteBody.templatesDeleted[0]).to.equal(templateName); + + catTemplateResponse = await catTemplate(templateName); + + expect(catTemplateResponse.find((template) => template.name === payload.name)).to.equal( + undefined + ); + }); + + it('should delete a legacy index template', async () => { const templateName = `template-${getRandomString()}`; const payload = getTemplatePayload(templateName, [getRandomString()], true);