From b99f498740659c24f2602400b97b3c571077de22 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Thu, 16 Jun 2022 10:12:49 -0700 Subject: [PATCH 01/30] [Reporting] Improve Network Policy error, and unskip network policy test (#134534) * [Reporting] Unskip network policy test * add DisallowedOutgoingUrl type of error for failing a job when we encounter a URL that is not allowed per network policy * fix test * fix ts * fix jest tests * Update x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts Co-authored-by: Michael Dokolin * correct consumer reference Co-authored-by: Michael Dokolin --- .../plugins/reporting/common/errors/index.ts | 12 ++++++- .../common/errors/map_to_reporting_error.ts | 11 +++--- .../plugins/screenshotting/common/errors.ts | 2 ++ .../server/browsers/chromium/driver.ts | 36 ++++++++++--------- .../chromium/driver_factory/index.test.ts | 4 +-- .../browsers/chromium/driver_factory/index.ts | 25 ++++++------- .../server/browsers/chromium/index.ts | 5 +++ .../screenshotting/server/browsers/mock.ts | 2 +- .../server/screenshots/index.test.ts | 2 +- .../server/screenshots/index.ts | 4 +-- .../reporting_and_security/network_policy.ts | 18 +++++----- 11 files changed, 70 insertions(+), 51 deletions(-) diff --git a/x-pack/plugins/reporting/common/errors/index.ts b/x-pack/plugins/reporting/common/errors/index.ts index 0876b84312053..9da4173aef91f 100644 --- a/x-pack/plugins/reporting/common/errors/index.ts +++ b/x-pack/plugins/reporting/common/errors/index.ts @@ -6,8 +6,8 @@ */ /* eslint-disable max-classes-per-file */ - import { i18n } from '@kbn/i18n'; + export abstract class ReportingError extends Error { /** * A string that uniquely brands an error type. This is used to power telemetry @@ -43,6 +43,16 @@ export class InvalidLayoutParametersError extends ReportingError { } } +/** + * While loading requests in the Kibana app, a URL was encountered that the network policy did not allow. + */ +export class DisallowedOutgoingUrl extends ReportingError { + static code = 'disallowed_outgoing_url_error' as const; + public get code() { + return DisallowedOutgoingUrl.code; + } +} + /** * While performing some reporting action, like fetching data from ES, our * access token expired. diff --git a/x-pack/plugins/reporting/common/errors/map_to_reporting_error.ts b/x-pack/plugins/reporting/common/errors/map_to_reporting_error.ts index 3a1c5d0987b05..f81c18d9421f7 100644 --- a/x-pack/plugins/reporting/common/errors/map_to_reporting_error.ts +++ b/x-pack/plugins/reporting/common/errors/map_to_reporting_error.ts @@ -7,14 +7,15 @@ import { errors } from '@kbn/screenshotting-plugin/common'; import { - UnknownError, - ReportingError, BrowserCouldNotLaunchError, - BrowserUnexpectedlyClosedError, BrowserScreenshotError, + BrowserUnexpectedlyClosedError, + DisallowedOutgoingUrl, + InvalidLayoutParametersError, PdfWorkerOutOfMemoryError, + ReportingError, + UnknownError, VisualReportingSoftDisabledError, - InvalidLayoutParametersError, } from '.'; /** @@ -33,6 +34,8 @@ export function mapToReportingError(error: unknown): ReportingError { switch (true) { case error instanceof errors.InvalidLayoutParametersError: return new InvalidLayoutParametersError((error as Error).message); + case error instanceof errors.DisallowedOutgoingUrl: + return new DisallowedOutgoingUrl((error as Error).message); case error instanceof errors.BrowserClosedUnexpectedly: return new BrowserUnexpectedlyClosedError((error as Error).message); case error instanceof errors.FailedToCaptureScreenshot: diff --git a/x-pack/plugins/screenshotting/common/errors.ts b/x-pack/plugins/screenshotting/common/errors.ts index 06823ef812922..193bb5d544d4d 100644 --- a/x-pack/plugins/screenshotting/common/errors.ts +++ b/x-pack/plugins/screenshotting/common/errors.ts @@ -14,6 +14,8 @@ export class FailedToSpawnBrowserError extends Error {} export class BrowserClosedUnexpectedly extends Error {} +export class DisallowedOutgoingUrl extends Error {} + export class FailedToCaptureScreenshot extends Error {} export class InsufficientMemoryAvailableOnCloudError extends Error {} diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts index 40dce4e9b914e..44cd29e664fe3 100644 --- a/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts @@ -5,15 +5,17 @@ * 2.0. */ -import { truncate } from 'lodash'; -import open from 'opn'; -import puppeteer, { ElementHandle, EvaluateFn, Page, SerializableOrJSHandle } from 'puppeteer'; -import { parse as parseUrl } from 'url'; import { Headers, Logger } from '@kbn/core/server'; import { KBN_SCREENSHOT_MODE_HEADER, ScreenshotModePluginSetup, } from '@kbn/screenshot-mode-plugin/server'; +import { truncate } from 'lodash'; +import open from 'opn'; +import puppeteer, { ElementHandle, EvaluateFn, Page, SerializableOrJSHandle } from 'puppeteer'; +import { Subject } from 'rxjs'; +import { parse as parseUrl } from 'url'; +import { getDisallowedOutgoingUrlError } from '.'; import { ConfigType } from '../../config'; import { allowRequest } from '../network_policy'; import { stripUnsafeHeaders } from './strip_unsafe_headers'; @@ -72,18 +74,14 @@ interface InterceptedRequest { const WAIT_FOR_DELAY_MS: number = 100; -function getDisallowedOutgoingUrlError(interceptedUrl: string) { - return new Error( - `Received disallowed outgoing URL: "${interceptedUrl}". Failing the request and closing the browser.` - ); -} - /** * @internal */ export class HeadlessChromiumDriver { private listenersAttached = false; private interceptedCount = 0; + private screenshottingErrorSubject = new Subject(); + readonly screenshottingError$ = this.screenshottingErrorSubject.asObservable(); constructor( private screenshotMode: ScreenshotModePluginSetup, @@ -106,7 +104,7 @@ export class HeadlessChromiumDriver { /* * Call Page.goto and wait to see the Kibana DOM content */ - async open( + public async open( url: string, { headers, context, waitForSelector: pageLoadSelector, timeout }: OpenOptions, logger: Logger @@ -183,7 +181,7 @@ export class HeadlessChromiumDriver { } } - async printA4Pdf({ title, logo }: { title: string; logo?: string }): Promise { + public async printA4Pdf({ title, logo }: { title: string; logo?: string }): Promise { await this.workaroundWebGLDrivenCanvases(); return this.page.pdf({ format: 'a4', @@ -199,7 +197,7 @@ export class HeadlessChromiumDriver { /* * Receive a PNG buffer of the page screenshot from Chromium */ - async screenshot(elementPosition: ElementPosition): Promise { + public async screenshot(elementPosition: ElementPosition): Promise { const { boundingClientRect, scroll } = elementPosition; const screenshot = await this.page.screenshot({ clip: { @@ -228,7 +226,7 @@ export class HeadlessChromiumDriver { return this.page.evaluate(fn, ...args); } - async waitForSelector( + public async waitForSelector( selector: string, opts: WaitForSelectorOpts, context: EvaluateMetaOpts, @@ -262,7 +260,7 @@ export class HeadlessChromiumDriver { * Setting the viewport is required to ensure that all capture elements are visible: anything not in the * viewport can not be captured. */ - async setViewport( + public async setViewport( { width: _width, height: _height, zoom }: { zoom: number; width: number; height: number }, logger: Logger ): Promise { @@ -308,7 +306,9 @@ export class HeadlessChromiumDriver { requestId, }); this.page.browser().close(); - logger.error(getDisallowedOutgoingUrlError(interceptedUrl)); + const error = getDisallowedOutgoingUrlError(interceptedUrl); + this.screenshottingErrorSubject.next(error); + logger.error(error); return; } @@ -358,7 +358,9 @@ export class HeadlessChromiumDriver { if (!allowed || !this.allowRequest(interceptedUrl)) { this.page.browser().close(); - logger.error(getDisallowedOutgoingUrlError(interceptedUrl)); + const error = getDisallowedOutgoingUrlError(interceptedUrl); + this.screenshottingErrorSubject.next(error); + logger.error(error); return; } }); diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.test.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.test.ts index eff3be3d0c439..d7c6ecb042a16 100644 --- a/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.test.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.test.ts @@ -68,7 +68,7 @@ describe('HeadlessChromiumDriverFactory', () => { }); describe('createPage', () => { - it('returns browser driver, unexpected process exit observable, and close callback', async () => { + it('returns browser driver, error observable, and close callback', async () => { await expect( factory .createPage({ openUrlTimeout: 0, defaultViewport: DEFAULT_VIEWPORT }) @@ -77,7 +77,7 @@ describe('HeadlessChromiumDriverFactory', () => { ).resolves.toEqual( expect.objectContaining({ driver: expect.anything(), - unexpectedExit$: expect.anything(), + error$: expect.anything(), close: expect.anything(), }) ); diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts index bec10bf0caec7..bc001470a8e6e 100644 --- a/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts @@ -20,8 +20,8 @@ import { catchError, concatMap, ignoreElements, - map, mergeMap, + map, reduce, takeUntil, tap, @@ -42,7 +42,7 @@ interface CreatePageOptions { interface CreatePageResult { driver: HeadlessChromiumDriver; - unexpectedExit$: Rx.Observable; + error$: Rx.Observable; /** * Close the page and the browser. * @@ -258,14 +258,13 @@ export class HeadlessChromiumDriverFactory { page ); - // Rx.Observable: stream to interrupt page capture - const unexpectedExit$ = this.getPageExit(browser, page); + const error$ = Rx.concat(driver.screenshottingError$, this.getPageExit(browser, page)).pipe( + mergeMap((err) => Rx.throwError(err)) + ); + + const close = () => Rx.from(childProcess.kill()); - observer.next({ - driver, - unexpectedExit$, - close: () => Rx.from(childProcess.kill()), - }); + observer.next({ driver, error$, close }); // unsubscribe logic makes a best-effort attempt to delete the user data directory used by chromium observer.add(() => { @@ -376,15 +375,13 @@ export class HeadlessChromiumDriverFactory { return processClose$; // ideally, this would also merge with observers for stdout and stderr } - getPageExit(browser: Browser, page: Page) { + getPageExit(browser: Browser, page: Page): Rx.Observable { const pageError$ = Rx.fromEvent(page, 'error').pipe( - mergeMap((err) => { - return Rx.throwError(`Reporting encountered an error: ${err.toString()}`); - }) + map((err) => new Error(`Reporting encountered an error: ${err.toString()}`)) ); const browserDisconnect$ = Rx.fromEvent(browser, 'disconnected').pipe( - mergeMap(() => Rx.throwError(getChromiumDisconnectedError())) + map(() => getChromiumDisconnectedError()) ); return Rx.merge(pageError$, browserDisconnect$); diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/index.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/index.ts index 70fb9caa45125..672ea2ec5f020 100644 --- a/x-pack/plugins/screenshotting/server/browsers/chromium/index.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/index.ts @@ -12,6 +12,11 @@ export const getChromiumDisconnectedError = () => 'Browser was closed unexpectedly! Check the server logs for more info.' ); +export const getDisallowedOutgoingUrlError = (interceptedUrl: string) => + new errors.DisallowedOutgoingUrl( + `Received disallowed outgoing URL [${interceptedUrl}]! Check the server logs for more info.` + ); + export { HeadlessChromiumDriver } from './driver'; export type { Context } from './driver'; export { DEFAULT_VIEWPORT, HeadlessChromiumDriverFactory } from './driver_factory'; diff --git a/x-pack/plugins/screenshotting/server/browsers/mock.ts b/x-pack/plugins/screenshotting/server/browsers/mock.ts index 028bc6fc439d7..275a02e2638c9 100644 --- a/x-pack/plugins/screenshotting/server/browsers/mock.ts +++ b/x-pack/plugins/screenshotting/server/browsers/mock.ts @@ -92,7 +92,7 @@ export function createMockBrowserDriverFactory( createPage: jest.fn(() => of({ driver: driver ?? createMockBrowserDriver(), - unexpectedExit$: NEVER, + error$: NEVER, close: () => of({}), }) ), diff --git a/x-pack/plugins/screenshotting/server/screenshots/index.test.ts b/x-pack/plugins/screenshotting/server/screenshots/index.test.ts index 1cc328509ab72..11ce25e0f86f1 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/index.test.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/index.test.ts @@ -169,7 +169,7 @@ describe('Screenshot Observable Pipeline', () => { driverFactory.createPage.mockReturnValue( of({ driver, - unexpectedExit$: throwError('Instant timeout has fired!'), + error$: throwError('Instant timeout has fired!'), close: () => of({}), }) ); diff --git a/x-pack/plugins/screenshotting/server/screenshots/index.ts b/x-pack/plugins/screenshotting/server/screenshots/index.ts index b0b591a16a5d0..a10ca72ab60c8 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/index.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/index.ts @@ -126,7 +126,7 @@ export class Screenshots { ) .pipe( this.semaphore.acquire(), - mergeMap(({ driver, unexpectedExit$, close }) => { + mergeMap(({ driver, error$, close }) => { const screen = new ScreenshotObservableHandler( driver, this.config, @@ -145,7 +145,7 @@ export class Screenshots { eventLogger.error(error, Transactions.SCREENSHOTTING); return of({ ...DEFAULT_SETUP_RESULT, error }); // allow failover screenshot capture }), - takeUntil(unexpectedExit$), + takeUntil(error$), screen.getScreenshots() ) ), diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/network_policy.ts b/x-pack/test/reporting_api_integration/reporting_and_security/network_policy.ts index 0bc164227fe14..40ee0986df9e5 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/network_policy.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/network_policy.ts @@ -15,11 +15,9 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); /* - * The Reporting API Functional Test config implements a network policy that - * is designed to disallow the following Canvas worksheet + * The tests server config implements a network policy that is designed to disallow the following Canvas worksheet */ - // FLAKY: https://github.com/elastic/kibana/issues/111381 - describe.skip('Network Policy', () => { + describe('Network Policy', () => { before(async () => { await reportingAPI.initLogs(); // includes a canvas worksheet with an offending image URL }); @@ -28,18 +26,20 @@ export default function ({ getService }: FtrProviderContext) { await reportingAPI.teardownLogs(); }); - it('should fail job when page voilates the network policy', async () => { + it('should fail job when page violates the network policy', async () => { const downloadPath = await reportingAPI.postJob( `/api/reporting/generate/printablePdf?jobParams=(layout:(dimensions:(height:720,width:1080),id:preserve_layout),objectType:'canvas%20workpad',relativeUrls:!(%2Fapp%2Fcanvas%23%2Fexport%2Fworkpad%2Fpdf%2Fworkpad-e7464259-0b75-4b8c-81c8-8422b15ff201%2Fpage%2F1),title:'My%20Canvas%20Workpad')` ); // Retry the download URL until a "failed" response status is returned + let body: any; await retry.tryForTime(120000, async () => { - const { body } = await supertest.get(downloadPath).expect(500); - expect(body.message).to.match( - /Reporting generation failed: ReportingError\(code: browser_unexpectedly_closed_error\) "/ - ); + body = (await supertest.get(downloadPath).expect(500)).body; }); + + expect(body.message).to.match( + /Reporting generation failed: ReportingError\(code: disallowed_outgoing_url_error\)/ + ); }); }); } From a020f058aa616e9c9bdba6e521f87433651869b5 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Thu, 16 Jun 2022 11:15:41 -0600 Subject: [PATCH 02/30] [Infrastructure UI][Rules] Fix viewInAppUrl for custom metrics for Inventory Threshold Rule (#134114) * [Infrastructure UI][Rules] Fix viewInAppUrl for custom metrics for Inventory Threshold Rule * Adding test for generating link, moving custom field inside if statement Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../alerting/metrics/alert_link.test.ts | 40 +++++++++++++++++++ .../common/alerting/metrics/alert_link.ts | 4 +- 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/infra/common/alerting/metrics/alert_link.test.ts diff --git a/x-pack/plugins/infra/common/alerting/metrics/alert_link.test.ts b/x-pack/plugins/infra/common/alerting/metrics/alert_link.test.ts new file mode 100644 index 0000000000000..80bac365d3562 --- /dev/null +++ b/x-pack/plugins/infra/common/alerting/metrics/alert_link.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common/parse_technical_fields'; +import { ALERT_RULE_PARAMETERS, TIMESTAMP } from '@kbn/rule-data-utils'; +import { getInventoryViewInAppUrl } from './alert_link'; + +describe('Inventory Threshold Rule', () => { + describe('getInventoryViewInAppUrl', () => { + it('should work with custom metrics', () => { + const fields = { + [TIMESTAMP]: '2022-01-01T00:00:00.000Z', + [`${ALERT_RULE_PARAMETERS}.nodeType`]: 'host', + [`${ALERT_RULE_PARAMETERS}.criteria.metric`]: ['custom'], + [`${ALERT_RULE_PARAMETERS}.criteria.customMetric.id`]: ['alert-custom-metric'], + [`${ALERT_RULE_PARAMETERS}.criteria.customMetric.aggregation`]: ['avg'], + [`${ALERT_RULE_PARAMETERS}.criteria.customMetric.field`]: ['system.cpu.user.pct'], + } as unknown as ParsedTechnicalFields & Record; + const url = getInventoryViewInAppUrl(fields); + expect(url).toEqual( + '/app/metrics/link-to/inventory?customMetric=%28aggregation%3Aavg%2Cfield%3Asystem.cpu.user.pct%2Cid%3Aalert-custom-metric%2Ctype%3Acustom%29&metric=%28aggregation%3Aavg%2Cfield%3Asystem.cpu.user.pct%2Cid%3Aalert-custom-metric%2Ctype%3Acustom%29&nodeType=h×tamp=1640995200000' + ); + }); + it('should work with non-custom metrics', () => { + const fields = { + [TIMESTAMP]: '2022-01-01T00:00:00.000Z', + [`${ALERT_RULE_PARAMETERS}.nodeType`]: 'host', + [`${ALERT_RULE_PARAMETERS}.criteria.metric`]: ['cpu'], + } as unknown as ParsedTechnicalFields & Record; + const url = getInventoryViewInAppUrl(fields); + expect(url).toEqual( + '/app/metrics/link-to/inventory?customMetric=&metric=%28type%3Acpu%29&nodeType=h×tamp=1640995200000' + ); + }); + }); +}); diff --git a/x-pack/plugins/infra/common/alerting/metrics/alert_link.ts b/x-pack/plugins/infra/common/alerting/metrics/alert_link.ts index 9fbb5fa9e4e98..11068df93b926 100644 --- a/x-pack/plugins/infra/common/alerting/metrics/alert_link.ts +++ b/x-pack/plugins/infra/common/alerting/metrics/alert_link.ts @@ -24,8 +24,8 @@ export const getInventoryViewInAppUrl = ( }; // We always pick the first criteria metric for the URL const criteriaMetric = fields[`${ALERT_RULE_PARAMETERS}.criteria.metric`][0]; - const criteriaCustomMetricId = fields[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.id`][0]; - if (criteriaCustomMetricId !== 'alert-custom-metric') { + if (criteriaMetric === 'custom') { + const criteriaCustomMetricId = fields[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.id`][0]; const criteriaCustomMetricAggregation = fields[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.aggregation`][0]; const criteriaCustomMetricField = From 0e195a6762f31f57b047194a53464440b579c5a6 Mon Sep 17 00:00:00 2001 From: Jason Rhodes Date: Thu, 16 Jun 2022 13:31:46 -0400 Subject: [PATCH 03/30] Remove unified observability codeowner bottleneck (#134584) As the unified obs team is currently without active engineers, we don't want CODEOWNERS to require reviews from that team. I've changed exploratory view back to the Uptime team, the overview page to the obs design team, and 1-2 others now have no CODEOWNERS and will just require a review from someone other than the code author. --- .github/CODEOWNERS | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index df984ce41e5e9..4c2348df2ffbe 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -90,12 +90,14 @@ # Observability Shared /x-pack/plugins/observability/public/components/shared/date_picker/ @elastic/uptime -# Unified Observability -/x-pack/plugins/observability/public/components/shared/exploratory_view @elastic/unified-observability -/x-pack/plugins/observability/public/context @elastic/unified-observability -/x-pack/plugins/observability/public/pages/home @elastic/unified-observability -/x-pack/plugins/observability/public/pages/landing @elastic/unified-observability -/x-pack/plugins/observability/public/pages/overview @elastic/unified-observability +# Unified Observability - on hold due to team capacity shortage +# For now, if you're changing these pages, get a review from someone who understand the changes +# /x-pack/plugins/observability/public/context @elastic/unified-observability + +# Home/Overview/Landing Pages +/x-pack/plugins/observability/public/pages/home @elastic/observability-design +/x-pack/plugins/observability/public/pages/landing @elastic/observability-design +/x-pack/plugins/observability/public/pages/overview @elastic/observability-design # Actionable Observability /x-pack/plugins/observability/common/rules @elastic/actionable-observability From 35874aa3a37782017845b2e260ed7bdd74a9b252 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Jun 2022 19:32:56 +0200 Subject: [PATCH 04/30] Update dependency @elastic/charts to v46.10.1 (main) (#134162) --- package.json | 2 +- .../charts/__snapshots__/donut_chart.test.tsx.snap | 9 +++++++++ yarn.lock | 8 ++++---- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index f1dd078bd6cbd..163bc55ab1e03 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "@elastic/apm-rum": "^5.12.0", "@elastic/apm-rum-react": "^1.4.2", "@elastic/apm-synthtrace": "link:bazel-bin/packages/elastic-apm-synthtrace", - "@elastic/charts": "46.9.0", + "@elastic/charts": "46.10.1", "@elastic/datemath": "5.0.3", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@8.2.0-canary.2", "@elastic/ems-client": "8.3.3", diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/common/charts/__snapshots__/donut_chart.test.tsx.snap b/x-pack/plugins/synthetics/public/legacy_uptime/components/common/charts/__snapshots__/donut_chart.test.tsx.snap index 3bc2751d2c9d6..2930ba4d4570d 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/common/charts/__snapshots__/donut_chart.test.tsx.snap +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/common/charts/__snapshots__/donut_chart.test.tsx.snap @@ -400,6 +400,15 @@ exports[`DonutChart component passes correct props without errors for valid prop "visible": true, }, }, + "metric": Object { + "background": "#FFFFFF", + "barBackground": "#EDF0F5", + "nonFiniteText": "N/A", + "text": Object { + "darkColor": "#343741", + "lightColor": "#E0E5EE", + }, + }, "partition": Object { "circlePadding": 2, "emptySizeRatio": 0, diff --git a/yarn.lock b/yarn.lock index cd15e767aa6d8..a97bdb8ce72d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1429,10 +1429,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@46.9.0": - version "46.9.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-46.9.0.tgz#45a615eb84c81d8cc5219eaa4eef8f4761c9fc05" - integrity sha512-RVDNgg9bZIZoq/8sfRg6n9gXHPzs4n8bAJogxnzv73f4sLcdytGqjNbdDpbDO2MPRKnA7nN92/L0Ewkp7SiU0Q== +"@elastic/charts@46.10.1": + version "46.10.1" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-46.10.1.tgz#4613db939e05efa041199ba14ee4a7aa9cf2d2e7" + integrity sha512-zpQBmB/NnqOS14rwMbQYePErFshxnvmCW0cFSVkU2B4+2VhVBSueDZhbPbWAoOtmE61RprRxHxgT51JKCUwcAQ== dependencies: "@popperjs/core" "^2.4.0" bezier-easing "^2.1.0" From 12b56ab525981422e1af0a59763795bc1840db47 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 16 Jun 2022 13:44:56 -0400 Subject: [PATCH 05/30] skip failing test suite (#116056) --- .../api_integration/apis/ml/data_frame_analytics/evaluate.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/ml/data_frame_analytics/evaluate.ts b/x-pack/test/api_integration/apis/ml/data_frame_analytics/evaluate.ts index 1cd71bc016262..896d797670166 100644 --- a/x-pack/test/api_integration/apis/ml/data_frame_analytics/evaluate.ts +++ b/x-pack/test/api_integration/apis/ml/data_frame_analytics/evaluate.ts @@ -111,7 +111,8 @@ export default ({ getService }: FtrProviderContext) => { } } - describe('POST data_frame/_evaluate', () => { + // Failing: See https://github.com/elastic/kibana/issues/116056 + describe.skip('POST data_frame/_evaluate', () => { before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/bm_classification'); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/egs_regression'); From 8da4cb29a754186e97be60b2a651ef33b483cc87 Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Thu, 16 Jun 2022 13:57:39 -0400 Subject: [PATCH 06/30] [Response Ops] Log stack traces in alerting/actions/task executors on error (#133931) * Showing stack track for alerting task runner * Adding stack traces for action and task running * wip * Updating unit tests * wip * Dont return stack trace in action result * Updating unit tests * Updating functional tests * Updating functional tests * Separate log for error * Separate log for error * Moving error log * Trying out putting stack trace in meta * two logs and tags * Adding tags to the error logging Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/actions/common/types.ts | 4 ++ .../server/lib/action_executor.test.ts | 10 ++++- .../actions/server/lib/action_executor.ts | 15 +++++-- x-pack/plugins/actions/server/types.ts | 2 +- .../server/task_runner/task_runner.test.ts | 43 ++++++++++--------- .../server/task_runner/task_runner.ts | 10 ++++- .../server/task_running/task_runner.test.ts | 9 +++- .../server/task_running/task_runner.ts | 5 ++- 8 files changed, 66 insertions(+), 32 deletions(-) diff --git a/x-pack/plugins/actions/common/types.ts b/x-pack/plugins/actions/common/types.ts index 751b403780080..690d5034303d6 100644 --- a/x-pack/plugins/actions/common/types.ts +++ b/x-pack/plugins/actions/common/types.ts @@ -50,6 +50,10 @@ export interface ActionTypeExecutorResult { retry?: null | boolean | Date; } +export type ActionTypeExecutorRawResult = ActionTypeExecutorResult & { + error?: Error; +}; + export function isActionTypeExecutorResult( result: unknown ): result is ActionTypeExecutorResult { diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index 12898cea5a482..5b50454ccec65 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -516,13 +516,19 @@ test('logs a warning when alert executor has an error', async () => { ); }); -test('logs a warning when alert executor throws an error', async () => { +test('logs a warning and error when alert executor throws an error', async () => { const executorMock = setupActionExecutorMock(); - executorMock.mockRejectedValue(new Error('this action execution is intended to fail')); + const err = new Error('this action execution is intended to fail'); + err.stack = 'foo error\n stack 1\n stack 2\n stack 3'; + executorMock.mockRejectedValue(err); await actionExecutor.execute(executeParams); expect(loggerMock.warn).toBeCalledWith( 'action execution failure: test:1: action-1: an error occurred while running the action: this action execution is intended to fail' ); + expect(loggerMock.error).toBeCalledWith(err, { + error: { stack_trace: 'foo error\n stack 1\n stack 2\n stack 3' }, + tags: ['test', '1', 'action-run-failed'], + }); }); test('logs a warning when alert executor returns invalid status', async () => { diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index b9ed252c6afc2..a27cfcccef9c8 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -21,6 +21,7 @@ import { import { ActionType, ActionTypeExecutorResult, + ActionTypeExecutorRawResult, ActionTypeRegistryContract, GetServicesFunction, PreConfiguredAction, @@ -203,7 +204,7 @@ export class ActionExecutor { eventLogger.logEvent(startEvent); - let rawResult: ActionTypeExecutorResult; + let rawResult: ActionTypeExecutorRawResult; try { const { validatedParams, validatedConfig, validatedSecrets } = validateAction({ actionId, @@ -231,6 +232,7 @@ export class ActionExecutor { status: 'error', message: 'an error occurred while running the action', serviceMessage: err.message, + error: err, retry: false, }; } @@ -256,6 +258,12 @@ export class ActionExecutor { event.message = `action execution failure: ${actionLabel}`; event.error = event.error || {}; event.error.message = actionErrorToMessage(result); + if (result.error) { + logger.error(result.error, { + tags: [actionTypeId, actionId, 'action-run-failed'], + error: { stack_trace: result.error.stack }, + }); + } logger.warn(`action execution failure: ${actionLabel}: ${event.error.message}`); } else { span?.setOutcome('failure'); @@ -269,7 +277,8 @@ export class ActionExecutor { } eventLogger.logEvent(event); - return result; + const { error, ...resultWithoutError } = result; + return resultWithoutError; } ); } @@ -393,7 +402,7 @@ async function getActionInfoInternal( }; } -function actionErrorToMessage(result: ActionTypeExecutorResult): string { +function actionErrorToMessage(result: ActionTypeExecutorRawResult): string { let message = result.message || 'unknown error running action'; if (result.serviceMessage) { diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 491d7ab5be2e4..4f3a2ae6fa4f1 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -22,7 +22,7 @@ import { ActionTypeExecutorResult } from '../common'; import { TaskInfo } from './lib/action_executor'; import { ConnectorTokenClient } from './builtin_action_types/lib/connector_token_client'; -export type { ActionTypeExecutorResult } from '../common'; +export type { ActionTypeExecutorResult, ActionTypeExecutorRawResult } from '../common'; export type { GetFieldsByIssueTypeResponse as JiraGetFieldsResponse } from './builtin_action_types/jira/types'; export type { GetCommonFieldsResponse as ServiceNowGetFieldsResponse } from './builtin_action_types/servicenow/types'; export type { GetCommonFieldsResponse as ResilientGetFieldsResponse } from './builtin_action_types/resilient/types'; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index f3d2c7039585b..5d2f6d7c1f659 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -87,6 +87,7 @@ jest.mock('../lib/wrap_scoped_cluster_client', () => ({ jest.mock('../lib/alerting_event_logger/alerting_event_logger'); let fakeTimer: sinon.SinonFakeTimers; +const logger: ReturnType = loggingSystemMock.createLogger(); const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract(); const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test'); @@ -139,7 +140,7 @@ describe('Task Runner', () => { actionsPlugin: actionsMock.createStart(), getRulesClientWithRequest: jest.fn().mockReturnValue(rulesClient), encryptedSavedObjectsClient, - logger: loggingSystemMock.create().get(), + logger, executionContext: executionContextServiceMock.createInternalStartContract(), spaceIdToNamespace: jest.fn().mockReturnValue(undefined), basePathService: httpServiceMock.createBasePath(), @@ -258,7 +259,6 @@ describe('Task Runner', () => { expect(call.services.scopedClusterClient).toBeTruthy(); expect(call.services).toBeTruthy(); - const logger = taskRunnerFactoryInitializerParams.logger; expect(logger.debug).toHaveBeenCalledTimes(4); expect(logger.debug).nthCalledWith(1, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); expect(logger.debug).nthCalledWith( @@ -331,7 +331,6 @@ describe('Task Runner', () => { expect(enqueueFunction).toHaveBeenCalledTimes(1); expect(enqueueFunction).toHaveBeenCalledWith(generateEnqueueFunctionInput()); - const logger = customTaskRunnerFactoryInitializerParams.logger; expect(logger.debug).toHaveBeenCalledTimes(5); expect(logger.debug).nthCalledWith(1, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); expect(logger.debug).nthCalledWith( @@ -415,7 +414,6 @@ describe('Task Runner', () => { await taskRunner.run(); expect(actionsClient.ephemeralEnqueuedExecution).toHaveBeenCalledTimes(0); - const logger = taskRunnerFactoryInitializerParams.logger; expect(logger.debug).toHaveBeenCalledTimes(6); expect(logger.debug).nthCalledWith(1, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); expect(logger.debug).nthCalledWith( @@ -541,7 +539,6 @@ describe('Task Runner', () => { expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(expectedExecutions); expect(actionsClient.ephemeralEnqueuedExecution).toHaveBeenCalledTimes(0); - const logger = taskRunnerFactoryInitializerParams.logger; const expectedMessage = `no scheduling of actions for rule test:1: '${RULE_NAME}': rule is snoozed.`; if (expectedExecutions) { expect(logger.debug).not.toHaveBeenCalledWith(expectedMessage); @@ -591,7 +588,6 @@ describe('Task Runner', () => { await taskRunner.run(); expect(enqueueFunction).toHaveBeenCalledTimes(1); - const logger = customTaskRunnerFactoryInitializerParams.logger; expect(logger.debug).toHaveBeenCalledTimes(6); expect(logger.debug).nthCalledWith(1, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); expect(logger.debug).nthCalledWith( @@ -671,7 +667,6 @@ describe('Task Runner', () => { await taskRunner.run(); // expect(enqueueFunction).toHaveBeenCalledTimes(1); - const logger = customTaskRunnerFactoryInitializerParams.logger; // expect(logger.debug).toHaveBeenCalledTimes(5); expect(logger.debug).nthCalledWith( 3, @@ -716,7 +711,6 @@ describe('Task Runner', () => { encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); await taskRunner.run(); expect(enqueueFunction).toHaveBeenCalledTimes(1); - const logger = customTaskRunnerFactoryInitializerParams.logger; expect(logger.debug).toHaveBeenCalledTimes(6); expect(logger.debug).nthCalledWith( 3, @@ -1099,7 +1093,6 @@ describe('Task Runner', () => { generateAlertInstance({ id: 1, duration: MOCK_DURATION, start: DATE_1969 }) ); - const logger = customTaskRunnerFactoryInitializerParams.logger; expect(logger.debug).toHaveBeenCalledTimes(6); expect(logger.debug).nthCalledWith(1, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); expect(logger.debug).nthCalledWith( @@ -1210,7 +1203,6 @@ describe('Task Runner', () => { const runnerResult = await taskRunner.run(); expect(runnerResult.state.alertInstances).toEqual(generateAlertInstance()); - const logger = customTaskRunnerFactoryInitializerParams.logger; expect(logger.debug).toHaveBeenCalledWith( `rule test:1: '${RULE_NAME}' has 1 active alerts: [{\"instanceId\":\"1\",\"actionGroup\":\"default\"}]` ); @@ -1455,9 +1447,13 @@ describe('Task Runner', () => { encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce(SAVED_OBJECT); const runnerResult = await taskRunner.run(); expect(runnerResult).toEqual(generateRunnerResult({ successRatio: 0 })); - expect(taskRunnerFactoryInitializerParams.logger.error).toHaveBeenCalledWith( - `Executing Rule foo:test:1 has resulted in Error: params invalid: [param1]: expected value of type [string] but got [undefined]` + const loggerCall = logger.error.mock.calls[0][0]; + const loggerMeta = logger.error.mock.calls[0][1]; + expect(loggerCall as string).toMatchInlineSnapshot( + `"Executing Rule foo:test:1 has resulted in Error: params invalid: [param1]: expected value of type [string] but got [undefined]"` ); + expect(loggerMeta?.tags).toEqual(['test', '1', 'rule-run-failed']); + expect(loggerMeta?.error?.stack_trace).toBeDefined(); expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); }); @@ -1583,6 +1579,12 @@ describe('Task Runner', () => { }); expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); + + const loggerCall = logger.error.mock.calls[0][0]; + const loggerMeta = logger.error.mock.calls[0][1]; + expect(loggerCall as string).toMatchInlineSnapshot(`[Error: GENERIC ERROR MESSAGE]`); + expect(loggerMeta?.tags).toEqual(['test', '1', 'rule-run-failed']); + expect(loggerMeta?.error?.stack_trace).toBeDefined(); }); test('recovers gracefully when the Alert Task Runner throws an exception when fetching the encrypted attributes', async () => { @@ -1789,12 +1791,13 @@ describe('Task Runner', () => { encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); - const logger = taskRunnerFactoryInitializerParams.logger; return taskRunner.run().catch((ex) => { expect(ex.toString()).toEqual(`Error: Saved object [alert/1] not found`); - expect(logger.debug).toHaveBeenCalledWith( - `Executing Rule foo:test:1 has resulted in Error: Saved object [alert/1] not found` + const executeRuleDebugLogger = logger.debug.mock.calls[3][0]; + expect(executeRuleDebugLogger as string).toMatchInlineSnapshot( + `"Executing Rule foo:test:1 has resulted in Error: Saved object [alert/1] not found"` ); + expect(logger.error).not.toHaveBeenCalled(); expect(logger.warn).toHaveBeenCalledTimes(1); expect(logger.warn).nthCalledWith( 1, @@ -1870,12 +1873,13 @@ describe('Task Runner', () => { encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(SAVED_OBJECT); - const logger = taskRunnerFactoryInitializerParams.logger; return taskRunner.run().catch((ex) => { expect(ex.toString()).toEqual(`Error: Saved object [alert/1] not found`); - expect(logger.debug).toHaveBeenCalledWith( - `Executing Rule test space:test:1 has resulted in Error: Saved object [alert/1] not found` + const ruleExecuteDebugLog = logger.debug.mock.calls[3][0]; + expect(ruleExecuteDebugLog as string).toMatchInlineSnapshot( + `"Executing Rule test space:test:1 has resulted in Error: Saved object [alert/1] not found"` ); + expect(logger.error).not.toHaveBeenCalled(); expect(logger.warn).toHaveBeenCalledTimes(1); expect(logger.warn).nthCalledWith( 1, @@ -2325,7 +2329,6 @@ describe('Task Runner', () => { expect(call.services.scopedClusterClient).toBeTruthy(); expect(call.services).toBeTruthy(); - const logger = taskRunnerFactoryInitializerParams.logger; expect(logger.debug).toHaveBeenCalledTimes(4); expect(logger.debug).nthCalledWith(1, 'executing rule test:1 at 1970-01-01T00:00:00.000Z'); expect(logger.debug).nthCalledWith( @@ -2584,7 +2587,6 @@ describe('Task Runner', () => { }) ); - const logger = taskRunnerFactoryInitializerParams.logger; expect(logger.debug).toHaveBeenCalledTimes(6); expect(logger.debug).nthCalledWith( @@ -2750,7 +2752,6 @@ describe('Task Runner', () => { }) ); - const logger = taskRunnerFactoryInitializerParams.logger; expect(logger.debug).toHaveBeenCalledTimes(6); expect(logger.debug).nthCalledWith( diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 9c8981ad5f4c2..b66452ca8c7ca 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -397,7 +397,10 @@ export class TaskRunner< `rule execution failure: ${ruleLabel}`, err.message ); - + this.logger.error(err, { + tags: [this.ruleType.id, ruleId, 'rule-run-failed'], + error: { stack_trace: err.stack }, + }); throw new ErrorWithReason(RuleExecutionStatusErrorReasons.Execute, err); } @@ -724,7 +727,10 @@ export class TaskRunner< if (isAlertSavedObjectNotFoundError(err, ruleId)) { this.logger.debug(message); } else { - this.logger.error(message); + this.logger.error(message, { + tags: [this.ruleType.id, ruleId, 'rule-run-failed'], + error: { stack_trace: err.stack }, + }); } return originalState; } diff --git a/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts b/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts index 5cce445e7bb4c..f61bd762de458 100644 --- a/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts +++ b/x-pack/plugins/task_manager/server/task_running/task_runner.test.ts @@ -719,8 +719,8 @@ describe('TaskManagerRunner', () => { }); expect(mockApmTrans.end).toHaveBeenCalledWith('success'); }); - test('makes calls to APM as expected when task fails', async () => { - const { runner } = await readyToRunStageSetup({ + test('makes calls to APM and logs errors as expected when task fails', async () => { + const { runner, logger } = await readyToRunStageSetup({ instance: { params: { a: 'b' }, state: { hey: 'there' }, @@ -741,6 +741,11 @@ describe('TaskManagerRunner', () => { childOf: 'apmTraceparent', }); expect(mockApmTrans.end).toHaveBeenCalledWith('failure'); + const loggerCall = logger.error.mock.calls[0][0]; + const loggerMeta = logger.error.mock.calls[0][1]; + expect(loggerCall as string).toMatchInlineSnapshot(`"Task bar \\"foo\\" failed: Error: rar"`); + expect(loggerMeta?.tags).toEqual(['bar', 'foo', 'task-run-failed']); + expect(loggerMeta?.error?.stack_trace).toBeDefined(); }); test('provides execution context on run', async () => { const { runner } = await readyToRunStageSetup({ diff --git a/x-pack/plugins/task_manager/server/task_running/task_runner.ts b/x-pack/plugins/task_manager/server/task_running/task_runner.ts index d305a49bef55e..0b735d6b0ede6 100644 --- a/x-pack/plugins/task_manager/server/task_running/task_runner.ts +++ b/x-pack/plugins/task_manager/server/task_running/task_runner.ts @@ -313,7 +313,10 @@ export class TaskManagerRunner implements TaskRunner { if (apmTrans) apmTrans.end('success'); return processedResult; } catch (err) { - this.logger.error(`Task ${this} failed: ${err}`); + this.logger.error(`Task ${this} failed: ${err}`, { + tags: [this.taskType, this.instance.task.id, 'task-run-failed'], + error: { stack_trace: err.stack }, + }); // in error scenario, we can not get the RunResult // re-use modifiedContext's state, which is correct as of beforeRun const processedResult = await withSpan({ name: 'process result', type: 'task manager' }, () => From 75f786bf734ada380ca829cbb0bc79672f89fa6a Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 16 Jun 2022 17:44:56 -0400 Subject: [PATCH 07/30] [Fleet] Add a pipeline processor to all the ingest_pipeline installed by fleet (#134578) --- .../{helper.test.ts => helpers.test.ts} | 68 ++++++++- .../elasticsearch/ingest_pipeline/helpers.ts | 49 ++++++- .../elasticsearch/ingest_pipeline/index.ts | 4 +- .../elasticsearch/ingest_pipeline/install.ts | 45 +++--- .../elasticsearch/ingest_pipeline/types.ts | 19 +++ .../server/services/epm/elasticsearch/meta.ts | 2 +- .../apis/epm/custom_ingest_pipeline.ts | 129 ++++++++++++++++++ .../fleet_api_integration/apis/epm/index.js | 1 + 8 files changed, 286 insertions(+), 31 deletions(-) rename x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/{helper.test.ts => helpers.test.ts} (68%) create mode 100644 x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/types.ts create mode 100644 x-pack/test/fleet_api_integration/apis/epm/custom_ingest_pipeline.ts diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/helper.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/helpers.test.ts similarity index 68% rename from x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/helper.test.ts rename to x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/helpers.test.ts index a36c53ca9bab5..3699db0531db6 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/helper.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/helpers.test.ts @@ -10,7 +10,12 @@ import path from 'path'; import type { RegistryDataStream } from '../../../../types'; -import { getPipelineNameForInstallation, rewriteIngestPipeline } from './helpers'; +import { + addCustomPipelineProcessor, + getCustomPipelineNameForDatastream, + getPipelineNameForInstallation, + rewriteIngestPipeline, +} from './helpers'; test('a json-format pipeline with pipeline references is correctly rewritten', () => { const inputStandard = readFileSync( @@ -137,3 +142,64 @@ test('getPipelineNameForInstallation gets correct name', () => { `${dataStream.type}-${dataStream.dataset}-${packageVersion}-${pipelineRefName}` ); }); + +describe('addCustomPipelineProcessor', () => { + it('add custom pipeline processor at the end of the pipeline for yaml pipeline', () => { + const pipelineInstall = addCustomPipelineProcessor({ + contentForInstallation: ` +processors: + - set: + field: test + value: toto + `, + extension: 'yml', + nameForInstallation: 'logs-test-1.0.0', + customIngestPipelineNameForInstallation: 'logs-test@custom', + }); + + expect(pipelineInstall.contentForInstallation).toMatchInlineSnapshot(` + "--- + processors: + - set: + field: test + value: toto + - pipeline: + name: logs-test@custom + ignore_missing_pipeline: true + " + `); + }); + + it('add custom pipeline processor at the end of the pipeline for json pipeline', () => { + const pipelineInstall = addCustomPipelineProcessor({ + contentForInstallation: `{ + "processors": [ + { + "set": { + "field": "test", + "value": "toto" + } + } + ] + }`, + extension: 'json', + nameForInstallation: 'logs-test-1.0.0', + customIngestPipelineNameForInstallation: 'logs-test@custom', + }); + + expect(pipelineInstall.contentForInstallation).toMatchInlineSnapshot( + `"{\\"processors\\":[{\\"set\\":{\\"field\\":\\"test\\",\\"value\\":\\"toto\\"}},{\\"pipeline\\":{\\"name\\":\\"logs-test@custom\\",\\"ignore_missing_pipeline\\":true}}]}"` + ); + }); +}); + +describe('getCustomPipelineNameForDatastream', () => { + it('return the correct custom pipeline for datastream', () => { + const res = getCustomPipelineNameForDatastream({ + type: 'logs', + dataset: 'test', + } as any); + + expect(res).toBe('logs-test@custom'); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/helpers.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/helpers.ts index bbddb0e454174..b2022bfdb8aa3 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/helpers.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/helpers.ts @@ -4,11 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { safeDump, safeLoad } from 'js-yaml'; import { ElasticsearchAssetType } from '../../../../types'; import type { RegistryDataStream } from '../../../../types'; import { getPathParts } from '../../archive'; +import type { PipelineInstall, RewriteSubstitution } from './types'; + export const isTopLevelPipeline = (path: string) => { const pathParts = getPathParts(path); return ( @@ -45,11 +48,9 @@ export const getPipelineNameForDatastream = ({ return `${dataStream.type}-${dataStream.dataset}-${packageVersion}`; }; -export interface RewriteSubstitution { - source: string; - target: string; - templateFunction: string; -} +export const getCustomPipelineNameForDatastream = (dataStream: RegistryDataStream): string => { + return `${dataStream.type}-${dataStream.dataset}@custom`; +}; export function rewriteIngestPipeline( pipeline: string, @@ -71,3 +72,41 @@ export function rewriteIngestPipeline( }); return pipeline; } + +function mutatePipelineContentWithNewProcessor(jsonPipelineContent: any, processor: any) { + if (!jsonPipelineContent.processors) { + jsonPipelineContent.processors = []; + } + + jsonPipelineContent.processors.push(processor); +} + +export function addCustomPipelineProcessor(pipeline: PipelineInstall): PipelineInstall { + if (!pipeline.customIngestPipelineNameForInstallation) { + return pipeline; + } + + const customPipelineProcessor = { + pipeline: { + name: pipeline.customIngestPipelineNameForInstallation, + ignore_missing_pipeline: true, + }, + }; + + if (pipeline.extension === 'yml') { + const parsedPipelineContent = safeLoad(pipeline.contentForInstallation); + mutatePipelineContentWithNewProcessor(parsedPipelineContent, customPipelineProcessor); + return { + ...pipeline, + contentForInstallation: `---\n${safeDump(parsedPipelineContent)}`, + }; + } + + const parsedPipelineContent = JSON.parse(pipeline.contentForInstallation); + mutatePipelineContentWithNewProcessor(parsedPipelineContent, customPipelineProcessor); + + return { + ...pipeline, + contentForInstallation: JSON.stringify(parsedPipelineContent), + }; +} diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/index.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/index.ts index 0c51cbcd9602a..a8f0065645b66 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/index.ts @@ -5,6 +5,6 @@ * 2.0. */ -export { prepareToInstallPipelines, isTopLevelPipeline } from './install'; -export { getPipelineNameForDatastream } from './helpers'; +export { prepareToInstallPipelines } from './install'; +export { getPipelineNameForDatastream, isTopLevelPipeline } from './helpers'; export { deletePreviousPipelines, deletePipeline } from './remove'; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts index 3401674dfda55..481d551cb07ac 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts @@ -19,28 +19,17 @@ import { } from '../../../../constants'; import { appendMetadataToIngestPipeline } from '../meta'; - import { retryTransientEsErrors } from '../retry'; import { + getCustomPipelineNameForDatastream, getPipelineNameForDatastream, getPipelineNameForInstallation, rewriteIngestPipeline, + isTopLevelPipeline, + addCustomPipelineProcessor, } from './helpers'; -import type { RewriteSubstitution } from './helpers'; - -interface PipelineInstall { - nameForInstallation: string; - contentForInstallation: string; - extension: string; -} - -export const isTopLevelPipeline = (path: string) => { - const pathParts = getPathParts(path); - return ( - pathParts.type === ElasticsearchAssetType.ingestPipeline && pathParts.dataset === undefined - ); -}; +import type { PipelineInstall, RewriteSubstitution } from './types'; export const prepareToInstallPipelines = ( installablePackage: InstallablePackage, @@ -156,8 +145,8 @@ export async function installAllPipelines({ ? paths.filter((path) => isDataStreamPipeline(path, dataStream.path)) : paths; const pipelinesInfos: Array<{ - name: string; nameForInstallation: string; + customIngestPipelineNameForInstallation?: string; content: string; extension: string; }> = []; @@ -176,8 +165,10 @@ export async function installAllPipelines({ }); const content = getAsset(path).toString('utf-8'); pipelinesInfos.push({ - name, nameForInstallation, + customIngestPipelineNameForInstallation: dataStream + ? getCustomPipelineNameForDatastream(dataStream) + : undefined, content, extension, }); @@ -203,6 +194,7 @@ export async function installAllPipelines({ pipelinesToInstall.push({ nameForInstallation, + customIngestPipelineNameForInstallation: getCustomPipelineNameForDatastream(dataStream), contentForInstallation: 'processors: []', extension: 'yml', }); @@ -220,27 +212,36 @@ async function installPipeline({ logger, pipeline, installablePackage, + shouldAddCustomPipelineProcessor = true, }: { esClient: ElasticsearchClient; logger: Logger; pipeline: PipelineInstall; installablePackage?: InstallablePackage; + shouldAddCustomPipelineProcessor?: boolean; }): Promise { - const pipelineWithMetadata = appendMetadataToIngestPipeline({ + let pipelineToInstall = appendMetadataToIngestPipeline({ pipeline, packageName: installablePackage?.name, }); + if (shouldAddCustomPipelineProcessor) { + pipelineToInstall = addCustomPipelineProcessor(pipelineToInstall); + } + const esClientParams = { - id: pipelineWithMetadata.nameForInstallation, - body: pipelineWithMetadata.contentForInstallation, + id: pipelineToInstall.nameForInstallation, + body: + pipelineToInstall.extension === 'yml' + ? pipelineToInstall.contentForInstallation + : JSON.parse(pipelineToInstall.contentForInstallation), }; const esClientRequestOptions: TransportRequestOptions = { ignore: [404], }; - if (pipelineWithMetadata.extension === 'yml') { + if (pipelineToInstall.extension === 'yml') { esClientRequestOptions.headers = { // pipeline is YAML 'Content-Type': 'application/yaml', @@ -255,7 +256,7 @@ async function installPipeline({ ); return { - id: pipelineWithMetadata.nameForInstallation, + id: pipelineToInstall.nameForInstallation, type: ElasticsearchAssetType.ingestPipeline, }; } diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/types.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/types.ts new file mode 100644 index 0000000000000..c9b7a68e3d892 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/types.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface PipelineInstall { + nameForInstallation: string; + contentForInstallation: string; + customIngestPipelineNameForInstallation?: string; + extension: string; +} + +export interface RewriteSubstitution { + source: string; + target: string; + templateFunction: string; +} diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/meta.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/meta.ts index d691cd8c700e5..0801de321f2b3 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/meta.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/meta.ts @@ -58,6 +58,6 @@ export function appendMetadataToIngestPipeline({ return { ...pipeline, - contentForInstallation: parsedPipelineContent, + contentForInstallation: JSON.stringify(parsedPipelineContent), }; } diff --git a/x-pack/test/fleet_api_integration/apis/epm/custom_ingest_pipeline.ts b/x-pack/test/fleet_api_integration/apis/epm/custom_ingest_pipeline.ts new file mode 100644 index 0000000000000..0ef5f648ab36d --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/epm/custom_ingest_pipeline.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { setupFleetAndAgents } from '../agents/services'; +import { skipIfNoDockerRegistry } from '../../helpers'; + +const TEST_INDEX = 'logs-log.log-test'; + +const CUSTOM_PIPELINE = 'logs-log.log@custom'; + +let pkgVersion: string; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const es = getService('es'); + const esArchiver = getService('esArchiver'); + + describe('custom ingest pipeline for fleet managed datastreams', () => { + skipIfNoDockerRegistry(providerContext); + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); + }); + setupFleetAndAgents(providerContext); + + // Use the custom log package to test the custom ingest pipeline + before(async () => { + const { body: getPackagesRes } = await supertest.get( + `/api/fleet/epm/packages?experimental=true` + ); + const logPackage = getPackagesRes.items.find((p: any) => p.name === 'log'); + if (!logPackage) { + throw new Error('No log package'); + } + + pkgVersion = logPackage.version; + + await supertest + .post(`/api/fleet/epm/packages/log/${pkgVersion}`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + }); + after(async () => { + await supertest + .delete(`/api/fleet/epm/packages/log/${pkgVersion}`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); + }); + + after(async () => { + const res = await es.search({ + index: TEST_INDEX, + }); + + for (const hit of res.hits.hits) { + await es.delete({ + id: hit._id, + index: hit._index, + }); + } + }); + + describe('Without custom pipeline', () => { + it('Should write doc correctly', async () => { + const res = await es.index({ + index: 'logs-log.log-test', + body: { + '@timestamp': '2020-01-01T09:09:00', + message: 'hello', + }, + }); + + await es.get({ + id: res._id, + index: res._index, + }); + }); + }); + + describe('Without custom pipeline', () => { + before(() => + es.ingest.putPipeline({ + id: CUSTOM_PIPELINE, + processors: [ + { + set: { + field: 'test', + value: 'itworks', + }, + }, + ], + }) + ); + + after(() => + es.ingest.deletePipeline({ + id: CUSTOM_PIPELINE, + }) + ); + it('Should write doc correctly', async () => { + const res = await es.index({ + index: 'logs-log.log-test', + body: { + '@timestamp': '2020-01-01T09:09:00', + message: 'hello', + }, + }); + + const doc = await es.get<{ test: string }>({ + id: res._id, + index: res._index, + }); + expect(doc._source?.test).to.eql('itworks'); + }); + }); + }); +} diff --git a/x-pack/test/fleet_api_integration/apis/epm/index.js b/x-pack/test/fleet_api_integration/apis/epm/index.js index ef103592dfb45..137d7d59d8bfa 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/index.js +++ b/x-pack/test/fleet_api_integration/apis/epm/index.js @@ -30,5 +30,6 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./remove_legacy_templates')); loadTestFile(require.resolve('./install_error_rollback')); loadTestFile(require.resolve('./final_pipeline')); + loadTestFile(require.resolve('./custom_ingest_pipeline')); }); } From 03fe5d11b51ac9b4c1bdc10ec8ce18454a95ce6e Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 16 Jun 2022 19:07:21 -0700 Subject: [PATCH 08/30] [DOCS] Add server log connector details to connector APIs (#134409) --- .../actions-and-connectors/create.asciidoc | 9 +++- .../actions-and-connectors/execute.asciidoc | 45 +++++++++++++++++-- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/docs/api/actions-and-connectors/create.asciidoc b/docs/api/actions-and-connectors/create.asciidoc index b8334573d2330..6a6a259135df3 100644 --- a/docs/api/actions-and-connectors/create.asciidoc +++ b/docs/api/actions-and-connectors/create.asciidoc @@ -31,7 +31,7 @@ You must have `all` privileges for the *Actions and Connectors* feature in the === {api-request-body-title} `config`:: -(Required, object) The configuration for the connector. Configuration properties +(Required^*^, object) The configuration for the connector. Configuration properties vary depending on the connector type. For example: + -- @@ -68,12 +68,15 @@ For more information, refer to {kibana-ref}/jira-action-type.html[{jira} connector and action]. ==== +This object is not required for server log connectors. + For more configuration properties, refer to <>. // end::connector-config[] -- `connector_type_id`:: -(Required, string) The connector type ID for the connector. +(Required, string) The connector type ID for the connector. For example, +`.index`, `.jira`, or `.server-log`. `name`:: (Required, string) The display name for the connector. @@ -99,6 +102,8 @@ authentication. `email`:: (Required, string) The account email for HTTP Basic authentication. ==== + +This object is not required for index or server log connectors. // end::connector-secrets[] -- diff --git a/docs/api/actions-and-connectors/execute.asciidoc b/docs/api/actions-and-connectors/execute.asciidoc index 19b03beaa56fe..ed66abf33f58b 100644 --- a/docs/api/actions-and-connectors/execute.asciidoc +++ b/docs/api/actions-and-connectors/execute.asciidoc @@ -34,17 +34,33 @@ If you use an index connector, you must also have `all`, `create`, `index`, or === {api-path-parms-title} `id`:: - (Required, string) The ID of the connector. +(Required, string) The ID of the connector. `space_id`:: - (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. +(Optional, string) An identifier for the space. If `space_id` is not provided in +the URL, the default space is used. +[role="child_attributes"] [[execute-connector-api-request-body]] === {api-request-body-title} `params`:: - (Required, object) The parameters of the connector. Parameter properties vary depending on - the connector type. For information about the parameter properties, refer to <>. +(Required, object) The parameters of the connector. Parameter properties vary +depending on the connector type. For information about the parameter properties, +refer to <>. ++ +-- +.Server log connectors +[%collapsible%open] +==== +`level`:: +(Optional, string) The log level of the message: `trace`, `debug`, `info`, +`warn`, `error`, or `fatal`. Defaults to `info`. + +`message`:: +(Required, string) The message to log. +==== +-- [[execute-connector-api-codes]] === {api-response-codes-title} @@ -105,3 +121,24 @@ The API returns the following: "connector_id": "c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad" } -------------------------------------------------- + +Run a server log connector: + +[source,sh] +-------------------------------------------------- +POST api/actions/connector/7fc7b9a0-ecc9-11ec-8736-e7d63118c907/_execute +{ + "params": { + "level": "warn", + "message": "Test warning message" + } +} +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{"status":"ok","connector_id":"7fc7b9a0-ecc9-11ec-8736-e7d63118c907"} +-------------------------------------------------- \ No newline at end of file From 1eeb0d86c03aeaa7de7a1f8909183dd2ca20f529 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 17 Jun 2022 00:47:25 -0400 Subject: [PATCH 09/30] [api-docs] Daily api_docs build (#134624) --- api_docs/actions.devdocs.json | 21 +++ api_docs/actions.mdx | 4 +- api_docs/advanced_settings.mdx | 2 +- api_docs/aiops.mdx | 2 +- api_docs/alerting.mdx | 2 +- api_docs/apm.devdocs.json | 24 +--- api_docs/apm.mdx | 2 +- api_docs/banners.mdx | 2 +- api_docs/bfetch.mdx | 2 +- api_docs/canvas.mdx | 2 +- api_docs/cases.mdx | 2 +- api_docs/charts.mdx | 2 +- api_docs/cloud.mdx | 2 +- api_docs/cloud_security_posture.mdx | 2 +- api_docs/console.mdx | 2 +- api_docs/controls.mdx | 2 +- api_docs/core.devdocs.json | 18 +-- api_docs/core.mdx | 2 +- api_docs/core_application.mdx | 2 +- api_docs/core_chrome.mdx | 2 +- api_docs/core_http.mdx | 2 +- api_docs/core_saved_objects.mdx | 2 +- api_docs/custom_integrations.mdx | 2 +- api_docs/dashboard.mdx | 2 +- api_docs/dashboard_enhanced.mdx | 2 +- api_docs/data.mdx | 2 +- api_docs/data_query.mdx | 2 +- api_docs/data_search.mdx | 2 +- api_docs/data_view_editor.mdx | 2 +- api_docs/data_view_field_editor.mdx | 2 +- api_docs/data_view_management.mdx | 2 +- api_docs/data_views.mdx | 2 +- api_docs/data_visualizer.mdx | 2 +- api_docs/deprecations_by_api.mdx | 2 +- api_docs/deprecations_by_plugin.mdx | 2 +- api_docs/deprecations_by_team.mdx | 2 +- api_docs/dev_tools.mdx | 2 +- api_docs/discover.mdx | 2 +- api_docs/discover_enhanced.mdx | 2 +- api_docs/elastic_apm_synthtrace.mdx | 2 +- api_docs/embeddable.mdx | 2 +- api_docs/embeddable_enhanced.mdx | 2 +- api_docs/encrypted_saved_objects.mdx | 2 +- api_docs/enterprise_search.mdx | 2 +- api_docs/es_ui_shared.mdx | 2 +- api_docs/event_annotation.mdx | 2 +- api_docs/event_log.mdx | 2 +- api_docs/expression_error.mdx | 2 +- api_docs/expression_gauge.mdx | 2 +- api_docs/expression_heatmap.mdx | 2 +- api_docs/expression_image.mdx | 2 +- api_docs/expression_metric.mdx | 2 +- api_docs/expression_metric_vis.mdx | 2 +- api_docs/expression_partition_vis.mdx | 2 +- api_docs/expression_repeat_image.mdx | 2 +- api_docs/expression_reveal_image.mdx | 2 +- api_docs/expression_shape.mdx | 2 +- api_docs/expression_tagcloud.mdx | 2 +- api_docs/expression_x_y.mdx | 2 +- api_docs/expressions.mdx | 2 +- api_docs/features.mdx | 2 +- api_docs/field_formats.mdx | 2 +- api_docs/file_upload.mdx | 2 +- api_docs/fleet.mdx | 2 +- api_docs/global_search.mdx | 2 +- api_docs/home.mdx | 2 +- api_docs/index_lifecycle_management.mdx | 2 +- api_docs/index_management.mdx | 2 +- api_docs/infra.mdx | 2 +- api_docs/inspector.mdx | 2 +- api_docs/interactive_setup.mdx | 2 +- api_docs/kbn_ace.mdx | 2 +- api_docs/kbn_aiops_utils.mdx | 2 +- api_docs/kbn_alerts.mdx | 2 +- api_docs/kbn_analytics.mdx | 2 +- api_docs/kbn_analytics_client.mdx | 2 +- ..._analytics_shippers_elastic_v3_browser.mdx | 2 +- ...n_analytics_shippers_elastic_v3_common.mdx | 2 +- ...n_analytics_shippers_elastic_v3_server.mdx | 2 +- api_docs/kbn_analytics_shippers_fullstory.mdx | 2 +- api_docs/kbn_apm_config_loader.mdx | 2 +- api_docs/kbn_apm_utils.mdx | 2 +- api_docs/kbn_axe_config.mdx | 2 +- api_docs/kbn_bazel_packages.mdx | 2 +- api_docs/kbn_bazel_runner.mdx | 2 +- api_docs/kbn_ci_stats_core.mdx | 2 +- api_docs/kbn_ci_stats_reporter.mdx | 2 +- api_docs/kbn_cli_dev_mode.mdx | 2 +- api_docs/kbn_coloring.mdx | 2 +- api_docs/kbn_config.mdx | 2 +- api_docs/kbn_config_mocks.mdx | 2 +- api_docs/kbn_config_schema.mdx | 2 +- .../kbn_core_analytics_browser.devdocs.json | 82 +++++++++++ api_docs/kbn_core_analytics_browser.mdx | 27 ++++ ...re_analytics_browser_internal.devdocs.json | 135 ++++++++++++++++++ .../kbn_core_analytics_browser_internal.mdx | 27 ++++ ..._core_analytics_browser_mocks.devdocs.json | 90 ++++++++++++ api_docs/kbn_core_analytics_browser_mocks.mdx | 27 ++++ .../kbn_core_base_browser_mocks.devdocs.json | 71 +++++++++ api_docs/kbn_core_base_browser_mocks.mdx | 27 ++++ api_docs/kbn_core_base_common.mdx | 2 +- api_docs/kbn_core_base_server_mocks.mdx | 2 +- api_docs/kbn_core_doc_links_browser.mdx | 2 +- api_docs/kbn_core_doc_links_browser_mocks.mdx | 2 +- api_docs/kbn_core_doc_links_server.mdx | 2 +- api_docs/kbn_core_doc_links_server_mocks.mdx | 2 +- .../kbn_core_injected_metadata_browser.mdx | 2 +- ...n_core_injected_metadata_browser_mocks.mdx | 2 +- api_docs/kbn_core_theme_browser.mdx | 2 +- api_docs/kbn_core_theme_browser_mocks.mdx | 2 +- api_docs/kbn_crypto.mdx | 2 +- api_docs/kbn_datemath.mdx | 2 +- api_docs/kbn_dev_cli_errors.mdx | 2 +- api_docs/kbn_dev_cli_runner.mdx | 2 +- api_docs/kbn_dev_proc_runner.mdx | 2 +- api_docs/kbn_dev_utils.mdx | 2 +- api_docs/kbn_doc_links.devdocs.json | 2 +- api_docs/kbn_doc_links.mdx | 2 +- api_docs/kbn_docs_utils.mdx | 2 +- api_docs/kbn_es_archiver.mdx | 2 +- api_docs/kbn_es_query.mdx | 2 +- api_docs/kbn_eslint_plugin_imports.mdx | 2 +- api_docs/kbn_field_types.mdx | 2 +- api_docs/kbn_find_used_node_modules.mdx | 2 +- api_docs/kbn_generate.mdx | 2 +- api_docs/kbn_handlebars.mdx | 2 +- api_docs/kbn_i18n.mdx | 2 +- api_docs/kbn_import_resolver.mdx | 2 +- api_docs/kbn_interpreter.mdx | 2 +- api_docs/kbn_io_ts_utils.mdx | 2 +- api_docs/kbn_jest_serializers.mdx | 2 +- api_docs/kbn_kibana_json_schema.mdx | 2 +- api_docs/kbn_logging.devdocs.json | 2 +- api_docs/kbn_logging.mdx | 2 +- api_docs/kbn_logging_mocks.mdx | 2 +- api_docs/kbn_mapbox_gl.mdx | 2 +- api_docs/kbn_monaco.mdx | 2 +- api_docs/kbn_optimizer.mdx | 2 +- api_docs/kbn_optimizer_webpack_helpers.mdx | 2 +- ..._performance_testing_dataset_extractor.mdx | 2 +- api_docs/kbn_plugin_discovery.mdx | 2 +- api_docs/kbn_plugin_generator.mdx | 2 +- api_docs/kbn_plugin_helpers.mdx | 2 +- api_docs/kbn_pm.mdx | 2 +- api_docs/kbn_react_field.mdx | 2 +- api_docs/kbn_rule_data_utils.mdx | 2 +- .../kbn_scalability_simulation_generator.mdx | 2 +- .../kbn_securitysolution_autocomplete.mdx | 2 +- api_docs/kbn_securitysolution_es_utils.mdx | 2 +- api_docs/kbn_securitysolution_hook_utils.mdx | 2 +- ..._securitysolution_io_ts_alerting_types.mdx | 2 +- .../kbn_securitysolution_io_ts_list_types.mdx | 2 +- api_docs/kbn_securitysolution_io_ts_types.mdx | 2 +- api_docs/kbn_securitysolution_io_ts_utils.mdx | 2 +- api_docs/kbn_securitysolution_list_api.mdx | 2 +- .../kbn_securitysolution_list_constants.mdx | 2 +- api_docs/kbn_securitysolution_list_hooks.mdx | 2 +- api_docs/kbn_securitysolution_list_utils.mdx | 2 +- api_docs/kbn_securitysolution_rules.mdx | 2 +- api_docs/kbn_securitysolution_t_grid.mdx | 2 +- api_docs/kbn_securitysolution_utils.mdx | 2 +- api_docs/kbn_server_http_tools.mdx | 2 +- api_docs/kbn_server_route_repository.mdx | 2 +- api_docs/kbn_shared_ux_components.mdx | 2 +- .../kbn_shared_ux_page_analytics_no_data.mdx | 2 +- .../kbn_shared_ux_page_kibana_no_data.mdx | 2 +- .../kbn_shared_ux_prompt_no_data_views.mdx | 2 +- api_docs/kbn_shared_ux_services.mdx | 2 +- api_docs/kbn_shared_ux_storybook.mdx | 2 +- api_docs/kbn_shared_ux_utility.mdx | 2 +- api_docs/kbn_sort_package_json.mdx | 2 +- api_docs/kbn_std.mdx | 2 +- api_docs/kbn_stdio_dev_helpers.mdx | 2 +- api_docs/kbn_storybook.mdx | 2 +- api_docs/kbn_telemetry_tools.mdx | 2 +- api_docs/kbn_test.mdx | 2 +- api_docs/kbn_test_jest_helpers.mdx | 2 +- api_docs/kbn_tooling_log.mdx | 2 +- api_docs/kbn_type_summarizer.mdx | 2 +- api_docs/kbn_typed_react_router_config.mdx | 2 +- api_docs/kbn_ui_theme.mdx | 2 +- api_docs/kbn_utility_types.mdx | 2 +- api_docs/kbn_utility_types_jest.mdx | 2 +- api_docs/kbn_utils.mdx | 2 +- api_docs/kibana_overview.mdx | 2 +- api_docs/kibana_react.devdocs.json | 8 +- api_docs/kibana_react.mdx | 2 +- api_docs/kibana_utils.mdx | 2 +- api_docs/kubernetes_security.mdx | 2 +- api_docs/lens.devdocs.json | 2 +- api_docs/lens.mdx | 2 +- api_docs/license_api_guard.mdx | 2 +- api_docs/license_management.mdx | 2 +- api_docs/licensing.mdx | 2 +- api_docs/lists.mdx | 2 +- api_docs/management.mdx | 2 +- api_docs/maps.mdx | 2 +- api_docs/maps_ems.mdx | 2 +- api_docs/ml.mdx | 2 +- api_docs/monitoring.mdx | 2 +- api_docs/monitoring_collection.mdx | 2 +- api_docs/navigation.mdx | 2 +- api_docs/newsfeed.mdx | 2 +- api_docs/observability.mdx | 2 +- api_docs/osquery.mdx | 2 +- api_docs/plugin_directory.mdx | 12 +- api_docs/presentation_util.mdx | 2 +- api_docs/remote_clusters.mdx | 2 +- api_docs/reporting.mdx | 2 +- api_docs/rollup.mdx | 2 +- api_docs/rule_registry.mdx | 2 +- api_docs/runtime_fields.mdx | 2 +- api_docs/saved_objects.mdx | 2 +- api_docs/saved_objects_management.mdx | 2 +- api_docs/saved_objects_tagging.mdx | 2 +- api_docs/saved_objects_tagging_oss.mdx | 2 +- api_docs/screenshot_mode.mdx | 2 +- api_docs/screenshotting.mdx | 2 +- api_docs/security.mdx | 2 +- api_docs/security_solution.mdx | 2 +- api_docs/session_view.mdx | 2 +- api_docs/share.mdx | 2 +- api_docs/shared_u_x.mdx | 2 +- api_docs/snapshot_restore.mdx | 2 +- api_docs/spaces.mdx | 2 +- api_docs/stack_alerts.mdx | 2 +- api_docs/task_manager.mdx | 2 +- api_docs/telemetry.mdx | 2 +- api_docs/telemetry_collection_manager.mdx | 2 +- api_docs/telemetry_collection_xpack.mdx | 2 +- api_docs/telemetry_management_section.mdx | 2 +- api_docs/timelines.mdx | 2 +- api_docs/transform.mdx | 2 +- api_docs/triggers_actions_ui.mdx | 2 +- api_docs/ui_actions.mdx | 2 +- api_docs/ui_actions_enhanced.mdx | 2 +- api_docs/unified_search.mdx | 2 +- api_docs/unified_search_autocomplete.mdx | 2 +- api_docs/url_forwarding.mdx | 2 +- api_docs/usage_collection.mdx | 2 +- api_docs/ux.mdx | 2 +- api_docs/vis_default_editor.mdx | 2 +- api_docs/vis_type_gauge.mdx | 2 +- api_docs/vis_type_heatmap.mdx | 2 +- api_docs/vis_type_pie.mdx | 2 +- api_docs/vis_type_table.mdx | 2 +- api_docs/vis_type_timelion.mdx | 2 +- api_docs/vis_type_timeseries.mdx | 2 +- api_docs/vis_type_vega.mdx | 2 +- api_docs/vis_type_vislib.mdx | 2 +- api_docs/vis_type_xy.mdx | 2 +- api_docs/visualizations.mdx | 2 +- 252 files changed, 766 insertions(+), 283 deletions(-) create mode 100644 api_docs/kbn_core_analytics_browser.devdocs.json create mode 100644 api_docs/kbn_core_analytics_browser.mdx create mode 100644 api_docs/kbn_core_analytics_browser_internal.devdocs.json create mode 100644 api_docs/kbn_core_analytics_browser_internal.mdx create mode 100644 api_docs/kbn_core_analytics_browser_mocks.devdocs.json create mode 100644 api_docs/kbn_core_analytics_browser_mocks.mdx create mode 100644 api_docs/kbn_core_base_browser_mocks.devdocs.json create mode 100644 api_docs/kbn_core_base_browser_mocks.mdx diff --git a/api_docs/actions.devdocs.json b/api_docs/actions.devdocs.json index 269b7af82a800..50d82f10e8ab6 100644 --- a/api_docs/actions.devdocs.json +++ b/api_docs/actions.devdocs.json @@ -3248,6 +3248,27 @@ "deprecated": false, "initialIsOpen": false }, + { + "parentPluginId": "actions", + "id": "def-common.ActionTypeExecutorRawResult", + "type": "Type", + "tags": [], + "label": "ActionTypeExecutorRawResult", + "description": [], + "signature": [ + { + "pluginId": "actions", + "scope": "common", + "docId": "kibActionsPluginApi", + "section": "def-common.ActionTypeExecutorResult", + "text": "ActionTypeExecutorResult" + }, + " & { error?: Error | undefined; }" + ], + "path": "x-pack/plugins/actions/common/types.ts", + "deprecated": false, + "initialIsOpen": false + }, { "parentPluginId": "actions", "id": "def-common.ALERT_HISTORY_PREFIX", diff --git a/api_docs/actions.mdx b/api_docs/actions.mdx index 4d28ab700d769..14e82bee3ef78 100644 --- a/api_docs/actions.mdx +++ b/api_docs/actions.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/actions title: "actions" image: https://source.unsplash.com/400x175/?github summary: API docs for the actions plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'actions'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- @@ -18,7 +18,7 @@ Contact [Response Ops](https://github.com/orgs/elastic/teams/response-ops) for q | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 240 | 0 | 235 | 19 | +| 241 | 0 | 236 | 19 | ## Client diff --git a/api_docs/advanced_settings.mdx b/api_docs/advanced_settings.mdx index 96832d772c1ac..425741568eebb 100644 --- a/api_docs/advanced_settings.mdx +++ b/api_docs/advanced_settings.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/advancedSettings title: "advancedSettings" image: https://source.unsplash.com/400x175/?github summary: API docs for the advancedSettings plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'advancedSettings'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/aiops.mdx b/api_docs/aiops.mdx index 88bfb18a342f7..6db3aebe550b5 100644 --- a/api_docs/aiops.mdx +++ b/api_docs/aiops.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/aiops title: "aiops" image: https://source.unsplash.com/400x175/?github summary: API docs for the aiops plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'aiops'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/alerting.mdx b/api_docs/alerting.mdx index 3afa8876a6202..92c92ea262f93 100644 --- a/api_docs/alerting.mdx +++ b/api_docs/alerting.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/alerting title: "alerting" image: https://source.unsplash.com/400x175/?github summary: API docs for the alerting plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'alerting'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/apm.devdocs.json b/api_docs/apm.devdocs.json index 5a7405626d5c0..df9448f3adc92 100644 --- a/api_docs/apm.devdocs.json +++ b/api_docs/apm.devdocs.json @@ -790,7 +790,7 @@ "label": "APIEndpoint", "description": [], "signature": [ - "\"POST /internal/apm/data_view/static\" | \"GET /internal/apm/data_view/dynamic\" | \"GET /internal/apm/environments\" | \"GET /internal/apm/services/{serviceName}/errors/groups/main_statistics\" | \"GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/errors/{groupId}\" | \"GET /internal/apm/services/{serviceName}/errors/distribution\" | \"POST /internal/apm/latency/overall_distribution\" | \"GET /internal/apm/services/{serviceName}/metrics/charts\" | \"GET /internal/apm/observability_overview\" | \"GET /internal/apm/observability_overview/has_data\" | \"GET /internal/apm/ux/client-metrics\" | \"GET /internal/apm/ux/page-load-distribution\" | \"GET /internal/apm/ux/page-load-distribution/breakdown\" | \"GET /internal/apm/ux/page-view-trends\" | \"GET /internal/apm/ux/visitor-breakdown\" | \"GET /internal/apm/ux/long-task-metrics\" | \"GET /api/apm/observability_overview/has_rum_data\" | \"GET /internal/apm/service-map\" | \"GET /internal/apm/service-map/service/{serviceName}\" | \"GET /internal/apm/service-map/backend\" | \"GET /internal/apm/services/{serviceName}/serviceNodes\" | \"GET /internal/apm/services\" | \"GET /internal/apm/services/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/metadata/details\" | \"GET /internal/apm/services/{serviceName}/metadata/icons\" | \"GET /internal/apm/services/{serviceName}/agent\" | \"GET /internal/apm/services/{serviceName}/transaction_types\" | \"GET /internal/apm/services/{serviceName}/node/{serviceNodeName}/metadata\" | \"GET /api/apm/services/{serviceName}/annotation/search\" | \"POST /api/apm/services/{serviceName}/annotation\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}\" | \"GET /internal/apm/services/{serviceName}/throughput\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/dependencies\" | \"GET /internal/apm/services/{serviceName}/dependencies/breakdown\" | \"GET /internal/apm/services/{serviceName}/profiling/timeline\" | \"GET /internal/apm/services/{serviceName}/profiling/statistics\" | \"GET /internal/apm/services/{serviceName}/infrastructure_attributes_for_logs\" | \"GET /internal/apm/services/{serviceName}/anomaly_charts\" | \"GET /internal/apm/sorted_and_filtered_services\" | \"GET /internal/apm/service-groups\" | \"GET /internal/apm/service-group\" | \"POST /internal/apm/service-group\" | \"DELETE /internal/apm/service-group\" | \"GET /internal/apm/service-group/services\" | \"GET /internal/apm/suggestions\" | \"GET /internal/apm/traces/{traceId}\" | \"GET /internal/apm/traces\" | \"GET /internal/apm/traces/{traceId}/root_transaction\" | \"GET /internal/apm/transactions/{transactionId}\" | \"GET /internal/apm/traces/find\" | \"GET /internal/apm/services/{serviceName}/transactions/groups/main_statistics\" | \"GET /internal/apm/services/{serviceName}/transactions/groups/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/latency\" | \"GET /internal/apm/services/{serviceName}/transactions/traces/samples\" | \"GET /internal/apm/services/{serviceName}/transaction/charts/breakdown\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/error_rate\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate_by_transaction_name\" | \"GET /internal/apm/alerts/chart_preview/transaction_error_rate\" | \"GET /internal/apm/alerts/chart_preview/transaction_duration\" | \"GET /internal/apm/alerts/chart_preview/transaction_error_count\" | \"GET /api/apm/settings/agent-configuration\" | \"GET /api/apm/settings/agent-configuration/view\" | \"DELETE /api/apm/settings/agent-configuration\" | \"PUT /api/apm/settings/agent-configuration\" | \"POST /api/apm/settings/agent-configuration/search\" | \"GET /api/apm/settings/agent-configuration/environments\" | \"GET /api/apm/settings/agent-configuration/agent_name\" | \"GET /internal/apm/settings/anomaly-detection/jobs\" | \"POST /internal/apm/settings/anomaly-detection/jobs\" | \"GET /internal/apm/settings/anomaly-detection/environments\" | \"POST /internal/apm/settings/anomaly-detection/update_to_v3\" | \"GET /internal/apm/settings/apm-index-settings\" | \"GET /internal/apm/settings/apm-indices\" | \"POST /internal/apm/settings/apm-indices/save\" | \"GET /internal/apm/settings/custom_links/transaction\" | \"GET /internal/apm/settings/custom_links\" | \"POST /internal/apm/settings/custom_links\" | \"PUT /internal/apm/settings/custom_links/{id}\" | \"DELETE /internal/apm/settings/custom_links/{id}\" | \"GET /api/apm/sourcemaps\" | \"POST /api/apm/sourcemaps\" | \"DELETE /api/apm/sourcemaps/{id}\" | \"GET /internal/apm/fleet/has_apm_policies\" | \"GET /internal/apm/fleet/agents\" | \"POST /api/apm/fleet/apm_server_schema\" | \"GET /internal/apm/fleet/apm_server_schema/unsupported\" | \"GET /internal/apm/fleet/migration_check\" | \"POST /internal/apm/fleet/cloud_apm_package_policy\" | \"GET /internal/apm/backends/top_backends\" | \"GET /internal/apm/backends/upstream_services\" | \"GET /internal/apm/backends/metadata\" | \"GET /internal/apm/backends/charts/latency\" | \"GET /internal/apm/backends/charts/throughput\" | \"GET /internal/apm/backends/charts/error_rate\" | \"GET /internal/apm/backends/operations\" | \"POST /internal/apm/correlations/p_values\" | \"GET /internal/apm/correlations/field_candidates\" | \"POST /internal/apm/correlations/field_stats\" | \"GET /internal/apm/correlations/field_value_stats\" | \"POST /internal/apm/correlations/field_value_pairs\" | \"POST /internal/apm/correlations/significant_correlations\" | \"GET /internal/apm/fallback_to_transactions\" | \"GET /internal/apm/has_data\" | \"GET /internal/apm/event_metadata/{processorEvent}/{id}\" | \"GET /internal/apm/agent_keys\" | \"GET /internal/apm/agent_keys/privileges\" | \"POST /internal/apm/api_key/invalidate\" | \"POST /api/apm/agent_keys\" | \"GET /internal/apm/traces/{traceId}/span_links/{spanId}/parents\" | \"GET /internal/apm/traces/{traceId}/span_links/{spanId}/children\" | \"GET /internal/apm/services/{serviceName}/infrastructure_attributes\" | \"GET /internal/apm/debug-telemetry\" | \"GET /internal/apm/time_range_metadata\"" + "\"POST /internal/apm/data_view/static\" | \"GET /internal/apm/data_view/dynamic\" | \"GET /internal/apm/environments\" | \"GET /internal/apm/services/{serviceName}/errors/groups/main_statistics\" | \"GET /internal/apm/services/{serviceName}/errors/groups/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/errors/{groupId}\" | \"GET /internal/apm/services/{serviceName}/errors/distribution\" | \"POST /internal/apm/latency/overall_distribution\" | \"GET /internal/apm/services/{serviceName}/metrics/charts\" | \"GET /internal/apm/observability_overview\" | \"GET /internal/apm/observability_overview/has_data\" | \"GET /internal/apm/ux/client-metrics\" | \"GET /internal/apm/ux/page-load-distribution\" | \"GET /internal/apm/ux/page-load-distribution/breakdown\" | \"GET /internal/apm/ux/page-view-trends\" | \"GET /internal/apm/ux/visitor-breakdown\" | \"GET /internal/apm/ux/long-task-metrics\" | \"GET /internal/apm/service-map\" | \"GET /internal/apm/service-map/service/{serviceName}\" | \"GET /internal/apm/service-map/backend\" | \"GET /internal/apm/services/{serviceName}/serviceNodes\" | \"GET /internal/apm/services\" | \"GET /internal/apm/services/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/metadata/details\" | \"GET /internal/apm/services/{serviceName}/metadata/icons\" | \"GET /internal/apm/services/{serviceName}/agent\" | \"GET /internal/apm/services/{serviceName}/transaction_types\" | \"GET /internal/apm/services/{serviceName}/node/{serviceNodeName}/metadata\" | \"GET /api/apm/services/{serviceName}/annotation/search\" | \"POST /api/apm/services/{serviceName}/annotation\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}\" | \"GET /internal/apm/services/{serviceName}/throughput\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/dependencies\" | \"GET /internal/apm/services/{serviceName}/dependencies/breakdown\" | \"GET /internal/apm/services/{serviceName}/profiling/timeline\" | \"GET /internal/apm/services/{serviceName}/profiling/statistics\" | \"GET /internal/apm/services/{serviceName}/infrastructure_attributes_for_logs\" | \"GET /internal/apm/services/{serviceName}/anomaly_charts\" | \"GET /internal/apm/sorted_and_filtered_services\" | \"GET /internal/apm/service-groups\" | \"GET /internal/apm/service-group\" | \"POST /internal/apm/service-group\" | \"DELETE /internal/apm/service-group\" | \"GET /internal/apm/service-group/services\" | \"GET /internal/apm/suggestions\" | \"GET /internal/apm/traces/{traceId}\" | \"GET /internal/apm/traces\" | \"GET /internal/apm/traces/{traceId}/root_transaction\" | \"GET /internal/apm/transactions/{transactionId}\" | \"GET /internal/apm/traces/find\" | \"GET /internal/apm/services/{serviceName}/transactions/groups/main_statistics\" | \"GET /internal/apm/services/{serviceName}/transactions/groups/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/latency\" | \"GET /internal/apm/services/{serviceName}/transactions/traces/samples\" | \"GET /internal/apm/services/{serviceName}/transaction/charts/breakdown\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/error_rate\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate_by_transaction_name\" | \"GET /internal/apm/alerts/chart_preview/transaction_error_rate\" | \"GET /internal/apm/alerts/chart_preview/transaction_duration\" | \"GET /internal/apm/alerts/chart_preview/transaction_error_count\" | \"GET /api/apm/settings/agent-configuration\" | \"GET /api/apm/settings/agent-configuration/view\" | \"DELETE /api/apm/settings/agent-configuration\" | \"PUT /api/apm/settings/agent-configuration\" | \"POST /api/apm/settings/agent-configuration/search\" | \"GET /api/apm/settings/agent-configuration/environments\" | \"GET /api/apm/settings/agent-configuration/agent_name\" | \"GET /internal/apm/settings/anomaly-detection/jobs\" | \"POST /internal/apm/settings/anomaly-detection/jobs\" | \"GET /internal/apm/settings/anomaly-detection/environments\" | \"POST /internal/apm/settings/anomaly-detection/update_to_v3\" | \"GET /internal/apm/settings/apm-index-settings\" | \"GET /internal/apm/settings/apm-indices\" | \"POST /internal/apm/settings/apm-indices/save\" | \"GET /internal/apm/settings/custom_links/transaction\" | \"GET /internal/apm/settings/custom_links\" | \"POST /internal/apm/settings/custom_links\" | \"PUT /internal/apm/settings/custom_links/{id}\" | \"DELETE /internal/apm/settings/custom_links/{id}\" | \"GET /api/apm/sourcemaps\" | \"POST /api/apm/sourcemaps\" | \"DELETE /api/apm/sourcemaps/{id}\" | \"GET /internal/apm/fleet/has_apm_policies\" | \"GET /internal/apm/fleet/agents\" | \"POST /api/apm/fleet/apm_server_schema\" | \"GET /internal/apm/fleet/apm_server_schema/unsupported\" | \"GET /internal/apm/fleet/migration_check\" | \"POST /internal/apm/fleet/cloud_apm_package_policy\" | \"GET /internal/apm/backends/top_backends\" | \"GET /internal/apm/backends/upstream_services\" | \"GET /internal/apm/backends/metadata\" | \"GET /internal/apm/backends/charts/latency\" | \"GET /internal/apm/backends/charts/throughput\" | \"GET /internal/apm/backends/charts/error_rate\" | \"GET /internal/apm/backends/operations\" | \"POST /internal/apm/correlations/p_values\" | \"GET /internal/apm/correlations/field_candidates\" | \"POST /internal/apm/correlations/field_stats\" | \"GET /internal/apm/correlations/field_value_stats\" | \"POST /internal/apm/correlations/field_value_pairs\" | \"POST /internal/apm/correlations/significant_correlations\" | \"GET /internal/apm/fallback_to_transactions\" | \"GET /internal/apm/has_data\" | \"GET /internal/apm/event_metadata/{processorEvent}/{id}\" | \"GET /internal/apm/agent_keys\" | \"GET /internal/apm/agent_keys/privileges\" | \"POST /internal/apm/api_key/invalidate\" | \"POST /api/apm/agent_keys\" | \"GET /internal/apm/traces/{traceId}/span_links/{spanId}/parents\" | \"GET /internal/apm/traces/{traceId}/span_links/{spanId}/children\" | \"GET /internal/apm/services/{serviceName}/infrastructure_attributes\" | \"GET /internal/apm/debug-telemetry\" | \"GET /internal/apm/time_range_metadata\"" ], "path": "x-pack/plugins/apm/server/routes/apm_routes/get_global_apm_server_route_repository.ts", "deprecated": false, @@ -4686,28 +4686,6 @@ "ServiceAnomalyStats", " | undefined; label: string | undefined; id?: string | undefined; parent?: string | undefined; position?: cytoscape.Position | undefined; } | { 'span.destination.service.resource': string; 'span.type': string; 'span.subtype': string; label: string | undefined; id?: string | undefined; parent?: string | undefined; position?: cytoscape.Position | undefined; } | { id: string; source: string | undefined; target: string | undefined; label: string | undefined; bidirectional?: boolean | undefined; isInverseEdge?: boolean | undefined; } | undefined)[]; }; } | { data: { id: string; source: string; target: string; }; })[]; }, ", "APMRouteCreateOptions", - ">; \"GET /api/apm/observability_overview/has_rum_data\": ", - "ServerRoute", - "<\"GET /api/apm/observability_overview/has_rum_data\", ", - "PartialC", - "<{ query: ", - "PartialC", - "<{ uiFilters: ", - "StringC", - "; start: ", - "Type", - "; end: ", - "Type", - "; }>; }>, ", - { - "pluginId": "apm", - "scope": "server", - "docId": "kibApmPluginApi", - "section": "def-server.APMRouteHandlerResources", - "text": "APMRouteHandlerResources" - }, - ", { indices: string; hasData: boolean; serviceName: string | number | undefined; }, ", - "APMRouteCreateOptions", ">; \"GET /internal/apm/ux/long-task-metrics\": ", "ServerRoute", "<\"GET /internal/apm/ux/long-task-metrics\", ", diff --git a/api_docs/apm.mdx b/api_docs/apm.mdx index fd5a7531ff270..80513c9fe2b20 100644 --- a/api_docs/apm.mdx +++ b/api_docs/apm.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/apm title: "apm" image: https://source.unsplash.com/400x175/?github summary: API docs for the apm plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'apm'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/banners.mdx b/api_docs/banners.mdx index deeb88b763c5d..63ef2ef6fc1fc 100644 --- a/api_docs/banners.mdx +++ b/api_docs/banners.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/banners title: "banners" image: https://source.unsplash.com/400x175/?github summary: API docs for the banners plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'banners'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/bfetch.mdx b/api_docs/bfetch.mdx index 3ff01f989cc54..219c7e206a0e9 100644 --- a/api_docs/bfetch.mdx +++ b/api_docs/bfetch.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/bfetch title: "bfetch" image: https://source.unsplash.com/400x175/?github summary: API docs for the bfetch plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'bfetch'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/canvas.mdx b/api_docs/canvas.mdx index 930c69ee86ffe..0bce7fad086b6 100644 --- a/api_docs/canvas.mdx +++ b/api_docs/canvas.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/canvas title: "canvas" image: https://source.unsplash.com/400x175/?github summary: API docs for the canvas plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'canvas'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/cases.mdx b/api_docs/cases.mdx index ee67d893f5d4d..b450cc77fa82a 100644 --- a/api_docs/cases.mdx +++ b/api_docs/cases.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/cases title: "cases" image: https://source.unsplash.com/400x175/?github summary: API docs for the cases plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cases'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/charts.mdx b/api_docs/charts.mdx index 208b573144445..d30c10e9ab203 100644 --- a/api_docs/charts.mdx +++ b/api_docs/charts.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/charts title: "charts" image: https://source.unsplash.com/400x175/?github summary: API docs for the charts plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'charts'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/cloud.mdx b/api_docs/cloud.mdx index d3972a83409d5..a8f37e2a27277 100644 --- a/api_docs/cloud.mdx +++ b/api_docs/cloud.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/cloud title: "cloud" image: https://source.unsplash.com/400x175/?github summary: API docs for the cloud plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloud'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/cloud_security_posture.mdx b/api_docs/cloud_security_posture.mdx index 6e039b8066b35..eb4dea98ab804 100644 --- a/api_docs/cloud_security_posture.mdx +++ b/api_docs/cloud_security_posture.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/cloudSecurityPosture title: "cloudSecurityPosture" image: https://source.unsplash.com/400x175/?github summary: API docs for the cloudSecurityPosture plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudSecurityPosture'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/console.mdx b/api_docs/console.mdx index 1fe970f82ca0c..012e1921786e2 100644 --- a/api_docs/console.mdx +++ b/api_docs/console.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/console title: "console" image: https://source.unsplash.com/400x175/?github summary: API docs for the console plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'console'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/controls.mdx b/api_docs/controls.mdx index 3f0209131ac98..a981bacf2343c 100644 --- a/api_docs/controls.mdx +++ b/api_docs/controls.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/controls title: "controls" image: https://source.unsplash.com/400x175/?github summary: API docs for the controls plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'controls'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/core.devdocs.json b/api_docs/core.devdocs.json index fe0959262596d..83f05f2617239 100644 --- a/api_docs/core.devdocs.json +++ b/api_docs/core.devdocs.json @@ -7525,7 +7525,7 @@ "tags": [], "label": "AnalyticsServiceSetup", "description": [ - "\nExposes the public APIs of the AnalyticsClient during the setup phase.\n{@link AnalyticsClient}" + "\r\nExposes the public APIs of the AnalyticsClient during the setup phase.\r\n{@link AnalyticsClient}" ], "signature": [ "{ optIn: (optInConfig: ", @@ -7546,7 +7546,7 @@ "ContextProviderOpts", ") => void; removeContextProvider: (contextProviderName: string) => void; }" ], - "path": "src/core/public/analytics/analytics_service.ts", + "path": "node_modules/@types/kbn__core-analytics-browser/index.d.ts", "deprecated": false, "initialIsOpen": false }, @@ -7557,7 +7557,7 @@ "tags": [], "label": "AnalyticsServiceStart", "description": [ - "\nExposes the public APIs of the AnalyticsClient during the start phase\n{@link AnalyticsClient}" + "\r\nExposes the public APIs of the AnalyticsClient during the start phase\r\n{@link AnalyticsClient}" ], "signature": [ "{ optIn: (optInConfig: ", @@ -7568,7 +7568,7 @@ "TelemetryCounter", ">; }" ], - "path": "src/core/public/analytics/analytics_service.ts", + "path": "node_modules/@types/kbn__core-analytics-browser/index.d.ts", "deprecated": false, "initialIsOpen": false }, @@ -7948,11 +7948,11 @@ "\nA sub-set of {@link UiSettingsParams} exposed to the client-side." ], "signature": [ - "{ type?: ", + "{ metric?: { type: string; name: string; } | undefined; type?: ", "UiSettingsType", " | undefined; value?: unknown; description?: string | undefined; name?: string | undefined; options?: string[] | undefined; order?: number | undefined; category?: string[] | undefined; optionLabels?: Record | undefined; requiresPageReload?: boolean | undefined; readonly?: boolean | undefined; sensitive?: boolean | undefined; deprecation?: ", "DeprecationSettings", - " | undefined; metric?: { type: string; name: string; } | undefined; }" + " | undefined; }" ], "path": "src/core/types/ui_settings.ts", "deprecated": false, @@ -24607,7 +24607,7 @@ "label": "EcsEventKind", "description": [], "signature": [ - "\"alert\" | \"state\" | \"metric\" | \"event\" | \"signal\" | \"pipeline_error\"" + "\"metric\" | \"alert\" | \"state\" | \"event\" | \"signal\" | \"pipeline_error\"" ], "path": "node_modules/@types/kbn__logging/index.d.ts", "deprecated": false, @@ -26986,11 +26986,11 @@ "\nA sub-set of {@link UiSettingsParams} exposed to the client-side." ], "signature": [ - "{ type?: ", + "{ metric?: { type: string; name: string; } | undefined; type?: ", "UiSettingsType", " | undefined; value?: unknown; description?: string | undefined; name?: string | undefined; options?: string[] | undefined; order?: number | undefined; category?: string[] | undefined; optionLabels?: Record | undefined; requiresPageReload?: boolean | undefined; readonly?: boolean | undefined; sensitive?: boolean | undefined; deprecation?: ", "DeprecationSettings", - " | undefined; metric?: { type: string; name: string; } | undefined; }" + " | undefined; }" ], "path": "src/core/types/ui_settings.ts", "deprecated": false, diff --git a/api_docs/core.mdx b/api_docs/core.mdx index 76c0c9eed54b1..9ab8c48a434e0 100644 --- a/api_docs/core.mdx +++ b/api_docs/core.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/core title: "core" image: https://source.unsplash.com/400x175/?github summary: API docs for the core plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'core'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/core_application.mdx b/api_docs/core_application.mdx index d0ef203875d56..2887bb671225c 100644 --- a/api_docs/core_application.mdx +++ b/api_docs/core_application.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/core-application title: "core.application" image: https://source.unsplash.com/400x175/?github summary: API docs for the core.application plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'core.application'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/core_chrome.mdx b/api_docs/core_chrome.mdx index 9237f3bf4d578..61ad90364a074 100644 --- a/api_docs/core_chrome.mdx +++ b/api_docs/core_chrome.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/core-chrome title: "core.chrome" image: https://source.unsplash.com/400x175/?github summary: API docs for the core.chrome plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'core.chrome'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/core_http.mdx b/api_docs/core_http.mdx index 9c68cd0c4afe4..2ffc57c7c3b3a 100644 --- a/api_docs/core_http.mdx +++ b/api_docs/core_http.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/core-http title: "core.http" image: https://source.unsplash.com/400x175/?github summary: API docs for the core.http plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'core.http'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/core_saved_objects.mdx b/api_docs/core_saved_objects.mdx index 146d5a9ee9add..bc8fe337e21f7 100644 --- a/api_docs/core_saved_objects.mdx +++ b/api_docs/core_saved_objects.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/core-savedObjects title: "core.savedObjects" image: https://source.unsplash.com/400x175/?github summary: API docs for the core.savedObjects plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'core.savedObjects'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/custom_integrations.mdx b/api_docs/custom_integrations.mdx index d087595293399..5aedc4f3e0951 100644 --- a/api_docs/custom_integrations.mdx +++ b/api_docs/custom_integrations.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/customIntegrations title: "customIntegrations" image: https://source.unsplash.com/400x175/?github summary: API docs for the customIntegrations plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'customIntegrations'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/dashboard.mdx b/api_docs/dashboard.mdx index eed342c9cd546..2f3723f1a5276 100644 --- a/api_docs/dashboard.mdx +++ b/api_docs/dashboard.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/dashboard title: "dashboard" image: https://source.unsplash.com/400x175/?github summary: API docs for the dashboard plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboard'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/dashboard_enhanced.mdx b/api_docs/dashboard_enhanced.mdx index d14c8bdd43f85..0125b564a2345 100644 --- a/api_docs/dashboard_enhanced.mdx +++ b/api_docs/dashboard_enhanced.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/dashboardEnhanced title: "dashboardEnhanced" image: https://source.unsplash.com/400x175/?github summary: API docs for the dashboardEnhanced plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dashboardEnhanced'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/data.mdx b/api_docs/data.mdx index bdb2a45c2fa52..ded5bcec67068 100644 --- a/api_docs/data.mdx +++ b/api_docs/data.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/data title: "data" image: https://source.unsplash.com/400x175/?github summary: API docs for the data plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/data_query.mdx b/api_docs/data_query.mdx index d7765e9834e9f..f3cc5aeb5fbe6 100644 --- a/api_docs/data_query.mdx +++ b/api_docs/data_query.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/data-query title: "data.query" image: https://source.unsplash.com/400x175/?github summary: API docs for the data.query plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.query'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/data_search.mdx b/api_docs/data_search.mdx index 4841f85cc8320..0d51bf5dde050 100644 --- a/api_docs/data_search.mdx +++ b/api_docs/data_search.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/data-search title: "data.search" image: https://source.unsplash.com/400x175/?github summary: API docs for the data.search plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'data.search'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/data_view_editor.mdx b/api_docs/data_view_editor.mdx index 573a6c5cc2414..3346c3daa699e 100644 --- a/api_docs/data_view_editor.mdx +++ b/api_docs/data_view_editor.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/dataViewEditor title: "dataViewEditor" image: https://source.unsplash.com/400x175/?github summary: API docs for the dataViewEditor plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewEditor'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/data_view_field_editor.mdx b/api_docs/data_view_field_editor.mdx index 383c1b67c843c..e743338467fad 100644 --- a/api_docs/data_view_field_editor.mdx +++ b/api_docs/data_view_field_editor.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/dataViewFieldEditor title: "dataViewFieldEditor" image: https://source.unsplash.com/400x175/?github summary: API docs for the dataViewFieldEditor plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewFieldEditor'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/data_view_management.mdx b/api_docs/data_view_management.mdx index de0e359fd5f0a..12e90c08271b5 100644 --- a/api_docs/data_view_management.mdx +++ b/api_docs/data_view_management.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/dataViewManagement title: "dataViewManagement" image: https://source.unsplash.com/400x175/?github summary: API docs for the dataViewManagement plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViewManagement'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/data_views.mdx b/api_docs/data_views.mdx index f47371d893291..ac66db25b92d5 100644 --- a/api_docs/data_views.mdx +++ b/api_docs/data_views.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/dataViews title: "dataViews" image: https://source.unsplash.com/400x175/?github summary: API docs for the dataViews plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataViews'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/data_visualizer.mdx b/api_docs/data_visualizer.mdx index 6358b2fb6dad8..ab964b2f9445b 100644 --- a/api_docs/data_visualizer.mdx +++ b/api_docs/data_visualizer.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/dataVisualizer title: "dataVisualizer" image: https://source.unsplash.com/400x175/?github summary: API docs for the dataVisualizer plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'dataVisualizer'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/deprecations_by_api.mdx b/api_docs/deprecations_by_api.mdx index 3586f39e6001a..fd794be9f40ee 100644 --- a/api_docs/deprecations_by_api.mdx +++ b/api_docs/deprecations_by_api.mdx @@ -3,7 +3,7 @@ id: kibDevDocsDeprecationsByApi slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-api title: Deprecated API usage by API summary: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. --- diff --git a/api_docs/deprecations_by_plugin.mdx b/api_docs/deprecations_by_plugin.mdx index 4f7b414967b0f..134b1bc796bcd 100644 --- a/api_docs/deprecations_by_plugin.mdx +++ b/api_docs/deprecations_by_plugin.mdx @@ -3,7 +3,7 @@ id: kibDevDocsDeprecationsByPlugin slug: /kibana-dev-docs/api-meta/deprecated-api-list-by-plugin title: Deprecated API usage by plugin summary: A list of deprecated APIs, which plugins are still referencing them, and when they need to be removed by. -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. --- diff --git a/api_docs/deprecations_by_team.mdx b/api_docs/deprecations_by_team.mdx index f27558d24406e..bc294651d31c0 100644 --- a/api_docs/deprecations_by_team.mdx +++ b/api_docs/deprecations_by_team.mdx @@ -3,7 +3,7 @@ id: kibDevDocsDeprecationsDueByTeam slug: /kibana-dev-docs/api-meta/deprecations-due-by-team title: Deprecated APIs due to be removed, by team summary: Lists the teams that are referencing deprecated APIs with a remove by date. -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. --- diff --git a/api_docs/dev_tools.mdx b/api_docs/dev_tools.mdx index aa196cbfe5c3d..8c9a730a702b1 100644 --- a/api_docs/dev_tools.mdx +++ b/api_docs/dev_tools.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/devTools title: "devTools" image: https://source.unsplash.com/400x175/?github summary: API docs for the devTools plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'devTools'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/discover.mdx b/api_docs/discover.mdx index cf505d782d95e..97b58fd6a9777 100644 --- a/api_docs/discover.mdx +++ b/api_docs/discover.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/discover title: "discover" image: https://source.unsplash.com/400x175/?github summary: API docs for the discover plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discover'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/discover_enhanced.mdx b/api_docs/discover_enhanced.mdx index ef0fa4b796a60..1bc062f2b93f0 100644 --- a/api_docs/discover_enhanced.mdx +++ b/api_docs/discover_enhanced.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/discoverEnhanced title: "discoverEnhanced" image: https://source.unsplash.com/400x175/?github summary: API docs for the discoverEnhanced plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discoverEnhanced'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/elastic_apm_synthtrace.mdx b/api_docs/elastic_apm_synthtrace.mdx index 34e430a447bb3..7bcf609af04fc 100644 --- a/api_docs/elastic_apm_synthtrace.mdx +++ b/api_docs/elastic_apm_synthtrace.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/elastic-apm-synthtrace title: "@elastic/apm-synthtrace" image: https://source.unsplash.com/400x175/?github summary: API docs for the @elastic/apm-synthtrace plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@elastic/apm-synthtrace'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/embeddable.mdx b/api_docs/embeddable.mdx index 72fe88b5faafd..d1888f2409312 100644 --- a/api_docs/embeddable.mdx +++ b/api_docs/embeddable.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/embeddable title: "embeddable" image: https://source.unsplash.com/400x175/?github summary: API docs for the embeddable plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'embeddable'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/embeddable_enhanced.mdx b/api_docs/embeddable_enhanced.mdx index 9e9b3c9a07111..26e0ae1962c2b 100644 --- a/api_docs/embeddable_enhanced.mdx +++ b/api_docs/embeddable_enhanced.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/embeddableEnhanced title: "embeddableEnhanced" image: https://source.unsplash.com/400x175/?github summary: API docs for the embeddableEnhanced plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'embeddableEnhanced'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/encrypted_saved_objects.mdx b/api_docs/encrypted_saved_objects.mdx index effd93686b7ce..399eccc588fcf 100644 --- a/api_docs/encrypted_saved_objects.mdx +++ b/api_docs/encrypted_saved_objects.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/encryptedSavedObjects title: "encryptedSavedObjects" image: https://source.unsplash.com/400x175/?github summary: API docs for the encryptedSavedObjects plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'encryptedSavedObjects'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/enterprise_search.mdx b/api_docs/enterprise_search.mdx index 9d6941e0d31e2..6f6f7e669baa8 100644 --- a/api_docs/enterprise_search.mdx +++ b/api_docs/enterprise_search.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/enterpriseSearch title: "enterpriseSearch" image: https://source.unsplash.com/400x175/?github summary: API docs for the enterpriseSearch plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'enterpriseSearch'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/es_ui_shared.mdx b/api_docs/es_ui_shared.mdx index 52983caf8e0f8..1c5ea02607abb 100644 --- a/api_docs/es_ui_shared.mdx +++ b/api_docs/es_ui_shared.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/esUiShared title: "esUiShared" image: https://source.unsplash.com/400x175/?github summary: API docs for the esUiShared plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'esUiShared'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/event_annotation.mdx b/api_docs/event_annotation.mdx index b65e56704097f..a34264a49d770 100644 --- a/api_docs/event_annotation.mdx +++ b/api_docs/event_annotation.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/eventAnnotation title: "eventAnnotation" image: https://source.unsplash.com/400x175/?github summary: API docs for the eventAnnotation plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventAnnotation'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/event_log.mdx b/api_docs/event_log.mdx index 9796ae9597399..23bc8a890fbf2 100644 --- a/api_docs/event_log.mdx +++ b/api_docs/event_log.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/eventLog title: "eventLog" image: https://source.unsplash.com/400x175/?github summary: API docs for the eventLog plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventLog'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expression_error.mdx b/api_docs/expression_error.mdx index a0c223ecfafc8..0cc6d4f9b19cd 100644 --- a/api_docs/expression_error.mdx +++ b/api_docs/expression_error.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressionError title: "expressionError" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressionError plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionError'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expression_gauge.mdx b/api_docs/expression_gauge.mdx index 5afad07465b02..2d46ef46f5d65 100644 --- a/api_docs/expression_gauge.mdx +++ b/api_docs/expression_gauge.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressionGauge title: "expressionGauge" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressionGauge plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionGauge'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expression_heatmap.mdx b/api_docs/expression_heatmap.mdx index 10bbe685b8a85..2cddb207e6342 100644 --- a/api_docs/expression_heatmap.mdx +++ b/api_docs/expression_heatmap.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressionHeatmap title: "expressionHeatmap" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressionHeatmap plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionHeatmap'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expression_image.mdx b/api_docs/expression_image.mdx index 659458a80f989..749140407ddda 100644 --- a/api_docs/expression_image.mdx +++ b/api_docs/expression_image.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressionImage title: "expressionImage" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressionImage plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionImage'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expression_metric.mdx b/api_docs/expression_metric.mdx index 5523e9a5d9b22..44dcc6139d9ca 100644 --- a/api_docs/expression_metric.mdx +++ b/api_docs/expression_metric.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressionMetric title: "expressionMetric" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressionMetric plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionMetric'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expression_metric_vis.mdx b/api_docs/expression_metric_vis.mdx index 81ad91e425bd4..1cc416f370df0 100644 --- a/api_docs/expression_metric_vis.mdx +++ b/api_docs/expression_metric_vis.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressionMetricVis title: "expressionMetricVis" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressionMetricVis plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionMetricVis'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expression_partition_vis.mdx b/api_docs/expression_partition_vis.mdx index 53db2d5afb59a..79843a8b130de 100644 --- a/api_docs/expression_partition_vis.mdx +++ b/api_docs/expression_partition_vis.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressionPartitionVis title: "expressionPartitionVis" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressionPartitionVis plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionPartitionVis'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expression_repeat_image.mdx b/api_docs/expression_repeat_image.mdx index d9c0f593df339..e341355358c2d 100644 --- a/api_docs/expression_repeat_image.mdx +++ b/api_docs/expression_repeat_image.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressionRepeatImage title: "expressionRepeatImage" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressionRepeatImage plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionRepeatImage'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expression_reveal_image.mdx b/api_docs/expression_reveal_image.mdx index febd4e7102bc5..f5e84bb696834 100644 --- a/api_docs/expression_reveal_image.mdx +++ b/api_docs/expression_reveal_image.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressionRevealImage title: "expressionRevealImage" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressionRevealImage plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionRevealImage'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expression_shape.mdx b/api_docs/expression_shape.mdx index 63698129558ea..e2aa75f04539e 100644 --- a/api_docs/expression_shape.mdx +++ b/api_docs/expression_shape.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressionShape title: "expressionShape" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressionShape plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionShape'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expression_tagcloud.mdx b/api_docs/expression_tagcloud.mdx index fae0b0703fcf5..caae83e871921 100644 --- a/api_docs/expression_tagcloud.mdx +++ b/api_docs/expression_tagcloud.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressionTagcloud title: "expressionTagcloud" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressionTagcloud plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionTagcloud'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expression_x_y.mdx b/api_docs/expression_x_y.mdx index ec4c89d362b8e..c9d2ca31aa1b9 100644 --- a/api_docs/expression_x_y.mdx +++ b/api_docs/expression_x_y.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressionXY title: "expressionXY" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressionXY plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionXY'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/expressions.mdx b/api_docs/expressions.mdx index d09308f991b92..f31b0e5d2bf2d 100644 --- a/api_docs/expressions.mdx +++ b/api_docs/expressions.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/expressions title: "expressions" image: https://source.unsplash.com/400x175/?github summary: API docs for the expressions plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressions'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/features.mdx b/api_docs/features.mdx index 08abd876b378b..7ffa6044ba520 100644 --- a/api_docs/features.mdx +++ b/api_docs/features.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/features title: "features" image: https://source.unsplash.com/400x175/?github summary: API docs for the features plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'features'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/field_formats.mdx b/api_docs/field_formats.mdx index 2aafc2dae0353..0382794a17f74 100644 --- a/api_docs/field_formats.mdx +++ b/api_docs/field_formats.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/fieldFormats title: "fieldFormats" image: https://source.unsplash.com/400x175/?github summary: API docs for the fieldFormats plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fieldFormats'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/file_upload.mdx b/api_docs/file_upload.mdx index 2c9d6aca1295a..3a953da9ab5f5 100644 --- a/api_docs/file_upload.mdx +++ b/api_docs/file_upload.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/fileUpload title: "fileUpload" image: https://source.unsplash.com/400x175/?github summary: API docs for the fileUpload plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fileUpload'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/fleet.mdx b/api_docs/fleet.mdx index e33d0ea894a81..ab876b56d2c4f 100644 --- a/api_docs/fleet.mdx +++ b/api_docs/fleet.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/fleet title: "fleet" image: https://source.unsplash.com/400x175/?github summary: API docs for the fleet plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'fleet'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/global_search.mdx b/api_docs/global_search.mdx index 2fae29cc07c47..4e1db11ead733 100644 --- a/api_docs/global_search.mdx +++ b/api_docs/global_search.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/globalSearch title: "globalSearch" image: https://source.unsplash.com/400x175/?github summary: API docs for the globalSearch plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'globalSearch'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/home.mdx b/api_docs/home.mdx index 1ca07ffb82c6c..118c4e6a1fe8a 100644 --- a/api_docs/home.mdx +++ b/api_docs/home.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/home title: "home" image: https://source.unsplash.com/400x175/?github summary: API docs for the home plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'home'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/index_lifecycle_management.mdx b/api_docs/index_lifecycle_management.mdx index 3c10da180b897..22b5f7c44703c 100644 --- a/api_docs/index_lifecycle_management.mdx +++ b/api_docs/index_lifecycle_management.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/indexLifecycleManagement title: "indexLifecycleManagement" image: https://source.unsplash.com/400x175/?github summary: API docs for the indexLifecycleManagement plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexLifecycleManagement'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/index_management.mdx b/api_docs/index_management.mdx index 322663333105e..34fe5b7cf827e 100644 --- a/api_docs/index_management.mdx +++ b/api_docs/index_management.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/indexManagement title: "indexManagement" image: https://source.unsplash.com/400x175/?github summary: API docs for the indexManagement plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'indexManagement'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/infra.mdx b/api_docs/infra.mdx index 64b862b209f95..7e4607a07826f 100644 --- a/api_docs/infra.mdx +++ b/api_docs/infra.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/infra title: "infra" image: https://source.unsplash.com/400x175/?github summary: API docs for the infra plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'infra'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/inspector.mdx b/api_docs/inspector.mdx index 2c3ac7eb456c8..3fa5862104d7a 100644 --- a/api_docs/inspector.mdx +++ b/api_docs/inspector.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/inspector title: "inspector" image: https://source.unsplash.com/400x175/?github summary: API docs for the inspector plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'inspector'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/interactive_setup.mdx b/api_docs/interactive_setup.mdx index cf1596c2f6455..f53f0af2ad8f9 100644 --- a/api_docs/interactive_setup.mdx +++ b/api_docs/interactive_setup.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/interactiveSetup title: "interactiveSetup" image: https://source.unsplash.com/400x175/?github summary: API docs for the interactiveSetup plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'interactiveSetup'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_ace.mdx b/api_docs/kbn_ace.mdx index 4c52cf51431e4..47347d8e4422b 100644 --- a/api_docs/kbn_ace.mdx +++ b/api_docs/kbn_ace.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-ace title: "@kbn/ace" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/ace plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ace'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_aiops_utils.mdx b/api_docs/kbn_aiops_utils.mdx index ea97f81828020..622e711358ca6 100644 --- a/api_docs/kbn_aiops_utils.mdx +++ b/api_docs/kbn_aiops_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-aiops-utils title: "@kbn/aiops-utils" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/aiops-utils plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-utils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_alerts.mdx b/api_docs/kbn_alerts.mdx index 1cbb2574f3890..778bf54528f15 100644 --- a/api_docs/kbn_alerts.mdx +++ b/api_docs/kbn_alerts.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-alerts title: "@kbn/alerts" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/alerts plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerts'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_analytics.mdx b/api_docs/kbn_analytics.mdx index ee0b434e4b97f..71e6eafb0595e 100644 --- a/api_docs/kbn_analytics.mdx +++ b/api_docs/kbn_analytics.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-analytics title: "@kbn/analytics" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/analytics plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_analytics_client.mdx b/api_docs/kbn_analytics_client.mdx index 2c43946be4cfc..6c688f9c642f7 100644 --- a/api_docs/kbn_analytics_client.mdx +++ b/api_docs/kbn_analytics_client.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-client title: "@kbn/analytics-client" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/analytics-client plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-client'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx index c5197df8e8c5c..059a4dfb94a2a 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_browser.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-browser title: "@kbn/analytics-shippers-elastic-v3-browser" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/analytics-shippers-elastic-v3-browser plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-browser'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx index b50ed3226aee9..13f7ac9fa1727 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_common.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-common title: "@kbn/analytics-shippers-elastic-v3-common" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/analytics-shippers-elastic-v3-common plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-common'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx b/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx index a3c49c7b2eee7..363428a068d5b 100644 --- a/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx +++ b/api_docs/kbn_analytics_shippers_elastic_v3_server.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-elastic-v3-server title: "@kbn/analytics-shippers-elastic-v3-server" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/analytics-shippers-elastic-v3-server plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-elastic-v3-server'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_analytics_shippers_fullstory.mdx b/api_docs/kbn_analytics_shippers_fullstory.mdx index 818eb28b38f72..b0f6c12b49f1f 100644 --- a/api_docs/kbn_analytics_shippers_fullstory.mdx +++ b/api_docs/kbn_analytics_shippers_fullstory.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-fullstory title: "@kbn/analytics-shippers-fullstory" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/analytics-shippers-fullstory plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-fullstory'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_apm_config_loader.mdx b/api_docs/kbn_apm_config_loader.mdx index 294463866bb3c..2759bb8df0ca1 100644 --- a/api_docs/kbn_apm_config_loader.mdx +++ b/api_docs/kbn_apm_config_loader.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-apm-config-loader title: "@kbn/apm-config-loader" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/apm-config-loader plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-config-loader'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_apm_utils.mdx b/api_docs/kbn_apm_utils.mdx index 065993bafaf25..dabf2f2192b99 100644 --- a/api_docs/kbn_apm_utils.mdx +++ b/api_docs/kbn_apm_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-apm-utils title: "@kbn/apm-utils" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/apm-utils plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-utils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_axe_config.mdx b/api_docs/kbn_axe_config.mdx index d234f4cdae936..b56378a4171e5 100644 --- a/api_docs/kbn_axe_config.mdx +++ b/api_docs/kbn_axe_config.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-axe-config title: "@kbn/axe-config" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/axe-config plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/axe-config'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_bazel_packages.mdx b/api_docs/kbn_bazel_packages.mdx index 450809b74c16d..cac719f110c16 100644 --- a/api_docs/kbn_bazel_packages.mdx +++ b/api_docs/kbn_bazel_packages.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-bazel-packages title: "@kbn/bazel-packages" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/bazel-packages plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/bazel-packages'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_bazel_runner.mdx b/api_docs/kbn_bazel_runner.mdx index 93a83ad186c78..927f0ac618e35 100644 --- a/api_docs/kbn_bazel_runner.mdx +++ b/api_docs/kbn_bazel_runner.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-bazel-runner title: "@kbn/bazel-runner" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/bazel-runner plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/bazel-runner'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_ci_stats_core.mdx b/api_docs/kbn_ci_stats_core.mdx index 6cd91b7bf01d4..10bc53b1cc005 100644 --- a/api_docs/kbn_ci_stats_core.mdx +++ b/api_docs/kbn_ci_stats_core.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-core title: "@kbn/ci-stats-core" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/ci-stats-core plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-core'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_ci_stats_reporter.mdx b/api_docs/kbn_ci_stats_reporter.mdx index 0534bda96362a..0365cc151c83b 100644 --- a/api_docs/kbn_ci_stats_reporter.mdx +++ b/api_docs/kbn_ci_stats_reporter.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-ci-stats-reporter title: "@kbn/ci-stats-reporter" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/ci-stats-reporter plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ci-stats-reporter'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_cli_dev_mode.mdx b/api_docs/kbn_cli_dev_mode.mdx index 0061a09a4d4bd..033e55d467de0 100644 --- a/api_docs/kbn_cli_dev_mode.mdx +++ b/api_docs/kbn_cli_dev_mode.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-cli-dev-mode title: "@kbn/cli-dev-mode" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/cli-dev-mode plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cli-dev-mode'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_coloring.mdx b/api_docs/kbn_coloring.mdx index c4820898d29b3..2abaf70ea13cf 100644 --- a/api_docs/kbn_coloring.mdx +++ b/api_docs/kbn_coloring.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-coloring title: "@kbn/coloring" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/coloring plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/coloring'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_config.mdx b/api_docs/kbn_config.mdx index 4800f5377e8f8..ca58427161495 100644 --- a/api_docs/kbn_config.mdx +++ b/api_docs/kbn_config.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-config title: "@kbn/config" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/config plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_config_mocks.mdx b/api_docs/kbn_config_mocks.mdx index e8de463192c51..02fa7d325d0df 100644 --- a/api_docs/kbn_config_mocks.mdx +++ b/api_docs/kbn_config_mocks.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-config-mocks title: "@kbn/config-mocks" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/config-mocks plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-mocks'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_config_schema.mdx b/api_docs/kbn_config_schema.mdx index 529fd3574cc57..614c046f9cd0c 100644 --- a/api_docs/kbn_config_schema.mdx +++ b/api_docs/kbn_config_schema.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-config-schema title: "@kbn/config-schema" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/config-schema plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-schema'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_analytics_browser.devdocs.json b/api_docs/kbn_core_analytics_browser.devdocs.json new file mode 100644 index 0000000000000..c64c779b2034d --- /dev/null +++ b/api_docs/kbn_core_analytics_browser.devdocs.json @@ -0,0 +1,82 @@ +{ + "id": "@kbn/core-analytics-browser", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [ + { + "parentPluginId": "@kbn/core-analytics-browser", + "id": "def-common.AnalyticsServiceSetup", + "type": "Type", + "tags": [], + "label": "AnalyticsServiceSetup", + "description": [ + "\nExposes the public APIs of the AnalyticsClient during the setup phase.\n{@link AnalyticsClient}" + ], + "signature": [ + "{ optIn: (optInConfig: ", + "OptInConfig", + ") => void; reportEvent: >(eventType: string, eventData: EventTypeData) => void; readonly telemetryCounter$: ", + "Observable", + "<", + "TelemetryCounter", + ">; registerEventType: (eventTypeOps: ", + "EventTypeOpts", + ") => void; registerShipper: (Shipper: ", + "ShipperClassConstructor", + ", shipperConfig: ShipperConfig, opts?: ", + "RegisterShipperOpts", + " | undefined) => void; registerContextProvider: (contextProviderOpts: ", + "ContextProviderOpts", + ") => void; removeContextProvider: (contextProviderName: string) => void; }" + ], + "path": "packages/core/analytics/core-analytics-browser/src/types.ts", + "deprecated": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/core-analytics-browser", + "id": "def-common.AnalyticsServiceStart", + "type": "Type", + "tags": [], + "label": "AnalyticsServiceStart", + "description": [ + "\nExposes the public APIs of the AnalyticsClient during the start phase\n{@link AnalyticsClient}" + ], + "signature": [ + "{ optIn: (optInConfig: ", + "OptInConfig", + ") => void; reportEvent: >(eventType: string, eventData: EventTypeData) => void; readonly telemetryCounter$: ", + "Observable", + "<", + "TelemetryCounter", + ">; }" + ], + "path": "packages/core/analytics/core-analytics-browser/src/types.ts", + "deprecated": false, + "initialIsOpen": false + } + ], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_core_analytics_browser.mdx b/api_docs/kbn_core_analytics_browser.mdx new file mode 100644 index 0000000000000..3a1518cee32fd --- /dev/null +++ b/api_docs/kbn_core_analytics_browser.mdx @@ -0,0 +1,27 @@ +--- +id: kibKbnCoreAnalyticsBrowserPluginApi +slug: /kibana-dev-docs/api/kbn-core-analytics-browser +title: "@kbn/core-analytics-browser" +image: https://source.unsplash.com/400x175/?github +summary: API docs for the @kbn/core-analytics-browser plugin +date: 2022-06-17 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser'] +warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. +--- +import kbnCoreAnalyticsBrowserObj from './kbn_core_analytics_browser.devdocs.json'; + + + +Contact [Owner missing] for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 2 | 0 | 0 | 0 | + +## Common + +### Consts, variables and types + + diff --git a/api_docs/kbn_core_analytics_browser_internal.devdocs.json b/api_docs/kbn_core_analytics_browser_internal.devdocs.json new file mode 100644 index 0000000000000..6f83d74200b91 --- /dev/null +++ b/api_docs/kbn_core_analytics_browser_internal.devdocs.json @@ -0,0 +1,135 @@ +{ + "id": "@kbn/core-analytics-browser-internal", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [ + { + "parentPluginId": "@kbn/core-analytics-browser-internal", + "id": "def-common.AnalyticsService", + "type": "Class", + "tags": [], + "label": "AnalyticsService", + "description": [], + "path": "packages/core/analytics/core-analytics-browser-internal/src/analytics_service.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/core-analytics-browser-internal", + "id": "def-common.AnalyticsService.Unnamed", + "type": "Function", + "tags": [], + "label": "Constructor", + "description": [], + "signature": [ + "any" + ], + "path": "packages/core/analytics/core-analytics-browser-internal/src/analytics_service.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/core-analytics-browser-internal", + "id": "def-common.AnalyticsService.Unnamed.$1", + "type": "Object", + "tags": [], + "label": "core", + "description": [], + "signature": [ + "CoreContext" + ], + "path": "packages/core/analytics/core-analytics-browser-internal/src/analytics_service.ts", + "deprecated": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/core-analytics-browser-internal", + "id": "def-common.AnalyticsService.setup", + "type": "Function", + "tags": [], + "label": "setup", + "description": [], + "signature": [ + "({ injectedMetadata }: ", + "AnalyticsServiceSetupDeps", + ") => ", + "AnalyticsServiceSetup" + ], + "path": "packages/core/analytics/core-analytics-browser-internal/src/analytics_service.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/core-analytics-browser-internal", + "id": "def-common.AnalyticsService.setup.$1", + "type": "Object", + "tags": [], + "label": "{ injectedMetadata }", + "description": [], + "signature": [ + "AnalyticsServiceSetupDeps" + ], + "path": "packages/core/analytics/core-analytics-browser-internal/src/analytics_service.ts", + "deprecated": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/core-analytics-browser-internal", + "id": "def-common.AnalyticsService.start", + "type": "Function", + "tags": [], + "label": "start", + "description": [], + "signature": [ + "() => ", + "AnalyticsServiceStart" + ], + "path": "packages/core/analytics/core-analytics-browser-internal/src/analytics_service.ts", + "deprecated": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/core-analytics-browser-internal", + "id": "def-common.AnalyticsService.stop", + "type": "Function", + "tags": [], + "label": "stop", + "description": [], + "signature": [ + "() => void" + ], + "path": "packages/core/analytics/core-analytics-browser-internal/src/analytics_service.ts", + "deprecated": false, + "children": [], + "returnComment": [] + } + ], + "initialIsOpen": false + } + ], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_core_analytics_browser_internal.mdx b/api_docs/kbn_core_analytics_browser_internal.mdx new file mode 100644 index 0000000000000..3c0dc4316ccc8 --- /dev/null +++ b/api_docs/kbn_core_analytics_browser_internal.mdx @@ -0,0 +1,27 @@ +--- +id: kibKbnCoreAnalyticsBrowserInternalPluginApi +slug: /kibana-dev-docs/api/kbn-core-analytics-browser-internal +title: "@kbn/core-analytics-browser-internal" +image: https://source.unsplash.com/400x175/?github +summary: API docs for the @kbn/core-analytics-browser-internal plugin +date: 2022-06-17 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser-internal'] +warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. +--- +import kbnCoreAnalyticsBrowserInternalObj from './kbn_core_analytics_browser_internal.devdocs.json'; + + + +Contact [Owner missing] for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 7 | 0 | 7 | 1 | + +## Common + +### Classes + + diff --git a/api_docs/kbn_core_analytics_browser_mocks.devdocs.json b/api_docs/kbn_core_analytics_browser_mocks.devdocs.json new file mode 100644 index 0000000000000..6a05b53e9e44d --- /dev/null +++ b/api_docs/kbn_core_analytics_browser_mocks.devdocs.json @@ -0,0 +1,90 @@ +{ + "id": "@kbn/core-analytics-browser-mocks", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [ + { + "parentPluginId": "@kbn/core-analytics-browser-mocks", + "id": "def-common.analyticsServiceMock", + "type": "Object", + "tags": [], + "label": "analyticsServiceMock", + "description": [], + "path": "packages/core/analytics/core-analytics-browser-mocks/src/analytics_service.mock.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/core-analytics-browser-mocks", + "id": "def-common.analyticsServiceMock.create", + "type": "Function", + "tags": [], + "label": "create", + "description": [], + "signature": [ + "() => jest.Mocked" + ], + "path": "packages/core/analytics/core-analytics-browser-mocks/src/analytics_service.mock.ts", + "deprecated": false, + "returnComment": [], + "children": [] + }, + { + "parentPluginId": "@kbn/core-analytics-browser-mocks", + "id": "def-common.analyticsServiceMock.createAnalyticsServiceSetup", + "type": "Function", + "tags": [], + "label": "createAnalyticsServiceSetup", + "description": [], + "signature": [ + "() => jest.Mocked<", + "AnalyticsServiceSetup", + ">" + ], + "path": "packages/core/analytics/core-analytics-browser-mocks/src/analytics_service.mock.ts", + "deprecated": false, + "returnComment": [], + "children": [] + }, + { + "parentPluginId": "@kbn/core-analytics-browser-mocks", + "id": "def-common.analyticsServiceMock.createAnalyticsServiceStart", + "type": "Function", + "tags": [], + "label": "createAnalyticsServiceStart", + "description": [], + "signature": [ + "() => jest.Mocked<", + "AnalyticsServiceStart", + ">" + ], + "path": "packages/core/analytics/core-analytics-browser-mocks/src/analytics_service.mock.ts", + "deprecated": false, + "returnComment": [], + "children": [] + } + ], + "initialIsOpen": false + } + ] + } +} \ No newline at end of file diff --git a/api_docs/kbn_core_analytics_browser_mocks.mdx b/api_docs/kbn_core_analytics_browser_mocks.mdx new file mode 100644 index 0000000000000..8762fe02af599 --- /dev/null +++ b/api_docs/kbn_core_analytics_browser_mocks.mdx @@ -0,0 +1,27 @@ +--- +id: kibKbnCoreAnalyticsBrowserMocksPluginApi +slug: /kibana-dev-docs/api/kbn-core-analytics-browser-mocks +title: "@kbn/core-analytics-browser-mocks" +image: https://source.unsplash.com/400x175/?github +summary: API docs for the @kbn/core-analytics-browser-mocks plugin +date: 2022-06-17 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-analytics-browser-mocks'] +warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. +--- +import kbnCoreAnalyticsBrowserMocksObj from './kbn_core_analytics_browser_mocks.devdocs.json'; + + + +Contact [Owner missing] for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 4 | 0 | 4 | 0 | + +## Common + +### Objects + + diff --git a/api_docs/kbn_core_base_browser_mocks.devdocs.json b/api_docs/kbn_core_base_browser_mocks.devdocs.json new file mode 100644 index 0000000000000..66f7ca69015a3 --- /dev/null +++ b/api_docs/kbn_core_base_browser_mocks.devdocs.json @@ -0,0 +1,71 @@ +{ + "id": "@kbn/core-base-browser-mocks", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [ + { + "parentPluginId": "@kbn/core-base-browser-mocks", + "id": "def-common.coreContextMock", + "type": "Object", + "tags": [], + "label": "coreContextMock", + "description": [], + "path": "packages/core/base/core-base-browser-mocks/src/core_context.mock.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/core-base-browser-mocks", + "id": "def-common.coreContextMock.create", + "type": "Function", + "tags": [], + "label": "create", + "description": [], + "signature": [ + "({ production }?: { production?: boolean | undefined; }) => ", + "CoreContext" + ], + "path": "packages/core/base/core-base-browser-mocks/src/core_context.mock.ts", + "deprecated": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "@kbn/core-base-browser-mocks", + "id": "def-common.coreContextMock.create.$1", + "type": "Object", + "tags": [], + "label": "__0", + "description": [], + "signature": [ + "{ production?: boolean | undefined; }" + ], + "path": "packages/core/base/core-base-browser-mocks/src/core_context.mock.ts", + "deprecated": false + } + ] + } + ], + "initialIsOpen": false + } + ] + } +} \ No newline at end of file diff --git a/api_docs/kbn_core_base_browser_mocks.mdx b/api_docs/kbn_core_base_browser_mocks.mdx new file mode 100644 index 0000000000000..70f76ae35ff88 --- /dev/null +++ b/api_docs/kbn_core_base_browser_mocks.mdx @@ -0,0 +1,27 @@ +--- +id: kibKbnCoreBaseBrowserMocksPluginApi +slug: /kibana-dev-docs/api/kbn-core-base-browser-mocks +title: "@kbn/core-base-browser-mocks" +image: https://source.unsplash.com/400x175/?github +summary: API docs for the @kbn/core-base-browser-mocks plugin +date: 2022-06-17 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-browser-mocks'] +warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. +--- +import kbnCoreBaseBrowserMocksObj from './kbn_core_base_browser_mocks.devdocs.json'; + + + +Contact [Owner missing] for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 3 | 0 | 3 | 0 | + +## Common + +### Objects + + diff --git a/api_docs/kbn_core_base_common.mdx b/api_docs/kbn_core_base_common.mdx index 72c2c0980276c..3e9ff4d92a0f8 100644 --- a/api_docs/kbn_core_base_common.mdx +++ b/api_docs/kbn_core_base_common.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-common title: "@kbn/core-base-common" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-base-common plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-common'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_base_server_mocks.mdx b/api_docs/kbn_core_base_server_mocks.mdx index 787d170d2999f..5369479e0364a 100644 --- a/api_docs/kbn_core_base_server_mocks.mdx +++ b/api_docs/kbn_core_base_server_mocks.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-base-server-mocks title: "@kbn/core-base-server-mocks" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-base-server-mocks plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-server-mocks'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_doc_links_browser.mdx b/api_docs/kbn_core_doc_links_browser.mdx index 1e330436a0ade..67263f141252d 100644 --- a/api_docs/kbn_core_doc_links_browser.mdx +++ b/api_docs/kbn_core_doc_links_browser.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-browser title: "@kbn/core-doc-links-browser" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-doc-links-browser plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-browser'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_doc_links_browser_mocks.mdx b/api_docs/kbn_core_doc_links_browser_mocks.mdx index ed8e1d1140e2f..c67287e5f516e 100644 --- a/api_docs/kbn_core_doc_links_browser_mocks.mdx +++ b/api_docs/kbn_core_doc_links_browser_mocks.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-browser-mocks title: "@kbn/core-doc-links-browser-mocks" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-doc-links-browser-mocks plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-browser-mocks'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_doc_links_server.mdx b/api_docs/kbn_core_doc_links_server.mdx index 36ebea2c7f0f3..30ad2c1234f08 100644 --- a/api_docs/kbn_core_doc_links_server.mdx +++ b/api_docs/kbn_core_doc_links_server.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-server title: "@kbn/core-doc-links-server" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-doc-links-server plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-server'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_doc_links_server_mocks.mdx b/api_docs/kbn_core_doc_links_server_mocks.mdx index 281328a03d6e7..df8236f2c41a6 100644 --- a/api_docs/kbn_core_doc_links_server_mocks.mdx +++ b/api_docs/kbn_core_doc_links_server_mocks.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-doc-links-server-mocks title: "@kbn/core-doc-links-server-mocks" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-doc-links-server-mocks plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-doc-links-server-mocks'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_injected_metadata_browser.mdx b/api_docs/kbn_core_injected_metadata_browser.mdx index 06277e6747b8b..814d708aad31c 100644 --- a/api_docs/kbn_core_injected_metadata_browser.mdx +++ b/api_docs/kbn_core_injected_metadata_browser.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-injected-metadata-browser title: "@kbn/core-injected-metadata-browser" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-injected-metadata-browser plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-injected-metadata-browser'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_injected_metadata_browser_mocks.mdx b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx index 4d3a2fd970b4e..e12dd474f00ba 100644 --- a/api_docs/kbn_core_injected_metadata_browser_mocks.mdx +++ b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-injected-metadata-browser-mocks title: "@kbn/core-injected-metadata-browser-mocks" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-injected-metadata-browser-mocks plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-injected-metadata-browser-mocks'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_theme_browser.mdx b/api_docs/kbn_core_theme_browser.mdx index ccedd8bbb046a..72f84be0166df 100644 --- a/api_docs/kbn_core_theme_browser.mdx +++ b/api_docs/kbn_core_theme_browser.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser title: "@kbn/core-theme-browser" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-theme-browser plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_core_theme_browser_mocks.mdx b/api_docs/kbn_core_theme_browser_mocks.mdx index c5876c0a67dc5..8ebe8ba0f5e74 100644 --- a/api_docs/kbn_core_theme_browser_mocks.mdx +++ b/api_docs/kbn_core_theme_browser_mocks.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-core-theme-browser-mocks title: "@kbn/core-theme-browser-mocks" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/core-theme-browser-mocks plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-theme-browser-mocks'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_crypto.mdx b/api_docs/kbn_crypto.mdx index 6834c06269c11..6cd9730f95152 100644 --- a/api_docs/kbn_crypto.mdx +++ b/api_docs/kbn_crypto.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-crypto title: "@kbn/crypto" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/crypto plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/crypto'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_datemath.mdx b/api_docs/kbn_datemath.mdx index 6df1aa7371d1d..c5fab9bbed628 100644 --- a/api_docs/kbn_datemath.mdx +++ b/api_docs/kbn_datemath.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-datemath title: "@kbn/datemath" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/datemath plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/datemath'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_dev_cli_errors.mdx b/api_docs/kbn_dev_cli_errors.mdx index d4d1d0fc73720..7963efbbd363e 100644 --- a/api_docs/kbn_dev_cli_errors.mdx +++ b/api_docs/kbn_dev_cli_errors.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-dev-cli-errors title: "@kbn/dev-cli-errors" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/dev-cli-errors plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-cli-errors'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_dev_cli_runner.mdx b/api_docs/kbn_dev_cli_runner.mdx index a75484107ace4..eb042da0255f3 100644 --- a/api_docs/kbn_dev_cli_runner.mdx +++ b/api_docs/kbn_dev_cli_runner.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-dev-cli-runner title: "@kbn/dev-cli-runner" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/dev-cli-runner plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-cli-runner'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_dev_proc_runner.mdx b/api_docs/kbn_dev_proc_runner.mdx index 4251ec667fab0..682702495035a 100644 --- a/api_docs/kbn_dev_proc_runner.mdx +++ b/api_docs/kbn_dev_proc_runner.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-dev-proc-runner title: "@kbn/dev-proc-runner" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/dev-proc-runner plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-proc-runner'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_dev_utils.mdx b/api_docs/kbn_dev_utils.mdx index 0bbf9439d5873..9416965929c10 100644 --- a/api_docs/kbn_dev_utils.mdx +++ b/api_docs/kbn_dev_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-dev-utils title: "@kbn/dev-utils" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/dev-utils plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dev-utils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_doc_links.devdocs.json b/api_docs/kbn_doc_links.devdocs.json index 848d48fff3212..6c5cd99cc6bce 100644 --- a/api_docs/kbn_doc_links.devdocs.json +++ b/api_docs/kbn_doc_links.devdocs.json @@ -510,7 +510,7 @@ "label": "securitySolution", "description": [], "signature": [ - "{ readonly trustedApps: string; readonly eventFilters: string; readonly blocklist: string; }" + "{ readonly trustedApps: string; readonly eventFilters: string; readonly blocklist: string; readonly policyResponseTroubleshooting: { full_disk_access: string; }; }" ], "path": "packages/kbn-doc-links/src/types.ts", "deprecated": false diff --git a/api_docs/kbn_doc_links.mdx b/api_docs/kbn_doc_links.mdx index 3de32f29e3948..f67aaa784e29b 100644 --- a/api_docs/kbn_doc_links.mdx +++ b/api_docs/kbn_doc_links.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-doc-links title: "@kbn/doc-links" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/doc-links plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/doc-links'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_docs_utils.mdx b/api_docs/kbn_docs_utils.mdx index 99b674a89b489..909aaa80fa0d2 100644 --- a/api_docs/kbn_docs_utils.mdx +++ b/api_docs/kbn_docs_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-docs-utils title: "@kbn/docs-utils" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/docs-utils plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/docs-utils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_es_archiver.mdx b/api_docs/kbn_es_archiver.mdx index 7dbb52b283616..979f943df99f4 100644 --- a/api_docs/kbn_es_archiver.mdx +++ b/api_docs/kbn_es_archiver.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-es-archiver title: "@kbn/es-archiver" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/es-archiver plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-archiver'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_es_query.mdx b/api_docs/kbn_es_query.mdx index bc84717443339..2f0ff0182ea7c 100644 --- a/api_docs/kbn_es_query.mdx +++ b/api_docs/kbn_es_query.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-es-query title: "@kbn/es-query" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/es-query plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-query'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_eslint_plugin_imports.mdx b/api_docs/kbn_eslint_plugin_imports.mdx index 60a0f79abb762..4e63429f6cb57 100644 --- a/api_docs/kbn_eslint_plugin_imports.mdx +++ b/api_docs/kbn_eslint_plugin_imports.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-eslint-plugin-imports title: "@kbn/eslint-plugin-imports" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/eslint-plugin-imports plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/eslint-plugin-imports'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_field_types.mdx b/api_docs/kbn_field_types.mdx index 1005c2bea691f..a5c39cd1867fa 100644 --- a/api_docs/kbn_field_types.mdx +++ b/api_docs/kbn_field_types.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-field-types title: "@kbn/field-types" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/field-types plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/field-types'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_find_used_node_modules.mdx b/api_docs/kbn_find_used_node_modules.mdx index 59d18084baeee..3326c532d0b6d 100644 --- a/api_docs/kbn_find_used_node_modules.mdx +++ b/api_docs/kbn_find_used_node_modules.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-find-used-node-modules title: "@kbn/find-used-node-modules" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/find-used-node-modules plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/find-used-node-modules'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_generate.mdx b/api_docs/kbn_generate.mdx index d5bf4dd29bbfc..a582ce3ff84de 100644 --- a/api_docs/kbn_generate.mdx +++ b/api_docs/kbn_generate.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-generate title: "@kbn/generate" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/generate plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_handlebars.mdx b/api_docs/kbn_handlebars.mdx index 80e605d285752..870e2c0bf6b71 100644 --- a/api_docs/kbn_handlebars.mdx +++ b/api_docs/kbn_handlebars.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-handlebars title: "@kbn/handlebars" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/handlebars plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/handlebars'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_i18n.mdx b/api_docs/kbn_i18n.mdx index 5e386b9a573a8..72895442ccdb3 100644 --- a/api_docs/kbn_i18n.mdx +++ b/api_docs/kbn_i18n.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-i18n title: "@kbn/i18n" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/i18n plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/i18n'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_import_resolver.mdx b/api_docs/kbn_import_resolver.mdx index 0dbdac2fb6e5f..c2dbc6c3cde98 100644 --- a/api_docs/kbn_import_resolver.mdx +++ b/api_docs/kbn_import_resolver.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-import-resolver title: "@kbn/import-resolver" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/import-resolver plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/import-resolver'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_interpreter.mdx b/api_docs/kbn_interpreter.mdx index 6d9bda35f0ce8..6eb59b9a99fb3 100644 --- a/api_docs/kbn_interpreter.mdx +++ b/api_docs/kbn_interpreter.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-interpreter title: "@kbn/interpreter" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/interpreter plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/interpreter'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_io_ts_utils.mdx b/api_docs/kbn_io_ts_utils.mdx index 3408b86d289ed..e13f2e083108c 100644 --- a/api_docs/kbn_io_ts_utils.mdx +++ b/api_docs/kbn_io_ts_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-io-ts-utils title: "@kbn/io-ts-utils" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/io-ts-utils plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/io-ts-utils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_jest_serializers.mdx b/api_docs/kbn_jest_serializers.mdx index 4eadab6a8a434..c422fa1fdb365 100644 --- a/api_docs/kbn_jest_serializers.mdx +++ b/api_docs/kbn_jest_serializers.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-jest-serializers title: "@kbn/jest-serializers" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/jest-serializers plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/jest-serializers'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_kibana_json_schema.mdx b/api_docs/kbn_kibana_json_schema.mdx index a028ea2818a63..87b0421bb2d95 100644 --- a/api_docs/kbn_kibana_json_schema.mdx +++ b/api_docs/kbn_kibana_json_schema.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-kibana-json-schema title: "@kbn/kibana-json-schema" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/kibana-json-schema plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/kibana-json-schema'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_logging.devdocs.json b/api_docs/kbn_logging.devdocs.json index eb835715a1c77..29d30a1c83b2d 100644 --- a/api_docs/kbn_logging.devdocs.json +++ b/api_docs/kbn_logging.devdocs.json @@ -630,7 +630,7 @@ "label": "EcsEventKind", "description": [], "signature": [ - "\"alert\" | \"state\" | \"metric\" | \"event\" | \"signal\" | \"pipeline_error\"" + "\"metric\" | \"alert\" | \"state\" | \"event\" | \"signal\" | \"pipeline_error\"" ], "path": "packages/kbn-logging/src/ecs/event.ts", "deprecated": false, diff --git a/api_docs/kbn_logging.mdx b/api_docs/kbn_logging.mdx index 4ab1195c94323..ba71b449bb39b 100644 --- a/api_docs/kbn_logging.mdx +++ b/api_docs/kbn_logging.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-logging title: "@kbn/logging" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/logging plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/logging'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_logging_mocks.mdx b/api_docs/kbn_logging_mocks.mdx index f0a5d54da56e8..157a04b180d39 100644 --- a/api_docs/kbn_logging_mocks.mdx +++ b/api_docs/kbn_logging_mocks.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-logging-mocks title: "@kbn/logging-mocks" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/logging-mocks plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/logging-mocks'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_mapbox_gl.mdx b/api_docs/kbn_mapbox_gl.mdx index 1e2273b6fba5d..8d7307cabb758 100644 --- a/api_docs/kbn_mapbox_gl.mdx +++ b/api_docs/kbn_mapbox_gl.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-mapbox-gl title: "@kbn/mapbox-gl" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/mapbox-gl plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/mapbox-gl'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_monaco.mdx b/api_docs/kbn_monaco.mdx index 48cc0d76f4079..b1529587ed608 100644 --- a/api_docs/kbn_monaco.mdx +++ b/api_docs/kbn_monaco.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-monaco title: "@kbn/monaco" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/monaco plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/monaco'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_optimizer.mdx b/api_docs/kbn_optimizer.mdx index a6f3fcd10d02f..71e04274c6e5a 100644 --- a/api_docs/kbn_optimizer.mdx +++ b/api_docs/kbn_optimizer.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-optimizer title: "@kbn/optimizer" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/optimizer plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/optimizer'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_optimizer_webpack_helpers.mdx b/api_docs/kbn_optimizer_webpack_helpers.mdx index e3be04849895b..709cd3289f655 100644 --- a/api_docs/kbn_optimizer_webpack_helpers.mdx +++ b/api_docs/kbn_optimizer_webpack_helpers.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-optimizer-webpack-helpers title: "@kbn/optimizer-webpack-helpers" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/optimizer-webpack-helpers plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/optimizer-webpack-helpers'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_performance_testing_dataset_extractor.mdx b/api_docs/kbn_performance_testing_dataset_extractor.mdx index 32e132773b18d..1493a49ad177b 100644 --- a/api_docs/kbn_performance_testing_dataset_extractor.mdx +++ b/api_docs/kbn_performance_testing_dataset_extractor.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-performance-testing-dataset-extractor title: "@kbn/performance-testing-dataset-extractor" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/performance-testing-dataset-extractor plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/performance-testing-dataset-extractor'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_plugin_discovery.mdx b/api_docs/kbn_plugin_discovery.mdx index ba61f1d92322e..ba0b6561f4ecf 100644 --- a/api_docs/kbn_plugin_discovery.mdx +++ b/api_docs/kbn_plugin_discovery.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-discovery title: "@kbn/plugin-discovery" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/plugin-discovery plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-discovery'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_plugin_generator.mdx b/api_docs/kbn_plugin_generator.mdx index 830e24508a95e..d27a33e3cbf00 100644 --- a/api_docs/kbn_plugin_generator.mdx +++ b/api_docs/kbn_plugin_generator.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-generator title: "@kbn/plugin-generator" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/plugin-generator plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-generator'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_plugin_helpers.mdx b/api_docs/kbn_plugin_helpers.mdx index 221dcc20a5baf..e482e27a9cca7 100644 --- a/api_docs/kbn_plugin_helpers.mdx +++ b/api_docs/kbn_plugin_helpers.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-plugin-helpers title: "@kbn/plugin-helpers" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/plugin-helpers plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/plugin-helpers'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_pm.mdx b/api_docs/kbn_pm.mdx index 68ecdaa011c7f..9a0e2f6ca782b 100644 --- a/api_docs/kbn_pm.mdx +++ b/api_docs/kbn_pm.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-pm title: "@kbn/pm" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/pm plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/pm'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_react_field.mdx b/api_docs/kbn_react_field.mdx index 66a1c9888ea1c..3c6f40ac4dc24 100644 --- a/api_docs/kbn_react_field.mdx +++ b/api_docs/kbn_react_field.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-react-field title: "@kbn/react-field" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/react-field plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-field'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_rule_data_utils.mdx b/api_docs/kbn_rule_data_utils.mdx index 9459554d95b74..49518a7bd805f 100644 --- a/api_docs/kbn_rule_data_utils.mdx +++ b/api_docs/kbn_rule_data_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-rule-data-utils title: "@kbn/rule-data-utils" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/rule-data-utils plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rule-data-utils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_scalability_simulation_generator.mdx b/api_docs/kbn_scalability_simulation_generator.mdx index 73b7c83f52ae1..e68ef70dfefc6 100644 --- a/api_docs/kbn_scalability_simulation_generator.mdx +++ b/api_docs/kbn_scalability_simulation_generator.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-scalability-simulation-generator title: "@kbn/scalability-simulation-generator" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/scalability-simulation-generator plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/scalability-simulation-generator'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_autocomplete.mdx b/api_docs/kbn_securitysolution_autocomplete.mdx index cf71b5468640a..b877e49a3c029 100644 --- a/api_docs/kbn_securitysolution_autocomplete.mdx +++ b/api_docs/kbn_securitysolution_autocomplete.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-autocomplete title: "@kbn/securitysolution-autocomplete" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-autocomplete plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-autocomplete'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_es_utils.mdx b/api_docs/kbn_securitysolution_es_utils.mdx index 191c8c4b74769..392944bcb48ce 100644 --- a/api_docs/kbn_securitysolution_es_utils.mdx +++ b/api_docs/kbn_securitysolution_es_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-es-utils title: "@kbn/securitysolution-es-utils" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-es-utils plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-es-utils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_hook_utils.mdx b/api_docs/kbn_securitysolution_hook_utils.mdx index 416c5bb57d30c..3a6219c2658c6 100644 --- a/api_docs/kbn_securitysolution_hook_utils.mdx +++ b/api_docs/kbn_securitysolution_hook_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-hook-utils title: "@kbn/securitysolution-hook-utils" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-hook-utils plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-hook-utils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx b/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx index 1635127fd5d2d..03ae9fd00c16f 100644 --- a/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-alerting-types title: "@kbn/securitysolution-io-ts-alerting-types" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-io-ts-alerting-types plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-alerting-types'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_io_ts_list_types.mdx b/api_docs/kbn_securitysolution_io_ts_list_types.mdx index 3ef212040e182..3ff41dbe18148 100644 --- a/api_docs/kbn_securitysolution_io_ts_list_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_list_types.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-list-types title: "@kbn/securitysolution-io-ts-list-types" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-io-ts-list-types plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-list-types'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_io_ts_types.mdx b/api_docs/kbn_securitysolution_io_ts_types.mdx index 623afbeed9b83..393a487d4bc4b 100644 --- a/api_docs/kbn_securitysolution_io_ts_types.mdx +++ b/api_docs/kbn_securitysolution_io_ts_types.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-types title: "@kbn/securitysolution-io-ts-types" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-io-ts-types plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-types'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_io_ts_utils.mdx b/api_docs/kbn_securitysolution_io_ts_utils.mdx index 06fd7e9469a3d..51aeddad968fd 100644 --- a/api_docs/kbn_securitysolution_io_ts_utils.mdx +++ b/api_docs/kbn_securitysolution_io_ts_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-io-ts-utils title: "@kbn/securitysolution-io-ts-utils" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-io-ts-utils plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-utils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_list_api.mdx b/api_docs/kbn_securitysolution_list_api.mdx index 48dac19441912..acbf663d3273a 100644 --- a/api_docs/kbn_securitysolution_list_api.mdx +++ b/api_docs/kbn_securitysolution_list_api.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-api title: "@kbn/securitysolution-list-api" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-list-api plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-api'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_list_constants.mdx b/api_docs/kbn_securitysolution_list_constants.mdx index df75588d4d575..dc3cd07644998 100644 --- a/api_docs/kbn_securitysolution_list_constants.mdx +++ b/api_docs/kbn_securitysolution_list_constants.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-constants title: "@kbn/securitysolution-list-constants" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-list-constants plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-constants'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_list_hooks.mdx b/api_docs/kbn_securitysolution_list_hooks.mdx index 6d655aac0aef3..ebe5deb1f5250 100644 --- a/api_docs/kbn_securitysolution_list_hooks.mdx +++ b/api_docs/kbn_securitysolution_list_hooks.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-hooks title: "@kbn/securitysolution-list-hooks" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-list-hooks plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-hooks'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_list_utils.mdx b/api_docs/kbn_securitysolution_list_utils.mdx index 67e0ec1be06cb..0e97eeb1d4a38 100644 --- a/api_docs/kbn_securitysolution_list_utils.mdx +++ b/api_docs/kbn_securitysolution_list_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-list-utils title: "@kbn/securitysolution-list-utils" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-list-utils plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-utils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_rules.mdx b/api_docs/kbn_securitysolution_rules.mdx index f422a119097a6..f97548e09ee49 100644 --- a/api_docs/kbn_securitysolution_rules.mdx +++ b/api_docs/kbn_securitysolution_rules.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-rules title: "@kbn/securitysolution-rules" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-rules plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-rules'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_t_grid.mdx b/api_docs/kbn_securitysolution_t_grid.mdx index 7058b40f4c5ac..e0da9e631a9aa 100644 --- a/api_docs/kbn_securitysolution_t_grid.mdx +++ b/api_docs/kbn_securitysolution_t_grid.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-t-grid title: "@kbn/securitysolution-t-grid" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-t-grid plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-t-grid'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_securitysolution_utils.mdx b/api_docs/kbn_securitysolution_utils.mdx index 9fecb35c397d4..f2c49a4344bd6 100644 --- a/api_docs/kbn_securitysolution_utils.mdx +++ b/api_docs/kbn_securitysolution_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-utils title: "@kbn/securitysolution-utils" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/securitysolution-utils plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-utils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_server_http_tools.mdx b/api_docs/kbn_server_http_tools.mdx index 7695de91fa574..6919db327def9 100644 --- a/api_docs/kbn_server_http_tools.mdx +++ b/api_docs/kbn_server_http_tools.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-server-http-tools title: "@kbn/server-http-tools" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/server-http-tools plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-http-tools'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_server_route_repository.mdx b/api_docs/kbn_server_route_repository.mdx index 17374e7faccaf..3935e723c61b6 100644 --- a/api_docs/kbn_server_route_repository.mdx +++ b/api_docs/kbn_server_route_repository.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-server-route-repository title: "@kbn/server-route-repository" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/server-route-repository plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/server-route-repository'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_shared_ux_components.mdx b/api_docs/kbn_shared_ux_components.mdx index 7557aa98eb1a1..2c258ed860458 100644 --- a/api_docs/kbn_shared_ux_components.mdx +++ b/api_docs/kbn_shared_ux_components.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-components title: "@kbn/shared-ux-components" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/shared-ux-components plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-components'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_shared_ux_page_analytics_no_data.mdx b/api_docs/kbn_shared_ux_page_analytics_no_data.mdx index 9e858743aca43..2e4fdb79ca33d 100644 --- a/api_docs/kbn_shared_ux_page_analytics_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_analytics_no_data.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-analytics-no-data title: "@kbn/shared-ux-page-analytics-no-data" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/shared-ux-page-analytics-no-data plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-analytics-no-data'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_shared_ux_page_kibana_no_data.mdx b/api_docs/kbn_shared_ux_page_kibana_no_data.mdx index 2c89e6a111f62..5ffd4b52d5b51 100644 --- a/api_docs/kbn_shared_ux_page_kibana_no_data.mdx +++ b/api_docs/kbn_shared_ux_page_kibana_no_data.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-page-kibana-no-data title: "@kbn/shared-ux-page-kibana-no-data" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/shared-ux-page-kibana-no-data plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-page-kibana-no-data'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_shared_ux_prompt_no_data_views.mdx b/api_docs/kbn_shared_ux_prompt_no_data_views.mdx index 24797800e3ad6..842ac36e7e27a 100644 --- a/api_docs/kbn_shared_ux_prompt_no_data_views.mdx +++ b/api_docs/kbn_shared_ux_prompt_no_data_views.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-no-data-views title: "@kbn/shared-ux-prompt-no-data-views" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/shared-ux-prompt-no-data-views plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-no-data-views'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_shared_ux_services.mdx b/api_docs/kbn_shared_ux_services.mdx index 639f9ede5025d..14c40c014d15c 100644 --- a/api_docs/kbn_shared_ux_services.mdx +++ b/api_docs/kbn_shared_ux_services.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-services title: "@kbn/shared-ux-services" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/shared-ux-services plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-services'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_shared_ux_storybook.mdx b/api_docs/kbn_shared_ux_storybook.mdx index c8cab050fb5f0..e1ff71868f097 100644 --- a/api_docs/kbn_shared_ux_storybook.mdx +++ b/api_docs/kbn_shared_ux_storybook.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-storybook title: "@kbn/shared-ux-storybook" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/shared-ux-storybook plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-storybook'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_shared_ux_utility.mdx b/api_docs/kbn_shared_ux_utility.mdx index bb54681d731e4..8c1e375041264 100644 --- a/api_docs/kbn_shared_ux_utility.mdx +++ b/api_docs/kbn_shared_ux_utility.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-utility title: "@kbn/shared-ux-utility" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/shared-ux-utility plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-utility'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_sort_package_json.mdx b/api_docs/kbn_sort_package_json.mdx index 9f8a72cb41b84..eb1f54528e50f 100644 --- a/api_docs/kbn_sort_package_json.mdx +++ b/api_docs/kbn_sort_package_json.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-sort-package-json title: "@kbn/sort-package-json" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/sort-package-json plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/sort-package-json'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_std.mdx b/api_docs/kbn_std.mdx index c37401c0aa6fb..d7d45419dd412 100644 --- a/api_docs/kbn_std.mdx +++ b/api_docs/kbn_std.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-std title: "@kbn/std" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/std plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/std'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_stdio_dev_helpers.mdx b/api_docs/kbn_stdio_dev_helpers.mdx index 0e72be0e49c15..9e767b31b5b6b 100644 --- a/api_docs/kbn_stdio_dev_helpers.mdx +++ b/api_docs/kbn_stdio_dev_helpers.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-stdio-dev-helpers title: "@kbn/stdio-dev-helpers" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/stdio-dev-helpers plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/stdio-dev-helpers'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_storybook.mdx b/api_docs/kbn_storybook.mdx index b72301ee0e856..d7602a2006000 100644 --- a/api_docs/kbn_storybook.mdx +++ b/api_docs/kbn_storybook.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-storybook title: "@kbn/storybook" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/storybook plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/storybook'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_telemetry_tools.mdx b/api_docs/kbn_telemetry_tools.mdx index 100149cff3ef2..64f73d996436d 100644 --- a/api_docs/kbn_telemetry_tools.mdx +++ b/api_docs/kbn_telemetry_tools.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-telemetry-tools title: "@kbn/telemetry-tools" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/telemetry-tools plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/telemetry-tools'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_test.mdx b/api_docs/kbn_test.mdx index ab97811f0ce3a..cf7531f5a03bf 100644 --- a/api_docs/kbn_test.mdx +++ b/api_docs/kbn_test.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-test title: "@kbn/test" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/test plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_test_jest_helpers.mdx b/api_docs/kbn_test_jest_helpers.mdx index 3d9854af58aaa..5cbedbbd4f208 100644 --- a/api_docs/kbn_test_jest_helpers.mdx +++ b/api_docs/kbn_test_jest_helpers.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-test-jest-helpers title: "@kbn/test-jest-helpers" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/test-jest-helpers plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-jest-helpers'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_tooling_log.mdx b/api_docs/kbn_tooling_log.mdx index 9f4c5e6ccccba..c1af03730b727 100644 --- a/api_docs/kbn_tooling_log.mdx +++ b/api_docs/kbn_tooling_log.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-tooling-log title: "@kbn/tooling-log" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/tooling-log plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/tooling-log'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_type_summarizer.mdx b/api_docs/kbn_type_summarizer.mdx index a49f048a50d29..606d5d1faff4c 100644 --- a/api_docs/kbn_type_summarizer.mdx +++ b/api_docs/kbn_type_summarizer.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-type-summarizer title: "@kbn/type-summarizer" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/type-summarizer plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/type-summarizer'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_typed_react_router_config.mdx b/api_docs/kbn_typed_react_router_config.mdx index 98b3f6deb66f6..e84cb275d20a6 100644 --- a/api_docs/kbn_typed_react_router_config.mdx +++ b/api_docs/kbn_typed_react_router_config.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-typed-react-router-config title: "@kbn/typed-react-router-config" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/typed-react-router-config plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/typed-react-router-config'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_ui_theme.mdx b/api_docs/kbn_ui_theme.mdx index 87738602829e4..442ef904b0b0a 100644 --- a/api_docs/kbn_ui_theme.mdx +++ b/api_docs/kbn_ui_theme.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-ui-theme title: "@kbn/ui-theme" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/ui-theme plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-theme'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_utility_types.mdx b/api_docs/kbn_utility_types.mdx index df4a858e5df42..1b9b85b9d594f 100644 --- a/api_docs/kbn_utility_types.mdx +++ b/api_docs/kbn_utility_types.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-utility-types title: "@kbn/utility-types" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/utility-types plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utility-types'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_utility_types_jest.mdx b/api_docs/kbn_utility_types_jest.mdx index cbd1b729a195d..b8bbdc07bad15 100644 --- a/api_docs/kbn_utility_types_jest.mdx +++ b/api_docs/kbn_utility_types_jest.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-utility-types-jest title: "@kbn/utility-types-jest" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/utility-types-jest plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utility-types-jest'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kbn_utils.mdx b/api_docs/kbn_utils.mdx index d726ead3e62eb..5c5ff15678791 100644 --- a/api_docs/kbn_utils.mdx +++ b/api_docs/kbn_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kbn-utils title: "@kbn/utils" image: https://source.unsplash.com/400x175/?github summary: API docs for the @kbn/utils plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/utils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kibana_overview.mdx b/api_docs/kibana_overview.mdx index ea49fd4401444..edd89efc527cb 100644 --- a/api_docs/kibana_overview.mdx +++ b/api_docs/kibana_overview.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kibanaOverview title: "kibanaOverview" image: https://source.unsplash.com/400x175/?github summary: API docs for the kibanaOverview plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaOverview'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kibana_react.devdocs.json b/api_docs/kibana_react.devdocs.json index cf9cfeb6d5569..10710db8a00bf 100644 --- a/api_docs/kibana_react.devdocs.json +++ b/api_docs/kibana_react.devdocs.json @@ -4383,13 +4383,7 @@ "description": [], "signature": [ "{ analytics?: ", - { - "pluginId": "core", - "scope": "public", - "docId": "kibCorePluginApi", - "section": "def-public.AnalyticsServiceStart", - "text": "AnalyticsServiceStart" - }, + "AnalyticsServiceStart", " | undefined; application?: ", { "pluginId": "core", diff --git a/api_docs/kibana_react.mdx b/api_docs/kibana_react.mdx index b4d3fcaea8d48..127311d55587f 100644 --- a/api_docs/kibana_react.mdx +++ b/api_docs/kibana_react.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kibanaReact title: "kibanaReact" image: https://source.unsplash.com/400x175/?github summary: API docs for the kibanaReact plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaReact'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kibana_utils.mdx b/api_docs/kibana_utils.mdx index 81408535d4cd1..54fbfec6cdf69 100644 --- a/api_docs/kibana_utils.mdx +++ b/api_docs/kibana_utils.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kibanaUtils title: "kibanaUtils" image: https://source.unsplash.com/400x175/?github summary: API docs for the kibanaUtils plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kibanaUtils'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/kubernetes_security.mdx b/api_docs/kubernetes_security.mdx index 058c1cbe5700b..3252797528030 100644 --- a/api_docs/kubernetes_security.mdx +++ b/api_docs/kubernetes_security.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/kubernetesSecurity title: "kubernetesSecurity" image: https://source.unsplash.com/400x175/?github summary: API docs for the kubernetesSecurity plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'kubernetesSecurity'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/lens.devdocs.json b/api_docs/lens.devdocs.json index 7045342fdff0d..b0c77e740df69 100644 --- a/api_docs/lens.devdocs.json +++ b/api_docs/lens.devdocs.json @@ -6857,7 +6857,7 @@ "section": "def-common.GaugeState", "text": "GaugeState" }, - ", \"goal\" | \"min\" | \"max\" | \"metric\"> & { metricAccessor?: string | undefined; minAccessor?: string | undefined; maxAccessor?: string | undefined; goalAccessor?: string | undefined; } & { layerId: string; layerType: ", + ", \"goal\" | \"metric\" | \"min\" | \"max\"> & { metricAccessor?: string | undefined; minAccessor?: string | undefined; maxAccessor?: string | undefined; goalAccessor?: string | undefined; } & { layerId: string; layerType: ", { "pluginId": "lens", "scope": "common", diff --git a/api_docs/lens.mdx b/api_docs/lens.mdx index fdbee38c92fc4..ba929e3728a69 100644 --- a/api_docs/lens.mdx +++ b/api_docs/lens.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/lens title: "lens" image: https://source.unsplash.com/400x175/?github summary: API docs for the lens plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lens'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/license_api_guard.mdx b/api_docs/license_api_guard.mdx index dd8bf621ec14c..b715981ec5e13 100644 --- a/api_docs/license_api_guard.mdx +++ b/api_docs/license_api_guard.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/licenseApiGuard title: "licenseApiGuard" image: https://source.unsplash.com/400x175/?github summary: API docs for the licenseApiGuard plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licenseApiGuard'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/license_management.mdx b/api_docs/license_management.mdx index c25f78dce1a58..6d0d51071820f 100644 --- a/api_docs/license_management.mdx +++ b/api_docs/license_management.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/licenseManagement title: "licenseManagement" image: https://source.unsplash.com/400x175/?github summary: API docs for the licenseManagement plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licenseManagement'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/licensing.mdx b/api_docs/licensing.mdx index aa648f86afce0..b203a0c81433a 100644 --- a/api_docs/licensing.mdx +++ b/api_docs/licensing.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/licensing title: "licensing" image: https://source.unsplash.com/400x175/?github summary: API docs for the licensing plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licensing'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/lists.mdx b/api_docs/lists.mdx index 4c63d3e08a297..0beeaa4a83069 100644 --- a/api_docs/lists.mdx +++ b/api_docs/lists.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/lists title: "lists" image: https://source.unsplash.com/400x175/?github summary: API docs for the lists plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'lists'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/management.mdx b/api_docs/management.mdx index 4c673466335ba..dee3f0cada74f 100644 --- a/api_docs/management.mdx +++ b/api_docs/management.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/management title: "management" image: https://source.unsplash.com/400x175/?github summary: API docs for the management plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'management'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/maps.mdx b/api_docs/maps.mdx index 75449a04fff5b..acd77d3cf9a5a 100644 --- a/api_docs/maps.mdx +++ b/api_docs/maps.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/maps title: "maps" image: https://source.unsplash.com/400x175/?github summary: API docs for the maps plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'maps'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/maps_ems.mdx b/api_docs/maps_ems.mdx index 7ef0840638c70..fb0bc148d18c0 100644 --- a/api_docs/maps_ems.mdx +++ b/api_docs/maps_ems.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/mapsEms title: "mapsEms" image: https://source.unsplash.com/400x175/?github summary: API docs for the mapsEms plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'mapsEms'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/ml.mdx b/api_docs/ml.mdx index bb0d485e98714..c713a39180419 100644 --- a/api_docs/ml.mdx +++ b/api_docs/ml.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/ml title: "ml" image: https://source.unsplash.com/400x175/?github summary: API docs for the ml plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ml'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/monitoring.mdx b/api_docs/monitoring.mdx index 42d653f944912..4e1ce81202ba3 100644 --- a/api_docs/monitoring.mdx +++ b/api_docs/monitoring.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/monitoring title: "monitoring" image: https://source.unsplash.com/400x175/?github summary: API docs for the monitoring plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'monitoring'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/monitoring_collection.mdx b/api_docs/monitoring_collection.mdx index b752aff4d64a5..e5f02ba72d852 100644 --- a/api_docs/monitoring_collection.mdx +++ b/api_docs/monitoring_collection.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/monitoringCollection title: "monitoringCollection" image: https://source.unsplash.com/400x175/?github summary: API docs for the monitoringCollection plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'monitoringCollection'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/navigation.mdx b/api_docs/navigation.mdx index d714e58941a58..27f94cc517ad2 100644 --- a/api_docs/navigation.mdx +++ b/api_docs/navigation.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/navigation title: "navigation" image: https://source.unsplash.com/400x175/?github summary: API docs for the navigation plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'navigation'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/newsfeed.mdx b/api_docs/newsfeed.mdx index 9d5bc311a2517..68cd556717b01 100644 --- a/api_docs/newsfeed.mdx +++ b/api_docs/newsfeed.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/newsfeed title: "newsfeed" image: https://source.unsplash.com/400x175/?github summary: API docs for the newsfeed plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'newsfeed'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/observability.mdx b/api_docs/observability.mdx index c05e264dfe97d..a0b77deedca0d 100644 --- a/api_docs/observability.mdx +++ b/api_docs/observability.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/observability title: "observability" image: https://source.unsplash.com/400x175/?github summary: API docs for the observability plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observability'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/osquery.mdx b/api_docs/osquery.mdx index aeb1d77a08452..1b8fa17dc7afe 100644 --- a/api_docs/osquery.mdx +++ b/api_docs/osquery.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/osquery title: "osquery" image: https://source.unsplash.com/400x175/?github summary: API docs for the osquery plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'osquery'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/plugin_directory.mdx b/api_docs/plugin_directory.mdx index bbf0d8f40b09b..ed3aec86b355a 100644 --- a/api_docs/plugin_directory.mdx +++ b/api_docs/plugin_directory.mdx @@ -3,7 +3,7 @@ id: kibDevDocsPluginDirectory slug: /kibana-dev-docs/api-meta/plugin-api-directory title: Directory summary: Directory of public APIs available through plugins or packages. -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- @@ -12,19 +12,19 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Count | Plugins or Packages with a
public API | Number of teams | |--------------|----------|------------------------| -| 283 | 226 | 35 | +| 287 | 230 | 35 | ### Public API health stats | API Count | Any Count | Missing comments | Missing exports | |--------------|----------|-----------------|--------| -| 26521 | 172 | 19084 | 1238 | +| 26538 | 172 | 19099 | 1241 | ## Plugin Directory | Plugin name           | Maintaining team | Description | API Cnt | Any Cnt | Missing
comments | Missing
exports | |--------------|----------------|-----------|--------------|----------|---------------|--------| -| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 240 | 0 | 235 | 19 | +| | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 241 | 0 | 236 | 19 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 23 | 0 | 19 | 1 | | | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | AIOps plugin maintained by ML team. | 11 | 0 | 0 | 0 | | | [Response Ops](https://github.com/orgs/elastic/teams/response-ops) | - | 359 | 0 | 350 | 20 | @@ -192,6 +192,10 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [Owner missing] | - | 73 | 0 | 44 | 1 | | | [Owner missing] | - | 16 | 0 | 16 | 0 | | | [Owner missing] | - | 129 | 3 | 127 | 17 | +| | [Owner missing] | - | 2 | 0 | 0 | 0 | +| | [Owner missing] | - | 7 | 0 | 7 | 1 | +| | [Owner missing] | - | 4 | 0 | 4 | 0 | +| | [Owner missing] | - | 3 | 0 | 3 | 0 | | | [Owner missing] | - | 12 | 0 | 3 | 0 | | | [Owner missing] | - | 3 | 0 | 3 | 0 | | | [Owner missing] | - | 4 | 0 | 4 | 0 | diff --git a/api_docs/presentation_util.mdx b/api_docs/presentation_util.mdx index 20930abddf141..7e3c34036a499 100644 --- a/api_docs/presentation_util.mdx +++ b/api_docs/presentation_util.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/presentationUtil title: "presentationUtil" image: https://source.unsplash.com/400x175/?github summary: API docs for the presentationUtil plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'presentationUtil'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/remote_clusters.mdx b/api_docs/remote_clusters.mdx index 7478e66b31c92..ae839caa65fb0 100644 --- a/api_docs/remote_clusters.mdx +++ b/api_docs/remote_clusters.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/remoteClusters title: "remoteClusters" image: https://source.unsplash.com/400x175/?github summary: API docs for the remoteClusters plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'remoteClusters'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/reporting.mdx b/api_docs/reporting.mdx index eff6da111ac5b..f1efebbc45e01 100644 --- a/api_docs/reporting.mdx +++ b/api_docs/reporting.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/reporting title: "reporting" image: https://source.unsplash.com/400x175/?github summary: API docs for the reporting plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'reporting'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/rollup.mdx b/api_docs/rollup.mdx index b0f56a234ac5b..260262c77889b 100644 --- a/api_docs/rollup.mdx +++ b/api_docs/rollup.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/rollup title: "rollup" image: https://source.unsplash.com/400x175/?github summary: API docs for the rollup plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'rollup'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/rule_registry.mdx b/api_docs/rule_registry.mdx index a0a0a693a26b9..364b4731b9f05 100644 --- a/api_docs/rule_registry.mdx +++ b/api_docs/rule_registry.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/ruleRegistry title: "ruleRegistry" image: https://source.unsplash.com/400x175/?github summary: API docs for the ruleRegistry plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ruleRegistry'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/runtime_fields.mdx b/api_docs/runtime_fields.mdx index 2087f6f1ec55f..8516236e1dcd9 100644 --- a/api_docs/runtime_fields.mdx +++ b/api_docs/runtime_fields.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/runtimeFields title: "runtimeFields" image: https://source.unsplash.com/400x175/?github summary: API docs for the runtimeFields plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'runtimeFields'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/saved_objects.mdx b/api_docs/saved_objects.mdx index b673115fe860b..2cb7bbadc983b 100644 --- a/api_docs/saved_objects.mdx +++ b/api_docs/saved_objects.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/savedObjects title: "savedObjects" image: https://source.unsplash.com/400x175/?github summary: API docs for the savedObjects plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjects'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/saved_objects_management.mdx b/api_docs/saved_objects_management.mdx index bbd52111d216c..fa5b93807b40d 100644 --- a/api_docs/saved_objects_management.mdx +++ b/api_docs/saved_objects_management.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/savedObjectsManagement title: "savedObjectsManagement" image: https://source.unsplash.com/400x175/?github summary: API docs for the savedObjectsManagement plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsManagement'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/saved_objects_tagging.mdx b/api_docs/saved_objects_tagging.mdx index 548f76a8b6fe2..3236c24b72bad 100644 --- a/api_docs/saved_objects_tagging.mdx +++ b/api_docs/saved_objects_tagging.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/savedObjectsTagging title: "savedObjectsTagging" image: https://source.unsplash.com/400x175/?github summary: API docs for the savedObjectsTagging plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsTagging'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/saved_objects_tagging_oss.mdx b/api_docs/saved_objects_tagging_oss.mdx index 0c40d26d95d7f..904146a129e50 100644 --- a/api_docs/saved_objects_tagging_oss.mdx +++ b/api_docs/saved_objects_tagging_oss.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/savedObjectsTaggingOss title: "savedObjectsTaggingOss" image: https://source.unsplash.com/400x175/?github summary: API docs for the savedObjectsTaggingOss plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'savedObjectsTaggingOss'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/screenshot_mode.mdx b/api_docs/screenshot_mode.mdx index 8b7bc2df3a995..b966e8a251f92 100644 --- a/api_docs/screenshot_mode.mdx +++ b/api_docs/screenshot_mode.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/screenshotMode title: "screenshotMode" image: https://source.unsplash.com/400x175/?github summary: API docs for the screenshotMode plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'screenshotMode'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/screenshotting.mdx b/api_docs/screenshotting.mdx index f9390f7570258..06c71f49159b4 100644 --- a/api_docs/screenshotting.mdx +++ b/api_docs/screenshotting.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/screenshotting title: "screenshotting" image: https://source.unsplash.com/400x175/?github summary: API docs for the screenshotting plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'screenshotting'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/security.mdx b/api_docs/security.mdx index 16d9f6a9ff667..4518615f3bed7 100644 --- a/api_docs/security.mdx +++ b/api_docs/security.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/security title: "security" image: https://source.unsplash.com/400x175/?github summary: API docs for the security plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'security'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/security_solution.mdx b/api_docs/security_solution.mdx index 930ff2ae95240..c3c7036439d7e 100644 --- a/api_docs/security_solution.mdx +++ b/api_docs/security_solution.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/securitySolution title: "securitySolution" image: https://source.unsplash.com/400x175/?github summary: API docs for the securitySolution plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'securitySolution'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/session_view.mdx b/api_docs/session_view.mdx index 3ced7ddd2ac58..50aa98aa0108a 100644 --- a/api_docs/session_view.mdx +++ b/api_docs/session_view.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/sessionView title: "sessionView" image: https://source.unsplash.com/400x175/?github summary: API docs for the sessionView plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'sessionView'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/share.mdx b/api_docs/share.mdx index 0a71613e949f2..e1a471d336d98 100644 --- a/api_docs/share.mdx +++ b/api_docs/share.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/share title: "share" image: https://source.unsplash.com/400x175/?github summary: API docs for the share plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'share'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/shared_u_x.mdx b/api_docs/shared_u_x.mdx index b72b705e16565..2ac1139d44805 100644 --- a/api_docs/shared_u_x.mdx +++ b/api_docs/shared_u_x.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/sharedUX title: "sharedUX" image: https://source.unsplash.com/400x175/?github summary: API docs for the sharedUX plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'sharedUX'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/snapshot_restore.mdx b/api_docs/snapshot_restore.mdx index c536b0d7c1519..16c511aa7406c 100644 --- a/api_docs/snapshot_restore.mdx +++ b/api_docs/snapshot_restore.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/snapshotRestore title: "snapshotRestore" image: https://source.unsplash.com/400x175/?github summary: API docs for the snapshotRestore plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'snapshotRestore'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/spaces.mdx b/api_docs/spaces.mdx index 896ff51177780..7f3821890bbce 100644 --- a/api_docs/spaces.mdx +++ b/api_docs/spaces.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/spaces title: "spaces" image: https://source.unsplash.com/400x175/?github summary: API docs for the spaces plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'spaces'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/stack_alerts.mdx b/api_docs/stack_alerts.mdx index 1c483f7415e98..42bc2368b94f9 100644 --- a/api_docs/stack_alerts.mdx +++ b/api_docs/stack_alerts.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/stackAlerts title: "stackAlerts" image: https://source.unsplash.com/400x175/?github summary: API docs for the stackAlerts plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'stackAlerts'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/task_manager.mdx b/api_docs/task_manager.mdx index b5aefb462d8f0..8c7110479b2a8 100644 --- a/api_docs/task_manager.mdx +++ b/api_docs/task_manager.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/taskManager title: "taskManager" image: https://source.unsplash.com/400x175/?github summary: API docs for the taskManager plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'taskManager'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/telemetry.mdx b/api_docs/telemetry.mdx index 62375b5cfdbd0..8ad7dc5b483e4 100644 --- a/api_docs/telemetry.mdx +++ b/api_docs/telemetry.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/telemetry title: "telemetry" image: https://source.unsplash.com/400x175/?github summary: API docs for the telemetry plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetry'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/telemetry_collection_manager.mdx b/api_docs/telemetry_collection_manager.mdx index d90ce413ad60b..264d86f33babc 100644 --- a/api_docs/telemetry_collection_manager.mdx +++ b/api_docs/telemetry_collection_manager.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/telemetryCollectionManager title: "telemetryCollectionManager" image: https://source.unsplash.com/400x175/?github summary: API docs for the telemetryCollectionManager plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryCollectionManager'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/telemetry_collection_xpack.mdx b/api_docs/telemetry_collection_xpack.mdx index b2c67c19bff4a..395d9c9170b04 100644 --- a/api_docs/telemetry_collection_xpack.mdx +++ b/api_docs/telemetry_collection_xpack.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/telemetryCollectionXpack title: "telemetryCollectionXpack" image: https://source.unsplash.com/400x175/?github summary: API docs for the telemetryCollectionXpack plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryCollectionXpack'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/telemetry_management_section.mdx b/api_docs/telemetry_management_section.mdx index 5236f864bbec8..2bdac69098e60 100644 --- a/api_docs/telemetry_management_section.mdx +++ b/api_docs/telemetry_management_section.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/telemetryManagementSection title: "telemetryManagementSection" image: https://source.unsplash.com/400x175/?github summary: API docs for the telemetryManagementSection plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'telemetryManagementSection'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/timelines.mdx b/api_docs/timelines.mdx index 2383a376db94f..283d5fddf5ad1 100644 --- a/api_docs/timelines.mdx +++ b/api_docs/timelines.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/timelines title: "timelines" image: https://source.unsplash.com/400x175/?github summary: API docs for the timelines plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'timelines'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/transform.mdx b/api_docs/transform.mdx index 30975b87dc4db..c73be6a3b325b 100644 --- a/api_docs/transform.mdx +++ b/api_docs/transform.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/transform title: "transform" image: https://source.unsplash.com/400x175/?github summary: API docs for the transform plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'transform'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/triggers_actions_ui.mdx b/api_docs/triggers_actions_ui.mdx index 8629252346fbc..8744787a89320 100644 --- a/api_docs/triggers_actions_ui.mdx +++ b/api_docs/triggers_actions_ui.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/triggersActionsUi title: "triggersActionsUi" image: https://source.unsplash.com/400x175/?github summary: API docs for the triggersActionsUi plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'triggersActionsUi'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/ui_actions.mdx b/api_docs/ui_actions.mdx index 8a2dd272ec7a1..7138b05a51332 100644 --- a/api_docs/ui_actions.mdx +++ b/api_docs/ui_actions.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/uiActions title: "uiActions" image: https://source.unsplash.com/400x175/?github summary: API docs for the uiActions plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uiActions'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/ui_actions_enhanced.mdx b/api_docs/ui_actions_enhanced.mdx index a5a6b8b157fdb..dbb6805add960 100644 --- a/api_docs/ui_actions_enhanced.mdx +++ b/api_docs/ui_actions_enhanced.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/uiActionsEnhanced title: "uiActionsEnhanced" image: https://source.unsplash.com/400x175/?github summary: API docs for the uiActionsEnhanced plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'uiActionsEnhanced'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/unified_search.mdx b/api_docs/unified_search.mdx index fa0e583851fa5..45056d1bcf5f8 100644 --- a/api_docs/unified_search.mdx +++ b/api_docs/unified_search.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/unifiedSearch title: "unifiedSearch" image: https://source.unsplash.com/400x175/?github summary: API docs for the unifiedSearch plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedSearch'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/unified_search_autocomplete.mdx b/api_docs/unified_search_autocomplete.mdx index 33e6e6a372462..ca9d8635db018 100644 --- a/api_docs/unified_search_autocomplete.mdx +++ b/api_docs/unified_search_autocomplete.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/unifiedSearch-autocomplete title: "unifiedSearch.autocomplete" image: https://source.unsplash.com/400x175/?github summary: API docs for the unifiedSearch.autocomplete plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedSearch.autocomplete'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/url_forwarding.mdx b/api_docs/url_forwarding.mdx index 68cc63dfa8720..35d39260ab2ff 100644 --- a/api_docs/url_forwarding.mdx +++ b/api_docs/url_forwarding.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/urlForwarding title: "urlForwarding" image: https://source.unsplash.com/400x175/?github summary: API docs for the urlForwarding plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'urlForwarding'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/usage_collection.mdx b/api_docs/usage_collection.mdx index 55b150d9c9e7c..7d28336057d36 100644 --- a/api_docs/usage_collection.mdx +++ b/api_docs/usage_collection.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/usageCollection title: "usageCollection" image: https://source.unsplash.com/400x175/?github summary: API docs for the usageCollection plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'usageCollection'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/ux.mdx b/api_docs/ux.mdx index de6b40411703c..8f44817ef172c 100644 --- a/api_docs/ux.mdx +++ b/api_docs/ux.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/ux title: "ux" image: https://source.unsplash.com/400x175/?github summary: API docs for the ux plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ux'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/vis_default_editor.mdx b/api_docs/vis_default_editor.mdx index 583915307439a..a8779d73312e7 100644 --- a/api_docs/vis_default_editor.mdx +++ b/api_docs/vis_default_editor.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/visDefaultEditor title: "visDefaultEditor" image: https://source.unsplash.com/400x175/?github summary: API docs for the visDefaultEditor plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visDefaultEditor'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/vis_type_gauge.mdx b/api_docs/vis_type_gauge.mdx index 7b5951cce5dde..00e43f99f59a2 100644 --- a/api_docs/vis_type_gauge.mdx +++ b/api_docs/vis_type_gauge.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/visTypeGauge title: "visTypeGauge" image: https://source.unsplash.com/400x175/?github summary: API docs for the visTypeGauge plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeGauge'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/vis_type_heatmap.mdx b/api_docs/vis_type_heatmap.mdx index bc518c8156412..465ccab4d9c81 100644 --- a/api_docs/vis_type_heatmap.mdx +++ b/api_docs/vis_type_heatmap.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/visTypeHeatmap title: "visTypeHeatmap" image: https://source.unsplash.com/400x175/?github summary: API docs for the visTypeHeatmap plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeHeatmap'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/vis_type_pie.mdx b/api_docs/vis_type_pie.mdx index a44f52447f08a..76805bc7416a4 100644 --- a/api_docs/vis_type_pie.mdx +++ b/api_docs/vis_type_pie.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/visTypePie title: "visTypePie" image: https://source.unsplash.com/400x175/?github summary: API docs for the visTypePie plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypePie'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/vis_type_table.mdx b/api_docs/vis_type_table.mdx index 7a31d8844fa79..ae424b974b381 100644 --- a/api_docs/vis_type_table.mdx +++ b/api_docs/vis_type_table.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/visTypeTable title: "visTypeTable" image: https://source.unsplash.com/400x175/?github summary: API docs for the visTypeTable plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTable'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/vis_type_timelion.mdx b/api_docs/vis_type_timelion.mdx index bea439614069e..d33d7b36de830 100644 --- a/api_docs/vis_type_timelion.mdx +++ b/api_docs/vis_type_timelion.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/visTypeTimelion title: "visTypeTimelion" image: https://source.unsplash.com/400x175/?github summary: API docs for the visTypeTimelion plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTimelion'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/vis_type_timeseries.mdx b/api_docs/vis_type_timeseries.mdx index 30b0159f0778b..28ebd5bcc88df 100644 --- a/api_docs/vis_type_timeseries.mdx +++ b/api_docs/vis_type_timeseries.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/visTypeTimeseries title: "visTypeTimeseries" image: https://source.unsplash.com/400x175/?github summary: API docs for the visTypeTimeseries plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeTimeseries'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/vis_type_vega.mdx b/api_docs/vis_type_vega.mdx index 404db107ebe8e..8c8ae0a7dce7f 100644 --- a/api_docs/vis_type_vega.mdx +++ b/api_docs/vis_type_vega.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/visTypeVega title: "visTypeVega" image: https://source.unsplash.com/400x175/?github summary: API docs for the visTypeVega plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeVega'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/vis_type_vislib.mdx b/api_docs/vis_type_vislib.mdx index ca7c8a49ad004..ae5aef7e06252 100644 --- a/api_docs/vis_type_vislib.mdx +++ b/api_docs/vis_type_vislib.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/visTypeVislib title: "visTypeVislib" image: https://source.unsplash.com/400x175/?github summary: API docs for the visTypeVislib plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeVislib'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/vis_type_xy.mdx b/api_docs/vis_type_xy.mdx index 7ea6323e773dc..9ad103431f8bb 100644 --- a/api_docs/vis_type_xy.mdx +++ b/api_docs/vis_type_xy.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/visTypeXy title: "visTypeXy" image: https://source.unsplash.com/400x175/?github summary: API docs for the visTypeXy plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visTypeXy'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- diff --git a/api_docs/visualizations.mdx b/api_docs/visualizations.mdx index 25c46f17f18c4..370b33d78d66d 100644 --- a/api_docs/visualizations.mdx +++ b/api_docs/visualizations.mdx @@ -4,7 +4,7 @@ slug: /kibana-dev-docs/api/visualizations title: "visualizations" image: https://source.unsplash.com/400x175/?github summary: API docs for the visualizations plugin -date: 2022-06-16 +date: 2022-06-17 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visualizations'] warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. --- From c81da028a98698917c682300c8cfa7ec67dd95d9 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Fri, 17 Jun 2022 10:16:50 +0200 Subject: [PATCH 10/30] [Discover] Unify definition of field names and field descriptions (#134463) * [Discover] Address "Don't call Hooks" React error message * [Discover] Unify definition of field names and field descriptions * [Discover] Bring back source message * [Discover] Update name function * [Discover] Revert source logic * [Discover] Update code style Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/discover/common/field_types.ts | 26 +++++++++ .../sidebar/discover_field_search.tsx | 11 +++- .../sidebar/lib/get_field_type_description.ts | 49 +++++++++------- .../public/utils/get_field_type_name.test.ts | 15 ++--- .../public/utils/get_field_type_name.ts | 57 +++++++++++-------- 5 files changed, 104 insertions(+), 54 deletions(-) create mode 100644 src/plugins/discover/common/field_types.ts diff --git a/src/plugins/discover/common/field_types.ts b/src/plugins/discover/common/field_types.ts new file mode 100644 index 0000000000000..bd24797ab5323 --- /dev/null +++ b/src/plugins/discover/common/field_types.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export enum KNOWN_FIELD_TYPES { + BOOLEAN = 'boolean', + CONFLICT = 'conflict', + DATE = 'date', + DATE_RANGE = 'date_range', + GEO_POINT = 'geo_point', + GEO_SHAPE = 'geo_shape', + HISTOGRAM = 'histogram', + IP = 'ip', + IP_RANGE = 'ip_range', + KEYWORD = 'keyword', + MURMUR3 = 'murmur3', + NUMBER = 'number', + NESTED = 'nested', + STRING = 'string', + TEXT = 'text', + VERSION = 'version', +} diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.tsx index 3e01c62c449ba..d6f88c0626e7f 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.tsx @@ -36,6 +36,7 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { FieldIcon } from '@kbn/react-field'; import { getFieldTypeDescription } from './lib/get_field_type_description'; +import { KNOWN_FIELD_TYPES } from '../../../../../common/field_types'; import { useDiscoverServices } from '../../../../utils/use_discover_services'; export interface State { @@ -107,7 +108,9 @@ export function DiscoverFieldSearch({ onChange, value, types, presentFieldTypes const { docLinks } = useDiscoverServices(); const items: FieldTypeTableItem[] = useMemo(() => { + const knownTypes = Object.values(KNOWN_FIELD_TYPES) as string[]; return presentFieldTypes + .filter((element) => knownTypes.includes(element)) .sort((one, another) => one.localeCompare(another)) .map((element, index) => ({ id: index, @@ -122,7 +125,9 @@ export function DiscoverFieldSearch({ onChange, value, types, presentFieldTypes const columnsSidebar: Array> = [ { field: 'dataType', - name: 'Data type', + name: i18n.translate('discover.fieldTypesPopover.dataTypeColumnTitle', { + defaultMessage: 'Data type', + }), width: '110px', render: (name: string) => ( @@ -135,7 +140,9 @@ export function DiscoverFieldSearch({ onChange, value, types, presentFieldTypes }, { field: 'description', - name: 'Description', + name: i18n.translate('discover.fieldTypesPopover.descriptionColumnTitle', { + defaultMessage: 'Description', + }), // eslint-disable-next-line react/no-danger render: (description: string) =>
, }, diff --git a/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_description.ts b/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_description.ts index 3e19c05d01770..3b5b6aaa016ce 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_description.ts +++ b/src/plugins/discover/public/application/main/components/sidebar/lib/get_field_type_description.ts @@ -8,22 +8,28 @@ import type { DocLinksStart } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; +import { KNOWN_FIELD_TYPES } from '../../../../../../common/field_types'; + +const UNKNOWN_FIELD_TYPE_DESC = i18n.translate('discover.fieldNameDescription.unknownField', { + defaultMessage: 'Unknown field', +}); export function getFieldTypeDescription(type: string, docLinks: DocLinksStart) { - switch (type) { - case 'boolean': + const knownType: KNOWN_FIELD_TYPES = type as KNOWN_FIELD_TYPES; + switch (knownType) { + case KNOWN_FIELD_TYPES.BOOLEAN: return i18n.translate('discover.fieldNameDescription.booleanField', { defaultMessage: 'True and false values.', }); - case 'conflict': + case KNOWN_FIELD_TYPES.CONFLICT: return i18n.translate('discover.fieldNameDescription.conflictField', { defaultMessage: 'Field has values of different types. Resolve in Management > Data Views.', }); - case 'date': + case KNOWN_FIELD_TYPES.DATE: return i18n.translate('discover.fieldNameDescription.dateField', { defaultMessage: 'A date string or the number of seconds or milliseconds since 1/1/1970.', }); - case 'date_range': + case KNOWN_FIELD_TYPES.DATE_RANGE: return i18n.translate('discover.fieldNameDescription.dateRangeField', { defaultMessage: 'Range of {dateFieldTypeLink} values. {viewSupportedDateFormatsLink}', values: { @@ -43,49 +49,52 @@ export function getFieldTypeDescription(type: string, docLinks: DocLinksStart) { '', }, }); - case 'geo_point': + case KNOWN_FIELD_TYPES.GEO_POINT: return i18n.translate('discover.fieldNameDescription.geoPointField', { defaultMessage: 'Latitude and longitude points.', }); - case 'geo_shape': + case KNOWN_FIELD_TYPES.GEO_SHAPE: return i18n.translate('discover.fieldNameDescription.geoShapeField', { defaultMessage: 'Complex shapes, such as polygons.', }); - case 'ip': + case KNOWN_FIELD_TYPES.HISTOGRAM: + return i18n.translate('discover.fieldNameDescription.histogramField', { + defaultMessage: 'Pre-aggregated numerical values in the form of a histogram.', + }); + case KNOWN_FIELD_TYPES.IP: return i18n.translate('discover.fieldNameDescription.ipAddressField', { defaultMessage: 'IPv4 and IPv6 addresses.', }); - case 'ip_range': + case KNOWN_FIELD_TYPES.IP_RANGE: return i18n.translate('discover.fieldNameDescription.ipAddressRangeField', { defaultMessage: 'Range of ip values supporting either IPv4 or IPv6 (or mixed) addresses.', }); - case 'murmur3': + case KNOWN_FIELD_TYPES.MURMUR3: return i18n.translate('discover.fieldNameDescription.murmur3Field', { defaultMessage: 'Field that computes and stores hashes of values.', }); - case 'number': + case KNOWN_FIELD_TYPES.NUMBER: return i18n.translate('discover.fieldNameDescription.numberField', { defaultMessage: 'Long, integer, short, byte, double, and float values.', }); - case 'string': + case KNOWN_FIELD_TYPES.STRING: return i18n.translate('discover.fieldNameDescription.stringField', { defaultMessage: 'Full text such as the body of an email or a product description.', }); - case 'text': + case KNOWN_FIELD_TYPES.TEXT: return i18n.translate('discover.fieldNameDescription.textField', { defaultMessage: 'Full text such as the body of an email or a product description.', }); - case 'keyword': + case KNOWN_FIELD_TYPES.KEYWORD: return i18n.translate('discover.fieldNameDescription.keywordField', { defaultMessage: 'Structured content such as an ID, email address, hostname, status code, or tag.', }); - - case 'nested': + case KNOWN_FIELD_TYPES.NESTED: return i18n.translate('discover.fieldNameDescription.nestedField', { defaultMessage: 'JSON object that preserves the relationship between its subfields.', }); - case 'version': + case KNOWN_FIELD_TYPES.VERSION: return i18n.translate('discover.fieldNameDescription.versionField', { defaultMessage: 'Software versions. Supports {SemanticVersioningLink} precedence rules.', values: { @@ -102,8 +111,8 @@ export function getFieldTypeDescription(type: string, docLinks: DocLinksStart) { }, }); default: - return i18n.translate('discover.fieldNameDescription.unknownField', { - defaultMessage: 'Unknown field', - }); + // If you see a typescript error here, that's a sign that there are missing switch cases ^^ + const _exhaustiveCheck: never = knownType; + return UNKNOWN_FIELD_TYPE_DESC || _exhaustiveCheck; } } diff --git a/src/plugins/discover/public/utils/get_field_type_name.test.ts b/src/plugins/discover/public/utils/get_field_type_name.test.ts index b612522d1c3ea..bada07e8ad9f7 100644 --- a/src/plugins/discover/public/utils/get_field_type_name.test.ts +++ b/src/plugins/discover/public/utils/get_field_type_name.test.ts @@ -6,11 +6,8 @@ * Side Public License, v 1. */ -import { - getFieldTypeName, - KNOWN_FIELD_TYPES, - UNKNOWN_FIELD_TYPE_MESSAGE, -} from './get_field_type_name'; +import { getFieldTypeName, UNKNOWN_FIELD_TYPE_MESSAGE } from './get_field_type_name'; +import { KNOWN_FIELD_TYPES } from '../../common/field_types'; describe('getFieldTypeName', () => { describe('known field types should be recognized', () => { @@ -28,7 +25,11 @@ describe('getFieldTypeName', () => { expect(getFieldTypeName(undefined)).toBe(UNKNOWN_FIELD_TYPE_MESSAGE); }); - it(`should return '${UNKNOWN_FIELD_TYPE_MESSAGE}' when passed an unknown field type`, () => { - expect(getFieldTypeName('unknown_field_type')).toBe(UNKNOWN_FIELD_TYPE_MESSAGE); + it(`should return '${UNKNOWN_FIELD_TYPE_MESSAGE}' when passed 'unknown'`, () => { + expect(getFieldTypeName('unknown')).toBe(UNKNOWN_FIELD_TYPE_MESSAGE); + }); + + it('should return the original type string back when passed an unknown field type', () => { + expect(getFieldTypeName('unknown_field_type')).toBe('unknown_field_type'); }); }); diff --git a/src/plugins/discover/public/utils/get_field_type_name.ts b/src/plugins/discover/public/utils/get_field_type_name.ts index 24d5e4602f3be..81a4346f63902 100644 --- a/src/plugins/discover/public/utils/get_field_type_name.ts +++ b/src/plugins/discover/public/utils/get_field_type_name.ts @@ -7,24 +7,8 @@ */ import { i18n } from '@kbn/i18n'; -import { KBN_FIELD_TYPES, ES_FIELD_TYPES } from '@kbn/data-plugin/public'; - -export const KNOWN_FIELD_TYPES = { - BOOLEAN: KBN_FIELD_TYPES.BOOLEAN, - CONFLICT: KBN_FIELD_TYPES.CONFLICT, - DATE: KBN_FIELD_TYPES.DATE, - GEO_POINT: KBN_FIELD_TYPES.GEO_POINT, - GEO_SHAPE: KBN_FIELD_TYPES.GEO_SHAPE, - IP: KBN_FIELD_TYPES.IP, - KEYWORD: ES_FIELD_TYPES.KEYWORD, - MURMUR3: KBN_FIELD_TYPES.MURMUR3, - NUMBER: KBN_FIELD_TYPES.NUMBER, - NESTED: KBN_FIELD_TYPES.NESTED, - SOURCE: 'source', - STRING: KBN_FIELD_TYPES.STRING, - TEXT: ES_FIELD_TYPES.TEXT, - VERSION: ES_FIELD_TYPES.VERSION, -}; +import { KBN_FIELD_TYPES } from '@kbn/data-plugin/public'; +import { KNOWN_FIELD_TYPES } from '../../common/field_types'; export const UNKNOWN_FIELD_TYPE_MESSAGE = i18n.translate( 'discover.fieldNameIcons.unknownFieldAriaLabel', @@ -34,7 +18,21 @@ export const UNKNOWN_FIELD_TYPE_MESSAGE = i18n.translate( ); export function getFieldTypeName(type?: string) { - switch (type) { + if (!type || type === KBN_FIELD_TYPES.UNKNOWN) { + return UNKNOWN_FIELD_TYPE_MESSAGE; + } + + if (type === 'source') { + // TODO: check if we can remove this logic as outdated + + // Note that this type is currently not provided, type for _source is undefined + return i18n.translate('discover.fieldNameIcons.sourceFieldAriaLabel', { + defaultMessage: 'Source field', + }); + } + + const knownType: KNOWN_FIELD_TYPES = type as KNOWN_FIELD_TYPES; + switch (knownType) { case KNOWN_FIELD_TYPES.BOOLEAN: return i18n.translate('discover.fieldNameIcons.booleanAriaLabel', { defaultMessage: 'Boolean field', @@ -47,6 +45,10 @@ export function getFieldTypeName(type?: string) { return i18n.translate('discover.fieldNameIcons.dateFieldAriaLabel', { defaultMessage: 'Date field', }); + case KNOWN_FIELD_TYPES.DATE_RANGE: + return i18n.translate('discover.fieldNameIcons.dateRangeFieldAriaLabel', { + defaultMessage: 'Date range field', + }); case KNOWN_FIELD_TYPES.GEO_POINT: return i18n.translate('discover.fieldNameIcons.geoPointFieldAriaLabel', { defaultMessage: 'Geo point field', @@ -55,10 +57,18 @@ export function getFieldTypeName(type?: string) { return i18n.translate('discover.fieldNameIcons.geoShapeFieldAriaLabel', { defaultMessage: 'Geo shape field', }); + case KNOWN_FIELD_TYPES.HISTOGRAM: + return i18n.translate('discover.fieldNameIcons.histogramFieldAriaLabel', { + defaultMessage: 'Histogram field', + }); case KNOWN_FIELD_TYPES.IP: return i18n.translate('discover.fieldNameIcons.ipAddressFieldAriaLabel', { defaultMessage: 'IP address field', }); + case KNOWN_FIELD_TYPES.IP_RANGE: + return i18n.translate('discover.fieldNameIcons.ipRangeFieldAriaLabel', { + defaultMessage: 'IP range field', + }); case KNOWN_FIELD_TYPES.MURMUR3: return i18n.translate('discover.fieldNameIcons.murmur3FieldAriaLabel', { defaultMessage: 'Murmur3 field', @@ -67,11 +77,6 @@ export function getFieldTypeName(type?: string) { return i18n.translate('discover.fieldNameIcons.numberFieldAriaLabel', { defaultMessage: 'Number field', }); - case KNOWN_FIELD_TYPES.SOURCE: - // Note that this type is currently not provided, type for _source is undefined - return i18n.translate('discover.fieldNameIcons.sourceFieldAriaLabel', { - defaultMessage: 'Source field', - }); case KNOWN_FIELD_TYPES.STRING: return i18n.translate('discover.fieldNameIcons.stringFieldAriaLabel', { defaultMessage: 'String field', @@ -93,6 +98,8 @@ export function getFieldTypeName(type?: string) { defaultMessage: 'Version field', }); default: - return UNKNOWN_FIELD_TYPE_MESSAGE; + // If you see a typescript error here, that's a sign that there are missing switch cases ^^ + const _exhaustiveCheck: never = knownType; + return knownType || _exhaustiveCheck; } } From ce980d4507ade75509a7020c596ceb21a2d47db1 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Fri, 17 Jun 2022 10:17:26 +0200 Subject: [PATCH 11/30] [Kibana Overview] Add AnalyticsNoDataPage (#134172) * Add test * Revert changes to jest.config * Fix i18n * Remove core import * Remove erroneus export * Updating test * Add mock import from data view editor * Access DataViewEditor through context * Change how we pass props around * Update mocks Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/kibana_overview/kibana.json | 2 +- .../kibana_overview/public/application.tsx | 7 +- .../kibana_overview/public/components/app.tsx | 2 +- .../__snapshots__/overview.test.tsx.snap | 317 ++++++++++++++++-- .../overview/overview.test.mocks.ts | 23 +- .../components/overview/overview.test.tsx | 48 ++- .../public/components/overview/overview.tsx | 187 +++++++---- src/plugins/kibana_overview/public/types.ts | 2 + src/plugins/kibana_overview/tsconfig.json | 1 + .../translations/translations/fr-FR.json | 4 - .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - 12 files changed, 477 insertions(+), 124 deletions(-) diff --git a/src/plugins/kibana_overview/kibana.json b/src/plugins/kibana_overview/kibana.json index ee646689226b6..955b74d558319 100644 --- a/src/plugins/kibana_overview/kibana.json +++ b/src/plugins/kibana_overview/kibana.json @@ -7,7 +7,7 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": ["navigation", "dataViews", "home", "share"], + "requiredPlugins": ["navigation", "dataViews", "home", "share", "dataViewEditor"], "optionalPlugins": ["newsfeed", "usageCollection"], "requiredBundles": ["kibanaReact", "newsfeed"] } diff --git a/src/plugins/kibana_overview/public/application.tsx b/src/plugins/kibana_overview/public/application.tsx index adfd53c04ef48..8453c35ecba23 100644 --- a/src/plugins/kibana_overview/public/application.tsx +++ b/src/plugins/kibana_overview/public/application.tsx @@ -41,12 +41,7 @@ export const renderApp = ( diff --git a/src/plugins/kibana_overview/public/components/app.tsx b/src/plugins/kibana_overview/public/components/app.tsx index 90e7051882cfd..9c82c9842e9df 100644 --- a/src/plugins/kibana_overview/public/components/app.tsx +++ b/src/plugins/kibana_overview/public/components/app.tsx @@ -49,7 +49,7 @@ export const KibanaOverviewApp = ({ - + diff --git a/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap b/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap index 48e70a03d7fbb..c185e1b85ae1a 100644 --- a/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap +++ b/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap @@ -227,7 +227,7 @@ exports[`Overview render 1`] = ` ] } > - + > + + `; @@ -455,35 +457,292 @@ exports[`Overview when there is no user data view 1`] = ` ] } > - , - "rightSideItems": Array [], + "IndexPatternEditorComponent": [MockFunction], + "openEditor": [MockFunction], + "userPermissions": Object { + "editDataView": [MockFunction], + }, } } - template="empty" - /> + dataViews={ + Object { + "hasData": Object { + "hasESData": [Function], + "hasUserDataView": [Function], + }, + "hasUserDataView": [MockFunction], + } + } + > + + `; @@ -668,7 +927,7 @@ exports[`Overview without features 1`] = ` ] } > - + > + + `; @@ -861,7 +1122,7 @@ exports[`Overview without solutions 1`] = ` } solutions={Array []} > - + > + + `; diff --git a/src/plugins/kibana_overview/public/components/overview/overview.test.mocks.ts b/src/plugins/kibana_overview/public/components/overview/overview.test.mocks.ts index 4c58de5cdd245..ce02f2cb483e7 100644 --- a/src/plugins/kibana_overview/public/components/overview/overview.test.mocks.ts +++ b/src/plugins/kibana_overview/public/components/overview/overview.test.mocks.ts @@ -7,16 +7,35 @@ */ import React from 'react'; +import { Observable } from 'rxjs'; +import { indexPatternEditorPluginMock } from '@kbn/data-view-editor-plugin/public/mocks'; -export const hasUserDataViewMock = jest.fn(); +export const hasUserDataView = jest.fn(); +export const hasESData = jest.fn(); jest.doMock('@kbn/kibana-react-plugin/public', () => ({ useKibana: jest.fn().mockReturnValue({ services: { + application: { + currentAppId$: new Observable(), + navigateToUrl: jest.fn(), + capabilities: { + navLinks: { + integrations: { + canAccessFleet: false, + }, + }, + }, + }, http: { basePath: { prepend: jest.fn((path: string) => (path ? path : 'path')) } }, dataViews: { - hasUserDataView: hasUserDataViewMock, + hasUserDataView: jest.fn(), + hasData: { + hasESData, + hasUserDataView, + }, }, + dataViewEditor: indexPatternEditorPluginMock.createStartContract(), share: { url: { locators: { get: () => ({ useUrl: () => '' }) } } }, uiSettings: { get: jest.fn() }, docLinks: { diff --git a/src/plugins/kibana_overview/public/components/overview/overview.test.tsx b/src/plugins/kibana_overview/public/components/overview/overview.test.tsx index 29d51087c3d67..b433e7a39da13 100644 --- a/src/plugins/kibana_overview/public/components/overview/overview.test.tsx +++ b/src/plugins/kibana_overview/public/components/overview/overview.test.tsx @@ -6,15 +6,40 @@ * Side Public License, v 1. */ -import { hasUserDataViewMock } from './overview.test.mocks'; +import React from 'react'; import { setTimeout as setTimeoutP } from 'timers/promises'; import moment from 'moment'; -import React from 'react'; import { act } from 'react-dom/test-utils'; import { ReactWrapper } from 'enzyme'; -import { Overview } from './overview'; +import { EuiLoadingSpinner } from '@elastic/eui'; import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { KibanaPageTemplate } from '@kbn/shared-ux-components'; import type { FeatureCatalogueCategory } from '@kbn/home-plugin/public'; +import { AnalyticsNoDataPageKibanaProvider } from '@kbn/shared-ux-page-analytics-no-data'; +import { hasESData, hasUserDataView } from './overview.test.mocks'; +import { Overview } from './overview'; + +jest.mock('@kbn/shared-ux-components', () => { + const MockedComponent: string = 'MockedKibanaPageTemplate'; + const mockedModule = { + ...jest.requireActual('@kbn/shared-ux-components'), + KibanaPageTemplate: () => { + return ; + }, + }; + return mockedModule; +}); + +jest.mock('@kbn/shared-ux-page-analytics-no-data', () => { + const MockedComponent: string = 'MockedAnalyticsNoDataPage'; + return { + ...jest.requireActual('@kbn/shared-ux-page-analytics-no-data'), + AnalyticsNoDataPageKibanaProvider: () => { + return ; + }, + }; +}); + const mockNewsFetchResult = { error: null, feedItems: [ @@ -135,8 +160,8 @@ const updateComponent = async (component: ReactWrapper) => { describe('Overview', () => { beforeEach(() => { - hasUserDataViewMock.mockClear(); - hasUserDataViewMock.mockResolvedValue(true); + hasESData.mockResolvedValue(true); + hasUserDataView.mockResolvedValue(true); }); afterAll(() => jest.clearAllMocks()); @@ -153,6 +178,7 @@ describe('Overview', () => { await updateComponent(component); expect(component).toMatchSnapshot(); + expect(component.find(KibanaPageTemplate).length).toBe(1); }); test('without solutions', async () => { @@ -176,7 +202,8 @@ describe('Overview', () => { }); test('when there is no user data view', async () => { - hasUserDataViewMock.mockResolvedValue(false); + hasESData.mockResolvedValue(true); + hasUserDataView.mockResolvedValue(false); const component = mountWithIntl( { await updateComponent(component); expect(component).toMatchSnapshot(); + expect(component.find(AnalyticsNoDataPageKibanaProvider).length).toBe(1); + expect(component.find(KibanaPageTemplate).length).toBe(0); + expect(component.find(EuiLoadingSpinner).length).toBe(0); }); test('during loading', async () => { - hasUserDataViewMock.mockImplementation(() => new Promise(() => {})); + hasESData.mockImplementation(() => new Promise(() => {})); + hasUserDataView.mockImplementation(() => new Promise(() => {})); const component = mountWithIntl( { await updateComponent(component); expect(component.render()).toMatchSnapshot(); + expect(component.find(AnalyticsNoDataPageKibanaProvider).length).toBe(0); + expect(component.find(KibanaPageTemplate).length).toBe(0); + expect(component.find(EuiLoadingSpinner).length).toBe(1); }); }); diff --git a/src/plugins/kibana_overview/public/components/overview/overview.tsx b/src/plugins/kibana_overview/public/components/overview/overview.tsx index 5859ea2e8a58b..2258c662fe94e 100644 --- a/src/plugins/kibana_overview/public/components/overview/overview.tsx +++ b/src/plugins/kibana_overview/public/components/overview/overview.tsx @@ -20,16 +20,21 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { CoreStart } from '@kbn/core/public'; -import { i18n } from '@kbn/i18n'; import { - RedirectAppLinks, useKibana, - KibanaPageTemplate, KibanaPageTemplateSolutionNavAvatar, - KibanaPageTemplateProps, overviewPageActions, OverviewPageFooter, } from '@kbn/kibana-react-plugin/public'; +import { KibanaPageTemplate } from '@kbn/shared-ux-components'; +import { + AnalyticsNoDataPageKibanaProvider, + AnalyticsNoDataPage, +} from '@kbn/shared-ux-page-analytics-no-data'; +import { + RedirectAppLinksContainer as RedirectAppLinks, + RedirectAppLinksKibanaProvider, +} from '@kbn/shared-ux-link-redirect-app'; import { FetchResult } from '@kbn/newsfeed-plugin/public'; import { FeatureCatalogueEntry, @@ -54,10 +59,12 @@ interface Props { export const Overview: FC = ({ newsFetchResult, solutions, features }) => { const [isNewKibanaInstance, setNewKibanaInstance] = useState(false); + const [hasESData, setHasESData] = useState(false); + const [hasDataView, setHasDataView] = useState(false); const [isLoading, setIsLoading] = useState(true); - const { - services: { http, docLinks, dataViews, share, uiSettings, application }, - } = useKibana(); + const { services } = useKibana(); + const { http, docLinks, dataViews, share, uiSettings, application, chrome, dataViewEditor } = + services; const addBasePath = http.basePath.prepend; const IS_DARK_THEME = uiSettings.get('theme:darkMode'); @@ -81,28 +88,6 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => const addDataFeatures = getFeaturesByCategory('data'); const manageDataFeatures = getFeaturesByCategory('admin'); const devTools = findFeatureById('console'); - const noDataConfig: KibanaPageTemplateProps['noDataConfig'] = { - solution: i18n.translate('kibanaOverview.noDataConfig.solutionName', { - defaultMessage: `Analytics`, - }), - pageTitle: i18n.translate('kibanaOverview.noDataConfig.pageTitle', { - defaultMessage: `Welcome to Analytics!`, - }), - logo: 'logoKibana', - actions: { - elasticAgent: { - title: i18n.translate('kibanaOverview.noDataConfig.title', { - defaultMessage: 'Add integrations', - }), - description: i18n.translate('kibanaOverview.noDataConfig.description', { - defaultMessage: - 'Use Elastic Agent or Beats to collect data and build out Analytics solutions.', - }), - 'data-test-subj': 'kbnOverviewAddIntegrations', - }, - }, - docsLink: docLinks.links.kibana.guide, - }; // Show card for console if none of the manage data plugins are available, most likely in OSS if (manageDataFeatures.length < 1 && devTools) { @@ -111,9 +96,21 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => useEffect(() => { const fetchIsNewKibanaInstance = async () => { - const hasUserIndexPattern = await dataViews.hasUserDataView().catch(() => true); + const checkData = async () => { + const hasUserDataViewValue = await dataViews.hasData.hasUserDataView(); + const hasESDataValue = await dataViews.hasData.hasESData(); + setNewKibanaInstance((!hasUserDataViewValue && hasESDataValue) || !hasESDataValue); + setHasDataView(hasUserDataViewValue); + setHasESData(hasESDataValue); + }; + + await checkData().catch((e) => { + setNewKibanaInstance(false); + setHasDataView(true); + setHasESData(true); + setIsLoading(false); + }); - setNewKibanaInstance(!hasUserIndexPattern); setIsLoading(false); }; @@ -125,21 +122,33 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => return app ? ( - - { - trackUiMetric(METRIC_TYPE.CLICK, `app_card_${appId}`); - }} - image={addBasePath( - `/plugins/${PLUGIN_ID}/assets/kibana_${appId}_${IS_DARK_THEME ? 'dark' : 'light'}.svg` - )} - title={app.title} - titleElement="h3" - titleSize="s" - /> - + + + { + trackUiMetric(METRIC_TYPE.CLICK, `app_card_${appId}`); + }} + image={addBasePath( + `/plugins/${PLUGIN_ID}/assets/kibana_${appId}_${ + IS_DARK_THEME ? 'dark' : 'light' + }.svg` + )} + title={app.title} + titleElement="h3" + titleSize="s" + /> + + ) : null; }; @@ -148,6 +157,10 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => const mainApps = ['dashboard', 'discover']; const remainingApps = kibanaApps.map(({ id }) => id).filter((id) => !mainApps.includes(id)); + const onDataViewCreated = () => { + setNewKibanaInstance(false); + }; + if (isLoading) { return ( @@ -158,6 +171,35 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => ); } + if (isNewKibanaInstance) { + const analyticsServices = { + coreStart: { + application, + chrome, + docLinks, + http, + }, + dataViews: { + ...dataViews, + hasData: { + ...dataViews.hasData, + + // We've already called this, so we can optimize the analytics services to + // use the already-retrieved data to avoid a double-call. + hasESData: () => Promise.resolve(hasESData), + hasUserDataView: () => Promise.resolve(hasDataView), + }, + }, + dataViewEditor, + }; + + return ( + + + + ); + } + return ( = ({ newsFetchResult, solutions, features }) => showManagementLink: !!manageDataFeatures, }), }} - noDataConfig={isNewKibanaInstance ? noDataConfig : undefined} template="empty" > <> @@ -243,27 +284,37 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => {solutions.map(({ id, title, description, icon, path }) => ( - - - } - image={addBasePath(getSolutionGraphicURL(snakeCase(id)))} - title={title} - titleElement="h3" - titleSize="xs" - onClick={() => { - trackUiMetric(METRIC_TYPE.CLICK, `solution_panel_${id}`); - }} - /> - + + + + } + image={addBasePath(getSolutionGraphicURL(snakeCase(id)))} + title={title} + titleElement="h3" + titleSize="xs" + onClick={() => { + trackUiMetric(METRIC_TYPE.CLICK, `solution_panel_${id}`); + }} + /> + + ))} diff --git a/src/plugins/kibana_overview/public/types.ts b/src/plugins/kibana_overview/public/types.ts index 7698b944bfc2f..9afe74434faa8 100644 --- a/src/plugins/kibana_overview/public/types.ts +++ b/src/plugins/kibana_overview/public/types.ts @@ -12,6 +12,7 @@ import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { NewsfeedPublicPluginStart } from '@kbn/newsfeed-plugin/public'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import { SharePluginStart } from '@kbn/share-plugin/public'; +import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface KibanaOverviewPluginSetup {} @@ -30,4 +31,5 @@ export interface AppPluginStartDependencies { navigation: NavigationPublicPluginStart; newsfeed?: NewsfeedPublicPluginStart; share: SharePluginStart; + dataViewEditor: DataViewEditorStart; } diff --git a/src/plugins/kibana_overview/tsconfig.json b/src/plugins/kibana_overview/tsconfig.json index fd9002d39ee8c..ffa89f25316a9 100644 --- a/src/plugins/kibana_overview/tsconfig.json +++ b/src/plugins/kibana_overview/tsconfig.json @@ -18,5 +18,6 @@ { "path": "../../plugins/newsfeed/tsconfig.json" }, { "path": "../../plugins/usage_collection/tsconfig.json" }, { "path": "../../plugins/kibana_react/tsconfig.json" }, + { "path": "../../plugins/data_view_editor/tsconfig.json" } ] } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 5dc1642ac8474..13edd89027a06 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -5057,10 +5057,6 @@ "kibanaOverview.manageData.sectionTitle": "Gérer vos données", "kibanaOverview.more.title": "Toujours plus avec Elastic", "kibanaOverview.news.title": "Nouveautés", - "kibanaOverview.noDataConfig.description": "Utilisez Elastic Agent ou Beats pour collecter des données et créer des solutions Analytics.", - "kibanaOverview.noDataConfig.pageTitle": "Bienvenue dans Analytics !", - "kibanaOverview.noDataConfig.solutionName": "Analyse", - "kibanaOverview.noDataConfig.title": "Ajouter des intégrations", "lists.exceptions.doesNotExistOperatorLabel": "n'existe pas", "lists.exceptions.existsOperatorLabel": "existe", "lists.exceptions.isInListOperatorLabel": "est dans la liste", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b763e0b6206aa..49a2a99abcfc3 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5153,10 +5153,6 @@ "kibanaOverview.manageData.sectionTitle": "データを管理", "kibanaOverview.more.title": "Elasticではさまざまなことが可能です", "kibanaOverview.news.title": "新機能", - "kibanaOverview.noDataConfig.description": "ElasticエージェントまたはBeatsを使用して、データを収集し、分析ソリューションを構築します。", - "kibanaOverview.noDataConfig.pageTitle": "Analyticsへようこそ!", - "kibanaOverview.noDataConfig.solutionName": "分析", - "kibanaOverview.noDataConfig.title": "統合の追加", "lists.exceptions.doesNotExistOperatorLabel": "存在しない", "lists.exceptions.existsOperatorLabel": "存在する", "lists.exceptions.isInListOperatorLabel": "リストにある", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 38ea4b6526b23..ab53a184d44e6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5164,10 +5164,6 @@ "kibanaOverview.manageData.sectionTitle": "管理您的数据", "kibanaOverview.more.title": "Elastic 让您事半功倍", "kibanaOverview.news.title": "最新动态", - "kibanaOverview.noDataConfig.description": "使用 Elastic 代理或 Beats 收集数据并构建分析解决方案。", - "kibanaOverview.noDataConfig.pageTitle": "欢迎使用分析!", - "kibanaOverview.noDataConfig.solutionName": "分析", - "kibanaOverview.noDataConfig.title": "添加集成", "lists.exceptions.doesNotExistOperatorLabel": "不存在", "lists.exceptions.existsOperatorLabel": "存在", "lists.exceptions.isInListOperatorLabel": "在列表中", From c7262544b54422f8602c57e68a365554cd83f846 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 17 Jun 2022 12:05:14 +0300 Subject: [PATCH 12/30] [ResponseOps] Sub actions framework: Create connector forms (#131718) * Create useSubAction hook * Create SimpleConnectorForm * Init create connector form * Show action types * Add banner to flyout * Return back to action types * Create useCreateConnector * Use useCreateConnector * POC: IBM resilient * Refinements * Show flyout header * Support password text field * Validate url * Change connector types * Jira * Teams * Pagerduty * ITOM * Server log * Slack * ES index draft * ES index fix * Password field * SN * Fix ES index * Webhoo draft * Webhook fixes * Index fixes * Email connector * Fix disabled * Xmatters connector * Swimlane connector * Swimlane fixes * Improve form * Presubmit validator * SN connector * Hidden field * Rename * fixes * Edit flyout * Test connector * Improve structure * Improve edit flyout structure * Improvements * Remove old files and fix tabs * Modal * Common label for encrypted fields * First tests * More tests * Fix types * Fix i18n * Fixes * Dynamic encrypted values callout * Add tests * Add flyout tests * Fix cypress test * Add README * Fix tests * Webhook serializer & deserializer * Validate email * Fix itom * Fix validators * Fix types * Add more tests * Add more tests * Add more tests * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Pass connector's services with context * Clean up translations * Pass readOnly to all fields * Fix bug with connectors provider * PR feedback * Fix i18n * Fix types * Fix subaction hook * Add hook tests * Fix bug with port Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../components/configure_cases/index.test.tsx | 7 +- .../components/configure_cases/index.tsx | 12 +- .../public/alerts/alert_form.test.tsx | 4 - .../cypress/screens/configure_cases.ts | 2 +- .../rules/rule_actions_field/index.tsx | 38 +- .../journeys/alerts/default_email_settings.ts | 2 +- .../settings/add_connector_flyout.tsx | 3 +- .../translations/translations/fr-FR.json | 87 --- .../translations/translations/ja-JP.json | 87 --- .../translations/translations/zh-CN.json | 87 --- x-pack/plugins/triggers_actions_ui/README.md | 495 ++++++------- .../application/action_type_registry.mock.ts | 1 - .../public/application/app.tsx | 45 +- .../builtin_action_types/email/email.test.tsx | 296 -------- .../builtin_action_types/email/email.tsx | 88 +-- .../email/email_connector.test.tsx | 673 ++++++++++++++---- .../email/email_connector.tsx | 529 +++++++------- .../email/exchange_form.test.tsx | 129 +++- .../email/exchange_form.tsx | 216 ++---- .../email/translations.ts | 108 ++- .../email/use_email_config.test.ts | 105 ++- .../email/use_email_config.ts | 117 +-- .../es_index/es_index.test.tsx | 53 -- .../es_index/es_index.tsx | 22 +- .../es_index/es_index_connector.test.tsx | 333 ++++++--- .../es_index/es_index_connector.tsx | 370 +++++----- .../es_index/translations.ts | 34 +- .../builtin_action_types/jira/jira.test.tsx | 63 -- .../builtin_action_types/jira/jira.tsx | 55 +- .../jira/jira_connectors.test.tsx | 259 +++---- .../jira/jira_connectors.tsx | 194 +---- .../builtin_action_types/jira/translations.ts | 85 --- .../pagerduty/pagerduty.test.tsx | 55 -- .../pagerduty/pagerduty.tsx | 23 +- .../pagerduty/pagerduty_connectors.test.tsx | 258 ++++--- .../pagerduty/pagerduty_connectors.tsx | 159 ++--- .../pagerduty/translations.ts | 21 + .../resilient/resilient.test.tsx | 63 -- .../resilient/resilient.tsx | 63 +- .../resilient/resilient_connectors.test.tsx | 256 +++---- .../resilient/resilient_connectors.tsx | 198 +----- .../resilient/translations.ts | 96 +-- .../server_log/server_log.test.tsx | 25 +- .../server_log/server_log.tsx | 9 +- .../application_required_callout.tsx | 2 +- .../auth_types/credentials_auth.tsx | 122 +--- .../servicenow/auth_types/oauth.tsx | 309 +++----- .../servicenow/credentials.test.tsx | 55 +- .../servicenow/credentials.tsx | 88 +-- .../servicenow/credentials_api_url.tsx | 78 +- .../servicenow/helpers.ts | 5 +- .../servicenow/servicenow.test.tsx | 191 ----- .../servicenow/servicenow.tsx | 89 +-- .../servicenow/servicenow_connectors.test.tsx | 603 ++++++++-------- .../servicenow/servicenow_connectors.tsx | 221 +++--- .../servicenow_connectors_no_app.test.tsx | 162 +++++ .../servicenow_connectors_no_app.tsx | 30 +- .../servicenow/sn_store_button.tsx | 6 +- .../servicenow/translations.ts | 61 +- .../servicenow/update_connector.test.tsx | 210 ++---- .../servicenow/update_connector.tsx | 272 ++++--- .../servicenow/use_get_app_info.tsx | 11 +- .../builtin_action_types/slack/slack.test.tsx | 93 --- .../builtin_action_types/slack/slack.tsx | 28 +- .../slack/slack_connectors.test.tsx | 177 +++-- .../slack/slack_connectors.tsx | 102 ++- .../slack/translations.ts | 19 +- .../builtin_action_types/swimlane/helpers.ts | 32 +- .../swimlane/steps/swimlane_connection.tsx | 190 ++--- .../swimlane/steps/swimlane_fields.tsx | 406 +++++------ .../swimlane/swimlane.test.tsx | 147 ---- .../swimlane/swimlane.tsx | 65 +- .../swimlane/swimlane_connectors.test.tsx | 384 ++++++---- .../swimlane/swimlane_connectors.tsx | 134 ++-- .../swimlane/translations.ts | 64 -- .../swimlane/use_get_application.test.tsx | 52 +- .../swimlane/use_get_application.tsx | 83 ++- .../builtin_action_types/teams/teams.test.tsx | 93 --- .../builtin_action_types/teams/teams.tsx | 28 +- .../teams/teams_connectors.test.tsx | 176 ++--- .../teams/teams_connectors.tsx | 102 ++- .../teams/translations.ts | 13 +- .../builtin_action_types/test_utils.tsx | 127 ++++ .../webhook/translations.ts | 84 ++- .../webhook/webhook.test.tsx | 135 ---- .../builtin_action_types/webhook/webhook.tsx | 55 +- .../webhook/webhook_connectors.test.tsx | 321 ++++++--- .../webhook/webhook_connectors.tsx | 544 +++++--------- .../xmatters/translations.ts | 64 +- .../xmatters/xmatters.test.tsx | 133 ---- .../xmatters/xmatters.tsx | 56 +- .../xmatters/xmatters_connectors.test.tsx | 343 +++++---- .../xmatters/xmatters_connectors.tsx | 332 ++++----- .../components/button_group_field.tsx | 80 +++ .../get_encrypted_field_notify_label.test.tsx | 70 -- .../get_encrypted_field_notify_label.tsx | 68 -- .../application/components/hidden_field.tsx | 25 + .../application/components/password_field.tsx | 86 +++ .../components/simple_connector_form.test.tsx | 149 ++++ .../components/simple_connector_form.tsx | 159 +++++ .../application/context/connector_context.tsx | 24 + .../context/use_connector_context.ts | 21 + .../hooks/use_create_connector.test.tsx | 49 ++ .../hooks/use_create_connector.tsx | 86 +++ .../application/hooks/use_edit_connector.tsx | 85 +++ .../hooks/use_execute_connector.test.tsx | 45 ++ .../hooks/use_execute_connector.tsx | 68 ++ .../application/hooks/use_sub_action.test.tsx | 75 ++ .../application/hooks/use_sub_action.tsx | 144 ++++ .../rules_list_sandbox.tsx | 9 +- .../lib/action_connector_api/create.test.ts | 17 +- .../lib/action_connector_api/create.ts | 8 +- .../action_connector_form.test.tsx | 61 -- .../action_connector_form.tsx | 254 ------- .../action_form.test.tsx | 23 +- .../action_type_form.test.tsx | 14 +- .../action_type_menu.test.tsx | 11 +- .../connector_add_flyout.test.tsx | 210 ------ .../connector_add_flyout.tsx | 416 ----------- .../connector_add_modal.test.tsx | 5 +- .../connector_add_modal.tsx | 248 +++---- .../connector_edit_flyout.scss | 3 - .../connector_edit_flyout.test.tsx | 128 ---- .../connector_edit_flyout.tsx | 433 ----------- .../connector_error_mock.tsx | 45 ++ .../connector_form.test.tsx | 150 ++++ .../action_connector_form/connector_form.tsx | 149 ++++ .../connector_form_fields.test.tsx | 73 ++ .../connector_form_fields.tsx | 75 ++ .../connector_form_fields_global.test.tsx | 74 ++ .../connector_form_fields_global.tsx | 61 ++ .../action_connector_form/connector_mock.tsx | 54 ++ .../connector_reducer.test.ts | 103 --- .../connector_reducer.ts | 135 ---- .../connectors_selection.test.tsx | 5 +- .../create_connector_flyout/foooter.tsx | 106 +++ .../create_connector_flyout/header.tsx | 67 ++ .../create_connector_flyout/index.test.tsx | 544 ++++++++++++++ .../create_connector_flyout/index.tsx | 199 ++++++ .../upgrade_license_callout.tsx | 69 ++ .../edit_connector_flyout/foooter.tsx | 92 +++ .../edit_connector_flyout/header.tsx | 118 +++ .../edit_connector_flyout/index.test.tsx | 630 ++++++++++++++++ .../edit_connector_flyout/index.tsx | 294 ++++++++ .../encrypted_fields_callout.test.tsx | 123 ++++ .../encrypted_fields_callout.tsx | 134 ++++ .../sections/action_connector_form/index.ts | 13 +- .../test_connector_form.test.tsx | 25 +- .../test_connector_form.tsx | 26 +- .../sections/action_connector_form/types.ts | 30 + .../actions_connectors_list.test.tsx | 19 +- .../components/actions_connectors_list.tsx | 32 +- .../public/application/sections/index.tsx | 10 +- .../sections/rule_form/rule_add.test.tsx | 4 - .../sections/rule_form/rule_edit.test.tsx | 10 +- .../sections/rule_form/rule_form.test.tsx | 11 - .../public/application/type_registry.test.ts | 6 - .../public/common/get_action_form.tsx | 22 + .../public/common/get_add_alert_flyout.tsx | 13 +- .../common/get_add_connector_flyout.tsx | 16 +- .../public/common/get_edit_alert_flyout.tsx | 13 +- .../common/get_edit_connector_flyout.tsx | 16 +- .../public/common/get_rules_list.tsx | 10 +- .../common/lib/kibana/kibana_react.mock.ts | 1 + .../triggers_actions_ui/public/index.ts | 4 +- .../triggers_actions_ui/public/mocks.ts | 21 +- .../triggers_actions_ui/public/plugin.ts | 45 +- .../triggers_actions_ui/public/types.ts | 62 +- .../alert_create_flyout.ts | 4 +- .../apps/triggers_actions_ui/connectors.ts | 22 +- 170 files changed, 9863 insertions(+), 9868 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors_no_app.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/test_utils.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/button_group_field.tsx delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/get_encrypted_field_notify_label.test.tsx delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/get_encrypted_field_notify_label.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/hidden_field.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/password_field.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/simple_connector_form.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/simple_connector_form.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/context/connector_context.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/context/use_connector_context.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/hooks/use_create_connector.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/hooks/use_create_connector.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/hooks/use_edit_connector.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/hooks/use_execute_connector.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/hooks/use_execute_connector.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/hooks/use_sub_action.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/hooks/use_sub_action.tsx delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.scss delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_error_mock.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form_fields.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form_fields.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form_fields_global.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form_fields_global.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_mock.tsx delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.test.ts delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/foooter.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/upgrade_license_callout.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/foooter.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/header.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/index.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/index.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/encrypted_fields_callout.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/encrypted_fields_callout.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/types.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/common/get_action_form.tsx diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx index 888ac6576ec23..890b8683ae6a5 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx @@ -553,7 +553,7 @@ describe('ConfigureCases', () => { expect(wrapper.find('[data-test-subj="add-connector-flyout"]').exists()).toBe(true); expect(getAddConnectorFlyoutMock).toHaveBeenCalledWith( expect.objectContaining({ - actionTypes: [ + supportedActionTypes: [ expect.objectContaining({ id: '.servicenow', }), @@ -575,9 +575,6 @@ describe('ConfigureCases', () => { test('it show the edit flyout when pressing the update connector button', async () => { const actionType = actionTypeRegistryMock.createMockActionTypeModel({ id: '.resilient', - validateConnector: () => { - return Promise.resolve({}); - }, validateParams: () => { const validationResult = { errors: {} }; return Promise.resolve(validationResult); @@ -603,7 +600,7 @@ describe('ConfigureCases', () => { wrapper.update(); expect(wrapper.find('[data-test-subj="edit-connector-flyout"]').exists()).toBe(true); expect(getEditConnectorFlyoutMock).toHaveBeenCalledWith( - expect.objectContaining({ initialConnector: connectors[1] }) + expect.objectContaining({ connector: connectors[1] }) ); }); diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.tsx index 16773306d6bef..2c04f9f0aa7da 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/index.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/index.tsx @@ -90,7 +90,7 @@ export const ConfigureCases: React.FC = React.memo(() => { [actionTypes] ); - const onConnectorUpdate = useCallback(async () => { + const onConnectorUpdated = useCallback(async () => { refetchConnectors(); refetchActionTypes(); refetchCaseConfigure(); @@ -168,10 +168,9 @@ export const ConfigureCases: React.FC = React.memo(() => { () => addFlyoutVisible ? triggersActionsUi.getAddConnectorFlyout({ - consumer: 'case', onClose: onCloseAddFlyout, - actionTypes: supportedActionTypes, - reloadConnectors: onConnectorUpdate, + supportedActionTypes, + onConnectorCreated: onConnectorUpdated, }) : null, // eslint-disable-next-line react-hooks/exhaustive-deps @@ -182,10 +181,9 @@ export const ConfigureCases: React.FC = React.memo(() => { () => editedConnectorItem && editFlyoutVisible ? triggersActionsUi.getEditConnectorFlyout({ - initialConnector: editedConnectorItem, - consumer: 'case', + connector: editedConnectorItem, onClose: onCloseEditFlyout, - reloadConnectors: onConnectorUpdate, + onConnectorUpdated, }) : null, // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx b/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx index e8c6d57fcba19..14dbac775eaf4 100644 --- a/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx +++ b/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx @@ -19,7 +19,6 @@ import { ruleTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/app import { ValidationResult, Rule, - ConnectorValidationResult, GenericValidationResult, RuleTypeModel, } from '@kbn/triggers-actions-ui-plugin/public/types'; @@ -91,9 +90,6 @@ describe('alert_form', () => { id: 'alert-action-type', iconClass: '', selectMessage: '', - validateConnector: (): Promise> => { - return Promise.resolve({}); - }, validateParams: (): Promise> => { const validationResult = { errors: {} }; return Promise.resolve(validationResult); diff --git a/x-pack/plugins/security_solution/cypress/screens/configure_cases.ts b/x-pack/plugins/security_solution/cypress/screens/configure_cases.ts index c9ed5299c0336..3e83a569d04ec 100644 --- a/x-pack/plugins/security_solution/cypress/screens/configure_cases.ts +++ b/x-pack/plugins/security_solution/cypress/screens/configure_cases.ts @@ -18,7 +18,7 @@ export const CONNECTORS_DROPDOWN = '[data-test-subj="dropdown-connectors"]'; export const PASSWORD = '[data-test-subj="connector-servicenow-password-form-input"]'; -export const SAVE_BTN = '[data-test-subj="saveNewActionButton"]'; +export const SAVE_BTN = '[data-test-subj="create-connector-flyout-save-btn"]'; export const SERVICE_NOW_CONNECTOR_CARD = '[data-test-subj=".servicenow-card"]'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx index 01e978ca2e61c..5798081833887 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx @@ -13,7 +13,6 @@ import ReactMarkdown from 'react-markdown'; import styled from 'styled-components'; import { - ActionForm, ActionType, loadActionTypes, ActionVariables, @@ -84,7 +83,7 @@ export const RuleActionsField: React.FC = ({ const { isSubmitted, isSubmitting, isValid } = form; const { http, - triggersActionsUi: { actionTypeRegistry }, + triggersActionsUi: { getActionForm }, } = useKibana().services; const actions: RuleAction[] = useMemo( @@ -135,6 +134,29 @@ export const RuleActionsField: React.FC = ({ [field.setValue, actions] ); + const actionForm = useMemo( + () => + getActionForm({ + actions, + messageVariables, + defaultActionGroupId: DEFAULT_ACTION_GROUP_ID, + setActionIdByIndex, + setActions: setAlertActionsProperty, + setActionParamsProperty, + actionTypes: supportedActionTypes, + defaultActionMessage: DEFAULT_ACTION_MESSAGE, + }), + [ + actions, + getActionForm, + messageVariables, + setActionIdByIndex, + setActionParamsProperty, + setAlertActionsProperty, + supportedActionTypes, + ] + ); + useEffect(() => { (async function () { const actionTypes = convertArrayToCamelCase(await loadActionTypes({ http })) as ActionType[]; @@ -168,17 +190,7 @@ export const RuleActionsField: React.FC = ({ ) : null} - + {actionForm} ); }; diff --git a/x-pack/plugins/synthetics/e2e/journeys/alerts/default_email_settings.ts b/x-pack/plugins/synthetics/e2e/journeys/alerts/default_email_settings.ts index a8be2f2129b32..6b0d9e541d4d3 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/alerts/default_email_settings.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/alerts/default_email_settings.ts @@ -62,7 +62,7 @@ journey('DefaultEmailSettings', async ({ page, params }) => { await page.fill(byTestId('emailHostInput'), 'test'); await page.fill(byTestId('emailPortInput'), '1025'); await page.click('text=Require authentication for this server'); - await page.click(byTestId('saveNewActionButton')); + await page.click(byTestId('create-connector-flyout-save-btn')); }); step('Select email connector', async () => { diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/settings/add_connector_flyout.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/settings/add_connector_flyout.tsx index 18f9e0b41d3db..f3cf70e9fe1ba 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/settings/add_connector_flyout.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/settings/add_connector_flyout.tsx @@ -56,13 +56,12 @@ export const AddConnectorFlyout = ({ focusInput, isDisabled }: Props) => { const ConnectorAddFlyout = useMemo( () => getAddConnectorFlyout({ - consumer: 'uptime', onClose: () => { dispatch(getConnectorsAction.get()); setAddFlyoutVisibility(false); focusInput(); }, - actionTypes: (actionTypes ?? []).filter((actionType) => + supportedActionTypes: (actionTypes ?? []).filter((actionType) => ALLOWED_ACTION_TYPES.includes(actionType.id as ActionTypeId) ), }), diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 13edd89027a06..8ece3d7c60386 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -28448,7 +28448,6 @@ "xpack.triggersActionsUI.actionVariables.ruleTagsLabel": "Balises de la règle.", "xpack.triggersActionsUI.actionVariables.ruleTypeLabel": "Type de règle.", "xpack.triggersActionsUI.appName": "Règles et connecteurs", - "xpack.triggersActionsUI.cases.configureCases.mappingFieldSummary": "Résumé", "xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByConfigMessage": "Ce connecteur est désactivé par la configuration de Kibana.", "xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage": "Ce connecteur requiert une licence {minimumLicenseRequired}.", "xpack.triggersActionsUI.checkRuleTypeEnabled.ruleTypeDisabledByLicenseMessage": "Ce type de règle requiert une licence {minimumLicenseRequired}.", @@ -28486,29 +28485,22 @@ "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.gmailServerTypeLabel": "Gmail", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.otherServerTypeLabel": "Autre", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.outlookServerTypeLabel": "Outlook", - "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterClientSecretLabel": "L'identifiant client secret est chiffré. Veuillez entrer à nouveau une valeur pour ce champ.", - "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterValuesLabel": "Le nom d'utilisateur et le mot de passe sont chiffrés. Veuillez entrer à nouveau les valeurs de ces champs.", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.selectMessageText": "Envoyez un e-mail à partir de votre serveur.", "xpack.triggersActionsUI.components.builtinActionTypes.error.badIndexOverrideSuffix": "L'index d'historique d'alertes doit contenir un suffixe valide.", "xpack.triggersActionsUI.components.builtinActionTypes.error.badIndexOverrideValue": "L'index d'historique d'alertes doit commencer par \"{alertHistoryPrefix}\".", - "xpack.triggersActionsUI.components.builtinActionTypes.error.formatFromText": "L'expéditeur n'est pas une adresse e-mail valide.", - "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredAuthPasswordText": "Le mot de passe est requis.", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredAuthUserNameText": "Le nom d'utilisateur est requis.", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredClientIdText": "L'ID client est requis.", - "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredClientSecretText": "L'identifiant client secret est requis.", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredDocumentJson": "Le document est requis et doit être un objet JSON valide.", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredEntryText": "Aucune entrée À, Cc ou Cci. Au moins une entrée est requise.", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredFromText": "L'expéditeur est requis.", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredHostText": "L'hôte est requis.", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredMessageText": "Le message est requis.", - "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPasswordText": "Le mot de passe est requis lorsque le nom d'utilisateur est utilisé.", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPortText": "Le port est requis.", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredServerLogMessageText": "Le message est requis.", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredServiceText": "Le service est requis.", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSlackMessageText": "Le message est requis.", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSubjectText": "Le sujet est requis.", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredTenantIdText": "L'ID locataire est requis.", - "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredUserText": "Le nom d'utilisateur est requis lorsque le mot de passe est utilisé.", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredWebhookBodyText": "Le corps est requis.", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.actionTypeTitle": "Données d'index", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.chooseLabel": "Choisir…", @@ -28517,7 +28509,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.definedateFieldTooltip": "Définissez ce champ temporel sur l'heure à laquelle le document a été indexé.", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.defineTimeFieldLabel": "Définissez l'heure pour chaque document", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.documentsFieldLabel": "Document à indexer", - "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.error.requiredIndexText": "L'index est requis.", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.executionTimeFieldLabel": "Champ temporel", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.howToBroadenSearchQueryDescription": "Utilisez le caractère * pour élargir votre recherche.", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.indexDocumentHelpLabel": "Exemple de document d'index.", @@ -28533,25 +28524,14 @@ "xpack.triggersActionsUI.components.builtinActionTypes.jira.actionTypeTitle": "Jira", "xpack.triggersActionsUI.components.builtinActionTypes.jira.apiTokenTextFieldLabel": "Token d'API", "xpack.triggersActionsUI.components.builtinActionTypes.jira.apiUrlTextFieldLabel": "URL", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.authenticationLabel": "Authentification", "xpack.triggersActionsUI.components.builtinActionTypes.jira.commentsTextAreaFieldLabel": "Commentaires supplémentaires", "xpack.triggersActionsUI.components.builtinActionTypes.jira.descriptionTextAreaFieldLabel": "Description", "xpack.triggersActionsUI.components.builtinActionTypes.jira.emailTextFieldLabel": "Adresse e-mail", "xpack.triggersActionsUI.components.builtinActionTypes.jira.impactSelectFieldLabel": "Étiquettes", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.invalidApiUrlTextField": "L'URL n'est pas valide.", "xpack.triggersActionsUI.components.builtinActionTypes.jira.labelsSpacesErrorMessage": "Les étiquettes ne peuvent pas contenir d'espaces.", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldComments": "Commentaires", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldDescription": "Description", "xpack.triggersActionsUI.components.builtinActionTypes.jira.parentIssueSearchLabel": "Problème lié au parent", "xpack.triggersActionsUI.components.builtinActionTypes.jira.projectKey": "Clé de projet", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.reenterValuesLabel": "Les informations d'authentification sont chiffrées. Veuillez entrer à nouveau les valeurs de ces champs.", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiTokenTextField": "Le token d'API est requis", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiUrlTextField": "L'URL est requise.", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredDescriptionTextField": "La description est requise.", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredEmailTextField": "L'adresse e-mail est requise", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredProjectKeyTextField": "La clé de projet est requise", "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredSummaryTextField": "Le résumé est requis.", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.requireHttpsApiUrlTextField": "L'URL doit commencer par https://.", "xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxAriaLabel": "Taper pour rechercher", "xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxPlaceholder": "Taper pour rechercher", "xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesLoading": "Chargement...", @@ -28563,7 +28543,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssuesMessage": "Impossible d'obtenir les problèmes", "xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssueTypesMessage": "Impossible d'obtenir les types d'erreurs", "xpack.triggersActionsUI.components.builtinActionTypes.jira.urgencySelectFieldLabel": "Type d'erreur", - "xpack.triggersActionsUI.components.builtinActionTypes.missingSecretsValuesLabel": "Les informations sensibles ne sont pas importées. Veuillez entrer {encryptedFieldsLength, plural, one {la valeur} other {les valeurs}} pour {encryptedFieldsLength, plural, one {le champ suivant} other {les champs suivants}}.", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.actionTypeTitle": "Envoyer à PagerDuty", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.apiUrlTextFieldLabel": "URL de l'API (facultative)", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.classFieldLabel": "Classe (facultative)", @@ -28579,7 +28558,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.eventSelectResolveOptionLabel": "Résoudre", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.eventSelectTriggerOptionLabel": "Déclencher", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.groupTextFieldLabel": "Regrouper (facultatif)", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.reenterValueLabel": "Cette clé est chiffrée. Veuillez entrer à nouveau une valeur pour ce champ.", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyNameHelpLabel": "Configurer un compte PagerDuty", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyTextFieldLabel": "Clé d'intégration", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.selectMessageText": "Envoyez un événement dans PagerDuty.", @@ -28591,29 +28569,15 @@ "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.sourceTextFieldLabel": "Source (facultative)", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.summaryFieldLabel": "Résumé", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.timestampTextFieldLabel": "Horodatage (facultatif)", - "xpack.triggersActionsUI.components.builtinActionTypes.rememberValueLabel": "Mémorisez {encryptedFieldsLength, plural, one {cette valeur} other {ces valeurs}}. Vous devrez {encryptedFieldsLength, plural, one {l'entrer} other {les entrer}} à nouveau chaque fois que vous modifierez le connecteur.", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.actionTypeTitle": "Résilient", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKey": "Clé d'API", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeyId": "ID", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeySecret": "Secret", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiUrlTextFieldLabel": "URL", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.commentsTextAreaFieldLabel": "Commentaires supplémentaires", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.descriptionTextAreaFieldLabel": "Description", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.invalidApiUrlTextField": "L'URL n'est pas valide.", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldComments": "Commentaires", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldDescription": "Description", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldShortDescription": "Nom", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.nameFieldLabel": "Nom (requis)", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.orgId": "ID d'organisation", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.reenterValuesLabel": "L'ID et le secret sont chiffrés. Veuillez entrer à nouveau les valeurs de ces champs.", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.rememberValuesLabel": "Mémorisez ces valeurs. Vous devrez les entrer à nouveau chaque fois que vous modifierez le connecteur.", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeyIdTextField": "L'ID est requis", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeySecretTextField": "Le secret est requis", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiUrlTextField": "L'URL est requise.", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredDescriptionTextField": "La description est requise.", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredNameTextField": "Le nom est requis.", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredOrgIdTextField": "L'ID d'organisation est requis", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requireHttpsApiUrlTextField": "L'URL doit commencer par https://.", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.selectMessageText": "Créez un incident dans IBM Resilient.", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.severity": "Sévérité", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.unableToGetIncidentTypesMessage": "Impossible d'obtenir les types d'incidents", @@ -28624,7 +28588,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.logMessageFieldLabel": "Message", "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.selectMessageText": "Ajouter un message au log Kibana.", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiInfoError": "Statut reçu : {status} lors de la tentative d'obtention d'informations sur l'application", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiUrlHelpText": "Spécifiez l'URL complète.", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiUrlTextFieldLabel": "URL d'instance ServiceNow", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.appInstallationInfo": "{update} {create} ", "xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.applicationRequiredCallout": "Application Elastic ServiceNow non installée", @@ -28643,20 +28606,14 @@ "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.descriptionTextAreaFieldLabel": "Description", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.eventClassTextAreaFieldLabel": "Instance source", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.impactSelectFieldLabel": "Impact", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.install": "installer", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.installationCalloutTitle": "Pour utiliser ce connecteur, installez d'abord l'application Elastic à partir de l'app store ServiceNow.", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.invalidApiUrlTextField": "L'URL n'est pas valide.", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.messageKeyTextAreaFieldLabel": "Clé de message", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.metricNameTextAreaFieldLabel": "Nom de l'indicateur", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.nodeTextAreaFieldLabel": "Nœud", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.passwordTextFieldLabel": "Mot de passe", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.prioritySelectFieldLabel": "Priorité", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.reenterValuesLabel": "Vous devrez vous authentifier chaque fois que vous modifierez le connecteur.", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredApiUrlTextField": "L'URL est requise.", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredPasswordTextField": "Le mot de passe est requis.", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredSeverityTextField": "La sévérité est requise.", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredUsernameTextField": "Le nom d'utilisateur est requis.", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requireHttpsApiUrlTextField": "L'URL doit commencer par https://.", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.resourceTextAreaFieldLabel": "Ressource", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.setupDevInstance": "configurer une instance de développeur", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severityRequiredSelectFieldLabel": "Sévérité (requise)", @@ -28693,10 +28650,7 @@ "xpack.triggersActionsUI.components.builtinActionTypes.serviceNowSIRAction.correlationIDHelpLabel": "Identificateur pour les incidents de mise à jour", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.actionTypeTitle": "Envoyer vers Slack", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.invalidWebhookUrlText": "L'URL de webhook n'est pas valide.", - "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requiredWebhookUrlText": "L'URL de webhook est requise.", - "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requireHttpsWebhookUrlText": "L'URL de webhook doit commencer par https://.", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.messageTextAreaFieldLabel": "Message", - "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.reenterValueLabel": "Cette URL est chiffrée. Veuillez entrer à nouveau une valeur pour ce champ.", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.selectMessageText": "Envoyez un message à un canal ou à un utilisateur Slack.", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlHelpLabel": "Créer une URL de webhook Slack", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlTextFieldLabel": "URL de webhook", @@ -28704,7 +28658,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.swimlane.unableToGetApplicationMessage": "Impossible d'obtenir l'application avec l'ID {id}", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.actionTypeTitle": "Créer l'enregistrement Swimlane", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertIdFieldLabel": "ID de l'alerte", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertSourceFieldLabel": "Source de l'alerte", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiTokenNameHelpLabel": "Fournir un token d'API Swimlane", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiTokenTextFieldLabel": "Token d'API", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiUrlTextFieldLabel": "URL d'API", @@ -28718,71 +28671,47 @@ "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.emptyMappingWarningDesc": "Ce connecteur ne peut pas être sélectionné, car il ne possède pas les mappings de champs d'alerte requis. Vous pouvez modifier ce connecteur pour ajouter les mappings de champs requis ou sélectionner un connecteur de type Alertes.", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.emptyMappingWarningTitle": "Ce connecteur ne possède pas de mappings de champs", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAlertID": "L'ID d'alerte est requis.", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAlertSource": "La source de l'alerte est requise.", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredApiTokenText": "Un token d'API est requis.", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAppIdText": "Un ID d'application est requis.", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredCaseID": "L'ID de cas est requis.", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredCaseName": "Le nom de cas est requis.", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredComments": "Les commentaires sont requis.", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredDescription": "La description est requise.", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredFieldMappingsText": "Les mappings de champs sont requis.", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredRuleName": "Le nom de règle est requis.", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredSeverity": "La sévérité est requise.", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.invalidApiUrlTextField": "L'URL n'est pas valide.", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingDescriptionTextFieldLabel": "Utilisé pour spécifier les noms de champs dans l'application Swimlane", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingFieldRequired": "Le mapping de champs est requis.", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingTitleTextFieldLabel": "Configurer les mappings de champs", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.nextStep": "Suivant", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.nextStepHelpText": "Si les mappings de champs ne sont pas configurés, le type de connecteur Swimlane sera défini sur Tous.", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.prevStep": "Précédent", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.reenterValueLabel": "Cette clé est chiffrée. Veuillez entrer à nouveau une valeur pour ce champ.", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.rememberValueLabel": "Mémorisez cette valeur. Vous devrez l'entrer à nouveau chaque fois que vous modifierez le connecteur.", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.requiredApiUrlTextField": "L'URL est requise.", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.retrieveConfigurationLabel": "Configurer les champs", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.ruleNameFieldLabel": "Nom de règle", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.selectMessageText": "Créer un enregistrement dans Swimlane", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.severityFieldLabel": "Sévérité", "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.actionTypeTitle": "Envoyer un message à un canal Microsoft Teams.", "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.invalidWebhookUrlText": "L'URL de webhook n'est pas valide.", "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredMessageText": "Le message est requis.", - "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredWebhookUrlText": "L'URL de webhook est requise.", - "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requireHttpsWebhookUrlText": "L'URL de webhook doit commencer par https://.", "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.messageTextAreaFieldLabel": "Message", - "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.reenterValueLabel": "Cette URL est chiffrée. Veuillez entrer à nouveau une valeur pour ce champ.", "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.selectMessageText": "Envoyer un message à un canal Microsoft Teams.", "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.webhookUrlHelpLabel": "Créer une URL de webhook Microsoft Teams", - "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.webhookUrlTextFieldLabel": "URL de webhook", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.actionTypeTitle": "Données de webhook", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeader": "Ajouter un en-tête", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeaderButton": "Ajouter", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.authenticationLabel": "Authentification", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.bodyCodeEditorAriaLabel": "Éditeur de code", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.bodyFieldLabel": "Corps", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.deleteHeaderButton": "Supprimer", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.invalidUrlTextField": "L'URL n'est pas valide.", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.requiredUrlText": "L'URL est requise.", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.hasAuthSwitchLabel": "Demander une authentification pour ce webhook", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.httpHeadersTitle": "En-têtes utilisés", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.keyTextFieldLabel": "Clé", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.methodTextFieldLabel": "Méthode", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.passwordTextFieldLabel": "Mot de passe", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.reenterValuesLabel": "Le nom d'utilisateur et le mot de passe sont chiffrés. Veuillez entrer à nouveau les valeurs de ces champs.", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.selectMessageText": "Envoyer une requête à un service Web.", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.urlTextFieldLabel": "URL", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.userTextFieldLabel": "Nom d'utilisateur", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.valueTextFieldLabel": "Valeur", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.viewHeadersSwitch": "Ajouter un en-tête HTTP", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.actionTypeTitle": "Données xMatters", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.authenticationLabel": "Authentification", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.basicAuthLabel": "Authentification de base", - "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.connectorSettingsFieldLabel": "URL d'initiation", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.connectorSettingsLabel": "Sélectionnez la méthode d'authentification utilisée pour la configuration du déclencheur xMatters.", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.error.invalidUrlTextField": "L'URL n'est pas valide.", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.error.requiredUrlText": "L'URL est requise.", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.initiationUrlHelpText": "Spécifiez l'URL xMatters complète.", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.passwordTextFieldLabel": "Mot de passe", - "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.reenterBasicAuthValuesLabel": "Le nom d'utilisateur et le mot de passe sont chiffrés. Veuillez entrer à nouveau les valeurs de ces champs.", - "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.reenterUrlAuthValuesLabel": "L'URL est chiffrée. Veuillez entrer à nouveau des valeurs pour ce champ.", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.selectMessageText": "Déclenchez un workflow xMatters.", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.severity": "Sévérité", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.severitySelectCriticalOptionLabel": "Critique", @@ -28855,10 +28784,6 @@ "xpack.triggersActionsUI.sections.actionConnectorAdd.upgradeYourPlanBannerLinkTitle": "Plans d'abonnement", "xpack.triggersActionsUI.sections.actionConnectorAdd.upgradeYourPlanBannerMessage": "Mettez à niveau votre licence ou démarrez un essai gratuit de 30 jours pour obtenir un accès immédiat à tous les connecteurs tiers.", "xpack.triggersActionsUI.sections.actionConnectorAdd.upgradeYourPlanBannerTitle": "Mettre à niveau votre licence pour accéder à tous les connecteurs", - "xpack.triggersActionsUI.sections.actionConnectorForm.actionNameLabel": "Nom du connecteur", - "xpack.triggersActionsUI.sections.actionConnectorForm.actions.actionConfigurationWarningHelpLinkText": "En savoir plus.", - "xpack.triggersActionsUI.sections.actionConnectorForm.actions.connectorTypeConfigurationWarningDescriptionText": "Pour créer ce connecteur, vous devez configurer au moins un compte {connectorType}. {docLink}", - "xpack.triggersActionsUI.sections.actionConnectorForm.actions.connectorTypeConfigurationWarningTitleText": "Type de connecteur non enregistré", "xpack.triggersActionsUI.sections.actionConnectorForm.connectorSettingsLabel": "Paramètres du connecteur", "xpack.triggersActionsUI.sections.actionConnectorForm.error.requiredNameText": "Le nom est requis.", "xpack.triggersActionsUI.sections.actionConnectorForm.loadingConnectorSettingsDescription": "Chargement des paramètres du connecteur…", @@ -28906,25 +28831,14 @@ "xpack.triggersActionsUI.sections.actionTypeForm.addNewActionConnectorActionGroup.display": "{actionGroupName} (non pris en charge actuellement)", "xpack.triggersActionsUI.sections.actionTypeForm.addNewConnectorEmptyButton": "Ajouter un connecteur", "xpack.triggersActionsUI.sections.actionTypeForm.existingAlertActionTypeEditTitle": "{actionConnectorName}", - "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthPasswordText": "Le mot de passe est requis.", "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthUserNameText": "Le nom d'utilisateur est requis.", - "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHeaderKeyText": "La clé est requise.", - "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHeaderValueText": "La valeur est requise.", "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText": "La méthode est requise.", - "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText": "Le mot de passe est requis lorsque le nom d'utilisateur est utilisé.", - "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredUserText": "Le nom d'utilisateur est requis lorsque le mot de passe est utilisé.", - "xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredAuthPasswordText": "Le mot de passe est requis.", - "xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredAuthUserNameText": "Le nom d'utilisateur est requis.", - "xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredPasswordText": "Le mot de passe est requis lorsque le nom d'utilisateur est utilisé.", - "xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredUserText": "Le nom d'utilisateur est requis lorsque le mot de passe est utilisé.", "xpack.triggersActionsUI.sections.addConnectorForm.flyoutTitle": "Connecteur {actionTypeName}", "xpack.triggersActionsUI.sections.addConnectorForm.selectConnectorFlyoutTitle": "Sélectionner un connecteur", - "xpack.triggersActionsUI.sections.addConnectorForm.updateErrorNotificationText": "Impossible de créer un connecteur.", "xpack.triggersActionsUI.sections.addConnectorForm.updateSuccessNotificationText": "Création de \"{connectorName}\" effectuée", "xpack.triggersActionsUI.sections.addModalConnectorForm.cancelButtonLabel": "Annuler", "xpack.triggersActionsUI.sections.addModalConnectorForm.flyoutTitle": "Connecteur {actionTypeName}", "xpack.triggersActionsUI.sections.addModalConnectorForm.saveButtonLabel": "Enregistrer", - "xpack.triggersActionsUI.sections.addModalConnectorForm.updateSuccessNotificationText": "Création de \"{connectorName}\" effectuée", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addBccButton": "Cci", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addCcButton": "Cc", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.authenticationLabel": "Authentification", @@ -28962,7 +28876,6 @@ "xpack.triggersActionsUI.sections.connectorAddInline.unableToLoadConnectorTitle'": "Impossible de charger le connecteur", "xpack.triggersActionsUI.sections.connectorAddInline.unauthorizedToCreateForEmptyConnectors": "Seuls les utilisateurs autorisés peuvent configurer un connecteur. Contactez votre administrateur.", "xpack.triggersActionsUI.sections.deprecatedTitleMessage": "(déclassé)", - "xpack.triggersActionsUI.sections.editConnectorForm.actionTypeDescription": "{actionDescription}", "xpack.triggersActionsUI.sections.editConnectorForm.cancelButtonLabel": "Annuler", "xpack.triggersActionsUI.sections.editConnectorForm.descriptionText": "Ce connecteur est en lecture seule.", "xpack.triggersActionsUI.sections.editConnectorForm.flyoutPreconfiguredTitle": "Modifier un connecteur", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 49a2a99abcfc3..4b1f81af84000 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -28609,7 +28609,6 @@ "xpack.triggersActionsUI.actionVariables.ruleTagsLabel": "ルールのタグ。", "xpack.triggersActionsUI.actionVariables.ruleTypeLabel": "ルールのタイプ。", "xpack.triggersActionsUI.appName": "ルールとコネクター", - "xpack.triggersActionsUI.cases.configureCases.mappingFieldSummary": "まとめ", "xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByConfigMessage": "このコネクターは Kibana の構成で無効になっています。", "xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage": "このコネクターには {minimumLicenseRequired} ライセンスが必要です。", "xpack.triggersActionsUI.checkRuleTypeEnabled.ruleTypeDisabledByLicenseMessage": "このルールタイプには{minimumLicenseRequired}ライセンスが必要です。", @@ -28647,29 +28646,22 @@ "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.gmailServerTypeLabel": "Gmail", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.otherServerTypeLabel": "その他", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.outlookServerTypeLabel": "Outlook", - "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterClientSecretLabel": "クライアントシークレットは暗号化されています。このフィールドの値を再入力してください。", - "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterValuesLabel": "ユーザー名とパスワードは暗号化されます。これらのフィールドの値を再入力してください。", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.selectMessageText": "サーバーからメールを送信します。", "xpack.triggersActionsUI.components.builtinActionTypes.error.badIndexOverrideSuffix": "アラート履歴インデックスには有効なサフィックスを含める必要があります。", "xpack.triggersActionsUI.components.builtinActionTypes.error.badIndexOverrideValue": "アラート履歴インデックスの先頭は\"{alertHistoryPrefix}\"でなければなりません。", - "xpack.triggersActionsUI.components.builtinActionTypes.error.formatFromText": "送信元は有効なメールアドレスではありません。", - "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredAuthPasswordText": "パスワードが必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredAuthUserNameText": "ユーザー名が必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredClientIdText": "クライアントIDは必須です。", - "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredClientSecretText": "クライアントシークレットは必須です。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredDocumentJson": "ドキュメントが必要です。有効なJSONオブジェクトにしてください。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredEntryText": "To、Cc、または Bcc のエントリーがありません。 1 つ以上のエントリーが必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredFromText": "送信元が必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredHostText": "ホストが必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredMessageText": "メッセージが必要です。", - "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPasswordText": "ユーザー名の使用時にはパスワードが必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPortText": "ポートが必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredServerLogMessageText": "メッセージが必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredServiceText": "サービスは必須です。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSlackMessageText": "メッセージが必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSubjectText": "件名が必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredTenantIdText": "テナントIDは必須です。", - "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredUserText": "パスワードの使用時にはユーザー名が必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredWebhookBodyText": "本文が必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.actionTypeTitle": "データをインデックスする", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.chooseLabel": "選択…", @@ -28678,7 +28670,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.definedateFieldTooltip": "この時間フィールドをドキュメントにインデックスが作成された時刻に設定します。", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.defineTimeFieldLabel": "各ドキュメントの時刻フィールドを定義", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.documentsFieldLabel": "インデックスするドキュメント", - "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.error.requiredIndexText": "インデックスが必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.executionTimeFieldLabel": "時間フィールド", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.howToBroadenSearchQueryDescription": "* で検索クエリの範囲を広げます。", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.indexDocumentHelpLabel": "インデックスドキュメントの例。", @@ -28694,25 +28685,14 @@ "xpack.triggersActionsUI.components.builtinActionTypes.jira.actionTypeTitle": "Jira", "xpack.triggersActionsUI.components.builtinActionTypes.jira.apiTokenTextFieldLabel": "APIトークン", "xpack.triggersActionsUI.components.builtinActionTypes.jira.apiUrlTextFieldLabel": "URL", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.authenticationLabel": "認証", "xpack.triggersActionsUI.components.builtinActionTypes.jira.commentsTextAreaFieldLabel": "追加のコメント", "xpack.triggersActionsUI.components.builtinActionTypes.jira.descriptionTextAreaFieldLabel": "説明", "xpack.triggersActionsUI.components.builtinActionTypes.jira.emailTextFieldLabel": "メールアドレス", "xpack.triggersActionsUI.components.builtinActionTypes.jira.impactSelectFieldLabel": "ラベル", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.invalidApiUrlTextField": "URL が無効です。", "xpack.triggersActionsUI.components.builtinActionTypes.jira.labelsSpacesErrorMessage": "ラベルにはスペースを使用できません。", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldComments": "コメント", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldDescription": "説明", "xpack.triggersActionsUI.components.builtinActionTypes.jira.parentIssueSearchLabel": "親問題", "xpack.triggersActionsUI.components.builtinActionTypes.jira.projectKey": "プロジェクトキー", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.reenterValuesLabel": "認証資格情報は暗号化されます。これらのフィールドの値を再入力してください。", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiTokenTextField": "APIトークンが必要です", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiUrlTextField": "URL が必要です。", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredDescriptionTextField": "説明が必要です。", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredEmailTextField": "メールアドレスが必要です", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredProjectKeyTextField": "プロジェクトキーが必要です", "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredSummaryTextField": "概要が必要です。", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.requireHttpsApiUrlTextField": "URL は https:// から始める必要があります。", "xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxAriaLabel": "入力して検索", "xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxPlaceholder": "入力して検索", "xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesLoading": "読み込み中...", @@ -28724,7 +28704,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssuesMessage": "問題を取得できません", "xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssueTypesMessage": "問題タイプを取得できません", "xpack.triggersActionsUI.components.builtinActionTypes.jira.urgencySelectFieldLabel": "問題タイプ", - "xpack.triggersActionsUI.components.builtinActionTypes.missingSecretsValuesLabel": "機密情報はインポートされません。次のフィールド{encryptedFieldsLength, plural, other {}}の値{encryptedFieldsLength, plural, other {}}入力してください。", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.actionTypeTitle": "PagerDuty に送信", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.apiUrlTextFieldLabel": "API URL(任意)", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.classFieldLabel": "クラス(任意)", @@ -28740,7 +28719,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.eventSelectResolveOptionLabel": "解決", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.eventSelectTriggerOptionLabel": "トリガー", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.groupTextFieldLabel": "グループ(任意)", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.reenterValueLabel": "このキーは暗号化されています。このフィールドの値を再入力してください。", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyNameHelpLabel": "PagerDuty アカウントを構成します", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyTextFieldLabel": "統合キー", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.selectMessageText": "PagerDuty でイベントを送信します。", @@ -28752,29 +28730,15 @@ "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.sourceTextFieldLabel": "ソース(任意)", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.summaryFieldLabel": "まとめ", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.timestampTextFieldLabel": "タイムスタンプ(任意)", - "xpack.triggersActionsUI.components.builtinActionTypes.rememberValueLabel": "{encryptedFieldsLength, plural, one {この} other {これらの}}値を記憶しておいてください。コネクターを編集するたびに、{encryptedFieldsLength, plural, one {それを} other {それらを}}再入力する必要があります。", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.actionTypeTitle": "Resilient", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKey": "API キー", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeyId": "ID", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeySecret": "シークレット", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiUrlTextFieldLabel": "URL", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.commentsTextAreaFieldLabel": "追加のコメント", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.descriptionTextAreaFieldLabel": "説明", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.invalidApiUrlTextField": "URL が無効です。", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldComments": "コメント", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldDescription": "説明", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldShortDescription": "名前", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.nameFieldLabel": "名前(必須)", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.orgId": "組織 ID", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.reenterValuesLabel": "ID とシークレットは暗号化されます。これらのフィールドの値を再入力してください。", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.rememberValuesLabel": "これらの値を覚えておいてください。コネクターを編集するたびに再入力する必要があります。", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeyIdTextField": "IDが必要です", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeySecretTextField": "シークレットが必要です", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiUrlTextField": "URL が必要です。", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredDescriptionTextField": "説明が必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredNameTextField": "名前が必要です。", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredOrgIdTextField": "組織 ID が必要です", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requireHttpsApiUrlTextField": "URL は https:// から始める必要があります。", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.selectMessageText": "IBM Resilient でインシデントを作成します。", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.severity": "深刻度", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.unableToGetIncidentTypesMessage": "インシデントタイプを取得できません", @@ -28785,7 +28749,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.logMessageFieldLabel": "メッセージ", "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.selectMessageText": "Kibana ログにメッセージを追加します。", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiInfoError": "アプリケーション情報の取得を試みるときの受信ステータス:{status}", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiUrlHelpText": "完全なURLを含めます。", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiUrlTextFieldLabel": "ServiceNowインスタンスURL", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.appInstallationInfo": "{update} {create} ", "xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.applicationRequiredCallout": "Elastic ServiceNowアプリがインストールされていません", @@ -28804,20 +28767,14 @@ "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.descriptionTextAreaFieldLabel": "説明", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.eventClassTextAreaFieldLabel": "ソースインスタンス", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.impactSelectFieldLabel": "インパクト", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.install": "インストール", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.installationCalloutTitle": "このコネクターを使用するには、まずServiceNowアプリストアからElasticアプリをインストールします。", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.invalidApiUrlTextField": "URL が無効です。", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.messageKeyTextAreaFieldLabel": "メッセージキー", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.metricNameTextAreaFieldLabel": "メトリック名", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.nodeTextAreaFieldLabel": "ノード", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.passwordTextFieldLabel": "パスワード", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.prioritySelectFieldLabel": "優先度", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.reenterValuesLabel": "コネクターを編集するたびに認証する必要があります。", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredApiUrlTextField": "URL が必要です。", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredPasswordTextField": "パスワードが必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredSeverityTextField": "重要度は必須です。", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredUsernameTextField": "ユーザー名が必要です。", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requireHttpsApiUrlTextField": "URL は https:// から始める必要があります。", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.resourceTextAreaFieldLabel": "リソース", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.setupDevInstance": "開発者インスタンスを設定", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severityRequiredSelectFieldLabel": "重要度(必須)", @@ -28854,10 +28811,7 @@ "xpack.triggersActionsUI.components.builtinActionTypes.serviceNowSIRAction.correlationIDHelpLabel": "インシデントを更新するID", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.actionTypeTitle": "Slack に送信", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.invalidWebhookUrlText": "Web フック URL が無効です。", - "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requiredWebhookUrlText": "Web フック URL が必要です。", - "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requireHttpsWebhookUrlText": "Web フック URL は https:// から始める必要があります。", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.messageTextAreaFieldLabel": "メッセージ", - "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.reenterValueLabel": "この URL は暗号化されています。このフィールドの値を再入力してください。", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.selectMessageText": "Slack チャネルにメッセージを送信します。", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlHelpLabel": "Slack Web フック URL を作成", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlTextFieldLabel": "Web フック URL", @@ -28865,7 +28819,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.swimlane.unableToGetApplicationMessage": "id {id}のアプリケーションフィールドを取得できません", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.actionTypeTitle": "Swimlaneレコードを作成", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertIdFieldLabel": "アラートID", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertSourceFieldLabel": "アラートソース", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiTokenNameHelpLabel": "Swimlane APIトークンを指定", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiTokenTextFieldLabel": "APIトークン", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiUrlTextFieldLabel": "API Url", @@ -28879,71 +28832,47 @@ "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.emptyMappingWarningDesc": "このコネクターを選択できません。必要なアラートフィールドマッピングがありません。このコネクターを編集して、必要なフィールドマッピングを追加するか、タイプがアラートのコネクターを選択できます。", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.emptyMappingWarningTitle": "このコネクターにはフィールドマッピングがありません。", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAlertID": "アラートIDは必須です。", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAlertSource": "アラートソースは必須です。", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredApiTokenText": "API トークンは必須です。", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAppIdText": "アプリIDは必須です。", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredCaseID": "ケースIDは必須です。", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredCaseName": "ケース名は必須です。", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredComments": "コメントは必須です。", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredDescription": "説明が必要です。", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredFieldMappingsText": "フィールドマッピングは必須です。", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredRuleName": "ルール名は必須です。", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredSeverity": "重要度は必須です。", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.invalidApiUrlTextField": "URL が無効です。", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingDescriptionTextFieldLabel": "Swimlaneアプリケーションでフィールド名を指定するために使用されます", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingFieldRequired": "フィールドマッピングは必須です。", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingTitleTextFieldLabel": "フィールドマッピングを構成", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.nextStep": "次へ", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.nextStepHelpText": "フィールドマッピングが構成されていない場合、Swimlaneコネクタータイプはすべてに設定されます。", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.prevStep": "戻る", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.reenterValueLabel": "このキーは暗号化されています。このフィールドの値を再入力してください。", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.rememberValueLabel": "この値を覚えておいてください。コネクターを編集するたびに再入力する必要があります。", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.requiredApiUrlTextField": "URL が必要です。", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.retrieveConfigurationLabel": "フィールドを構成", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.ruleNameFieldLabel": "ルール名", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.selectMessageText": "Swimlaneでレコードを作成", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.severityFieldLabel": "深刻度", "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.actionTypeTitle": "メッセージを Microsoft Teams チャネルに送信します。", "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.invalidWebhookUrlText": "Web フック URL が無効です。", "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredMessageText": "メッセージが必要です。", - "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredWebhookUrlText": "Web フック URL が必要です。", - "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requireHttpsWebhookUrlText": "Web フック URL は https:// から始める必要があります。", "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.messageTextAreaFieldLabel": "メッセージ", - "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.reenterValueLabel": "この URL は暗号化されています。このフィールドの値を再入力してください。", "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.selectMessageText": "メッセージを Microsoft Teams チャネルに送信します。", "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.webhookUrlHelpLabel": "Microsoft Teams Web フック URL を作成", - "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.webhookUrlTextFieldLabel": "Web フック URL", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.actionTypeTitle": "Web フックデータ", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeader": "ヘッダーを追加", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeaderButton": "追加", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.authenticationLabel": "認証", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.bodyCodeEditorAriaLabel": "コードエディター", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.bodyFieldLabel": "本文", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.deleteHeaderButton": "削除", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.invalidUrlTextField": "URL が無効です。", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.requiredUrlText": "URL が必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.hasAuthSwitchLabel": "この Web フックの認証が必要です", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.httpHeadersTitle": "使用中のヘッダー", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.keyTextFieldLabel": "キー", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.methodTextFieldLabel": "メソド", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.passwordTextFieldLabel": "パスワード", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.reenterValuesLabel": "ユーザー名とパスワードは暗号化されます。これらのフィールドの値を再入力してください。", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.selectMessageText": "Web サービスにリクエストを送信してください。", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.urlTextFieldLabel": "URL", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.userTextFieldLabel": "ユーザー名", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.valueTextFieldLabel": "値", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.viewHeadersSwitch": "HTTP ヘッダーを追加", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.actionTypeTitle": "xMattersデータ", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.authenticationLabel": "認証", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.basicAuthLabel": "基本認証", - "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.connectorSettingsFieldLabel": "開始URL", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.connectorSettingsLabel": "xMattersトリガーを設定するときに使用される認証方法を選択します。", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.error.invalidUrlTextField": "URL が無効です。", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.error.requiredUrlText": "URL が必要です。", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.initiationUrlHelpText": "完全なxMatters URLを含めます。", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.passwordTextFieldLabel": "パスワード", - "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.reenterBasicAuthValuesLabel": "ユーザー名とパスワードは暗号化されます。これらのフィールドの値を再入力してください。", - "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.reenterUrlAuthValuesLabel": "URLは暗号化されています。このフィールドの値を再入力してください。", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.selectMessageText": "xMattersワークフローをトリガーします。", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.severity": "深刻度", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.severitySelectCriticalOptionLabel": "重大", @@ -29017,10 +28946,6 @@ "xpack.triggersActionsUI.sections.actionConnectorAdd.upgradeYourPlanBannerLinkTitle": "サブスクリプションオプション", "xpack.triggersActionsUI.sections.actionConnectorAdd.upgradeYourPlanBannerMessage": "すべてのサードパーティコネクターにすぐにアクセスするには、ライセンスをアップグレードするか、30日間無料の試用版を開始してください。", "xpack.triggersActionsUI.sections.actionConnectorAdd.upgradeYourPlanBannerTitle": "ライセンスをアップグレードしてすべてのコネクターにアクセス", - "xpack.triggersActionsUI.sections.actionConnectorForm.actionNameLabel": "コネクター名", - "xpack.triggersActionsUI.sections.actionConnectorForm.actions.actionConfigurationWarningHelpLinkText": "詳細情報", - "xpack.triggersActionsUI.sections.actionConnectorForm.actions.connectorTypeConfigurationWarningDescriptionText": "コネクターを作成するには、1 つ以上の{connectorType}アカウントを構成する必要があります。{docLink}", - "xpack.triggersActionsUI.sections.actionConnectorForm.actions.connectorTypeConfigurationWarningTitleText": "コネクタータイプが登録されていません", "xpack.triggersActionsUI.sections.actionConnectorForm.connectorSettingsLabel": "コネクター設定", "xpack.triggersActionsUI.sections.actionConnectorForm.error.requiredNameText": "名前が必要です。", "xpack.triggersActionsUI.sections.actionConnectorForm.loadingConnectorSettingsDescription": "コネクター設定を読み込んでいます...", @@ -29068,25 +28993,14 @@ "xpack.triggersActionsUI.sections.actionTypeForm.addNewActionConnectorActionGroup.display": "{actionGroupName}(現在サポートされていません)", "xpack.triggersActionsUI.sections.actionTypeForm.addNewConnectorEmptyButton": "コネクターの追加", "xpack.triggersActionsUI.sections.actionTypeForm.existingAlertActionTypeEditTitle": "{actionConnectorName}", - "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthPasswordText": "パスワードが必要です。", "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthUserNameText": "ユーザー名が必要です。", - "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHeaderKeyText": "キーが必要です。", - "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHeaderValueText": "値が必要です。", "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText": "メソッドが必要です。", - "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText": "ユーザー名の使用時にはパスワードが必要です。", - "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredUserText": "パスワードの使用時にはユーザー名が必要です。", - "xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredAuthPasswordText": "パスワードが必要です。", - "xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredAuthUserNameText": "ユーザー名が必要です。", - "xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredPasswordText": "ユーザー名の使用時にはパスワードが必要です。", - "xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredUserText": "パスワードの使用時にはユーザー名が必要です。", "xpack.triggersActionsUI.sections.addConnectorForm.flyoutTitle": "{actionTypeName}コネクター", "xpack.triggersActionsUI.sections.addConnectorForm.selectConnectorFlyoutTitle": "コネクターを選択", - "xpack.triggersActionsUI.sections.addConnectorForm.updateErrorNotificationText": "コネクターを作成できません。", "xpack.triggersActionsUI.sections.addConnectorForm.updateSuccessNotificationText": "「{connectorName}」を作成しました", "xpack.triggersActionsUI.sections.addModalConnectorForm.cancelButtonLabel": "キャンセル", "xpack.triggersActionsUI.sections.addModalConnectorForm.flyoutTitle": "{actionTypeName}コネクター", "xpack.triggersActionsUI.sections.addModalConnectorForm.saveButtonLabel": "保存", - "xpack.triggersActionsUI.sections.addModalConnectorForm.updateSuccessNotificationText": "「{connectorName}」を作成しました", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addBccButton": "Bcc", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addCcButton": "Cc", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.authenticationLabel": "認証", @@ -29124,7 +29038,6 @@ "xpack.triggersActionsUI.sections.connectorAddInline.unableToLoadConnectorTitle'": "コネクターを読み込めません", "xpack.triggersActionsUI.sections.connectorAddInline.unauthorizedToCreateForEmptyConnectors": "許可されたユーザーのみがコネクターを構成できます。管理者にお問い合わせください。", "xpack.triggersActionsUI.sections.deprecatedTitleMessage": "(非推奨)", - "xpack.triggersActionsUI.sections.editConnectorForm.actionTypeDescription": "{actionDescription}", "xpack.triggersActionsUI.sections.editConnectorForm.cancelButtonLabel": "キャンセル", "xpack.triggersActionsUI.sections.editConnectorForm.descriptionText": "このコネクターは読み取り専用です。", "xpack.triggersActionsUI.sections.editConnectorForm.flyoutPreconfiguredTitle": "コネクターを編集", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ab53a184d44e6..fa9b9677ccbe1 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -28643,7 +28643,6 @@ "xpack.triggersActionsUI.actionVariables.ruleTagsLabel": "规则的标签。", "xpack.triggersActionsUI.actionVariables.ruleTypeLabel": "规则的类型。", "xpack.triggersActionsUI.appName": "规则和连接器", - "xpack.triggersActionsUI.cases.configureCases.mappingFieldSummary": "摘要", "xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByConfigMessage": "连接器已由 Kibana 配置禁用。", "xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage": "此连接器需要{minimumLicenseRequired}许可证。", "xpack.triggersActionsUI.checkRuleTypeEnabled.ruleTypeDisabledByLicenseMessage": "此规则类型需要{minimumLicenseRequired}许可证。", @@ -28681,29 +28680,22 @@ "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.gmailServerTypeLabel": "Gmail", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.otherServerTypeLabel": "其他", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.outlookServerTypeLabel": "Outlook", - "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterClientSecretLabel": "客户端密钥已加密。请为此字段重新输入值。", - "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterValuesLabel": "用户名和密码已加密。请为这些字段重新输入值。", "xpack.triggersActionsUI.components.builtinActionTypes.emailAction.selectMessageText": "从您的服务器发送电子邮件。", "xpack.triggersActionsUI.components.builtinActionTypes.error.badIndexOverrideSuffix": "告警历史记录索引必须包含有效的后缀。", "xpack.triggersActionsUI.components.builtinActionTypes.error.badIndexOverrideValue": "告警历史记录索引必须以“{alertHistoryPrefix}”开头。", - "xpack.triggersActionsUI.components.builtinActionTypes.error.formatFromText": "发送者电子邮件地址无效。", - "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredAuthPasswordText": "“密码”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredAuthUserNameText": "“用户名”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredClientIdText": "“客户端 ID”必填。", - "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredClientSecretText": "“客户端密钥”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredDocumentJson": "“文档”必填,并且应为有效的 JSON 对象。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredEntryText": "未输入收件人、抄送、密送。 至少需要输入一个。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredFromText": "“发送者”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredHostText": "“主机”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredMessageText": "“消息”必填。", - "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPasswordText": "使用用户名时,“密码”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPortText": "“端口”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredServerLogMessageText": "“消息”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredServiceText": "“服务”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSlackMessageText": "“消息”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSubjectText": "“主题”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredTenantIdText": "“租户 ID”必填。", - "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredUserText": "使用密码时,“用户名”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.error.requiredWebhookBodyText": "“正文”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.actionTypeTitle": "索引数据", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.chooseLabel": "选择……", @@ -28712,7 +28704,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.definedateFieldTooltip": "将此时间字段设置为索引文档的时间。", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.defineTimeFieldLabel": "为每个文档定义时间字段", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.documentsFieldLabel": "要索引的文档", - "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.error.requiredIndexText": "“索引”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.executionTimeFieldLabel": "时间字段", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.howToBroadenSearchQueryDescription": "使用 * 可扩大您的查询范围。", "xpack.triggersActionsUI.components.builtinActionTypes.indexAction.indexDocumentHelpLabel": "索引文档示例。", @@ -28728,25 +28719,14 @@ "xpack.triggersActionsUI.components.builtinActionTypes.jira.actionTypeTitle": "Jira", "xpack.triggersActionsUI.components.builtinActionTypes.jira.apiTokenTextFieldLabel": "API 令牌", "xpack.triggersActionsUI.components.builtinActionTypes.jira.apiUrlTextFieldLabel": "URL", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.authenticationLabel": "身份验证", "xpack.triggersActionsUI.components.builtinActionTypes.jira.commentsTextAreaFieldLabel": "其他注释", "xpack.triggersActionsUI.components.builtinActionTypes.jira.descriptionTextAreaFieldLabel": "描述", "xpack.triggersActionsUI.components.builtinActionTypes.jira.emailTextFieldLabel": "电子邮件地址", "xpack.triggersActionsUI.components.builtinActionTypes.jira.impactSelectFieldLabel": "标签", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.invalidApiUrlTextField": "URL 无效。", "xpack.triggersActionsUI.components.builtinActionTypes.jira.labelsSpacesErrorMessage": "标签不能包含空格。", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldComments": "注释", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldDescription": "描述", "xpack.triggersActionsUI.components.builtinActionTypes.jira.parentIssueSearchLabel": "父问题", "xpack.triggersActionsUI.components.builtinActionTypes.jira.projectKey": "项目键", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.reenterValuesLabel": "验证凭据已加密。请为这些字段重新输入值。", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiTokenTextField": "“API 令牌”必填", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiUrlTextField": "“URL”必填。", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredDescriptionTextField": "“描述”必填。", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredEmailTextField": "电子邮件地址必填", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredProjectKeyTextField": "“项目键”必填", "xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredSummaryTextField": "“摘要”必填。", - "xpack.triggersActionsUI.components.builtinActionTypes.jira.requireHttpsApiUrlTextField": "URL 必须以 https:// 开头。", "xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxAriaLabel": "键入内容进行搜索", "xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesComboBoxPlaceholder": "键入内容进行搜索", "xpack.triggersActionsUI.components.builtinActionTypes.jira.searchIssuesLoading": "正在加载……", @@ -28758,7 +28738,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssuesMessage": "无法获取问题", "xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssueTypesMessage": "无法获取问题类型", "xpack.triggersActionsUI.components.builtinActionTypes.jira.urgencySelectFieldLabel": "问题类型", - "xpack.triggersActionsUI.components.builtinActionTypes.missingSecretsValuesLabel": "未导入敏感信息。请为以下字段{encryptedFieldsLength, plural, other {}}输入值{encryptedFieldsLength, plural, other {}}。", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.actionTypeTitle": "发送到 PagerDuty", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.apiUrlTextFieldLabel": "API URL(可选)", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.classFieldLabel": "类(可选)", @@ -28774,7 +28753,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.eventSelectResolveOptionLabel": "解决", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.eventSelectTriggerOptionLabel": "触发", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.groupTextFieldLabel": "组(可选)", - "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.reenterValueLabel": "此密钥已加密。请为此字段重新输入值。", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyNameHelpLabel": "配置 PagerDuty 帐户", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyTextFieldLabel": "集成密钥", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.selectMessageText": "在 PagerDuty 中发送事件。", @@ -28786,29 +28764,15 @@ "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.sourceTextFieldLabel": "源(可选)", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.summaryFieldLabel": "摘要", "xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.timestampTextFieldLabel": "时间戳(可选)", - "xpack.triggersActionsUI.components.builtinActionTypes.rememberValueLabel": "记住{encryptedFieldsLength, plural, one {此} other {这些}}值。每次编辑连接器时都必须重新输入{encryptedFieldsLength, plural, other {值}}。", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.actionTypeTitle": "Resilient", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKey": "API 密钥", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeyId": "ID", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeySecret": "机密", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiUrlTextFieldLabel": "URL", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.commentsTextAreaFieldLabel": "其他注释", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.descriptionTextAreaFieldLabel": "描述", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.invalidApiUrlTextField": "URL 无效。", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldComments": "注释", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldDescription": "描述", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldShortDescription": "名称", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.nameFieldLabel": "名称(必填)", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.orgId": "组织 ID", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.reenterValuesLabel": "ID 和密钥已加密。请为这些字段重新输入值。", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.rememberValuesLabel": "请记住这些值。每次编辑连接器时都必须重新输入。", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeyIdTextField": "“ID”必填", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeySecretTextField": "“密钥”必填", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiUrlTextField": "“URL”必填。", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredDescriptionTextField": "“描述”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredNameTextField": "“名称”必填。", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredOrgIdTextField": "“组织 ID”必填", - "xpack.triggersActionsUI.components.builtinActionTypes.resilient.requireHttpsApiUrlTextField": "URL 必须以 https:// 开头。", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.selectMessageText": "在 IBM Resilient 中创建事件。", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.severity": "严重性", "xpack.triggersActionsUI.components.builtinActionTypes.resilient.unableToGetIncidentTypesMessage": "无法获取事件类型", @@ -28819,7 +28783,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.logMessageFieldLabel": "消息", "xpack.triggersActionsUI.components.builtinActionTypes.serverLogAction.selectMessageText": "将消息添加到 Kibana 日志。", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiInfoError": "尝试获取应用程序信息时收到的状态:{status}", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiUrlHelpText": "包括完整 URL。", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiUrlTextFieldLabel": "ServiceNow 实例 URL", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.appInstallationInfo": "{update} {create} ", "xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.applicationRequiredCallout": "未安装 Elastic ServiceNow 应用", @@ -28838,20 +28801,14 @@ "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.descriptionTextAreaFieldLabel": "描述", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.eventClassTextAreaFieldLabel": "源实例", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.impactSelectFieldLabel": "影响", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.install": "安装", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.installationCalloutTitle": "要使用此连接器,请先从 ServiceNow 应用商店安装 Elastic 应用。", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.invalidApiUrlTextField": "URL 无效。", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.messageKeyTextAreaFieldLabel": "消息密钥", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.metricNameTextAreaFieldLabel": "指标名称", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.nodeTextAreaFieldLabel": "节点", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.passwordTextFieldLabel": "密码", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.prioritySelectFieldLabel": "优先级", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.reenterValuesLabel": "每次编辑连接器时都必须进行身份验证。", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredApiUrlTextField": "“URL”必填。", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredPasswordTextField": "“密码”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredSeverityTextField": "“严重性”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredUsernameTextField": "“用户名”必填。", - "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requireHttpsApiUrlTextField": "URL 必须以 https:// 开头。", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.resourceTextAreaFieldLabel": "资源", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.setupDevInstance": "设置开发者实例", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.severityRequiredSelectFieldLabel": "严重性(必需)", @@ -28888,10 +28845,7 @@ "xpack.triggersActionsUI.components.builtinActionTypes.serviceNowSIRAction.correlationIDHelpLabel": "用于更新事件的标识符", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.actionTypeTitle": "发送到 Slack", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.invalidWebhookUrlText": "Webhook URL 无效。", - "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requiredWebhookUrlText": "“Webhook URL”必填。", - "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requireHttpsWebhookUrlText": "Webhook URL 必须以 https:// 开头。", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.messageTextAreaFieldLabel": "消息", - "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.reenterValueLabel": "此 URL 已加密。请为此字段重新输入值。", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.selectMessageText": "向 Slack 频道或用户发送消息。", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlHelpLabel": "创建 Slack webhook URL", "xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlTextFieldLabel": "Webhook URL", @@ -28899,7 +28853,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.swimlane.unableToGetApplicationMessage": "无法获取 ID 为 {id} 的应用程序", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.actionTypeTitle": "创建泳道记录", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertIdFieldLabel": "告警 ID", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertSourceFieldLabel": "告警源", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiTokenNameHelpLabel": "提供泳道 API 令牌", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiTokenTextFieldLabel": "API 令牌", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiUrlTextFieldLabel": "API URL", @@ -28913,71 +28866,47 @@ "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.emptyMappingWarningDesc": "无法选择此连接器,因为其缺失所需的告警字段映射。您可以编辑此连接器以添加所需的字段映射或选择告警类型的连接器。", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.emptyMappingWarningTitle": "此连接器缺失字段映射", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAlertID": "“告警 ID”必填。", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAlertSource": "“告警源”必填。", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredApiTokenText": "“API 令牌”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAppIdText": "“应用 ID”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredCaseID": "“案例 ID”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredCaseName": "“案例名称”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredComments": "“注释”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredDescription": "“描述”必填。", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredFieldMappingsText": "“字段映射”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredRuleName": "“规则名称”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredSeverity": "“严重性”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.invalidApiUrlTextField": "URL 无效。", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingDescriptionTextFieldLabel": "用于指定泳道应用程序中的字段名称", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingFieldRequired": "“字段映射”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingTitleTextFieldLabel": "配置字段映射", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.nextStep": "下一步", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.nextStepHelpText": "如果未配置字段映射,泳道连接器类型将设置为 all。", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.prevStep": "返回", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.reenterValueLabel": "此密钥已加密。请为此字段重新输入值。", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.rememberValueLabel": "请记住此值。每次编辑连接器时都必须重新输入。", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.requiredApiUrlTextField": "“URL”必填。", - "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.retrieveConfigurationLabel": "配置字段", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.ruleNameFieldLabel": "规则名称", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.selectMessageText": "在泳道中创建记录", "xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.severityFieldLabel": "严重性", "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.actionTypeTitle": "向 Microsoft Teams 频道发送消息。", "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.invalidWebhookUrlText": "Webhook URL 无效。", "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredMessageText": "“消息”必填。", - "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredWebhookUrlText": "“Webhook URL”必填。", - "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requireHttpsWebhookUrlText": "Webhook URL 必须以 https:// 开头。", "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.messageTextAreaFieldLabel": "消息", - "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.reenterValueLabel": "此 URL 已加密。请为此字段重新输入值。", "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.selectMessageText": "向 Microsoft Teams 频道发送消息。", "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.webhookUrlHelpLabel": "创建 Microsoft Teams Webhook URL", - "xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.webhookUrlTextFieldLabel": "Webhook URL", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.actionTypeTitle": "Webhook 数据", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeader": "添加标头", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeaderButton": "添加", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.authenticationLabel": "身份验证", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.bodyCodeEditorAriaLabel": "代码编辑器", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.bodyFieldLabel": "正文", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.deleteHeaderButton": "删除", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.invalidUrlTextField": "URL 无效。", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.requiredUrlText": "“URL”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.hasAuthSwitchLabel": "此 Webhook 需要身份验证", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.httpHeadersTitle": "在用的标头", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.keyTextFieldLabel": "钥匙", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.methodTextFieldLabel": "方法", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.passwordTextFieldLabel": "密码", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.reenterValuesLabel": "用户名和密码已加密。请为这些字段重新输入值。", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.selectMessageText": "将请求发送到 Web 服务。", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.urlTextFieldLabel": "URL", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.userTextFieldLabel": "用户名", - "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.valueTextFieldLabel": "值", "xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.viewHeadersSwitch": "添加 HTTP 标头", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.actionTypeTitle": "xMatters 数据", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.authenticationLabel": "身份验证", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.basicAuthLabel": "基本身份验证", - "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.connectorSettingsFieldLabel": "发起 URL", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.connectorSettingsLabel": "选择在设置 xMatters 触发器时使用的身份验证方法。", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.error.invalidUrlTextField": "URL 无效。", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.error.requiredUrlText": "“URL”必填。", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.initiationUrlHelpText": "包括完整 xMatters url。", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.passwordTextFieldLabel": "密码", - "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.reenterBasicAuthValuesLabel": "用户和密码已加密。请为这些字段重新输入值。", - "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.reenterUrlAuthValuesLabel": "URL 已加密。请为此字段重新输入值。", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.selectMessageText": "触发 xMatters 工作流。", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.severity": "严重性", "xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.severitySelectCriticalOptionLabel": "紧急", @@ -29050,10 +28979,6 @@ "xpack.triggersActionsUI.sections.actionConnectorAdd.upgradeYourPlanBannerLinkTitle": "订阅计划", "xpack.triggersActionsUI.sections.actionConnectorAdd.upgradeYourPlanBannerMessage": "升级您的许可证或开始为期 30 天的免费试用,以便可以立即使用所有第三方连接器。", "xpack.triggersActionsUI.sections.actionConnectorAdd.upgradeYourPlanBannerTitle": "升级您的许可证以访问所有连接器", - "xpack.triggersActionsUI.sections.actionConnectorForm.actionNameLabel": "连接器名称", - "xpack.triggersActionsUI.sections.actionConnectorForm.actions.actionConfigurationWarningHelpLinkText": "了解详情。", - "xpack.triggersActionsUI.sections.actionConnectorForm.actions.connectorTypeConfigurationWarningDescriptionText": "要创建此连接器,必须至少配置一个 {connectorType} 帐户。{docLink}", - "xpack.triggersActionsUI.sections.actionConnectorForm.actions.connectorTypeConfigurationWarningTitleText": "未注册连接器类型", "xpack.triggersActionsUI.sections.actionConnectorForm.connectorSettingsLabel": "连接器设置", "xpack.triggersActionsUI.sections.actionConnectorForm.error.requiredNameText": "“名称”必填。", "xpack.triggersActionsUI.sections.actionConnectorForm.loadingConnectorSettingsDescription": "正在加载连接器设置……", @@ -29101,25 +29026,14 @@ "xpack.triggersActionsUI.sections.actionTypeForm.addNewActionConnectorActionGroup.display": "{actionGroupName}(当前不支持)", "xpack.triggersActionsUI.sections.actionTypeForm.addNewConnectorEmptyButton": "添加连接器", "xpack.triggersActionsUI.sections.actionTypeForm.existingAlertActionTypeEditTitle": "{actionConnectorName}", - "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthPasswordText": "“密码”必填。", "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthUserNameText": "“用户名”必填。", - "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHeaderKeyText": "“键”必填。", - "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHeaderValueText": "“值”必填。", "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText": "“方法”必填", - "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText": "使用用户名时,“密码”必填。", - "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredUserText": "使用密码时,“用户名”必填。", - "xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredAuthPasswordText": "“密码”必填。", - "xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredAuthUserNameText": "“用户名”必填。", - "xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredPasswordText": "使用用户名时,“密码”必填。", - "xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredUserText": "使用密码时,“用户名”必填。", "xpack.triggersActionsUI.sections.addConnectorForm.flyoutTitle": "{actionTypeName} 连接器", "xpack.triggersActionsUI.sections.addConnectorForm.selectConnectorFlyoutTitle": "选择连接器", - "xpack.triggersActionsUI.sections.addConnectorForm.updateErrorNotificationText": "无法创建连接器。", "xpack.triggersActionsUI.sections.addConnectorForm.updateSuccessNotificationText": "已创建“{connectorName}”", "xpack.triggersActionsUI.sections.addModalConnectorForm.cancelButtonLabel": "取消", "xpack.triggersActionsUI.sections.addModalConnectorForm.flyoutTitle": "{actionTypeName} 连接器", "xpack.triggersActionsUI.sections.addModalConnectorForm.saveButtonLabel": "保存", - "xpack.triggersActionsUI.sections.addModalConnectorForm.updateSuccessNotificationText": "已创建“{connectorName}”", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addBccButton": "密送", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.addCcButton": "抄送", "xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.authenticationLabel": "身份验证", @@ -29157,7 +29071,6 @@ "xpack.triggersActionsUI.sections.connectorAddInline.unableToLoadConnectorTitle'": "无法加载连接器", "xpack.triggersActionsUI.sections.connectorAddInline.unauthorizedToCreateForEmptyConnectors": "只有获得授权的用户才能配置连接器。请联系您的管理员。", "xpack.triggersActionsUI.sections.deprecatedTitleMessage": "(已过时)", - "xpack.triggersActionsUI.sections.editConnectorForm.actionTypeDescription": "{actionDescription}", "xpack.triggersActionsUI.sections.editConnectorForm.cancelButtonLabel": "取消", "xpack.triggersActionsUI.sections.editConnectorForm.descriptionText": "此连接器为只读。", "xpack.triggersActionsUI.sections.editConnectorForm.flyoutPreconfiguredTitle": "编辑连接器", diff --git a/x-pack/plugins/triggers_actions_ui/README.md b/x-pack/plugins/triggers_actions_ui/README.md index 7fde1f1512302..b106118ef8d34 100644 --- a/x-pack/plugins/triggers_actions_ui/README.md +++ b/x-pack/plugins/triggers_actions_ui/README.md @@ -13,42 +13,43 @@ As a developer you can reuse and extend built-in alerts and actions UI functiona Table of Contents - [Kibana Alerts and Actions UI](#kibana-alerts-and-actions-ui) - - [Build and register Alert Types](#build-and-register-alert-types) - - [Built-in Alert Types](#built-in-alert-types) - - [Index Threshold Alert](#index-threshold-alert) - - [Alert type model definition](#alert-type-model-definition) - - [Register alert type model](#register-alert-type-model) - - [Create and register new alert type UI example](#create-and-register-new-alert-type-ui-example) - - [Common expression components](#common-expression-components) - - [WHEN expression component](#when-expression-component) - - [OF expression component](#of-expression-component) - - [GROUPED BY expression component](#grouped-by-expression-component) - - [FOR THE LAST expression component](#for-the-last-expression-component) - - [THRESHOLD expression component](#threshold-expression-component) - - [Alert Conditions Components](#alert-conditions-components) - - [Embed the Create Alert flyout within any Kibana plugin](#embed-the-create-alert-flyout-within-any-kibana-plugin) + - [Built-in Alert Types](#built-in-alert-types) + - [Index Threshold Alert](#index-threshold-alert) + - [Alert type model definition](#alert-type-model-definition) + - [Register alert type model](#register-alert-type-model) + - [Create and register new alert type UI example](#create-and-register-new-alert-type-ui-example) + - [Common expression components](#common-expression-components) + - [WHEN expression component](#when-expression-component) + - [OF expression component](#of-expression-component) + - [GROUPED BY expression component](#grouped-by-expression-component) + - [FOR THE LAST expression component](#for-the-last-expression-component) + - [THRESHOLD expression component](#threshold-expression-component) + - [Alert Conditions Components](#alert-conditions-components) + - [The AlertConditions component](#the-alertconditions-component) + - [The AlertConditionsGroup component](#the-alertconditionsgroup-component) + - [Embed the Create Alert flyout within any Kibana plugin](#embed-the-create-alert-flyout-within-any-kibana-plugin) - [Build and register Action Types](#build-and-register-action-types) - - [Built-in Action Types](#built-in-action-types) - - [Server log](#server-log) - - [Email](#email) - - [Slack](#slack) - - [Index](#index) - - [Webhook](#webhook) - - [PagerDuty](#pagerduty) - - [Action type model definition](#action-type-model-definition) - - [Register action type model](#register-action-type-model) - - [Create and register new action type UI example](#reate-and-register-new-action-type-ui-example) - - [Embed the Alert Actions form within any Kibana plugin](#embed-the-alert-actions-form-within-any-kibana-plugin) - - [Embed the Create Connector flyout within any Kibana plugin](#embed-the-create-connector-flyout-within-any-kibana-plugin) - - [Embed the Edit Connector flyout within any Kibana plugin](#embed-the-edit-connector-flyout-within-any-kibana-plugin) + - [Server log](#server-log) + - [Email](#email) + - [Slack](#slack) + - [Index](#index) + - [Webhook](#webhook) + - [PagerDuty](#pagerduty) + - [Action type model definition](#action-type-model-definition) + - [CustomConnectorSelectionItem Properties](#customconnectorselectionitem-properties) + - [Register action type model](#register-action-type-model) + - [Create and register new action type UI](#create-and-register-new-action-type-ui) + - [Embed the Alert Actions form within any Kibana plugin](#embed-the-alert-actions-form-within-any-kibana-plugin) + - [Embed the Create Connector flyout within any Kibana plugin](#embed-the-create-connector-flyout-within-any-kibana-plugin) + - [Embed the Edit Connector flyout within any Kibana plugin](#embed-the-edit-connector-flyout-within-any-kibana-plugin) ## Built-in Alert Types Kibana ships with several built-in alert types: -|Type|Id|Description| -|---|---|---| -|[Index Threshold](#index-threshold-alert)|`threshold`|Index Threshold Alert| +| Type | Id | Description | +| ----------------------------------------- | ----------- | --------------------- | +| [Index Threshold](#index-threshold-alert) | `threshold` | Index Threshold Alert | Every alert type must be registered server side, and can optionally be registered client side. Only alert types registered on both client and server will be displayed in the Create Alert flyout, as a part of the UI. @@ -89,12 +90,12 @@ interface IndexThresholdProps { } ``` -|Property|Description| -|---|---| -|ruleParams|Set of Alert params relevant for the index threshold alert type.| -|setRuleParams|Alert reducer method, which is used to create a new copy of alert object with the changed params property any subproperty value.| -|setRuleProperty|Alert reducer method, which is used to create a new copy of alert object with the changed any direct alert property value.| -|errors|Alert level errors tracking object.| +| Property | Description | +| --------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| ruleParams | Set of Alert params relevant for the index threshold alert type. | +| setRuleParams | Alert reducer method, which is used to create a new copy of alert object with the changed params property any subproperty value. | +| setRuleProperty | Alert reducer method, which is used to create a new copy of alert object with the changed any direct alert property value. | +| errors | Alert level errors tracking object. | Alert reducer is defined on the AlertAdd functional component level and passed down to the subcomponents to provide a new state of Alert object: @@ -245,16 +246,16 @@ Each alert type should be defined as `RuleTypeModel` object with the these prope >; defaultActionMessage?: string; ``` -|Property|Description| -|---|---| -|id|Alert type id. Should be the same as on the server side.| -|name|Name of the alert type that will be displayed on the select card in the UI.| -|iconClass|Icon of the alert type that will be displayed on the select card in the UI.| -|validate|Validation function for the alert params.| -|ruleParamsExpression| A lazy loaded React component for building UI of the current alert type params.| -|defaultActionMessage|Optional property for providing default messages for all added actions, excluding the Recovery action group, with `message` property. | -|defaultRecoveryMessage|Optional property for providing a default message for all added actions with `message` property for the Recovery action group.| -|requiresAppContext|Define if alert type is enabled for create and edit in the alerting management UI.| +| Property | Description | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| id | Alert type id. Should be the same as on the server side. | +| name | Name of the alert type that will be displayed on the select card in the UI. | +| iconClass | Icon of the alert type that will be displayed on the select card in the UI. | +| validate | Validation function for the alert params. | +| ruleParamsExpression | A lazy loaded React component for building UI of the current alert type params. | +| defaultActionMessage | Optional property for providing default messages for all added actions, excluding the Recovery action group, with `message` property. | +| defaultRecoveryMessage | Optional property for providing a default message for all added actions with `message` property for the Recovery action group. | +| requiresAppContext | Define if alert type is enabled for create and edit in the alerting management UI. | IMPORTANT: The current UI supports a single action group only. Action groups are mapped from the server API result for [GET /api/alerts/list_alert_types: List alert types](https://github.com/elastic/kibana/tree/main/x-pack/plugins/alerting#get-apialerttypes-list-alert-types). @@ -445,12 +446,12 @@ interface WhenExpressionProps { } ``` -|Property|Description| -|---|---| -|aggType|Selected aggregation type that will be set as the alert type property.| -|customAggTypesOptions|(Optional) List of aggregation types that replaces the default options defined in constants `x-pack/plugins/triggers_actions_ui/public/common/constants/aggregation_types.ts`.| -|onChangeSelectedAggType|event handler that will be executed when selected aggregation type is changed.| -|popupPosition|(Optional) expression popup position. Default is `downLeft`. Recommend changing it for a small parent window space.| +| Property | Description | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| aggType | Selected aggregation type that will be set as the alert type property. | +| customAggTypesOptions | (Optional) List of aggregation types that replaces the default options defined in constants `x-pack/plugins/triggers_actions_ui/public/common/constants/aggregation_types.ts`. | +| onChangeSelectedAggType | event handler that will be executed when selected aggregation type is changed. | +| popupPosition | (Optional) expression popup position. Default is `downLeft`. Recommend changing it for a small parent window space. | ### OF expression component @@ -486,15 +487,15 @@ interface OfExpressionProps { } ``` -|Property|Description| -|---|---| -|aggType|Selected aggregation type that will be set as the alert type property.| -|aggField|Selected aggregation field that will be set as the alert type property.| -|errors|List of errors with proper messages for the alert params that should be validated. In current component is validated `aggField`.| -|onChangeSelectedAggField|Event handler that will be excuted if selected aggregation field is changed.| -|fields|Fields list that will be available in the OF `Select a field` dropdown.| -|customAggTypesOptions|(Optional) List of aggregation types that replaces the default options defined in constants `x-pack/plugins/triggers_actions_ui/public/common/constants/aggregation_types.ts`.| -|popupPosition|(Optional) expression popup position. Default is `downRight`. Recommend changing it for a small parent window space.| +| Property | Description | +| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| aggType | Selected aggregation type that will be set as the alert type property. | +| aggField | Selected aggregation field that will be set as the alert type property. | +| errors | List of errors with proper messages for the alert params that should be validated. In current component is validated `aggField`. | +| onChangeSelectedAggField | Event handler that will be excuted if selected aggregation field is changed. | +| fields | Fields list that will be available in the OF `Select a field` dropdown. | +| customAggTypesOptions | (Optional) List of aggregation types that replaces the default options defined in constants `x-pack/plugins/triggers_actions_ui/public/common/constants/aggregation_types.ts`. | +| popupPosition | (Optional) expression popup position. Default is `downRight`. Recommend changing it for a small parent window space. | ### GROUPED BY expression component @@ -536,18 +537,18 @@ interface GroupByExpressionProps { } ``` -|Property|Description| -|---|---| -|groupBy|Selected group by type that will be set as the alert type property.| -|termSize|Selected term size that will be set as the alert type property.| -|termField|Selected term field that will be set as the alert type property.| -|errors|List of errors with proper messages for the alert params that should be validated. In current component is validated `termSize` and `termField`.| -|onChangeSelectedTermSize|Event handler that will be excuted if selected term size is changed.| -|onChangeSelectedTermField|Event handler that will be excuted if selected term field is changed.| -|onChangeSelectedGroupBy|Event handler that will be excuted if selected group by is changed.| -|fields|Fields list with options for the `termField` dropdown.| -|customGroupByTypes|(Optional) List of group by types that replaces the default options defined in constants `x-pack/plugins/triggers_actions_ui/public/common/constants/group_by_types.ts`.| -|popupPosition|(Optional) expression popup position. Default is `downLeft`. Recommend changing it for a small parent window space.| +| Property | Description | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| groupBy | Selected group by type that will be set as the alert type property. | +| termSize | Selected term size that will be set as the alert type property. | +| termField | Selected term field that will be set as the alert type property. | +| errors | List of errors with proper messages for the alert params that should be validated. In current component is validated `termSize` and `termField`. | +| onChangeSelectedTermSize | Event handler that will be excuted if selected term size is changed. | +| onChangeSelectedTermField | Event handler that will be excuted if selected term field is changed. | +| onChangeSelectedGroupBy | Event handler that will be excuted if selected group by is changed. | +| fields | Fields list with options for the `termField` dropdown. | +| customGroupByTypes | (Optional) List of group by types that replaces the default options defined in constants `x-pack/plugins/triggers_actions_ui/public/common/constants/group_by_types.ts`. | +| popupPosition | (Optional) expression popup position. Default is `downLeft`. Recommend changing it for a small parent window space. | ### FOR THE LAST expression component @@ -580,14 +581,14 @@ interface ForLastExpressionProps { } ``` -|Property|Description| -|---|---| -|timeWindowSize|Selected time window size that will be set as the alert type property.| -|timeWindowUnit|Selected time window unit that will be set as the alert type property.| -|errors|List of errors with proper messages for the alert params that should be validated. In current component is validated `termWindowSize`.| -|onChangeWindowSize|Event handler that will be excuted if selected window size is changed.| -|onChangeWindowUnit|Event handler that will be excuted if selected window unit is changed.| -|popupPosition|(Optional) expression popup position. Default is `downLeft`. Recommend changing it for a small parent window space.| +| Property | Description | +| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | +| timeWindowSize | Selected time window size that will be set as the alert type property. | +| timeWindowUnit | Selected time window unit that will be set as the alert type property. | +| errors | List of errors with proper messages for the alert params that should be validated. In current component is validated `termWindowSize`. | +| onChangeWindowSize | Event handler that will be excuted if selected window size is changed. | +| onChangeWindowUnit | Event handler that will be excuted if selected window unit is changed. | +| popupPosition | (Optional) expression popup position. Default is `downLeft`. Recommend changing it for a small parent window space. | ### THRESHOLD expression component @@ -623,15 +624,15 @@ interface ThresholdExpressionProps { } ``` -|Property|Description| -|---|---| -|thresholdComparator|Selected time window size that will be set as the alert type property.| -|threshold|Selected time window size that will be set as the alert type property.| -|errors|List of errors with proper messages for the alert params that should be validated. In current component is validated `threshold0` and `threshold1`.| -|onChangeSelectedThresholdComparator|Event handler that will be excuted if selected threshold comparator is changed.| -|onChangeSelectedThreshold|Event handler that will be excuted if selected threshold is changed.| -|customComparators|(Optional) List of comparators that replaces the default options defined in constants `x-pack/plugins/triggers_actions_ui/public/common/constants/comparators.ts`.| -|popupPosition|(Optional) expression popup position. Default is `downLeft`. Recommend changing it for a small parent window space.| +| Property | Description | +| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| thresholdComparator | Selected time window size that will be set as the alert type property. | +| threshold | Selected time window size that will be set as the alert type property. | +| errors | List of errors with proper messages for the alert params that should be validated. In current component is validated `threshold0` and `threshold1`. | +| onChangeSelectedThresholdComparator | Event handler that will be excuted if selected threshold comparator is changed. | +| onChangeSelectedThreshold | Event handler that will be excuted if selected threshold is changed. | +| customComparators | (Optional) List of comparators that replaces the default options defined in constants `x-pack/plugins/triggers_actions_ui/public/common/constants/comparators.ts`. | +| popupPosition | (Optional) expression popup position. Default is `downLeft`. Recommend changing it for a small parent window space. | ## Alert Conditions Components To aid in creating a uniform UX across Alert Types, we provide two components for specifying the conditions for detection of a certain alert under within any specific Action Groups: @@ -767,19 +768,19 @@ const DEFAULT_THRESHOLDS: ThresholdAlertTypeParams['threshold] = { This component will render the `Conditions` header & headline, along with the selectors for adding every Action Group you specity. Additionally it will clone its `children` for _each_ action group which has a `condition` specified for it, passing in the appropriate `actionGroup` prop for each one. -|Property|Description| -|---|---| -|headline|The headline title displayed above the fields | -|actionGroups|A list of `ActionGroupWithCondition` which includes all the action group you wish to offer the user and what conditions they are already configured to follow| -|onInitializeConditionsFor|A callback which is called when the user ask for a certain actionGroup to be initialized with an initial default condition. If you have no specific default, that's fine, as the component will render the action group's field even if the condition is empty (using a `null` or an `undefined`) and determines whether to render these fields by _the very presence_ of a `condition` field| +| Property | Description | +| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| headline | The headline title displayed above the fields | +| actionGroups | A list of `ActionGroupWithCondition` which includes all the action group you wish to offer the user and what conditions they are already configured to follow | +| onInitializeConditionsFor | A callback which is called when the user ask for a certain actionGroup to be initialized with an initial default condition. If you have no specific default, that's fine, as the component will render the action group's field even if the condition is empty (using a `null` or an `undefined`) and determines whether to render these fields by _the very presence_ of a `condition` field | ### The AlertConditionsGroup component This component renders a standard EuiTitle foe each action group, wrapping the Alert Type specific component, in addition to a "reset" button which allows the user to reset the condition for that action group. The definition of what a _reset_ actually means is Alert Type specific, and up to the implementor to decide. In some case it might mean removing the condition, in others it might mean to reset it to some default value on the server side. In either case, it should _delete_ the `condition` field from the appropriate `actionGroup` as per the above example. -|Property|Description| -|---|---| -|onResetConditionsFor|A callback which is called when the user clicks the _reset_ button besides the action group's title. The implementor should use this to remove the `condition` from the specified actionGroup| +| Property | Description | +| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| onResetConditionsFor | A callback which is called when the user clicks the _reset_ button besides the action group's title. The implementor should use this to remove the `condition` from the specified actionGroup | ## Embed the Create Alert flyout within any Kibana plugin @@ -839,29 +840,29 @@ interface AlertAddProps { } ``` -|Property|Description| -|---|---| -|consumer|Name of the plugin that creates an alert.| -|addFlyoutVisible|Visibility state of the Create Alert flyout.| -|setAddFlyoutVisibility|Function for changing visibility state of the Create Alert flyout.| -|alertTypeId|Optional property to preselect alert type.| -|canChangeTrigger|Optional property, that hides change alert type possibility.| -|onSave|Optional function, which will be executed if alert was saved sucsessfuly.| -|initialValues|Default values for Alert properties.| -|metadata|Optional generic property, which allows to define component specific metadata. This metadata can be used for passing down preloaded data for Alert type expression component.| +| Property | Description | +| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| consumer | Name of the plugin that creates an alert. | +| addFlyoutVisible | Visibility state of the Create Alert flyout. | +| setAddFlyoutVisibility | Function for changing visibility state of the Create Alert flyout. | +| alertTypeId | Optional property to preselect alert type. | +| canChangeTrigger | Optional property, that hides change alert type possibility. | +| onSave | Optional function, which will be executed if alert was saved sucsessfuly. | +| initialValues | Default values for Alert properties. | +| metadata | Optional generic property, which allows to define component specific metadata. This metadata can be used for passing down preloaded data for Alert type expression component. | ## Build and register Action Types Kibana ships with a set of built-in action types UI: -|Type|Id|Description| -|---|---|---| -|[Server log](#server-log)|`.log`|Logs messages to the Kibana log using `server.log()`| -|[Email](#email)|`.email`|Sends an email using SMTP| -|[Slack](#slack)|`.slack`|Posts a message to a Slack channel| -|[Index](#index)|`.index`|Indexes document(s) into Elasticsearch| -|[Webhook](#webhook)|`.webhook`|Sends a payload to a web service using HTTP POST or PUT| -|[PagerDuty](#pagerduty)|`.pagerduty`|Triggers, resolves, or acknowledges an incident to a PagerDuty service| +| Type | Id | Description | +| ------------------------- | ------------ | ---------------------------------------------------------------------- | +| [Server log](#server-log) | `.log` | Logs messages to the Kibana log using `server.log()` | +| [Email](#email) | `.email` | Sends an email using SMTP | +| [Slack](#slack) | `.slack` | Posts a message to a Slack channel | +| [Index](#index) | `.index` | Indexes document(s) into Elasticsearch | +| [Webhook](#webhook) | `.webhook` | Sends a payload to a web service using HTTP POST or PUT | +| [PagerDuty](#pagerduty) | `.pagerduty` | Triggers, resolves, or acknowledges an incident to a PagerDuty service | Every action type should be registered server side, and can be optionally registered client side. Only action types registered on both client and server will be displayed in the Alerts and Actions UI. @@ -889,9 +890,6 @@ export function getActionType(): ActionTypeModel { defaultMessage: 'Send to Server log', } ), - validateConnector: (): Promise => { - return { errors: {} }; - }, validateParams: (actionParams: ServerLogActionParams): Promise => { // validation of action params implementation }, @@ -930,9 +928,6 @@ export function getActionType(): ActionTypeModel { defaultMessage: 'Send to email', } ), - validateConnector: (action: EmailActionConnector): Promise => { - // validation of connector properties implementation - }, validateParams: (actionParams: EmailActionParams): Promise => { // validation of action params implementation }, @@ -968,9 +963,6 @@ export function getActionType(): ActionTypeModel { defaultMessage: 'Send to Slack', } ), - validateConnector: (action: SlackActionConnector): Promise => { - // validation of connector properties implementation - }, validateParams: (actionParams: SlackActionParams): Promise => { // validation of action params implementation }, @@ -1001,9 +993,6 @@ export function getActionType(): ActionTypeModel { defaultMessage: 'Index data into Elasticsearch.', } ), - validateConnector: (): Promise => { - return { errors: {} }; - }, actionConnectorFields: IndexActionConnectorFields, actionParamsFields: IndexParamsFields, validateParams: (): Promise => { @@ -1047,9 +1036,6 @@ export function getActionType(): ActionTypeModel { defaultMessage: 'Send a request to a web service.', } ), - validateConnector: (action: WebhookActionConnector): Promise => { - // validation of connector properties implementation - }, validateParams: (actionParams: WebhookActionParams): Promise => { // validation of action params implementation }, @@ -1087,9 +1073,6 @@ export function getActionType(): ActionTypeModel { defaultMessage: 'Send to PagerDuty', } ), - validateConnector: (action: PagerDutyActionConnector): Promise => { - // validation of connector properties implementation - }, validateParams: (actionParams: PagerDutyActionParams): Promise => { // validation of action params implementation }, @@ -1114,22 +1097,20 @@ Each action type should be defined as an `ActionTypeModel` object with the follo iconClass: IconType; selectMessage: string; actionTypeTitle?: string; - validateConnector: (connector: any) => Promise; validateParams: (actionParams: any) => Promise; actionConnectorFields: React.FunctionComponent | null; actionParamsFields: React.LazyExoticComponent>>; customConnectorSelectItem?: CustomConnectorSelectionItem; ``` -|Property|Description| -|---|---| -|id|Action type id. Should be the same as on server side.| -|iconClass|Setting for icon to be displayed to the user. EUI supports any known EUI icon, SVG URL, or a lazy loaded React component, ReactElement.| -|selectMessage|Short description of action type responsibility, that will be displayed on the select card in UI.| -|validateConnector|Validation function for action connector.| -|validateParams|Validation function for action params.| -|actionConnectorFields|A lazy loaded React component for building UI of current action type connector.| -|actionParamsFields|A lazy loaded React component for building UI of current action type params. Displayed as a part of Create Alert flyout.| -|customConnectorSelectItem|Optional, an object for customizing the selection row of the action connector form.| +| Property | Description | +| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| id | Action type id. Should be the same as on server side. | +| iconClass | Setting for icon to be displayed to the user. EUI supports any known EUI icon, SVG URL, or a lazy loaded React component, ReactElement. | +| selectMessage | Short description of action type responsibility, that will be displayed on the select card in UI. | +| validateParams | Validation function for action params. | +| actionConnectorFields | A lazy loaded React component for building UI of current action type connector. | +| actionParamsFields | A lazy loaded React component for building UI of current action type params. Displayed as a part of Create Alert flyout. | +| customConnectorSelectItem | Optional, an object for customizing the selection row of the action connector form. | ### CustomConnectorSelectionItem Properties @@ -1139,10 +1120,10 @@ Each action type should be defined as an `ActionTypeModel` object with the follo LazyExoticComponent | undefined; ``` -|Property|Description| -|---|---| -|getText|Function for returning the text to display for the row.| -|getComponent|Function for returning a lazy loaded React component for customizing the selection row of the action connector form. Or undefined if if no customization is needed.| +| Property | Description | +| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| getText | Function for returning the text to display for the row. | +| getComponent | Function for returning a lazy loaded React component for customizing the selection row of the action connector form. Or undefined if if no customization is needed. | ## Register action type model @@ -1169,6 +1150,8 @@ Before starting the UI implementation, the [server side registration](https://gi Action type UI is expected to be defined as `ActionTypeModel` object. +The framework uses the [Form lib](https://github.com/elastic/kibana/blob/main/src/plugins/es_ui_shared/static/forms/docs/welcome.mdx). Please refer to the documentation of the library to learn more. + Below is a list of steps that should be done to build and register a new action type with the name `Example Action Type`: 1. At any suitable place in Kibana, create a file, which will expose an object implementing interface [ActionTypeModel]: @@ -1202,24 +1185,6 @@ export function getActionType(): ActionTypeModel { defaultMessage: 'Example Action', } ), - validateConnector: (action: ExampleActionConnector): Promise => { - const validationResult = { errors: {} }; - const errors = { - someConnectorField: new Array(), - }; - validationResult.errors = errors; - if (!action.config.someConnectorField) { - errors.someConnectorField.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSomeConnectorFieldeText', - { - defaultMessage: 'SomeConnectorField is required.', - } - ) - ); - } - return validationResult; - }, validateParams: (actionParams: ExampleActionParams): Promise => { const validationResult = { errors: {} }; const errors = { @@ -1248,42 +1213,39 @@ export function getActionType(): ActionTypeModel { ``` import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiFieldText } from '@elastic/eui'; -import { EuiTextArea } from '@elastic/eui'; -import { - ActionTypeModel, - ValidationResult, - ActionConnectorFieldsProps, - ActionParamsProps, -} from '../../../types'; +import { FieldConfig, UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; +import { TextField } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import { ActionConnectorFieldsProps } from '../../../types'; -interface ExampleActionConnector { - config: { - someConnectorField: string; - }; -} +const { emptyField } = fieldValidators; -const ExampleConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, errors }) => { - const { someConnectorField } = action.config; - return ( - <> - 0 && someConnectorField !== undefined} - name="someConnectorField" - value={someConnectorField || ''} - onChange={e => { - editActionConfig('someConnectorField', e.target.value); - }} - onBlur={() => { - if (!someConnectorField) { - editActionConfig('someConnectorField', ''); +const fieldConfig: FieldConfig = { + label: 'My field', + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.triggersActionsUI.sections.actionConnectorForm.error.requiredField', + { + defaultMessage: 'Field is required.', } + ) + ), + }, + ], +}; + +const ExampleConnectorFields: React.FunctionComponent = ({ isEdit, readOnly, registerPreSubmitValidator }) => { + return ( + - ); }; @@ -1466,35 +1428,35 @@ interface ActionAccordionFormProps { ``` -|Property|Description| -|---|---| -|actions|List of actions comes from alert.actions property.| -|defaultActionGroupId|Default action group id to which each new action will belong by default.| -|actionGroups|Optional. List of action groups to which new action can be assigned. The RunWhen field is only displayed when these action groups are specified| -|setActionIdByIndex|Function for changing action 'id' by the proper index in alert.actions array.| -|setActionGroupIdByIndex|Function for changing action 'group' by the proper index in alert.actions array.| -|setRuleProperty|Function for changing alert property 'actions'. Used when deleting action from the array to reset it.| -|setActionParamsProperty|Function for changing action key/value property by index in alert.actions array.| -|http|HttpSetup needed for executing API calls.| -|actionTypeRegistry|Registry for action types.| -|toastNotifications|Toast messages Plugin Setup Contract.| -|docLinks|Documentation links Plugin Start Contract.| -|actionTypes|Optional property, which allows to define a list of available actions specific for a current plugin.| -|messageVariables|Optional property, which allows to define a list of variables for action 'message' property. Set `useWithTripleBracesInTemplates` to true if you don't want the variable escaped when rendering.| -|defaultActionMessage|Optional property, which allows to define a message value for action with 'message' property.| -|capabilities|Kibana core's Capabilities ApplicationStart['capabilities'].| - -|Property|Description| -|---|---| -|onSave|Optional function, which will be executed if alert was saved sucsessfuly.| -|http|HttpSetup needed for executing API calls.| -|ruleTypeRegistry|Registry for alert types.| -|actionTypeRegistry|Registry for action types.| -|uiSettings|Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring.| -|docLinks|Documentation Links, needed to link to the documentation from informational callouts.| -|toastNotifications|Toast messages.| -|charts|Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring.| -|dataFieldsFormats|Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring.| +| Property | Description | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| actions | List of actions comes from alert.actions property. | +| defaultActionGroupId | Default action group id to which each new action will belong by default. | +| actionGroups | Optional. List of action groups to which new action can be assigned. The RunWhen field is only displayed when these action groups are specified | +| setActionIdByIndex | Function for changing action 'id' by the proper index in alert.actions array. | +| setActionGroupIdByIndex | Function for changing action 'group' by the proper index in alert.actions array. | +| setRuleProperty | Function for changing alert property 'actions'. Used when deleting action from the array to reset it. | +| setActionParamsProperty | Function for changing action key/value property by index in alert.actions array. | +| http | HttpSetup needed for executing API calls. | +| actionTypeRegistry | Registry for action types. | +| toastNotifications | Toast messages Plugin Setup Contract. | +| docLinks | Documentation links Plugin Start Contract. | +| actionTypes | Optional property, which allows to define a list of available actions specific for a current plugin. | +| messageVariables | Optional property, which allows to define a list of variables for action 'message' property. Set `useWithTripleBracesInTemplates` to true if you don't want the variable escaped when rendering. | +| defaultActionMessage | Optional property, which allows to define a message value for action with 'message' property. | +| capabilities | Kibana core's Capabilities ApplicationStart['capabilities']. | + +| Property | Description | +| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | +| onSave | Optional function, which will be executed if alert was saved sucsessfuly. | +| http | HttpSetup needed for executing API calls. | +| ruleTypeRegistry | Registry for alert types. | +| actionTypeRegistry | Registry for action types. | +| uiSettings | Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring. | +| docLinks | Documentation Links, needed to link to the documentation from informational callouts. | +| toastNotifications | Toast messages. | +| charts | Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring. | +| dataFieldsFormats | Optional property, which is needed to display visualization of alert type expression. Will be changed after visualization refactoring. | ## Embed the Create Connector flyout within any Kibana plugin @@ -1517,10 +1479,11 @@ Then this dependency will be used to embed Create Connector flyout or register n 2. Add Create Connector flyout to React component: ``` // import section -import { ActionsConnectorsContextProvider, ConnectorAddFlyout } from '../../../../../../../triggers_actions_ui/public'; +import { ActionsConnectorsContextProvider, CreateConnectorFlyout } from '../../../../../../../triggers_actions_ui/public'; // in the component state definition section const [addFlyoutVisible, setAddFlyoutVisibility] = useState(false); +const onClose = useCallback(() => setAddFlyoutVisibility(false), []); // load required dependancied const { http, triggersActionsUi, notifications, application, docLinks } = useKibana().services; @@ -1549,35 +1512,38 @@ const connector = { // in render section of component - ``` -ConnectorAddFlyout Props definition: +CreateConnectorFlyout Props definition: ``` export interface ConnectorAddFlyoutProps { - addFlyoutVisible: boolean; - setAddFlyoutVisibility: React.Dispatch>; - actionTypes?: ActionType[]; + actionTypeRegistry: ActionTypeRegistryContract; + onClose: () => void; + supportedActionTypes?: ActionType[]; + onConnectorCreated?: (connector: ActionConnector) => void; + onTestConnector?: (connector: ActionConnector) => void; } ``` -|Property|Description| -|---|---| -|addFlyoutVisible|Visibility state of the Create Connector flyout.| -|setAddFlyoutVisibility|Function for changing visibility state of the Create Connector flyout.| -|actionTypes|Optional property, that allows to define only specific action types list which is available for a current plugin.| +| Property | Description | +| -------------------- | ----------------------------------------------------------------------------------------------------------------- | +| actionTypeRegistry | The action type registry. | +| onClose | Called when closing the flyout | +| supportedActionTypes | Optional property, that allows to define only specific action types list which is available for a current plugin. | +| onConnectorCreated | Optional property. Function to be called after the creation of the connector. | +| onTestConnector | Optional property. Function to be called when the user press the Save & Test button. | ## Embed the Edit Connector flyout within any Kibana plugin @@ -1600,7 +1566,7 @@ Then this dependency will be used to embed Edit Connector flyout. 2. Add Create Connector flyout to React component: ``` // import section -import { ActionsConnectorsContextProvider, ConnectorEditFlyout } from '../../../../../../../triggers_actions_ui/public'; +import { ActionsConnectorsContextProvider, EditConnectorFlyout } from '../../../../../../../triggers_actions_ui/public'; // in the component state definition section const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); @@ -1622,31 +1588,32 @@ const { http, triggersActionsUi, notifications, application } = useKibana().serv // in render section of component - ``` -ConnectorEditFlyout Props definition: +EditConnectorFlyout Props definition: ``` export interface ConnectorEditProps { - initialConnector: ActionConnector; + actionTypeRegistry: ActionTypeRegistryContract; + connector: ActionConnector; onClose: () => void; - tab?: EditConectorTabs; - reloadConnectors?: () => Promise; - consumer?: string; + tab?: EditConnectorTabs; + onConnectorUpdated?: (connector: ActionConnector) => void; } ``` -|Property|Description| -|---|---| -|initialConnector|Property, that allows to define the initial state of edited connector.| -|editFlyoutVisible|Visibility state of the Edit Connector flyout.| -|setEditFlyoutVisibility|Function for changing visibility state of the Edit Connector flyout.| +| Property | Description | +| ------------------ | --------------------------------------------------------------------------- | +| actionTypeRegistry | The action type registry. | +| connector | Property, that allows to define the initial state of edited connector. | +| onClose | Called when closing the flyout | +| onConnectorUpdated | Optional property. Function to be called after the update of the connector. | ActionsConnectorsContextValue options: ``` @@ -1662,10 +1629,10 @@ export interface ActionsConnectorsContextValue { } ``` -|Property|Description| -|---|---| -|http|HttpSetup needed for executing API calls.| -|actionTypeRegistry|Registry for action types.| -|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.| +| Property | Description | +| ------------------ | ------------------------------------------------------------------------------------------------------------- | +| http | HttpSetup needed for executing API calls. | +| actionTypeRegistry | Registry for action types. | +| 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. | diff --git a/x-pack/plugins/triggers_actions_ui/public/application/action_type_registry.mock.ts b/x-pack/plugins/triggers_actions_ui/public/application/action_type_registry.mock.ts index 3c5c1b551028e..8260f208f353d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/action_type_registry.mock.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/action_type_registry.mock.ts @@ -31,7 +31,6 @@ const createMockActionTypeModel = (actionType: Partial = {}): A id, iconClass: `iconClass-${id}`, selectMessage: `selectMessage-${id}`, - validateConnector: jest.fn(), validateParams: jest.fn(), actionConnectorFields: null, actionParamsFields: mockedActionParamsFields, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx index c4c273bd003c5..d47529c47c19d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx @@ -23,6 +23,7 @@ import type { SpacesPluginStart } from '@kbn/spaces-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; +import { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public'; import { suspendedComponentWithProps } from './lib/suspended_component_with_props'; import { ActionTypeRegistryContract, @@ -32,7 +33,8 @@ import { import { Section, routeToRuleDetails, legacyRouteToRuleDetails } from './constants'; import { setDataViewsService } from '../common/lib/data_apis'; -import { KibanaContextProvider } from '../common/lib/kibana'; +import { KibanaContextProvider, useKibana } from '../common/lib/kibana'; +import { ConnectorProvider } from './context/connector_context'; const TriggersActionsUIHome = lazy(() => import('./home')); const RuleDetailsRoute = lazy( @@ -40,6 +42,7 @@ const RuleDetailsRoute = lazy( ); export interface TriggersAndActionsUiServices extends CoreStart { + actions: ActionsPublicPluginSetup; data: DataPublicPluginStart; dataViews: DataViewsPublicPluginStart; charts: ChartsPluginStart; @@ -89,23 +92,29 @@ export const App = ({ deps }: { deps: TriggersAndActionsUiServices }) => { }; export const AppWithoutRouter = ({ sectionsRegex }: { sectionsRegex: string }) => { + const { + actions: { validateEmailAddresses }, + } = useKibana().services; + return ( - - - - } - /> - - - + + + + + } + /> + + + + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx index 31ff85a20ed3b..9e61d84f5fb31 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.test.tsx @@ -8,7 +8,6 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel } from '../../../../types'; -import { EmailActionConnector } from '../types'; import { getEmailServices } from './email'; import { ValidatedEmail, @@ -73,301 +72,6 @@ describe('getEmailServices', () => { }); }); -describe('connector validation', () => { - test('connector validation succeeds when connector config is valid', async () => { - const actionConnector = { - secrets: { - user: 'user', - password: 'pass', - clientSecret: null, - }, - id: 'test', - actionTypeId: '.email', - name: 'email', - isPreconfigured: false, - isDeprecated: false, - config: { - from: 'test@test.com', - port: 2323, - host: 'localhost', - test: 'test', - hasAuth: true, - service: 'other', - }, - } as EmailActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - from: [], - port: [], - host: [], - service: [], - clientId: [], - tenantId: [], - }, - }, - secrets: { - errors: { - user: [], - password: [], - clientSecret: [], - }, - }, - }); - }); - - test('connector validation succeeds when connector config is valid with empty user/password', async () => { - const actionConnector = { - secrets: { - user: null, - password: null, - clientSecret: null, - }, - id: 'test', - actionTypeId: '.email', - isPreconfigured: false, - isDeprecated: false, - name: 'email', - config: { - from: 'test@test.com', - port: 2323, - host: 'localhost', - test: 'test', - hasAuth: false, - service: 'other', - }, - } as EmailActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - from: [], - port: [], - host: [], - service: [], - clientId: [], - tenantId: [], - }, - }, - secrets: { - errors: { - user: [], - password: [], - clientSecret: [], - }, - }, - }); - }); - test('connector validation fails when connector config is not valid', async () => { - const actionConnector = { - secrets: { - user: 'user', - password: 'pass', - }, - id: 'test', - actionTypeId: '.email', - name: 'email', - config: { - from: 'test@notallowed.com', - hasAuth: true, - service: 'other', - }, - } as EmailActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - from: ['Email address test@notallowed.com is not allowed.'], - port: ['Port is required.'], - host: ['Host is required.'], - service: [], - clientId: [], - tenantId: [], - }, - }, - secrets: { - errors: { - user: [], - password: [], - clientSecret: [], - }, - }, - }); - - // also check that mustache is not valid - actionConnector.config.from = '{{mustached}}'; - const validation = await actionTypeModel.validateConnector(actionConnector); - expect(validation?.config?.errors?.from).toEqual(['Email address {{mustached}} is not valid.']); - }); - - test('connector validation fails when user specified but not password', async () => { - const actionConnector = { - secrets: { - user: 'user', - password: null, - clientSecret: null, - }, - id: 'test', - actionTypeId: '.email', - isPreconfigured: false, - isDeprecated: false, - name: 'email', - config: { - from: 'test@test.com', - port: 2323, - host: 'localhost', - test: 'test', - hasAuth: true, - service: 'other', - }, - } as EmailActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - from: [], - port: [], - host: [], - service: [], - clientId: [], - tenantId: [], - }, - }, - secrets: { - errors: { - user: [], - password: ['Password is required when username is used.'], - clientSecret: [], - }, - }, - }); - }); - test('connector validation fails when password specified but not user', async () => { - const actionConnector = { - secrets: { - user: null, - password: 'password', - clientSecret: null, - }, - id: 'test', - actionTypeId: '.email', - isPreconfigured: false, - isDeprecated: false, - name: 'email', - config: { - from: 'test@test.com', - port: 2323, - host: 'localhost', - test: 'test', - hasAuth: true, - service: 'other', - }, - } as EmailActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - from: [], - port: [], - host: [], - service: [], - clientId: [], - tenantId: [], - }, - }, - secrets: { - errors: { - user: ['Username is required when password is used.'], - password: [], - clientSecret: [], - }, - }, - }); - }); - test('connector validation fails when server type is not selected', async () => { - const actionConnector = { - secrets: { - user: 'user', - password: 'password', - }, - id: 'test', - actionTypeId: '.email', - isPreconfigured: false, - isDeprecated: false, - name: 'email', - config: { - from: 'test@test.com', - port: 2323, - host: 'localhost', - test: 'test', - hasAuth: true, - }, - }; - - expect( - await actionTypeModel.validateConnector(actionConnector as unknown as EmailActionConnector) - ).toEqual({ - config: { - errors: { - from: [], - port: [], - host: [], - service: ['Service is required.'], - clientId: [], - tenantId: [], - }, - }, - secrets: { - errors: { - user: [], - password: [], - clientSecret: [], - }, - }, - }); - }); - test('connector validation fails when for exchange service selected, but clientId, tenantId and clientSecrets were not defined', async () => { - const actionConnector = { - secrets: { - user: 'user', - password: 'pass', - clientSecret: null, - }, - id: 'test', - actionTypeId: '.email', - name: 'email', - isPreconfigured: false, - isDeprecated: false, - config: { - from: 'test@test.com', - hasAuth: true, - service: 'exchange_server', - }, - } as EmailActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - from: [], - port: [], - host: [], - service: [], - clientId: ['Client ID is required.'], - tenantId: ['Tenant ID is required.'], - }, - }, - secrets: { - errors: { - clientSecret: ['Client Secret is required.'], - password: [], - user: [], - }, - }, - }); - }); -}); - describe('action params validation', () => { test('action params validation succeeds when action params is valid', async () => { const actionParams = { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx index 0add2396a74d0..b44d13fb02ec1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email.tsx @@ -9,13 +9,9 @@ import { uniq } from 'lodash'; import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSelectOption } from '@elastic/eui'; -import { AdditionalEmailServices, InvalidEmailReason } from '@kbn/actions-plugin/common'; -import { - ActionTypeModel, - ConnectorValidationResult, - GenericValidationResult, -} from '../../../../types'; -import { EmailActionParams, EmailConfig, EmailSecrets, EmailActionConnector } from '../types'; +import { InvalidEmailReason } from '@kbn/actions-plugin/common'; +import { ActionTypeModel, GenericValidationResult } from '../../../../types'; +import { EmailActionParams, EmailConfig, EmailSecrets } from '../types'; import { RegistrationServices } from '..'; const emailServices: EuiSelectOption[] = [ @@ -99,84 +95,6 @@ export function getActionType( defaultMessage: 'Send to email', } ), - validateConnector: async ( - action: EmailActionConnector - ): Promise< - ConnectorValidationResult, EmailSecrets> - > => { - const translations = await import('./translations'); - const configErrors = { - from: new Array(), - port: new Array(), - host: new Array(), - service: new Array(), - clientId: new Array(), - tenantId: new Array(), - }; - const secretsErrors = { - user: new Array(), - password: new Array(), - clientSecret: new Array(), - }; - - const validationResult = { - config: { errors: configErrors }, - secrets: { errors: secretsErrors }, - }; - if (!action.config.from) { - configErrors.from.push(translations.SENDER_REQUIRED); - } else { - const validatedEmail = services.validateEmailAddresses([action.config.from])[0]; - if (!validatedEmail.valid) { - const message = - validatedEmail.reason === InvalidEmailReason.notAllowed - ? translations.getNotAllowedEmailAddress(action.config.from) - : translations.getInvalidEmailAddress(action.config.from); - configErrors.from.push(message); - } - } - if (action.config.service !== AdditionalEmailServices.EXCHANGE) { - if (!action.config.port) { - configErrors.port.push(translations.PORT_REQUIRED); - } - if (!action.config.host) { - configErrors.host.push(translations.HOST_REQUIRED); - } - if (action.config.hasAuth && !action.secrets.user && !action.secrets.password) { - secretsErrors.user.push(translations.USERNAME_REQUIRED); - } - if (action.config.hasAuth && !action.secrets.user && !action.secrets.password) { - secretsErrors.password.push(translations.PASSWORD_REQUIRED); - } - } else { - if (!action.config.clientId) { - configErrors.clientId.push(translations.CLIENT_ID_REQUIRED); - } - if (!action.config.tenantId) { - configErrors.tenantId.push(translations.TENANT_ID_REQUIRED); - } - if (!action.secrets.clientSecret) { - secretsErrors.clientSecret.push(translations.CLIENT_SECRET_REQUIRED); - } - } - if (!action.config.service) { - configErrors.service.push(translations.SERVICE_REQUIRED); - } - if (action.secrets.user && !action.secrets.password) { - secretsErrors.password.push(translations.PASSWORD_REQUIRED_FOR_USER_USED); - } - if (!action.secrets.user && action.secrets.password) { - secretsErrors.user.push( - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredUserText', - { - defaultMessage: 'Username is required when password is used.', - } - ) - ); - } - return validationResult; - }, validateParams: async ( actionParams: EmailActionParams ): Promise> => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx index 742e8981a1a1a..b06340c7a180d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx @@ -5,16 +5,25 @@ * 2.0. */ -import React from 'react'; +import React, { Suspense } from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { EmailActionConnector } from '../types'; +import { act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { useKibana } from '../../../../common/lib/kibana'; import EmailActionConnectorFields from './email_connector'; import * as hooks from './use_email_config'; +import { + AppMockRenderer, + ConnectorFormTestProvider, + createAppMockRenderer, + waitForComponentToUpdate, +} from '../test_utils'; jest.mock('../../../../common/lib/kibana'); +const useKibanaMock = useKibana as jest.Mocked; -describe('EmailActionConnectorFields renders', () => { - test('all connector fields is rendered', () => { +describe('EmailActionConnectorFields', () => { + test('all connector fields are rendered', async () => { const actionConnector = { secrets: { user: 'user', @@ -27,18 +36,21 @@ describe('EmailActionConnectorFields renders', () => { from: 'test@test.com', hasAuth: true, }, - } as EmailActionConnector; + isDeprecated: false, + }; + const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> + + {}} + /> + ); + + await waitForComponentToUpdate(); + expect(wrapper.find('[data-test-subj="emailFromInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="emailFromInput"]').first().prop('value')).toBe( 'test@test.com' @@ -50,7 +62,7 @@ describe('EmailActionConnectorFields renders', () => { expect(wrapper.find('[data-test-subj="emailPasswordInput"]').length > 0).toBeTruthy(); }); - test('secret connector fields is not rendered when hasAuth false', () => { + test('secret connector fields are not rendered when hasAuth false', async () => { const actionConnector = { secrets: {}, id: 'test', @@ -60,18 +72,21 @@ describe('EmailActionConnectorFields renders', () => { from: 'test@test.com', hasAuth: false, }, - } as EmailActionConnector; + isDeprecated: false, + }; + const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> + + {}} + /> + ); + + await waitForComponentToUpdate(); + expect(wrapper.find('[data-test-subj="emailFromInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="emailFromInput"]').first().prop('value')).toBe( 'test@test.com' @@ -82,7 +97,7 @@ describe('EmailActionConnectorFields renders', () => { expect(wrapper.find('[data-test-subj="emailPasswordInput"]').length > 0).toBeFalsy(); }); - test('service field defaults to empty when not defined', () => { + test('service field defaults to empty when not defined', async () => { const actionConnector = { secrets: { user: 'user', @@ -95,18 +110,21 @@ describe('EmailActionConnectorFields renders', () => { from: 'test@test.com', hasAuth: true, }, - } as EmailActionConnector; + isDeprecated: false, + }; + const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> + + {}} + /> + ); + + await waitForComponentToUpdate(); + expect(wrapper.find('[data-test-subj="emailFromInput"]').first().prop('value')).toBe( 'test@test.com' ); @@ -116,7 +134,7 @@ describe('EmailActionConnectorFields renders', () => { ); }); - test('service field is correctly selected when defined', () => { + test('service field are correctly selected when defined', async () => { const actionConnector = { secrets: { user: 'user', @@ -130,28 +148,35 @@ describe('EmailActionConnectorFields renders', () => { hasAuth: true, service: 'gmail', }, - } as EmailActionConnector; + isDeprecated: false, + }; + const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> + + {}} + /> + ); + + await waitForComponentToUpdate(); + expect(wrapper.find('[data-test-subj="emailServiceSelectInput"]').length > 0).toBeTruthy(); expect(wrapper.find('select[data-test-subj="emailServiceSelectInput"]').prop('value')).toEqual( 'gmail' ); }); - test('host, port and secure fields should be disabled when service field is set to well known service', () => { + test('host, port and secure fields should be disabled when service field is set to well known service', async () => { + const getEmailServiceConfig = jest + .fn() + .mockResolvedValue({ host: 'https://example.com', port: 80, secure: false }); jest .spyOn(hooks, 'useEmailConfig') - .mockImplementation(() => ({ emailServiceConfigurable: false, setEmailService: jest.fn() })); + .mockImplementation(() => ({ isLoading: false, getEmailServiceConfig })); + const actionConnector = { secrets: { user: 'user', @@ -165,18 +190,22 @@ describe('EmailActionConnectorFields renders', () => { hasAuth: true, service: 'gmail', }, - } as EmailActionConnector; + isDeprecated: false, + }; + const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> + + {}} + /> + ); + + await waitForComponentToUpdate(); + + wrapper.update(); expect(wrapper.find('[data-test-subj="emailHostInput"]').first().prop('disabled')).toBe(true); expect(wrapper.find('[data-test-subj="emailPortInput"]').first().prop('disabled')).toBe(true); expect(wrapper.find('[data-test-subj="emailSecureSwitch"]').first().prop('disabled')).toBe( @@ -184,10 +213,14 @@ describe('EmailActionConnectorFields renders', () => { ); }); - test('host, port and secure fields should not be disabled when service field is set to other', () => { + test('host, port and secure fields should not be disabled when service field is set to other', async () => { + const getEmailServiceConfig = jest + .fn() + .mockResolvedValue({ host: 'https://example.com', port: 80, secure: false }); jest .spyOn(hooks, 'useEmailConfig') - .mockImplementation(() => ({ emailServiceConfigurable: true, setEmailService: jest.fn() })); + .mockImplementation(() => ({ isLoading: false, getEmailServiceConfig })); + const actionConnector = { secrets: { user: 'user', @@ -201,18 +234,21 @@ describe('EmailActionConnectorFields renders', () => { hasAuth: true, service: 'other', }, - } as EmailActionConnector; + isDeprecated: false, + }; + const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> + + {}} + /> + ); + + await waitForComponentToUpdate(); + expect(wrapper.find('[data-test-subj="emailHostInput"]').first().prop('disabled')).toBe(false); expect(wrapper.find('[data-test-subj="emailPortInput"]').first().prop('disabled')).toBe(false); expect(wrapper.find('[data-test-subj="emailSecureSwitch"]').first().prop('disabled')).toBe( @@ -220,75 +256,436 @@ describe('EmailActionConnectorFields renders', () => { ); }); - test('should display a message to remember username and password when creating a connector with authentication', () => { - const actionConnector = { - actionTypeId: '.email', - config: { - hasAuth: true, - }, - secrets: {}, - } as EmailActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); - }); + describe('Validation', () => { + let appMockRenderer: AppMockRenderer; + const onSubmit = jest.fn(); + const validateEmailAddresses = jest.fn(); - test('should display a message for missing secrets after import', () => { - const actionConnector = { - actionTypeId: '.email', - config: { - hasAuth: true, - }, - isMissingSecrets: true, - secrets: {}, - } as EmailActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0); - }); + beforeEach(() => { + jest.clearAllMocks(); + appMockRenderer = createAppMockRenderer(); + validateEmailAddresses.mockReturnValue([{ valid: true }]); + }); - test('should display a message when editing an authenticated email connector explaining why username and password must be re-entered', () => { - const actionConnector = { - secrets: {}, - id: 'test', - actionTypeId: '.email', - name: 'email', - config: { - from: 'test@test.com', - hasAuth: true, - }, - } as EmailActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> + it('submits the connector', async () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + clientSecret: null, + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: { + from: 'test@test.com', + port: 2323, + host: 'localhost', + test: 'test', + hasAuth: true, + service: 'other', + }, + isDeprecated: false, + }; + + const { getByTestId } = appMockRenderer.render( + + {}} + /> + + ); + + await waitForComponentToUpdate(); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: { + actionTypeId: '.email', + config: { + from: 'test@test.com', + hasAuth: true, + host: 'localhost', + port: 2323, + secure: false, + service: 'other', + }, + id: 'test', + isDeprecated: false, + name: 'email', + secrets: { + user: 'user', + password: 'pass', + }, + }, + isValid: true, + }); + }); + + it('submits the connector with auth false', async () => { + const actionConnector = { + secrets: { + user: null, + password: null, + clientSecret: null, + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: { + from: 'test@test.com', + port: 2323, + host: 'localhost', + test: 'test', + hasAuth: false, + service: 'other', + }, + isDeprecated: false, + }; + + const { getByTestId } = appMockRenderer.render( + + {}} + /> + + ); + + await waitForComponentToUpdate(); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: { + actionTypeId: '.email', + config: { + from: 'test@test.com', + port: 2323, + host: 'localhost', + hasAuth: false, + service: 'other', + secure: false, + }, + id: 'test', + isDeprecated: false, + name: 'email', + }, + isValid: true, + }); + }); + + it('connector validation fails when connector config is not valid', async () => { + useKibanaMock().services.actions.validateEmailAddresses = jest + .fn() + .mockReturnValue([{ valid: false }]); + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: { + from: 'test@notallowed.com', + hasAuth: true, + service: 'other', + }, + isDeprecated: false, + }; + + const { getByTestId } = appMockRenderer.render( + + {}} + /> + + ); + + await waitForComponentToUpdate(); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: {}, + isValid: false, + }); + }); + + it('connector validation fails when user specified but not password', async () => { + const actionConnector = { + secrets: { + user: 'user', + password: '', + clientSecret: null, + }, + id: 'test', + actionTypeId: '.email', + isPreconfigured: false, + isDeprecated: false, + name: 'email', + config: { + from: 'test@test.com', + port: 2323, + host: 'localhost', + test: 'test', + hasAuth: true, + service: 'other', + }, + }; + + const { getByTestId } = appMockRenderer.render( + + {}} + /> + + ); + + await waitForComponentToUpdate(); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: {}, + isValid: false, + }); + }); + + it('connector validation fails when server type is not selected', async () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'password', + }, + id: 'test', + actionTypeId: '.email', + isPreconfigured: false, + isDeprecated: false, + name: 'email', + config: { + from: 'test@test.com', + port: 2323, + host: 'localhost', + test: 'test', + hasAuth: true, + }, + }; + + const { getByTestId } = appMockRenderer.render( + + {}} + /> + + ); + + await waitForComponentToUpdate(); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: {}, + isValid: false, + }); + }); + + it('connector validation fails when exchange service is selected, but clientId, tenantId and clientSecrets were not defined', async () => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + clientSecret: null, + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + isPreconfigured: false, + isDeprecated: false, + config: { + from: 'test@test.com', + hasAuth: true, + service: 'exchange_server', + }, + }; + + const { getByTestId } = appMockRenderer.render( + + + {}} + /> + + + ); + + await waitForComponentToUpdate(); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: {}, + isValid: false, + }); + }); + + it.each([[123.5], ['123.5']])( + 'connector validation fails when port is not an integer: %p', + async (port) => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: { + from: 'test@notallowed.com', + hasAuth: true, + service: 'other', + host: 'my-host', + port, + }, + isDeprecated: false, + }; + + const { getByTestId } = appMockRenderer.render( + + {}} + /> + + ); + + await waitForComponentToUpdate(); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: {}, + isValid: false, + }); + } ); - expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); + + it.each([[123], ['123']])('connector validation pass when port is valid: %p', async (port) => { + const actionConnector = { + secrets: { + user: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + config: { + from: 'test@notallowed.com', + hasAuth: true, + service: 'other', + host: 'my-host', + port, + }, + isDeprecated: false, + }; + + const { getByTestId } = appMockRenderer.render( + + {}} + /> + + ); + + await waitForComponentToUpdate(); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: { + actionTypeId: '.email', + config: { + from: 'test@notallowed.com', + hasAuth: true, + host: 'my-host', + port, + secure: false, + service: 'other', + }, + id: 'test', + isDeprecated: false, + name: 'email', + secrets: { + password: 'pass', + user: 'user', + }, + }, + isValid: true, + }); + }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx index e870e0ef38439..c2a27ab8bae23 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.tsx @@ -5,314 +5,244 @@ * 2.0. */ -import React, { lazy, useEffect } from 'react'; -import { - EuiFieldText, - EuiFlexItem, - EuiFlexGroup, - EuiFieldNumber, - EuiFieldPassword, - EuiSelect, - EuiSwitch, - EuiFormRow, - EuiTitle, - EuiSpacer, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import React, { lazy, useEffect, useMemo } from 'react'; +import { isEmpty } from 'lodash'; +import { EuiFlexItem, EuiFlexGroup, EuiTitle, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiLink } from '@elastic/eui'; -import { AdditionalEmailServices } from '@kbn/actions-plugin/common'; +import { AdditionalEmailServices, InvalidEmailReason } from '@kbn/actions-plugin/common'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; +import { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public'; +import { + UseField, + useFormContext, + useFormData, + FieldConfig, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { + NumericField, + SelectField, + TextField, + ToggleField, +} from '@kbn/es-ui-shared-plugin/static/forms/components'; import { ActionConnectorFieldsProps } from '../../../../types'; -import { EmailActionConnector } from '../types'; import { useKibana } from '../../../../common/lib/kibana'; -import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label'; import { getEmailServices } from './email'; import { useEmailConfig } from './use_email_config'; +import { PasswordField } from '../../password_field'; +import * as i18n from './translations'; +import { useConnectorContext } from '../../../context/use_connector_context'; + +const { emptyField } = fieldValidators; const ExchangeFormFields = lazy(() => import('./exchange_form')); -export const EmailActionConnectorFields: React.FunctionComponent< - ActionConnectorFieldsProps -> = ({ action, editActionConfig, editActionSecrets, errors, readOnly }) => { - const { docLinks, http, isCloud } = useKibana().services; - const { from, host, port, secure, hasAuth, service } = action.config; - const { user, password } = action.secrets; - const { emailServiceConfigurable, setEmailService } = useEmailConfig( +const shouldDisableEmailConfiguration = (service: string | null | undefined) => + isEmpty(service) || + (service !== AdditionalEmailServices.EXCHANGE && service !== AdditionalEmailServices.OTHER); + +const getEmailConfig = ( + href: string, + validateFunc: ActionsPublicPluginSetup['validateEmailAddresses'] +): FieldConfig => ({ + label: i18n.FROM_LABEL, + helpText: ( + + + + ), + validations: [ + { validator: emptyField(i18n.SENDER_REQUIRED) }, + { + validator: ({ value }) => { + const validatedEmail = validateFunc([value])[0]; + if (!validatedEmail.valid) { + const message = + validatedEmail.reason === InvalidEmailReason.notAllowed + ? i18n.getNotAllowedEmailAddress(value) + : i18n.getInvalidEmailAddress(value); + + return { + message, + }; + } + }, + }, + ], +}); + +const portConfig: FieldConfig = { + label: i18n.PORT_LABEL, + validations: [ + { + validator: emptyField(i18n.PORT_REQUIRED), + }, + { + validator: ({ value }) => { + const port = Number.parseFloat(value); + + if (!Number.isInteger(port)) { + return { message: i18n.PORT_INVALID }; + } + }, + }, + ], +}; + +export const EmailActionConnectorFields: React.FunctionComponent = ({ + readOnly, +}) => { + const { + docLinks, http, - service, - editActionConfig + isCloud, + notifications: { toasts }, + } = useKibana().services; + const { + services: { validateEmailAddresses }, + } = useConnectorContext(); + + const form = useFormContext(); + const { updateFieldValues } = form; + const [{ config }] = useFormData({ + watch: ['config.service', 'config.hasAuth'], + }); + + const emailFieldConfig = useMemo( + () => getEmailConfig(docLinks.links.alerting.emailActionConfig, validateEmailAddresses), + [docLinks.links.alerting.emailActionConfig, validateEmailAddresses] ); + const { service = null, hasAuth = false } = config ?? {}; + const disableServiceConfig = shouldDisableEmailConfiguration(service); + const { isLoading, getEmailServiceConfig } = useEmailConfig({ http, toasts }); + useEffect(() => { - if (!action.id) { - editActionConfig('hasAuth', true); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + async function fetchConfig() { + if ( + service === null || + service === AdditionalEmailServices.OTHER || + service === AdditionalEmailServices.EXCHANGE + ) { + return; + } - const isFromInvalid: boolean = - from !== undefined && errors.from !== undefined && errors.from.length > 0; - const isHostInvalid: boolean = - host !== undefined && errors.host !== undefined && errors.host.length > 0; - const isServiceInvalid: boolean = - service !== undefined && errors.service !== undefined && errors.service.length > 0; - const isPortInvalid: boolean = - port !== undefined && errors.port !== undefined && errors.port.length > 0; + const emailConfig = await getEmailServiceConfig(service); + updateFieldValues({ + config: { + host: emailConfig?.host, + port: emailConfig?.port, + secure: emailConfig?.secure, + }, + }); + } - const isPasswordInvalid: boolean = - password !== undefined && errors.password !== undefined && errors.password.length > 0; - const isUserInvalid: boolean = - user !== undefined && errors.user !== undefined && errors.user.length > 0; - - const authForm = ( - <> - {getEncryptedFieldNotifyLabel( - !action.id, - 2, - action.isMissingSecrets ?? false, - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterValuesLabel', - { - defaultMessage: - 'Username and password are encrypted. Please reenter values for these fields.', - } - ) - )} - - - - { - editActionSecrets('user', nullableString(e.target.value)); - }} - onBlur={() => { - if (!user) { - editActionSecrets('user', ''); - } - }} - /> - - - - - { - editActionSecrets('password', nullableString(e.target.value)); - }} - onBlur={() => { - if (!password) { - editActionSecrets('password', ''); - } - }} - /> - - - - - ); + fetchConfig(); + }, [updateFieldValues, getEmailServiceConfig, service]); return ( <> - - - - } - > - { - editActionConfig('from', e.target.value); - }} - onBlur={() => { - if (!from) { - editActionConfig('from', ''); - } - }} - /> - + - - { - setEmailService(e.target.value); - }} - /> - + {service === AdditionalEmailServices.EXCHANGE ? ( - + ) : ( <> - - { - editActionConfig('host', e.target.value); - }} - onBlur={() => { - if (!host) { - editActionConfig('host', ''); - } - }} - /> - + - - { - editActionConfig('port', parseInt(e.target.value, 10)); - }} - onBlur={() => { - if (!port) { - editActionConfig('port', 0); - } - }} - /> - + - - - { - editActionConfig('secure', e.target.checked); - }} - /> - - + @@ -329,26 +259,51 @@ export const EmailActionConnectorFields: React.FunctionComponent< - { - editActionConfig('hasAuth', e.target.checked); - if (!e.target.checked) { - editActionSecrets('user', null); - editActionSecrets('password', null); - } + - {hasAuth ? authForm : null} + {hasAuth ? ( + <> + + + + + + + + + + ) : null} )} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.test.tsx index d87efe5134794..a07ecde41c76d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.test.tsx @@ -7,36 +7,35 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { EmailActionConnector } from '../types'; import ExchangeFormFields from './exchange_form'; +import { ConnectorFormTestProvider } from '../test_utils'; +import { act, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; jest.mock('../../../../common/lib/kibana'); describe('ExchangeFormFields renders', () => { + const actionConnector = { + secrets: { + clientSecret: 'secret', + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + isDeprecated: false, + config: { + from: 'test@test.com', + service: 'exchange_server', + tenantId: 'tenant-id', + clientId: 'clientId-id', + }, + }; + test('should display exchange form fields', () => { - const actionConnector = { - secrets: { - clientSecret: 'user', - }, - id: 'test', - actionTypeId: '.email', - name: 'exchange email', - config: { - from: 'test@test.com', - hasAuth: true, - service: 'exchange_server', - clientId: '123', - tenantId: '1234', - }, - } as EmailActionConnector; const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - /> + + + ); expect(wrapper.find('[data-test-subj="emailClientSecret"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="emailClientId"]').length > 0).toBeTruthy(); @@ -44,25 +43,18 @@ describe('ExchangeFormFields renders', () => { }); test('exchange field defaults to empty when not defined', () => { - const actionConnector = { + const connector = { + ...actionConnector, secrets: {}, - id: 'test', - actionTypeId: '.email', - name: 'email', config: { from: 'test@test.com', - hasAuth: true, - service: 'exchange_server', }, - } as EmailActionConnector; + }; + const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - /> + + + ); expect(wrapper.find('[data-test-subj="emailClientSecret"]').length > 0).toBeTruthy(); expect(wrapper.find('input[data-test-subj="emailClientSecret"]').prop('value')).toEqual(''); @@ -73,4 +65,67 @@ describe('ExchangeFormFields renders', () => { expect(wrapper.find('[data-test-subj="emailTenantId"]').length > 0).toBeTruthy(); expect(wrapper.find('input[data-test-subj="emailTenantId"]').prop('value')).toEqual(''); }); + + describe('Validation', () => { + const onSubmit = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const tests: Array<[string, string]> = [ + ['emailTenantId', ''], + ['emailClientId', ''], + ['emailClientSecret', ''], + ]; + + it('connector validation succeeds when connector config is valid', async () => { + const { getByTestId } = render( + + + + ); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: { + secrets: { + clientSecret: 'secret', + }, + id: 'test', + actionTypeId: '.email', + name: 'email', + isDeprecated: false, + config: { + tenantId: 'tenant-id', + clientId: 'clientId-id', + }, + }, + isValid: true, + }); + }); + + it.each(tests)('validates correctly %p', async (field, value) => { + const res = render( + + + + ); + + await act(async () => { + await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, { + delay: 10, + }); + }); + + await act(async () => { + userEvent.click(res.getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }); + }); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.tsx index 1616e964147ab..41f82b3a32bfc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/exchange_form.tsx @@ -6,158 +6,88 @@ */ import React from 'react'; -import { - EuiFieldText, - EuiFlexItem, - EuiFlexGroup, - EuiFormRow, - EuiFieldPassword, - EuiLink, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import { EuiFlexItem, EuiFlexGroup, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { IErrorObject } from '../../../../types'; -import { EmailActionConnector } from '../types'; -import { nullableString } from './email_connector'; -import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; +import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { TextField } from '@kbn/es-ui-shared-plugin/static/forms/components'; import { useKibana } from '../../../../common/lib/kibana'; +import * as i18n from './translations'; +import { PasswordField } from '../../password_field'; + +const { emptyField } = fieldValidators; interface ExchangeFormFieldsProps { - action: EmailActionConnector; - editActionConfig: (property: string, value: unknown) => void; - editActionSecrets: (property: string, value: unknown) => void; - errors: IErrorObject; readOnly: boolean; } -const ExchangeFormFields: React.FunctionComponent = ({ - action, - editActionConfig, - editActionSecrets, - errors, - readOnly, -}) => { +const ExchangeFormFields: React.FC = ({ readOnly }) => { const { docLinks } = useKibana().services; - const { tenantId, clientId } = action.config; - const { clientSecret } = action.secrets; - - const isClientIdInvalid: boolean = - clientId !== undefined && errors.clientId !== undefined && errors.clientId.length > 0; - const isTenantIdInvalid: boolean = - tenantId !== undefined && errors.tenantId !== undefined && errors.tenantId.length > 0; - const isClientSecretInvalid: boolean = - clientSecret !== undefined && - errors.clientSecret !== undefined && - errors.clientSecret.length > 0; return ( <> - - - - } - > - { - editActionConfig('tenantId', nullableString(e.target.value)); - }} - onBlur={() => { - if (!tenantId) { - editActionConfig('tenantId', ''); - } - }} - /> - + + + + ), + validations: [ + { + validator: emptyField(i18n.TENANT_ID_REQUIRED), + }, + ], + }} + componentProps={{ + euiFieldProps: { 'data-test-subj': 'emailTenantId' }, + readOnly, + placeholder: '00000000-0000-0000-0000-000000000000', + }} + /> - - - - } - > - { - editActionConfig('clientId', nullableString(e.target.value)); - }} - onBlur={() => { - if (!clientId) { - editActionConfig('clientId', ''); - } - }} - /> - + + + + ), + validations: [ + { + validator: emptyField(i18n.CLIENT_ID_REQUIRED), + }, + ], + }} + componentProps={{ + euiFieldProps: { 'data-test-subj': 'emailClientId' }, + readOnly, + placeholder: '00000000-0000-0000-0000-000000000000', + }} + /> - {getEncryptedFieldNotifyLabel( - !action.id, - 1, - action.isMissingSecrets ?? false, - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.reenterClientSecretLabel', - { - defaultMessage: 'Client Secret is encrypted. Please reenter value for this field.', - } - ) - )} - = ({ /> } - > - { - editActionSecrets('clientSecret', nullableString(e.target.value)); - }} - onBlur={() => { - if (!clientSecret) { - editActionSecrets('clientSecret', ''); - } - }} - /> - + data-test-subj="emailClientSecret" + /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts index e1bee12d98993..65fc2bdb542e8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/translations.ts @@ -7,17 +7,87 @@ import { i18n } from '@kbn/i18n'; -export const SENDER_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredFromText', +export const USERNAME_LABEL = i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.userTextFieldLabel', { - defaultMessage: 'Sender is required.', + defaultMessage: 'Username', + } +); + +export const PASSWORD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.passwordFieldLabel', + { + defaultMessage: 'Password', + } +); + +export const FROM_LABEL = i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.fromTextFieldLabel', + { + defaultMessage: 'Sender', + } +); + +export const SERVICE_LABEL = i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.serviceTextFieldLabel', + { + defaultMessage: 'Service', + } +); + +export const TENANT_ID_LABEL = i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.tenantIdFieldLabel', + { + defaultMessage: 'Tenant ID', + } +); + +export const CLIENT_ID_LABEL = i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.clientIdFieldLabel', + { + defaultMessage: 'Client ID', + } +); + +export const CLIENT_SECRET_LABEL = i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.clientSecretTextFieldLabel', + { + defaultMessage: 'Client Secret', + } +); + +export const HOST_LABEL = i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.hostTextFieldLabel', + { + defaultMessage: 'Host', + } +); + +export const PORT_LABEL = i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.portTextFieldLabel', + { + defaultMessage: 'Port', } ); -export const SENDER_NOT_VALID = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.formatFromText', +export const SECURE_LABEL = i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.secureSwitchLabel', + { + defaultMessage: 'Secure', + } +); + +export const HAS_AUTH_LABEL = i18n.translate( + 'xpack.triggersActionsUI.sections.builtinActionTypes.emailAction.hasAuthSwitchLabel', + { + defaultMessage: 'Require authentication for this server', + } +); + +export const SENDER_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredFromText', { - defaultMessage: 'Sender is not a valid email address.', + defaultMessage: 'Sender is required.', } ); @@ -35,17 +105,17 @@ export const TENANT_ID_REQUIRED = i18n.translate( } ); -export const CLIENT_SECRET_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredClientSecretText', +export const PORT_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPortText', { - defaultMessage: 'Client Secret is required.', + defaultMessage: 'Port is required.', } ); -export const PORT_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPortText', +export const PORT_INVALID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.invalidPortText', { - defaultMessage: 'Port is required.', + defaultMessage: 'Port is invalid.', } ); @@ -70,20 +140,6 @@ export const USERNAME_REQUIRED = i18n.translate( } ); -export const PASSWORD_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredAuthPasswordText', - { - defaultMessage: 'Password is required.', - } -); - -export const PASSWORD_REQUIRED_FOR_USER_USED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredPasswordText', - { - defaultMessage: 'Password is required when username is used.', - } -); - export const TO_CC_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredEntryText', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.test.ts index 6a640cc734071..f03c869ea5ca4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.test.ts @@ -5,112 +5,93 @@ * 2.0. */ +import { httpServiceMock, notificationServiceMock } from '@kbn/core/public/mocks'; import { renderHook, act } from '@testing-library/react-hooks'; -import { HttpSetup } from '@kbn/core/public'; import { useEmailConfig } from './use_email_config'; -const http = { - get: jest.fn(), -}; - -const editActionConfig = jest.fn(); +const http = httpServiceMock.createStartContract(); +const toasts = notificationServiceMock.createStartContract().toasts; const renderUseEmailConfigHook = (currentService?: string) => - renderHook(() => useEmailConfig(http as unknown as HttpSetup, currentService, editActionConfig)); + renderHook(() => useEmailConfig({ http, toasts })); describe('useEmailConfig', () => { beforeEach(() => jest.clearAllMocks()); - it('should call get email config API when service changes and handle result', async () => { + it('should return the correct result when requesting the config of a service', async () => { http.get.mockResolvedValueOnce({ host: 'smtp.gmail.com', port: 465, secure: true, }); - const { result, waitForNextUpdate } = renderUseEmailConfigHook(); + + const { result } = renderUseEmailConfigHook(); await act(async () => { - result.current.setEmailService('gmail'); - await waitForNextUpdate(); + const res = await result.current.getEmailServiceConfig('gmail'); + expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/gmail'); + expect(res).toEqual({ + host: 'smtp.gmail.com', + port: 465, + secure: true, + }); }); - - expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/gmail'); - expect(editActionConfig).toHaveBeenCalledWith('service', 'gmail'); - - expect(editActionConfig).toHaveBeenCalledWith('host', 'smtp.gmail.com'); - expect(editActionConfig).toHaveBeenCalledWith('port', 465); - expect(editActionConfig).toHaveBeenCalledWith('secure', true); - - expect(result.current.emailServiceConfigurable).toEqual(false); }); - it('should call get email config API when service changes and handle partial result', async () => { + it('should return the correct result when requesting the config of a service on partial result', async () => { http.get.mockResolvedValueOnce({ host: 'smtp.gmail.com', port: 465, }); - const { result, waitForNextUpdate } = renderUseEmailConfigHook(); + + const { result } = renderUseEmailConfigHook(); await act(async () => { - result.current.setEmailService('gmail'); - await waitForNextUpdate(); + const res = await result.current.getEmailServiceConfig('gmail'); + expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/gmail'); + expect(res).toEqual({ + host: 'smtp.gmail.com', + port: 465, + secure: false, + }); }); - - expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/gmail'); - expect(editActionConfig).toHaveBeenCalledWith('service', 'gmail'); - - expect(editActionConfig).toHaveBeenCalledWith('host', 'smtp.gmail.com'); - expect(editActionConfig).toHaveBeenCalledWith('port', 465); - expect(editActionConfig).toHaveBeenCalledWith('secure', false); - - expect(result.current.emailServiceConfigurable).toEqual(false); }); - it('should call get email config API when service changes and handle empty result', async () => { + it('should return the correct result when requesting the config of a service on empty result', async () => { http.get.mockResolvedValueOnce({}); - const { result, waitForNextUpdate } = renderUseEmailConfigHook(); + const { result } = renderUseEmailConfigHook(); + await act(async () => { - result.current.setEmailService('foo'); - await waitForNextUpdate(); + const res = await result.current.getEmailServiceConfig('foo'); + expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/foo'); + expect(res).toEqual({ + host: '', + port: 0, + secure: false, + }); }); - - expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/foo'); - expect(editActionConfig).toHaveBeenCalledWith('service', 'foo'); - - expect(editActionConfig).toHaveBeenCalledWith('host', ''); - expect(editActionConfig).toHaveBeenCalledWith('port', 0); - expect(editActionConfig).toHaveBeenCalledWith('secure', false); - - expect(result.current.emailServiceConfigurable).toEqual(true); }); - it('should call get email config API when service changes and handle errors', async () => { + it('should show a danger toaster on error', async () => { http.get.mockImplementationOnce(() => { throw new Error('no!'); }); + const { result, waitForNextUpdate } = renderUseEmailConfigHook(); + await act(async () => { - result.current.setEmailService('foo'); + result.current.getEmailServiceConfig('foo'); await waitForNextUpdate(); + expect(toasts.addDanger).toHaveBeenCalled(); }); - - expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/foo'); - expect(editActionConfig).toHaveBeenCalledWith('service', 'foo'); - - expect(editActionConfig).toHaveBeenCalledWith('host', ''); - expect(editActionConfig).toHaveBeenCalledWith('port', 0); - expect(editActionConfig).toHaveBeenCalledWith('secure', false); - - expect(result.current.emailServiceConfigurable).toEqual(true); }); - it('should call get email config API when initial service value is passed and determine if config is editable without overwriting config', async () => { + it('should not make an API call if the service is empty', async () => { http.get.mockResolvedValueOnce({ host: 'smtp.gmail.com', port: 465, secure: true, }); - const { result } = renderUseEmailConfigHook('gmail'); - expect(http.get).toHaveBeenCalledWith('/internal/actions/connector/_email_config/gmail'); - expect(editActionConfig).not.toHaveBeenCalled(); - expect(result.current.emailServiceConfigurable).toEqual(false); + + renderUseEmailConfigHook(''); + expect(http.get).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.ts index 53ec70b8b62b3..c706630f1f6bb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/use_email_config.ts @@ -5,66 +5,95 @@ * 2.0. */ -import { useCallback, useEffect, useState } from 'react'; -import { HttpSetup } from '@kbn/core/public'; import { isEmpty } from 'lodash'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { HttpSetup, IToasts } from '@kbn/core/public'; import { AdditionalEmailServices } from '@kbn/actions-plugin/common'; +import { i18n } from '@kbn/i18n'; import { EmailConfig } from '../types'; import { getServiceConfig } from './api'; -export function useEmailConfig( - http: HttpSetup, - currentService: string | undefined, - editActionConfig: (property: string, value: unknown) => void -) { - const [emailServiceConfigurable, setEmailServiceConfigurable] = useState(false); - const [emailService, setEmailService] = useState(undefined); +interface Props { + http: HttpSetup; + toasts: IToasts; +} + +interface UseEmailConfigReturnValue { + isLoading: boolean; + getEmailServiceConfig: ( + service: string + ) => Promise> | undefined>; +} + +const getConfig = ( + service: string, + config: Partial> +): Pick | undefined => { + if (service) { + if (service === AdditionalEmailServices.EXCHANGE) { + return; + } + + return { + host: config?.host ? config.host : '', + port: config?.port ? config.port : 0, + secure: null != config?.secure ? config.secure : false, + }; + } +}; + +export function useEmailConfig({ http, toasts }: Props): UseEmailConfigReturnValue { + const [isLoading, setIsLoading] = useState(false); + const abortCtrlRef = useRef(new AbortController()); + const isMounted = useRef(false); const getEmailServiceConfig = useCallback( - async (service: string) => { - let serviceConfig: Partial>; - try { - serviceConfig = await getServiceConfig({ http, service }); - setEmailServiceConfigurable(isEmpty(serviceConfig)); - } catch (err) { - serviceConfig = {}; - setEmailServiceConfigurable(true); + async (service: string | null) => { + if (service == null || isEmpty(service)) { + return; } - return serviceConfig; - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [editActionConfig] - ); + setIsLoading(true); + isMounted.current = true; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); - useEffect(() => { - (async () => { - if (emailService) { - editActionConfig('service', emailService); - if (emailService === AdditionalEmailServices.EXCHANGE) { - return; + try { + const serviceConfig = await getServiceConfig({ http, service }); + if (isMounted.current) { + setIsLoading(false); } - const serviceConfig = await getEmailServiceConfig(emailService); - editActionConfig('host', serviceConfig?.host ? serviceConfig.host : ''); - editActionConfig('port', serviceConfig?.port ? serviceConfig.port : 0); - editActionConfig('secure', null != serviceConfig?.secure ? serviceConfig.secure : false); + return getConfig(service, serviceConfig); + } catch (error) { + if (isMounted.current) { + setIsLoading(false); + + if (error.name !== 'AbortError') { + toasts.addDanger( + error.body?.message ?? + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.emailAction.updateErrorNotificationText', + { defaultMessage: 'Cannot get service configuration' } + ) + ); + } + } } - })(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [emailService]); + }, + [http, toasts] + ); useEffect(() => { - (async () => { - if (currentService) { - await getEmailServiceConfig(currentService); - } - })(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentService]); + isMounted.current = true; + return () => { + isMounted.current = false; + abortCtrlRef.current.abort(); + }; + }, []); return { - emailServiceConfigurable, - setEmailService, + isLoading, + getEmailServiceConfig, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx index 42dc8a16a8b19..ccf90cb91d837 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.test.tsx @@ -8,7 +8,6 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel } from '../../../../types'; -import { EsIndexActionConnector } from '../types'; import { registrationServicesMock } from '../../../../mocks'; const ACTION_TYPE_ID = '.index'; @@ -30,58 +29,6 @@ describe('actionTypeRegistry.get() works', () => { }); }); -describe('index connector validation', () => { - test('connector validation succeeds when connector config is valid', async () => { - const actionConnector = { - secrets: {}, - id: 'test', - actionTypeId: '.index', - name: 'es_index', - config: { - index: 'test_es_index', - refresh: false, - executionTimeField: '1', - }, - } as EsIndexActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - index: [], - }, - }, - secrets: { - errors: {}, - }, - }); - }); -}); - -describe('index connector validation with minimal config', () => { - test('connector validation succeeds when connector config is valid', async () => { - const actionConnector = { - secrets: {}, - id: 'test', - actionTypeId: '.index', - name: 'es_index', - config: { - index: 'test_es_index', - }, - } as EsIndexActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - index: [], - }, - }, - secrets: { - errors: {}, - }, - }); - }); -}); - describe('action params validation', () => { test('action params validation succeeds when action params are valid', async () => { expect( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.tsx index 80d38bda22ab3..75666c1282da3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index.tsx @@ -7,13 +7,8 @@ import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; -import { - ActionTypeModel, - GenericValidationResult, - ConnectorValidationResult, - ALERT_HISTORY_PREFIX, -} from '../../../../types'; -import { EsIndexActionConnector, EsIndexConfig, IndexActionParams } from '../types'; +import { ActionTypeModel, GenericValidationResult, ALERT_HISTORY_PREFIX } from '../../../../types'; +import { EsIndexConfig, IndexActionParams } from '../types'; export function getActionType(): ActionTypeModel { return { @@ -31,19 +26,6 @@ export function getActionType(): ActionTypeModel, unknown>> => { - const translations = await import('./translations'); - const configErrors = { - index: new Array(), - }; - const validationResult = { config: { errors: configErrors }, secrets: { errors: {} } }; - if (!action.config.index) { - configErrors.index.push(translations.INDEX_REQUIRED); - } - return validationResult; - }, actionConnectorFields: lazy(() => import('./es_index_connector')), actionParamsFields: lazy(() => import('./es_index_params')), validateParams: async ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx index bddc408026698..a44daf93e7ff9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx @@ -6,12 +6,14 @@ */ import React from 'react'; -import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; import { act } from 'react-dom/test-utils'; -import { EsIndexActionConnector } from '../types'; +import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; +import { screen, fireEvent, waitFor, render } from '@testing-library/react'; import IndexActionConnectorFields from './es_index_connector'; import { EuiComboBox, EuiSwitch, EuiSwitchEvent, EuiSelect } from '@elastic/eui'; -import { screen, render, fireEvent } from '@testing-library/react'; +import { AppMockRenderer, ConnectorFormTestProvider, createAppMockRenderer } from '../test_utils'; +import userEvent from '@testing-library/user-event'; + jest.mock('../../../../common/lib/kibana'); jest.mock('lodash', () => { @@ -23,7 +25,10 @@ jest.mock('lodash', () => { }); jest.mock('../../../../common/index_controls', () => ({ - firstFieldOption: jest.fn(), + firstFieldOption: { + text: 'Select a field', + value: '', + }, getFields: jest.fn(), getIndexOptions: jest.fn(), })); @@ -42,12 +47,22 @@ getIndexOptions.mockResolvedValueOnce([ const { getFields } = jest.requireMock('../../../../common/index_controls'); -async function setup(props: any) { - const wrapper = mountWithIntl(); +async function setup(actionConnector: any) { + const wrapper = mountWithIntl( + + {}} + /> + + ); + await act(async () => { await nextTick(); wrapper.update(); }); + return wrapper; } @@ -64,22 +79,16 @@ function setupGetFieldsResponse(getFieldsWithDateMapping: boolean) { ]); } -describe('IndexActionConnectorFields renders', () => { +describe('IndexActionConnectorFields', () => { test('renders correctly when creating connector', async () => { - const props = { - action: { - actionTypeId: '.index', - config: {}, - secrets: {}, - } as EsIndexActionConnector, - editActionConfig: () => {}, - editActionSecrets: () => {}, - errors: { index: [] }, - readOnly: false, - setCallbacks: () => {}, - isEdit: false, + const connector = { + actionTypeId: '.index', + config: {}, + secrets: {}, }; - const wrapper = mountWithIntl(); + + setupGetFieldsResponse(false); + const wrapper = await setup(connector); expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').exists()).toBeTruthy(); @@ -95,34 +104,41 @@ describe('IndexActionConnectorFields renders', () => { // time field switch should show up if index has date type field mapping setupGetFieldsResponse(true); await act(async () => { - indexComboBox.prop('onChange')!([{ label: 'selection' }]); + indexComboBox.prop('onChange')!([{ label: 'selection', value: 'selection' }]); await nextTick(); wrapper.update(); }); + + wrapper.update(); expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy(); + // time field switch should show up if index has date type field mapping // time field switch should go away if index does not has date type field mapping setupGetFieldsResponse(false); await act(async () => { - indexComboBox.prop('onChange')!([{ label: 'selection' }]); + indexComboBox.prop('onChange')!([{ label: 'selection', value: 'selection' }]); await nextTick(); wrapper.update(); }); + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeFalsy(); expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy(); // time field dropdown should show up if index has date type field mapping and time switch is clicked setupGetFieldsResponse(true); await act(async () => { - indexComboBox.prop('onChange')!([{ label: 'selection' }]); + indexComboBox.prop('onChange')!([{ label: 'selection', value: 'selection' }]); await nextTick(); wrapper.update(); }); + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeTruthy(); + const timeFieldSwitch = wrapper .find(EuiSwitch) .filter('[data-test-subj="hasTimeFieldCheckbox"]'); + await act(async () => { timeFieldSwitch.prop('onChange')!({ target: { checked: true }, @@ -130,26 +146,25 @@ describe('IndexActionConnectorFields renders', () => { await nextTick(); wrapper.update(); }); - expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeTruthy(); + + await waitFor(() => { + wrapper.update(); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeTruthy(); + }); }); test('renders correctly when editing connector - no date type field mapping', async () => { const indexName = 'index-no-date-fields'; const props = { - action: { - name: 'Index Connector for Index With No Date Type', - actionTypeId: '.index', - config: { - index: indexName, - refresh: false, - }, - secrets: {}, - } as EsIndexActionConnector, - editActionConfig: () => {}, - editActionSecrets: () => {}, - errors: { index: [] }, - readOnly: false, + name: 'Index Connector for Index With No Date Type', + actionTypeId: '.index', + config: { + index: indexName, + refresh: false, + }, + secrets: {}, }; + setupGetFieldsResponse(false); const wrapper = await setup(props); @@ -172,20 +187,15 @@ describe('IndexActionConnectorFields renders', () => { test('renders correctly when editing connector - refresh set to true', async () => { const indexName = 'index-no-date-fields'; const props = { - action: { - name: 'Index Connector for Index With No Date Type', - actionTypeId: '.index', - config: { - index: indexName, - refresh: true, - }, - secrets: {}, - } as EsIndexActionConnector, - editActionConfig: () => {}, - editActionSecrets: () => {}, - errors: { index: [] }, - readOnly: false, + name: 'Index Connector for Index With No Date Type', + actionTypeId: '.index', + config: { + index: indexName, + refresh: true, + }, + secrets: {}, }; + setupGetFieldsResponse(false); const wrapper = await setup(props); @@ -206,27 +216,24 @@ describe('IndexActionConnectorFields renders', () => { test('renders correctly when editing connector - with date type field mapping but no time field selected', async () => { const indexName = 'index-no-date-fields'; const props = { - action: { - name: 'Index Connector for Index With No Date Type', - actionTypeId: '.index', - config: { - index: indexName, - refresh: false, - }, - secrets: {}, - } as EsIndexActionConnector, - editActionConfig: () => {}, - editActionSecrets: () => {}, - errors: { index: [] }, - readOnly: false, + name: 'Index Connector for Index With No Date Type', + actionTypeId: '.index', + config: { + index: indexName, + refresh: false, + }, + secrets: {}, }; + setupGetFieldsResponse(true); const wrapper = await setup(props); - expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy(); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeFalsy(); + }); const indexComboBox = wrapper .find(EuiComboBox) @@ -245,28 +252,25 @@ describe('IndexActionConnectorFields renders', () => { test('renders correctly when editing connector - with date type field mapping and selected time field', async () => { const indexName = 'index-no-date-fields'; const props = { - action: { - name: 'Index Connector for Index With No Date Type', - actionTypeId: '.index', - config: { - index: indexName, - refresh: false, - executionTimeField: 'test1', - }, - secrets: {}, - } as EsIndexActionConnector, - editActionConfig: () => {}, - editActionSecrets: () => {}, - errors: { index: [] }, - readOnly: false, + name: 'Index Connector for Index With No Date Type', + actionTypeId: '.index', + config: { + index: indexName, + refresh: false, + executionTimeField: 'test1', + }, }; + setupGetFieldsResponse(true); const wrapper = await setup(props); - expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeTruthy(); + await waitFor(() => { + wrapper.update(); + expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="hasTimeFieldCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="executionTimeFieldSelect"]').exists()).toBeTruthy(); + }); const indexComboBox = wrapper .find(EuiComboBox) @@ -289,20 +293,23 @@ describe('IndexActionConnectorFields renders', () => { test('fetches index names on index combobox input change', async () => { const mockIndexName = 'test-index'; - const props = { - action: { - actionTypeId: '.index', - config: {}, - secrets: {}, - } as EsIndexActionConnector, - editActionConfig: () => {}, - editActionSecrets: () => {}, - errors: { index: [] }, - readOnly: false, - setCallbacks: () => {}, - isEdit: false, + const connector = { + actionTypeId: '.index', + name: 'index', + isDeprecated: false, + config: {}, + secrets: {}, }; - render(); + + render( + + {}} + /> + + ); const indexComboBox = await screen.findByTestId('connectorIndexesComboBox'); @@ -322,4 +329,134 @@ describe('IndexActionConnectorFields renders', () => { expect(screen.getByText('indexPattern1')).toBeInTheDocument(); expect(screen.getByText('indexPattern2')).toBeInTheDocument(); }); + + describe('Validation', () => { + let appMockRenderer: AppMockRenderer; + const onSubmit = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + appMockRenderer = createAppMockRenderer(); + }); + + test('connector validation succeeds when connector config is valid', async () => { + const actionConnector = { + secrets: {}, + id: 'test', + actionTypeId: '.index', + name: 'es_index', + config: { + index: 'test_es_index', + refresh: false, + executionTimeField: '1', + }, + isDeprecated: false, + }; + + const { getByTestId } = appMockRenderer.render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: { + actionTypeId: '.index', + config: { + index: 'test_es_index', + refresh: false, + executionTimeField: '1', + }, + id: 'test', + isDeprecated: false, + __internal__: { + hasTimeFieldCheckbox: true, + }, + name: 'es_index', + }, + isValid: true, + }); + }); + + test('connector validation succeeds when connector config is valid with minimal config', async () => { + const actionConnector = { + secrets: {}, + id: 'test', + actionTypeId: '.index', + name: 'es_index', + config: { + index: 'test_es_index', + }, + isDeprecated: false, + }; + + const { getByTestId } = appMockRenderer.render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: { + actionTypeId: '.index', + config: { + index: 'test_es_index', + refresh: false, + }, + id: 'test', + isDeprecated: false, + name: 'es_index', + }, + isValid: true, + }); + }); + + test('connector validation fails when index is empty', async () => { + const actionConnector = { + secrets: {}, + id: 'test', + actionTypeId: '.index', + name: 'es_index', + config: { + index: '', + }, + isDeprecated: false, + }; + + const { getByTestId } = appMockRenderer.render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: {}, + isValid: false, + }); + }); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx index 1e7259dd802d4..12fb79bd2845a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.tsx @@ -6,54 +6,95 @@ */ import React, { useState, useEffect } from 'react'; +import { debounce } from 'lodash'; import { EuiFormRow, - EuiSwitch, EuiSpacer, EuiComboBox, EuiComboBoxOptionOption, - EuiSelect, EuiTitle, EuiIconTip, EuiLink, } from '@elastic/eui'; +import { + FieldConfig, + getFieldValidityAndErrorMessage, + UseField, + useFormContext, + useFormData, + VALIDATION_TYPES, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; +import { ToggleField, SelectField } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import { DocLinksStart } from '@kbn/core/public'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { debounce } from 'lodash'; import { ActionConnectorFieldsProps } from '../../../../types'; -import { EsIndexActionConnector } from '../types'; import { getTimeFieldOptions } from '../../../../common/lib/get_time_options'; import { firstFieldOption, getFields, getIndexOptions } from '../../../../common/index_controls'; import { useKibana } from '../../../../common/lib/kibana'; +import * as translations from './translations'; interface TimeFieldOptions { value: string; text: string; } -const IndexActionConnectorFields: React.FunctionComponent< - ActionConnectorFieldsProps -> = ({ action, editActionConfig, errors, readOnly }) => { +const { indexPatternField, emptyField } = fieldValidators; + +const getIndexConfig = (docLinks: DocLinksStart): FieldConfig => ({ + label: translations.INDEX_LABEL, + helpText: ( + <> + + + + + + + ), + validations: [ + { + validator: emptyField(translations.INDEX_IS_NOT_VALID), + }, + { + validator: indexPatternField(i18n), + type: VALIDATION_TYPES.ARRAY_ITEM, + }, + ], +}); + +const IndexActionConnectorFields: React.FunctionComponent = ({ + readOnly, +}) => { const { http, docLinks } = useKibana().services; - const { index, refresh, executionTimeField } = action.config; - const [showTimeFieldCheckbox, setShowTimeFieldCheckboxState] = useState( - executionTimeField != null - ); - const [hasTimeFieldCheckbox, setHasTimeFieldCheckboxState] = useState( - executionTimeField != null - ); + const { getFieldDefaultValue } = useFormContext(); + const [{ config, __internal__ }] = useFormData({ + watch: ['config.executionTimeField', 'config.index', '__internal__.hasTimeFieldCheckbox'], + }); + + const { index = null } = config ?? {}; const [indexOptions, setIndexOptions] = useState([]); const [timeFieldOptions, setTimeFieldOptions] = useState([]); const [areIndiciesLoading, setAreIndicesLoading] = useState(false); + const hasTimeFieldCheckboxDefaultValue = !!getFieldDefaultValue( + 'config.executionTimeField' + ); + const showTimeFieldCheckbox = index != null && timeFieldOptions.length > 0; + const showTimeFieldSelect = __internal__ != null ? __internal__.hasTimeFieldCheckbox : false; + const setTimeFields = (fields: TimeFieldOptions[]) => { if (fields.length > 0) { - setShowTimeFieldCheckboxState(true); setTimeFieldOptions([firstFieldOption, ...fields]); } else { - setHasTimeFieldCheckboxState(false); - setShowTimeFieldCheckboxState(false); setTimeFieldOptions([]); } }; @@ -68,14 +109,13 @@ const IndexActionConnectorFields: React.FunctionComponent< const indexPatternsFunction = async () => { if (index) { const currentEsFields = await getFields(http!, [index]); - setTimeFields(getTimeFieldOptions(currentEsFields as any)); + if (Array.isArray(currentEsFields)) { + setTimeFields(getTimeFieldOptions(currentEsFields as any)); + } } }; indexPatternsFunction(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - const isIndexInvalid: boolean = - errors.index !== undefined && errors.index.length > 0 && index !== undefined; + }, [http, index]); return ( <> @@ -88,57 +128,13 @@ const IndexActionConnectorFields: React.FunctionComponent< - - } - isInvalid={isIndexInvalid} - error={errors.index} - helpText={ - <> - - - - - - - } - > - { - editActionConfig('index', selected.length > 0 ? selected[0].value : ''); - const indices = selected.map((s) => s.value as string); + + {(field) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + const onComboChange = async (options: EuiComboBoxOptionOption[]) => { + field.setValue(options.length > 0 ? options[0].value : ''); + const indices = options.map((s) => s.value as string); // reset time field and expression fields if indices are deleted if (indices.length === 0) { @@ -147,117 +143,149 @@ const IndexActionConnectorFields: React.FunctionComponent< } const currentEsFields = await getFields(http!, indices); setTimeFields(getTimeFieldOptions(currentEsFields as any)); - }} - onSearchChange={loadIndexOptions} - onBlur={() => { - if (!index) { - editActionConfig('index', ''); + }; + + const onSearchComboChange = (value: string) => { + if (value !== undefined) { + field.clearErrors(VALIDATION_TYPES.ARRAY_ITEM); } - }} - /> - + + loadIndexOptions(value); + }; + + return ( + + } + isInvalid={isInvalid} + error={errorMessage} + helpText={ + <> + + + + + + + } + > + + + ); + }} + - { - editActionConfig('refresh', e.target.checked); + + {' '} + + + ), + disabled: readOnly, + 'data-test-subj': 'indexRefreshCheckbox', + }, }} - label={ - <> - {' '} - - - } /> - {showTimeFieldCheckbox && ( - { - setHasTimeFieldCheckboxState(!hasTimeFieldCheckbox); - // if changing from checked to not checked (hasTimeField === true), - // set time field to null - if (hasTimeFieldCheckbox) { - editActionConfig('executionTimeField', null); - } + {showTimeFieldCheckbox ? ( + + + + + ), + disabled: readOnly, + 'data-test-subj': 'hasTimeFieldCheckbox', + }, }} - label={ - <> - - - - } /> - )} - {hasTimeFieldCheckbox && ( + ) : null} + {showTimeFieldSelect ? ( <> - - } - > - { - editActionConfig('executionTimeField', nullableString(e.target.value)); - }} - onBlur={() => { - if (executionTimeField === undefined) { - editActionConfig('executionTimeField', null); - } - }} - /> - + - )} + ) : null} ); }; -// if the string == null or is empty, return null, else return string -function nullableString(str: string | null | undefined) { - if (str == null || str.trim() === '') return null; - return str; -} - // eslint-disable-next-line import/no-default-export export { IndexActionConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/translations.ts index b7dd6ac749909..d86824fd1813f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/translations.ts @@ -7,10 +7,10 @@ import { i18n } from '@kbn/i18n'; -export const INDEX_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.error.requiredIndexText', +export const INDEX_IS_NOT_VALID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.error.notValidIndexText', { - defaultMessage: 'Index is required.', + defaultMessage: 'Index is not valid.', } ); @@ -27,3 +27,31 @@ export const HISTORY_NOT_VALID = i18n.translate( defaultMessage: 'Alert history index must contain valid suffix.', } ); + +export const EXECUTION_TIME_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.executionTimeFieldLabel', + { + defaultMessage: 'Time field', + } +); + +export const SHOW_TIME_FIELD_TOGGLE_TOOLTIP = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.definedateFieldTooltip', + { + defaultMessage: `Set this time field to the time the document was indexed.`, + } +); + +export const REFRESH_FIELD_TOGGLE_TOOLTIP = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.refreshTooltip', + { + defaultMessage: 'Refresh the affected shards to make this operation visible to search.', + } +); + +export const INDEX_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.indicesToQueryLabel', + { + defaultMessage: 'Index', + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx index 3bb023b135c40..07aa45cfe1925 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx @@ -8,7 +8,6 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel } from '../../../../types'; -import { JiraActionConnector } from './types'; import { registrationServicesMock } from '../../../../mocks'; const ACTION_TYPE_ID = '.jira'; @@ -29,68 +28,6 @@ describe('actionTypeRegistry.get() works', () => { }); }); -describe('jira connector validation', () => { - test('connector validation succeeds when connector config is valid', async () => { - const actionConnector = { - secrets: { - email: 'email', - apiToken: 'apiToken', - }, - id: 'test', - actionTypeId: '.jira', - name: 'jira', - isPreconfigured: false, - isDeprecated: false, - config: { - apiUrl: 'https://siem-kibana.atlassian.net', - projectKey: 'CK', - }, - } as JiraActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - apiUrl: [], - projectKey: [], - }, - }, - secrets: { - errors: { - apiToken: [], - email: [], - }, - }, - }); - }); - - test('connector validation fails when connector config is not valid', async () => { - const actionConnector = { - secrets: { - email: 'user', - }, - id: '.jira', - actionTypeId: '.jira', - name: 'jira', - config: {}, - } as unknown as JiraActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - apiUrl: ['URL is required.'], - projectKey: ['Project key is required'], - }, - }, - secrets: { - errors: { - apiToken: ['API token is required'], - email: [], - }, - }, - }); - }); -}); - describe('jira action params validation', () => { test('action params validation succeeds when action params is valid', async () => { const actionParams = { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx index 8e3424a16c295..627429a39b5b3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx @@ -7,58 +7,8 @@ import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; -import { - GenericValidationResult, - ActionTypeModel, - ConnectorValidationResult, -} from '../../../../types'; -import { JiraActionConnector, JiraConfig, JiraSecrets, JiraActionParams } from './types'; -import { isValidUrl } from '../../../lib/value_validators'; - -const validateConnector = async ( - action: JiraActionConnector -): Promise> => { - const translations = await import('./translations'); - const configErrors = { - apiUrl: new Array(), - projectKey: new Array(), - }; - const secretsErrors = { - email: new Array(), - apiToken: new Array(), - }; - - const validationResult = { - config: { errors: configErrors }, - secrets: { errors: secretsErrors }, - }; - - if (!action.config.apiUrl) { - configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_REQUIRED]; - } - - if (action.config.apiUrl) { - if (!isValidUrl(action.config.apiUrl)) { - configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_INVALID]; - } else if (!isValidUrl(action.config.apiUrl, 'https:')) { - configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_REQUIRE_HTTPS]; - } - } - - if (!action.config.projectKey) { - configErrors.projectKey = [...configErrors.projectKey, translations.JIRA_PROJECT_KEY_REQUIRED]; - } - - if (!action.secrets.email) { - secretsErrors.email = [...secretsErrors.email, translations.JIRA_EMAIL_REQUIRED]; - } - - if (!action.secrets.apiToken) { - secretsErrors.apiToken = [...secretsErrors.apiToken, translations.JIRA_API_TOKEN_REQUIRED]; - } - - return validationResult; -}; +import { GenericValidationResult, ActionTypeModel } from '../../../../types'; +import { JiraConfig, JiraSecrets, JiraActionParams } from './types'; export const JIRA_DESC = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.selectMessageText', @@ -80,7 +30,6 @@ export function getActionType(): ActionTypeModel import('./logo')), selectMessage: JIRA_DESC, actionTypeTitle: JIRA_TITLE, - validateConnector, actionConnectorFields: lazy(() => import('./jira_connectors')), validateParams: async ( actionParams: JiraActionParams diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx index 22d154373ea66..3afb0d16f8cbd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx @@ -8,168 +8,141 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import JiraConnectorFields from './jira_connectors'; -import { JiraActionConnector } from './types'; +import { ConnectorFormTestProvider } from '../test_utils'; +import { act, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + jest.mock('../../../../common/lib/kibana'); describe('JiraActionConnectorFields renders', () => { - test('alerting Jira connector fields are rendered', () => { + test('Jira connector fields are rendered', () => { const actionConnector = { - secrets: { - email: 'email', - apiToken: 'token', - }, - id: 'test', actionTypeId: '.jira', - isPreconfigured: false, - isDeprecated: false, name: 'jira', config: { - apiUrl: 'https://test/', + apiUrl: 'https://test.com', projectKey: 'CK', }, - } as JiraActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - - expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="connector-jira-project-key-form-input"]').length > 0 - ).toBeTruthy(); - - expect( - wrapper.find('[data-test-subj="connector-jira-email-form-input"]').length > 0 - ).toBeTruthy(); - - expect( - wrapper.find('[data-test-subj="connector-jira-apiToken-form-input"]').length > 0 - ).toBeTruthy(); - }); - - test('case specific Jira connector fields is rendered', () => { - const actionConnector = { secrets: { email: 'email', apiToken: 'token', }, - id: 'test', - actionTypeId: '.jira', - isPreconfigured: false, isDeprecated: false, - name: 'jira', - config: { - apiUrl: 'https://test/', - projectKey: 'CK', - }, - } as JiraActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - consumer={'case'} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="connector-jira-project-key-form-input"]').length > 0 - ).toBeTruthy(); - - expect( - wrapper.find('[data-test-subj="connector-jira-email-form-input"]').length > 0 - ).toBeTruthy(); - - expect( - wrapper.find('[data-test-subj="connector-jira-apiToken-form-input"]').length > 0 - ).toBeTruthy(); - }); + }; - test('should display a message on create to remember credentials', () => { - const actionConnector = { - actionTypeId: '.jira', - isPreconfigured: false, - isDeprecated: false, - secrets: {}, - config: {}, - } as JiraActionConnector; const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> + + {}} + /> + ); - expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); - }); - test('should display a message when secrets is missing', () => { - const actionConnector = { - actionTypeId: '.jira', - isPreconfigured: false, - isDeprecated: false, - isMissingSecrets: true, - secrets: {}, - config: {}, - } as JiraActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="config.apiUrl-input"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="config.projectKey-input"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="secrets.email-input"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="secrets.apiToken-input"]').length > 0).toBeTruthy(); }); - test('should display a message on edit to re-enter credentials', () => { - const actionConnector = { - secrets: { - email: 'email', - apiToken: 'token', - }, - id: 'test', - actionTypeId: '.jira', - isPreconfigured: false, - isDeprecated: false, - name: 'jira', - config: { - apiUrl: 'https://test/', - projectKey: 'CK', - }, - } as JiraActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); + describe('Validation', () => { + const onSubmit = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const tests: Array<[string, string]> = [ + ['config.apiUrl-input', 'not-valid'], + ['config.projectKey-input', ''], + ['secrets.email-input', ''], + ['secrets.apiToken-input', ''], + ]; + + it('connector validation succeeds when connector config is valid', async () => { + const actionConnector = { + actionTypeId: '.jira', + name: 'jira', + config: { + apiUrl: 'https://test.com', + projectKey: 'CK', + }, + secrets: { + email: 'email', + apiToken: 'token', + }, + isDeprecated: false, + }; + + const { getByTestId } = render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: { + actionTypeId: '.jira', + name: 'jira', + config: { + apiUrl: 'https://test.com', + projectKey: 'CK', + }, + secrets: { + email: 'email', + apiToken: 'token', + }, + isDeprecated: false, + }, + isValid: true, + }); + }); + + it.each(tests)('validates correctly %p', async (field, value) => { + const actionConnector = { + actionTypeId: '.jira', + name: 'jira', + config: { + apiUrl: 'https://test.com', + projectKey: 'CK', + }, + secrets: { + email: 'email', + apiToken: 'token', + }, + isDeprecated: false, + }; + + const res = render( + + {}} + /> + + ); + + await act(async () => { + await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, { + delay: 10, + }); + }); + + await act(async () => { + userEvent.click(res.getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }); + }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx index 7aec0a405d0d5..5d056281cd38a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx @@ -5,182 +5,34 @@ * 2.0. */ -import React, { useCallback } from 'react'; - -import { - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiFieldPassword, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; +import React from 'react'; import { ActionConnectorFieldsProps } from '../../../../types'; import * as i18n from './translations'; -import { JiraActionConnector } from './types'; -import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label'; - -const JiraConnectorFields: React.FC> = ({ - action, - editActionSecrets, - editActionConfig, - errors, - readOnly, -}) => { - const { apiUrl, projectKey } = action.config; - - const isApiUrlInvalid: boolean = - apiUrl !== undefined && errors.apiUrl !== undefined && errors.apiUrl.length > 0; - - const { email, apiToken } = action.secrets; - - const isProjectKeyInvalid: boolean = - projectKey !== undefined && errors.projectKey !== undefined && errors.projectKey.length > 0; - const isEmailInvalid: boolean = - email !== undefined && errors.email !== undefined && errors.email.length > 0; - const isApiTokenInvalid: boolean = - apiToken !== undefined && errors.apiToken !== undefined && errors.apiToken.length > 0; - - const handleOnChangeActionConfig = useCallback( - (key: string, value: string) => editActionConfig(key, value), - [editActionConfig] - ); - - const handleOnChangeSecretConfig = useCallback( - (key: string, value: string) => editActionSecrets(key, value), - [editActionSecrets] - ); - - const handleResetField = useCallback( - (checkValue, fieldName: string, actionField: 'config' | 'secrets') => { - if (!checkValue) { - if (actionField === 'config') { - handleOnChangeActionConfig(fieldName, ''); - } else { - handleOnChangeSecretConfig(fieldName, ''); - } - } - }, - [handleOnChangeActionConfig, handleOnChangeSecretConfig] - ); - +import { + ConfigFieldSchema, + SimpleConnectorForm, + SecretsFieldSchema, +} from '../../simple_connector_form'; + +const configFormSchema: ConfigFieldSchema[] = [ + { id: 'apiUrl', label: i18n.API_URL_LABEL, isUrlField: true }, + { id: 'projectKey', label: i18n.JIRA_PROJECT_KEY_LABEL }, +]; +const secretsFormSchema: SecretsFieldSchema[] = [ + { id: 'email', label: i18n.JIRA_EMAIL_LABEL }, + { id: 'apiToken', label: i18n.JIRA_API_TOKEN_LABEL, isPasswordField: true }, +]; + +const JiraConnectorFields: React.FC = ({ readOnly, isEdit }) => { return ( - <> - - - - handleOnChangeActionConfig('apiUrl', evt.target.value)} - onBlur={() => handleResetField(apiUrl, 'apiUrl', 'config')} - /> - - - - - - - - handleOnChangeActionConfig('projectKey', evt.target.value)} - onBlur={() => handleResetField(projectKey, 'projectKey', 'config')} - /> - - - - - - - -

{i18n.JIRA_AUTHENTICATION_LABEL}

-
-
-
- - - - - {getEncryptedFieldNotifyLabel( - !action.id, - 2, - action.isMissingSecrets ?? false, - i18n.JIRA_REENTER_VALUES_LABEL - )} - - - - - - - - handleOnChangeSecretConfig('email', evt.target.value)} - onBlur={() => handleResetField(email, 'email', 'secrets')} - /> - - - - - - - - handleOnChangeSecretConfig('apiToken', evt.target.value)} - onBlur={() => handleResetField(apiToken, 'apiToken', 'secrets')} - /> - - - - + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts index 1b9f3e2caa6ef..0a0fd0462456f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts @@ -14,27 +14,6 @@ export const API_URL_LABEL = i18n.translate( } ); -export const API_URL_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiUrlTextField', - { - defaultMessage: 'URL is required.', - } -); - -export const API_URL_INVALID = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.jira.invalidApiUrlTextField', - { - defaultMessage: 'URL is invalid.', - } -); - -export const API_URL_REQUIRE_HTTPS = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requireHttpsApiUrlTextField', - { - defaultMessage: 'URL must start with https://.', - } -); - export const JIRA_PROJECT_KEY_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.projectKey', { @@ -42,28 +21,6 @@ export const JIRA_PROJECT_KEY_LABEL = i18n.translate( } ); -export const JIRA_PROJECT_KEY_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredProjectKeyTextField', - { - defaultMessage: 'Project key is required', - } -); - -export const JIRA_AUTHENTICATION_LABEL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.jira.authenticationLabel', - { - defaultMessage: 'Authentication', - } -); - -export const JIRA_REENTER_VALUES_LABEL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.jira.reenterValuesLabel', - { - defaultMessage: - 'Authentication credentials are encrypted. Please reenter values for these fields.', - } -); - export const JIRA_EMAIL_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.emailTextFieldLabel', { @@ -71,13 +28,6 @@ export const JIRA_EMAIL_LABEL = i18n.translate( } ); -export const JIRA_EMAIL_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredEmailTextField', - { - defaultMessage: 'Email address is required', - } -); - export const JIRA_API_TOKEN_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.apiTokenTextFieldLabel', { @@ -85,27 +35,6 @@ export const JIRA_API_TOKEN_LABEL = i18n.translate( } ); -export const JIRA_API_TOKEN_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiTokenTextField', - { - defaultMessage: 'API token is required', - } -); - -export const MAPPING_FIELD_SUMMARY = i18n.translate( - 'xpack.triggersActionsUI.cases.configureCases.mappingFieldSummary', - { - defaultMessage: 'Summary', - } -); - -export const DESCRIPTION_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredDescriptionTextField', - { - defaultMessage: 'Description is required.', - } -); - export const SUMMARY_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredSummaryTextField', { @@ -113,20 +42,6 @@ export const SUMMARY_REQUIRED = i18n.translate( } ); -export const MAPPING_FIELD_DESC = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldDescription', - { - defaultMessage: 'Description', - } -); - -export const MAPPING_FIELD_COMMENTS = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldComments', - { - defaultMessage: 'Comments', - } -); - export const ISSUE_TYPES_API_ERROR = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssueTypesMessage', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx index a8274729506af..59e2eaa55c5bc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx @@ -8,7 +8,6 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel } from '../../../../types'; -import { PagerDutyActionConnector } from '../types'; import { registrationServicesMock } from '../../../../mocks'; const ACTION_TYPE_ID = '.pagerduty'; @@ -30,60 +29,6 @@ describe('actionTypeRegistry.get() works', () => { }); }); -describe('pagerduty connector validation', () => { - test('connector validation succeeds when connector config is valid', async () => { - const actionConnector = { - secrets: { - routingKey: 'test', - }, - id: 'test', - actionTypeId: '.pagerduty', - name: 'pagerduty', - config: { - apiUrl: 'http:\\test', - }, - } as PagerDutyActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - secrets: { - errors: { - routingKey: [], - }, - }, - }); - - delete actionConnector.config.apiUrl; - actionConnector.secrets.routingKey = 'test1'; - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - secrets: { - errors: { - routingKey: [], - }, - }, - }); - }); - - test('connector validation fails when connector config is not valid', async () => { - const actionConnector = { - secrets: {}, - id: 'test', - actionTypeId: '.pagerduty', - name: 'pagerduty', - config: { - apiUrl: 'http:\\test', - }, - } as PagerDutyActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - secrets: { - errors: { - routingKey: ['An integration key / routing key is required.'], - }, - }, - }); - }); -}); - describe('pagerduty action params validation', () => { test('action params validation succeeds when action params is valid', async () => { const actionParams = { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx index 42813186fe9f1..53b41ea49a274 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx @@ -8,13 +8,8 @@ import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; +import { ActionTypeModel, GenericValidationResult } from '../../../../types'; import { - ActionTypeModel, - GenericValidationResult, - ConnectorValidationResult, -} from '../../../../types'; -import { - PagerDutyActionConnector, PagerDutyConfig, PagerDutySecrets, PagerDutyActionParams, @@ -42,22 +37,6 @@ export function getActionType(): ActionTypeModel< defaultMessage: 'Send to PagerDuty', } ), - validateConnector: async ( - action: PagerDutyActionConnector - ): Promise> => { - const translations = await import('./translations'); - const secretsErrors = { - routingKey: new Array(), - }; - const validationResult = { - secrets: { errors: secretsErrors }, - }; - - if (!action.secrets.routingKey) { - secretsErrors.routingKey.push(translations.INTEGRATION_KEY_REQUIRED); - } - return validationResult; - }, validateParams: async ( actionParams: PagerDutyActionParams ): Promise< diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx index ee0b4f214cb5c..1ccd7fba702f6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx @@ -8,8 +8,11 @@ import React from 'react'; import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; import { act } from 'react-dom/test-utils'; -import { PagerDutyActionConnector } from '../types'; import PagerDutyActionConnectorFields from './pagerduty_connectors'; +import { ConnectorFormTestProvider } from '../test_utils'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + jest.mock('../../../../common/lib/kibana'); describe('PagerDutyActionConnectorFields renders', () => { @@ -22,20 +25,19 @@ describe('PagerDutyActionConnectorFields renders', () => { actionTypeId: '.pagerduty', name: 'pagerduty', config: { - apiUrl: 'http:\\test', + apiUrl: 'http://test.com', }, - } as PagerDutyActionConnector; + isDeprecated: false, + }; const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> + + {}} + /> + ); await act(async () => { @@ -45,83 +47,171 @@ describe('PagerDutyActionConnectorFields renders', () => { expect(wrapper.find('[data-test-subj="pagerdutyApiUrlInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="pagerdutyApiUrlInput"]').first().prop('value')).toBe( - 'http:\\test' + 'http://test.com' ); expect(wrapper.find('[data-test-subj="pagerdutyRoutingKeyInput"]').length > 0).toBeTruthy(); }); - test('should display a message on create to remember credentials', () => { - const actionConnector = { - actionTypeId: '.pagerduty', - secrets: {}, - config: {}, - } as PagerDutyActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); - }); + describe('Validation', () => { + const onSubmit = jest.fn(); - test('should display a message on edit to re-enter credentials', () => { - const actionConnector = { - secrets: { - routingKey: 'test', - }, - id: 'test', - actionTypeId: '.pagerduty', - name: 'pagerduty', - config: { - apiUrl: 'http:\\test', - }, - } as PagerDutyActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); - }); + beforeEach(() => { + jest.clearAllMocks(); + }); - test('should display a message for missing secrets after import', () => { - const actionConnector = { - secrets: { - routingKey: 'test', - }, - id: 'test', - actionTypeId: '.pagerduty', - isMissingSecrets: true, - name: 'pagerduty', - config: { - apiUrl: 'http:\\test', - }, - } as PagerDutyActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0); + it('connector validation succeeds when connector config is valid', async () => { + const actionConnector = { + secrets: { + routingKey: 'test', + }, + id: 'test', + actionTypeId: '.pagerduty', + name: 'pagerduty', + config: { + apiUrl: 'http://test.com', + }, + isDeprecated: false, + }; + + const res = render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(res.getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: { + secrets: { + routingKey: 'test', + }, + id: 'test', + actionTypeId: '.pagerduty', + name: 'pagerduty', + config: { + apiUrl: 'http://test.com', + }, + isDeprecated: false, + }, + isValid: true, + }); + }); + + it('validates correctly if the apiUrl is empty', async () => { + const actionConnector = { + secrets: { + routingKey: 'test', + }, + id: 'test', + actionTypeId: '.pagerduty', + name: 'pagerduty', + config: { + apiUrl: '', + }, + isDeprecated: false, + }; + + const res = render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(res.getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: { + secrets: { + routingKey: 'test', + }, + id: 'test', + actionTypeId: '.pagerduty', + name: 'pagerduty', + isDeprecated: false, + }, + isValid: true, + }); + }); + + it('validates correctly if the apiUrl is not empty and not a valid url', async () => { + const actionConnector = { + secrets: { + routingKey: 'test', + }, + id: 'test', + actionTypeId: '.pagerduty', + name: 'pagerduty', + config: { + apiUrl: 'not-valid', + }, + isDeprecated: false, + }; + + const res = render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(res.getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: {}, + isValid: false, + }); + }); + + it('validates correctly the routingKey', async () => { + const actionConnector = { + secrets: { + routingKey: '', + }, + id: 'test', + actionTypeId: '.pagerduty', + name: 'pagerduty', + config: { + apiUrl: 'not-valid', + }, + isDeprecated: false, + }; + + const res = render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(res.getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: {}, + isValid: false, + }); + }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx index 49aaec5bb6f24..21f7443b6b545 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx @@ -6,99 +6,88 @@ */ import React from 'react'; -import { EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import { EuiLink } from '@elastic/eui'; +import { isEmpty } from 'lodash'; import { FormattedMessage } from '@kbn/i18n-react'; +import { FieldConfig, UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; +import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import { DocLinksStart } from '@kbn/core/public'; import { ActionConnectorFieldsProps } from '../../../../types'; -import { PagerDutyActionConnector } from '../types'; import { useKibana } from '../../../../common/lib/kibana'; -import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label'; +import * as i18n from './translations'; -const PagerDutyActionConnectorFields: React.FunctionComponent< - ActionConnectorFieldsProps -> = ({ errors, action, editActionConfig, editActionSecrets, readOnly }) => { +const { emptyField, urlField } = fieldValidators; + +const getApiURLConfig = (): FieldConfig => ({ + label: i18n.API_URL_LABEL, + validations: [ + { + validator: (args) => { + const { value } = args; + /** + * The field is optional so if it is empty + * we do not validate + */ + if (isEmpty(value)) { + return; + } + + return urlField(i18n.API_URL_INVALID)(args); + }, + }, + ], +}); + +const getRoutingKeyConfig = (docLinks: DocLinksStart): FieldConfig => ({ + label: i18n.INTEGRATION_KEY_LABEL, + helpText: ( + + + + ), + validations: [ + { + validator: emptyField(i18n.INTEGRATION_KEY_REQUIRED), + }, + ], +}); + +const PagerDutyActionConnectorFields: React.FunctionComponent = ({ + readOnly, + isEdit, +}) => { const { docLinks } = useKibana().services; - const { apiUrl } = action.config; - const { routingKey } = action.secrets; - const isRoutingKeyInvalid: boolean = - routingKey !== undefined && errors.routingKey !== undefined && errors.routingKey.length > 0; return ( <> - - ) => { - editActionConfig('apiUrl', e.target.value); - }} - onBlur={() => { - if (!apiUrl) { - editActionConfig('apiUrl', ''); - } - }} - /> - - - - - } - error={errors.routingKey} - isInvalid={isRoutingKeyInvalid} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyTextFieldLabel', - { - defaultMessage: 'Integration key', - } - )} - > - <> - {getEncryptedFieldNotifyLabel( - !action.id, - 1, - action.isMissingSecrets ?? false, - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.reenterValueLabel', - { defaultMessage: 'This key is encrypted. Please reenter a value for this field.' } - ) - )} - ) => { - editActionSecrets('routingKey', e.target.value); - }} - onBlur={() => { - if (!routingKey) { - editActionSecrets('routingKey', ''); - } - }} - /> - - + + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/translations.ts index a907b19a1d733..22649354f20ff 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/translations.ts @@ -27,3 +27,24 @@ export const INTEGRATION_KEY_REQUIRED = i18n.translate( defaultMessage: 'An integration key / routing key is required.', } ); + +export const API_URL_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.apiUrlTextFieldLabel', + { + defaultMessage: 'API URL (optional)', + } +); + +export const API_URL_INVALID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.apiUrlInvalid', + { + defaultMessage: 'Invalid API URL', + } +); + +export const INTEGRATION_KEY_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.routingKeyTextFieldLabel', + { + defaultMessage: 'Integration key', + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx index 209606913dce6..c46bcd6a02c71 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx @@ -8,7 +8,6 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel } from '../../../../types'; -import { ResilientActionConnector } from './types'; import { registrationServicesMock } from '../../../../mocks'; const ACTION_TYPE_ID = '.resilient'; @@ -29,68 +28,6 @@ describe('actionTypeRegistry.get() works', () => { }); }); -describe('resilient connector validation', () => { - test('connector validation succeeds when connector config is valid', async () => { - const actionConnector = { - secrets: { - apiKeyId: 'email', - apiKeySecret: 'token', - }, - id: 'test', - actionTypeId: '.resilient', - isPreconfigured: false, - isDeprecated: false, - name: 'resilient', - config: { - apiUrl: 'https://test/', - orgId: '201', - }, - } as ResilientActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - apiUrl: [], - orgId: [], - }, - }, - secrets: { - errors: { - apiKeySecret: [], - apiKeyId: [], - }, - }, - }); - }); - - test('connector validation fails when connector config is not valid', async () => { - const actionConnector = { - secrets: { - apiKeyId: 'user', - }, - id: '.jira', - actionTypeId: '.jira', - name: 'jira', - config: {}, - } as unknown as ResilientActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - apiUrl: ['URL is required.'], - orgId: ['Organization ID is required'], - }, - }, - secrets: { - errors: { - apiKeySecret: ['Secret is required'], - apiKeyId: [], - }, - }, - }); - }); -}); - describe('resilient action params validation', () => { test('action params validation succeeds when action params is valid', async () => { const actionParams = { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx index f20204af17697..2297107e914cc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx @@ -7,66 +7,8 @@ import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; -import { - GenericValidationResult, - ActionTypeModel, - ConnectorValidationResult, -} from '../../../../types'; -import { - ResilientActionConnector, - ResilientConfig, - ResilientSecrets, - ResilientActionParams, -} from './types'; -import { isValidUrl } from '../../../lib/value_validators'; - -const validateConnector = async ( - action: ResilientActionConnector -): Promise> => { - const translations = await import('./translations'); - const configErrors = { - apiUrl: new Array(), - orgId: new Array(), - }; - const secretsErrors = { - apiKeyId: new Array(), - apiKeySecret: new Array(), - }; - - const validationResult = { - config: { errors: configErrors }, - secrets: { errors: secretsErrors }, - }; - - if (!action.config.apiUrl) { - configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_REQUIRED]; - } - - if (action.config.apiUrl) { - if (!isValidUrl(action.config.apiUrl)) { - configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_INVALID]; - } else if (!isValidUrl(action.config.apiUrl, 'https:')) { - configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_REQUIRE_HTTPS]; - } - } - - if (!action.config.orgId) { - configErrors.orgId = [...configErrors.orgId, translations.ORG_ID_REQUIRED]; - } - - if (!action.secrets.apiKeyId) { - secretsErrors.apiKeyId = [...secretsErrors.apiKeyId, translations.API_KEY_ID_REQUIRED]; - } - - if (!action.secrets.apiKeySecret) { - secretsErrors.apiKeySecret = [ - ...secretsErrors.apiKeySecret, - translations.API_KEY_SECRET_REQUIRED, - ]; - } - - return validationResult; -}; +import { GenericValidationResult, ActionTypeModel } from '../../../../types'; +import { ResilientConfig, ResilientSecrets, ResilientActionParams } from './types'; export const DESC = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.selectMessageText', @@ -92,7 +34,6 @@ export function getActionType(): ActionTypeModel< iconClass: lazy(() => import('./logo')), selectMessage: DESC, actionTypeTitle: TITLE, - validateConnector, actionConnectorFields: lazy(() => import('./resilient_connectors')), validateParams: async ( actionParams: ResilientActionParams diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx index 7db5cc113fcb9..0fbe63b28b166 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx @@ -8,169 +8,141 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import ResilientConnectorFields from './resilient_connectors'; -import { ResilientActionConnector } from './types'; +import { ConnectorFormTestProvider } from '../test_utils'; +import { act, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + jest.mock('../../../../common/lib/kibana'); describe('ResilientActionConnectorFields renders', () => { test('alerting Resilient connector fields are rendered', () => { const actionConnector = { - secrets: { - apiKeyId: 'key', - apiKeySecret: 'secret', - }, - id: 'test', actionTypeId: '.resilient', - isPreconfigured: false, - isDeprecated: false, name: 'resilient', config: { - apiUrl: 'https://test/', + apiUrl: 'https://test.com', orgId: '201', }, - } as ResilientActionConnector; + secrets: { + apiKeyId: 'key', + apiKeySecret: 'secret', + }, + isDeprecated: false, + }; + const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> + + {}} + /> + ); - expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="connector-resilient-orgId-form-input"]').length > 0 - ).toBeTruthy(); + expect(wrapper.find('[data-test-subj="config.apiUrl-input"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="config.orgId-input"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="secrets.apiKeyId-input"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="secrets.apiKeySecret-input"]').length > 0).toBeTruthy(); + }); - expect( - wrapper.find('[data-test-subj="connector-resilient-apiKeySecret-form-input"]').length > 0 - ).toBeTruthy(); + describe('Validation', () => { + const onSubmit = jest.fn(); - expect( - wrapper.find('[data-test-subj="connector-resilient-apiKeySecret-form-input"]').length > 0 - ).toBeTruthy(); - }); + beforeEach(() => { + jest.clearAllMocks(); + }); - test('case specific Resilient connector fields is rendered', () => { - const actionConnector = { - secrets: { - apiKeyId: 'email', - apiKeySecret: 'token', - }, - id: 'test', - actionTypeId: '.resilient', - isPreconfigured: false, - isDeprecated: false, - name: 'resilient', - config: { - apiUrl: 'https://test/', - orgId: '201', - }, - } as ResilientActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - consumer={'case'} - setCallbacks={() => {}} - isEdit={false} - /> - ); + const tests: Array<[string, string]> = [ + ['config.apiUrl-input', 'not-valid'], + ['config.orgId-input', ''], + ['secrets.apiKeyId-input', ''], + ['secrets.apiKeySecret-input', ''], + ]; - expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="connector-resilient-orgId-form-input"]').length > 0 - ).toBeTruthy(); + it('connector validation succeeds when connector config is valid', async () => { + const actionConnector = { + actionTypeId: '.resilient', + name: 'resilient', + config: { + apiUrl: 'https://test.com', + orgId: '201', + }, + secrets: { + apiKeyId: 'key', + apiKeySecret: 'secret', + }, + isDeprecated: false, + }; - expect( - wrapper.find('[data-test-subj="connector-resilient-apiKeySecret-form-input"]').length > 0 - ).toBeTruthy(); + const { getByTestId } = render( + + {}} + /> + + ); - expect( - wrapper.find('[data-test-subj="connector-resilient-apiKeySecret-form-input"]').length > 0 - ).toBeTruthy(); - }); + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); - test('should display a message on create to remember credentials', () => { - const actionConnector = { - actionTypeId: '.resilient', - isPreconfigured: false, - isDeprecated: false, - config: {}, - secrets: {}, - } as ResilientActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); - }); + expect(onSubmit).toBeCalledWith({ + data: { + actionTypeId: '.resilient', + name: 'resilient', + config: { + apiUrl: 'https://test.com', + orgId: '201', + }, + secrets: { + apiKeyId: 'key', + apiKeySecret: 'secret', + }, + isDeprecated: false, + }, + isValid: true, + }); + }); - test('should display a message for missing secrets after import', () => { - const actionConnector = { - actionTypeId: '.resilient', - isPreconfigured: false, - isDeprecated: false, - config: {}, - secrets: {}, - isMissingSecrets: true, - } as ResilientActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0); - }); + it.each(tests)('validates correctly %p', async (field, value) => { + const actionConnector = { + actionTypeId: '.resilient', + name: 'resilient', + config: { + apiUrl: 'https://test.com', + orgId: '201', + }, + secrets: { + apiKeyId: 'key', + apiKeySecret: 'secret', + }, + isDeprecated: false, + }; - test('should display a message on edit to re-enter credentials', () => { - const actionConnector = { - secrets: { - apiKeyId: 'key', - apiKeySecret: 'secret', - }, - id: 'test', - actionTypeId: '.resilient', - isPreconfigured: false, - isDeprecated: false, - name: 'resilient', - config: { - apiUrl: 'https://test/', - orgId: '201', - }, - } as ResilientActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); + const res = render( + + {}} + /> + + ); + + await act(async () => { + await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, { + delay: 10, + }); + }); + + await act(async () => { + userEvent.click(res.getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }); + }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx index 1270f19820f4c..6cc4d31f1b405 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx @@ -5,185 +5,33 @@ * 2.0. */ -import React, { useCallback } from 'react'; - -import { - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiFieldPassword, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; +import React from 'react'; import { ActionConnectorFieldsProps } from '../../../../types'; import * as i18n from './translations'; -import { ResilientActionConnector } from './types'; -import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label'; - -const ResilientConnectorFields: React.FC> = ({ - action, - editActionSecrets, - editActionConfig, - errors, - readOnly, -}) => { - const { apiUrl, orgId } = action.config; - const isApiUrlInvalid: boolean = - apiUrl !== undefined && errors.apiUrl !== undefined && errors.apiUrl.length > 0; - - const { apiKeyId, apiKeySecret } = action.secrets; - - const isOrgIdInvalid: boolean = - orgId !== undefined && errors.orgId !== undefined && errors.orgId.length > 0; - const isApiKeyInvalid: boolean = - apiKeyId !== undefined && errors.apiKeyId !== undefined && errors.apiKeyId.length > 0; - const isApiKeySecretInvalid: boolean = - apiKeySecret !== undefined && - errors.apiKeySecret !== undefined && - errors.apiKeySecret.length > 0; - - const handleOnChangeActionConfig = useCallback( - (key: string, value: string) => editActionConfig(key, value), - [editActionConfig] - ); - - const handleOnChangeSecretConfig = useCallback( - (key: string, value: string) => editActionSecrets(key, value), - [editActionSecrets] - ); - +import { + ConfigFieldSchema, + SecretsFieldSchema, + SimpleConnectorForm, +} from '../../simple_connector_form'; + +const configFormSchema: ConfigFieldSchema[] = [ + { id: 'apiUrl', label: i18n.API_URL_LABEL, isUrlField: true }, + { id: 'orgId', label: i18n.ORG_ID_LABEL }, +]; +const secretsFormSchema: SecretsFieldSchema[] = [ + { id: 'apiKeyId', label: i18n.API_KEY_ID_LABEL }, + { id: 'apiKeySecret', label: i18n.API_KEY_SECRET_LABEL, isPasswordField: true }, +]; + +const ResilientConnectorFields: React.FC = ({ readOnly, isEdit }) => { return ( - <> - - - - handleOnChangeActionConfig('apiUrl', evt.target.value)} - onBlur={() => { - if (!apiUrl) { - editActionConfig('apiUrl', ''); - } - }} - /> - - - - - - - - handleOnChangeActionConfig('orgId', evt.target.value)} - onBlur={() => { - if (!orgId) { - editActionConfig('orgId', ''); - } - }} - /> - - - - - - - -

{i18n.API_KEY_LABEL}

-
-
-
- - - - - {getEncryptedFieldNotifyLabel( - !action.id, - 2, - action.isMissingSecrets ?? false, - i18n.REENTER_VALUES_LABEL - )} - - - - - - - - handleOnChangeSecretConfig('apiKeyId', evt.target.value)} - onBlur={() => { - if (!apiKeyId) { - editActionSecrets('apiKeyId', ''); - } - }} - /> - - - - - - - - handleOnChangeSecretConfig('apiKeySecret', evt.target.value)} - onBlur={() => { - if (!apiKeySecret) { - editActionSecrets('apiKeySecret', ''); - } - }} - /> - - - - + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts index 6821c73b259f8..df4c15b0f322a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts @@ -14,27 +14,6 @@ export const API_URL_LABEL = i18n.translate( } ); -export const API_URL_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiUrlTextField', - { - defaultMessage: 'URL is required.', - } -); - -export const API_URL_INVALID = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.invalidApiUrlTextField', - { - defaultMessage: 'URL is invalid.', - } -); - -export const API_URL_REQUIRE_HTTPS = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requireHttpsApiUrlTextField', - { - defaultMessage: 'URL must start with https://.', - } -); - export const ORG_ID_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.orgId', { @@ -42,88 +21,17 @@ export const ORG_ID_LABEL = i18n.translate( } ); -export const ORG_ID_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredOrgIdTextField', - { - defaultMessage: 'Organization ID is required', - } -); - -export const API_KEY_LABEL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKey', - { - defaultMessage: 'API key', - } -); - -export const REMEMBER_VALUES_LABEL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.rememberValuesLabel', - { - defaultMessage: - 'Remember these values. You must reenter them each time you edit the connector.', - } -); - -export const REENTER_VALUES_LABEL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.reenterValuesLabel', - { - defaultMessage: 'ID and secret are encrypted. Please reenter values for these fields.', - } -); - export const API_KEY_ID_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeyId', { - defaultMessage: 'ID', - } -); - -export const API_KEY_ID_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeyIdTextField', - { - defaultMessage: 'ID is required', + defaultMessage: 'API Key ID', } ); export const API_KEY_SECRET_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeySecret', { - defaultMessage: 'Secret', - } -); - -export const API_KEY_SECRET_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeySecretTextField', - { - defaultMessage: 'Secret is required', - } -); - -export const MAPPING_FIELD_NAME = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldShortDescription', - { - defaultMessage: 'Name', - } -); - -export const MAPPING_FIELD_DESC = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldDescription', - { - defaultMessage: 'Description', - } -); - -export const MAPPING_FIELD_COMMENTS = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldComments', - { - defaultMessage: 'Comments', - } -); - -export const DESCRIPTION_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredDescriptionTextField', - { - defaultMessage: 'Description is required.', + defaultMessage: 'API Key Secret', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.test.tsx index 012a233973012..098c65f294560 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.test.tsx @@ -7,7 +7,7 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '..'; -import { ActionTypeModel, UserConfiguredActionConnector } from '../../../../types'; +import { ActionTypeModel } from '../../../../types'; import { registrationServicesMock } from '../../../../mocks'; const ACTION_TYPE_ID = '.server-log'; @@ -29,29 +29,6 @@ describe('actionTypeRegistry.get() works', () => { }); }); -describe('server-log connector validation', () => { - test('connector validation succeeds when connector config is valid', async () => { - const actionConnector: UserConfiguredActionConnector<{}, {}> = { - secrets: {}, - id: 'test', - actionTypeId: '.server-log', - name: 'server-log', - config: {}, - isPreconfigured: false, - isDeprecated: false, - }; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: {}, - }, - secrets: { - errors: {}, - }, - }); - }); -}); - describe('action params validation', () => { test('action params validation succeeds when action params is valid', async () => { const actionParams = { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.tsx index 066c5c0a2f385..4fe024f8861f0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log.tsx @@ -7,11 +7,7 @@ import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; -import { - ActionTypeModel, - GenericValidationResult, - ConnectorValidationResult, -} from '../../../../types'; +import { ActionTypeModel, GenericValidationResult } from '../../../../types'; import { ServerLogActionParams } from '../types'; export function getActionType(): ActionTypeModel { @@ -30,9 +26,6 @@ export function getActionType(): ActionTypeModel> => { - return Promise.resolve({ config: { errors: {} }, secrets: { errors: {} } }); - }, validateParams: ( actionParams: ServerLogActionParams ): Promise>> => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/application_required_callout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/application_required_callout.tsx index 462d68d50508d..9aefd7a2259b2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/application_required_callout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/application_required_callout.tsx @@ -25,7 +25,7 @@ const ERROR_MESSAGE = i18n.translate( ); interface Props { - appId: string; + appId?: string; message?: string | null; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/auth_types/credentials_auth.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/auth_types/credentials_auth.tsx index a9a7b7b0ebdb7..b996d8feac8e2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/auth_types/credentials_auth.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/auth_types/credentials_auth.tsx @@ -5,104 +5,52 @@ * 2.0. */ -import React, { memo, useCallback } from 'react'; -import { EuiFormRow, EuiFieldText, EuiFieldPassword } from '@elastic/eui'; -import type { ActionConnectorFieldsProps } from '../../../../../types'; +import React, { memo } from 'react'; +import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; +import { TextField } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import { PasswordField } from '../../../password_field'; import * as i18n from '../translations'; -import type { ServiceNowActionConnector } from '../types'; -import { isFieldInvalid } from '../helpers'; -import { getEncryptedFieldNotifyLabel } from '../../../get_encrypted_field_notify_label'; interface Props { - action: ActionConnectorFieldsProps['action']; - errors: ActionConnectorFieldsProps['errors']; readOnly: boolean; isLoading: boolean; - editActionSecrets: ActionConnectorFieldsProps['editActionSecrets']; + pathPrefix?: string; } -const NUMBER_OF_FIELDS = 2; - -const CredentialsAuthComponent: React.FC = ({ - action, - errors, - isLoading, - readOnly, - editActionSecrets, -}) => { - const { username, password } = action.secrets; - - const isUsernameInvalid = isFieldInvalid(username, errors.username); - const isPasswordInvalid = isFieldInvalid(password, errors.password); - - const onChangeUsernameEvent = useCallback( - (event?: React.ChangeEvent) => - editActionSecrets('username', event?.target.value ?? ''), - [editActionSecrets] - ); - - const onChangePasswordEvent = useCallback( - (event?: React.ChangeEvent) => - editActionSecrets('password', event?.target.value ?? ''), - [editActionSecrets] - ); +const { emptyField } = fieldValidators; +const CredentialsAuthComponent: React.FC = ({ isLoading, readOnly, pathPrefix = '' }) => { return ( <> - - {getEncryptedFieldNotifyLabel( - !action.id, - NUMBER_OF_FIELDS, - action.isMissingSecrets ?? false, - i18n.REENTER_VALUES_LABEL - )} - - - { - if (!username) { - onChangeUsernameEvent(); - } - }} - disabled={isLoading} - /> - - + - { - if (!password) { - onChangePasswordEvent(); - } - }} - disabled={isLoading} - /> - + readOnly={readOnly} + data-test-subj="connector-servicenow-password-form-input" + isLoading={isLoading} + disabled={readOnly || isLoading} + /> ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/auth_types/oauth.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/auth_types/oauth.tsx index f08759b675454..4c51641ea0bf1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/auth_types/oauth.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/auth_types/oauth.tsx @@ -5,224 +5,121 @@ * 2.0. */ -import React, { memo, useCallback } from 'react'; -import { EuiFormRow, EuiFieldText, EuiFieldPassword, EuiTextArea } from '@elastic/eui'; -import { getEncryptedFieldNotifyLabel } from '../../../get_encrypted_field_notify_label'; -import type { ActionConnectorFieldsProps } from '../../../../../types'; -import type { ServiceNowActionConnector } from '../types'; +import React, { memo } from 'react'; +import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; +import { TextAreaField, TextField } from '@kbn/es-ui-shared-plugin/static/forms/components'; import * as i18n from '../translations'; -import { isFieldInvalid } from '../helpers'; -import { PRIVATE_KEY_PASSWORD_HELPER_TEXT } from '../translations'; +import { PasswordField } from '../../../password_field'; interface Props { - action: ActionConnectorFieldsProps['action']; - errors: ActionConnectorFieldsProps['errors']; readOnly: boolean; isLoading: boolean; - editActionSecrets: ActionConnectorFieldsProps['editActionSecrets']; - editActionConfig: ActionConnectorFieldsProps['editActionConfig']; + pathPrefix?: string; } -const NUMBER_OF_FIELDS = 3; - -const OAuthComponent: React.FC = ({ - action, - errors, - isLoading, - readOnly, - editActionSecrets, - editActionConfig, -}) => { - const { clientId, userIdentifierValue, jwtKeyId } = action.config; - const { clientSecret, privateKey, privateKeyPassword } = action.secrets; - - const isClientIdInvalid = isFieldInvalid(clientId, errors.clientId); - const isUserIdentifierInvalid = isFieldInvalid(userIdentifierValue, errors.userIdentifierValue); - const isKeyIdInvalid = isFieldInvalid(jwtKeyId, errors.jwtKeyId); - const isClientSecretInvalid = isFieldInvalid(clientSecret, errors.clientSecret); - const isPrivateKeyInvalid = isFieldInvalid(privateKey, errors.privateKey); - - const onChangeClientIdEvent = useCallback( - (event?: React.ChangeEvent) => - editActionConfig('clientId', event?.target.value ?? ''), - [editActionConfig] - ); - const onChangeUserIdentifierInvalidEvent = useCallback( - (event?: React.ChangeEvent) => - editActionConfig('userIdentifierValue', event?.target.value ?? ''), - [editActionConfig] - ); - const onChangeJWTKeyIdEvent = useCallback( - (event?: React.ChangeEvent) => - editActionConfig('jwtKeyId', event?.target.value ?? ''), - [editActionConfig] - ); - - const onChangeClientSecretEvent = useCallback( - (event?: React.ChangeEvent) => - editActionSecrets('clientSecret', event?.target.value ?? ''), - [editActionSecrets] - ); - - const onChangePrivateKeyEvent = useCallback( - (event?: React.ChangeEvent) => - editActionSecrets('privateKey', event?.target.value ?? ''), - [editActionSecrets] - ); - - const onChangePrivateKeyPasswordEvent = useCallback( - (event?: React.ChangeEvent) => - editActionSecrets('privateKeyPassword', event?.target.value ?? ''), - [editActionSecrets] - ); +const { emptyField } = fieldValidators; +const OAuthComponent: React.FC = ({ isLoading, readOnly, pathPrefix = '' }) => { return ( <> - - {getEncryptedFieldNotifyLabel( - !action.id, - NUMBER_OF_FIELDS, - action.isMissingSecrets ?? false, - i18n.REENTER_VALUES_LABEL - )} - - - { - if (!clientId) { - onChangeClientIdEvent(); - } - }} - disabled={isLoading} - /> - - - { - if (!userIdentifierValue) { - onChangeUserIdentifierInvalidEvent(); - } - }} - disabled={isLoading} - /> - - - { - if (!jwtKeyId) { - onChangeJWTKeyIdEvent(); - } - }} - disabled={isLoading} - /> - - + + + - { - if (!clientSecret) { - onChangeClientSecretEvent(); - } - }} - disabled={isLoading} - /> - - - { - if (!privateKey) { - onChangePrivateKeyEvent(); - } - }} - disabled={isLoading} - /> - - + + - { - if (!privateKeyPassword) { - onChangePrivateKeyPasswordEvent(); - } - }} - disabled={isLoading} - /> - + helpText={i18n.PRIVATE_KEY_PASSWORD_HELPER_TEXT} + validate={false} + data-test-subj="connector-servicenow-private-key-password-form-input" + readOnly={readOnly} + isLoading={isLoading} + disabled={readOnly || isLoading} + /> ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/credentials.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/credentials.test.tsx index 81e7a720dff3b..d0b0b50b0ffc6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/credentials.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/credentials.test.tsx @@ -8,31 +8,29 @@ import React from 'react'; import { fireEvent, render, screen } from '@testing-library/react'; import { Credentials } from './credentials'; -import { ServiceNowActionConnector } from './types'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; -jest.mock('../../../../common/lib/kibana'); - -const editActionConfigMock = jest.fn(); -const editActionSecretsMock = jest.fn(); +import { ConnectorFormTestProvider } from '../test_utils'; -const basicAuthConnector: ServiceNowActionConnector = { - secrets: { username: 'test', password: 'test' }, - config: { isOAuth: false, apiUrl: 'https://example.com', usesTableApi: false }, -} as ServiceNowActionConnector; +jest.mock('../../../../common/lib/kibana'); describe('Credentials', () => { + const connector = { + id: 'test', + actionTypeId: '.servicenow', + isPreconfigured: false, + isDeprecated: true, + name: 'SN', + config: {}, + secrets: {}, + }; + it('renders basic auth form', async () => { render( - - {}} - editActionConfig={() => {}} - /> - + + + + + ); expect(screen.getByRole('switch')).toBeInTheDocument(); expect((await screen.findByRole('switch')).getAttribute('aria-checked')).toEqual('false'); @@ -50,24 +48,15 @@ describe('Credentials', () => { it('switches to oauth form', async () => { render( - - - + + + + + ); fireEvent.click(screen.getByRole('switch')); - expect(editActionConfigMock).toHaveBeenCalledWith('isOAuth', true); - expect(editActionSecretsMock).toHaveBeenCalledWith('username', null); - expect(editActionSecretsMock).toHaveBeenCalledWith('password', null); - expect((await screen.findByRole('switch')).getAttribute('aria-checked')).toEqual('true'); expect(screen.getByLabelText('ServiceNow instance URL')).toBeInTheDocument(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/credentials.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/credentials.tsx index 4073deca3981a..8953757ac2f9e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/credentials.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/credentials.tsx @@ -5,56 +5,21 @@ * 2.0. */ -import React, { memo, useState } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiTitle, - EuiSwitch, - EuiSwitchEvent, -} from '@elastic/eui'; -import { ActionConnectorFieldsProps } from '../../../../types'; +import React, { memo } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { ToggleField } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import * as i18n from './translations'; -import { ServiceNowActionConnector } from './types'; import { CredentialsApiUrl } from './credentials_api_url'; import { CredentialsAuth, OAuth } from './auth_types'; interface Props { - action: ActionConnectorFieldsProps['action']; - errors: ActionConnectorFieldsProps['errors']; + isOAuth: boolean; readOnly: boolean; isLoading: boolean; - editActionSecrets: ActionConnectorFieldsProps['editActionSecrets']; - editActionConfig: ActionConnectorFieldsProps['editActionConfig']; } -const CredentialsComponent: React.FC = ({ - action, - errors, - readOnly, - isLoading, - editActionSecrets, - editActionConfig, -}) => { - const [isOAuth, setIsOAuth] = useState(action.config.isOAuth); - - const switchIsOAuth = (e: EuiSwitchEvent) => { - setIsOAuth(e.target.checked); - editActionConfig('isOAuth', e.target.checked); - if (!e.target.checked) { - editActionConfig('clientId', null); - editActionConfig('userIdentifierValue', null); - editActionConfig('jwtKeyId', null); - editActionSecrets('clientSecret', null); - editActionSecrets('privateKey', null); - editActionSecrets('privateKeyPassword', null); - } else { - editActionSecrets('username', null); - editActionSecrets('password', null); - } - }; - +const CredentialsComponent: React.FC = ({ readOnly, isLoading, isOAuth }) => { return ( <> @@ -62,13 +27,7 @@ const CredentialsComponent: React.FC = ({

{i18n.SN_INSTANCE_LABEL}

- +
@@ -80,31 +39,24 @@ const CredentialsComponent: React.FC = ({
- {isOAuth ? ( - + ) : ( - + )} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/credentials_api_url.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/credentials_api_url.tsx index f6247bd12f61c..392b173080346 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/credentials_api_url.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/credentials_api_url.tsx @@ -5,40 +5,26 @@ * 2.0. */ -import React, { memo, useCallback } from 'react'; +import React, { memo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiFormRow, EuiLink, EuiFieldText, EuiSpacer } from '@elastic/eui'; +import { EuiFormRow, EuiLink, EuiSpacer } from '@elastic/eui'; +import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; +import { TextField } from '@kbn/es-ui-shared-plugin/static/forms/components'; + import { useKibana } from '../../../../common/lib/kibana'; -import type { ActionConnectorFieldsProps } from '../../../../types'; import * as i18n from './translations'; -import type { ServiceNowActionConnector } from './types'; -import { isFieldInvalid } from './helpers'; interface Props { - action: ActionConnectorFieldsProps['action']; - errors: ActionConnectorFieldsProps['errors']; readOnly: boolean; isLoading: boolean; - editActionConfig: ActionConnectorFieldsProps['editActionConfig']; + pathPrefix?: string; } -const CredentialsApiUrlComponent: React.FC = ({ - action, - errors, - isLoading, - readOnly, - editActionConfig, -}) => { - const { docLinks } = useKibana().services; - const { apiUrl } = action.config; - - const isApiUrlInvalid = isFieldInvalid(apiUrl, errors.apiUrl); +const { urlField } = fieldValidators; - const onChangeApiUrlEvent = useCallback( - (event?: React.ChangeEvent) => - editActionConfig('apiUrl', event?.target.value ?? ''), - [editActionConfig] - ); +const CredentialsApiUrlComponent: React.FC = ({ isLoading, readOnly, pathPrefix = '' }) => { + const { docLinks } = useKibana().services; return ( <> @@ -58,30 +44,26 @@ const CredentialsApiUrlComponent: React.FC = ({

- - { - if (!apiUrl) { - onChangeApiUrlEvent(); - } - }} - disabled={isLoading} - /> - + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts index 0f99c67128326..3798a312083e1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts @@ -16,8 +16,9 @@ export const DEFAULT_CORRELATION_ID = '{{rule.id}}:{{alert.id}}'; export const choicesToEuiOptions = (choices: Choice[]): EuiSelectOption[] => choices.map((choice) => ({ value: choice.value, text: choice.label })); -export const isRESTApiError = (res: AppInfo | RESTApiError): res is RESTApiError => - (res as RESTApiError).error != null || (res as RESTApiError).status === 'failure'; +export const isRESTApiError = (res: AppInfo | RESTApiError | undefined): res is RESTApiError => + res != null && + ((res as RESTApiError).error != null || (res as RESTApiError).status === 'failure'); export const isFieldInvalid = ( field: string | undefined | null, 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 index aa37e31100a1e..9e786a189ec42 100644 --- 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 @@ -8,7 +8,6 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel } from '../../../../types'; -import { ServiceNowActionConnector } from './types'; import { registrationServicesMock } from '../../../../mocks'; const SERVICENOW_ITSM_ACTION_TYPE_ID = '.servicenow'; @@ -34,196 +33,6 @@ describe('actionTypeRegistry.get() works', () => { }); }); -describe('servicenow connector validation', () => { - [ - SERVICENOW_ITSM_ACTION_TYPE_ID, - SERVICENOW_SIR_ACTION_TYPE_ID, - SERVICENOW_ITOM_ACTION_TYPE_ID, - ].forEach((id) => { - test(`${id}: connector validation succeeds when connector config is valid`, async () => { - const actionTypeModel = actionTypeRegistry.get(id); - const actionConnector = { - secrets: { - username: 'user', - password: 'pass', - }, - id: 'test', - actionTypeId: id, - name: 'ServiceNow', - isPreconfigured: false, - isDeprecated: false, - config: { - isOAuth: false, - apiUrl: 'https://dev94428.service-now.com/', - usesTableApi: false, - }, - } as ServiceNowActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - apiUrl: [], - clientId: [], - jwtKeyId: [], - userIdentifierValue: [], - usesTableApi: [], - }, - }, - secrets: { - errors: { - username: [], - password: [], - clientSecret: [], - privateKey: [], - }, - }, - }); - }); - - test(`${id}: connector validation fails when connector config is not valid`, async () => { - const actionTypeModel = actionTypeRegistry.get(id); - const actionConnector = { - secrets: { - username: 'user', - }, - id, - actionTypeId: id, - name: 'servicenow', - config: {}, - } as unknown as ServiceNowActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - apiUrl: ['URL is required.'], - usesTableApi: [], - clientId: [], - jwtKeyId: [], - userIdentifierValue: [], - }, - }, - secrets: { - errors: { - username: [], - password: ['Password is required.'], - clientSecret: [], - privateKey: [], - }, - }, - }); - }); - }); -}); - -describe('servicenow connector validation for OAuth', () => { - [ - SERVICENOW_ITSM_ACTION_TYPE_ID, - SERVICENOW_SIR_ACTION_TYPE_ID, - SERVICENOW_ITOM_ACTION_TYPE_ID, - ].forEach((id) => { - const mockConnector = ({ - actionTypeId = '', - clientSecret = 'clientSecret', - privateKey = 'privateKey', - privateKeyPassword = 'privateKeyPassword', - isOAuth = true, - apiUrl = 'https://dev94428.service-now.com/', - usesTableApi = false, - clientId = 'clientId', - jwtKeyId = 'jwtKeyId', - userIdentifierValue = 'userIdentifierValue', - }: { - actionTypeId?: string | null; - clientSecret?: string | null; - privateKey?: string | null; - privateKeyPassword?: string | null; - isOAuth?: boolean; - apiUrl?: string | null; - usesTableApi?: boolean | null; - clientId?: string | null; - jwtKeyId?: string | null; - userIdentifierValue?: string | null; - }) => - ({ - secrets: { - clientSecret, - privateKey, - privateKeyPassword, - }, - id, - actionTypeId, - name: 'servicenow', - config: { - isOAuth, - apiUrl, - usesTableApi, - clientId, - jwtKeyId, - userIdentifierValue, - }, - } as unknown as ServiceNowActionConnector); - - test(`${id}: valid OAuth Connector`, async () => { - const actionTypeModel = actionTypeRegistry.get(id); - const actionConnector = mockConnector({ actionTypeId: id }); - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - apiUrl: [], - usesTableApi: [], - clientId: [], - jwtKeyId: [], - userIdentifierValue: [], - }, - }, - secrets: { - errors: { - username: [], - password: [], - clientSecret: [], - privateKey: [], - }, - }, - }); - }); - - test(`${id}: has invalid fields`, async () => { - const actionTypeModel = actionTypeRegistry.get(id); - const actionConnector = mockConnector({ - actionTypeId: id, - apiUrl: null, - clientId: null, - jwtKeyId: null, - userIdentifierValue: null, - clientSecret: null, - privateKey: null, - privateKeyPassword: null, - }); - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - apiUrl: ['URL is required.'], - usesTableApi: [], - clientId: ['Client ID is required.'], - jwtKeyId: ['JWT Verifier Key ID is required.'], - userIdentifierValue: ['User Identifier is required.'], - }, - }, - secrets: { - errors: { - username: [], - password: [], - clientSecret: ['Client Secret is required.'], - privateKey: ['Private Key is required.'], - }, - }, - }); - }); - }); -}); - describe('servicenow action params validation', () => { [SERVICENOW_ITSM_ACTION_TYPE_ID, SERVICENOW_SIR_ACTION_TYPE_ID].forEach((id) => { test(`${id}: action params validation succeeds when action params is valid`, async () => { 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 index a5f0a65d90abe..85b2aea862848 100644 --- 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 @@ -7,20 +7,14 @@ import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; +import { ActionTypeModel, GenericValidationResult } from '../../../../types'; import { - ActionTypeModel, - ConnectorValidationResult, - GenericValidationResult, -} from '../../../../types'; -import { - ServiceNowActionConnector, ServiceNowConfig, ServiceNowITOMActionParams, ServiceNowITSMActionParams, ServiceNowSecrets, ServiceNowSIRActionParams, } from './types'; -import { isValidUrl } from '../../../lib/value_validators'; import { getConnectorDescriptiveTitle, getSelectedConnectorIcon } from './helpers'; export const SERVICENOW_ITOM_TITLE = i18n.translate( @@ -65,84 +59,6 @@ export const SERVICENOW_SIR_TITLE = i18n.translate( } ); -const validateConnector = async ( - action: ServiceNowActionConnector -): Promise< - ConnectorValidationResult< - Omit, - Omit - > -> => { - const translations = await import('./translations'); - - const configErrors = { - apiUrl: new Array(), - usesTableApi: new Array(), - clientId: new Array(), - userIdentifierValue: new Array(), - jwtKeyId: new Array(), - }; - - const secretsErrors = { - username: new Array(), - password: new Array(), - clientSecret: new Array(), - privateKey: new Array(), - }; - - if (!action.config.apiUrl) { - configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_REQUIRED]; - } - - if (action.config.apiUrl) { - if (!isValidUrl(action.config.apiUrl)) { - configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_INVALID]; - } else if (!isValidUrl(action.config.apiUrl, 'https:')) { - configErrors.apiUrl = [...configErrors.apiUrl, translations.API_URL_REQUIRE_HTTPS]; - } - } - - if (action.config.isOAuth) { - if (!action.config.clientId) { - configErrors.clientId = [...configErrors.clientId, translations.CLIENTID_REQUIRED]; - } - - if (!action.config.userIdentifierValue) { - configErrors.userIdentifierValue = [ - ...configErrors.userIdentifierValue, - translations.USER_EMAIL_REQUIRED, - ]; - } - - if (!action.config.jwtKeyId) { - configErrors.jwtKeyId = [...configErrors.jwtKeyId, translations.KEYID_REQUIRED]; - } - - if (!action.secrets.clientSecret) { - secretsErrors.clientSecret = [ - ...secretsErrors.clientSecret, - translations.CLIENTSECRET_REQUIRED, - ]; - } - - if (!action.secrets.privateKey) { - secretsErrors.privateKey = [...secretsErrors.privateKey, translations.PRIVATE_KEY_REQUIRED]; - } - } else { - if (!action.secrets.username) { - secretsErrors.username = [...secretsErrors.username, translations.USERNAME_REQUIRED]; - } - if (!action.secrets.password) { - secretsErrors.password = [...secretsErrors.password, translations.PASSWORD_REQUIRED]; - } - } - - return { - config: { errors: configErrors }, - secrets: { errors: secretsErrors }, - }; -}; - export function getServiceNowITSMActionType(): ActionTypeModel< ServiceNowConfig, ServiceNowSecrets, @@ -153,7 +69,6 @@ export function getServiceNowITSMActionType(): ActionTypeModel< iconClass: lazy(() => import('./logo')), selectMessage: SERVICENOW_ITSM_DESC, actionTypeTitle: SERVICENOW_ITSM_TITLE, - validateConnector, actionConnectorFields: lazy(() => import('./servicenow_connectors')), validateParams: async ( actionParams: ServiceNowITSMActionParams @@ -192,7 +107,6 @@ export function getServiceNowSIRActionType(): ActionTypeModel< iconClass: lazy(() => import('./logo')), selectMessage: SERVICENOW_SIR_DESC, actionTypeTitle: SERVICENOW_SIR_TITLE, - validateConnector, actionConnectorFields: lazy(() => import('./servicenow_connectors')), validateParams: async ( actionParams: ServiceNowSIRActionParams @@ -231,7 +145,6 @@ export function getServiceNowITOMActionType(): ActionTypeModel< iconClass: lazy(() => import('./logo')), selectMessage: SERVICENOW_ITOM_DESC, actionTypeTitle: SERVICENOW_ITOM_TITLE, - validateConnector, actionConnectorFields: lazy(() => import('./servicenow_connectors_no_app')), validateParams: async ( actionParams: ServiceNowITOMActionParams 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 index 75afe08bc87ca..f3ec5594144ec 100644 --- 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 @@ -6,15 +6,18 @@ */ import React from 'react'; -import { act } from '@testing-library/react'; +import { act, within } from '@testing-library/react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { render, act as reactAct } from '@testing-library/react'; +import { ConnectorValidationFunc } from '../../../../types'; import { useKibana } from '../../../../common/lib/kibana'; -import { ActionConnectorFieldsSetCallbacks } from '../../../../types'; import { updateActionConnector } from '../../../lib/action_connector_api'; import ServiceNowConnectorFields from './servicenow_connectors'; -import { ServiceNowActionConnector } from './types'; import { getAppInfo } from './api'; +import { ConnectorFormTestProvider } from '../test_utils'; +import { mount } from 'enzyme'; +import userEvent from '@testing-library/user-event'; jest.mock('../../../../common/lib/kibana'); jest.mock('../../../lib/action_connector_api'); @@ -26,41 +29,57 @@ const updateActionConnectorMock = updateActionConnector as jest.Mock; describe('ServiceNowActionConnectorFields renders', () => { const usesTableApiConnector = { - secrets: { - username: 'user', - password: 'pass', - }, id: 'test', actionTypeId: '.servicenow', - isPreconfigured: false, isDeprecated: true, name: 'SN', config: { - apiUrl: 'https://test/', + apiUrl: 'https://test.com', usesTableApi: true, }, - } as ServiceNowActionConnector; + secrets: { + username: 'user', + password: 'pass', + }, + }; const usesImportSetApiConnector = { ...usesTableApiConnector, isDeprecated: false, config: { ...usesTableApiConnector.config, + isOAuth: false, + usesTableApi: false, + }, + }; + + const usesImportSetApiConnectorOauth = { + ...usesTableApiConnector, + isDeprecated: false, + config: { + ...usesTableApiConnector.config, + isOAuth: true, usesTableApi: false, + clientId: 'test-id', + userIdentifierValue: 'email', + jwtKeyId: 'test-id', + }, + secrets: { + clientSecret: 'secret', + privateKey: 'secret-key', + privateKeyPassword: 'secret-pass', }, - } as ServiceNowActionConnector; + }; test('alerting servicenow connector fields are rendered', () => { const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> + + {}} + /> + ); expect( wrapper.find('[data-test-subj="connector-servicenow-username-form-input"]').length > 0 @@ -74,16 +93,13 @@ describe('ServiceNowActionConnectorFields renders', () => { test('case specific servicenow connector fields is rendered', () => { const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - consumer={'case'} - setCallbacks={() => {}} - isEdit={false} - /> + + {}} + /> + ); expect(wrapper.find('[data-test-subj="credentialsApiUrlFromInput"]').length > 0).toBeTruthy(); @@ -92,70 +108,6 @@ describe('ServiceNowActionConnectorFields renders', () => { ).toBeTruthy(); }); - test('should display a message on create to remember credentials', () => { - const actionConnector = { - actionTypeId: '.servicenow', - isPreconfigured: false, - isDeprecated: false, - config: {}, - secrets: {}, - } as ServiceNowActionConnector; - - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); - }); - - test('should display a message for missing secrets after import', () => { - const actionConnector = { - actionTypeId: '.servicenow', - isPreconfigured: false, - isDeprecated: false, - isMissingSecrets: true, - config: {}, - secrets: {}, - } as ServiceNowActionConnector; - - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0); - }); - - test('should display a message on edit to re-enter credentials', () => { - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); - }); - describe('Elastic certified ServiceNow application', () => { const { services } = useKibanaMock(); const applicationInfoData = { @@ -164,14 +116,11 @@ describe('ServiceNowActionConnectorFields renders', () => { version: '1.0.0', }; - let beforeActionConnectorSaveFn: () => Promise; - const setCallbacks = (({ - beforeActionConnectorSave, - }: { - beforeActionConnectorSave: () => Promise; - }) => { - beforeActionConnectorSaveFn = beforeActionConnectorSave; - }) as ActionConnectorFieldsSetCallbacks; + let preSubmitValidator: ConnectorValidationFunc; + + const registerPreSubmitValidator = (validator: ConnectorValidationFunc) => { + preSubmitValidator = validator; + }; beforeEach(() => { jest.clearAllMocks(); @@ -179,16 +128,15 @@ describe('ServiceNowActionConnectorFields renders', () => { test('should render the correct callouts when the connectors needs the application', () => { const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> + + + ); + expect(wrapper.find('[data-test-subj="snInstallationCallout"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="snDeprecatedCallout"]').exists()).toBeFalsy(); expect(wrapper.find('[data-test-subj="snApplicationCallout"]').exists()).toBeFalsy(); @@ -196,16 +144,15 @@ describe('ServiceNowActionConnectorFields renders', () => { test('should render the correct callouts if the connector uses the table API', () => { const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> + + + ); + expect(wrapper.find('[data-test-subj="snInstallationCallout"]').exists()).toBeFalsy(); expect(wrapper.find('[data-test-subj="snDeprecatedCallout"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="snApplicationCallout"]').exists()).toBeFalsy(); @@ -215,19 +162,17 @@ describe('ServiceNowActionConnectorFields renders', () => { getAppInfoMock.mockResolvedValue(applicationInfoData); const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={setCallbacks} - isEdit={false} - /> + + + ); await act(async () => { - await beforeActionConnectorSaveFn(); + await preSubmitValidator(); }); expect(getAppInfoMock).toHaveBeenCalledTimes(1); @@ -236,19 +181,17 @@ describe('ServiceNowActionConnectorFields renders', () => { test('should NOT get application information when the connector uses the old API', async () => { const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={setCallbacks} - isEdit={false} - /> + + + ); await act(async () => { - await beforeActionConnectorSaveFn(); + await preSubmitValidator(); }); expect(getAppInfoMock).toHaveBeenCalledTimes(0); @@ -256,110 +199,113 @@ describe('ServiceNowActionConnectorFields renders', () => { }); test('should render error when save failed', async () => { - expect.assertions(4); - const errorMessage = 'request failed'; getAppInfoMock.mockRejectedValueOnce(new Error(errorMessage)); - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={setCallbacks} - isEdit={false} - /> + mountWithIntl( + + + ); - await expect( - // The async is needed so the act will finished before asserting for the callout - async () => await act(async () => await beforeActionConnectorSaveFn()) - ).rejects.toThrow(errorMessage); - expect(getAppInfoMock).toHaveBeenCalledTimes(1); - - wrapper.update(); - expect(wrapper.find('[data-test-subj="snApplicationCallout"]').exists()).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="snApplicationCallout"]') - .first() - .text() - .includes(errorMessage) - ).toBeTruthy(); + await act(async () => { + const res = await preSubmitValidator(); + const messageWrapper = mount(<>{res?.message}); + + expect(getAppInfoMock).toHaveBeenCalledTimes(1); + expect( + messageWrapper.find('[data-test-subj="snApplicationCallout"]').exists() + ).toBeTruthy(); + + expect( + messageWrapper + .find('[data-test-subj="snApplicationCallout"]') + .first() + .text() + .includes(errorMessage) + ).toBeTruthy(); + }); }); test('should render error when the response is a REST api error', async () => { - expect.assertions(4); - const errorMessage = 'request failed'; getAppInfoMock.mockResolvedValue({ error: { message: errorMessage }, status: 'failure' }); - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={setCallbacks} - isEdit={false} - /> + mountWithIntl( + + + ); - await expect( - // The async is needed so the act will finished before asserting for the callout - async () => await act(async () => await beforeActionConnectorSaveFn()) - ).rejects.toThrow(errorMessage); - expect(getAppInfoMock).toHaveBeenCalledTimes(1); - - wrapper.update(); - expect(wrapper.find('[data-test-subj="snApplicationCallout"]').exists()).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="snApplicationCallout"]') - .first() - .text() - .includes(errorMessage) - ).toBeTruthy(); + await act(async () => { + const res = await preSubmitValidator(); + const messageWrapper = mount(<>{res?.message}); + + expect(getAppInfoMock).toHaveBeenCalledTimes(1); + expect( + messageWrapper.find('[data-test-subj="snApplicationCallout"]').exists() + ).toBeTruthy(); + + expect( + messageWrapper + .find('[data-test-subj="snApplicationCallout"]') + .first() + .text() + .includes(errorMessage) + ).toBeTruthy(); + }); }); - test('should migrate the deprecated connector when the application throws', async () => { + test('should migrate the deprecated connector correctly', async () => { getAppInfoMock.mockResolvedValue(applicationInfoData); - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={setCallbacks} - isEdit={false} - /> + updateActionConnectorMock.mockResolvedValue({ isDeprecated: false }); + + const { getByTestId, queryByTestId } = render( + + + ); - expect(wrapper.find('[data-test-subj="update-connector-btn"]').exists()).toBeTruthy(); - wrapper - .find('[data-test-subj="update-connector-btn"]') - .first() - .find('button') - .simulate('click'); - expect(wrapper.find('[data-test-subj="updateConnectorForm"]').exists()).toBeTruthy(); + await reactAct(async () => { + userEvent.click(getByTestId('update-connector-btn')); + }); - await act(async () => { - // Update the connector - wrapper.find('[data-test-subj="snUpdateInstallationSubmit"]').first().simulate('click'); + await reactAct(async () => { + const updateConnectorForm = getByTestId('updateConnectorForm'); + const urlInput = within(updateConnectorForm).getByTestId('credentialsApiUrlFromInput'); + const usernameInput = within(updateConnectorForm).getByTestId( + 'connector-servicenow-username-form-input' + ); + const passwordInput = within(updateConnectorForm).getByTestId( + 'connector-servicenow-password-form-input' + ); + + await userEvent.type(urlInput, 'https://example.com', { delay: 100 }); + await userEvent.type(usernameInput, 'user', { delay: 100 }); + await userEvent.type(passwordInput, 'pass', { delay: 100 }); + userEvent.click(within(updateConnectorForm).getByTestId('snUpdateInstallationSubmit')); }); expect(getAppInfoMock).toHaveBeenCalledTimes(1); expect(updateActionConnectorMock).toHaveBeenCalledWith( expect.objectContaining({ - id: usesTableApiConnector.id, connector: { - name: usesTableApiConnector.name, - config: { ...usesTableApiConnector.config, usesTableApi: false }, - secrets: usesTableApiConnector.secrets, + config: { apiUrl: 'https://example.com', usesTableApi: false }, + id: 'test', + name: 'SN', + secrets: { password: 'pass', username: 'user' }, }, }) ); @@ -369,100 +315,181 @@ describe('ServiceNowActionConnectorFields renders', () => { title: 'SN connector updated', }); - // The flyout is closed - wrapper.update(); - expect(wrapper.find('[data-test-subj="updateConnectorForm"]').exists()).toBeFalsy(); + expect(queryByTestId('updateConnectorForm')).toBe(null); + expect(queryByTestId('snDeprecatedCallout')).toBe(null); + expect(getByTestId('snInstallationCallout')).toBeInTheDocument(); }); test('should NOT migrate the deprecated connector when there is an error', async () => { const errorMessage = 'request failed'; getAppInfoMock.mockRejectedValueOnce(new Error(errorMessage)); - - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={setCallbacks} - isEdit={false} - /> + updateActionConnectorMock.mockResolvedValue({ isDeprecated: false }); + + const { getByTestId } = render( + + + ); - expect(wrapper.find('[data-test-subj="update-connector-btn"]').exists()).toBeTruthy(); - wrapper - .find('[data-test-subj="update-connector-btn"]') - .first() - .find('button') - .simulate('click'); - expect(wrapper.find('[data-test-subj="updateConnectorForm"]').exists()).toBeTruthy(); + await reactAct(async () => { + userEvent.click(getByTestId('update-connector-btn')); + }); - // The async is needed so the act will finished before asserting for the callout - await act(async () => { - wrapper.find('[data-test-subj="snUpdateInstallationSubmit"]').first().simulate('click'); + await reactAct(async () => { + const updateConnectorForm = getByTestId('updateConnectorForm'); + const urlInput = within(updateConnectorForm).getByTestId('credentialsApiUrlFromInput'); + const usernameInput = within(updateConnectorForm).getByTestId( + 'connector-servicenow-username-form-input' + ); + const passwordInput = within(updateConnectorForm).getByTestId( + 'connector-servicenow-password-form-input' + ); + + await userEvent.type(urlInput, 'https://example.com', { delay: 100 }); + await userEvent.type(usernameInput, 'user', { delay: 100 }); + await userEvent.type(passwordInput, 'pass', { delay: 100 }); + userEvent.click(within(updateConnectorForm).getByTestId('snUpdateInstallationSubmit')); }); expect(getAppInfoMock).toHaveBeenCalledTimes(1); expect(updateActionConnectorMock).not.toHaveBeenCalled(); - expect(services.notifications.toasts.addSuccess).not.toHaveBeenCalled(); + expect(getByTestId('updateConnectorForm')).toBeInTheDocument(); + expect( + within(getByTestId('updateConnectorForm')).getByTestId('snApplicationCallout') + ).toBeInTheDocument(); + }); + }); - // The flyout is still open - wrapper.update(); - expect(wrapper.find('[data-test-subj="updateConnectorForm"]').exists()).toBeTruthy(); + describe('Validation', () => { + const onSubmit = jest.fn(); - // The error message should be shown to the user - expect( - wrapper - .find('[data-test-subj="updateConnectorForm"] [data-test-subj="snApplicationCallout"]') - .exists() - ).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="updateConnectorForm"] [data-test-subj="snApplicationCallout"]') - .first() - .text() - .includes(errorMessage) - ).toBeTruthy(); + beforeEach(() => { + jest.clearAllMocks(); }); - test('should set the usesTableApi to false when creating a connector', async () => { - const newConnector = { ...usesTableApiConnector, config: {}, secrets: {} }; - const editActionConfig = jest.fn(); + const basicAuthTests: Array<[string, string]> = [ + ['credentialsApiUrlFromInput', 'not-valid'], + ['connector-servicenow-username-form-input', ''], + ['connector-servicenow-password-form-input', ''], + ]; + + const oauthTests: Array<[string, string]> = [ + ['credentialsApiUrlFromInput', 'not-valid'], + ['connector-servicenow-client-id-form-input', ''], + ['connector-servicenow-user-identifier-form-input', ''], + ['connector-servicenow-jwt-key-id-form-input', ''], + ['connector-servicenow-client-secret-form-input', ''], + ['connector-servicenow-private-key-form-input', ''], + ]; + + it.each([[usesImportSetApiConnector], [usesImportSetApiConnectorOauth]])( + 'connector validation succeeds when connector config is valid', + async (connector) => { + const { getByTestId } = render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ data: { ...connector }, isValid: true }); + } + ); - mountWithIntl( - {}} - readOnly={false} - setCallbacks={setCallbacks} - isEdit={false} - /> + it('submits if the private key password is empty', async () => { + const connector = { + ...usesImportSetApiConnectorOauth, + secrets: { + ...usesImportSetApiConnectorOauth.secrets, + clientSecret: 'secret', + privateKey: 'secret-key', + privateKeyPassword: '', + }, + }; + + const { getByTestId } = render( + + {}} + /> + ); - expect(editActionConfig).toHaveBeenCalledWith('usesTableApi', false); + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + const { + secrets: { clientSecret, privateKey }, + ...rest + } = connector; + + expect(onSubmit).toHaveBeenCalledWith({ + data: { ...rest, secrets: { clientSecret, privateKey } }, + isValid: true, + }); }); - test('it should set the legacy attribute if it is not undefined', async () => { - const editActionConfig = jest.fn(); + it.each(basicAuthTests)('validates correctly %p', async (field, value) => { + const res = render( + + {}} + /> + + ); - mountWithIntl( - {}} - readOnly={false} - setCallbacks={setCallbacks} - isEdit={false} - /> + await act(async () => { + await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, { + delay: 10, + }); + }); + + await act(async () => { + userEvent.click(res.getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }); + }); + + it.each(oauthTests)('validates correctly %p', async (field, value) => { + const res = render( + + {}} + /> + ); - expect(editActionConfig).not.toHaveBeenCalled(); + await act(async () => { + await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, { + delay: 10, + }); + }); + + await act(async () => { + userEvent.click(res.getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }); }); }); }); 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 index cc2756d178f11..54cb052c50212 100644 --- 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 @@ -5,157 +5,176 @@ * 2.0. */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { EuiSpacer } from '@elastic/eui'; import { snExternalServiceConfig } from '@kbn/actions-plugin/common'; -import { ActionConnectorFieldsProps } from '../../../../types'; +import { useFormContext, useFormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; -import * as i18n from './translations'; -import { ServiceNowActionConnector } from './types'; +import { ActionConnectorFieldsProps } from '../../../../types'; import { useKibana } from '../../../../common/lib/kibana'; import { DeprecatedCallout } from './deprecated_callout'; import { useGetAppInfo } from './use_get_app_info'; import { ApplicationRequiredCallout } from './application_required_callout'; import { isRESTApiError } from './helpers'; import { InstallationCallout } from './installation_callout'; -import { UpdateConnector } from './update_connector'; +import { UpdateConnector, UpdateConnectorFormSchema } from './update_connector'; import { updateActionConnector } from '../../../lib/action_connector_api'; import { Credentials } from './credentials'; +import * as i18n from './translations'; +import { ServiceNowActionConnector, ServiceNowConfig, ServiceNowSecrets } from './types'; +import { HiddenField } from '../../hidden_field'; +import { ConnectorFormSchema } from '../../../sections/action_connector_form/types'; // eslint-disable-next-line import/no-default-export export { ServiceNowConnectorFields as default }; -const ServiceNowConnectorFields: React.FC< - ActionConnectorFieldsProps -> = ({ action, editActionSecrets, editActionConfig, errors, readOnly, setCallbacks }) => { +const ServiceNowConnectorFields: React.FC = ({ + readOnly, + registerPreSubmitValidator, + isEdit, +}) => { const { http, notifications: { toasts }, } = useKibana().services; - const { config, secrets } = action; - const requiresNewApplication = !action.isDeprecated; + const { updateFieldValues } = useFormContext(); + const [{ id, isDeprecated, actionTypeId, name, config, secrets }] = useFormData< + ConnectorFormSchema + >({ + watch: [ + 'id', + 'isDeprecated', + 'actionTypeId', + 'name', + 'config.apiUrl', + 'config.isOAuth', + 'secrets.username', + 'secrets.password', + ], + }); - const [showUpdateConnector, setShowUpdateConnector] = useState(false); + const requiresNewApplication = isDeprecated != null ? !isDeprecated : true; + const { isOAuth = false } = config ?? {}; + const action = useMemo( + () => ({ + name, + actionTypeId, + config, + secrets, + }), + [name, actionTypeId, config, secrets] + ) as ServiceNowActionConnector; + + const [showUpdateConnector, setShowUpdateConnector] = useState(false); + const [updateErrorMessage, setUpdateErrorMessage] = useState(null); const { fetchAppInfo, isLoading } = useGetAppInfo({ - actionTypeId: action.actionTypeId, + actionTypeId, http, }); - const [showApplicationRequiredCallout, setShowApplicationRequiredCallout] = - useState(false); - const [applicationInfoErrorMsg, setApplicationInfoErrorMsg] = useState(null); - - const getApplicationInfo = useCallback(async () => { - setShowApplicationRequiredCallout(false); - setApplicationInfoErrorMsg(null); - - try { - const res = await fetchAppInfo(action); - if (isRESTApiError(res)) { - throw new Error(res.error?.message ?? i18n.UNKNOWN); + const getApplicationInfo = useCallback( + async (connector: ServiceNowActionConnector) => { + try { + const res = await fetchAppInfo(connector); + if (isRESTApiError(res)) { + throw new Error(res.error?.message ?? i18n.UNKNOWN); + } + + return res; + } catch (e) { + throw e; } + }, + [fetchAppInfo] + ); - return res; - } catch (e) { - setShowApplicationRequiredCallout(true); - setApplicationInfoErrorMsg(e.message); - // We need to throw here so the connector will be not be saved. - throw e; - } - }, [action, fetchAppInfo]); - - const beforeActionConnectorSave = useCallback(async () => { + const preSubmitValidator = useCallback(async () => { if (requiresNewApplication) { - await getApplicationInfo(); + try { + await getApplicationInfo(action); + } catch (error) { + return { + message: ( + + ), + }; + } } - }, [getApplicationInfo, requiresNewApplication]); + }, [action, actionTypeId, getApplicationInfo, requiresNewApplication]); useEffect( - () => setCallbacks({ beforeActionConnectorSave }), - [beforeActionConnectorSave, setCallbacks] + () => registerPreSubmitValidator(preSubmitValidator), + [preSubmitValidator, registerPreSubmitValidator] ); const onMigrateClick = useCallback(() => setShowUpdateConnector(true), []); const onModalCancel = useCallback(() => setShowUpdateConnector(false), []); - const onUpdateConnectorConfirm = useCallback(async () => { - try { - await getApplicationInfo(); - - await updateActionConnector({ - http, - connector: { - name: action.name, - config: { ...config, usesTableApi: false }, - secrets: { ...secrets }, - }, - id: action.id, - }); - - editActionConfig('usesTableApi', false); - setShowUpdateConnector(false); - - toasts.addSuccess({ - title: i18n.UPDATE_SUCCESS_TOAST_TITLE(action.name), - text: i18n.UPDATE_SUCCESS_TOAST_TEXT, - }); - } catch (err) { - /** - * getApplicationInfo may throw an error if the request - * fails or if there is a REST api error. - * - * We silent the errors as a callout will show and inform the user - */ - } - }, [getApplicationInfo, http, action.name, action.id, secrets, config, editActionConfig, toasts]); - - /** - * Defaults the usesTableApi attribute to false - * if it is not defined. The usesTableApi attribute - * will be undefined only at the creation of - * the connector. - */ - useEffect(() => { - if (config.usesTableApi == null) { - editActionConfig('usesTableApi', false); - } - }); + const onUpdateConnectorConfirm = useCallback( + async (updatedConnector: UpdateConnectorFormSchema['updatedConnector']) => { + const connectorToUpdate = { + name: name ?? '', + config: { ...updatedConnector.config, usesTableApi: false }, + secrets: { ...updatedConnector.secrets }, + id: id ?? '', + }; + + try { + await getApplicationInfo({ + ...connectorToUpdate, + isDeprecated, + isPreconfigured: false, + actionTypeId, + }); + + const res = await updateActionConnector({ + http, + connector: connectorToUpdate, + id: id ?? '', + }); + + toasts.addSuccess({ + title: i18n.UPDATE_SUCCESS_TOAST_TITLE(name ?? ''), + text: i18n.UPDATE_SUCCESS_TOAST_TEXT, + }); + + setShowUpdateConnector(false); + + updateFieldValues({ + isDeprecated: res.isDeprecated, + config: updatedConnector.config, + }); + } catch (err) { + setUpdateErrorMessage(err.message); + } + }, + [name, id, getApplicationInfo, isDeprecated, actionTypeId, http, updateFieldValues, toasts] + ); return ( <> - {showUpdateConnector && ( + {actionTypeId && showUpdateConnector && ( )} {requiresNewApplication && ( - + )} {!requiresNewApplication && } - - {showApplicationRequiredCallout && requiresNewApplication && ( - - )} + + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors_no_app.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors_no_app.test.tsx new file mode 100644 index 0000000000000..a0dda6edf76e0 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors_no_app.test.tsx @@ -0,0 +1,162 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { AppMockRenderer, ConnectorFormTestProvider, createAppMockRenderer } from '../test_utils'; +import ServiceNowConnectorFieldsNoApp from './servicenow_connectors_no_app'; + +describe('ServiceNowActionConnectorFields renders', () => { + const basicAuthConnector = { + id: 'test', + actionTypeId: '.servicenow', + isDeprecated: true, + name: 'SN', + config: { + apiUrl: 'https://test.com', + isOAuth: false, + usesTableApi: false, + }, + secrets: { + username: 'user', + password: 'pass', + }, + }; + + const oauthConnector = { + id: 'test', + actionTypeId: '.servicenow', + isDeprecated: true, + name: 'SN', + config: { + apiUrl: 'https://test.com', + isOAuth: true, + usesTableApi: false, + clientId: 'test-id', + userIdentifierValue: 'email', + jwtKeyId: 'test-id', + }, + secrets: { + clientSecret: 'secret', + privateKey: 'secret-key', + privateKeyPassword: 'secret-pass', + }, + }; + + let appMockRenderer: AppMockRenderer; + beforeEach(() => { + jest.clearAllMocks(); + appMockRenderer = createAppMockRenderer(); + }); + + it('renders a basic auth connector', () => { + const { getByTestId } = appMockRenderer.render( + + {}} + /> + + ); + + expect(getByTestId('credentialsApiUrlFromInput')).toBeInTheDocument(); + expect(getByTestId('connector-servicenow-username-form-input')).toBeInTheDocument(); + expect(getByTestId('connector-servicenow-password-form-input')).toBeInTheDocument(); + }); + + it('renders an oauth connector', () => { + const { getByTestId } = appMockRenderer.render( + + {}} + /> + + ); + + expect(getByTestId('credentialsApiUrlFromInput')).toBeInTheDocument(); + expect(getByTestId('connector-servicenow-client-id-form-input')).toBeInTheDocument(); + expect(getByTestId('connector-servicenow-user-identifier-form-input')).toBeInTheDocument(); + expect(getByTestId('connector-servicenow-jwt-key-id-form-input')).toBeInTheDocument(); + expect(getByTestId('connector-servicenow-client-secret-form-input')).toBeInTheDocument(); + expect(getByTestId('connector-servicenow-private-key-form-input')).toBeInTheDocument(); + }); + + describe('Validation', () => { + const onSubmit = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const basicAuthTests: Array<[string, string]> = [ + ['credentialsApiUrlFromInput', 'not-valid'], + ['connector-servicenow-username-form-input', ''], + ['connector-servicenow-password-form-input', ''], + ]; + + const oauthTests: Array<[string, string]> = [ + ['credentialsApiUrlFromInput', 'not-valid'], + ['connector-servicenow-client-id-form-input', ''], + ['connector-servicenow-user-identifier-form-input', ''], + ['connector-servicenow-jwt-key-id-form-input', ''], + ['connector-servicenow-client-secret-form-input', ''], + ['connector-servicenow-private-key-form-input', ''], + ]; + + it.each(basicAuthTests)('validates correctly %p', async (field, value) => { + const res = appMockRenderer.render( + + {}} + /> + + ); + + await act(async () => { + await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, { + delay: 10, + }); + }); + + await act(async () => { + userEvent.click(res.getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }); + }); + + it.each(oauthTests)('validates correctly %p', async (field, value) => { + const res = appMockRenderer.render( + + {}} + /> + + ); + + await act(async () => { + await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, { + delay: 10, + }); + }); + + await act(async () => { + userEvent.click(res.getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }); + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors_no_app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors_no_app.tsx index f49c2d34d3a8d..e4332f163151b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors_no_app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors_no_app.tsx @@ -6,27 +6,23 @@ */ import React from 'react'; +import { useFormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { ActionConnectorFieldsProps } from '../../../../types'; - -import { ServiceNowActionConnector } from './types'; import { Credentials } from './credentials'; +import { ConnectorFormSchema } from '../../../sections/action_connector_form/types'; +import { ServiceNowConfig, ServiceNowSecrets } from './types'; + +const ServiceNowConnectorFieldsNoApp: React.FC = ({ + isEdit, + readOnly, +}) => { + const [{ config }] = useFormData>({ + watch: ['config.isOAuth'], + }); + const { isOAuth = false } = config ?? {}; -const ServiceNowConnectorFieldsNoApp: React.FC< - ActionConnectorFieldsProps -> = ({ action, editActionSecrets, editActionConfig, errors, readOnly }) => { - return ( - <> - - - ); + return ; }; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/sn_store_button.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/sn_store_button.tsx index 3cbec513d179c..1fb58b24c68e9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/sn_store_button.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/sn_store_button.tsx @@ -14,11 +14,11 @@ const getStoreURL = (appId: string): string => `https://store.servicenow.com/sn_appstore_store.do#!/store/application/${appId}`; interface Props { - appId: string; + appId?: string; color: EuiButtonProps['color']; } -const SNStoreButtonComponent: React.FC = ({ color, appId }) => { +const SNStoreButtonComponent: React.FC = ({ color, appId = '' }) => { return ( = ({ color, appId }) => { export const SNStoreButton = memo(SNStoreButtonComponent); -const SNStoreLinkComponent: React.FC> = ({ appId }) => ( +const SNStoreLinkComponent: React.FC> = ({ appId = '' }) => ( {i18n.VISIT_SN_STORE} 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 index 77416287cfdcc..504e95636749b 100644 --- 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 @@ -14,20 +14,6 @@ export const API_URL_LABEL = i18n.translate( } ); -export const API_URL_HELPTEXT = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiUrlHelpText', - { - defaultMessage: 'Include the full 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', { @@ -35,13 +21,6 @@ export const API_URL_INVALID = i18n.translate( } ); -export const API_URL_REQUIRE_HTTPS = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requireHttpsApiUrlTextField', - { - defaultMessage: 'URL must start with https://.', - } -); - export const AUTHENTICATION_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.authenticationLabel', { @@ -49,13 +28,6 @@ export const AUTHENTICATION_LABEL = i18n.translate( } ); -export const REENTER_VALUES_LABEL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.reenterValuesLabel', - { - defaultMessage: 'You must authenticate each time you edit the connector.', - } -); - export const USERNAME_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.usernameTextFieldLabel', { @@ -77,13 +49,6 @@ export const PASSWORD_LABEL = i18n.translate( } ); -export const PASSWORD_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredPasswordTextField', - { - defaultMessage: 'Password is required.', - } -); - export const TITLE_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.common.requiredShortDescTextField', { @@ -181,13 +146,6 @@ export const API_INFO_ERROR = (status: number) => defaultMessage: 'Received status: {status} when attempting to get application information', }); -export const INSTALL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.install', - { - defaultMessage: 'install', - } -); - export const INSTALLATION_CALLOUT_TITLE = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.installationCalloutTitle', { @@ -350,7 +308,7 @@ export const KEY_ID_LABEL = i18n.translate( } ); -export const USER_EMAIL_LABEL = i18n.translate( +export const USER_IDENTIFIER_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.userEmailTextFieldLabel', { defaultMessage: 'User Identifier', @@ -392,27 +350,20 @@ export const PRIVATE_KEY_REQUIRED = i18n.translate( } ); -export const CLIENTSECRET_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredClientSecretTextField', +export const KEYID_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredKeyIdTextField', { - defaultMessage: 'Client Secret is required.', + defaultMessage: 'JWT Verifier Key ID is required.', } ); -export const USER_EMAIL_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredUserEmailTextField', +export const USER_IDENTIFIER_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredUserIdentifierTextField', { defaultMessage: 'User Identifier is required.', } ); -export const KEYID_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.requiredKeyIdTextField', - { - defaultMessage: 'JWT Verifier Key ID is required.', - } -); - export const IS_OAUTH = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.useOAuth', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.test.tsx index 53939ba5644b5..219ba68114852 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.test.tsx @@ -6,62 +6,21 @@ */ import React from 'react'; +import { I18nProvider } from '@kbn/i18n-react'; +import userEvent from '@testing-library/user-event'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { Props, UpdateConnector } from './update_connector'; -import { ServiceNowActionConnector } from './types'; -import { ActionConnectorFieldsProps } from '../../../../types'; +import { act } from 'react-dom/test-utils'; +import { render, act as reactAct } from '@testing-library/react'; jest.mock('../../../../common/lib/kibana'); -const actionConnectorBasicAuth: ServiceNowActionConnector = { - secrets: { - username: 'user', - password: 'pass', - }, - id: 'test', - actionTypeId: '.servicenow', - isPreconfigured: false, - isDeprecated: false, - name: 'servicenow', - config: { - apiUrl: 'https://test/', - usesTableApi: true, - isOAuth: false, - }, -}; - -const actionConnectorOAuth: ServiceNowActionConnector = { - secrets: { - clientSecret: 'clientSecret', - privateKey: 'privateKey', - privateKeyPassword: 'privateKeyPassword', - }, - id: 'test', - actionTypeId: '.servicenow', - isPreconfigured: false, - isDeprecated: false, - name: 'servicenow', - config: { - apiUrl: 'https://test/', - usesTableApi: true, - isOAuth: true, - clientId: 'cid', - userIdentifierValue: 'test@testuserIdentifierValue.com', - jwtKeyId: 'jwtKeyId', - }, -}; - -const mountUpdateConnector = ( - props: Partial = {}, - action: ActionConnectorFieldsProps['action'] = actionConnectorBasicAuth -) => { +const mountUpdateConnector = (props: Partial = {}, isOAuth: boolean = false) => { return mountWithIntl( {}} - editActionSecrets={() => {}} + actionTypeId=".servicenow" + isOAuth={isOAuth} + updateErrorMessage={null} readOnly={false} isLoading={false} onConfirm={() => {}} @@ -87,7 +46,7 @@ describe('UpdateConnector renders', () => { }); it('should render update connector fields for OAuth', () => { - const wrapper = mountUpdateConnector({}, actionConnectorOAuth); + const wrapper = mountUpdateConnector({}, true); expect( wrapper.find('[data-test-subj="connector-servicenow-client-id-form-input"]').exists() ).toBeTruthy(); @@ -114,8 +73,9 @@ describe('UpdateConnector renders', () => { ).toBeTruthy(); }); - it('should disable inputs on loading', () => { + it('should disable inputs on loading', async () => { const wrapper = mountUpdateConnector({ isLoading: true }); + expect( wrapper.find('[data-test-subj="credentialsApiUrlFromInput"]').first().prop('disabled') ).toBeTruthy(); @@ -134,7 +94,7 @@ describe('UpdateConnector renders', () => { }); it('should disable inputs on loading for OAuth', () => { - const wrapper = mountUpdateConnector({ isLoading: true }, actionConnectorOAuth); + const wrapper = mountUpdateConnector({ isLoading: true }, true); expect( wrapper @@ -199,7 +159,7 @@ describe('UpdateConnector renders', () => { }); it('should set inputs as read-only for OAuth', () => { - const wrapper = mountUpdateConnector({ readOnly: true }, actionConnectorOAuth); + const wrapper = mountUpdateConnector({ readOnly: true }, true); expect( wrapper @@ -243,116 +203,58 @@ describe('UpdateConnector renders', () => { ).toBeTruthy(); }); - it('should disable submit button if errors or fields missing', () => { - const wrapper = mountUpdateConnector( - { - errors: { apiUrl: ['some error'], username: [], password: [] }, - }, - actionConnectorBasicAuth - ); - - expect( - wrapper.find('[data-test-subj="snUpdateInstallationSubmit"]').first().prop('disabled') - ).toBeTruthy(); - - wrapper.setProps({ ...wrapper.props(), errors: { apiUrl: [], username: [], password: [] } }); - expect( - wrapper.find('[data-test-subj="snUpdateInstallationSubmit"]').first().prop('disabled') - ).toBeFalsy(); + it('should disable submit button on form errors', async () => { + const wrapper = mountUpdateConnector(); - wrapper.setProps({ - ...wrapper.props(), - action: { - ...actionConnectorBasicAuth, - secrets: { ...actionConnectorBasicAuth.secrets, username: undefined }, - }, + await act(async () => { + wrapper.find('[data-test-subj="snUpdateInstallationSubmit"]').first().simulate('click'); + wrapper.update(); }); + expect( wrapper.find('[data-test-subj="snUpdateInstallationSubmit"]').first().prop('disabled') ).toBeTruthy(); }); - it('should call editActionConfig when editing api url', () => { - const editActionConfig = jest.fn(); - const wrapper = mountUpdateConnector({ editActionConfig }); - - expect(editActionConfig).not.toHaveBeenCalled(); - wrapper - .find('input[data-test-subj="credentialsApiUrlFromInput"]') - .simulate('change', { target: { value: 'newUrl' } }); - expect(editActionConfig).toHaveBeenCalledWith('apiUrl', 'newUrl'); - }); - - it('should call editActionSecrets when editing username or password', () => { - const editActionSecrets = jest.fn(); - const wrapper = mountUpdateConnector({ editActionSecrets }); - - expect(editActionSecrets).not.toHaveBeenCalled(); - wrapper - .find('input[data-test-subj="connector-servicenow-username-form-input"]') - .simulate('change', { target: { value: 'new username' } }); - expect(editActionSecrets).toHaveBeenCalledWith('username', 'new username'); - - wrapper - .find('input[data-test-subj="connector-servicenow-password-form-input"]') - .simulate('change', { target: { value: 'new pass' } }); - - expect(editActionSecrets).toHaveBeenCalledTimes(2); - expect(editActionSecrets).toHaveBeenLastCalledWith('password', 'new pass'); - }); + it('should confirm the update when submit button clicked', async () => { + const onConfirm = jest.fn(); - it('should call editActionSecrets and/or editActionConfig when editing oAuth fields', () => { - const editActionSecrets = jest.fn(); - const editActionConfig = jest.fn(); - const wrapper = mountUpdateConnector( - { editActionSecrets, editActionConfig }, - actionConnectorOAuth + const { getByTestId } = render( + + {}} + /> + ); - expect(editActionSecrets).not.toHaveBeenCalled(); - - wrapper - .find('input[data-test-subj="connector-servicenow-client-id-form-input"]') - .simulate('change', { target: { value: 'new-value' } }); - expect(editActionConfig).toHaveBeenCalledWith('clientId', 'new-value'); - - wrapper - .find('input[data-test-subj="connector-servicenow-user-identifier-form-input"]') - .simulate('change', { target: { value: 'new-value' } }); - expect(editActionConfig).toHaveBeenCalledWith('userIdentifierValue', 'new-value'); - - wrapper - .find('input[data-test-subj="connector-servicenow-jwt-key-id-form-input"]') - .simulate('change', { target: { value: 'new-value' } }); - expect(editActionConfig).toHaveBeenCalledWith('jwtKeyId', 'new-value'); - - wrapper - .find('input[data-test-subj="connector-servicenow-client-secret-form-input"]') - .simulate('change', { target: { value: 'new-value' } }); - expect(editActionSecrets).toHaveBeenCalledWith('clientSecret', 'new-value'); - - wrapper - .find('textarea[data-test-subj="connector-servicenow-private-key-form-input"]') - .simulate('change', { target: { value: 'new-value' } }); - expect(editActionSecrets).toHaveBeenCalledWith('privateKey', 'new-value'); - - wrapper - .find('input[data-test-subj="connector-servicenow-private-key-password-form-input"]') - .simulate('change', { target: { value: 'new-value' } }); - expect(editActionSecrets).toHaveBeenCalledWith('privateKeyPassword', 'new-value'); - - expect(editActionConfig).toHaveBeenCalledTimes(3); - expect(editActionSecrets).toHaveBeenCalledTimes(3); - expect(editActionSecrets).toHaveBeenLastCalledWith('privateKeyPassword', 'new-value'); - }); + expect(onConfirm).not.toHaveBeenCalled(); - it('should confirm the update when submit button clicked', () => { - const onConfirm = jest.fn(); - const wrapper = mountUpdateConnector({ onConfirm }); + await reactAct(async () => { + const urlInput = getByTestId('credentialsApiUrlFromInput'); + const usernameInput = getByTestId('connector-servicenow-username-form-input'); + const passwordInput = getByTestId('connector-servicenow-password-form-input'); - expect(onConfirm).not.toHaveBeenCalled(); - wrapper.find('[data-test-subj="snUpdateInstallationSubmit"]').first().simulate('click'); - expect(onConfirm).toHaveBeenCalled(); + await userEvent.type(urlInput, 'https://example.com', { delay: 100 }); + await userEvent.type(usernameInput, 'user', { delay: 100 }); + await userEvent.type(passwordInput, 'pass', { delay: 100 }); + userEvent.click(getByTestId('snUpdateInstallationSubmit')); + }); + + expect(onConfirm).toHaveBeenCalledWith({ + config: { + apiUrl: 'https://example.com', + }, + secrets: { + password: 'pass', + username: 'user', + }, + }); }); it('should cancel the update when cancel button clicked', () => { @@ -365,14 +267,14 @@ describe('UpdateConnector renders', () => { }); it('should show error message if present', () => { - const applicationInfoErrorMsg = 'some application error'; + const updateErrorMessage = 'some application error'; const wrapper = mountUpdateConnector({ - applicationInfoErrorMsg, + updateErrorMessage, }); expect(wrapper.find('[data-test-subj="snApplicationCallout"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="snApplicationCallout"]').first().text()).toContain( - applicationInfoErrorMsg + updateErrorMessage ); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.tsx index d28ca1bdf2001..16363cd0b8e43 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo } from 'react'; +import React, { memo, useCallback } from 'react'; import { EuiButton, EuiButtonEmpty, @@ -22,13 +22,12 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { snExternalServiceConfig } from '@kbn/actions-plugin/common'; -import { ActionConnectorFieldsProps } from '../../../../types'; -import { ServiceNowActionConnector } from './types'; +import { useForm, Form } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { CredentialsApiUrl } from './credentials_api_url'; -import { isFieldInvalid } from './helpers'; -import { ApplicationRequiredCallout } from './application_required_callout'; -import { SNStoreLink } from './sn_store_button'; import { CredentialsAuth, OAuth } from './auth_types'; +import { SNStoreLink } from './sn_store_button'; +import { ApplicationRequiredCallout } from './application_required_callout'; +import { ServiceNowConfig, ServiceNowSecrets } from './types'; const title = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.updateFormTitle', @@ -78,167 +77,142 @@ const warningMessage = i18n.translate( defaultMessage: 'This updates all instances of this connector and cannot be reversed.', } ); +export interface UpdateConnectorFormSchema { + updatedConnector: { + config: ServiceNowConfig; + secrets: ServiceNowSecrets; + }; +} export interface Props { - action: ActionConnectorFieldsProps['action']; - applicationInfoErrorMsg: string | null; - errors: ActionConnectorFieldsProps['errors']; + actionTypeId: string; + isOAuth: boolean; isLoading: boolean; readOnly: boolean; - editActionSecrets: ActionConnectorFieldsProps['editActionSecrets']; - editActionConfig: ActionConnectorFieldsProps['editActionConfig']; + updateErrorMessage?: string | null; onCancel: () => void; - onConfirm: () => void; + onConfirm: (connector: UpdateConnectorFormSchema['updatedConnector']) => void; } +const PATH_PREFIX = 'updatedConnector.'; + const UpdateConnectorComponent: React.FC = ({ - action, - applicationInfoErrorMsg, - errors, + actionTypeId, + isOAuth, isLoading, readOnly, - editActionSecrets, - editActionConfig, onCancel, onConfirm, + updateErrorMessage, }) => { - const { apiUrl, isOAuth, jwtKeyId, userIdentifierValue, clientId } = action.config; - const { username, password, privateKeyPassword, privateKey, clientSecret } = action.secrets; + const { form } = useForm(); + const { submit, isValid } = form; - let hasErrorsOrEmptyFields; + const onSubmit = useCallback(async () => { + const { data, isValid: isSubmitValid } = await submit(); + if (!isSubmitValid) { + return; + } - hasErrorsOrEmptyFields = apiUrl === undefined || isFieldInvalid(apiUrl, errors.apiUrl); - - if (isOAuth) { - hasErrorsOrEmptyFields = - hasErrorsOrEmptyFields || - jwtKeyId === undefined || - userIdentifierValue === undefined || - clientId === undefined || - privateKeyPassword === undefined || - privateKey === undefined || - clientSecret === undefined || - isFieldInvalid(jwtKeyId, errors.apiUrl) || - isFieldInvalid(userIdentifierValue, errors.userIdentifierValue) || - isFieldInvalid(clientId, errors.clientId) || - isFieldInvalid(privateKeyPassword, errors.privateKeyPassword) || - isFieldInvalid(privateKey, errors.privateKey) || - isFieldInvalid(clientSecret, errors.clientSecret); - } else { - hasErrorsOrEmptyFields = - hasErrorsOrEmptyFields || - username === undefined || - password === undefined || - isFieldInvalid(username, errors.username) || - isFieldInvalid(password, errors.password); - } + const { updatedConnector } = data; + onConfirm(updatedConnector); + }, [onConfirm, submit]); return ( - - - -

{title}

-
-
- - } - > - - - ), - }} - /> - ), - }, - { - title: step2InstanceUrlTitle, - children: ( - - ), - }, - { - title: step3CredentialsTitle, - children: isOAuth ? ( - - ) : ( - - ), - }, - ]} - /> - - - - {applicationInfoErrorMsg && ( - - )} - - - - - - - - {cancelButtonText} - - - - + + + +

{title}

+
+
+ - {confirmButtonText} -
-
-
-
-
+ iconType="alert" + data-test-subj="snUpdateInstallationCallout" + title={warningMessage} + /> + } + > + + + ), + }} + /> + ), + }, + { + title: step2InstanceUrlTitle, + children: ( + + ), + }, + { + title: step3CredentialsTitle, + children: isOAuth ? ( + + ) : ( + + ), + }, + ]} + /> + + + + {updateErrorMessage != null ? ( + + ) : null} + + + + + + + + {cancelButtonText} + + + + + {confirmButtonText} + + + + + + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_get_app_info.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_get_app_info.tsx index 21a913b63641f..a086071a63487 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_get_app_info.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_get_app_info.tsx @@ -5,18 +5,21 @@ * 2.0. */ +import { isEmpty } from 'lodash'; import { useState, useEffect, useRef, useCallback } from 'react'; import { HttpStart } from '@kbn/core/public'; import { getAppInfo } from './api'; import { AppInfo, RESTApiError, ServiceNowActionConnector } from './types'; export interface UseGetAppInfoProps { - actionTypeId: string; + actionTypeId?: string; http: HttpStart; } export interface UseGetAppInfo { - fetchAppInfo: (connector: ServiceNowActionConnector) => Promise; + fetchAppInfo: ( + connector: ServiceNowActionConnector + ) => Promise; isLoading: boolean; } @@ -28,6 +31,10 @@ export const useGetAppInfo = ({ actionTypeId, http }: UseGetAppInfoProps): UseGe const fetchAppInfo = useCallback( async (connector) => { try { + if (!actionTypeId || isEmpty(actionTypeId)) { + return; + } + didCancel.current = false; abortCtrl.current.abort(); abortCtrl.current = new AbortController(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.test.tsx index 76a23ab94d972..45689fbd50264 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.test.tsx @@ -8,7 +8,6 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel } from '../../../../types'; -import { SlackActionConnector } from '../types'; import { registrationServicesMock } from '../../../../mocks'; const ACTION_TYPE_ID = '.slack'; @@ -30,98 +29,6 @@ describe('actionTypeRegistry.get() works', () => { }); }); -describe('slack connector validation', () => { - test('connector validation succeeds when connector config is valid', async () => { - const actionConnector = { - secrets: { - webhookUrl: 'https:\\test', - }, - id: 'test', - actionTypeId: '.slack', - name: 'slack', - config: {}, - } as SlackActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: {}, - }, - secrets: { - errors: { - webhookUrl: [], - }, - }, - }); - }); - - test('connector validation fails when connector config is not valid - no webhook url', async () => { - const actionConnector = { - secrets: {}, - id: 'test', - actionTypeId: '.slack', - name: 'slack', - config: {}, - } as SlackActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: {}, - }, - secrets: { - errors: { - webhookUrl: ['Webhook URL is required.'], - }, - }, - }); - }); - - test('connector validation fails when connector config is not valid - invalid webhook protocol', async () => { - const actionConnector = { - secrets: { - webhookUrl: 'http:\\test', - }, - id: 'test', - actionTypeId: '.slack', - name: 'slack', - config: {}, - } as SlackActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: {}, - }, - secrets: { - errors: { - webhookUrl: ['Webhook URL must start with https://.'], - }, - }, - }); - }); - - test('connector validation fails when connector config is not valid - invalid webhook url', async () => { - const actionConnector = { - secrets: { - webhookUrl: 'h', - }, - id: 'test', - actionTypeId: '.slack', - name: 'slack', - config: {}, - } as SlackActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: {}, - }, - secrets: { - errors: { - webhookUrl: ['Webhook URL is invalid.'], - }, - }, - }); - }); -}); - describe('slack action params validation', () => { test('if action params validation succeeds when action params is valid', async () => { const actionParams = { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.tsx index d3df034a90bf2..2252677084ba6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack.tsx @@ -7,13 +7,8 @@ import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; -import { - ActionTypeModel, - GenericValidationResult, - ConnectorValidationResult, -} from '../../../../types'; -import { SlackActionParams, SlackSecrets, SlackActionConnector } from '../types'; -import { isValidUrl } from '../../../lib/value_validators'; +import { ActionTypeModel, GenericValidationResult } from '../../../../types'; +import { SlackActionParams, SlackSecrets } from '../types'; export function getActionType(): ActionTypeModel { return { @@ -31,25 +26,6 @@ export function getActionType(): ActionTypeModel> => { - const translations = await import('./translations'); - const secretsErrors = { - webhookUrl: new Array(), - }; - const validationResult = { config: { errors: {} }, secrets: { errors: secretsErrors } }; - if (!action.secrets.webhookUrl) { - secretsErrors.webhookUrl.push(translations.WEBHOOK_URL_REQUIRED); - } else if (action.secrets.webhookUrl) { - if (!isValidUrl(action.secrets.webhookUrl)) { - secretsErrors.webhookUrl.push(translations.WEBHOOK_URL_INVALID); - } else if (!isValidUrl(action.secrets.webhookUrl, 'https:')) { - secretsErrors.webhookUrl.push(translations.WEBHOOK_URL_HTTP_INVALID); - } - } - return validationResult; - }, validateParams: async ( actionParams: SlackActionParams ): Promise> => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx index c0f6a17cae4fe..640bef0c22685 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx @@ -7,108 +7,127 @@ import React from 'react'; import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; -import { act } from '@testing-library/react'; -import { SlackActionConnector } from '../types'; +import { act, render } from '@testing-library/react'; import SlackActionFields from './slack_connectors'; +import { ConnectorFormTestProvider } from '../test_utils'; +import userEvent from '@testing-library/user-event'; + jest.mock('../../../../common/lib/kibana'); describe('SlackActionFields renders', () => { test('all connector fields is rendered', async () => { const actionConnector = { secrets: { - webhookUrl: 'http:\\test', + webhookUrl: 'http://test.com', }, id: 'test', - actionTypeId: '.email', - name: 'email', + actionTypeId: '.slack', + name: 'slack', config: {}, - } as SlackActionConnector; + isDeprecated: false, + }; + const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> + + {}} /> + ); await act(async () => { await nextTick(); wrapper.update(); }); + expect(wrapper.find('[data-test-subj="slackWebhookUrlInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="slackWebhookUrlInput"]').first().prop('value')).toBe( - 'http:\\test' + 'http://test.com' ); }); - test('should display a message on create to remember credentials', () => { - const actionConnector = { - actionTypeId: '.email', - config: {}, - secrets: {}, - } as SlackActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); - }); + describe('Validation', () => { + const onSubmit = jest.fn(); - test('should display a message for missing secrets after import', () => { - const actionConnector = { - actionTypeId: '.email', - isMissingSecrets: true, - config: {}, - secrets: {}, - } as SlackActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0); - }); + beforeEach(() => { + jest.clearAllMocks(); + }); - test('should display a message on edit to re-enter credentials', () => { - const actionConnector = { - secrets: { - webhookUrl: 'http:\\test', - }, - id: 'test', - actionTypeId: '.email', - name: 'email', - config: {}, - } as SlackActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); + it('connector validation succeeds when connector config is valid', async () => { + const actionConnector = { + secrets: { + webhookUrl: 'http://test.com', + }, + id: 'test', + actionTypeId: '.slack', + name: 'slack', + config: {}, + isDeprecated: false, + }; + + const { getByTestId } = render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: { + secrets: { + webhookUrl: 'http://test.com', + }, + id: 'test', + actionTypeId: '.slack', + name: 'slack', + isDeprecated: false, + }, + isValid: true, + }); + }); + + it('validates teh web hook url field correctly', async () => { + const actionConnector = { + secrets: { + webhookUrl: 'http://test.com', + }, + id: 'test', + actionTypeId: '.slack', + name: 'slack', + config: {}, + isDeprecated: false, + }; + + const { getByTestId } = render( + + {}} + /> + + ); + + await act(async () => { + await userEvent.type( + getByTestId('slackWebhookUrlInput'), + `{selectall}{backspace}no-valid`, + { + delay: 10, + } + ); + }); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }); + }); }); }); 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 39e05c2c09c7e..0725096c5a719 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 @@ -6,73 +6,55 @@ */ import React from 'react'; -import { EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import { EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { FieldConfig, UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; +import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import { DocLinksStart } from '@kbn/core/public'; + import { ActionConnectorFieldsProps } from '../../../../types'; -import { SlackActionConnector } from '../types'; import { useKibana } from '../../../../common/lib/kibana'; -import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label'; +import * as i18n from './translations'; + +const { urlField } = fieldValidators; + +const getWebhookUrlConfig = (docLinks: DocLinksStart): FieldConfig => ({ + label: i18n.WEBHOOK_URL_LABEL, + helpText: ( + + + + ), + validations: [ + { + validator: urlField(i18n.WEBHOOK_URL_INVALID), + }, + ], +}); -const SlackActionFields: React.FunctionComponent< - ActionConnectorFieldsProps -> = ({ action, editActionSecrets, errors, readOnly }) => { +const SlackActionFields: React.FunctionComponent = ({ + isEdit, + readOnly, +}) => { const { docLinks } = useKibana().services; - const { webhookUrl } = action.secrets; - const isWebhookUrlInvalid: boolean = - errors.webhookUrl !== undefined && errors.webhookUrl.length > 0 && webhookUrl !== undefined; return ( - <> - - - - } - error={errors.webhookUrl} - isInvalid={isWebhookUrlInvalid} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlTextFieldLabel', - { - defaultMessage: 'Webhook URL', - } - )} - > - <> - {getEncryptedFieldNotifyLabel( - !action.id, - 1, - action.isMissingSecrets ?? false, - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.reenterValueLabel', - { defaultMessage: 'This URL is encrypted. Please reenter a value for this field.' } - ) - )} - { - editActionSecrets('webhookUrl', e.target.value); - }} - onBlur={() => { - if (!webhookUrl) { - editActionSecrets('webhookUrl', ''); - } - }} - /> - - - + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/translations.ts index bd1fd8ea194f6..e7d37082b53ff 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/translations.ts @@ -7,13 +7,6 @@ import { i18n } from '@kbn/i18n'; -export const WEBHOOK_URL_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requiredWebhookUrlText', - { - defaultMessage: 'Webhook URL is required.', - } -); - export const WEBHOOK_URL_INVALID = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.invalidWebhookUrlText', { @@ -21,16 +14,16 @@ export const WEBHOOK_URL_INVALID = i18n.translate( } ); -export const WEBHOOK_URL_HTTP_INVALID = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.error.requireHttpsWebhookUrlText', +export const MESSAGE_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSlackMessageText', { - defaultMessage: 'Webhook URL must start with https://.', + defaultMessage: 'Message is required.', } ); -export const MESSAGE_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.error.requiredSlackMessageText', +export const WEBHOOK_URL_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.slackAction.webhookUrlTextFieldLabel', { - defaultMessage: 'Message is required.', + defaultMessage: 'Webhook URL', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts index 413b952675b8c..4384cebf42e5a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { isEmpty } from 'lodash'; import { SwimlaneConnectorType, SwimlaneMappingConfig, MappingConfigurationKeys } from './types'; import * as i18n from './translations'; @@ -18,7 +19,7 @@ const casesFields = [...casesRequiredFields]; const alertsRequiredFields: MappingConfigurationKeys[] = ['ruleNameConfig', 'alertIdConfig']; const alertsFields = ['severityConfig', 'commentsConfig', ...alertsRequiredFields]; -const translationMapping: Record = { +export const translationMapping: Record = { caseIdConfig: i18n.SW_REQUIRED_CASE_ID, alertIdConfig: i18n.SW_REQUIRED_ALERT_ID, caseNameConfig: i18n.SW_REQUIRED_CASE_NAME, @@ -29,16 +30,33 @@ const translationMapping: Record = { }; export const isValidFieldForConnector = ( - connector: SwimlaneConnectorType, - field: MappingConfigurationKeys + connectorType: SwimlaneConnectorType, + fieldId: MappingConfigurationKeys ): boolean => { - if (connector === SwimlaneConnectorType.All) { + if (connectorType === SwimlaneConnectorType.All) { + return true; + } + + return connectorType === SwimlaneConnectorType.Alerts + ? alertsFields.includes(fieldId) + : casesFields.includes(fieldId); +}; + +export const isRequiredField = ( + connectorType: SwimlaneConnectorType, + fieldId: MappingConfigurationKeys | undefined +) => { + if (connectorType === SwimlaneConnectorType.All) { + return false; + } + + if (fieldId == null || isEmpty(fieldId)) { return true; } - return connector === SwimlaneConnectorType.Alerts - ? alertsFields.includes(field) - : casesFields.includes(field); + return connectorType === SwimlaneConnectorType.Alerts + ? alertsFields.includes(fieldId) + : casesFields.includes(fieldId); }; export const validateMappingForConnector = ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx index 823883b3a5b60..1dbb2f21a4fb8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx @@ -4,115 +4,69 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { - EuiCallOut, - EuiFieldText, - EuiFormRow, - EuiLink, - EuiSpacer, - EuiText, - EuiFieldPassword, -} from '@elastic/eui'; -import React, { useCallback } from 'react'; + +import React from 'react'; +import { EuiLink } from '@elastic/eui'; +import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { TextField } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; import { FormattedMessage } from '@kbn/i18n-react'; -import * as i18n from '../translations'; +import { PasswordField } from '../../../password_field'; import { useKibana } from '../../../../../common/lib/kibana'; -import { SwimlaneActionConnector } from '../types'; -import { IErrorObject } from '../../../../../types'; +import * as i18n from '../translations'; interface Props { - action: SwimlaneActionConnector; - editActionConfig: (property: string, value: any) => void; - editActionSecrets: (property: string, value: any) => void; - errors: IErrorObject; readOnly: boolean; } -const SwimlaneConnectionComponent: React.FunctionComponent = ({ - action, - editActionConfig, - editActionSecrets, - errors, - readOnly, -}) => { - const { apiUrl, appId } = action.config; - const { apiToken } = action.secrets; - const { docLinks } = useKibana().services; - - const onChangeConfig = useCallback( - (e: React.ChangeEvent, key: 'apiUrl' | 'appId') => { - editActionConfig(key, e.target.value); - }, - [editActionConfig] - ); - - const onBlurConfig = useCallback( - (key: 'apiUrl' | 'appId') => { - if (!action.config[key]) { - editActionConfig(key, ''); - } - }, - [action.config, editActionConfig] - ); - - const onChangeSecrets = useCallback( - (e: React.ChangeEvent) => { - editActionSecrets('apiToken', e.target.value); - }, - [editActionSecrets] - ); - - const onBlurSecrets = useCallback(() => { - if (!apiToken) { - editActionSecrets('apiToken', ''); - } - }, [apiToken, editActionSecrets]); - - const isApiUrlInvalid = errors.apiUrl?.length > 0 && apiToken !== undefined; - const isAppIdInvalid = errors.appId?.length > 0 && apiToken !== undefined; - const isApiTokenInvalid = errors.apiToken?.length > 0 && apiToken !== undefined; +const { emptyField, urlField } = fieldValidators; +const SwimlaneConnectionComponent: React.FunctionComponent = ({ readOnly }) => { + const { docLinks } = useKibana().services; return ( <> - - onChangeConfig(e, 'apiUrl')} - onBlur={() => onBlurConfig('apiUrl')} - /> - - - onChangeConfig(e, 'appId')} - onBlur={() => onBlurConfig('appId')} - /> - - + + = ({ /> } - error={errors.apiToken} - isInvalid={isApiTokenInvalid} - label={i18n.SW_API_TOKEN_TEXT_FIELD_LABEL} - > - <> - {!action.id ? ( - <> - - - {i18n.SW_REMEMBER_VALUE_LABEL} - - - - ) : ( - <> - - - - - )} - - - + data-test-subj="swimlaneApiTokenInput" + /> ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx index 707687f5b6716..10afa07c65a16 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx @@ -5,17 +5,25 @@ * 2.0. */ -import React, { useMemo, useCallback, useEffect, useRef } from 'react'; -import { EuiFormRow, EuiComboBox, EuiComboBoxOptionOption, EuiButtonGroup } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { EuiComboBox, EuiComboBoxOptionOption, EuiComboBoxProps, EuiFormRow } from '@elastic/eui'; +import { + FieldConfig, + getFieldValidityAndErrorMessage, + UseField, + useFormData, + VALIDATION_TYPES, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { ComboBoxField } from '@kbn/es-ui-shared-plugin/static/forms/components'; + import * as i18n from '../translations'; import { - SwimlaneActionConnector, + MappingConfigurationKeys, SwimlaneConnectorType, SwimlaneFieldMappingConfig, - SwimlaneMappingConfig, } from '../types'; -import { IErrorObject } from '../../../../../types'; -import { isValidFieldForConnector } from '../helpers'; +import { isRequiredField, isValidFieldForConnector } from '../helpers'; +import { ButtonGroupField } from '../../../button_group_field'; const SINGLE_SELECTION = { asPlainText: true }; const EMPTY_COMBO_BOX_ARRAY: Array> | undefined = []; @@ -29,11 +37,8 @@ const createSelectedOption = (field: SwimlaneFieldMappingConfig | null | undefin field != null ? [formatOption(field)] : EMPTY_COMBO_BOX_ARRAY; interface Props { - action: SwimlaneActionConnector; - editActionConfig: (property: string, value: any) => void; - updateCurrentStep: (step: number) => void; + readOnly: boolean; fields: SwimlaneFieldMappingConfig[]; - errors: IErrorObject; } const connectorTypeButtons = [ @@ -42,16 +47,107 @@ const connectorTypeButtons = [ { id: SwimlaneConnectorType.Cases, label: 'Cases' }, ]; -const SwimlaneFieldsComponent: React.FC = ({ - action, - editActionConfig, - updateCurrentStep, - fields, - errors, -}) => { - const { mappings, connectorType = SwimlaneConnectorType.All } = action.config; - const prevConnectorType = useRef(connectorType); - const hasChangedConnectorType = connectorType !== prevConnectorType.current; +const mappingConfig: FieldConfig = { + defaultValue: null, + validations: [ + { + validator: ({ value, customData }) => { + const data = customData.value as { + connectorType: SwimlaneConnectorType; + validationLabel: string; + }; + if (isRequiredField(data.connectorType, value?.id as MappingConfigurationKeys)) { + return { + message: data.validationLabel, + }; + } + }, + }, + ], +}; + +const MappingField: React.FC<{ + path: string; + label: string; + validationLabel: string; + options: EuiComboBoxProps['options']; + fieldIdMap: Map; + connectorType: SwimlaneConnectorType; + readOnly: boolean; + dataTestSubj?: string; +}> = React.memo( + ({ + path, + options, + label, + validationLabel, + dataTestSubj, + fieldIdMap, + connectorType, + readOnly, + }) => { + return ( + + path={path} + component={ComboBoxField} + config={mappingConfig} + validationData={{ connectorType, validationLabel }} + > + {(field) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + const onComboChange = (opt: Array>) => { + const option = opt[0]; + + const item = fieldIdMap.get(option?.value ?? ''); + if (!item) { + field.setValue(null); + return; + } + + field.setValue({ + id: item.id, + name: item.name, + key: item.key, + fieldType: item.fieldType, + }); + }; + + const onSearchComboChange = (value: string) => { + if (value !== undefined) { + field.clearErrors(VALIDATION_TYPES.ARRAY_ITEM); + } + }; + + const selectedOptions = createSelectedOption(fieldIdMap.get(field.value?.id ?? '')); + + return ( + + + + ); + }} + + ); + } +); + +const SwimlaneFieldsComponent: React.FC = ({ fields, readOnly }) => { + const [{ config }] = useFormData({ + watch: ['config.connectorType'], + }); + + const connectorType = config?.connectorType ?? SwimlaneConnectorType.All; const [fieldTypeMap, fieldIdMap] = useMemo( () => @@ -78,214 +174,98 @@ const SwimlaneFieldsComponent: React.FC = ({ const textOptions = useMemo(() => fieldTypeMap.get('text') ?? [], [fieldTypeMap]); const commentsOptions = useMemo(() => fieldTypeMap.get('comments') ?? [], [fieldTypeMap]); - const state = useMemo( - () => ({ - alertIdConfig: createSelectedOption(mappings?.alertIdConfig), - severityConfig: createSelectedOption(mappings?.severityConfig), - ruleNameConfig: createSelectedOption(mappings?.ruleNameConfig), - caseIdConfig: createSelectedOption(mappings?.caseIdConfig), - caseNameConfig: createSelectedOption(mappings?.caseNameConfig), - commentsConfig: createSelectedOption(mappings?.commentsConfig), - descriptionConfig: createSelectedOption(mappings?.descriptionConfig), - }), - [mappings] - ); - - const mappingErrors: Record = useMemo( - () => (Array.isArray(errors?.mappings) ? errors?.mappings[0] : {}), - [errors] - ); - - const editMappings = useCallback( - (key: keyof SwimlaneMappingConfig, e: Array>) => { - if (e.length === 0) { - const newProps = { - ...mappings, - [key]: null, - }; - editActionConfig('mappings', newProps); - return; - } - - const option = e[0]; - const item = fieldIdMap.get(option.value ?? ''); - if (!item) { - return; - } - - const newProps = { - ...mappings, - [key]: { id: item.id, name: item.name, key: item.key, fieldType: item.fieldType }, - }; - editActionConfig('mappings', newProps); - }, - [editActionConfig, fieldIdMap, mappings] - ); - - useEffect(() => { - if (connectorType !== prevConnectorType.current) { - prevConnectorType.current = connectorType; - } - }, [connectorType]); - return ( <> - - editActionConfig('connectorType', type)} - buttonSize="compressed" - /> - + {isValidFieldForConnector(connectorType as SwimlaneConnectorType.All, 'alertIdConfig') && ( - <> - - editMappings('alertIdConfig', e)} - isInvalid={mappingErrors?.alertIdConfig != null && !hasChangedConnectorType} - /> - - + )} {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'ruleNameConfig') && ( - <> - - editMappings('ruleNameConfig', e)} - isInvalid={mappingErrors?.ruleNameConfig != null && !hasChangedConnectorType} - /> - - + )} {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'severityConfig') && ( - <> - - editMappings('severityConfig', e)} - isInvalid={mappingErrors?.severityConfig != null && !hasChangedConnectorType} - /> - - + )} {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'caseIdConfig') && ( - <> - - editMappings('caseIdConfig', e)} - isInvalid={mappingErrors?.caseIdConfig != null && !hasChangedConnectorType} - /> - - + )} {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'caseNameConfig') && ( - <> - - editMappings('caseNameConfig', e)} - isInvalid={mappingErrors?.caseNameConfig != null && !hasChangedConnectorType} - /> - - + )} {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'commentsConfig') && ( - <> - - editMappings('commentsConfig', e)} - isInvalid={mappingErrors?.commentsConfig != null && !hasChangedConnectorType} - /> - - + )} {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'descriptionConfig') && ( - <> - - editMappings('descriptionConfig', e)} - isInvalid={mappingErrors?.descriptionConfig != null && !hasChangedConnectorType} - /> - - + )} ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx index 45d68c8ab39e8..479496b57ba83 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx @@ -8,7 +8,6 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel } from '../../../../types'; -import { SwimlaneActionConnector } from './types'; import { registrationServicesMock } from '../../../../mocks'; const ACTION_TYPE_ID = '.swimlane'; @@ -29,152 +28,6 @@ describe('actionTypeRegistry.get() works', () => { }); }); -describe('swimlane connector validation', () => { - test('connector validation succeeds when connector is valid', async () => { - const actionConnector = { - secrets: { - apiToken: 'test', - }, - id: 'test', - actionTypeId: '.swimlane', - name: 'swimlane', - config: { - apiUrl: 'http:\\test', - appId: '1234567asbd32', - connectorType: 'all', - mappings: { - alertIdConfig: { id: '1234' }, - severityConfig: { id: '1234' }, - ruleNameConfig: { id: '1234' }, - caseIdConfig: { id: '1234' }, - caseNameConfig: { id: '1234' }, - descriptionConfig: { id: '1234' }, - commentsConfig: { id: '1234' }, - }, - }, - } as SwimlaneActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { errors: { apiUrl: [], appId: [], mappings: [], connectorType: [] } }, - secrets: { errors: { apiToken: [] } }, - }); - }); - - test('it validates correctly when connectorType=all', async () => { - const actionConnector = { - secrets: { - apiToken: 'test', - }, - id: 'test', - actionTypeId: '.swimlane', - name: 'swimlane', - config: { - apiUrl: 'http:\\test', - appId: '1234567asbd32', - connectorType: 'all', - mappings: {}, - }, - } as SwimlaneActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { errors: { apiUrl: [], appId: [], mappings: [], connectorType: [] } }, - secrets: { errors: { apiToken: [] } }, - }); - }); - - test('it validates correctly when connectorType=cases', async () => { - const actionConnector = { - secrets: { - apiToken: 'test', - }, - id: 'test', - actionTypeId: '.swimlane', - name: 'swimlane', - config: { - apiUrl: 'http:\\test', - appId: '1234567asbd32', - connectorType: 'cases', - mappings: {}, - }, - } as SwimlaneActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - apiUrl: [], - appId: [], - mappings: [ - { - caseIdConfig: 'Case ID is required.', - caseNameConfig: 'Case name is required.', - commentsConfig: 'Comments are required.', - descriptionConfig: 'Description is required.', - }, - ], - connectorType: [], - }, - }, - secrets: { errors: { apiToken: [] } }, - }); - }); - - test('it validates correctly when connectorType=alerts', async () => { - const actionConnector = { - secrets: { - apiToken: 'test', - }, - id: 'test', - actionTypeId: '.swimlane', - name: 'swimlane', - config: { - apiUrl: 'http:\\test', - appId: '1234567asbd32', - connectorType: 'alerts', - mappings: {}, - }, - } as SwimlaneActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - apiUrl: [], - appId: [], - mappings: [ - { - alertIdConfig: 'Alert ID is required.', - ruleNameConfig: 'Rule name is required.', - }, - ], - connectorType: [], - }, - }, - secrets: { errors: { apiToken: [] } }, - }); - }); - - test('it validates correctly required config/secrets fields', async () => { - const actionConnector = { - secrets: {}, - id: 'test', - actionTypeId: '.swimlane', - name: 'swimlane', - config: {}, - } as SwimlaneActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - apiUrl: ['URL is required.'], - appId: ['An App ID is required.'], - mappings: [], - connectorType: [], - }, - }, - secrets: { errors: { apiToken: ['An API token is required.'] } }, - }); - }); -}); - describe('swimlane action params validation', () => { test('action params validation succeeds when action params is valid', async () => { const actionParams = { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx index 5797017de7baf..5a072a54efb67 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx @@ -5,22 +5,10 @@ * 2.0. */ -import { isEmpty } from 'lodash'; import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; -import { - ActionTypeModel, - ConnectorValidationResult, - GenericValidationResult, -} from '../../../../types'; -import { - SwimlaneActionConnector, - SwimlaneConfig, - SwimlaneSecrets, - SwimlaneActionParams, -} from './types'; -import { isValidUrl } from '../../../lib/value_validators'; -import { validateMappingForConnector } from './helpers'; +import { ActionTypeModel, GenericValidationResult } from '../../../../types'; +import { SwimlaneConfig, SwimlaneSecrets, SwimlaneActionParams } from './types'; export const SW_SELECT_MESSAGE_TEXT = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.selectMessageText', @@ -46,55 +34,6 @@ export function getActionType(): ActionTypeModel< iconClass: lazy(() => import('./logo')), selectMessage: SW_SELECT_MESSAGE_TEXT, actionTypeTitle: SW_ACTION_TYPE_TITLE, - validateConnector: async ( - action: SwimlaneActionConnector - ): Promise> => { - const translations = await import('./translations'); - const configErrors = { - apiUrl: new Array(), - appId: new Array(), - connectorType: new Array(), - mappings: new Array>(), - }; - const secretsErrors = { - apiToken: new Array(), - }; - - const validationResult = { - config: { errors: configErrors }, - secrets: { errors: secretsErrors }, - }; - - if (!action.config.apiUrl) { - configErrors.apiUrl = [...configErrors.apiUrl, translations.SW_API_URL_REQUIRED]; - } else if (action.config.apiUrl) { - if (!isValidUrl(action.config.apiUrl)) { - configErrors.apiUrl = [...configErrors.apiUrl, translations.SW_API_URL_INVALID]; - } - } - - if (!action.secrets.apiToken) { - secretsErrors.apiToken = [ - ...secretsErrors.apiToken, - translations.SW_REQUIRED_API_TOKEN_TEXT, - ]; - } - - if (!action.config.appId) { - configErrors.appId = [...configErrors.appId, translations.SW_REQUIRED_APP_ID_TEXT]; - } - - const mappingErrors = validateMappingForConnector( - action.config.connectorType, - action.config.mappings - ); - - if (!isEmpty(mappingErrors)) { - configErrors.mappings = [...configErrors.mappings, mappingErrors]; - } - - return validationResult; - }, validateParams: async ( actionParams: SwimlaneActionParams ): Promise> => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx index 47e5c09c13864..bbe86a4fe7fe1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx @@ -8,10 +8,13 @@ import React from 'react'; import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; import { act } from 'react-dom/test-utils'; -import { SwimlaneActionConnector } from './types'; import SwimlaneActionConnectorFields from './swimlane_connectors'; import { useGetApplication } from './use_get_application'; import { applicationFields, mappings } from './mocks'; +import { ConnectorFormTestProvider } from '../test_utils'; +import { waitFor } from '@testing-library/dom'; +import userEvent from '@testing-library/user-event'; +import { render } from '@testing-library/react'; jest.mock('../../../../common/lib/kibana'); jest.mock('./use_get_application'); @@ -29,10 +32,6 @@ describe('SwimlaneActionConnectorFields renders', () => { test('all connector fields are rendered', async () => { const actionConnector = { - secrets: { - apiToken: 'test', - }, - id: 'test', actionTypeId: '.swimlane', name: 'swimlane', config: { @@ -41,18 +40,20 @@ describe('SwimlaneActionConnectorFields renders', () => { connectorType: 'all', mappings, }, - } as SwimlaneActionConnector; + secrets: { + apiToken: 'test', + }, + isDeprecated: false, + }; const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> + + {}} + /> + ); await act(async () => { @@ -65,69 +66,12 @@ describe('SwimlaneActionConnectorFields renders', () => { expect(wrapper.find('[data-test-subj="swimlaneApiTokenInput"]').exists()).toBeTruthy(); }); - test('should display a message on create to remember credentials', () => { - const actionConnector = { - actionTypeId: '.swimlane', - secrets: {}, - config: {}, - } as SwimlaneActionConnector; - - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); - }); - - test('should display a message on edit to re-enter credentials', () => { - const actionConnector = { - secrets: { - apiToken: 'test', - }, - id: 'test', - actionTypeId: '.swimlane', - name: 'swimlane', - config: { - apiUrl: 'http:\\test', - appId: '1234567asbd32', - connectorType: 'all', - mappings, - }, - } as SwimlaneActionConnector; - - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); - }); - test('renders the mappings correctly - connector type all', async () => { getApplication.mockResolvedValue({ fields: applicationFields, }); const actionConnector = { - secrets: { - apiToken: 'test', - }, - id: 'test', actionTypeId: '.swimlane', name: 'swimlane', config: { @@ -136,18 +80,20 @@ describe('SwimlaneActionConnectorFields renders', () => { connectorType: 'all', mappings, }, - } as SwimlaneActionConnector; + secrets: { + apiToken: 'test', + }, + isDeprecated: false, + }; const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> + + {}} + /> + ); await act(async () => { @@ -156,13 +102,15 @@ describe('SwimlaneActionConnectorFields renders', () => { wrapper.update(); }); - expect(wrapper.find('[data-test-subj="swimlaneAlertIdInput"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="swimlaneAlertNameInput"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="swimlaneSeverityInput"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="swimlaneCaseIdConfig"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="swimlaneCaseNameConfig"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="swimlaneCommentsConfig"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="swimlaneDescriptionConfig"]').exists()).toBeTruthy(); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="swimlaneAlertIdInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneAlertNameInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneSeverityInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneCaseIdConfig"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneCaseNameConfig"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneCommentsConfig"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneDescriptionConfig"]').exists()).toBeTruthy(); + }); }); test('renders the mappings correctly - connector type cases', async () => { @@ -171,10 +119,6 @@ describe('SwimlaneActionConnectorFields renders', () => { }); const actionConnector = { - secrets: { - apiToken: 'test', - }, - id: 'test', actionTypeId: '.swimlane', name: 'swimlane', config: { @@ -183,18 +127,20 @@ describe('SwimlaneActionConnectorFields renders', () => { connectorType: 'cases', mappings, }, - } as SwimlaneActionConnector; + secrets: { + apiToken: 'test', + }, + isDeprecated: false, + }; const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> + + {}} + /> + ); await act(async () => { @@ -203,6 +149,7 @@ describe('SwimlaneActionConnectorFields renders', () => { wrapper.update(); }); + await waitFor(() => {}); expect(wrapper.find('[data-test-subj="swimlaneAlertIdInput"]').exists()).toBeFalsy(); expect(wrapper.find('[data-test-subj="swimlaneAlertNameInput"]').exists()).toBeFalsy(); expect(wrapper.find('[data-test-subj="swimlaneSeverityInput"]').exists()).toBeFalsy(); @@ -218,10 +165,6 @@ describe('SwimlaneActionConnectorFields renders', () => { }); const actionConnector = { - secrets: { - apiToken: 'test', - }, - id: 'test', actionTypeId: '.swimlane', name: 'swimlane', config: { @@ -230,18 +173,20 @@ describe('SwimlaneActionConnectorFields renders', () => { connectorType: 'alerts', mappings, }, - } as SwimlaneActionConnector; + secrets: { + apiToken: 'test', + }, + isDeprecated: false, + }; const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> + + {}} + /> + ); await act(async () => { @@ -250,13 +195,15 @@ describe('SwimlaneActionConnectorFields renders', () => { wrapper.update(); }); - expect(wrapper.find('[data-test-subj="swimlaneAlertIdInput"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="swimlaneAlertNameInput"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="swimlaneSeverityInput"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="swimlaneCaseIdConfig"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="swimlaneCaseNameConfig"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="swimlaneCommentsConfig"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="swimlaneDescriptionConfig"]').exists()).toBeFalsy(); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="swimlaneAlertIdInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneAlertNameInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneSeverityInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneCaseIdConfig"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="swimlaneCaseNameConfig"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="swimlaneCommentsConfig"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneDescriptionConfig"]').exists()).toBeFalsy(); + }); }); test('renders the correct options per field', async () => { @@ -265,19 +212,19 @@ describe('SwimlaneActionConnectorFields renders', () => { }); const actionConnector = { - secrets: { - apiToken: 'test', - }, - id: 'test', actionTypeId: '.swimlane', name: 'swimlane', config: { - apiUrl: 'http:\\test', + apiUrl: 'http://test.com', appId: '1234567asbd32', connectorType: 'all', mappings, }, - } as SwimlaneActionConnector; + secrets: { + apiToken: 'test', + }, + isDeprecated: false, + }; const textOptions = [ { label: 'Alert Id (alert-id)', value: 'a6ide' }, @@ -291,17 +238,23 @@ describe('SwimlaneActionConnectorFields renders', () => { const commentOptions = [{ label: 'Comments (notes)', value: 'a6fdf' }]; const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> + + {}} + /> + ); + await waitFor(async () => { + await nextTick(); + wrapper.update(); + expect(wrapper.find('[data-test-subj="swimlaneApiUrlInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneAppIdInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneApiTokenInput"]').exists()).toBeTruthy(); + }); + await act(async () => { wrapper.find('[data-test-subj="swimlaneConfigureMapping"]').first().simulate('click'); await nextTick(); @@ -330,4 +283,157 @@ describe('SwimlaneActionConnectorFields renders', () => { wrapper.find('[data-test-subj="swimlaneDescriptionConfig"]').first().prop('options') ).toEqual(textOptions); }); + + describe('Validation', () => { + const onSubmit = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + getApplication.mockResolvedValue({ + fields: applicationFields, + }); + }); + + const getConnector = (connectorType: string = 'all') => ({ + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http://test.com', + appId: '1234567asbd32', + connectorType: 'all', + mappings, + }, + secrets: { + apiToken: 'test', + }, + isDeprecated: false, + }); + + const getConnectorWithEmptyMappings = (connectorType: string = 'all') => { + const actionConnector = getConnector(connectorType); + return { + ...actionConnector, + config: { + ...actionConnector.config, + connectorType, + mappings: {}, + }, + }; + }; + + const tests: Array<[string, string]> = [ + ['swimlaneApiUrlInput', 'not-valid'], + ['swimlaneAppIdInput', ''], + ['swimlaneApiTokenInput', ''], + ]; + + it.each([['cases'], ['alerts']])( + 'connector validation succeeds when connector config is valid for connectorType=%p', + async (connectorType) => { + const connector = getConnector(connectorType); + const { getByTestId } = render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ data: { ...connector }, isValid: true }); + } + ); + + it.each(tests)('validates correctly %p', async (field, value) => { + const connector = getConnector(); + const res = render( + + {}} + /> + + ); + + await act(async () => { + await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, { + delay: 10, + }); + }); + + await act(async () => { + userEvent.click(res.getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }); + }); + + it('connector validation succeeds when when connectorType=all with empty mappings', async () => { + const connector = getConnectorWithEmptyMappings(); + const { getByTestId } = render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ + data: { + ...connector, + config: { + ...connector.config, + mappings: { + alertIdConfig: null, + caseIdConfig: null, + caseNameConfig: null, + commentsConfig: null, + descriptionConfig: null, + ruleNameConfig: null, + severityConfig: null, + }, + }, + }, + isValid: true, + }); + }); + + it.each([['cases'], ['alerts']])( + 'validates correctly when when connectorType=%p', + async (connectorType) => { + const connector = getConnectorWithEmptyMappings(connectorType); + + const { getByTestId } = render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ + data: {}, + isValid: false, + }); + } + ); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx index c8db48421264c..af59b9517b91d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx @@ -5,46 +5,42 @@ * 2.0. */ -import React, { Fragment, useCallback, useMemo, useState, useEffect } from 'react'; +import React, { Fragment, useCallback, useMemo, useState } from 'react'; import { EuiForm, EuiSpacer, EuiStepsHorizontal, - EuiStepStatus, EuiButton, EuiFormRow, + EuiStepStatus, } from '@elastic/eui'; +import { useFormContext, useFormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { useKibana } from '../../../../common/lib/kibana'; import { ActionConnectorFieldsProps } from '../../../../types'; -import { - SwimlaneActionConnector, - SwimlaneConnectorType, - SwimlaneFieldMappingConfig, -} from './types'; +import { SwimlaneFieldMappingConfig } from './types'; import { SwimlaneConnection, SwimlaneFields } from './steps'; import { useGetApplication } from './use_get_application'; import * as i18n from './translations'; -const SwimlaneActionConnectorFields: React.FunctionComponent< - ActionConnectorFieldsProps -> = ({ errors, action, editActionConfig, editActionSecrets, readOnly }) => { +const SwimlaneActionConnectorFields: React.FunctionComponent = ({ + readOnly, +}) => { const { notifications: { toasts }, } = useKibana().services; - const { apiUrl, appId, mappings, connectorType } = action.config; - const { apiToken } = action.secrets; - + const [hasConfigurationErrors, setHasConfigurationError] = useState(false); + const { isValid, validateFields } = useFormContext(); + const [{ config, secrets }] = useFormData({ + watch: ['config.apiUrl', 'config.appId', 'secrets.apiToken'], + }); const { getApplication, isLoading: isLoadingApplication } = useGetApplication({ toastNotifications: toasts, - apiToken, - appId, - apiUrl, }); - const hasConfigurationErrors = - errors.apiUrl?.length > 0 || errors.appId?.length > 0 || errors.apiToken?.length > 0; - + const apiUrl = config?.apiUrl ?? ''; + const appId = config?.appId ?? ''; + const apiToken = secrets?.apiToken ?? ''; const [currentStep, setCurrentStep] = useState(1); const [fields, setFields] = useState([]); @@ -53,25 +49,37 @@ const SwimlaneActionConnectorFields: React.FunctionComponent< }, []); const onNextStep = useCallback(async () => { + setHasConfigurationError(false); + + const { areFieldsValid } = await validateFields([ + 'config.apiUrl', + 'config.appId', + 'secrets.apiToken', + ]); + + if (!areFieldsValid) { + setHasConfigurationError(true); + return; + } + // fetch swimlane application configuration - const application = await getApplication(); + const application = await getApplication({ + apiUrl, + appId, + apiToken, + }); if (application?.fields) { const allFields = application.fields; setFields(allFields); setCurrentStep(2); } - }, [getApplication]); + }, [apiToken, apiUrl, appId, getApplication, validateFields]); const resetConnection = useCallback(() => { setCurrentStep(1); }, []); - const hasMappingErrors = useMemo( - () => Object.values(errors?.mappings ?? {}).some((mappingError) => mappingError.length !== 0), - [errors?.mappings] - ); - const steps = useMemo( () => [ { @@ -88,7 +96,7 @@ const SwimlaneActionConnectorFields: React.FunctionComponent< title: i18n.SW_MAPPING_TITLE_TEXT_FIELD_LABEL, disabled: hasConfigurationErrors || isLoadingApplication, onClick: onNextStep, - status: (hasMappingErrors + status: (!isValid ? 'danger' : currentStep === 2 ? 'selected' @@ -98,68 +106,40 @@ const SwimlaneActionConnectorFields: React.FunctionComponent< [ currentStep, hasConfigurationErrors, - hasMappingErrors, isLoadingApplication, + isValid, onNextStep, updateCurrentStep, ] ); - /** - * Connector type needs to be updated on mount to All. - * Otherwise it is undefined and this will cause an error - * if the user saves the connector without going to the - * second step. Same for mapping. - */ - useEffect(() => { - editActionConfig('connectorType', connectorType ?? SwimlaneConnectorType.All); - editActionConfig('mappings', mappings ?? {}); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - return ( - {currentStep === 1 && ( - <> - - - - - {i18n.SW_NEXT} - - - - )} - {currentStep === 2 && ( - <> - - - {i18n.SW_BACK} +
+ + + + + {i18n.SW_NEXT} - - )} + +
+
+ + + {i18n.SW_BACK} + +
); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts index a1dbf88f92994..631e69e8e7a66 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts @@ -21,20 +21,6 @@ export const SW_REQUIRED_APP_ID_TEXT = i18n.translate( } ); -export const SW_REQUIRED_FIELD_MAPPINGS_TEXT = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredFieldMappingsText', - { - defaultMessage: 'Field mappings are required.', - } -); - -export const SW_REQUIRED_API_TOKEN_TEXT = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredApiTokenText', - { - defaultMessage: 'An API token is required.', - } -); - export const SW_GET_APPLICATION_API_ERROR = (id: string | null) => i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlane.unableToGetApplicationMessage', @@ -58,13 +44,6 @@ export const SW_API_URL_TEXT_FIELD_LABEL = i18n.translate( } ); -export const SW_API_URL_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.requiredApiUrlTextField', - { - defaultMessage: 'URL is required.', - } -); - export const SW_API_URL_INVALID = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.invalidApiUrlTextField', { @@ -93,13 +72,6 @@ export const SW_MAPPING_TITLE_TEXT_FIELD_LABEL = i18n.translate( } ); -export const SW_ALERT_SOURCE_FIELD_LABEL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertSourceFieldLabel', - { - defaultMessage: 'Alert source', - } -); - export const SW_SEVERITY_FIELD_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.severityFieldLabel', { @@ -107,13 +79,6 @@ export const SW_SEVERITY_FIELD_LABEL = i18n.translate( } ); -export const SW_MAPPING_DESCRIPTION_TEXT_FIELD_LABEL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingDescriptionTextFieldLabel', - { - defaultMessage: 'Used to specify the field names in the Swimlane Application', - } -); - export const SW_RULE_NAME_FIELD_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.ruleNameFieldLabel', { @@ -156,26 +121,11 @@ export const SW_DESCRIPTION_FIELD_LABEL = i18n.translate( } ); -export const SW_REMEMBER_VALUE_LABEL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.rememberValueLabel', - { defaultMessage: 'Remember this value. You must reenter it each time you edit the connector.' } -); - -export const SW_REENTER_VALUE_LABEL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.reenterValueLabel', - { defaultMessage: 'This key is encrypted. Please reenter a value for this field.' } -); - export const SW_CONFIGURE_CONNECTION_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.configureConnectionLabel', { defaultMessage: 'Configure API Connection' } ); -export const SW_RETRIEVE_CONFIGURATION_LABEL = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.retrieveConfigurationLabel', - { defaultMessage: 'Configure Fields' } -); - export const SW_CONNECTOR_TYPE_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.connectorType', { @@ -183,13 +133,6 @@ export const SW_CONNECTOR_TYPE_LABEL = i18n.translate( } ); -export const SW_FIELD_MAPPING_IS_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingFieldRequired', - { - defaultMessage: 'Field mapping is required.', - } -); - export const EMPTY_MAPPING_WARNING_TITLE = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.emptyMappingWarningTitle', { @@ -205,13 +148,6 @@ export const EMPTY_MAPPING_WARNING_DESC = i18n.translate( } ); -export const SW_REQUIRED_ALERT_SOURCE = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAlertSource', - { - defaultMessage: 'Alert source is required.', - } -); - export const SW_REQUIRED_SEVERITY = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredSeverity', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.test.tsx index fc0ae22efe377..f852d40ebef2f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.test.tsx @@ -9,7 +9,6 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useKibana } from '../../../../common/lib/kibana'; import { getApplication } from './api'; -import { SwimlaneActionConnector } from './types'; import { useGetApplication, UseGetApplication } from './use_get_application'; jest.mock('./api'); @@ -30,7 +29,7 @@ const action = { appId: 'bcq16kdTbz5jlwM6h', mappings: {}, }, -} as SwimlaneActionConnector; +}; describe('useGetApplication', () => { const { services } = useKibanaMock(); @@ -48,9 +47,6 @@ describe('useGetApplication', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => useGetApplication({ - appId: action.config.appId, - apiToken: action.secrets.apiToken, - apiUrl: action.config.apiUrl, toastNotifications: services.notifications.toasts, }) ); @@ -67,16 +63,18 @@ describe('useGetApplication', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => useGetApplication({ - appId: action.config.appId, - apiToken: action.secrets.apiToken, - apiUrl: action.config.apiUrl, toastNotifications: services.notifications.toasts, }) ); await waitForNextUpdate(); - result.current.getApplication(); + result.current.getApplication({ + appId: action.config.appId, + apiToken: action.secrets.apiToken, + apiUrl: action.config.apiUrl, + }); + await waitForNextUpdate(); expect(getApplicationMock).toBeCalledWith({ signal: abortCtrl.signal, @@ -91,15 +89,16 @@ describe('useGetApplication', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => useGetApplication({ - appId: action.config.appId, - apiToken: action.secrets.apiToken, - apiUrl: action.config.apiUrl, toastNotifications: services.notifications.toasts, }) ); await waitForNextUpdate(); - result.current.getApplication(); + result.current.getApplication({ + appId: action.config.appId, + apiToken: action.secrets.apiToken, + apiUrl: action.config.apiUrl, + }); await waitForNextUpdate(); expect(result.current).toEqual({ @@ -113,15 +112,16 @@ describe('useGetApplication', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => useGetApplication({ - appId: action.config.appId, - apiToken: action.secrets.apiToken, - apiUrl: action.config.apiUrl, toastNotifications: services.notifications.toasts, }) ); await waitForNextUpdate(); - result.current.getApplication(); + result.current.getApplication({ + appId: action.config.appId, + apiToken: action.secrets.apiToken, + apiUrl: action.config.apiUrl, + }); expect(result.current.isLoading).toBe(true); }); @@ -135,14 +135,15 @@ describe('useGetApplication', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => useGetApplication({ - appId: action.config.appId, - apiToken: action.secrets.apiToken, - apiUrl: action.config.apiUrl, toastNotifications: services.notifications.toasts, }) ); await waitForNextUpdate(); - result.current.getApplication(); + result.current.getApplication({ + appId: action.config.appId, + apiToken: action.secrets.apiToken, + apiUrl: action.config.apiUrl, + }); expect(result.current).toEqual({ isLoading: false, @@ -162,14 +163,15 @@ describe('useGetApplication', () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => useGetApplication({ - appId: action.config.appId, - apiToken: action.secrets.apiToken, - apiUrl: action.config.apiUrl, toastNotifications: services.notifications.toasts, }) ); await waitForNextUpdate(); - result.current.getApplication(); + result.current.getApplication({ + appId: action.config.appId, + apiToken: action.secrets.apiToken, + apiUrl: action.config.apiUrl, + }); await waitForNextUpdate(); expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx index fc35c6c26c945..15b449f10f5bf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx @@ -6,6 +6,7 @@ */ import { useState, useCallback, useRef } from 'react'; +import { isEmpty } from 'lodash'; import { ToastsApi } from '@kbn/core/public'; import { getApplication as getApplicationApi } from './api'; import * as i18n from './translations'; @@ -22,58 +23,64 @@ interface Props { } export interface UseGetApplication { - getApplication: () => Promise<{ fields?: SwimlaneFieldMappingConfig[] } | undefined>; + getApplication: ( + args: Omit + ) => Promise<{ fields?: SwimlaneFieldMappingConfig[] } | undefined>; isLoading: boolean; } export const useGetApplication = ({ toastNotifications, - appId, - apiToken, - apiUrl, -}: Props): UseGetApplication => { +}: Pick): UseGetApplication => { const [isLoading, setIsLoading] = useState(false); const isCancelledRef = useRef(false); const abortCtrlRef = useRef(new AbortController()); - const getApplication = useCallback(async () => { - try { - isCancelledRef.current = false; - abortCtrlRef.current.abort(); - abortCtrlRef.current = new AbortController(); - setIsLoading(true); + const getApplication = useCallback( + async ({ appId, apiToken, apiUrl }: Omit) => { + try { + if (isEmpty(appId) || isEmpty(apiToken) || isEmpty(apiUrl)) { + return; + } - const data = await getApplicationApi({ - signal: abortCtrlRef.current.signal, - appId, - apiToken, - url: apiUrl, - }); + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + setIsLoading(true); - if (!isCancelledRef.current) { - setIsLoading(false); - if (!data.fields) { - // If the response was malformed and fields doesn't exist, show an error toast - toastNotifications.addDanger({ - title: i18n.SW_GET_APPLICATION_API_ERROR(appId), - text: i18n.SW_GET_APPLICATION_API_NO_FIELDS_ERROR, - }); - return; + const data = await getApplicationApi({ + signal: abortCtrlRef.current.signal, + appId, + apiToken, + url: apiUrl, + }); + + if (!isCancelledRef.current) { + setIsLoading(false); + if (!data.fields) { + // If the response was malformed and fields doesn't exist, show an error toast + toastNotifications.addDanger({ + title: i18n.SW_GET_APPLICATION_API_ERROR(appId), + text: i18n.SW_GET_APPLICATION_API_NO_FIELDS_ERROR, + }); + return; + } + return data; } - return data; - } - } catch (error) { - if (!isCancelledRef.current) { - if (error.name !== 'AbortError') { - toastNotifications.addDanger({ - title: i18n.SW_GET_APPLICATION_API_ERROR(appId), - text: error.message, - }); + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { + toastNotifications.addDanger({ + title: i18n.SW_GET_APPLICATION_API_ERROR(appId), + text: error.message, + }); + } + setIsLoading(false); } - setIsLoading(false); } - } - }, [apiToken, apiUrl, appId, toastNotifications]); + }, + [toastNotifications] + ); return { isLoading, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.test.tsx index 8590433f39cc0..e08853abf9c12 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.test.tsx @@ -8,7 +8,6 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel } from '../../../../types'; -import { TeamsActionConnector } from '../types'; import { registrationServicesMock } from '../../../../mocks'; const ACTION_TYPE_ID = '.teams'; @@ -29,98 +28,6 @@ describe('actionTypeRegistry.get() works', () => { }); }); -describe('teams connector validation', () => { - test('connector validation succeeds when connector config is valid', async () => { - const actionConnector = { - secrets: { - webhookUrl: 'https:\\test', - }, - id: 'test', - actionTypeId: '.teams', - name: 'team', - config: {}, - } as TeamsActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: {}, - }, - secrets: { - errors: { - webhookUrl: [], - }, - }, - }); - }); - - test('connector validation fails when connector config is not valid - empty webhook url', async () => { - const actionConnector = { - secrets: {}, - id: 'test', - actionTypeId: '.teams', - name: 'team', - config: {}, - } as TeamsActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: {}, - }, - secrets: { - errors: { - webhookUrl: ['Webhook URL is required.'], - }, - }, - }); - }); - - test('connector validation fails when connector config is not valid - invalid webhook url', async () => { - const actionConnector = { - secrets: { - webhookUrl: 'h', - }, - id: 'test', - actionTypeId: '.teams', - name: 'team', - config: {}, - } as TeamsActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: {}, - }, - secrets: { - errors: { - webhookUrl: ['Webhook URL is invalid.'], - }, - }, - }); - }); - - test('connector validation fails when connector config is not valid - invalid webhook url protocol', async () => { - const actionConnector = { - secrets: { - webhookUrl: 'http://insecure', - }, - id: 'test', - actionTypeId: '.teams', - name: 'team', - config: {}, - } as TeamsActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: {}, - }, - secrets: { - errors: { - webhookUrl: ['Webhook URL must start with https://.'], - }, - }, - }); - }); -}); - describe('teams action params validation', () => { test('if action params validation succeeds when action params is valid', async () => { const actionParams = { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.tsx index c48b4f950855d..e9c286cdc1b56 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.tsx @@ -7,13 +7,8 @@ import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; -import { - ActionTypeModel, - GenericValidationResult, - ConnectorValidationResult, -} from '../../../../types'; -import { TeamsActionParams, TeamsSecrets, TeamsActionConnector } from '../types'; -import { isValidUrl } from '../../../lib/value_validators'; +import { ActionTypeModel, GenericValidationResult } from '../../../../types'; +import { TeamsActionParams, TeamsSecrets } from '../types'; export function getActionType(): ActionTypeModel { return { @@ -31,25 +26,6 @@ export function getActionType(): ActionTypeModel> => { - const translations = await import('./translations'); - const secretsErrors = { - webhookUrl: new Array(), - }; - const validationResult = { config: { errors: {} }, secrets: { errors: secretsErrors } }; - if (!action.secrets.webhookUrl) { - secretsErrors.webhookUrl.push(translations.WEBHOOK_URL_REQUIRED); - } else if (action.secrets.webhookUrl) { - if (!isValidUrl(action.secrets.webhookUrl)) { - secretsErrors.webhookUrl.push(translations.WEBHOOK_URL_INVALID); - } else if (!isValidUrl(action.secrets.webhookUrl, 'https:')) { - secretsErrors.webhookUrl.push(translations.WEBHOOK_URL_HTTP_INVALID); - } - } - return validationResult; - }, validateParams: async ( actionParams: TeamsActionParams ): Promise> => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.test.tsx index 2d2b6d5052bb8..a0a082d36a864 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.test.tsx @@ -7,112 +7,126 @@ import React from 'react'; import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; -import { act } from '@testing-library/react'; -import { TeamsActionConnector } from '../types'; +import { act, render } from '@testing-library/react'; import TeamsActionFields from './teams_connectors'; +import { ConnectorFormTestProvider } from '../test_utils'; +import userEvent from '@testing-library/user-event'; jest.mock('../../../../common/lib/kibana'); describe('TeamsActionFields renders', () => { test('all connector fields are rendered', async () => { const actionConnector = { secrets: { - webhookUrl: 'https:\\test', + webhookUrl: 'https://test.com', }, id: 'test', actionTypeId: '.teams', name: 'teams', config: {}, - } as TeamsActionConnector; + isDeprecated: false, + }; + const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> + + {}} /> + ); await act(async () => { await nextTick(); wrapper.update(); }); + expect(wrapper.find('[data-test-subj="teamsWebhookUrlInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="teamsWebhookUrlInput"]').first().prop('value')).toBe( - 'https:\\test' + 'https://test.com' ); }); - test('should display a message on create to remember credentials', () => { - const actionConnector = { - actionTypeId: '.teams', - config: {}, - secrets: {}, - } as TeamsActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); - }); + describe('Validation', () => { + const onSubmit = jest.fn(); - test('should display a message on edit to re-enter credentials', () => { - const actionConnector = { - secrets: { - webhookUrl: 'http:\\test', - }, - id: 'test', - actionTypeId: '.teams', - name: 'teams', - config: {}, - } as TeamsActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); - }); + beforeEach(() => { + jest.clearAllMocks(); + }); - test('should display a message for missing secrets after import', () => { - const actionConnector = { - secrets: { - webhookUrl: 'http:\\test', - }, - id: 'test', - actionTypeId: '.teams', - isMissingSecrets: true, - name: 'teams', - config: {}, - } as TeamsActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0); + it('connector validation succeeds when connector config is valid', async () => { + const actionConnector = { + secrets: { + webhookUrl: 'https://test.com', + }, + id: 'test', + actionTypeId: '.teams', + name: 'teams', + config: {}, + isDeprecated: false, + }; + + const { getByTestId } = render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: { + secrets: { + webhookUrl: 'https://test.com', + }, + id: 'test', + actionTypeId: '.teams', + name: 'teams', + isDeprecated: false, + }, + isValid: true, + }); + }); + + it('validates teh web hook url field correctly', async () => { + const actionConnector = { + secrets: { + webhookUrl: 'https://test.com', + }, + id: 'test', + actionTypeId: '.teams', + name: 'teams', + config: {}, + isDeprecated: false, + }; + + const { getByTestId } = render( + + {}} + /> + + ); + + await act(async () => { + await userEvent.type( + getByTestId('teamsWebhookUrlInput'), + `{selectall}{backspace}no-valid`, + { + delay: 10, + } + ); + }); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }); + }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.tsx index b6c41cb649dbd..34e2e02a0611c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.tsx @@ -6,74 +6,54 @@ */ import React from 'react'; -import { EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import { EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { FieldConfig, UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; +import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import { DocLinksStart } from '@kbn/core/public'; import { ActionConnectorFieldsProps } from '../../../../types'; -import { TeamsActionConnector } from '../types'; import { useKibana } from '../../../../common/lib/kibana'; -import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label'; +import * as i18n from './translations'; -const TeamsActionFields: React.FunctionComponent< - ActionConnectorFieldsProps -> = ({ action, editActionSecrets, errors, readOnly }) => { - const { webhookUrl } = action.secrets; - const { docLinks } = useKibana().services; +const { urlField } = fieldValidators; + +const getWebhookUrlConfig = (docLinks: DocLinksStart): FieldConfig => ({ + label: i18n.WEBHOOK_URL_LABEL, + helpText: ( + + + + ), + validations: [ + { + validator: urlField(i18n.WEBHOOK_URL_INVALID), + }, + ], +}); - const isWebhookUrlInvalid: boolean = - errors.webhookUrl !== undefined && errors.webhookUrl.length > 0 && webhookUrl !== undefined; +const TeamsActionFields: React.FunctionComponent = ({ + readOnly, + isEdit, +}) => { + const { docLinks } = useKibana().services; return ( - <> - - - - } - error={errors.webhookUrl} - isInvalid={isWebhookUrlInvalid} - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.webhookUrlTextFieldLabel', - { - defaultMessage: 'Webhook URL', - } - )} - > - <> - {getEncryptedFieldNotifyLabel( - !action.id, - 1, - action.isMissingSecrets ?? false, - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.reenterValueLabel', - { defaultMessage: 'This URL is encrypted. Please reenter a value for this field.' } - ) - )} - { - editActionSecrets('webhookUrl', e.target.value); - }} - onBlur={() => { - if (!webhookUrl) { - editActionSecrets('webhookUrl', ''); - } - }} - /> - - - + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/translations.ts index 790a3b3bac32f..2bf4cae881f7b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/translations.ts @@ -7,10 +7,10 @@ import { i18n } from '@kbn/i18n'; -export const WEBHOOK_URL_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredWebhookUrlText', +export const WEBHOOK_URL_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.webhookUrlTextLabel', { - defaultMessage: 'Webhook URL is required.', + defaultMessage: 'Webhook URL', } ); @@ -21,13 +21,6 @@ export const WEBHOOK_URL_INVALID = i18n.translate( } ); -export const WEBHOOK_URL_HTTP_INVALID = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requireHttpsWebhookUrlText', - { - defaultMessage: 'Webhook URL must start with https://.', - } -); - export const MESSAGE_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.error.requiredMessageText', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/test_utils.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/test_utils.tsx new file mode 100644 index 0000000000000..c1c5eaefaa42c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/test_utils.tsx @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { ReactWrapper } from 'enzyme'; +import { of } from 'rxjs'; +import { I18nProvider } from '@kbn/i18n-react'; +import { EuiButton } from '@elastic/eui'; +import { Form, useForm, FormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { act } from 'react-dom/test-utils'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { render as reactRender, RenderOptions, RenderResult } from '@testing-library/react'; +import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; + +import { ConnectorServices } from '../../../types'; +import { TriggersAndActionsUiServices } from '../../..'; +import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock'; +import { ConnectorFormSchema } from '../../sections/action_connector_form/types'; +import { ConnectorFormFieldsGlobal } from '../../sections/action_connector_form/connector_form_fields_global'; +import { ConnectorProvider } from '../../context/connector_context'; + +interface FormTestProviderProps { + children: React.ReactNode; + defaultValue?: Record; + onSubmit?: ({ data, isValid }: { data: FormData; isValid: boolean }) => Promise; + connectorServices?: ConnectorServices; +} + +type ConnectorFormTestProviderProps = Omit & { + connector: ConnectorFormSchema; +}; + +const ConnectorFormTestProviderComponent: React.FC = ({ + children, + connector, + onSubmit, + connectorServices, +}) => { + return ( + + + {children} + + ); +}; + +ConnectorFormTestProviderComponent.displayName = 'ConnectorFormTestProvider'; +export const ConnectorFormTestProvider = React.memo(ConnectorFormTestProviderComponent); + +const FormTestProviderComponent: React.FC = ({ + children, + defaultValue, + onSubmit, + connectorServices = { validateEmailAddresses: jest.fn() }, +}) => { + const { form } = useForm({ defaultValue }); + const { submit } = form; + + const onClick = useCallback(async () => { + const res = await submit(); + if (onSubmit) { + onSubmit(res); + } + }, [onSubmit, submit]); + + return ( + + +
{children}
+ +
+
+ ); +}; + +FormTestProviderComponent.displayName = 'FormTestProvider'; +export const FormTestProvider = React.memo(FormTestProviderComponent); + +export async function waitForComponentToPaint

(wrapper: ReactWrapper

, amount = 0) { + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, amount)); + wrapper.update(); + }); +} + +export const waitForComponentToUpdate = async () => + await act(async () => { + return Promise.resolve(); + }); + +type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult; +export interface AppMockRenderer { + render: UiRender; + coreStart: TriggersAndActionsUiServices; +} + +export const createAppMockRenderer = (): AppMockRenderer => { + const services = createStartServicesMock(); + const theme$ = of({ darkMode: false }); + + const AppWrapper: React.FC<{ children: React.ReactElement }> = ({ children }) => ( + + + {children} + + + ); + AppWrapper.displayName = 'AppWrapper'; + const render: UiRender = (ui, options) => { + return reactRender(ui, { + wrapper: AppWrapper, + ...options, + }); + }; + return { + coreStart: services, + render, + }; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/translations.ts index 3550121e81694..27a7d08b8c767 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/translations.ts @@ -7,52 +7,94 @@ import { i18n } from '@kbn/i18n'; -export const URL_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.requiredUrlText', +export const METHOD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.methodTextFieldLabel', { - defaultMessage: 'URL is required.', + defaultMessage: 'Method', } ); -export const URL_INVALID = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.invalidUrlTextField', +export const HAS_AUTH_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.hasAuthSwitchLabel', { - defaultMessage: 'URL is invalid.', + defaultMessage: 'Require authentication for this webhook', } ); -export const METHOD_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText', +export const URL_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.urlTextFieldLabel', { - defaultMessage: 'Method is required.', + defaultMessage: 'URL', } ); -export const USERNAME_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthUserNameText', +export const USERNAME_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.userTextFieldLabel', { - defaultMessage: 'Username is required.', + defaultMessage: 'Username', + } +); + +export const PASSWORD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.passwordTextFieldLabel', + { + defaultMessage: 'Password', + } +); + +export const ADD_HEADERS_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.viewHeadersSwitch', + { + defaultMessage: 'Add HTTP header', + } +); + +export const HEADER_KEY_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.headerKeyTextFieldLabel', + { + defaultMessage: 'Key', } ); -export const PASSWORD_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthPasswordText', +export const REMOVE_ITEM_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.removeHeaderIconLabel', { - defaultMessage: 'Password is required.', + defaultMessage: 'Key', } ); -export const PASSWORD_REQUIRED_FOR_USER = i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText', +export const ADD_HEADER_BTN = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.addHeaderButtonLabel', { - defaultMessage: 'Password is required when username is used.', + defaultMessage: 'Add header', } ); -export const USERNAME_REQUIRED_FOR_PASSWORD = i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredUserText', +export const HEADER_VALUE_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.headerValueTextFieldLabel', { - defaultMessage: 'Username is required when password is used.', + defaultMessage: 'Value', + } +); + +export const URL_INVALID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.error.invalidUrlTextField', + { + defaultMessage: 'URL is invalid.', + } +); + +export const METHOD_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText', + { + defaultMessage: 'Method is required.', + } +); + +export const USERNAME_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredAuthUserNameText', + { + defaultMessage: 'Username is required.', } ); 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 771786157ed4c..dfc3aae39586d 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 @@ -8,7 +8,6 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel } from '../../../../types'; -import { WebhookActionConnector } from '../types'; import { registrationServicesMock } from '../../../../mocks'; const ACTION_TYPE_ID = '.webhook'; @@ -30,140 +29,6 @@ describe('actionTypeRegistry.get() works', () => { }); }); -describe('webhook connector validation', () => { - test('connector validation succeeds when hasAuth is true and connector config is valid', async () => { - const actionConnector = { - secrets: { - user: 'user', - password: 'pass', - }, - id: 'test', - actionTypeId: '.webhook', - name: 'webhook', - isPreconfigured: false, - isDeprecated: false, - config: { - method: 'PUT', - url: 'http://test.com', - headers: { 'content-type': 'text' }, - hasAuth: true, - }, - } as WebhookActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - url: [], - method: [], - }, - }, - secrets: { - errors: { - user: [], - password: [], - }, - }, - }); - }); - - test('connector validation succeeds when hasAuth is false and connector config is valid', async () => { - const actionConnector = { - secrets: { - user: '', - password: '', - }, - id: 'test', - actionTypeId: '.webhook', - name: 'webhook', - isPreconfigured: false, - isDeprecated: false, - config: { - method: 'PUT', - url: 'http://test.com', - headers: { 'content-type': 'text' }, - hasAuth: false, - }, - } as WebhookActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - url: [], - method: [], - }, - }, - secrets: { - errors: { - user: [], - password: [], - }, - }, - }); - }); - - test('connector validation fails when connector config is not valid', async () => { - const actionConnector = { - secrets: { - user: 'user', - }, - id: 'test', - actionTypeId: '.webhook', - name: 'webhook', - config: { - method: 'PUT', - hasAuth: true, - }, - } as WebhookActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - url: ['URL is required.'], - method: [], - }, - }, - secrets: { - errors: { - user: [], - password: ['Password is required when username is used.'], - }, - }, - }); - }); - - test('connector validation fails when url in config is not valid', async () => { - const actionConnector = { - secrets: { - user: 'user', - password: 'pass', - }, - id: 'test', - actionTypeId: '.webhook', - name: 'webhook', - config: { - method: 'PUT', - url: 'invalid.url', - hasAuth: true, - }, - } as WebhookActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - url: ['URL is invalid.'], - method: [], - }, - }, - secrets: { - errors: { - user: [], - password: [], - }, - }, - }); - }); -}); - describe('webhook action params validation', () => { test('action params validation succeeds when action params is valid', async () => { const actionParams = { 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 a668f531a6d4c..5ee08cc027003 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,18 +7,8 @@ import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; -import { - ActionTypeModel, - GenericValidationResult, - ConnectorValidationResult, -} from '../../../../types'; -import { - WebhookActionParams, - WebhookConfig, - WebhookSecrets, - WebhookActionConnector, -} from '../types'; -import { isValidUrl } from '../../../lib/value_validators'; +import { ActionTypeModel, GenericValidationResult } from '../../../../types'; +import { WebhookActionParams, WebhookConfig, WebhookSecrets } from '../types'; export function getActionType(): ActionTypeModel< WebhookConfig, @@ -40,47 +30,6 @@ export function getActionType(): ActionTypeModel< defaultMessage: 'Webhook data', } ), - validateConnector: async ( - action: WebhookActionConnector - ): Promise< - ConnectorValidationResult, WebhookSecrets> - > => { - const translations = await import('./translations'); - const configErrors = { - url: new Array(), - method: new Array(), - }; - const secretsErrors = { - user: new Array(), - password: new Array(), - }; - const validationResult = { - config: { errors: configErrors }, - secrets: { errors: secretsErrors }, - }; - if (!action.config.url) { - configErrors.url.push(translations.URL_REQUIRED); - } - if (action.config.url && !isValidUrl(action.config.url)) { - configErrors.url = [...configErrors.url, translations.URL_INVALID]; - } - if (!action.config.method) { - configErrors.method.push(translations.METHOD_REQUIRED); - } - if (action.config.hasAuth && !action.secrets.user && !action.secrets.password) { - secretsErrors.user.push(translations.USERNAME_REQUIRED); - } - if (action.config.hasAuth && !action.secrets.user && !action.secrets.password) { - secretsErrors.password.push(translations.PASSWORD_REQUIRED); - } - if (action.secrets.user && !action.secrets.password) { - secretsErrors.password.push(translations.PASSWORD_REQUIRED_FOR_USER); - } - if (!action.secrets.user && action.secrets.password) { - secretsErrors.user.push(translations.USERNAME_REQUIRED_FOR_PASSWORD); - } - return validationResult; - }, validateParams: async ( actionParams: WebhookActionParams ): Promise> => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx index 533d6d9a9b605..d3b933e9e2dc4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx @@ -7,41 +7,42 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { WebhookActionConnector } from '../types'; import WebhookActionConnectorFields from './webhook_connectors'; +import { ConnectorFormTestProvider, waitForComponentToUpdate } from '../test_utils'; +import { act, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; describe('WebhookActionConnectorFields renders', () => { - test('all connector fields is rendered', () => { + test('all connector fields is rendered', async () => { const actionConnector = { - secrets: { - user: 'user', - password: 'pass', - }, - id: 'test', actionTypeId: '.webhook', - isPreconfigured: false, - isDeprecated: false, name: 'webhook', config: { method: 'PUT', - url: 'http:\\test', - headers: { 'content-type': 'text' }, + url: 'https://test.com', + headers: [{ key: 'content-type', value: 'text' }], hasAuth: true, }, - } as WebhookActionConnector; + secrets: { + user: 'user', + password: 'pass', + }, + isDeprecated: false, + }; + const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> + + {}} + /> + ); + + await waitForComponentToUpdate(); + expect(wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').length > 0).toBeTruthy(); - expect(wrapper.find('[data-test-subj="webhookHeaderText"]').length > 0).toBeTruthy(); wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').first().simulate('click'); expect(wrapper.find('[data-test-subj="webhookMethodSelect"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="webhookUrlText"]').length > 0).toBeTruthy(); @@ -49,94 +50,216 @@ describe('WebhookActionConnectorFields renders', () => { expect(wrapper.find('[data-test-subj="webhookPasswordInput"]').length > 0).toBeTruthy(); }); - test('should display a message on create to remember credentials', () => { - const actionConnector = { - secrets: {}, - actionTypeId: '.webhook', - isPreconfigured: false, - isDeprecated: false, - config: { - hasAuth: true, - }, - } as WebhookActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); - }); - - test('should display a message on edit to re-enter credentials', () => { + describe('Validation', () => { + const onSubmit = jest.fn(); const actionConnector = { - secrets: { - user: 'user', - password: 'pass', - }, - id: 'test', actionTypeId: '.webhook', - isPreconfigured: false, - isDeprecated: false, name: 'webhook', config: { method: 'PUT', - url: 'http:\\test', - headers: { 'content-type': 'text' }, + url: 'https://test.com', + headers: [{ key: 'content-type', value: 'text' }], hasAuth: true, }, - } as WebhookActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); - }); - - test('should display a message for missing secrets after import', () => { - const actionConnector = { secrets: { user: 'user', password: 'pass', }, - id: 'test', - actionTypeId: '.webhook', - isPreconfigured: false, isDeprecated: false, - isMissingSecrets: true, - name: 'webhook', - config: { - method: 'PUT', - url: 'http:\\test', - headers: { 'content-type': 'text' }, - hasAuth: true, - }, - } as WebhookActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0); + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const tests: Array<[string, string]> = [ + ['webhookUrlText', 'not-valid'], + ['webhookUserInput', ''], + ['webhookPasswordInput', ''], + ]; + + it('connector validation succeeds when connector config is valid', async () => { + const { getByTestId } = render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: { + actionTypeId: '.webhook', + name: 'webhook', + config: { + method: 'PUT', + url: 'https://test.com', + headers: [{ key: 'content-type', value: 'text' }], + hasAuth: true, + }, + secrets: { + user: 'user', + password: 'pass', + }, + __internal__: { + hasHeaders: true, + }, + isDeprecated: false, + }, + isValid: true, + }); + }); + + it('connector validation succeeds when auth=false', async () => { + const connector = { + ...actionConnector, + config: { + ...actionConnector.config, + hasAuth: false, + }, + }; + + const { getByTestId } = render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: { + actionTypeId: '.webhook', + name: 'webhook', + config: { + method: 'PUT', + url: 'https://test.com', + headers: [{ key: 'content-type', value: 'text' }], + hasAuth: false, + }, + __internal__: { + hasHeaders: true, + }, + isDeprecated: false, + }, + isValid: true, + }); + }); + + it('connector validation succeeds without headers', async () => { + const connector = { + ...actionConnector, + config: { + method: 'PUT', + url: 'https://test.com', + hasAuth: true, + }, + }; + + const { getByTestId } = render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: { + actionTypeId: '.webhook', + name: 'webhook', + config: { + method: 'PUT', + url: 'https://test.com', + hasAuth: true, + }, + secrets: { + user: 'user', + password: 'pass', + }, + __internal__: { + hasHeaders: false, + }, + isDeprecated: false, + }, + isValid: true, + }); + }); + + it('validates correctly if the method is empty', async () => { + const connector = { + ...actionConnector, + config: { + ...actionConnector.config, + method: '', + }, + }; + + const res = render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(res.getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }); + }); + + it.each(tests)('validates correctly %p', async (field, value) => { + const connector = { + ...actionConnector, + config: { + ...actionConnector.config, + headers: [], + }, + }; + + const res = render( + + {}} + /> + + ); + + await act(async () => { + await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, { + delay: 10, + }); + }); + + await act(async () => { + userEvent.click(res.getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }); + }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx index fbd3bdbef2e71..7981f8fa4fa78 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx @@ -5,285 +5,92 @@ * 2.0. */ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { - EuiFieldPassword, - EuiFieldText, - EuiFormRow, - EuiSelect, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiButtonIcon, - EuiDescriptionList, - EuiDescriptionListDescription, - EuiDescriptionListTitle, EuiTitle, - EuiSwitch, EuiButtonEmpty, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import { + UseArray, + UseField, + useFormContext, + useFormData, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { + Field, + SelectField, + TextField, + ToggleField, +} from '@kbn/es-ui-shared-plugin/static/forms/components'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; import { ActionConnectorFieldsProps } from '../../../../types'; -import { WebhookActionConnector } from '../types'; -import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label'; +import * as i18n from './translations'; +import { PasswordField } from '../../password_field'; const HTTP_VERBS = ['post', 'put']; - -const WebhookActionConnectorFields: React.FunctionComponent< - ActionConnectorFieldsProps -> = ({ action, editActionConfig, editActionSecrets, errors, readOnly }) => { - const { user, password } = action.secrets; - const { method, url, headers, hasAuth } = action.config; - - const [httpHeaderKey, setHttpHeaderKey] = useState(''); - const [httpHeaderValue, setHttpHeaderValue] = useState(''); - const [hasHeaders, setHasHeaders] = useState(false); - - useEffect(() => { - if (!action.id) { - editActionConfig('hasAuth', true); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - if (!method) { - editActionConfig('method', 'post'); // set method to POST by default - } - - const headerErrors = { - keyHeader: new Array(), - valueHeader: new Array(), - }; - if (!httpHeaderKey && httpHeaderValue) { - headerErrors.keyHeader.push( - i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHeaderKeyText', - { - defaultMessage: 'Key is required.', - } - ) - ); - } - if (httpHeaderKey && !httpHeaderValue) { - headerErrors.valueHeader.push( - i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHeaderValueText', - { - defaultMessage: 'Value is required.', - } - ) - ); - } - const hasHeaderErrors: boolean = - (headerErrors.keyHeader !== undefined && - headerErrors.valueHeader !== undefined && - headerErrors.keyHeader.length > 0) || - headerErrors.valueHeader.length > 0; - - function addHeader() { - if (headers && !!Object.keys(headers).find((key) => key === httpHeaderKey)) { - return; - } - const updatedHeaders = headers - ? { ...headers, [httpHeaderKey]: httpHeaderValue } - : { [httpHeaderKey]: httpHeaderValue }; - editActionConfig('headers', updatedHeaders); - setHttpHeaderKey(''); - setHttpHeaderValue(''); - } - - function viewHeaders() { - setHasHeaders(!hasHeaders); - if (!hasHeaders && !headers) { - editActionConfig('headers', {}); - } - } - - function removeHeader(keyToRemove: string) { - const updatedHeaders = Object.keys(headers) - .filter((key) => key !== keyToRemove) - .reduce((headerToRemove: Record, key: string) => { - headerToRemove[key] = headers[key]; - return headerToRemove; - }, {}); - editActionConfig('headers', updatedHeaders); - } - - let headerControl; - if (hasHeaders) { - headerControl = ( - <> - -

- -
- - - - - - { - setHttpHeaderKey(e.target.value); - }} - /> - - - - - { - setHttpHeaderValue(e.target.value); - }} - /> - - - - - addHeader()} - > - - - - - - - ); - } - - const headersList = Object.keys(headers || {}).map((key: string) => { - return ( - - - removeHeader(key)} - /> - - - - {key} - {headers[key]} - - - - ); +const { emptyField, urlField } = fieldValidators; + +const WebhookActionConnectorFields: React.FunctionComponent = ({ + readOnly, +}) => { + const { getFieldDefaultValue } = useFormContext(); + const [{ config, __internal__ }] = useFormData({ + watch: ['config.hasAuth', '__internal__.hasHeaders'], }); - const isUrlInvalid: boolean = - errors.url !== undefined && errors.url.length > 0 && url !== undefined; - const isPasswordInvalid: boolean = - password !== undefined && errors.password !== undefined && errors.password.length > 0; - const isUserInvalid: boolean = - user !== undefined && errors.user !== undefined && errors.user.length > 0; + const hasHeadersDefaultValue = !!getFieldDefaultValue('config.headers'); + + const hasAuth = config == null ? true : config.hasAuth; + const hasHeaders = __internal__ != null ? __internal__.hasHeaders : false; return ( <> - - ({ text: verb.toUpperCase(), value: verb }))} - onChange={(e) => { - editActionConfig('method', e.target.value); - }} - /> - + ({ text: verb.toUpperCase(), value: verb })), + fullWidth: true, + readOnly, + }, + }} + /> - - { - editActionConfig('url', e.target.value); - }} - onBlur={() => { - if (!url) { - editActionConfig('url', ''); - } - }} - /> - + @@ -298,140 +105,115 @@ const WebhookActionConnectorFields: React.FunctionComponent< - { - editActionConfig('hasAuth', e.target.checked); - if (!e.target.checked) { - editActionSecrets('user', null); - editActionSecrets('password', null); - } + {hasAuth ? ( - <> - {getEncryptedFieldNotifyLabel( - !action.id, - 2, - action.isMissingSecrets ?? false, - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.webhookAction.reenterValuesLabel', - { - defaultMessage: - 'Username and password are encrypted. Please reenter values for these fields.', - } - ) - )} - - - + + - { - editActionSecrets('user', e.target.value); - }} - onBlur={() => { - if (!user) { - editActionSecrets('user', ''); - } - }} - /> - - - - - { - editActionSecrets('password', e.target.value); - }} - onBlur={() => { - if (!password) { - editActionSecrets('password', ''); - } - }} - /> - - - - + validator: emptyField(i18n.USERNAME_REQUIRED), + }, + ], + }} + component={Field} + componentProps={{ + euiFieldProps: { readOnly, 'data-test-subj': 'webhookUserInput', fullWidth: true }, + }} + /> + + + + + ) : null} - viewHeaders()} + - -
- {Object.keys(headers || {}).length > 0 ? ( - <> - - -
- -
-
- - {headersList} - - ) : null} - - {hasHeaders && headerControl} - -
+ {hasHeaders ? ( + + {({ items, addItem, removeItem }) => { + return ( + <> + {items.map((item) => ( + + + + + + + + + removeItem(item.id)} + iconType="minusInCircle" + aria-label={i18n.REMOVE_ITEM_LABEL} + style={{ marginTop: '28px' }} + /> + + + ))} + + + {i18n.ADD_HEADER_BTN} + + + + ); + }} + + ) : null} ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/translations.ts index 51626b6a31ea6..d883b4d98c81b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/translations.ts @@ -7,44 +7,72 @@ import { i18n } from '@kbn/i18n'; -export const URL_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.error.requiredUrlText', +export const BASIC_AUTH_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.connectorSettingsLabel', { - defaultMessage: 'URL is required.', + defaultMessage: 'Select the authentication method used when setting up the xMatters trigger.', } ); -export const URL_INVALID = i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.error.invalidUrlTextField', +export const BASIC_AUTH_BUTTON_GROUP_LEGEND = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.basicAuthButtonGroupLegend', { - defaultMessage: 'URL is invalid.', + defaultMessage: 'Basic Authentication', + } +); + +export const URL_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.urlLabel', + { + defaultMessage: 'Initiation URL', + } +); + +export const USERNAME_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.userTextFieldLabel', + { + defaultMessage: 'Username', } ); -export const USERNAME_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredAuthUserNameText', +export const PASSWORD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.passwordTextFieldLabel', { - defaultMessage: 'Username is required.', + defaultMessage: 'Password', } ); -export const PASSWORD_REQUIRED = i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredAuthPasswordText', +export const BASIC_AUTH_BUTTON_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.basicAuthLabel', { - defaultMessage: 'Password is required.', + defaultMessage: 'Basic Authentication', } ); -export const PASSWORD_REQUIRED_FOR_USER = i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredPasswordText', +export const URL_AUTH_BUTTON_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.urlAuthLabel', { - defaultMessage: 'Password is required when username is used.', + defaultMessage: 'URL Authentication', + } +); + +export const URL_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.error.requiredUrlText', + { + defaultMessage: 'URL is required.', + } +); + +export const URL_INVALID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.error.invalidUrlTextField', + { + defaultMessage: 'URL is invalid.', } ); -export const USERNAME_REQUIRED_FOR_PASSWORD = i18n.translate( - 'xpack.triggersActionsUI.sections.addAction.xmattersAction.error.requiredUserText', +export const USERNAME_INVALID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.error.invalidUsernameTextField', { - defaultMessage: 'Username is required when password is used.', + defaultMessage: 'Username is invalid.', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters.test.tsx index 7e9dbc4cf885a..980fa90caf4bb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters.test.tsx @@ -8,7 +8,6 @@ import { TypeRegistry } from '../../../type_registry'; import { registerBuiltInActionTypes } from '..'; import { ActionTypeModel } from '../../../../types'; -import { XmattersActionConnector } from '../types'; import { registrationServicesMock } from '../../../../mocks'; const ACTION_TYPE_ID = '.xmatters'; @@ -30,138 +29,6 @@ describe('actionTypeRegistry.get() works', () => { }); }); -describe('xmatters connector validation', () => { - test('connector validation succeeds when usesBasic is true and connector config is valid', async () => { - const actionConnector = { - secrets: { - user: 'user', - password: 'pass', - }, - id: 'test', - actionTypeId: '.xmatters', - name: 'xmatters', - isPreconfigured: false, - isDeprecated: false, - config: { - configUrl: 'http://test.com', - usesBasic: true, - }, - } as XmattersActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - configUrl: [], - }, - }, - secrets: { - errors: { - user: [], - password: [], - secretsUrl: [], - }, - }, - }); - }); - - test('connector validation succeeds when usesBasic is false and connector config is valid', async () => { - const actionConnector = { - secrets: { - user: '', - password: '', - secretsUrl: 'https://test.com?apiKey=someKey', - }, - id: 'test', - actionTypeId: '.xmatters', - name: 'xmatters', - isPreconfigured: false, - isDeprecated: false, - config: { - usesBasic: false, - }, - } as XmattersActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - configUrl: [], - }, - }, - secrets: { - errors: { - user: [], - password: [], - secretsUrl: [], - }, - }, - }); - }); - - test('connector validation fails when connector config is not valid', async () => { - const actionConnector = { - secrets: { - user: 'user', - }, - id: 'test', - actionTypeId: '.xmatters', - name: 'xmatters', - config: { - usesBasic: true, - }, - isPreconfigured: false, - isDeprecated: false, - } as XmattersActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - configUrl: ['URL is required.'], - }, - }, - secrets: { - errors: { - user: [], - password: ['Password is required when username is used.'], - secretsUrl: [], - }, - }, - }); - }); - - test('connector validation fails when url in config is not valid', async () => { - const actionConnector = { - secrets: { - user: 'user', - password: 'pass', - }, - id: 'test', - actionTypeId: '.xmatters', - name: 'xmatters', - config: { - configUrl: 'invalid.url', - usesBasic: true, - }, - isPreconfigured: false, - isDeprecated: false, - } as XmattersActionConnector; - - expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ - config: { - errors: { - configUrl: ['URL is invalid.'], - }, - }, - secrets: { - errors: { - user: [], - password: [], - secretsUrl: [], - }, - }, - }); - }); -}); - describe('xmatters action params validation', () => { test('action params validation succeeds when action params is valid', async () => { const actionParams = { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters.tsx index d638056ba31d9..2ba8d78d90aec 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters.tsx @@ -7,18 +7,8 @@ import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; -import { - ActionTypeModel, - GenericValidationResult, - ConnectorValidationResult, -} from '../../../../types'; -import { - XmattersActionParams, - XmattersConfig, - XmattersSecrets, - XmattersActionConnector, -} from '../types'; -import { isValidUrl } from '../../../lib/value_validators'; +import { ActionTypeModel, GenericValidationResult } from '../../../../types'; +import { XmattersActionParams, XmattersConfig, XmattersSecrets } from '../types'; export function getActionType(): ActionTypeModel< XmattersConfig, @@ -40,48 +30,6 @@ export function getActionType(): ActionTypeModel< defaultMessage: 'xMatters data', } ), - validateConnector: async ( - action: XmattersActionConnector - ): Promise, XmattersSecrets>> => { - const translations = await import('./translations'); - const configErrors = { - configUrl: new Array(), - }; - const secretsErrors = { - user: new Array(), - password: new Array(), - secretsUrl: new Array(), - }; - const validationResult = { - config: { errors: configErrors }, - secrets: { errors: secretsErrors }, - }; - // basic auth validation - if (!action.config.configUrl && action.config.usesBasic) { - configErrors.configUrl.push(translations.URL_REQUIRED); - } - if (action.config.usesBasic && !action.secrets.user && !action.secrets.password) { - secretsErrors.user.push(translations.USERNAME_REQUIRED); - secretsErrors.password.push(translations.PASSWORD_REQUIRED); - } - if (action.config.configUrl && !isValidUrl(action.config.configUrl)) { - configErrors.configUrl = [...configErrors.configUrl, translations.URL_INVALID]; - } - if (action.config.usesBasic && action.secrets.user && !action.secrets.password) { - secretsErrors.password.push(translations.PASSWORD_REQUIRED_FOR_USER); - } - if (action.config.usesBasic && !action.secrets.user && action.secrets.password) { - secretsErrors.user.push(translations.USERNAME_REQUIRED_FOR_PASSWORD); - } - // API Key auth validation - if (!action.config.usesBasic && !action.secrets.secretsUrl) { - secretsErrors.secretsUrl.push(translations.URL_REQUIRED); - } - if (action.secrets.secretsUrl && !isValidUrl(action.secrets.secretsUrl)) { - secretsErrors.secretsUrl.push(translations.URL_INVALID); - } - return validationResult; - }, validateParams: async ( actionParams: XmattersActionParams ): Promise< diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters_connectors.test.tsx index 7da6cfe0a4a37..cef4c2c2bef81 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters_connectors.test.tsx @@ -7,70 +7,72 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { XmattersActionConnector } from '../types'; import XmattersActionConnectorFields from './xmatters_connectors'; +import { ConnectorFormTestProvider, waitForComponentToUpdate } from '../test_utils'; +import userEvent from '@testing-library/user-event'; +import { act } from 'react-dom/test-utils'; +import { render } from '@testing-library/react'; describe('XmattersActionConnectorFields renders', () => { - test('all connector fields is rendered', () => { + test('all connector fields is rendered', async () => { const actionConnector = { - secrets: { - user: 'user', - password: 'pass', - }, - id: 'test', actionTypeId: '.xmatters', - isPreconfigured: false, - isDeprecated: false, name: 'xmatters', config: { - configUrl: 'http:\\test', + configUrl: 'https://test.com', usesBasic: true, }, - } as XmattersActionConnector; + secrets: { + user: 'user', + password: 'pass', + }, + isDeprecated: false, + }; + const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> + + {}} + /> + ); - expect(wrapper.find('[data-test-subj="xmattersUrlText"]').length > 0).toBeTruthy(); + + await waitForComponentToUpdate(); + + expect(wrapper.find('[data-test-subj="config.configUrl"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="xmattersUserInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="xmattersPasswordInput"]').length > 0).toBeTruthy(); }); test('should show only basic auth info when basic selected', () => { const actionConnector = { - secrets: { - user: 'user', - password: 'pass', - }, id: 'test', actionTypeId: '.xmatters', - isPreconfigured: false, - isDeprecated: false, name: 'xmatters', config: { - configUrl: 'http:\\test', + configUrl: 'https://test.com', usesBasic: true, }, - } as XmattersActionConnector; + secrets: { + user: 'user', + password: 'pass', + }, + isDeprecated: false, + }; + const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> + + {}} + /> + ); - expect(wrapper.find('[data-test-subj="xmattersUrlText"]').length > 0).toBeTruthy(); + + expect(wrapper.find('[data-test-subj="config.configUrl"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="xmattersUserInput"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="xmattersPasswordInput"]').length > 0).toBeTruthy(); }); @@ -78,7 +80,7 @@ describe('XmattersActionConnectorFields renders', () => { test('should show only url auth info when url selected', () => { const actionConnector = { secrets: { - secretsUrl: 'http:\\test', + secretsUrl: 'https://test.com', }, id: 'test', actionTypeId: '.xmatters', @@ -88,136 +90,175 @@ describe('XmattersActionConnectorFields renders', () => { config: { usesBasic: false, }, - } as XmattersActionConnector; + }; + const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> + + {}} + /> + ); - expect(wrapper.find('[data-test-subj="xmattersUrlText"]').length > 0).toBeTruthy(); + + expect(wrapper.find('[data-test-subj="secrets.secretsUrl"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="xmattersUserInput"]').length === 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="xmattersPasswordInput"]').length === 0).toBeTruthy(); }); - test('should display a message on create to remember credentials', () => { - const actionConnector = { - secrets: {}, - actionTypeId: '.xmatters', - isPreconfigured: false, - isDeprecated: false, - config: { - usesBasic: true, - }, - } as XmattersActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); - }); - - test('should display a message on edit to re-enter credentials, Basic Auth', () => { - const actionConnector = { - secrets: { - user: 'user', - password: 'pass', - }, - id: 'test', + describe('Validation', () => { + const onSubmit = jest.fn(); + const basicAuthConnector = { actionTypeId: '.xmatters', - isPreconfigured: false, - isDeprecated: false, name: 'xmatters', config: { - configUrl: 'http:\\test', + configUrl: 'https://test.com', usesBasic: true, }, - } as XmattersActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); - }); - - test('should display a message for missing secrets after import', () => { - const actionConnector = { secrets: { user: 'user', password: 'pass', }, - id: 'test', - actionTypeId: '.xmatters', - isPreconfigured: false, isDeprecated: false, - isMissingSecrets: true, - name: 'xmatters', - config: { - configUrl: 'http:\\test', - usesBasic: true, - }, - } as XmattersActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0); - }); + }; - test('should display a message on edit to re-enter credentials, URL Auth', () => { - const actionConnector = { - secrets: { - secretsUrl: 'http:\\test?apiKey=someKey', - }, - id: 'test', - actionTypeId: '.xmatters', - isPreconfigured: false, - isDeprecated: false, - name: 'xmatters', + const urlAuthConnector = { + ...basicAuthConnector, config: { usesBasic: false, }, - } as XmattersActionConnector; - const wrapper = mountWithIntl( - {}} - editActionSecrets={() => {}} - readOnly={false} - setCallbacks={() => {}} - isEdit={false} - /> - ); - expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); - expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); + secrets: { + secretsUrl: 'https://test.com', + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const basicAuthTests: Array<[string, string]> = [ + ['config.configUrl', 'not-valid'], + ['xmattersUserInput', ''], + ['xmattersPasswordInput', ''], + ]; + + const urlAuthTests: Array<[string, string]> = [['secrets.secretsUrl', 'not-valid']]; + + it('connector validation succeeds when connector config is valid and uses basic auth', async () => { + const { getByTestId } = render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: { + actionTypeId: '.xmatters', + name: 'xmatters', + config: { + configUrl: 'https://test.com', + usesBasic: true, + }, + secrets: { + user: 'user', + password: 'pass', + }, + __internal__: { + auth: 'Basic Authentication', + }, + isDeprecated: false, + }, + isValid: true, + }); + }); + + it('connector validation succeeds when connector config is valid and uses url auth', async () => { + const { getByTestId } = render( + + {}} + /> + + ); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toBeCalledWith({ + data: { + actionTypeId: '.xmatters', + name: 'xmatters', + config: { + usesBasic: false, + }, + secrets: { + secretsUrl: 'https://test.com', + }, + __internal__: { auth: 'URL Authentication' }, + isDeprecated: false, + }, + isValid: true, + }); + }); + + it.each(basicAuthTests)('validates correctly %p', async (field, value) => { + const res = render( + + {}} + /> + + ); + + await act(async () => { + await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, { + delay: 10, + }); + }); + + await act(async () => { + userEvent.click(res.getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }); + }); + + it.each(urlAuthTests)('validates correctly %p', async (field, value) => { + const res = render( + + {}} + /> + + ); + + await act(async () => { + await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, { + delay: 10, + }); + }); + + await act(async () => { + userEvent.click(res.getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }); + }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters_connectors.tsx index 20ade11303350..247a700876d14 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/xmatters/xmatters_connectors.tsx @@ -5,78 +5,94 @@ * 2.0. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; import { - EuiFieldPassword, - EuiFieldText, - EuiFormRow, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiTitle, - EuiButtonGroup, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; + UseField, + useFormContext, + useFormData, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { TextField } from '@kbn/es-ui-shared-plugin/static/forms/components'; import { ActionConnectorFieldsProps } from '../../../../types'; -import { XmattersActionConnector, XmattersAuthenticationType } from '../types'; -import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label'; +import { XmattersAuthenticationType } from '../types'; +import { ButtonGroupField } from '../../button_group_field'; +import * as i18n from './translations'; +import { PasswordField } from '../../password_field'; +import { HiddenField } from '../../hidden_field'; -const XmattersActionConnectorFields: React.FunctionComponent< - ActionConnectorFieldsProps -> = ({ action, editActionConfig, editActionSecrets, errors, readOnly }) => { - const { user, password, secretsUrl } = action.secrets; - const { configUrl, usesBasic } = action.config; +const { emptyField, urlField } = fieldValidators; - useEffect(() => { - if (!action.id) { - editActionConfig('usesBasic', true); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); +const isBasicAuth = (auth: { auth: string } | null | undefined) => { + if (auth == null) { + return true; + } - const isUrlInvalid: boolean = usesBasic - ? errors.configUrl !== undefined && errors.configUrl.length > 0 && configUrl !== undefined - : errors.secretsUrl !== undefined && errors.secretsUrl.length > 0 && secretsUrl !== undefined; - const isPasswordInvalid: boolean = - password !== undefined && errors.password !== undefined && errors.password.length > 0; - const isUserInvalid: boolean = - user !== undefined && errors.user !== undefined && errors.user.length > 0; + return auth.auth === XmattersAuthenticationType.Basic ? true : false; +}; - const authenticationButtons = [ - { - id: XmattersAuthenticationType.Basic, - label: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.basicAuthLabel', - { - defaultMessage: 'Basic Authentication', - } - ), - }, - { - id: XmattersAuthenticationType.URL, - label: i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.urlAuthLabel', - { - defaultMessage: 'URL Authentication', - } - ), - }, - ]; +const authenticationButtons = [ + { + id: XmattersAuthenticationType.Basic, + label: i18n.BASIC_AUTH_BUTTON_LABEL, + }, + { + id: XmattersAuthenticationType.URL, + label: i18n.URL_AUTH_BUTTON_LABEL, + }, +]; - let initialState; - if (typeof usesBasic === 'undefined') { - initialState = XmattersAuthenticationType.Basic; - } else { - initialState = usesBasic ? XmattersAuthenticationType.Basic : XmattersAuthenticationType.URL; - if (usesBasic) { - editActionSecrets('secretsUrl', ''); - } else { - editActionConfig('configUrl', ''); - } - } - const [selectedAuth, setSelectedAuth] = useState(initialState); +const XmattersUrlField: React.FC<{ path: string; readOnly: boolean }> = ({ path, readOnly }) => { + return ( + + ), + validations: [ + { + validator: urlField(i18n.URL_INVALID), + }, + ], + }} + componentProps={{ + euiFieldProps: { 'data-test-subj': path, readOnly }, + }} + /> + ); +}; + +const XmattersActionConnectorFields: React.FunctionComponent = ({ + readOnly, +}) => { + const { setFieldValue, getFieldDefaultValue } = useFormContext(); + const [{ config, __internal__ }] = useFormData({ + watch: ['config.usesBasic', '__internal__.auth'], + }); + + const usesBasicDefaultValue = + getFieldDefaultValue('config.usesBasic') ?? true; + + const selectedAuthDefaultValue = usesBasicDefaultValue + ? XmattersAuthenticationType.Basic + : XmattersAuthenticationType.URL; + + const selectedAuth = + config != null && !config.usesBasic + ? XmattersAuthenticationType.URL + : XmattersAuthenticationType.Basic; + + useEffect(() => { + setFieldValue('config.usesBasic', isBasicAuth(__internal__)); + }, [__internal__, setFieldValue]); return ( <> @@ -89,92 +105,29 @@ const XmattersActionConnectorFields: React.FunctionComponent< - -

- -

-
- - { - if (id === XmattersAuthenticationType.Basic) { - setSelectedAuth(XmattersAuthenticationType.Basic); - editActionConfig('usesBasic', true); - editActionSecrets('secretsUrl', ''); - } else { - setSelectedAuth(XmattersAuthenticationType.URL); - editActionConfig('usesBasic', false); - editActionConfig('configUrl', ''); - editActionSecrets('user', ''); - editActionSecrets('password', ''); - } - }} /> + {selectedAuth === XmattersAuthenticationType.URL ? ( - <> - {getEncryptedFieldNotifyLabel( - !action.id, - 1, - action.isMissingSecrets ?? false, - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.reenterUrlAuthValuesLabel', - { - defaultMessage: 'URL is encrypted. Please reenter values for this field.', - } - ) - )} - + + + + + ) : null} - - - - } - > - { - if (selectedAuth === XmattersAuthenticationType.Basic) { - editActionConfig('configUrl', e.target.value); - } else { - editActionSecrets('secretsUrl', e.target.value); - } - }} - /> - - - {selectedAuth === XmattersAuthenticationType.Basic ? ( <> + + + + +

@@ -186,83 +139,38 @@ const XmattersActionConnectorFields: React.FunctionComponent<

- {getEncryptedFieldNotifyLabel( - !action.id, - 2, - action.isMissingSecrets ?? false, - i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.xmattersAction.reenterBasicAuthValuesLabel', - { - defaultMessage: - 'User and password are encrypted. Please reenter values for these fields.', - } - ) - )} - - { - editActionSecrets('user', e.target.value); - }} - onBlur={() => { - if (!user) { - editActionSecrets('user', ''); - } - }} - /> - + - - { - editActionSecrets('password', e.target.value); - }} - onBlur={() => { - if (!password) { - editActionSecrets('password', ''); - } - }} - /> - + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/button_group_field.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/button_group_field.tsx new file mode 100644 index 0000000000000..475329049d782 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/button_group_field.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, ReactNode } from 'react'; +import { EuiButtonGroup, EuiFormRow } from '@elastic/eui'; +import { + getFieldValidityAndErrorMessage, + UseField, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; +import { i18n } from '@kbn/i18n'; + +const { emptyField } = fieldValidators; + +const getFieldConfig = ({ label, defaultValue }: { label: string; defaultValue?: string }) => ({ + label, + defaultValue, + validations: [ + { + validator: emptyField( + i18n.translate('xpack.triggersActionsUI.components.buttonGroupField.error.requiredField', { + values: { label }, + defaultMessage: '{label} is required.', + }) + ), + }, + ], +}); + +interface Props { + path: string; + label: string; + defaultValue?: string; + helpText?: string | ReactNode; + [key: string]: any; +} + +const ButtonGroupFieldComponent: React.FC = ({ + path, + label, + helpText, + defaultValue, + ...rest +}) => { + return ( + path={path} config={getFieldConfig({ label, defaultValue })}> + {(field) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + return ( + + + + ); + }} +
+ ); +}; + +export const ButtonGroupField = memo(ButtonGroupFieldComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/get_encrypted_field_notify_label.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/get_encrypted_field_notify_label.test.tsx deleted file mode 100644 index d627f75080f5b..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/get_encrypted_field_notify_label.test.tsx +++ /dev/null @@ -1,70 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getEncryptedFieldNotifyLabel } from './get_encrypted_field_notify_label'; - -describe('getEncryptedFieldNotifyLabel', () => { - test('renders proper notify label when isCreate equals true', () => { - const jsxObject = getEncryptedFieldNotifyLabel(true, 2, false, 'test'); - - expect( - jsxObject.props.children.filter( - (child: any) => child.props['data-test-subj'] === 'rememberValuesMessage' - ).length - ).toBeGreaterThan(0); - expect( - jsxObject.props.children.filter( - (child: any) => child.props['data-test-subj'] === 'missingSecretsMessage' - ).length - ).toBe(0); - expect( - jsxObject.props.children.filter( - (child: any) => child.props['data-test-subj'] === 'reenterValuesMessage' - ).length - ).toBe(0); - }); - - test('renders proper notify label when secrets is missing', () => { - const jsxObject = getEncryptedFieldNotifyLabel(false, 2, true, 'test'); - - expect( - jsxObject.props.children.filter( - (child: any) => child.props['data-test-subj'] === 'rememberValuesMessage' - ).length - ).toBe(0); - expect( - jsxObject.props.children.filter( - (child: any) => child.props['data-test-subj'] === 'missingSecretsMessage' - ).length - ).toBeGreaterThan(0); - expect( - jsxObject.props.children.filter( - (child: any) => child.props['data-test-subj'] === 'reenterValuesMessage' - ).length - ).toBe(0); - }); - - test('renders proper notify label when isCreate false (edit mode) and isMissingSecrets false', () => { - const jsxObject = getEncryptedFieldNotifyLabel(false, 2, false, 'test'); - - expect( - jsxObject.props.children.filter( - (child: any) => child.props['data-test-subj'] === 'rememberValuesMessage' - ).length - ).toBe(0); - expect( - jsxObject.props.children.filter( - (child: any) => child.props['data-test-subj'] === 'missingSecretsMessage' - ).length - ).toBe(0); - expect( - jsxObject.props.children.filter( - (child: any) => child.props['data-test-subj'] === 'reenterValuesMessage' - ).length - ).toBeGreaterThan(0); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/get_encrypted_field_notify_label.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/get_encrypted_field_notify_label.tsx deleted file mode 100644 index af23c471f2b41..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/get_encrypted_field_notify_label.tsx +++ /dev/null @@ -1,68 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiSpacer, EuiCallOut, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; - -export const getEncryptedFieldNotifyLabel = ( - isCreate: boolean, - encryptedFieldsLength: number, - isMissingSecrets: boolean, - reEnterDefaultMessage: string -) => { - if (isMissingSecrets) { - return ( - <> - - - - - ); - } - if (isCreate) { - return ( - <> - - - - - - - ); - } - return ( - <> - - - - - ); -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/hidden_field.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/hidden_field.tsx new file mode 100644 index 0000000000000..89385be8e5e1c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/hidden_field.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { UseField, UseFieldProps } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; + +const HiddenFieldComponent = (props: UseFieldProps) => { + return ( + {...props}> + {(field) => { + /** + * This is a hidden field. We return null so we do not render + * any field on the form + */ + return null; + }} +
+ ); +}; + +export const HiddenField = memo(HiddenFieldComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/password_field.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/password_field.tsx new file mode 100644 index 0000000000000..841e1fe7b40f5 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/password_field.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, ReactNode } from 'react'; +import { EuiFieldPassword, EuiFormRow } from '@elastic/eui'; +import { + getFieldValidityAndErrorMessage, + UseField, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; +import { i18n } from '@kbn/i18n'; + +const { emptyField } = fieldValidators; + +const getFieldConfig = ({ label, validate }: { label: string; validate: boolean }) => ({ + label, + validations: [ + ...(validate + ? [ + { + validator: emptyField( + i18n.translate( + 'xpack.triggersActionsUI.components.passwordField.error.requiredNameText', + { + values: { label }, + defaultMessage: '{label} is required.', + } + ) + ), + }, + ] + : []), + ], +}); + +interface PasswordFieldProps { + path: string; + label: string; + helpText?: string | ReactNode; + validate?: boolean; + isLoading?: boolean; + [key: string]: any; +} + +const PasswordFieldComponent: React.FC = ({ + path, + label, + helpText, + validate = true, + isLoading, + ...rest +}) => { + return ( + path={path} config={getFieldConfig({ label, validate })}> + {(field) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + return ( + + + + ); + }} +
+ ); +}; + +export const PasswordField = memo(PasswordFieldComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/simple_connector_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/simple_connector_form.test.tsx new file mode 100644 index 0000000000000..62745a6eb0995 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/simple_connector_form.test.tsx @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { act, render, RenderResult } from '@testing-library/react'; +import { FormTestProvider } from './builtin_action_types/test_utils'; +import { + ConfigFieldSchema, + SecretsFieldSchema, + SimpleConnectorForm, +} from './simple_connector_form'; +import userEvent from '@testing-library/user-event'; + +const fillForm = async ({ getByTestId }: RenderResult) => { + await act(async () => { + await userEvent.type(getByTestId('config.url-input'), 'https://example.com', { + delay: 10, + }); + }); + + await act(async () => { + await userEvent.type(getByTestId('config.test-config-input'), 'My text field', { + delay: 10, + }); + }); + + await act(async () => { + await userEvent.type(getByTestId('secrets.username-input'), 'elastic', { + delay: 10, + }); + }); + + await act(async () => { + await userEvent.type(getByTestId('secrets.password-input'), 'changeme', { + delay: 10, + }); + }); +}; + +describe('SimpleConnectorForm', () => { + const configFormSchema: ConfigFieldSchema[] = [ + { id: 'url', label: 'Url', isUrlField: true }, + { id: 'test-config', label: 'Test config', helpText: 'Test help text' }, + ]; + const secretsFormSchema: SecretsFieldSchema[] = [ + { id: 'username', label: 'Username' }, + { id: 'password', label: 'Password', isPasswordField: true }, + ]; + + const onSubmit = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly', async () => { + const { getByText } = render( + + + + ); + + expect(getByText('Url')).toBeInTheDocument(); + expect(getByText('Test config')).toBeInTheDocument(); + expect(getByText('Test help text')).toBeInTheDocument(); + + expect(getByText('Authentication')).toBeInTheDocument(); + expect(getByText('Username')).toBeInTheDocument(); + expect(getByText('Password')).toBeInTheDocument(); + }); + + it('submits correctly', async () => { + const res = render( + + + + ); + + await fillForm(res); + + await act(async () => { + userEvent.click(res.getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ + data: { + config: { + 'test-config': 'My text field', + url: 'https://example.com', + }, + secrets: { + password: 'changeme', + username: 'elastic', + }, + }, + isValid: true, + }); + }); + + describe('Validation', () => { + const tests: Array<[string, string]> = [ + ['config.url-input', 'not-valid'], + ['config.test-config-input', ''], + ['secrets.username-input', ''], + ['secrets.password-input', ''], + ]; + + it.each(tests)('validates correctly %p', async (field, value) => { + const res = render( + + + + ); + + await fillForm(res); + + await act(async () => { + await userEvent.type(res.getByTestId(field), `{selectall}{backspace}${value}`, { + delay: 10, + }); + }); + + await act(async () => { + userEvent.click(res.getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ data: {}, isValid: false }); + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/simple_connector_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/simple_connector_form.tsx new file mode 100644 index 0000000000000..6bbf88ebc104d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/simple_connector_form.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import { getUseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; +import { i18n } from '@kbn/i18n'; +import { PasswordField } from './password_field'; + +export interface CommonFieldSchema { + id: string; + label: string; + helpText?: string; +} + +export interface ConfigFieldSchema extends CommonFieldSchema { + isUrlField?: boolean; +} + +export interface SecretsFieldSchema extends CommonFieldSchema { + isPasswordField?: boolean; +} + +interface SimpleConnectorFormProps { + isEdit: boolean; + readOnly: boolean; + configFormSchema: ConfigFieldSchema[]; + secretsFormSchema: SecretsFieldSchema[]; +} + +type FormRowProps = ConfigFieldSchema & SecretsFieldSchema & { readOnly: boolean }; + +const UseField = getUseField({ component: Field }); +const { emptyField, urlField } = fieldValidators; + +const getFieldConfig = ({ + label, + isUrlField = false, +}: { + label: string; + isUrlField?: boolean; +}) => ({ + label, + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.triggersActionsUI.sections.actionConnectorForm.error.requireFieldText', + { + values: { label }, + defaultMessage: `{label} is required.`, + } + ) + ), + }, + ...(isUrlField + ? [ + { + validator: urlField( + i18n.translate( + 'xpack.triggersActionsUI.sections.actionConnectorForm.error.invalidURL', + { + defaultMessage: 'Invalid URL', + } + ) + ), + }, + ] + : []), + ], +}); + +const FormRow: React.FC = ({ + id, + label, + readOnly, + isPasswordField, + isUrlField, + helpText, +}) => { + const dataTestSub = `${id}-input`; + return ( + <> + + + {!isPasswordField ? ( + + ) : ( + + )} + + + + ); +}; + +const SimpleConnectorFormComponent: React.FC = ({ + isEdit, + readOnly, + configFormSchema, + secretsFormSchema, +}) => { + return ( + <> + {configFormSchema.map(({ id, ...restConfigSchema }, index) => ( + + + {index !== configFormSchema.length ? : null} + + ))} + + + +

+ {i18n.translate( + 'xpack.triggersActionsUI.components.simpleConnectorForm.secrets.authenticationLabel', + { + defaultMessage: 'Authentication', + } + )} +

+
+
+
+ + {secretsFormSchema.map(({ id, ...restSecretsSchema }, index) => ( + + + {index !== secretsFormSchema.length ? : null} + + ))} + + ); +}; + +export const SimpleConnectorForm = memo(SimpleConnectorFormComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/context/connector_context.tsx b/x-pack/plugins/triggers_actions_ui/public/application/context/connector_context.tsx new file mode 100644 index 0000000000000..e97cf789936ae --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/context/connector_context.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { ConnectorServices } from '../../types'; + +export interface ConnectorContextValue { + services: ConnectorServices; +} + +export const ConnectorContext = React.createContext(undefined); + +export const ConnectorProvider: React.FC<{ value: ConnectorContextValue }> = ({ + children, + value, +}) => { + return {children}; +}; + +ConnectorProvider.displayName = 'ConnectorProvider'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/context/use_connector_context.ts b/x-pack/plugins/triggers_actions_ui/public/application/context/use_connector_context.ts new file mode 100644 index 0000000000000..84cfb3279d09c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/context/use_connector_context.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useContext } from 'react'; +import { ConnectorContext } from './connector_context'; + +export const useConnectorContext = () => { + const connectorContext = useContext(ConnectorContext); + + if (!connectorContext) { + throw new Error( + 'useConnectorContext must be used within a ConnectorProvider and have a defined value.' + ); + } + + return connectorContext; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_create_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_create_connector.test.tsx new file mode 100644 index 0000000000000..5fe09dd8ae906 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_create_connector.test.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { useKibana } from '../../common/lib/kibana'; +import { useCreateConnector } from './use_create_connector'; + +jest.mock('../../common/lib/kibana'); + +const useKibanaMock = useKibana as jest.Mocked; + +describe('useCreateConnector', () => { + beforeEach(() => { + jest.clearAllMocks(); + useKibanaMock().services.http.post = jest.fn().mockResolvedValue({ id: 'test-id' }); + }); + + it('init', async () => { + const { result } = renderHook(() => useCreateConnector()); + + expect(result.current).toEqual({ + isLoading: false, + createConnector: expect.anything(), + }); + }); + + it('executes correctly', async () => { + const { result, waitForNextUpdate } = renderHook(() => useCreateConnector()); + + act(() => { + result.current.createConnector({ + actionTypeId: '.test', + name: 'test', + config: {}, + secrets: {}, + }); + }); + + await waitForNextUpdate(); + + expect(useKibanaMock().services.http.post).toHaveBeenCalledWith('/api/actions/connector', { + body: '{"name":"test","config":{},"secrets":{},"connector_type_id":".test"}', + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_create_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_create_connector.tsx new file mode 100644 index 0000000000000..3771e53497e9d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_create_connector.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useRef, useState, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { ActionConnector, ActionConnectorWithoutId } from '../../types'; +import { createActionConnector } from '../lib/action_connector_api'; +import { useKibana } from '../../common/lib/kibana'; + +type CreateConnectorSchema = Pick< + ActionConnectorWithoutId, + 'actionTypeId' | 'name' | 'config' | 'secrets' +>; + +interface UseCreateConnectorReturnValue { + isLoading: boolean; + createConnector: (connector: CreateConnectorSchema) => Promise; +} + +export const useCreateConnector = (): UseCreateConnectorReturnValue => { + const { + http, + notifications: { toasts }, + } = useKibana().services; + + const [isLoading, setIsLoading] = useState(false); + const abortCtrlRef = useRef(new AbortController()); + const isMounted = useRef(false); + + async function createConnector(connector: CreateConnectorSchema) { + setIsLoading(true); + isMounted.current = true; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + + try { + const res = await createActionConnector({ http, connector }); + + if (isMounted.current) { + setIsLoading(false); + + toasts.addSuccess( + i18n.translate( + 'xpack.triggersActionsUI.sections.addConnectorForm.updateSuccessNotificationText', + { + defaultMessage: "Created '{connectorName}'", + values: { + connectorName: res.name, + }, + } + ) + ); + } + + return res; + } catch (error) { + if (isMounted.current) { + setIsLoading(false); + + if (error.name !== 'AbortError') { + toasts.addDanger( + error.body?.message ?? + i18n.translate( + 'xpack.triggersActionsUI.sections.useCreateConnector.updateErrorNotificationText', + { defaultMessage: 'Cannot create a connector.' } + ) + ); + } + } + } + } + + useEffect(() => { + isMounted.current = true; + return () => { + isMounted.current = false; + abortCtrlRef.current.abort(); + }; + }, []); + + return { isLoading, createConnector }; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_edit_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_edit_connector.tsx new file mode 100644 index 0000000000000..531da7ee51f1d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_edit_connector.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useRef, useState, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { ActionConnector, ActionConnectorWithoutId } from '../../types'; +import { updateActionConnector } from '../lib/action_connector_api'; +import { useKibana } from '../../common/lib/kibana'; + +type UpdateConnectorSchema = Pick & { + id: string; +}; + +interface UseUpdateConnectorReturnValue { + isLoading: boolean; + updateConnector: (connector: UpdateConnectorSchema) => Promise; +} + +export const useUpdateConnector = (): UseUpdateConnectorReturnValue => { + const { + http, + notifications: { toasts }, + } = useKibana().services; + + const [isLoading, setIsLoading] = useState(false); + const abortCtrlRef = useRef(new AbortController()); + const isMounted = useRef(false); + + async function updateConnector(connector: UpdateConnectorSchema) { + setIsLoading(true); + isMounted.current = true; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + + try { + const res = await updateActionConnector({ http, connector, id: connector.id }); + + if (isMounted.current) { + setIsLoading(false); + + toasts.addSuccess( + i18n.translate( + 'xpack.triggersActionsUI.sections.editConnectorForm.updateSuccessNotificationText', + { + defaultMessage: "Updated '{connectorName}'", + values: { + connectorName: res.name, + }, + } + ) + ); + } + + return res; + } catch (error) { + if (isMounted.current) { + setIsLoading(false); + + if (error.name !== 'AbortError') { + toasts.addDanger( + error.body?.message ?? + i18n.translate( + 'xpack.triggersActionsUI.sections.editConnectorForm.updateErrorNotificationText', + { defaultMessage: 'Cannot update a connector.' } + ) + ); + } + } + } + } + + useEffect(() => { + isMounted.current = true; + return () => { + isMounted.current = false; + abortCtrlRef.current.abort(); + }; + }, []); + + return { isLoading, updateConnector }; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_execute_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_execute_connector.test.tsx new file mode 100644 index 0000000000000..4b93900ea6b4e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_execute_connector.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { useKibana } from '../../common/lib/kibana'; +import { useExecuteConnector } from './use_execute_connector'; + +jest.mock('../../common/lib/kibana'); + +const useKibanaMock = useKibana as jest.Mocked; + +describe('useExecuteConnector', () => { + beforeEach(() => { + jest.clearAllMocks(); + useKibanaMock().services.http.post = jest.fn().mockResolvedValue({ status: 'ok', data: {} }); + }); + + it('init', async () => { + const { result } = renderHook(() => useExecuteConnector()); + + expect(result.current).toEqual({ + isLoading: false, + executeConnector: expect.anything(), + }); + }); + + it('executes correctly', async () => { + const { result, waitForNextUpdate } = renderHook(() => useExecuteConnector()); + + act(() => { + result.current.executeConnector({ connectorId: 'test-id', params: {} }); + }); + + await waitForNextUpdate(); + + expect(useKibanaMock().services.http.post).toHaveBeenCalledWith( + '/api/actions/connector/test-id/_execute', + { body: '{"params":{}}' } + ); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_execute_connector.tsx b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_execute_connector.tsx new file mode 100644 index 0000000000000..9b68eb3722210 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_execute_connector.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ActionTypeExecutorResult } from '@kbn/actions-plugin/common'; +import { useRef, useState, useEffect } from 'react'; +import { executeAction } from '../lib/action_connector_api'; +import { useKibana } from '../../common/lib/kibana'; + +interface UseExecuteConnectorReturnValue { + isLoading: boolean; + executeConnector: (args: { + connectorId: string; + params: Record; + }) => Promise | undefined>; +} + +export const useExecuteConnector = (): UseExecuteConnectorReturnValue => { + const { http } = useKibana().services; + + const [isLoading, setIsLoading] = useState(false); + const abortCtrlRef = useRef(new AbortController()); + const isMounted = useRef(false); + + async function executeConnector({ + connectorId, + params, + }: { + connectorId: string; + params: Record; + }) { + setIsLoading(true); + isMounted.current = true; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + + try { + const res = await executeAction({ http, id: connectorId, params }); + + if (isMounted.current) { + setIsLoading(false); + } + + return res; + } catch (error) { + if (isMounted.current) { + setIsLoading(false); + + if (error.name !== 'AbortError') { + throw error; + } + } + } + } + + useEffect(() => { + isMounted.current = true; + return () => { + isMounted.current = false; + abortCtrlRef.current.abort(); + }; + }, []); + + return { isLoading, executeConnector }; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_sub_action.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_sub_action.test.tsx new file mode 100644 index 0000000000000..01ad50456b04b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_sub_action.test.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { useKibana } from '../../common/lib/kibana'; +import { useSubAction } from './use_sub_action'; + +jest.mock('../../common/lib/kibana'); + +const useKibanaMock = useKibana as jest.Mocked; + +describe('useSubAction', () => { + const params = { + connectorId: 'test-id', + subAction: 'test', + subActionParams: {}, + }; + + beforeEach(() => { + jest.clearAllMocks(); + useKibanaMock().services.http.post = jest.fn().mockResolvedValue({ status: 'ok', data: {} }); + }); + + it('init', async () => { + const { result, waitForNextUpdate } = renderHook(() => useSubAction(params)); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + isError: false, + isLoading: false, + response: {}, + error: null, + }); + }); + + it('executes the sub action correctly', async () => { + const { waitForNextUpdate } = renderHook(() => useSubAction(params)); + await waitForNextUpdate(); + + expect(useKibanaMock().services.http.post).toHaveBeenCalledWith( + '/api/actions/connector/test-id/_execute', + { body: '{"params":{"subAction":"test","subActionParams":{}}}' } + ); + }); + + it('returns an error correctly', async () => { + useKibanaMock().services.http.post = jest.fn().mockRejectedValue(new Error('error executing')); + + const { result, waitForNextUpdate } = renderHook(() => useSubAction(params)); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + isError: true, + isLoading: false, + response: undefined, + error: expect.anything(), + }); + }); + + it('does not execute if params are null', async () => { + const { result } = renderHook(() => useSubAction(null)); + + expect(useKibanaMock().services.http.post).not.toHaveBeenCalled(); + expect(result.current).toEqual({ + isError: false, + isLoading: false, + response: undefined, + error: null, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_sub_action.tsx b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_sub_action.tsx new file mode 100644 index 0000000000000..a219caa9b4d23 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/hooks/use_sub_action.tsx @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useEffect, useReducer, useRef } from 'react'; +import { ActionTypeExecutorResult } from '@kbn/actions-plugin/common'; +import { useKibana } from '../../common/lib/kibana'; +import { executeAction } from '../lib/action_connector_api'; + +interface UseSubActionParams { + connectorId: string; + subAction: string; + subActionParams: Record; +} + +interface SubActionsState { + isLoading: boolean; + isError: boolean; + response: unknown | undefined; + error: Error | null; +} + +enum SubActionsActionsList { + INIT, + LOADING, + SUCCESS, + ERROR, +} + +type Action = + | { type: SubActionsActionsList.INIT } + | { type: SubActionsActionsList.LOADING } + | { type: SubActionsActionsList.SUCCESS; payload: T | undefined } + | { type: SubActionsActionsList.ERROR; payload: Error | null }; + +const dataFetchReducer = (state: SubActionsState, action: Action): SubActionsState => { + switch (action.type) { + case SubActionsActionsList.INIT: + return { + ...state, + isLoading: false, + isError: false, + }; + + case SubActionsActionsList.LOADING: + return { + ...state, + isLoading: true, + isError: false, + }; + + case SubActionsActionsList.SUCCESS: + return { + ...state, + response: action.payload, + isLoading: false, + isError: false, + }; + + case SubActionsActionsList.ERROR: + return { + ...state, + error: action.payload, + isLoading: false, + isError: true, + }; + + default: + return state; + } +}; + +export const useSubAction = (params: UseSubActionParams | null) => { + const { http } = useKibana().services; + const [state, dispatch] = useReducer(dataFetchReducer, { + isError: false, + isLoading: false, + response: undefined, + error: null, + }); + + const abortCtrl = useRef(new AbortController()); + const isMounted = useRef(false); + + const executeSubAction = useCallback(async () => { + if (params == null) { + return; + } + + const { connectorId, subAction, subActionParams } = params; + dispatch({ type: SubActionsActionsList.INIT }); + + try { + abortCtrl.current.abort(); + abortCtrl.current = new AbortController(); + dispatch({ type: SubActionsActionsList.LOADING }); + + const res = (await executeAction({ + id: connectorId, + http, + params: { + subAction, + subActionParams, + }, + })) as ActionTypeExecutorResult; + + if (isMounted.current) { + if (res.status && res.status === 'error') { + dispatch({ + type: SubActionsActionsList.ERROR, + payload: new Error(`${res.message}: ${res.serviceMessage}`), + }); + } + + dispatch({ type: SubActionsActionsList.SUCCESS, payload: res.data }); + } + + return res.data; + } catch (e) { + if (isMounted.current) { + dispatch({ + type: SubActionsActionsList.ERROR, + payload: e, + }); + } + } + }, [http, params]); + + useEffect(() => { + isMounted.current = true; + executeSubAction(); + return () => { + isMounted.current = false; + abortCtrl.current.abort(); + }; + }, [executeSubAction]); + + return { + ...state, + }; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rules_list_sandbox.tsx b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rules_list_sandbox.tsx index 7702b914cfd36..0083811802955 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rules_list_sandbox.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rules_list_sandbox.tsx @@ -6,11 +6,18 @@ */ import React from 'react'; import { getRulesListLazy } from '../../../common/get_rules_list'; +import { useConnectorContext } from '../../context/use_connector_context'; const style = { flex: 1, }; export const RulesListSandbox = () => { - return
{getRulesListLazy()}
; + const { + services: { validateEmailAddresses }, + } = useConnectorContext(); + + return ( +
{getRulesListLazy({ connectorServices: { validateEmailAddresses } })}
+ ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.test.ts index a2ed111daa2be..fa8af621b9521 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.test.ts @@ -26,15 +26,22 @@ describe('createActionConnector', () => { }; http.post.mockResolvedValueOnce(apiResponse); - const connector: ActionConnectorWithoutId<{}, {}> = { + const connector: Pick< + ActionConnectorWithoutId, + 'actionTypeId' | 'name' | 'config' | 'secrets' + > = { actionTypeId: 'test', - isPreconfigured: false, - isDeprecated: false, name: 'My test', config: {}, secrets: {}, }; - const resolvedValue = { ...connector, id: '123' }; + const resolvedValue = { + ...connector, + id: '123', + isDeprecated: false, + isPreconfigured: false, + isMissingSecrets: undefined, + }; const result = await createActionConnector({ http, connector }); expect(result).toEqual(resolvedValue); @@ -42,7 +49,7 @@ describe('createActionConnector', () => { Array [ "/api/actions/connector", Object { - "body": "{\\"name\\":\\"My test\\",\\"config\\":{},\\"secrets\\":{},\\"connector_type_id\\":\\"test\\",\\"is_preconfigured\\":false,\\"is_deprecated\\":false}", + "body": "{\\"name\\":\\"My test\\",\\"config\\":{},\\"secrets\\":{},\\"connector_type_id\\":\\"test\\"}", }, ] `); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.ts index 9227c4747c84a..287c27a1c0e3f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.ts @@ -14,12 +14,10 @@ import type { } from '../../../types'; const rewriteBodyRequest: RewriteResponseCase< - Omit -> = ({ actionTypeId, isPreconfigured, isDeprecated, ...res }) => ({ + Pick +> = ({ actionTypeId, ...res }) => ({ ...res, connector_type_id: actionTypeId, - is_preconfigured: isPreconfigured, - is_deprecated: isDeprecated, }); const rewriteBodyRes: RewriteRequestCase< @@ -43,7 +41,7 @@ export async function createActionConnector({ connector, }: { http: HttpSetup; - connector: Omit; + connector: Pick; }): Promise { const res = await http.post[0]>( `${BASE_ACTION_API_PATH}/connector`, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx deleted file mode 100644 index b86a3952eb0a6..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx +++ /dev/null @@ -1,61 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as React from 'react'; -import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { actionTypeRegistryMock } from '../../action_type_registry.mock'; -import { - UserConfiguredActionConnector, - GenericValidationResult, - ConnectorValidationResult, -} from '../../../types'; -import { ActionConnectorForm } from './action_connector_form'; -const actionTypeRegistry = actionTypeRegistryMock.create(); -jest.mock('../../../common/lib/kibana'); - -describe('action_connector_form', () => { - it('renders action_connector_form', () => { - const actionType = actionTypeRegistryMock.createMockActionTypeModel({ - id: 'my-action-type', - iconClass: 'test', - selectMessage: 'test', - validateConnector: (): Promise> => { - return Promise.resolve({}); - }, - validateParams: (): Promise> => { - const validationResult = { errors: {} }; - return Promise.resolve(validationResult); - }, - }); - actionTypeRegistry.get.mockReturnValue(actionType); - actionTypeRegistry.has.mockReturnValue(true); - - const initialConnector: UserConfiguredActionConnector<{}, {}> = { - id: '123', - name: '', - actionTypeId: actionType.id, - config: {}, - secrets: {}, - isPreconfigured: false, - isDeprecated: false, - }; - const wrapper = mountWithIntl( - {}} - errors={{ name: [] }} - actionTypeRegistry={actionTypeRegistry} - setCallbacks={() => {}} - isEdit={false} - /> - ); - const connectorNameField = wrapper?.find('[data-test-subj="nameInput"]'); - expect(connectorNameField?.exists()).toBeTruthy(); - expect(connectorNameField?.first().prop('value')).toBe(''); - }); -}); 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 deleted file mode 100644 index b2000f172ef32..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx +++ /dev/null @@ -1,254 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { Suspense } from 'react'; -import { - EuiForm, - EuiCallOut, - EuiLink, - EuiText, - EuiSpacer, - EuiFieldText, - EuiFormRow, - EuiErrorBoundary, - EuiTitle, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { - IErrorObject, - ActionTypeRegistryContract, - UserConfiguredActionConnector, - ActionTypeModel, - ActionConnectorFieldsSetCallbacks, -} from '../../../types'; -import { hasSaveActionsCapability } from '../../lib/capabilities'; -import { useKibana } from '../../../common/lib/kibana'; -import { SectionLoading } from '../../components/section_loading'; -import { ConnectorReducerAction } from './connector_reducer'; - -export function validateBaseProperties( - actionObject: UserConfiguredActionConnector -) { - const validationResult = { errors: {} }; - const verrors = { - name: new Array(), - }; - validationResult.errors = verrors; - if (!actionObject.name) { - verrors.name.push( - i18n.translate( - 'xpack.triggersActionsUI.sections.actionConnectorForm.error.requiredNameText', - { - defaultMessage: 'Name is required.', - } - ) - ); - } - return validationResult; -} - -export async function getConnectorErrors( - connector: UserConfiguredActionConnector, - actionTypeModel: ActionTypeModel -) { - const connectorValidationResult = await actionTypeModel?.validateConnector(connector); - const configErrors = ( - connectorValidationResult.config ? connectorValidationResult.config.errors : {} - ) as IErrorObject; - const secretsErrors = ( - connectorValidationResult.secrets ? connectorValidationResult.secrets.errors : {} - ) as IErrorObject; - const connectorBaseErrors = validateBaseProperties(connector).errors; - const connectorErrors = { - ...configErrors, - ...secretsErrors, - ...connectorBaseErrors, - } as IErrorObject; - return { - configErrors, - secretsErrors, - connectorBaseErrors, - connectorErrors, - }; -} - -interface ActionConnectorProps< - ConnectorConfig = Record, - ConnectorSecrets = Record -> { - connector: UserConfiguredActionConnector; - dispatch: React.Dispatch>; - errors: IErrorObject; - actionTypeRegistry: ActionTypeRegistryContract; - consumer?: string; - actionTypeName?: string; - serverError?: { - body: { message: string; error: string }; - }; - setCallbacks: ActionConnectorFieldsSetCallbacks; - isEdit: boolean; -} - -export const ActionConnectorForm = ({ - connector, - dispatch, - actionTypeName, - serverError, - errors, - actionTypeRegistry, - consumer, - setCallbacks, - isEdit, -}: ActionConnectorProps) => { - const { - docLinks, - application: { capabilities }, - } = useKibana().services; - const canSave = hasSaveActionsCapability(capabilities); - - const setActionProperty = < - Key extends keyof UserConfiguredActionConnector< - Record, - Record - > - >( - key: Key, - value: - | UserConfiguredActionConnector, Record>[Key] - | null - ) => { - dispatch({ command: { type: 'setProperty' }, payload: { key, value } }); - }; - - const setActionConfigProperty = >( - key: Key, - value: Record[Key] - ) => { - dispatch({ command: { type: 'setConfigProperty' }, payload: { key, value } }); - }; - - const setActionSecretsProperty = >( - key: Key, - value: Record[Key] - ) => { - dispatch({ command: { type: 'setSecretsProperty' }, payload: { key, value } }); - }; - - const actionTypeRegistered = actionTypeRegistry.get(connector.actionTypeId); - if (!actionTypeRegistered) - return ( - <> - - -

- - - - ), - }} - /> -

-
-
- - - ); - - const FieldsComponent = actionTypeRegistered.actionConnectorFields; - const isNameInvalid: boolean = - connector.name !== undefined && errors.name !== undefined && errors.name.length > 0; - return ( - - - } - isInvalid={isNameInvalid} - error={errors.name} - > - { - setActionProperty('name', e.target.value); - }} - onBlur={() => { - if (!connector.name) { - setActionProperty('name', ''); - } - }} - /> - - - {FieldsComponent !== null ? ( - <> - -

- -

-
- - - - - - } - > - - - - - ) : 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 93b912113a1a9..002475c21882e 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 @@ -11,13 +11,7 @@ import { EuiAccordion } from '@elastic/eui'; import { coreMock } from '@kbn/core/public/mocks'; import { act } from 'react-dom/test-utils'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; -import { - ValidationResult, - Rule, - RuleAction, - ConnectorValidationResult, - GenericValidationResult, -} from '../../../types'; +import { ValidationResult, Rule, RuleAction, GenericValidationResult } from '../../../types'; import ActionForm from './action_form'; import { useKibana } from '../../../common/lib/kibana'; import { @@ -53,9 +47,6 @@ describe('action_form', () => { id: 'my-action-type', iconClass: 'test', selectMessage: 'test', - validateConnector: (): Promise> => { - return Promise.resolve({}); - }, validateParams: (): Promise> => { const validationResult = { errors: {} }; return Promise.resolve(validationResult); @@ -68,9 +59,6 @@ describe('action_form', () => { id: 'disabled-by-config', iconClass: 'test', selectMessage: 'test', - validateConnector: (): Promise> => { - return Promise.resolve({}); - }, validateParams: (): Promise> => { const validationResult = { errors: {} }; return Promise.resolve(validationResult); @@ -83,9 +71,6 @@ describe('action_form', () => { id: '.jira', iconClass: 'test', selectMessage: 'test', - validateConnector: (): Promise> => { - return Promise.resolve({}); - }, validateParams: (): Promise> => { const validationResult = { errors: {} }; return Promise.resolve(validationResult); @@ -98,9 +83,6 @@ describe('action_form', () => { id: 'disabled-by-license', iconClass: 'test', selectMessage: 'test', - validateConnector: (): Promise> => { - return Promise.resolve({}); - }, validateParams: (): Promise> => { const validationResult = { errors: {} }; return Promise.resolve(validationResult); @@ -113,9 +95,6 @@ describe('action_form', () => { id: 'preconfigured', iconClass: 'test', selectMessage: 'test', - validateConnector: (): Promise> => { - return Promise.resolve({}); - }, validateParams: (): Promise> => { const validationResult = { errors: {} }; return Promise.resolve(validationResult); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.test.tsx index 7b89d720eabe3..a06cba11a4134 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.test.tsx @@ -8,13 +8,7 @@ import * as React from 'react'; import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; import { ActionTypeForm } from './action_type_form'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; -import { - ActionConnector, - ActionType, - RuleAction, - ConnectorValidationResult, - GenericValidationResult, -} from '../../../types'; +import { ActionConnector, ActionType, RuleAction, GenericValidationResult } from '../../../types'; import { act } from 'react-dom/test-utils'; import { EuiFieldText } from '@elastic/eui'; import { DefaultActionParams } from '../../lib/get_defaults_for_action_params'; @@ -43,9 +37,6 @@ describe('action_type_form', () => { id: '.pagerduty', iconClass: 'test', selectMessage: 'test', - validateConnector: (): Promise> => { - return Promise.resolve({}); - }, validateParams: (): Promise> => { const validationResult = { errors: {} }; return Promise.resolve(validationResult); @@ -92,9 +83,6 @@ describe('action_type_form', () => { id: '.pagerduty', iconClass: 'test', selectMessage: 'test', - validateConnector: (): Promise> => { - return Promise.resolve({}); - }, validateParams: (): Promise> => { const validationResult = { errors: {} }; return Promise.resolve(validationResult); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx index 8063bc97334f5..ef803e50bb60d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_menu.test.tsx @@ -10,7 +10,7 @@ import { mountWithIntl } from '@kbn/test-jest-helpers'; import { coreMock } from '@kbn/core/public/mocks'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ActionTypeMenu } from './action_type_menu'; -import { ConnectorValidationResult, GenericValidationResult } from '../../../types'; +import { GenericValidationResult } from '../../../types'; import { useKibana } from '../../../common/lib/kibana'; jest.mock('../../../common/lib/kibana'); const actionTypeRegistry = actionTypeRegistryMock.create(); @@ -40,9 +40,6 @@ describe('connector_add_flyout', () => { id: 'my-action-type', iconClass: 'test', selectMessage: 'test', - validateConnector: (): Promise> => { - return Promise.resolve({}); - }, validateParams: (): Promise> => { const validationResult = { errors: {} }; return Promise.resolve(validationResult); @@ -77,9 +74,6 @@ describe('connector_add_flyout', () => { id: 'my-action-type', iconClass: 'test', selectMessage: 'test', - validateConnector: (): Promise> => { - return Promise.resolve({}); - }, validateParams: (): Promise> => { const validationResult = { errors: {} }; return Promise.resolve(validationResult); @@ -114,9 +108,6 @@ describe('connector_add_flyout', () => { id: 'my-action-type', iconClass: 'test', selectMessage: 'test', - validateConnector: (): Promise> => { - return Promise.resolve({}); - }, validateParams: (): Promise> => { const validationResult = { errors: {} }; return Promise.resolve(validationResult); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx deleted file mode 100644 index 02819a714c833..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.test.tsx +++ /dev/null @@ -1,210 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as React from 'react'; -import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { coreMock } from '@kbn/core/public/mocks'; -import ConnectorAddFlyout from './connector_add_flyout'; -import { actionTypeRegistryMock } from '../../action_type_registry.mock'; -import { ConnectorValidationResult, GenericValidationResult } from '../../../types'; -import { useKibana } from '../../../common/lib/kibana'; -jest.mock('../../../common/lib/kibana'); - -const actionTypeRegistry = actionTypeRegistryMock.create(); -const useKibanaMock = useKibana as jest.Mocked; - -describe('connector_add_flyout', () => { - beforeAll(async () => { - const mocks = coreMock.createSetup(); - const [ - { - application: { capabilities }, - }, - ] = await mocks.getStartServices(); - useKibanaMock().services.application.capabilities = { - ...capabilities, - actions: { - show: true, - save: true, - delete: true, - }, - }; - }); - - it('renders action type menu on flyout open', () => { - const actionType = createActionType(); - actionTypeRegistry.get.mockReturnValueOnce(actionType); - actionTypeRegistry.has.mockReturnValue(true); - - const wrapper = mountWithIntl( - {}} - actionTypes={[ - { - id: actionType.id, - enabled: true, - name: 'Test', - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'basic', - }, - ]} - reloadConnectors={() => { - return new Promise(() => {}); - }} - actionTypeRegistry={actionTypeRegistry} - /> - ); - expect(wrapper.find('ActionTypeMenu')).toHaveLength(1); - expect(wrapper.find(`[data-test-subj="${actionType.id}-card"]`).exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="cancelButton"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="backButton"]').exists()).toBeFalsy(); - }); - - it('renders banner with subscription links when gold features are disabled due to licensing ', () => { - const actionType = createActionType(); - const disabledActionType = createActionType(); - - actionTypeRegistry.get.mockReturnValueOnce(actionType); - actionTypeRegistry.has.mockReturnValue(true); - - const wrapper = mountWithIntl( - {}} - actionTypes={[ - { - id: actionType.id, - enabled: true, - name: 'Test', - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'basic', - }, - { - id: disabledActionType.id, - enabled: true, - name: 'Test', - enabledInConfig: true, - enabledInLicense: false, - minimumLicenseRequired: 'gold', - }, - ]} - reloadConnectors={() => { - return new Promise(() => {}); - }} - actionTypeRegistry={actionTypeRegistry} - /> - ); - const callout = wrapper.find('UpgradeYourLicenseCallOut'); - expect(callout).toHaveLength(1); - - const manageLink = callout.find('EuiButton'); - expect(manageLink).toHaveLength(1); - expect(manageLink.getElements()[0].props.href).toMatchInlineSnapshot( - `"/app/management/stack/license_management"` - ); - - const subscriptionLink = callout.find('EuiButtonEmpty'); - expect(subscriptionLink).toHaveLength(1); - expect(subscriptionLink.getElements()[0].props.href).toMatchInlineSnapshot( - `"https://www.elastic.co/subscriptions"` - ); - }); - - it('does not render banner with subscription links when only platinum features are disabled due to licensing ', () => { - const actionType = createActionType(); - const disabledActionType = createActionType(); - - actionTypeRegistry.get.mockReturnValueOnce(actionType); - actionTypeRegistry.has.mockReturnValue(true); - - const wrapper = mountWithIntl( - {}} - actionTypes={[ - { - id: actionType.id, - enabled: true, - name: 'Test', - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'basic', - }, - { - id: disabledActionType.id, - enabled: true, - name: 'Test', - enabledInConfig: true, - enabledInLicense: false, - minimumLicenseRequired: 'platinum', - }, - ]} - reloadConnectors={() => { - return new Promise(() => {}); - }} - actionTypeRegistry={actionTypeRegistry} - /> - ); - const callout = wrapper.find('UpgradeYourLicenseCallOut'); - expect(callout).toHaveLength(0); - }); - - it('does not render banner with subscription links when only enterprise features are disabled due to licensing ', () => { - const actionType = createActionType(); - const disabledActionType = createActionType(); - - actionTypeRegistry.get.mockReturnValueOnce(actionType); - actionTypeRegistry.has.mockReturnValue(true); - - const wrapper = mountWithIntl( - {}} - actionTypes={[ - { - id: actionType.id, - enabled: true, - name: 'Test', - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'basic', - }, - { - id: disabledActionType.id, - enabled: true, - name: 'Test', - enabledInConfig: true, - enabledInLicense: false, - minimumLicenseRequired: 'enterprise', - }, - ]} - reloadConnectors={() => { - return new Promise(() => {}); - }} - actionTypeRegistry={actionTypeRegistry} - /> - ); - const callout = wrapper.find('UpgradeYourLicenseCallOut'); - expect(callout).toHaveLength(0); - }); -}); - -let count = 0; -function createActionType() { - return actionTypeRegistryMock.createMockActionTypeModel({ - id: `my-action-type-${++count}`, - iconClass: 'test', - selectMessage: 'test', - validateConnector: (): Promise> => { - return Promise.resolve({}); - }, - validateParams: (): Promise> => { - const validationResult = { errors: {} }; - return Promise.resolve(validationResult); - }, - actionConnectorFields: null, - }); -} 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 deleted file mode 100644 index 06874f22b6428..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx +++ /dev/null @@ -1,416 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useCallback, useState, useReducer, useEffect } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiTitle, - EuiFlyoutHeader, - EuiFlyout, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiText, - EuiFlyoutFooter, - EuiButtonEmpty, - EuiButton, - EuiFlyoutBody, - EuiCallOut, - EuiSpacer, -} from '@elastic/eui'; -import { HttpSetup } from '@kbn/core/public'; -import { i18n } from '@kbn/i18n'; -import { ActionTypeMenu } from './action_type_menu'; -import { ActionConnectorForm, getConnectorErrors } from './action_connector_form'; -import { - ActionType, - ActionConnector, - UserConfiguredActionConnector, - IErrorObject, - ConnectorAddFlyoutProps, - ActionTypeModel, - ActionConnectorFieldsCallbacks, -} from '../../../types'; -import { hasSaveActionsCapability } from '../../lib/capabilities'; -import { createActionConnector } from '../../lib/action_connector_api'; -import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; -import { useKibana } from '../../../common/lib/kibana'; -import { createConnectorReducer, InitialConnector, ConnectorReducer } from './connector_reducer'; -import { getConnectorWithInvalidatedFields } from '../../lib/value_validators'; -import { CenterJustifiedSpinner } from '../../components/center_justified_spinner'; - -const ConnectorAddFlyout: React.FunctionComponent = ({ - onClose, - actionTypes, - onTestConnector, - reloadConnectors, - consumer, - actionTypeRegistry, -}) => { - const [hasErrors, setHasErrors] = useState(true); - let actionTypeModel: ActionTypeModel | undefined; - - const { - http, - notifications: { toasts }, - application: { capabilities }, - } = useKibana().services; - const [actionType, setActionType] = useState(undefined); - const [hasActionsUpgradeableByTrial, setHasActionsUpgradeableByTrial] = useState(false); - const [errors, setErrors] = useState<{ - configErrors: IErrorObject; - connectorBaseErrors: IErrorObject; - connectorErrors: IErrorObject; - secretsErrors: IErrorObject; - }>({ - configErrors: {}, - connectorBaseErrors: {}, - connectorErrors: {}, - secretsErrors: {}, - }); - // hooks - const initialConnector: InitialConnector, Record> = { - actionTypeId: actionType?.id ?? '', - config: {}, - secrets: {}, - }; - - const reducer: ConnectorReducer< - Record, - Record - > = createConnectorReducer, Record>(); - const [{ connector }, dispatch] = useReducer(reducer, { - connector: initialConnector as UserConfiguredActionConnector< - Record, - Record - >, - }); - const [isLoading, setIsLoading] = useState(false); - - useEffect(() => { - (async () => { - if (actionTypeModel) { - setIsLoading(true); - const res = await getConnectorErrors(connector, actionTypeModel); - setHasErrors( - !!Object.keys(res.connectorErrors).find( - (errorKey) => (res.connectorErrors as IErrorObject)[errorKey].length >= 1 - ) - ); - setIsLoading(false); - setErrors({ ...res }); - } - })(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [connector, actionType]); - - const setActionProperty = ( - key: Key, - value: - | UserConfiguredActionConnector, Record>[Key] - | null - ) => { - dispatch({ command: { type: 'setProperty' }, payload: { key, value } }); - }; - - const setConnector = (value: any) => { - dispatch({ command: { type: 'setConnector' }, payload: { key: 'connector', value } }); - }; - - const [isSaving, setIsSaving] = useState(false); - const [callbacks, setCallbacks] = useState(null); - - const closeFlyout = useCallback(() => { - onClose(); - }, [onClose]); - - const canSave = hasSaveActionsCapability(capabilities); - - function onActionTypeChange(newActionType: ActionType) { - setActionType(newActionType); - setActionProperty('actionTypeId', newActionType.id); - } - - let currentForm; - let saveButton; - if (!actionType) { - currentForm = ( - - ); - } else { - actionTypeModel = actionTypeRegistry.get(actionType.id); - - currentForm = ( - - ); - - const onActionConnectorSave = async (): Promise => - await createActionConnector({ http, connector }) - .then((savedConnector) => { - if (toasts) { - toasts.addSuccess( - i18n.translate( - 'xpack.triggersActionsUI.sections.addConnectorForm.updateSuccessNotificationText', - { - defaultMessage: "Created '{connectorName}'", - values: { - connectorName: savedConnector.name, - }, - } - ) - ); - } - return savedConnector; - }) - .catch((errorRes) => { - toasts.addDanger( - errorRes.body?.message ?? - i18n.translate( - 'xpack.triggersActionsUI.sections.addConnectorForm.updateErrorNotificationText', - { defaultMessage: 'Cannot create a connector.' } - ) - ); - return undefined; - }); - - const onSaveClicked = async () => { - if (hasErrors) { - setConnector( - getConnectorWithInvalidatedFields( - connector, - errors.configErrors, - errors.secretsErrors, - errors.connectorBaseErrors - ) - ); - return; - } - - setIsSaving(true); - // Do not allow to save the connector if there is an error - try { - await callbacks?.beforeActionConnectorSave?.(); - } catch (e) { - setIsSaving(false); - return; - } - - const savedAction = await onActionConnectorSave(); - - setIsSaving(false); - if (savedAction) { - await callbacks?.afterActionConnectorSave?.(savedAction); - closeFlyout(); - if (reloadConnectors) { - await reloadConnectors(); - } - } - return savedAction; - }; - - saveButton = ( - <> - {onTestConnector && ( - - { - const savedConnector = await onSaveClicked(); - if (savedConnector) { - onTestConnector(savedConnector); - } - }} - > - - - - )} - - - - - - - ); - } - - return ( - - - - {!!actionTypeModel && actionTypeModel.iconClass ? ( - - - - ) : null} - - {!!actionTypeModel && actionType ? ( - <> - -

- -

-
- - {actionTypeModel.selectMessage} - - - ) : ( - -

- -

-
- )} -
-
-
- - ) : ( - <> - ) - } - > - <> - {currentForm} - {isLoading ? ( - <> - - {' '} - - ) : ( - <> - )} - - - - - - - {!actionType ? ( - - {i18n.translate( - 'xpack.triggersActionsUI.sections.actionConnectorAdd.cancelButtonLabel', - { - defaultMessage: 'Cancel', - } - )} - - ) : ( - { - setActionType(undefined); - setConnector(initialConnector); - }} - > - {i18n.translate( - 'xpack.triggersActionsUI.sections.actionConnectorAdd.backButtonLabel', - { - defaultMessage: 'Back', - } - )} - - )} - - - - {canSave && !!actionTypeModel && actionType ? saveButton : null} - - - - -
- ); -}; - -const UpgradeYourLicenseCallOut = ({ http }: { http: HttpSetup }) => ( - - - - - - - - - - - - - - - - -); - -// eslint-disable-next-line import/no-default-export -export { ConnectorAddFlyout as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx index 0079af71d19f5..301f7e64c39a4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx @@ -10,7 +10,7 @@ import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; import { act } from 'react-dom/test-utils'; import ConnectorAddModal from './connector_add_modal'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; -import { ActionType, ConnectorValidationResult, GenericValidationResult } from '../../../types'; +import { ActionType, GenericValidationResult } from '../../../types'; import { useKibana } from '../../../common/lib/kibana'; import { coreMock } from '@kbn/core/public/mocks'; @@ -41,9 +41,6 @@ describe('connector_add_modal', () => { id: 'my-action-type', iconClass: 'test', selectMessage: 'test', - validateConnector: (): Promise> => { - return Promise.resolve({ config: { errors: {} }, secrets: { errors: {} } }); - }, validateParams: (): Promise> => { const validationResult = { errors: {} }; return Promise.resolve(validationResult); 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 9b6344498559b..0b8095f0058eb 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 @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react'; +import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiModal, @@ -19,140 +19,116 @@ import { EuiFlexItem, EuiIcon, EuiFlexGroup, - EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ActionConnectorForm, getConnectorErrors } from './action_connector_form'; -import { createConnectorReducer, InitialConnector, ConnectorReducer } from './connector_reducer'; -import { createActionConnector } from '../../lib/action_connector_api'; import './connector_add_modal.scss'; import { hasSaveActionsCapability } from '../../lib/capabilities'; -import { - ActionType, - ActionConnector, - ActionTypeRegistryContract, - UserConfiguredActionConnector, - IErrorObject, - ActionConnectorFieldsCallbacks, -} from '../../../types'; +import { ActionType, ActionConnector, ActionTypeRegistryContract } from '../../../types'; import { useKibana } from '../../../common/lib/kibana'; -import { getConnectorWithInvalidatedFields } from '../../lib/value_validators'; -import { CenterJustifiedSpinner } from '../../components/center_justified_spinner'; +import { useCreateConnector } from '../../hooks/use_create_connector'; +import { ConnectorForm, ConnectorFormState } from './connector_form'; +import { ConnectorFormSchema } from './types'; -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -type ConnectorAddModalProps = { +export interface ConnectorAddModalProps { actionType: ActionType; onClose: () => void; postSaveEventHandler?: (savedAction: ActionConnector) => void; - consumer?: string; actionTypeRegistry: ActionTypeRegistryContract; -}; +} const ConnectorAddModal = ({ actionType, onClose, postSaveEventHandler, - consumer, actionTypeRegistry, }: ConnectorAddModalProps) => { const { - http, - notifications: { toasts }, application: { capabilities }, } = useKibana().services; - const [hasErrors, setHasErrors] = useState(true); - const initialConnector: InitialConnector< - Record, - Record - > = useMemo( - () => ({ - actionTypeId: actionType.id, - config: {}, - secrets: {}, - }), - [actionType.id] - ); - const [isSaving, setIsSaving] = useState(false); - const [isLoading, setIsLoading] = useState(false); + + const { isLoading: isSavingConnector, createConnector } = useCreateConnector(); + const isMounted = useRef(false); + const initialConnector = { + actionTypeId: actionType.id, + isDeprecated: false, + isMissingSecrets: false, + config: {}, + secrets: {}, + }; + const canSave = hasSaveActionsCapability(capabilities); + const actionTypeModel = actionTypeRegistry.get(actionType.id); - const reducer: ConnectorReducer< - Record, - Record - > = createConnectorReducer, Record>(); - const [{ connector }, dispatch] = useReducer(reducer, { - connector: initialConnector as UserConfiguredActionConnector< - Record, - Record - >, - }); - const [errors, setErrors] = useState<{ - configErrors: IErrorObject; - connectorBaseErrors: IErrorObject; - connectorErrors: IErrorObject; - secretsErrors: IErrorObject; - }>({ - configErrors: {}, - connectorBaseErrors: {}, - connectorErrors: {}, - secretsErrors: {}, + const [preSubmitValidationErrorMessage, setPreSubmitValidationErrorMessage] = + useState(null); + + const [formState, setFormState] = useState({ + isSubmitted: false, + isSubmitting: false, + isValid: undefined, + submit: async () => ({ isValid: false, data: {} as ConnectorFormSchema }), + preSubmitValidator: null, }); - const [callbacks, setCallbacks] = useState(null); - const actionTypeModel = actionTypeRegistry.get(actionType.id); + const { preSubmitValidator, submit, isValid: isFormValid, isSubmitting } = formState; + const hasErrors = isFormValid === false; + const isSaving = isSavingConnector || isSubmitting; - useEffect(() => { - (async () => { - setIsLoading(true); - const res = await getConnectorErrors(connector, actionTypeModel); - setHasErrors( - !!Object.keys(res.connectorErrors).find( - (errorKey) => (res.connectorErrors as IErrorObject)[errorKey].length >= 1 - ) - ); - setIsLoading(false); - setErrors({ ...res }); - })(); - }, [connector, actionTypeModel]); + const validateAndCreateConnector = useCallback(async () => { + setPreSubmitValidationErrorMessage(null); - const setConnector = (value: any) => { - dispatch({ command: { type: 'setConnector' }, payload: { key: 'connector', value } }); - }; - const [serverError, setServerError] = useState< - | { - body: { message: string; error: string }; + const { isValid, data } = await submit(); + + if (!isMounted.current) { + // User has closed the modal meanwhile submitting the form + return; + } + + if (isValid) { + if (preSubmitValidator) { + const validatorRes = await preSubmitValidator(); + + if (validatorRes) { + setPreSubmitValidationErrorMessage(validatorRes.message); + return; + } } - | undefined - >(undefined); + + /** + * At this point the form is valid + * and there are no pre submit error messages. + */ + + const { actionTypeId, name, config, secrets } = data; + const validConnector = { actionTypeId, name: name ?? '', config, secrets }; + + const createdConnector = await createConnector(validConnector); + return createdConnector; + } + }, [submit, preSubmitValidator, createConnector]); const closeModal = useCallback(() => { - setConnector(initialConnector); - setServerError(undefined); onClose(); - }, [initialConnector, onClose]); + }, [onClose]); - const onActionConnectorSave = async (): Promise => - await createActionConnector({ http, connector }) - .then((savedConnector) => { - if (toasts) { - toasts.addSuccess( - i18n.translate( - 'xpack.triggersActionsUI.sections.addModalConnectorForm.updateSuccessNotificationText', - { - defaultMessage: "Created '{connectorName}'", - values: { - connectorName: savedConnector.name, - }, - } - ) - ); - } - return savedConnector; - }) - .catch((errorRes) => { - setServerError(errorRes); - return undefined; - }); + const onSubmit = useCallback(async () => { + const createdConnector = await validateAndCreateConnector(); + if (createdConnector) { + closeModal(); + + if (postSaveEventHandler) { + postSaveEventHandler(createdConnector); + } + } + }, [validateAndCreateConnector, closeModal, postSaveEventHandler]); + + useEffect(() => { + isMounted.current = true; + + return () => { + isMounted.current = false; + }; + }, []); return ( @@ -182,30 +158,16 @@ const ConnectorAddModal = ({ - <> - - {isLoading ? ( - <> - - {' '} - - ) : ( - <> - )} - + + {preSubmitValidationErrorMessage} - + {i18n.translate( 'xpack.triggersActionsUI.sections.addModalConnectorForm.cancelButtonLabel', { @@ -221,38 +183,8 @@ const ConnectorAddModal = ({ type="submit" iconType="check" isLoading={isSaving} - onClick={async () => { - if (hasErrors) { - setConnector( - getConnectorWithInvalidatedFields( - connector, - errors.configErrors, - errors.secretsErrors, - errors.connectorBaseErrors - ) - ); - return; - } - setIsSaving(true); - // Do not allow to save the connector if there is an error - try { - await callbacks?.beforeActionConnectorSave?.(); - } catch (e) { - setIsSaving(false); - return; - } - - const savedAction = await onActionConnectorSave(); - - setIsSaving(false); - if (savedAction) { - await callbacks?.afterActionConnectorSave?.(savedAction); - if (postSaveEventHandler) { - postSaveEventHandler(savedAction); - } - closeModal(); - } - }} + disabled={hasErrors} + onClick={onSubmit} > ; - -describe('connector_edit_flyout', () => { - beforeAll(async () => { - const mockes = coreMock.createSetup(); - const [ - { - application: { capabilities }, - }, - ] = await mockes.getStartServices(); - useKibanaMock().services.application.capabilities = { - ...capabilities, - actions: { - show: true, - save: false, - delete: false, - }, - }; - }); - - test('if input connector render correct in the edit form', () => { - const connector = { - secrets: {}, - id: 'test', - actionTypeId: 'test-action-type-id', - actionType: 'test-action-type-name', - name: 'action-connector', - isPreconfigured: false, - isDeprecated: false, - referencedByCount: 0, - config: {}, - }; - - const actionType = actionTypeRegistryMock.createMockActionTypeModel({ - id: 'test-action-type-id', - iconClass: 'test', - selectMessage: 'test', - validateConnector: (): Promise> => { - return Promise.resolve({}); - }, - validateParams: (): Promise> => { - const validationResult = { errors: {} }; - return Promise.resolve(validationResult); - }, - actionConnectorFields: null, - }); - actionTypeRegistry.get.mockReturnValue(actionType); - actionTypeRegistry.has.mockReturnValue(true); - useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; - const wrapper = mountWithIntl( - {}} - reloadConnectors={() => { - return new Promise(() => {}); - }} - actionTypeRegistry={actionTypeRegistry} - /> - ); - - const connectorNameField = wrapper.find('[data-test-subj="nameInput"]'); - expect(connectorNameField.exists()).toBeTruthy(); - expect(connectorNameField.first().prop('value')).toBe('action-connector'); - }); - - test('if preconfigured connector rendered correct in the edit form', () => { - const connector = { - secrets: {}, - id: 'test', - actionTypeId: 'test-action-type-id', - actionType: 'test-action-type-name', - name: 'preconfigured-connector', - isPreconfigured: true, - isDeprecated: false, - referencedByCount: 0, - config: {}, - }; - - const actionType = actionTypeRegistryMock.createMockActionTypeModel({ - id: 'test-action-type-id', - iconClass: 'test', - selectMessage: 'test', - validateConnector: (): Promise> => { - return Promise.resolve({}); - }, - validateParams: (): Promise> => { - const validationResult = { errors: {} }; - return Promise.resolve(validationResult); - }, - actionConnectorFields: null, - }); - actionTypeRegistry.get.mockReturnValue(actionType); - actionTypeRegistry.has.mockReturnValue(true); - useKibanaMock().services.actionTypeRegistry = actionTypeRegistry; - - const wrapper = mountWithIntl( - {}} - reloadConnectors={() => { - return new Promise(() => {}); - }} - actionTypeRegistry={actionTypeRegistry} - /> - ); - - const preconfiguredBadge = wrapper.find('[data-test-subj="preconfiguredBadge"]'); - expect(preconfiguredBadge.exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="saveAndCloseEditedActionButton"]').exists()).toBeFalsy(); - }); -}); 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 deleted file mode 100644 index 89c1d72a32716..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx +++ /dev/null @@ -1,433 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useCallback, useReducer, useState, useEffect } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiTitle, - EuiFlyoutHeader, - EuiFlyout, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiButtonEmpty, - EuiButton, - EuiBetaBadge, - EuiText, - EuiLink, - EuiTabs, - EuiTab, - EuiSpacer, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { Option, none, some } from 'fp-ts/lib/Option'; -import { ActionTypeExecutorResult, isActionTypeExecutorResult } from '@kbn/actions-plugin/common'; -import { ActionConnectorForm, getConnectorErrors } from './action_connector_form'; -import { TestConnectorForm } from './test_connector_form'; -import { - ActionConnector, - ConnectorEditFlyoutProps, - IErrorObject, - EditConectorTabs, - UserConfiguredActionConnector, - ActionConnectorFieldsCallbacks, -} from '../../../types'; -import { ConnectorReducer, createConnectorReducer } from './connector_reducer'; -import { updateActionConnector, executeAction } from '../../lib/action_connector_api'; -import { hasSaveActionsCapability } from '../../lib/capabilities'; -import './connector_edit_flyout.scss'; -import { useKibana } from '../../../common/lib/kibana'; -import { getConnectorWithInvalidatedFields } from '../../lib/value_validators'; -import { CenterJustifiedSpinner } from '../../components/center_justified_spinner'; - -const ConnectorEditFlyout = ({ - initialConnector, - onClose, - tab = EditConectorTabs.Configuration, - reloadConnectors, - consumer, - actionTypeRegistry, -}: ConnectorEditFlyoutProps) => { - const [hasErrors, setHasErrors] = useState(true); - const { - http, - notifications: { toasts }, - docLinks, - application: { capabilities }, - } = useKibana().services; - - const getConnectorWithoutSecrets = () => ({ - ...(initialConnector as UserConfiguredActionConnector< - Record, - Record - >), - secrets: {}, - }); - const canSave = hasSaveActionsCapability(capabilities); - - const reducer: ConnectorReducer< - Record, - Record - > = createConnectorReducer, Record>(); - const [{ connector }, dispatch] = useReducer(reducer, { - connector: getConnectorWithoutSecrets(), - }); - const [isLoading, setIsLoading] = useState(false); - const [errors, setErrors] = useState<{ - configErrors: IErrorObject; - connectorBaseErrors: IErrorObject; - connectorErrors: IErrorObject; - secretsErrors: IErrorObject; - }>({ - configErrors: {}, - connectorBaseErrors: {}, - connectorErrors: {}, - secretsErrors: {}, - }); - - const actionTypeModel = actionTypeRegistry.get(connector.actionTypeId); - - useEffect(() => { - (async () => { - setIsLoading(true); - const res = await getConnectorErrors(connector, actionTypeModel); - setHasErrors( - !!Object.keys(res.connectorErrors).find( - (errorKey) => (res.connectorErrors as IErrorObject)[errorKey].length >= 1 - ) - ); - setIsLoading(false); - setErrors({ ...res }); - })(); - }, [connector, actionTypeModel]); - - const [isSaving, setIsSaving] = useState(false); - const [selectedTab, setTab] = useState(tab); - - const [hasChanges, setHasChanges] = useState(false); - - const setConnector = (value: any) => { - dispatch({ command: { type: 'setConnector' }, payload: { key: 'connector', value } }); - }; - - const [testExecutionActionParams, setTestExecutionActionParams] = useState< - Record - >({}); - const [testExecutionResult, setTestExecutionResult] = - useState>>(none); - const [isExecutingAction, setIsExecutinAction] = useState(false); - const handleSetTab = useCallback( - () => - setTab((prevTab) => { - if (prevTab === EditConectorTabs.Configuration) { - return EditConectorTabs.Test; - } - if (testExecutionResult !== none) { - setTestExecutionResult(none); - } - return EditConectorTabs.Configuration; - }), - [testExecutionResult] - ); - - const [callbacks, setCallbacks] = useState(null); - - const closeFlyout = useCallback(() => { - setConnector(getConnectorWithoutSecrets()); - setHasChanges(false); - setTestExecutionResult(none); - onClose(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [onClose]); - - const onActionConnectorSave = async (): Promise => - await updateActionConnector({ http, connector, id: connector.id }) - .then((savedConnector) => { - toasts.addSuccess( - i18n.translate( - 'xpack.triggersActionsUI.sections.editConnectorForm.updateSuccessNotificationText', - { - defaultMessage: "Updated '{connectorName}'", - values: { - connectorName: savedConnector.name, - }, - } - ) - ); - return savedConnector; - }) - .catch((errorRes) => { - toasts.addDanger( - errorRes.body?.message ?? - i18n.translate( - 'xpack.triggersActionsUI.sections.editConnectorForm.updateErrorNotificationText', - { defaultMessage: 'Cannot update a connector.' } - ) - ); - return undefined; - }); - - const flyoutTitle = connector.isPreconfigured ? ( - <> - -

- -   - -

-
- - - - - ) : ( - -

- -

-
- ); - - const onExecutAction = () => { - setIsExecutinAction(true); - return executeAction({ id: connector.id, params: testExecutionActionParams, http }) - .then((result) => { - setIsExecutinAction(false); - setTestExecutionResult(some(result)); - return result; - }) - .catch((ex: Error | ActionTypeExecutorResult) => { - const result: ActionTypeExecutorResult = isActionTypeExecutorResult(ex) - ? ex - : { - actionId: connector.id, - status: 'error', - message: ex.message, - }; - setIsExecutinAction(false); - setTestExecutionResult(some(result)); - return result; - }); - }; - - const setConnectorWithErrors = () => - setConnector( - getConnectorWithInvalidatedFields( - connector, - errors.configErrors, - errors.secretsErrors, - errors.connectorBaseErrors - ) - ); - - const onSaveClicked = async (closeAfterSave: boolean = true) => { - if (hasErrors) { - setConnectorWithErrors(); - return; - } - - setIsSaving(true); - - // Do not allow to save the connector if there is an error - try { - await callbacks?.beforeActionConnectorSave?.(); - } catch (e) { - setIsSaving(false); - return; - } - - const savedAction = await onActionConnectorSave(); - setIsSaving(false); - - if (savedAction) { - setHasChanges(false); - await callbacks?.afterActionConnectorSave?.(savedAction); - if (closeAfterSave) { - closeFlyout(); - } - if (connector.isMissingSecrets) { - connector.isMissingSecrets = false; - } - if (reloadConnectors) { - reloadConnectors(); - } - } - }; - - return ( - - - - {actionTypeModel ? ( - - - - ) : null} - {flyoutTitle} - - - - {i18n.translate('xpack.triggersActionsUI.sections.editConnectorForm.tabText', { - defaultMessage: 'Configuration', - })} - - - {i18n.translate('xpack.triggersActionsUI.sections.testConnectorForm.tabText', { - defaultMessage: 'Test', - })} - - - - - {selectedTab === EditConectorTabs.Configuration ? ( - !connector.isPreconfigured ? ( - <> - { - setHasChanges(true); - // if the user changes the connector, "forget" the last execution - // so the user comes back to a clean form ready to run a fresh test - setTestExecutionResult(none); - dispatch(changes); - }} - actionTypeRegistry={actionTypeRegistry} - consumer={consumer} - setCallbacks={setCallbacks} - isEdit={true} - /> - {isLoading ? ( - <> - - {' '} - - ) : ( - <> - )} - - ) : ( - <> - - {i18n.translate( - 'xpack.triggersActionsUI.sections.editConnectorForm.descriptionText', - { - defaultMessage: 'This connector is readonly.', - } - )} - - - - - - ) - ) : ( - - )} - - - - - - {i18n.translate( - 'xpack.triggersActionsUI.sections.editConnectorForm.cancelButtonLabel', - { - defaultMessage: 'Cancel', - } - )} - - - - - {canSave && actionTypeModel && !connector.isPreconfigured ? ( - <> - - { - await onSaveClicked(false); - }} - > - - - - - { - await onSaveClicked(); - }} - > - - - - - ) : null} - - - - - - ); -}; - -// eslint-disable-next-line import/no-default-export -export { ConnectorEditFlyout as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_error_mock.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_error_mock.tsx new file mode 100644 index 0000000000000..0e9ce3a88cab8 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_error_mock.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useEffect } from 'react'; +import { TextField } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { ActionConnectorFieldsProps } from '../../../types'; + +const TestConnectorError: React.FunctionComponent = ({ + readOnly, + isEdit, + registerPreSubmitValidator, +}) => { + const preSubmitValidator = useCallback(async () => { + return { + message: <>{'Error on pre submit validator'}, + }; + }, []); + + useEffect( + () => registerPreSubmitValidator(preSubmitValidator), + [preSubmitValidator, registerPreSubmitValidator] + ); + + return ( + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { TestConnectorError as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form.test.tsx new file mode 100644 index 0000000000000..b4cd8d1b979ac --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form.test.tsx @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { lazy } from 'react'; +import { + AppMockRenderer, + createAppMockRenderer, +} from '../../components/builtin_action_types/test_utils'; +import { ConnectorForm } from './connector_form'; +import { actionTypeRegistryMock } from '../../action_type_registry.mock'; +import userEvent from '@testing-library/user-event'; +import { waitFor } from '@testing-library/dom'; +import { act } from '@testing-library/react'; + +describe('ConnectorForm', () => { + let appMockRenderer: AppMockRenderer; + const onChange = jest.fn(); + const onFormModifiedChange = jest.fn(); + + const connector = { + actionTypeId: 'test', + isDeprecated: false, + config: {}, + secrets: {}, + isMissingSecrets: false, + }; + + beforeEach(() => { + jest.clearAllMocks(); + appMockRenderer = createAppMockRenderer(); + }); + + it('calls on change with correct init state', async () => { + const actionTypeModel = actionTypeRegistryMock.createMockActionTypeModel({ + actionConnectorFields: lazy(() => import('./connector_mock')), + }); + + const result = appMockRenderer.render( + + ); + + expect(result.getByTestId('nameInput')).toBeInTheDocument(); + expect(onChange).toHaveBeenCalledWith({ + isSubmitted: false, + isSubmitting: false, + isValid: undefined, + preSubmitValidator: null, + submit: expect.anything(), + }); + expect(onFormModifiedChange).toHaveBeenCalledWith(false); + }); + + it('calls onFormModifiedChange when form is modified', async () => { + appMockRenderer.coreStart.application.capabilities = { + ...appMockRenderer.coreStart.application.capabilities, + actions: { save: true, show: true }, + }; + + const actionTypeModel = actionTypeRegistryMock.createMockActionTypeModel({ + actionConnectorFields: lazy(() => import('./connector_mock')), + }); + + const result = appMockRenderer.render( + + ); + + expect(result.getByTestId('nameInput')).toBeInTheDocument(); + await act(async () => { + await userEvent.type(result.getByRole('textbox'), 'My connector', { delay: 100 }); + }); + + await waitFor(() => { + expect(onFormModifiedChange).toHaveBeenCalledWith(true); + }); + }); + + it('calls onChange when the form is invalid', async () => { + const actionTypeModel = actionTypeRegistryMock.createMockActionTypeModel({ + actionConnectorFields: lazy(() => import('./connector_mock')), + }); + + const result = appMockRenderer.render( + + ); + + expect(result.getByTestId('nameInput')).toBeInTheDocument(); + await act(async () => { + const submit = onChange.mock.calls[0][0].submit; + await submit(); + }); + + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith({ + isSubmitted: true, + isSubmitting: true, + isValid: false, + preSubmitValidator: null, + submit: expect.anything(), + }); + }); + }); + + it('registers the pre submit validator correctly', async () => { + const actionTypeModel = actionTypeRegistryMock.createMockActionTypeModel({ + actionConnectorFields: lazy(() => import('./connector_mock')), + }); + + appMockRenderer.render( + + ); + + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith({ + isSubmitted: false, + isSubmitting: false, + isValid: undefined, + preSubmitValidator: expect.anything(), + submit: expect.anything(), + }); + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form.tsx new file mode 100644 index 0000000000000..f3dabe0b1284c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form.tsx @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useEffect, useState } from 'react'; +import { isEmpty } from 'lodash'; +import { + Form, + FormHook, + useForm, + useFormIsModified, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { EuiSpacer } from '@elastic/eui'; +import { ActionTypeModel, ConnectorValidationFunc } from '../../../types'; +import { ConnectorFormFields } from './connector_form_fields'; +import { ConnectorFormSchema } from './types'; +import { EncryptedFieldsCallout } from './encrypted_fields_callout'; + +export interface ConnectorFormState { + isValid: boolean | undefined; + isSubmitted: boolean; + isSubmitting: boolean; + submit: FormHook['submit']; + preSubmitValidator: ConnectorValidationFunc | null; +} + +interface Props { + actionTypeModel: ActionTypeModel | null; + connector: ConnectorFormSchema & { isMissingSecrets: boolean }; + isEdit: boolean; + /** Handler to receive state changes updates */ + onChange?: (state: ConnectorFormState) => void; + /** Handler to receive update on the form "isModified" state */ + onFormModifiedChange?: (isModified: boolean) => void; +} +/** + * The serializer and deserializer are needed to transform the headers of + * the webhook connectors. The webhook connector uses the UseArray component + * to add dynamic headers to the form. The UseArray component formats the fields + * as an array of objects. The schema for the headers of the webhook connector + * is Record. We need to transform the UseArray format to the one + * accepted by the backend. At the moment, the UseArray does not accepts + * a serializer and deserializer so it has to be done on the form level. When issue #133107 + * is resolved we should move the serializer and deserializer functions to the + * webhook connector. + */ + +// TODO: Remove when https://github.com/elastic/kibana/issues/133107 is resolved +const formDeserializer = (data: ConnectorFormSchema): ConnectorFormSchema => { + if (data.actionTypeId !== '.webhook') { + return data; + } + + const webhookData = data as { config: { headers?: Record } }; + const headers = Object.entries(webhookData?.config?.headers ?? {}).map(([key, value]) => ({ + key, + value, + })); + + return { + ...data, + config: { + ...data.config, + headers: isEmpty(headers) ? undefined : headers, + }, + }; +}; + +// TODO: Remove when https://github.com/elastic/kibana/issues/133107 is resolved +const formSerializer = (formData: ConnectorFormSchema): ConnectorFormSchema => { + if (formData.actionTypeId !== '.webhook') { + return formData; + } + + const webhookFormData = formData as { + config: { headers?: Array<{ key: string; value: string }> }; + }; + const headers = (webhookFormData?.config?.headers ?? []).reduce( + (acc, header) => ({ + ...acc, + [header.key]: header.value, + }), + {} + ); + + return { + ...formData, + config: { + ...formData.config, + headers: isEmpty(headers) ? null : headers, + }, + }; +}; + +const ConnectorFormComponent: React.FC = ({ + actionTypeModel, + connector, + isEdit, + onChange, + onFormModifiedChange, +}) => { + const { form } = useForm({ + defaultValue: connector, + serializer: formSerializer, + deserializer: formDeserializer, + }); + const { submit, isValid: isFormValid, isSubmitted, isSubmitting } = form; + const [preSubmitValidator, setPreSubmitValidator] = useState( + null + ); + + const registerPreSubmitValidator = useCallback((validator: ConnectorValidationFunc) => { + setPreSubmitValidator(() => validator); + }, []); + + const isFormModified = useFormIsModified({ + form, + discard: ['__internal__'], + }); + + useEffect(() => { + if (onChange) { + onChange({ isValid: isFormValid, isSubmitted, isSubmitting, submit, preSubmitValidator }); + } + }, [onChange, isFormValid, isSubmitted, isSubmitting, submit, preSubmitValidator]); + + useEffect(() => { + if (onFormModifiedChange) { + onFormModifiedChange(isFormModified); + } + }, [isFormModified, onFormModifiedChange]); + + return ( +
+ + + + + ); +}; + +export const ConnectorForm = React.memo(ConnectorFormComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form_fields.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form_fields.test.tsx new file mode 100644 index 0000000000000..747a4925d35f8 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form_fields.test.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { lazy } from 'react'; +import { coreMock } from '@kbn/core/public/mocks'; +import { + AppMockRenderer, + createAppMockRenderer, + FormTestProvider, +} from '../../components/builtin_action_types/test_utils'; +import { ConnectorFormFields } from './connector_form_fields'; +import { actionTypeRegistryMock } from '../../action_type_registry.mock'; +import { waitFor } from '@testing-library/dom'; + +describe('ConnectorFormFields', () => { + let appMockRenderer: AppMockRenderer; + const onSubmit = jest.fn(); + const defaultValue = { + id: 'test-id', + actionTypeId: '.test', + isDeprecated: 'false', + name: 'My test connector', + }; + + beforeEach(() => { + jest.clearAllMocks(); + coreMock.createSetup(); + appMockRenderer = createAppMockRenderer(); + }); + + it('does not show the fields component if it is null', async () => { + const actionTypeModel = actionTypeRegistryMock.createMockActionTypeModel({ + actionConnectorFields: null, + }); + + const result = appMockRenderer.render( + + {}} + /> + + ); + + expect(result.queryByTestId('connector-settings-label')).toBeFalsy(); + }); + + it('shows the connector fields', async () => { + const actionTypeModel = actionTypeRegistryMock.createMockActionTypeModel({ + actionConnectorFields: lazy(() => import('./connector_mock')), + }); + + const result = appMockRenderer.render( + + {}} + /> + + ); + + expect(result.getByTestId('connector-settings-label')).toBeInTheDocument(); + await waitFor(() => { + expect(result.getByTestId('test-connector-text-field')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form_fields.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form_fields.tsx new file mode 100644 index 0000000000000..6fd63f6c64ed4 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form_fields.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, Suspense } from 'react'; + +import { EuiTitle, EuiSpacer, EuiErrorBoundary } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { ActionTypeModel, ConnectorValidationFunc } from '../../../types'; +import { SectionLoading } from '../../components/section_loading'; +import { hasSaveActionsCapability } from '../../lib/capabilities'; +import { useKibana } from '../../../common/lib/kibana'; +import { ConnectorFormFieldsGlobal } from './connector_form_fields_global'; + +interface ConnectorFormFieldsProps { + actionTypeModel: ActionTypeModel | null; + isEdit: boolean; + registerPreSubmitValidator: (validator: ConnectorValidationFunc) => void; +} + +const ConnectorFormFieldsComponent: React.FC = ({ + actionTypeModel, + isEdit, + registerPreSubmitValidator, +}) => { + const { + application: { capabilities }, + } = useKibana().services; + const canSave = hasSaveActionsCapability(capabilities); + const FieldsComponent = actionTypeModel?.actionConnectorFields ?? null; + + return ( + <> + + + {FieldsComponent !== null ? ( + <> + +

+ +

+
+ + + + + + } + > + + + + + ) : null} + + ); +}; + +export const ConnectorFormFields = memo(ConnectorFormFieldsComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form_fields_global.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form_fields_global.test.tsx new file mode 100644 index 0000000000000..9df345c45f1e1 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form_fields_global.test.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import userEvent from '@testing-library/user-event'; +import { render, act } from '@testing-library/react'; +import { FormTestProvider } from '../../components/builtin_action_types/test_utils'; +import { ConnectorFormFieldsGlobal } from './connector_form_fields_global'; + +describe('ConnectorFormFieldsGlobal', () => { + const onSubmit = jest.fn(); + const defaultValue = { + id: 'test-id', + actionTypeId: '.test', + isDeprecated: 'false', + name: 'My test connector', + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('submits correctly', async () => { + const { getByTestId } = render( + + + + ); + + expect(getByTestId('nameInput')).toBeInTheDocument(); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(onSubmit).toHaveBeenCalledWith({ + data: { + actionTypeId: '.test', + id: 'test-id', + isDeprecated: 'false', + name: 'My test connector', + }, + isValid: true, + }); + }); + + it('validates the name correctly', async () => { + const { getByTestId, getByText } = render( + /** + * By removing the default value we initiate the form + * with an empty state. Submitting the form + * should return an error because the text field "name" + * is empty. + */ + + + + ); + + await act(async () => { + userEvent.click(getByTestId('form-test-provide-submit')); + }); + + expect(getByText('Name is required.')).toBeInTheDocument(); + expect(onSubmit).toHaveBeenCalledWith({ + data: {}, + isValid: false, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form_fields_global.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form_fields_global.tsx new file mode 100644 index 0000000000000..3e26897111358 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_form_fields_global.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; + +import { FieldConfig, UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers'; +import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import { i18n } from '@kbn/i18n'; + +import { HiddenField } from '../../components/hidden_field'; + +interface ConnectorFormData { + name: string; +} + +interface ConnectorFormFieldsProps { + canSave: boolean; +} + +const { emptyField } = fieldValidators; + +const nameConfig: FieldConfig<{ name: string }, ConnectorFormData> = { + label: 'Connector name', + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.triggersActionsUI.sections.actionConnectorForm.error.requiredNameText', + { + defaultMessage: 'Name is required.', + } + ) + ), + }, + ], +}; + +const ConnectorFormFieldsGlobalComponent: React.FC = ({ canSave }) => { + return ( + <> + + + + + + ); +}; + +export const ConnectorFormFieldsGlobal = memo(ConnectorFormFieldsGlobalComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_mock.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_mock.tsx new file mode 100644 index 0000000000000..9b6f35df7916d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_mock.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useEffect } from 'react'; +import { TextField } from '@kbn/es-ui-shared-plugin/static/forms/components'; +import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { ActionConnectorFieldsProps } from '../../../types'; + +const TestConnector: React.FunctionComponent = ({ + readOnly, + isEdit, + registerPreSubmitValidator, +}) => { + const preSubmitValidator = useCallback(async () => {}, []); + + useEffect( + () => registerPreSubmitValidator(preSubmitValidator), + [preSubmitValidator, registerPreSubmitValidator] + ); + + return ( + <> + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { TestConnector as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.test.ts deleted file mode 100644 index bc23b87176598..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.test.ts +++ /dev/null @@ -1,103 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { UserConfiguredActionConnector } from '../../../types'; -import { createConnectorReducer, ConnectorReducer } from './connector_reducer'; - -describe('connector reducer', () => { - let initialConnector: UserConfiguredActionConnector< - Record, - Record - >; - beforeAll(() => { - initialConnector = { - secrets: {}, - id: 'test', - actionTypeId: 'test-action-type-id', - name: 'action-connector', - referencedByCount: 0, - isPreconfigured: false, - isDeprecated: false, - config: {}, - }; - }); - - const connectorReducer: ConnectorReducer< - Record, - Record - > = createConnectorReducer, Record>(); - - test('if property name was changed', () => { - const updatedConnector = connectorReducer( - { connector: initialConnector }, - { - command: { type: 'setProperty' }, - payload: { - key: 'name', - value: 'new name', - }, - } - ); - expect(updatedConnector.connector.name).toBe('new name'); - }); - - test('if config property was added and updated', () => { - const updatedConnector = connectorReducer( - { connector: initialConnector }, - { - command: { type: 'setConfigProperty' }, - payload: { - key: 'testConfig', - value: 'new test config property', - }, - } - ); - expect(updatedConnector.connector.config.testConfig).toBe('new test config property'); - - const updatedConnectorUpdatedProperty = connectorReducer( - { connector: updatedConnector.connector }, - { - command: { type: 'setConfigProperty' }, - payload: { - key: 'testConfig', - value: 'test config property updated', - }, - } - ); - expect(updatedConnectorUpdatedProperty.connector.config.testConfig).toBe( - 'test config property updated' - ); - }); - - test('if secrets property was added', () => { - const updatedConnector = connectorReducer( - { connector: initialConnector }, - { - command: { type: 'setSecretsProperty' }, - payload: { - key: 'testSecret', - value: 'new test secret property', - }, - } - ); - expect(updatedConnector.connector.secrets.testSecret).toBe('new test secret property'); - - const updatedConnectorUpdatedProperty = connectorReducer( - { connector: updatedConnector.connector }, - { - command: { type: 'setSecretsProperty' }, - payload: { - key: 'testSecret', - value: 'test secret property updated', - }, - } - ); - expect(updatedConnectorUpdatedProperty.connector.secrets.testSecret).toBe( - 'test secret property updated' - ); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.ts deleted file mode 100644 index 54ed950a4e96d..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_reducer.ts +++ /dev/null @@ -1,135 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { isEqual } from 'lodash'; -import { Reducer } from 'react'; -import { UserConfiguredActionConnector } from '../../../types'; - -export type InitialConnector = Partial< - UserConfiguredActionConnector -> & - Pick, 'actionTypeId' | 'config' | 'secrets'>; - -interface CommandType< - T extends 'setConnector' | 'setProperty' | 'setConfigProperty' | 'setSecretsProperty' -> { - type: T; -} - -interface Payload { - key: Keys; - value: Value; -} - -interface TPayload { - key: Key; - value: T[Key] | null; -} - -export type ConnectorReducerAction = - | { - command: CommandType<'setConnector'>; - payload: Payload<'connector', InitialConnector>; - } - | { - command: CommandType<'setProperty'>; - payload: TPayload< - UserConfiguredActionConnector, - keyof UserConfiguredActionConnector - >; - } - | { - command: CommandType<'setConfigProperty'>; - payload: TPayload; - } - | { - command: CommandType<'setSecretsProperty'>; - payload: TPayload; - }; - -export type ConnectorReducer = Reducer< - { connector: UserConfiguredActionConnector }, - ConnectorReducerAction ->; - -export const createConnectorReducer = - () => - < - ConnectorPhase extends - | InitialConnector - | UserConfiguredActionConnector - >( - state: { connector: ConnectorPhase }, - action: ConnectorReducerAction - ) => { - const { connector } = state; - - switch (action.command.type) { - case 'setConnector': { - const { key, value } = action.payload as Payload<'connector', ConnectorPhase>; - if (key === 'connector') { - return { - ...state, - connector: value, - }; - } else { - return state; - } - } - case 'setProperty': { - const { key, value } = action.payload as TPayload< - UserConfiguredActionConnector, - keyof UserConfiguredActionConnector - >; - if (isEqual(connector[key], value)) { - return state; - } else { - return { - ...state, - connector: { - ...connector, - [key]: value, - }, - }; - } - } - case 'setConfigProperty': { - const { key, value } = action.payload as TPayload; - if (isEqual(connector.config[key], value)) { - return state; - } else { - return { - ...state, - connector: { - ...connector, - config: { - ...(connector.config as Config), - [key]: value, - }, - }, - }; - } - } - case 'setSecretsProperty': { - const { key, value } = action.payload as TPayload; - if (isEqual(connector.secrets[key], value)) { - return state; - } else { - return { - ...state, - connector: { - ...connector, - secrets: { - ...(connector.secrets as Secrets), - [key]: value, - }, - }, - }; - } - } - } - }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connectors_selection.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connectors_selection.test.tsx index b56e245633ece..9f3760337adee 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connectors_selection.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connectors_selection.test.tsx @@ -11,7 +11,7 @@ import { mountWithIntl } from '@kbn/test-jest-helpers'; import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; import { ConnectorsSelection } from './connectors_selection'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; -import { ActionType, ConnectorValidationResult, GenericValidationResult } from '../../../types'; +import { ActionType, GenericValidationResult } from '../../../types'; import { EuiFieldText } from '@elastic/eui'; describe('connectors_selection', () => { @@ -76,9 +76,6 @@ describe('connectors_selection', () => { id: '.pagerduty', iconClass: 'test', selectMessage: 'test', - validateConnector: (): Promise> => { - return Promise.resolve({}); - }, validateParams: (): Promise> => { const validationResult = { errors: {} }; return Promise.resolve(validationResult); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/foooter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/foooter.tsx new file mode 100644 index 0000000000000..4fed596ebde68 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/foooter.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutFooter, + EuiButton, + EuiButtonEmpty, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; + +interface Props { + isSaving: boolean; + disabled: boolean; + buttonType: 'back' | 'cancel'; + onSubmit: () => Promise; + onTestConnector?: () => void; + onBack: () => void; + onCancel: () => void; +} + +const FlyoutFooterComponent: React.FC = ({ + isSaving, + disabled, + buttonType, + onCancel, + onBack, + onTestConnector, + onSubmit, +}) => { + return ( + + + + {buttonType === 'back' ? ( + + {i18n.translate( + 'xpack.triggersActionsUI.sections.actionConnectorAdd.backButtonLabel', + { + defaultMessage: 'Back', + } + )} + + ) : ( + + {i18n.translate( + 'xpack.triggersActionsUI.sections.actionConnectorAdd.cancelButtonLabel', + { + defaultMessage: 'Cancel', + } + )} + + )} + + + + <> + {onTestConnector && ( + + + + + + )} + + + + + + + + + + + ); +}; + +export const FlyoutFooter = memo(FlyoutFooterComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.tsx new file mode 100644 index 0000000000000..7da1919b55654 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/header.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiText, + EuiFlyoutHeader, + IconType, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +const FlyoutHeaderComponent: React.FC<{ + icon?: IconType | null; + actionTypeName?: string | null; + actionTypeMessage?: string | null; +}> = ({ icon, actionTypeName, actionTypeMessage }) => { + return ( + + + {icon ? ( + + + + ) : null} + + {actionTypeName && actionTypeMessage ? ( + <> + +

+ +

+
+ + {actionTypeMessage} + + + ) : ( + +

+ +

+
+ )} +
+
+
+ ); +}; + +export const FlyoutHeader = memo(FlyoutHeaderComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.test.tsx new file mode 100644 index 0000000000000..11a71c80a9851 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.test.tsx @@ -0,0 +1,544 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { lazy } from 'react'; + +import { actionTypeRegistryMock } from '../../../action_type_registry.mock'; +import userEvent from '@testing-library/user-event'; +import { waitFor } from '@testing-library/dom'; +import { act } from '@testing-library/react'; +import { + AppMockRenderer, + createAppMockRenderer, +} from '../../../components/builtin_action_types/test_utils'; +import CreateConnectorFlyout from '.'; + +const createConnectorResponse = { + connector_type_id: 'test', + is_preconfigured: false, + is_deprecated: false, + name: 'My test', + config: { testTextField: 'My text field' }, + secrets: {}, + id: '123', +}; + +describe('CreateConnectorFlyout', () => { + let appMockRenderer: AppMockRenderer; + const onClose = jest.fn(); + const onConnectorCreated = jest.fn(); + const onTestConnector = jest.fn(); + + const actionTypeModel = actionTypeRegistryMock.createMockActionTypeModel({ + actionConnectorFields: lazy(() => import('../connector_mock')), + }); + + const actionTypes = [ + { + id: actionTypeModel.id, + enabled: true, + name: 'Test', + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic' as const, + }, + ]; + + const actionTypeRegistry = actionTypeRegistryMock.create(); + + beforeEach(() => { + jest.clearAllMocks(); + actionTypeRegistry.has.mockReturnValue(true); + actionTypeRegistry.get.mockReturnValue(actionTypeModel); + appMockRenderer = createAppMockRenderer(); + appMockRenderer.coreStart.application.capabilities = { + ...appMockRenderer.coreStart.application.capabilities, + actions: { save: true, show: true }, + }; + appMockRenderer.coreStart.http.post = jest.fn().mockResolvedValue(createConnectorResponse); + }); + + it('renders', async () => { + const { getByTestId } = appMockRenderer.render( + + ); + + expect(getByTestId('create-connector-flyout')).toBeInTheDocument(); + expect(getByTestId('create-connector-flyout-header')).toBeInTheDocument(); + expect(getByTestId('create-connector-flyout-footer')).toBeInTheDocument(); + }); + + it('renders action type menu on flyout open', () => { + const { getByTestId } = appMockRenderer.render( + + ); + + expect(getByTestId(`${actionTypeModel.id}-card`)).toBeInTheDocument(); + }); + + describe('Licensing', () => { + it('renders banner with subscription links when gold features are disabled due to licensing', () => { + const disabledActionType = actionTypeRegistryMock.createMockActionTypeModel(); + + const { getByTestId } = appMockRenderer.render( + + ); + + expect(getByTestId('upgrade-your-license-callout')).toBeInTheDocument(); + }); + + it('does not render banner with subscription links when only platinum features are disabled due to licensing', () => { + const disabledActionType = actionTypeRegistryMock.createMockActionTypeModel(); + + const { queryByTestId } = appMockRenderer.render( + + ); + + expect(queryByTestId('upgrade-your-license-callout')).toBeFalsy(); + }); + + it('does not render banner with subscription links when only enterprise features are disabled due to licensing', () => { + const disabledActionType = actionTypeRegistryMock.createMockActionTypeModel(); + + const { queryByTestId } = appMockRenderer.render( + + ); + + expect(queryByTestId('upgrade-your-license-callout')).toBeFalsy(); + }); + }); + + describe('Header', () => { + it('does not shows the icon when selection connector type', async () => { + const { queryByTestId } = appMockRenderer.render( + + ); + + expect(queryByTestId('create-connector-flyout-header-icon')).not.toBeInTheDocument(); + }); + + it('shows the correct title when selecting connector type', async () => { + const { getByText } = appMockRenderer.render( + + ); + + expect(getByText('Select a connector')).toBeInTheDocument(); + }); + + it('shows the icon when the connector type is selected', async () => { + const { getByTestId, getByText, queryByText } = appMockRenderer.render( + + ); + + act(() => { + userEvent.click(getByTestId(`${actionTypeModel.id}-card`)); + }); + + await waitFor(() => { + expect(getByTestId('test-connector-text-field')).toBeInTheDocument(); + }); + + expect(queryByText('Select a connector')).not.toBeInTheDocument(); + expect(getByTestId('create-connector-flyout-header-icon')).toBeInTheDocument(); + expect(getByText('Test connector')).toBeInTheDocument(); + expect(getByText(`selectMessage-${actionTypeModel.id}`)).toBeInTheDocument(); + }); + }); + + describe('Submitting', () => { + it('creates a connector correctly', async () => { + const { getByTestId } = appMockRenderer.render( + + ); + + act(() => { + userEvent.click(getByTestId(`${actionTypeModel.id}-card`)); + }); + + await waitFor(() => { + expect(getByTestId('test-connector-text-field')).toBeInTheDocument(); + }); + + await act(async () => { + await userEvent.type(getByTestId('nameInput'), 'My test', { + delay: 100, + }); + await userEvent.type(getByTestId('test-connector-text-field'), 'My text field', { + delay: 100, + }); + }); + + act(() => { + userEvent.click(getByTestId('create-connector-flyout-save-btn')); + }); + + await waitFor(() => { + expect(appMockRenderer.coreStart.http.post).toHaveBeenCalledWith('/api/actions/connector', { + body: `{"name":"My test","config":{"testTextField":"My text field"},"secrets":{},"connector_type_id":"${actionTypeModel.id}"}`, + }); + }); + + expect(onClose).toHaveBeenCalled(); + expect(onTestConnector).not.toHaveBeenCalled(); + expect(onConnectorCreated).toHaveBeenCalledWith({ + actionTypeId: 'test', + config: { testTextField: 'My text field' }, + id: '123', + isDeprecated: false, + isMissingSecrets: undefined, + isPreconfigured: false, + name: 'My test', + secrets: {}, + }); + }); + + it('runs pre submit validator correctly', async () => { + const errorActionTypeModel = actionTypeRegistryMock.createMockActionTypeModel({ + actionConnectorFields: lazy(() => import('../connector_error_mock')), + }); + actionTypeRegistry.get.mockReturnValue(errorActionTypeModel); + + const { getByTestId, getByText } = appMockRenderer.render( + + ); + + act(() => { + userEvent.click(getByTestId(`${errorActionTypeModel.id}-card`)); + }); + + await waitFor(() => { + expect(getByTestId('test-connector-error-text-field')).toBeInTheDocument(); + }); + + await act(async () => { + await userEvent.type(getByTestId('nameInput'), 'My test', { + delay: 100, + }); + await userEvent.type(getByTestId('test-connector-error-text-field'), 'My text field', { + delay: 100, + }); + }); + + act(() => { + userEvent.click(getByTestId('create-connector-flyout-save-btn')); + }); + + await waitFor(() => { + expect(getByText('Error on pre submit validator')).toBeInTheDocument(); + }); + }); + }); + + describe('Testing', () => { + it('saves and test correctly', async () => { + const { getByTestId } = appMockRenderer.render( + + ); + + act(() => { + userEvent.click(getByTestId(`${actionTypeModel.id}-card`)); + }); + + await waitFor(() => { + expect(getByTestId('test-connector-text-field')).toBeInTheDocument(); + }); + + await act(async () => { + await userEvent.type(getByTestId('nameInput'), 'My test', { + delay: 100, + }); + await userEvent.type(getByTestId('test-connector-text-field'), 'My text field', { + delay: 100, + }); + }); + + act(() => { + userEvent.click(getByTestId('create-connector-flyout-save-test-btn')); + }); + + await waitFor(() => { + expect(appMockRenderer.coreStart.http.post).toHaveBeenCalledWith('/api/actions/connector', { + body: `{"name":"My test","config":{"testTextField":"My text field"},"secrets":{},"connector_type_id":"${actionTypeModel.id}"}`, + }); + }); + + expect(onClose).toHaveBeenCalled(); + expect(onTestConnector).toHaveBeenCalledWith({ + actionTypeId: 'test', + config: { testTextField: 'My text field' }, + id: '123', + isDeprecated: false, + isMissingSecrets: undefined, + isPreconfigured: false, + name: 'My test', + secrets: {}, + }); + expect(onConnectorCreated).toHaveBeenCalledWith({ + actionTypeId: 'test', + config: { testTextField: 'My text field' }, + id: '123', + isDeprecated: false, + isMissingSecrets: undefined, + isPreconfigured: false, + name: 'My test', + secrets: {}, + }); + }); + }); + + describe('Footer', () => { + it('shows the buttons', async () => { + const { getByTestId } = appMockRenderer.render( + + ); + + expect(getByTestId('create-connector-flyout-cancel-btn')).toBeInTheDocument(); + expect(getByTestId('create-connector-flyout-save-test-btn')).toBeInTheDocument(); + expect(getByTestId('create-connector-flyout-save-btn')).toBeInTheDocument(); + }); + + it('shows the back button when selecting an action type', async () => { + const { getByTestId } = appMockRenderer.render( + + ); + + act(() => { + userEvent.click(getByTestId(`${actionTypeModel.id}-card`)); + }); + + await waitFor(() => { + expect(getByTestId('create-connector-flyout-back-btn')).toBeInTheDocument(); + }); + }); + + it('shows the action types when pressing the back button', async () => { + const { getByTestId } = appMockRenderer.render( + + ); + + act(() => { + userEvent.click(getByTestId(`${actionTypeModel.id}-card`)); + }); + + await waitFor(() => { + expect(getByTestId('create-connector-flyout-back-btn')).toBeInTheDocument(); + expect(getByTestId('nameInput')).toBeInTheDocument(); + }); + + act(() => { + userEvent.click(getByTestId('create-connector-flyout-back-btn')); + }); + + expect(getByTestId(`${actionTypeModel.id}-card`)).toBeInTheDocument(); + }); + + it('does not show the save and test button if the onTestConnector is not provided', async () => { + const { queryByTestId } = appMockRenderer.render( + + ); + + expect(queryByTestId('create-connector-flyout-save-test-btn')).not.toBeInTheDocument(); + }); + + it('closes the flyout when pressing cancel', async () => { + const { getByTestId } = appMockRenderer.render( + + ); + + act(() => { + userEvent.click(getByTestId('create-connector-flyout-cancel-btn')); + }); + + expect(onClose).toHaveBeenCalled(); + }); + + it('disables the buttons when the user does not have permissions to create a connector', async () => { + appMockRenderer.coreStart.application.capabilities = { + ...appMockRenderer.coreStart.application.capabilities, + actions: { save: false, show: true }, + }; + + const { getByTestId } = appMockRenderer.render( + + ); + + expect(getByTestId('create-connector-flyout-cancel-btn')).not.toBeDisabled(); + expect(getByTestId('create-connector-flyout-save-test-btn')).toBeDisabled(); + expect(getByTestId('create-connector-flyout-save-btn')).toBeDisabled(); + }); + + it('disables the buttons when there are error on the form', async () => { + const { getByTestId } = appMockRenderer.render( + + ); + + act(() => { + userEvent.click(getByTestId(`${actionTypeModel.id}-card`)); + }); + + await waitFor(() => { + expect(getByTestId('test-connector-text-field')).toBeInTheDocument(); + }); + + act(() => { + userEvent.click(getByTestId('create-connector-flyout-save-btn')); + }); + + await waitFor(() => { + expect(getByTestId('create-connector-flyout-back-btn')).not.toBeDisabled(); + expect(getByTestId('create-connector-flyout-save-test-btn')).toBeDisabled(); + expect(getByTestId('create-connector-flyout-save-btn')).toBeDisabled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.tsx new file mode 100644 index 0000000000000..63eab1bf93229 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.tsx @@ -0,0 +1,199 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, ReactNode, useCallback, useEffect, useRef, useState } from 'react'; +import { EuiFlyout, EuiFlyoutBody } from '@elastic/eui'; + +import { + ActionConnector, + ActionType, + ActionTypeModel, + ActionTypeRegistryContract, +} from '../../../../types'; +import { hasSaveActionsCapability } from '../../../lib/capabilities'; +import { useKibana } from '../../../../common/lib/kibana'; +import { ActionTypeMenu } from '../action_type_menu'; +import { useCreateConnector } from '../../../hooks/use_create_connector'; +import { ConnectorForm, ConnectorFormState } from '../connector_form'; +import { ConnectorFormSchema } from '../types'; +import { FlyoutHeader } from './header'; +import { FlyoutFooter } from './foooter'; +import { UpgradeLicenseCallOut } from './upgrade_license_callout'; + +export interface CreateConnectorFlyoutProps { + actionTypeRegistry: ActionTypeRegistryContract; + onClose: () => void; + supportedActionTypes?: ActionType[]; + onConnectorCreated?: (connector: ActionConnector) => void; + onTestConnector?: (connector: ActionConnector) => void; +} + +const CreateConnectorFlyoutComponent: React.FC = ({ + actionTypeRegistry, + supportedActionTypes, + onClose, + onConnectorCreated, + onTestConnector, +}) => { + const { + application: { capabilities }, + } = useKibana().services; + const { isLoading: isSavingConnector, createConnector } = useCreateConnector(); + + const isMounted = useRef(false); + const [actionType, setActionType] = useState(null); + const [hasActionsUpgradeableByTrial, setHasActionsUpgradeableByTrial] = useState(false); + const canSave = hasSaveActionsCapability(capabilities); + + const [preSubmitValidationErrorMessage, setPreSubmitValidationErrorMessage] = + useState(null); + + const [formState, setFormState] = useState({ + isSubmitted: false, + isSubmitting: false, + isValid: undefined, + submit: async () => ({ isValid: false, data: {} as ConnectorFormSchema }), + preSubmitValidator: null, + }); + + const initialConnector = { + actionTypeId: actionType?.id ?? '', + isDeprecated: false, + config: {}, + secrets: {}, + isMissingSecrets: false, + }; + + const { preSubmitValidator, submit, isValid: isFormValid, isSubmitting } = formState; + + const hasErrors = isFormValid === false; + const isSaving = isSavingConnector || isSubmitting; + const footerButtonType = actionType != null ? 'back' : 'cancel'; + const actionTypeModel: ActionTypeModel | null = + actionType != null ? actionTypeRegistry.get(actionType.id) : null; + + const validateAndCreateConnector = useCallback(async () => { + setPreSubmitValidationErrorMessage(null); + + const { isValid, data } = await submit(); + if (!isMounted.current) { + // User has closed the flyout meanwhile submitting the form + return; + } + + if (isValid) { + if (preSubmitValidator) { + const validatorRes = await preSubmitValidator(); + + if (validatorRes) { + setPreSubmitValidationErrorMessage(validatorRes.message); + return; + } + } + + /** + * At this point the form is valid + * and there are no pre submit error messages. + */ + + const { actionTypeId, name, config, secrets } = data; + const validConnector = { + actionTypeId, + name: name ?? '', + config: config ?? {}, + secrets: secrets ?? {}, + }; + + const createdConnector = await createConnector(validConnector); + return createdConnector; + } + }, [submit, preSubmitValidator, createConnector]); + + const resetActionType = useCallback(() => setActionType(null), []); + + const testConnector = useCallback(async () => { + const createdConnector = await validateAndCreateConnector(); + + if (createdConnector) { + if (onConnectorCreated) { + onConnectorCreated(createdConnector); + } + + if (onTestConnector) { + onTestConnector(createdConnector); + } + + onClose(); + } + }, [validateAndCreateConnector, onClose, onConnectorCreated, onTestConnector]); + + const onSubmit = useCallback(async () => { + const createdConnector = await validateAndCreateConnector(); + if (createdConnector) { + if (onConnectorCreated) { + onConnectorCreated(createdConnector); + } + + onClose(); + } + }, [validateAndCreateConnector, onClose, onConnectorCreated]); + + useEffect(() => { + isMounted.current = true; + + return () => { + isMounted.current = false; + }; + }, []); + + return ( + + + : null} + > + {actionType == null ? ( + + ) : null} + {actionType != null ? ( + <> + + {preSubmitValidationErrorMessage} + + ) : null} + + + + ); +}; + +export const CreateConnectorFlyout = memo(CreateConnectorFlyoutComponent); + +// eslint-disable-next-line import/no-default-export +export { CreateConnectorFlyout as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/upgrade_license_callout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/upgrade_license_callout.tsx new file mode 100644 index 0000000000000..191422e6f6336 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/upgrade_license_callout.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { + EuiCallOut, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiButtonEmpty, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { VIEW_LICENSE_OPTIONS_LINK } from '../../../../common/constants'; +import { useKibana } from '../../../../common/lib/kibana'; + +const UpgradeLicenseCallOutComponent: React.FC = () => { + const { http } = useKibana().services; + + return ( + + + + + + + + + + + + + + + + + ); +}; + +export const UpgradeLicenseCallOut = memo(UpgradeLicenseCallOutComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/foooter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/foooter.tsx new file mode 100644 index 0000000000000..5f19449df2bb5 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/foooter.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutFooter, + EuiButton, + EuiButtonEmpty, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; + +interface Props { + isSaving: boolean; + showButtons: boolean; + disabled: boolean; + onCancel: () => void; + onSubmit: () => void; + onSubmitAndClose: () => void; +} + +const FlyoutFooterComponent: React.FC = ({ + isSaving, + showButtons, + disabled, + onCancel, + onSubmit, + onSubmitAndClose, +}) => { + return ( + + + + + {i18n.translate( + 'xpack.triggersActionsUI.sections.editConnectorForm.cancelButtonLabel', + { + defaultMessage: 'Cancel', + } + )} + + + + + {showButtons ? ( + <> + + + + + + + + + + + + ) : null} + + + + + ); +}; + +export const FlyoutFooter = memo(FlyoutFooterComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/header.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/header.tsx new file mode 100644 index 0000000000000..9e3f5d58f0b55 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/header.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { css } from '@emotion/react'; +import { + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiText, + EuiFlyoutHeader, + IconType, + EuiBetaBadge, + EuiTab, + EuiTabs, + useEuiTheme, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { EditConnectorTabs } from '../../../../types'; + +const FlyoutHeaderComponent: React.FC<{ + isPreconfigured: boolean; + connectorName: string; + connectorTypeDesc: string; + selectedTab: EditConnectorTabs; + setTab: () => void; + icon?: IconType | null; +}> = ({ icon, isPreconfigured, connectorName, connectorTypeDesc, selectedTab, setTab }) => { + const { euiTheme } = useEuiTheme(); + + return ( + + + {icon ? ( + + + + ) : null} + + {isPreconfigured ? ( + <> + +

+ +   + +

+
+ + + + + ) : ( + +

+ +

+
+ )} +
+
+ + + {i18n.translate('xpack.triggersActionsUI.sections.editConnectorForm.tabText', { + defaultMessage: 'Configuration', + })} + + + {i18n.translate('xpack.triggersActionsUI.sections.testConnectorForm.tabText', { + defaultMessage: 'Test', + })} + + +
+ ); +}; + +export const FlyoutHeader = memo(FlyoutHeaderComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/index.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/index.test.tsx new file mode 100644 index 0000000000000..75b9091e19294 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/index.test.tsx @@ -0,0 +1,630 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { lazy } from 'react'; + +import { actionTypeRegistryMock } from '../../../action_type_registry.mock'; +import userEvent from '@testing-library/user-event'; +import { waitFor } from '@testing-library/dom'; +import { act } from '@testing-library/react'; +import { + AppMockRenderer, + createAppMockRenderer, +} from '../../../components/builtin_action_types/test_utils'; +import EditConnectorFlyout from '.'; +import { ActionConnector, EditConnectorTabs, GenericValidationResult } from '../../../../types'; + +const updateConnectorResponse = { + connector_type_id: 'test', + is_preconfigured: false, + is_deprecated: false, + name: 'My test', + config: { testTextField: 'My text field' }, + secrets: {}, + id: '123', +}; + +const executeConnectorResponse = { + status: 'ok', + data: {}, +}; + +const connector: ActionConnector = { + id: '123', + name: 'My test', + actionTypeId: '.test', + config: { testTextField: 'My text field' }, + secrets: { secretTextField: 'super secret' }, + isDeprecated: false, + isPreconfigured: false, + isMissingSecrets: false, +}; + +describe('EditConnectorFlyout', () => { + let appMockRenderer: AppMockRenderer; + const onClose = jest.fn(); + const onConnectorUpdated = jest.fn(); + + const actionTypeModel = actionTypeRegistryMock.createMockActionTypeModel({ + actionConnectorFields: lazy(() => import('../connector_mock')), + validateParams: (): Promise> => { + const validationResult = { errors: {} }; + return Promise.resolve(validationResult); + }, + }); + + const actionTypeRegistry = actionTypeRegistryMock.create(); + + beforeEach(() => { + jest.clearAllMocks(); + actionTypeRegistry.has.mockReturnValue(true); + actionTypeRegistry.get.mockReturnValue(actionTypeModel); + appMockRenderer = createAppMockRenderer(); + appMockRenderer.coreStart.application.capabilities = { + ...appMockRenderer.coreStart.application.capabilities, + actions: { save: true, show: true }, + }; + appMockRenderer.coreStart.http.put = jest.fn().mockResolvedValue(updateConnectorResponse); + appMockRenderer.coreStart.http.post = jest.fn().mockResolvedValue(executeConnectorResponse); + }); + + it('renders', async () => { + const { getByTestId } = appMockRenderer.render( + + ); + + expect(getByTestId('edit-connector-flyout')).toBeInTheDocument(); + expect(getByTestId('edit-connector-flyout-header')).toBeInTheDocument(); + expect(getByTestId('edit-connector-flyout-footer')).toBeInTheDocument(); + }); + + it('renders the connector form correctly', async () => { + const { getByTestId, queryByText } = appMockRenderer.render( + + ); + + await waitFor(() => { + expect(getByTestId('nameInput')).toBeInTheDocument(); + expect(getByTestId('test-connector-text-field')).toBeInTheDocument(); + }); + + expect(queryByText('This connector is readonly.')).not.toBeInTheDocument(); + expect(getByTestId('nameInput')).toHaveValue('My test'); + expect(getByTestId('test-connector-text-field')).toHaveValue('My text field'); + }); + + it('removes the secrets from the connector', async () => { + const { getByTestId } = appMockRenderer.render( + + ); + + await waitFor(() => { + expect(getByTestId('test-connector-secret-text-field')).toBeInTheDocument(); + }); + + expect(getByTestId('test-connector-secret-text-field')).toHaveValue(''); + }); + + it('renders correctly if the connector is preconfigured', async () => { + const { getByText } = appMockRenderer.render( + + ); + + expect(getByText('This connector is readonly.')).toBeInTheDocument(); + }); + + describe('Header', () => { + it('shows the icon', async () => { + const { getByTestId } = appMockRenderer.render( + + ); + + expect(getByTestId('edit-connector-flyout-header-icon')).toBeInTheDocument(); + }); + + it('does not shows the icon when is not defined', async () => { + // @ts-expect-error + actionTypeRegistry.get.mockReturnValue({ ...actionTypeModel, iconClass: undefined }); + const { queryByTestId } = appMockRenderer.render( + + ); + + expect(queryByTestId('edit-connector-flyout-header-icon')).not.toBeInTheDocument(); + }); + + it('shows the correct title', async () => { + const { getByText } = appMockRenderer.render( + + ); + + expect(getByText('Edit connector')).toBeInTheDocument(); + }); + + it('shows the correct on preconfigured connectors', async () => { + const { getByTestId } = appMockRenderer.render( + + ); + + expect(getByTestId('preconfiguredBadge')).toBeInTheDocument(); + }); + }); + + describe('Tabs', () => { + it('shows the tabs', async () => { + const { getByTestId } = appMockRenderer.render( + + ); + + expect(getByTestId('configureConnectorTab')).toBeInTheDocument(); + expect(getByTestId('testConnectorTab')).toBeInTheDocument(); + }); + + it('navigates to the test form', async () => { + const { getByTestId } = appMockRenderer.render( + + ); + + expect(getByTestId('configureConnectorTab')).toBeInTheDocument(); + expect(getByTestId('testConnectorTab')).toBeInTheDocument(); + + act(() => { + userEvent.click(getByTestId('testConnectorTab')); + }); + + await waitFor(() => { + expect(getByTestId('test-connector-form')).toBeInTheDocument(); + }); + }); + + it('opens the provided tab', async () => { + const { getByTestId } = appMockRenderer.render( + + ); + + await waitFor(() => { + expect(getByTestId('test-connector-form')).toBeInTheDocument(); + }); + }); + }); + + describe('Submitting', () => { + it('updates the connector correctly', async () => { + const { getByTestId } = appMockRenderer.render( + + ); + + await waitFor(() => { + expect(getByTestId('test-connector-text-field')).toBeInTheDocument(); + }); + + await act(async () => { + await userEvent.clear(getByTestId('nameInput')); + await userEvent.type(getByTestId('nameInput'), 'My new name', { + delay: 100, + }); + await userEvent.type(getByTestId('test-connector-secret-text-field'), 'password', { + delay: 100, + }); + }); + + act(() => { + userEvent.click(getByTestId('edit-connector-flyout-save-btn')); + }); + + await waitFor(() => { + expect(appMockRenderer.coreStart.http.put).toHaveBeenCalledWith( + '/api/actions/connector/123', + { + body: '{"name":"My new name","config":{"testTextField":"My text field"},"secrets":{"secretTextField":"password"}}', + } + ); + }); + + expect(onClose).not.toHaveBeenCalled(); + expect(onConnectorUpdated).toHaveBeenCalledWith({ + actionTypeId: 'test', + config: { testTextField: 'My text field' }, + id: '123', + isDeprecated: false, + isMissingSecrets: undefined, + isPreconfigured: false, + name: 'My test', + secrets: {}, + }); + }); + + it('updates the connector and close the flyout correctly', async () => { + const { getByTestId } = appMockRenderer.render( + + ); + + await waitFor(() => { + expect(getByTestId('test-connector-text-field')).toBeInTheDocument(); + }); + + await act(async () => { + await userEvent.clear(getByTestId('nameInput')); + await userEvent.type(getByTestId('nameInput'), 'My new name', { + delay: 100, + }); + await userEvent.type(getByTestId('test-connector-secret-text-field'), 'password', { + delay: 100, + }); + }); + + act(() => { + userEvent.click(getByTestId('edit-connector-flyout-save-close-btn')); + }); + + await waitFor(() => { + expect(appMockRenderer.coreStart.http.put).toHaveBeenCalledWith( + '/api/actions/connector/123', + { + body: '{"name":"My new name","config":{"testTextField":"My text field"},"secrets":{"secretTextField":"password"}}', + } + ); + }); + + expect(onClose).toHaveBeenCalled(); + expect(onConnectorUpdated).toHaveBeenCalledWith({ + actionTypeId: 'test', + config: { testTextField: 'My text field' }, + id: '123', + isDeprecated: false, + isMissingSecrets: undefined, + isPreconfigured: false, + name: 'My test', + secrets: {}, + }); + }); + + it('runs pre submit validator correctly', async () => { + const errorActionTypeModel = actionTypeRegistryMock.createMockActionTypeModel({ + actionConnectorFields: lazy(() => import('../connector_error_mock')), + }); + actionTypeRegistry.get.mockReturnValue(errorActionTypeModel); + + const { getByTestId, getByText } = appMockRenderer.render( + + ); + + await waitFor(() => { + expect(getByTestId('test-connector-error-text-field')).toBeInTheDocument(); + }); + + act(() => { + userEvent.click(getByTestId('edit-connector-flyout-save-btn')); + }); + + await waitFor(() => { + expect(getByText('Error on pre submit validator')).toBeInTheDocument(); + }); + }); + }); + + describe('Testing', () => { + it('tests the connector correctly', async () => { + const { getByTestId } = appMockRenderer.render( + + ); + + await waitFor(() => { + expect(getByTestId('test-connector-form')).toBeInTheDocument(); + }); + + expect(getByTestId('executionAwaiting')).toBeInTheDocument(); + + act(() => { + userEvent.click(getByTestId('executeActionButton')); + }); + + await waitFor(() => { + expect(appMockRenderer.coreStart.http.post).toHaveBeenCalledWith( + '/api/actions/connector/123/_execute', + { body: '{"params":{}}' } + ); + }); + + expect(getByTestId('executionSuccessfulResult')).toBeInTheDocument(); + }); + + it('resets the results when changing tabs', async () => { + const { getByTestId } = appMockRenderer.render( + + ); + + await waitFor(() => { + expect(getByTestId('test-connector-form')).toBeInTheDocument(); + }); + + expect(getByTestId('executionAwaiting')).toBeInTheDocument(); + + act(() => { + userEvent.click(getByTestId('executeActionButton')); + }); + + await waitFor(() => { + expect(getByTestId('executionSuccessfulResult')).toBeInTheDocument(); + }); + + act(() => { + userEvent.click(getByTestId('configureConnectorTab')); + }); + + await waitFor(() => { + expect(getByTestId('nameInput')).toBeInTheDocument(); + }); + + act(() => { + userEvent.click(getByTestId('testConnectorTab')); + }); + + await waitFor(() => { + expect(getByTestId('test-connector-form')).toBeInTheDocument(); + }); + + expect(getByTestId('executionAwaiting')).toBeInTheDocument(); + }); + + it('throws an error correctly', async () => { + appMockRenderer.coreStart.http.post = jest + .fn() + .mockRejectedValue(new Error('error executing')); + + const { getByTestId } = appMockRenderer.render( + + ); + + await waitFor(() => { + expect(getByTestId('test-connector-form')).toBeInTheDocument(); + }); + + act(() => { + userEvent.click(getByTestId('executeActionButton')); + }); + + await waitFor(() => { + expect(getByTestId('executionFailureResult')).toBeInTheDocument(); + }); + }); + + it('resets the results when modifying the form', async () => { + const { getByTestId } = appMockRenderer.render( + + ); + + await waitFor(() => { + expect(getByTestId('test-connector-form')).toBeInTheDocument(); + }); + + act(() => { + userEvent.click(getByTestId('executeActionButton')); + }); + + await waitFor(() => { + expect(getByTestId('executionSuccessfulResult')).toBeInTheDocument(); + }); + + act(() => { + userEvent.click(getByTestId('configureConnectorTab')); + }); + + await waitFor(() => { + expect(getByTestId('nameInput')).toBeInTheDocument(); + }); + + await act(async () => { + await userEvent.clear(getByTestId('nameInput')); + await userEvent.type(getByTestId('nameInput'), 'My new name', { + delay: 10, + }); + }); + + act(() => { + userEvent.click(getByTestId('testConnectorTab')); + }); + + await waitFor(() => { + expect(getByTestId('test-connector-form')).toBeInTheDocument(); + }); + + expect(getByTestId('executionAwaiting')).toBeInTheDocument(); + expect(getByTestId('executeActionButton')).toBeDisabled(); + }); + }); + + describe('Footer', () => { + it('shows the buttons', async () => { + const { getByTestId } = appMockRenderer.render( + + ); + + expect(getByTestId('edit-connector-flyout-cancel-btn')).toBeInTheDocument(); + expect(getByTestId('edit-connector-flyout-save-btn')).toBeInTheDocument(); + expect(getByTestId('edit-connector-flyout-save-close-btn')).toBeInTheDocument(); + }); + + it('does not show the save and save and close button if the use does not have permissions to update connector', async () => { + appMockRenderer.coreStart.application.capabilities = { + ...appMockRenderer.coreStart.application.capabilities, + actions: { save: false, show: true }, + }; + + const { queryByTestId } = appMockRenderer.render( + + ); + + expect(queryByTestId('edit-connector-flyout-save-btn')).not.toBeInTheDocument(); + expect(queryByTestId('edit-connector-flyout-save-close-btn')).not.toBeInTheDocument(); + }); + + it('does not show the save and save and close button if the connector is preconfigured', async () => { + const { queryByTestId } = appMockRenderer.render( + + ); + + expect(queryByTestId('edit-connector-flyout-save-btn')).not.toBeInTheDocument(); + expect(queryByTestId('edit-connector-flyout-save-close-btn')).not.toBeInTheDocument(); + }); + + it('closes the flyout when pressing cancel', async () => { + const { getByTestId } = appMockRenderer.render( + + ); + + act(() => { + userEvent.click(getByTestId('edit-connector-flyout-cancel-btn')); + }); + + expect(onClose).toHaveBeenCalled(); + }); + + it('disables the buttons when there are error on the form', async () => { + const { getByTestId } = appMockRenderer.render( + + ); + + await waitFor(() => { + expect(getByTestId('test-connector-text-field')).toBeInTheDocument(); + }); + + act(() => { + /** + * Clear the name so the form can be invalid + */ + userEvent.clear(getByTestId('nameInput')); + userEvent.click(getByTestId('edit-connector-flyout-save-btn')); + }); + + await waitFor(() => { + expect(getByTestId('edit-connector-flyout-cancel-btn')).not.toBeDisabled(); + expect(getByTestId('edit-connector-flyout-save-close-btn')).toBeDisabled(); + expect(getByTestId('edit-connector-flyout-save-btn')).toBeDisabled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/index.tsx new file mode 100644 index 0000000000000..3dcbc3c700f45 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/index.tsx @@ -0,0 +1,294 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, ReactNode, useCallback, useEffect, useRef, useState } from 'react'; +import { EuiFlyout, EuiText, EuiFlyoutBody, EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { ActionTypeExecutorResult, isActionTypeExecutorResult } from '@kbn/actions-plugin/common'; +import { Option, none, some } from 'fp-ts/lib/Option'; +import { + ActionConnector, + ActionTypeModel, + ActionTypeRegistryContract, + EditConnectorTabs, + UserConfiguredActionConnector, +} from '../../../../types'; +import { ConnectorForm, ConnectorFormState } from '../connector_form'; +import type { ConnectorFormSchema } from '../types'; +import { useUpdateConnector } from '../../../hooks/use_edit_connector'; +import { useKibana } from '../../../../common/lib/kibana'; +import { hasSaveActionsCapability } from '../../../lib/capabilities'; +import TestConnectorForm from '../test_connector_form'; +import { useExecuteConnector } from '../../../hooks/use_execute_connector'; +import { FlyoutHeader } from './header'; +import { FlyoutFooter } from './foooter'; + +export interface EditConnectorFlyoutProps { + actionTypeRegistry: ActionTypeRegistryContract; + connector: ActionConnector; + onClose: () => void; + tab?: EditConnectorTabs; + onConnectorUpdated?: (connector: ActionConnector) => void; +} + +const getConnectorWithoutSecrets = ( + connector: UserConfiguredActionConnector, Record> +) => ({ + ...connector, + isMissingSecrets: connector.isMissingSecrets ?? false, + secrets: {}, +}); + +const ReadOnlyConnectorMessage: React.FC<{ href: string }> = ({ href }) => { + return ( + <> + + {i18n.translate('xpack.triggersActionsUI.sections.editConnectorForm.descriptionText', { + defaultMessage: 'This connector is readonly.', + })} + + + + + + ); +}; + +const EditConnectorFlyoutComponent: React.FC = ({ + actionTypeRegistry, + connector, + onClose, + tab = EditConnectorTabs.Configuration, + onConnectorUpdated, +}) => { + const { + docLinks, + application: { capabilities }, + } = useKibana().services; + const isMounted = useRef(false); + const canSave = hasSaveActionsCapability(capabilities); + const { isLoading: isUpdatingConnector, updateConnector } = useUpdateConnector(); + const { isLoading: isExecutingConnector, executeConnector } = useExecuteConnector(); + + const [preSubmitValidationErrorMessage, setPreSubmitValidationErrorMessage] = + useState(null); + + const [formState, setFormState] = useState({ + isSubmitted: false, + isSubmitting: false, + isValid: undefined, + submit: async () => ({ isValid: false, data: {} as ConnectorFormSchema }), + preSubmitValidator: null, + }); + + const [selectedTab, setTab] = useState(tab); + /** + * Test connector + */ + + const [testExecutionActionParams, setTestExecutionActionParams] = useState< + Record + >({}); + const [testExecutionResult, setTestExecutionResult] = + useState | undefined>>(none); + + const handleSetTab = useCallback( + () => + setTab((prevTab) => { + if (prevTab === EditConnectorTabs.Configuration) { + return EditConnectorTabs.Test; + } + + if (testExecutionResult !== none) { + setTestExecutionResult(none); + } + + return EditConnectorTabs.Configuration; + }), + [testExecutionResult] + ); + + const [isFormModified, setIsFormModified] = useState(false); + + const { preSubmitValidator, submit, isValid: isFormValid, isSubmitting } = formState; + const hasErrors = isFormValid === false; + const isSaving = isUpdatingConnector || isSubmitting || isExecutingConnector; + const actionTypeModel: ActionTypeModel | null = actionTypeRegistry.get(connector.actionTypeId); + const showButtons = canSave && actionTypeModel && !connector.isPreconfigured; + + const onExecutionAction = useCallback(async () => { + try { + const res = await executeConnector({ + connectorId: connector.id, + params: testExecutionActionParams, + }); + + setTestExecutionResult(some(res)); + } catch (error) { + const result: ActionTypeExecutorResult = isActionTypeExecutorResult(error) + ? error + : { + actionId: connector.id, + status: 'error', + message: error.message, + }; + setTestExecutionResult(some(result)); + } + }, [connector.id, executeConnector, testExecutionActionParams]); + + const onFormModifiedChange = useCallback( + (formModified: boolean) => { + setIsFormModified(formModified); + setTestExecutionResult(none); + }, + [setIsFormModified] + ); + + const closeFlyout = useCallback(() => { + onClose(); + }, [onClose]); + + const onClickSave = useCallback( + async (closeAfterSave: boolean = true) => { + setPreSubmitValidationErrorMessage(null); + + const { isValid, data } = await submit(); + if (!isMounted.current) { + // User has closed the flyout meanwhile submitting the form + return; + } + + if (isValid) { + if (preSubmitValidator) { + const validatorRes = await preSubmitValidator(); + + if (validatorRes) { + setPreSubmitValidationErrorMessage(validatorRes.message); + return; + } + } + + /** + * At this point the form is valid + * and there are no pre submit error messages. + */ + + const { name, config, secrets } = data; + const validConnector = { + id: connector.id, + name: name ?? '', + config: config ?? {}, + secrets: secrets ?? {}, + }; + + const updatedConnector = await updateConnector(validConnector); + + if (updatedConnector) { + /** + * ConnectorFormSchema has been saved. + * Set the from to clean state. + */ + onFormModifiedChange(false); + + if (onConnectorUpdated && updatedConnector) { + onConnectorUpdated(updatedConnector); + } + + if (closeAfterSave) { + closeFlyout(); + } + } + + return updatedConnector; + } + }, + [ + submit, + preSubmitValidator, + connector.id, + updateConnector, + onFormModifiedChange, + onConnectorUpdated, + closeFlyout, + ] + ); + + const onSubmit = useCallback(() => onClickSave(false), [onClickSave]); + const onSubmitAndClose = useCallback(() => onClickSave(true), [onClickSave]); + + useEffect(() => { + isMounted.current = true; + + return () => { + isMounted.current = false; + }; + }, []); + + return ( + + + + {selectedTab === EditConnectorTabs.Configuration ? ( + !connector.isPreconfigured ? ( + <> + + {preSubmitValidationErrorMessage} + + ) : ( + + ) + ) : ( + + )} + + + + ); +}; + +export const EditConnectorFlyout = memo(EditConnectorFlyoutComponent); + +// eslint-disable-next-line import/no-default-export +export { EditConnectorFlyout as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/encrypted_fields_callout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/encrypted_fields_callout.test.tsx new file mode 100644 index 0000000000000..6c55cd34c0655 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/encrypted_fields_callout.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; +import { FormTestProvider } from '../../components/builtin_action_types/test_utils'; +import { EncryptedFieldsCallout } from './encrypted_fields_callout'; +import { render, RenderResult } from '@testing-library/react'; + +const renderWithSecretFields = ({ + isEdit, + isMissingSecrets, + numberOfSecretFields, +}: { + isEdit: boolean; + isMissingSecrets: boolean; + numberOfSecretFields: number; +}): RenderResult => { + return render( + + + {Array.from({ length: numberOfSecretFields }).map((_, index) => { + return ( + + ); + })} + + + ); +}; + +describe('EncryptedFieldsCallout', () => { + const isCreateTests: Array<[number, string]> = [ + [1, 'Remember value label0. You must reenter it each time you edit the connector.'], + [ + 2, + 'Remember values label0 and label1. You must reenter them each time you edit the connector.', + ], + [ + 3, + 'Remember values label0, label1, and label2. You must reenter them each time you edit the connector.', + ], + ]; + + const isEditTests: Array<[number, string]> = [ + [1, 'Value label0 is encrypted. Please reenter value for this field.'], + [2, 'Values label0 and label1 are encrypted. Please reenter values for these fields.'], + [3, 'Values label0, label1, and label2 are encrypted. Please reenter values for these fields.'], + ]; + + const isMissingSecretsTests: Array<[number, string]> = [ + [ + 1, + 'Sensitive information is not imported. Please enter value for the following field label0.', + ], + [ + 2, + 'Sensitive information is not imported. Please enter values for the following fields label0 and label1.', + ], + [ + 3, + 'Sensitive information is not imported. Please enter values for the following fields label0, label1, and label2.', + ], + ]; + + const noSecretsTests: Array<[{ isEdit: boolean; isMissingSecrets: boolean }, string]> = [ + [{ isEdit: false, isMissingSecrets: false }, 'create-connector-secrets-callout'], + [{ isEdit: true, isMissingSecrets: false }, 'edit-connector-secrets-callout'], + [{ isEdit: false, isMissingSecrets: true }, 'missing-secrets-callout'], + ]; + + it.each(isCreateTests)( + 'shows the create connector callout correctly with number of secrets %d', + (numberOfSecretFields, label) => { + const { getByText } = renderWithSecretFields({ + isEdit: false, + isMissingSecrets: false, + numberOfSecretFields, + }); + + expect(getByText(label)).toBeInTheDocument(); + } + ); + + it.each(isEditTests)( + 'shows the edit connector callout correctly with number of secrets %d', + (numberOfSecretFields, label) => { + const { getByText } = renderWithSecretFields({ + isEdit: true, + isMissingSecrets: false, + numberOfSecretFields, + }); + + expect(getByText(label)).toBeInTheDocument(); + } + ); + + it.each(isMissingSecretsTests)( + 'shows the is missing secrets connector callout correctly with number of secrets %d', + (numberOfSecretFields, label) => { + const { getByText } = renderWithSecretFields({ + isEdit: false, + isMissingSecrets: true, + numberOfSecretFields, + }); + + expect(getByText(label)).toBeInTheDocument(); + } + ); + + it.each(noSecretsTests)('does not shows the callouts without secrets: %p', (props, testId) => { + const { queryByTestId } = renderWithSecretFields({ + ...props, + numberOfSecretFields: 0, + }); + + expect(queryByTestId(testId)).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/encrypted_fields_callout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/encrypted_fields_callout.tsx new file mode 100644 index 0000000000000..7bc0fdbc0703c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/encrypted_fields_callout.tsx @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { EuiSpacer, EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { + FieldsMap, + useFormContext, + useFormData, +} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; + +export interface EncryptedFieldsCalloutProps { + isEdit: boolean; + isMissingSecrets?: boolean | undefined; +} + +const Callout: React.FC<{ title: string; dataTestSubj: string }> = ({ title, dataTestSubj }) => { + return ( + <> + + + + + ); +}; + +const isEmpty = (value: string | undefined): value is string => value != null || !isEmpty(value); + +const getSecretFields = (fields: FieldsMap): FieldsMap => + Object.keys(fields) + .filter((fieldPath) => fieldPath.includes('secrets')) + .reduce((filteredFields, path) => ({ ...filteredFields, [path]: fields[path] }), {}); + +const getLabelsFromFields = (fields: FieldsMap): string[] => + Object.keys(fields) + .map((fieldPath) => fields[fieldPath].label) + .filter(isEmpty); + +const getCommaSeparatedLabel = (labels: string[]) => { + if (labels.length === 0) { + return ''; + } + + if (labels.length === 1) { + return `${labels[0]}`; + } + + if (labels.length === 2) { + return labels.join(' and '); + } + + const lastLabel = labels[labels.length - 1]; + const commaSeparatedLabelsWithoutLastItem = labels.slice(0, -1).join(', '); + + return `${commaSeparatedLabelsWithoutLastItem}, and ${lastLabel}`; +}; + +const EncryptedFieldsCalloutComponent: React.FC = ({ + isEdit, + isMissingSecrets, +}) => { + /** + * This is needed to rerender on any form change + * and listen to any form field changes. + */ + const [_] = useFormData(); + const { getFields } = useFormContext(); + + const allFields = getFields(); + const secretFields = getSecretFields(allFields); + const totalSecretFields = Object.keys(secretFields).length; + const secretFieldsLabel = getCommaSeparatedLabel(getLabelsFromFields(secretFields)); + + if (Object.keys(secretFields).length === 0) { + return null; + } + + if (isMissingSecrets) { + return ( + + ); + } + + if (!isEdit) { + return ( + + ); + } + + if (isEdit) { + return ( + + ); + } + + return null; +}; + +export const EncryptedFieldsCallout = memo(EncryptedFieldsCalloutComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/index.ts index 75d29fd4b0c09..993139926b696 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/index.ts @@ -7,16 +7,19 @@ import { lazy } from 'react'; import { suspendedComponentWithProps } from '../../lib/suspended_component_with_props'; +import { ConnectorAddModalProps } from './connector_add_modal'; +import type { CreateConnectorFlyoutProps } from './create_connector_flyout'; +import type { EditConnectorFlyoutProps } from './edit_connector_flyout'; -export const ConnectorAddFlyout = suspendedComponentWithProps( - lazy(() => import('./connector_add_flyout')) +export const CreateConnectorFlyout = suspendedComponentWithProps( + lazy(() => import('./create_connector_flyout')) ); -export const ConnectorEditFlyout = suspendedComponentWithProps( - lazy(() => import('./connector_edit_flyout')) +export const EditConnectorFlyout = suspendedComponentWithProps( + lazy(() => import('./edit_connector_flyout')) ); export const ActionForm = suspendedComponentWithProps(lazy(() => import('./action_form'))); -export const ConnectorAddModal = suspendedComponentWithProps( +export const ConnectorAddModal = suspendedComponentWithProps( lazy(() => import('./connector_add_modal')) ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.test.tsx index cc928d5c30fd9..17aaf90e0ddbf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.test.tsx @@ -9,11 +9,7 @@ import React, { lazy } from 'react'; import { I18nProvider } from '@kbn/i18n-react'; import TestConnectorForm from './test_connector_form'; import { none, some } from 'fp-ts/lib/Option'; -import { - ActionConnector, - ConnectorValidationResult, - GenericValidationResult, -} from '../../../types'; +import { ActionConnector, GenericValidationResult } from '../../../types'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { EuiFormRow, EuiFieldText, EuiText, EuiLink, EuiForm, EuiSelect } from '@elastic/eui'; import { mountWithIntl } from '@kbn/test-jest-helpers'; @@ -53,9 +49,6 @@ const actionType = { id: 'my-action-type', iconClass: 'test', selectMessage: 'test', - validateConnector: (): Promise> => { - return Promise.resolve({}); - }, validateParams: (): Promise> => { const validationResult = { errors: {} }; return Promise.resolve(validationResult); @@ -81,10 +74,7 @@ describe('test_connector_form', () => { actionParams={{}} setActionParams={() => {}} isExecutingAction={false} - onExecutAction={async () => ({ - actionId: '', - status: 'ok', - })} + onExecutionAction={async () => {}} executionResult={none} actionTypeRegistry={actionTypeRegistry} /> @@ -112,10 +102,7 @@ describe('test_connector_form', () => { actionParams={{}} setActionParams={() => {}} isExecutingAction={false} - onExecutAction={async () => ({ - actionId: '', - status: 'ok', - })} + onExecutionAction={async () => {}} executionResult={some({ actionId: '', status: 'ok', @@ -142,11 +129,7 @@ describe('test_connector_form', () => { actionParams={{}} setActionParams={() => {}} isExecutingAction={false} - onExecutAction={async () => ({ - actionId: '', - status: 'error', - message: 'Error Message', - })} + onExecutionAction={async () => {}} executionResult={some({ actionId: '', status: 'error', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx index bd7105edf139d..739ee4100d535 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx @@ -25,14 +25,14 @@ import { i18n } from '@kbn/i18n'; import { ActionTypeExecutorResult } from '@kbn/actions-plugin/common'; import { ActionConnector, ActionTypeRegistryContract, IErrorObject } from '../../../types'; -export interface ConnectorAddFlyoutProps { +export interface TestConnectorFormProps { connector: ActionConnector; executeEnabled: boolean; isExecutingAction: boolean; setActionParams: (params: Record) => void; actionParams: Record; - onExecutAction: () => Promise>; - executionResult: Option>; + onExecutionAction: () => Promise; + executionResult: Option | undefined>; actionTypeRegistry: ActionTypeRegistryContract; } @@ -42,10 +42,10 @@ export const TestConnectorForm = ({ executionResult, actionParams, setActionParams, - onExecutAction, + onExecutionAction, isExecutingAction, actionTypeRegistry, -}: ConnectorAddFlyoutProps) => { +}: TestConnectorFormProps) => { const [actionErrors, setActionErrors] = useState({}); const [hasErrors, setHasErrors] = useState(false); const actionTypeModel = actionTypeRegistry.get(connector.actionTypeId); @@ -129,7 +129,7 @@ export const TestConnectorForm = ({ isLoading={isExecutingAction} isDisabled={!executeEnabled || hasErrors || isExecutingAction} data-test-subj="executeActionButton" - onClick={onExecutAction} + onClick={onExecutionAction} > - result.status === 'ok' ? ( + result?.status === 'ok' ? ( ) : ( @@ -161,7 +161,7 @@ export const TestConnectorForm = ({ }, ]; - return ; + return ; }; const AwaitingExecution = () => ( @@ -198,9 +198,9 @@ const SuccessfulExecution = () => ( ); const FailedExecussion = ({ - executionResult: { message, serviceMessage }, + executionResult, }: { - executionResult: ActionTypeExecutorResult; + executionResult: ActionTypeExecutorResult | undefined; }) => { const items = [ { @@ -211,7 +211,7 @@ const FailedExecussion = ({ } ), description: - message ?? + executionResult?.message ?? i18n.translate( 'xpack.triggersActionsUI.sections.testConnectorForm.executionFailureUnknownReason', { @@ -220,7 +220,7 @@ const FailedExecussion = ({ ), }, ]; - if (serviceMessage) { + if (executionResult?.serviceMessage) { items.push({ title: i18n.translate( 'xpack.triggersActionsUI.sections.testConnectorForm.executionFailureAdditionalDetails', @@ -228,7 +228,7 @@ const FailedExecussion = ({ defaultMessage: 'Details:', } ), - description: serviceMessage, + description: executionResult.serviceMessage, }); } return ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/types.ts new file mode 100644 index 0000000000000..1f27a055a0b3c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/types.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { UserConfiguredActionConnector } from '../../../types'; + +/** + * The following type is equivalent to: + * + * interface ConnectorFormSchema { + * id?: string, + * name?: string, + * actionTypeId: string, + * isDeprecated: boolean, + * config: Config, + * secrets: Secrets, + * } + */ + +export type ConnectorFormSchema< + Config = Record, + Secrets = Record +> = Pick< + UserConfiguredActionConnector, + 'actionTypeId' | 'isDeprecated' | 'config' | 'secrets' +> & + Partial, 'id' | 'name'>>; 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 580acd6a16b88..5af7e73211d61 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 @@ -18,11 +18,7 @@ import { actionTypeRegistryMock } from '../../../action_type_registry.mock'; import { useKibana } from '../../../../common/lib/kibana'; jest.mock('../../../../common/lib/kibana'); -import { - ActionConnector, - ConnectorValidationResult, - GenericValidationResult, -} from '../../../../types'; +import { ActionConnector, GenericValidationResult } from '../../../../types'; import { times } from 'lodash'; jest.mock('../../../lib/action_connector_api', () => ({ @@ -87,10 +83,10 @@ describe('actions_connectors_list component empty', () => { ).toHaveLength(1); }); - test('if click create button should render ConnectorAddFlyout', async () => { + test('if click create button should render CreateConnectorFlyout', async () => { await setup(); wrapper.find('[data-test-subj="createFirstActionButton"]').first().simulate('click'); - expect(wrapper.find('ConnectorAddFlyout')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="create-connector-flyout"]').exists()).toBeTruthy(); }); }); @@ -168,9 +164,6 @@ describe('actions_connectors_list component with items', () => { id: 'test', iconClass: 'test', selectMessage: 'test', - validateConnector: (): Promise> => { - return Promise.resolve({}); - }, validateParams: (): Promise> => { const validationResult = { errors: {} }; return Promise.resolve(validationResult); @@ -263,12 +256,10 @@ describe('actions_connectors_list component with items', () => { `); }); - test('if select item for edit should render ConnectorEditFlyout', async () => { + test('if select item for edit should render EditConnectorFlyout', async () => { await setup(); await wrapper.find('[data-test-subj="edit1"]').first().find('button').simulate('click'); - - const edit = await wrapper.find('ConnectorEditFlyout'); - expect(edit).toHaveLength(1); + expect(wrapper.find('[data-test-subj="edit-connector-flyout"]').exists()).toBeTruthy(); }); }); 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 60b7b9711c557..043e562691c14 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 @@ -40,17 +40,17 @@ import { ActionConnector, ActionConnectorTableItem, ActionTypeIndex, - EditConectorTabs, + EditConnectorTabs, } from '../../../../types'; import { EmptyConnectorsPrompt } from '../../../components/prompts/empty_connectors_prompt'; import { useKibana } from '../../../../common/lib/kibana'; import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner'; -import ConnectorEditFlyout from '../../action_connector_form/connector_edit_flyout'; -import ConnectorAddFlyout from '../../action_connector_form/connector_add_flyout'; import { connectorDeprecatedMessage, deprecatedMessage, } from '../../../../common/connectors_selection'; +import { CreateConnectorFlyout } from '../../action_connector_form/create_connector_flyout'; +import { EditConnectorFlyout } from '../../action_connector_form/edit_connector_flyout'; const ConnectorIconTipWithSpacing = withTheme(({ theme }: { theme: EuiTheme }) => { return ( @@ -96,7 +96,7 @@ const ActionsConnectorsList: React.FunctionComponent = () => { const [addFlyoutVisible, setAddFlyoutVisibility] = useState(false); const [editConnectorProps, setEditConnectorProps] = useState<{ initialConnector?: ActionConnector; - tab?: EditConectorTabs; + tab?: EditConnectorTabs; isFix?: boolean; }>({}); const [connectorsToDelete, setConnectorsToDelete] = useState([]); @@ -175,7 +175,7 @@ const ActionsConnectorsList: React.FunctionComponent = () => { async function editItem( actionConnector: ActionConnector, - tab: EditConectorTabs, + tab: EditConnectorTabs, isFix?: boolean ) { setEditConnectorProps({ initialConnector: actionConnector, tab, isFix: isFix ?? false }); @@ -209,7 +209,7 @@ const ActionsConnectorsList: React.FunctionComponent = () => { <> editItem(item, EditConectorTabs.Configuration)} + onClick={() => editItem(item, EditConnectorTabs.Configuration)} key={item.id} disabled={actionTypesIndex ? !actionTypesIndex[item.actionTypeId]?.enabled : true} > @@ -280,7 +280,7 @@ const ActionsConnectorsList: React.FunctionComponent = () => { editItem(item, EditConectorTabs.Configuration, true)} + onClick={() => editItem(item, EditConnectorTabs.Configuration, true)} > {i18n.translate( 'xpack.triggersActionsUI.sections.actionsConnectorsList.connectorsListTable.columns.fixButtonLabel', @@ -297,7 +297,7 @@ const ActionsConnectorsList: React.FunctionComponent = () => { editItem(item, EditConectorTabs.Test)} + onRun={() => editItem(item, EditConnectorTabs.Test)} /> )} @@ -442,6 +442,7 @@ const ActionsConnectorsList: React.FunctionComponent = () => { )} setIsLoadingState={(isLoading: boolean) => setIsLoadingActionTypes(isLoading)} /> + {/* Render the view based on if there's data or if they can save */} {(isLoadingActions || isLoadingActionTypes) && } @@ -454,26 +455,29 @@ const ActionsConnectorsList: React.FunctionComponent = () => { )} {actionConnectorTableItems.length === 0 && !canSave && } {addFlyoutVisible ? ( - { setAddFlyoutVisibility(false); }} - onTestConnector={(connector) => editItem(connector, EditConectorTabs.Test)} - reloadConnectors={loadActions} + onTestConnector={(connector) => editItem(connector, EditConnectorTabs.Test)} + onConnectorCreated={loadActions} actionTypeRegistry={actionTypeRegistry} /> ) : null} {editConnectorProps.initialConnector ? ( - { setEditConnectorProps(omit(editConnectorProps, 'initialConnector')); }} - reloadConnectors={loadActions} + onConnectorUpdated={(connector) => { + setEditConnectorProps({ initialConnector: connector }); + loadActions(); + }} actionTypeRegistry={actionTypeRegistry} /> ) : null} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx index 1bb319f707a78..13adb84d5039b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx @@ -7,6 +7,8 @@ import { lazy } from 'react'; import { suspendedComponentWithProps } from '../lib/suspended_component_with_props'; +import type { CreateConnectorFlyoutProps } from './action_connector_form/create_connector_flyout'; +import type { EditConnectorFlyoutProps } from './action_connector_form/edit_connector_flyout'; export type { ActionGroupWithCondition, @@ -19,11 +21,11 @@ export const AlertConditionsGroup = lazy(() => import('./rule_form/rule_conditio export const AlertAdd = suspendedComponentWithProps(lazy(() => import('./rule_form/rule_add'))); export const AlertEdit = suspendedComponentWithProps(lazy(() => import('./rule_form/rule_edit'))); -export const ConnectorAddFlyout = suspendedComponentWithProps( - lazy(() => import('./action_connector_form/connector_add_flyout')) +export const ConnectorAddFlyout = suspendedComponentWithProps( + lazy(() => import('./action_connector_form/create_connector_flyout')) ); -export const ConnectorEditFlyout = suspendedComponentWithProps( - lazy(() => import('./action_connector_form/connector_edit_flyout')) +export const ConnectorEditFlyout = suspendedComponentWithProps( + lazy(() => import('./action_connector_form/edit_connector_flyout')) ); export const ActionForm = suspendedComponentWithProps( lazy(() => import('./action_connector_form/action_form')) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx index 06d5754c5ed4e..fba42f9038444 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.test.tsx @@ -19,7 +19,6 @@ import { Rule, RuleAddProps, RuleFlyoutCloseReason, - ConnectorValidationResult, GenericValidationResult, ValidationResult, } from '../../../types'; @@ -156,9 +155,6 @@ describe('rule_add', () => { id: 'my-action-type', iconClass: 'test', selectMessage: 'test', - validateConnector: (): Promise> => { - return Promise.resolve({}); - }, validateParams: (): Promise> => { const validationResult = { errors: {} }; return Promise.resolve(validationResult); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx index de09a3857ee3b..dfebee8e5d6ff 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_edit.test.tsx @@ -10,12 +10,7 @@ import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; import { act } from 'react-dom/test-utils'; import { coreMock } from '@kbn/core/public/mocks'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; -import { - ValidationResult, - Rule, - ConnectorValidationResult, - GenericValidationResult, -} from '../../../types'; +import { ValidationResult, Rule, GenericValidationResult } from '../../../types'; import { ruleTypeRegistryMock } from '../../rule_type_registry.mock'; import { ReactWrapper } from 'enzyme'; import RuleEdit from './rule_edit'; @@ -130,9 +125,6 @@ describe('rule_edit', () => { id: 'my-action-type', iconClass: 'test', selectMessage: 'test', - validateConnector: (): Promise> => { - return Promise.resolve({}); - }, validateParams: (): Promise> => { const validationResult = { errors: {} }; return Promise.resolve(validationResult); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx index c000dd15218b4..e622ee9ded9d9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.test.tsx @@ -17,7 +17,6 @@ import { Rule, RuleType, RuleTypeModel, - ConnectorValidationResult, GenericValidationResult, } from '../../../types'; import { RuleForm } from './rule_form'; @@ -56,16 +55,6 @@ describe('rule_form', () => { id: 'my-action-type', iconClass: 'test', selectMessage: 'test', - validateConnector: (): Promise> => { - return Promise.resolve({ - config: { - errors: {}, - }, - secrets: { - errors: {}, - }, - }); - }, validateParams: (): Promise> => { const validationResult = { errors: {} }; return Promise.resolve(validationResult); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/type_registry.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/type_registry.test.ts index 4b4ab8e7415d1..a64d7e6704a3a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/type_registry.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/type_registry.test.ts @@ -10,7 +10,6 @@ import { ValidationResult, RuleTypeModel, ActionTypeModel, - ConnectorValidationResult, GenericValidationResult, } from '../types'; import { actionTypeRegistryMock } from './action_type_registry.mock'; @@ -42,9 +41,6 @@ const getTestActionType = ( id: id || 'my-action-type', iconClass: iconClass || 'test', selectMessage: selectedMessage || 'test', - validateConnector: (): Promise> => { - return Promise.resolve({}); - }, validateParams: (): Promise> => { const validationResult = { errors: {} }; return Promise.resolve(validationResult); @@ -90,7 +86,6 @@ describe('get()', () => { "iconClass": "test", "id": "my-action-type-snapshot", "selectMessage": "test", - "validateConnector": [Function], "validateParams": [Function], } `); @@ -119,7 +114,6 @@ describe('list()', () => { selectMessage: 'test', actionConnectorFields: null, actionParamsFields: actionType.actionParamsFields, - validateConnector: actionTypes[0].validateConnector, validateParams: actionTypes[0].validateParams, }, ]); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_action_form.tsx new file mode 100644 index 0000000000000..e1c602296f66f --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_action_form.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { ConnectorProvider } from '../application/context/connector_context'; +import { ActionForm } from '../application/sections'; +import { ActionAccordionFormProps } from '../application/sections/action_connector_form/action_form'; +import { ConnectorServices } from '../types'; + +export const getActionFormLazy = ( + props: ActionAccordionFormProps & { connectorServices: ConnectorServices } +) => { + return ( + + + + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_add_alert_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_add_alert_flyout.tsx index 85735a7854d3f..22820c0e1d057 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/get_add_alert_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_add_alert_flyout.tsx @@ -6,9 +6,16 @@ */ import React from 'react'; +import { ConnectorProvider } from '../application/context/connector_context'; import { RuleAdd } from '../application/sections/rule_form'; -import type { RuleAddProps as AlertAddProps } from '../types'; +import type { ConnectorServices, RuleAddProps as AlertAddProps } from '../types'; -export const getAddAlertFlyoutLazy = (props: AlertAddProps) => { - return ; +export const getAddAlertFlyoutLazy = ( + props: AlertAddProps & { connectorServices: ConnectorServices } +) => { + return ( + + + + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_add_connector_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_add_connector_flyout.tsx index 09261714cf8cf..6c0c71bfc7888 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/get_add_connector_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_add_connector_flyout.tsx @@ -6,9 +6,17 @@ */ import React from 'react'; -import { ConnectorAddFlyout } from '../application/sections/action_connector_form'; -import type { ConnectorAddFlyoutProps } from '../types'; +import { ConnectorProvider } from '../application/context/connector_context'; +import { CreateConnectorFlyout } from '../application/sections/action_connector_form'; +import { CreateConnectorFlyoutProps } from '../application/sections/action_connector_form/create_connector_flyout'; +import { ConnectorServices } from '../types'; -export const getAddConnectorFlyoutLazy = (props: ConnectorAddFlyoutProps) => { - return ; +export const getAddConnectorFlyoutLazy = ( + props: CreateConnectorFlyoutProps & { connectorServices: ConnectorServices } +) => { + return ( + + + + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_edit_alert_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_edit_alert_flyout.tsx index 58bdc43e15377..6b45fade83025 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/get_edit_alert_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_edit_alert_flyout.tsx @@ -6,9 +6,16 @@ */ import React from 'react'; +import { ConnectorProvider } from '../application/context/connector_context'; import { RuleEdit } from '../application/sections/rule_form'; -import type { RuleEditProps as AlertEditProps } from '../types'; +import type { ConnectorServices, RuleEditProps as AlertEditProps } from '../types'; -export const getEditAlertFlyoutLazy = (props: AlertEditProps) => { - return ; +export const getEditAlertFlyoutLazy = ( + props: AlertEditProps & { connectorServices: ConnectorServices } +) => { + return ( + + + + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_edit_connector_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_edit_connector_flyout.tsx index 90ecea56856f5..493a36ee9dff1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/get_edit_connector_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_edit_connector_flyout.tsx @@ -6,9 +6,17 @@ */ import React from 'react'; -import { ConnectorEditFlyout } from '../application/sections/action_connector_form'; -import type { ConnectorEditFlyoutProps } from '../types'; +import { ConnectorProvider } from '../application/context/connector_context'; +import { EditConnectorFlyout } from '../application/sections/action_connector_form'; +import { EditConnectorFlyoutProps } from '../application/sections/action_connector_form/edit_connector_flyout'; +import { ConnectorServices } from '../types'; -export const getEditConnectorFlyoutLazy = (props: ConnectorEditFlyoutProps) => { - return ; +export const getEditConnectorFlyoutLazy = ( + props: EditConnectorFlyoutProps & { connectorServices: ConnectorServices } +) => { + return ( + + + + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_rules_list.tsx index b315668c4fab9..dd112d46b48a2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/get_rules_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_rules_list.tsx @@ -6,8 +6,14 @@ */ import React from 'react'; +import { ConnectorProvider } from '../application/context/connector_context'; import { RulesList } from '../application/sections'; +import { ConnectorServices } from '../types'; -export const getRulesListLazy = () => { - return ; +export const getRulesListLazy = (props: { connectorServices: ConnectorServices }) => { + return ( + + + + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts index f551ebae3bb81..a19643a8f51bd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts @@ -23,6 +23,7 @@ export const createStartServicesMock = (): TriggersAndActionsUiServices => { const core = coreMock.createStart(); return { ...core, + actions: { validateEmailAddresses: jest.fn() }, ruleTypeRegistry: { has: jest.fn(), register: jest.fn(), diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index 14e75198be5d1..3f21434864c49 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -45,8 +45,8 @@ export { AlertsTableFlyoutState } from './types'; export { ActionForm, - ConnectorAddFlyout, - ConnectorEditFlyout, + CreateConnectorFlyout, + EditConnectorFlyout, } from './application/sections/action_connector_form'; export type { ActionGroupWithCondition } from './application/sections'; diff --git a/x-pack/plugins/triggers_actions_ui/public/mocks.ts b/x-pack/plugins/triggers_actions_ui/public/mocks.ts index 71995641751e5..e05c50b71e0ed 100644 --- a/x-pack/plugins/triggers_actions_ui/public/mocks.ts +++ b/x-pack/plugins/triggers_actions_ui/public/mocks.ts @@ -19,8 +19,6 @@ import { RuleAddProps, RuleEditProps, RuleTypeModel, - ConnectorAddFlyoutProps, - ConnectorEditFlyoutProps, AlertsTableProps, AlertsTableConfigurationRegistry, } from './types'; @@ -34,22 +32,31 @@ import { getRulesListLazy } from './common/get_rules_list'; import { getAlertsTableStateLazy } from './common/get_alerts_table_state'; import { getRulesListNotifyBadgeLazy } from './common/get_rules_list_notify_badge'; import { AlertsTableStateProps } from './application/sections/alerts_table/alerts_table_state'; +import { CreateConnectorFlyoutProps } from './application/sections/action_connector_form/create_connector_flyout'; +import { EditConnectorFlyoutProps } from './application/sections/action_connector_form/edit_connector_flyout'; +import { getActionFormLazy } from './common/get_action_form'; +import { ActionAccordionFormProps } from './application/sections/action_connector_form/action_form'; function createStartMock(): TriggersAndActionsUIPublicPluginStart { const actionTypeRegistry = new TypeRegistry(); const ruleTypeRegistry = new TypeRegistry(); const alertsTableConfigurationRegistry = new TypeRegistry(); + const connectorServices = { validateEmailAddresses: jest.fn() }; return { actionTypeRegistry, ruleTypeRegistry, alertsTableConfigurationRegistry, - getAddConnectorFlyout: (props: Omit) => { - return getAddConnectorFlyoutLazy({ ...props, actionTypeRegistry }); + getActionForm: (props: Omit) => { + return getActionFormLazy({ ...props, actionTypeRegistry, connectorServices }); }, - getEditConnectorFlyout: (props: Omit) => { + getAddConnectorFlyout: (props: Omit) => { + return getAddConnectorFlyoutLazy({ ...props, actionTypeRegistry, connectorServices }); + }, + getEditConnectorFlyout: (props: Omit) => { return getEditConnectorFlyoutLazy({ ...props, actionTypeRegistry, + connectorServices, }); }, getAddAlertFlyout: (props: Omit) => { @@ -57,6 +64,7 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart { ...props, actionTypeRegistry, ruleTypeRegistry, + connectorServices, }); }, getEditAlertFlyout: (props: Omit) => { @@ -64,6 +72,7 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart { ...props, actionTypeRegistry, ruleTypeRegistry, + connectorServices, }); }, getAlertsStateTable: (props: AlertsTableStateProps) => { @@ -91,7 +100,7 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart { return getRulesListNotifyBadgeLazy(props); }, getRulesList: () => { - return getRulesListLazy(); + return getRulesListLazy({ connectorServices }); }, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 63a7b605d28ba..a572a0c15030b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -37,6 +37,7 @@ import { getRuleTagBadgeLazy } from './common/get_rule_tag_badge'; import { getRuleEventLogListLazy } from './common/get_rule_event_log_list'; import { getRulesListNotifyBadgeLazy } from './common/get_rules_list_notify_badge'; import { getRulesListLazy } from './common/get_rules_list'; +import { getActionFormLazy } from './common/get_action_form'; import { ExperimentalFeaturesService } from './common/experimental_features_service'; import { ExperimentalFeatures, @@ -48,8 +49,6 @@ import type { RuleAddProps, RuleEditProps, RuleTypeModel, - ConnectorAddFlyoutProps, - ConnectorEditFlyoutProps, AlertsTableProps, RuleStatusDropdownProps, RuleTagFilterProps, @@ -58,12 +57,16 @@ import type { RuleEventLogListProps, RulesListNotifyBadgeProps, AlertsTableConfigurationRegistry, + CreateConnectorFlyoutProps, + EditConnectorFlyoutProps, + ConnectorServices, } from './types'; import { TriggersActionsUiConfigType } from '../common/types'; import { registerAlertsTableConfiguration } from './application/sections/alerts_table/alerts_page/register_alerts_table_configuration'; import { PLUGIN_ID } from './common/constants'; import type { AlertsTableStateProps } from './application/sections/alerts_table/alerts_table_state'; import { getAlertsTableStateLazy } from './common/get_alerts_table_state'; +import { ActionAccordionFormProps } from './application/sections/action_connector_form/action_form'; export interface TriggersAndActionsUIPublicPluginSetup { actionTypeRegistry: TypeRegistry; @@ -75,12 +78,15 @@ export interface TriggersAndActionsUIPublicPluginStart { actionTypeRegistry: TypeRegistry; ruleTypeRegistry: TypeRegistry>; alertsTableConfigurationRegistry: TypeRegistry; + getActionForm: ( + props: Omit + ) => ReactElement; getAddConnectorFlyout: ( - props: Omit - ) => ReactElement; + props: Omit + ) => ReactElement; getEditConnectorFlyout: ( - props: Omit - ) => ReactElement; + props: Omit + ) => ReactElement; getAddAlertFlyout: ( props: Omit ) => ReactElement; @@ -131,6 +137,7 @@ export class Plugin private ruleTypeRegistry: TypeRegistry; private alertsTableConfigurationRegistry: TypeRegistry; private config: TriggersActionsUiConfigType; + private connectorServices?: ConnectorServices; readonly experimentalFeatures: ExperimentalFeatures; constructor(ctx: PluginInitializerContext) { @@ -145,6 +152,9 @@ export class Plugin const actionTypeRegistry = this.actionTypeRegistry; const ruleTypeRegistry = this.ruleTypeRegistry; const alertsTableConfigurationRegistry = this.alertsTableConfigurationRegistry; + this.connectorServices = { + validateEmailAddresses: plugins.actions.validateEmailAddresses, + }; ExperimentalFeaturesService.init({ experimentalFeatures: this.experimentalFeatures }); @@ -195,6 +205,7 @@ export class Plugin return renderApp({ ...coreStart, + actions: plugins.actions, data: pluginsStart.data, dataViews: pluginsStart.dataViews, charts: pluginsStart.charts, @@ -240,13 +251,25 @@ export class Plugin actionTypeRegistry: this.actionTypeRegistry, ruleTypeRegistry: this.ruleTypeRegistry, alertsTableConfigurationRegistry: this.alertsTableConfigurationRegistry, - getAddConnectorFlyout: (props: Omit) => { - return getAddConnectorFlyoutLazy({ ...props, actionTypeRegistry: this.actionTypeRegistry }); + getActionForm: (props: Omit) => { + return getActionFormLazy({ + ...props, + actionTypeRegistry: this.actionTypeRegistry, + connectorServices: this.connectorServices!, + }); + }, + getAddConnectorFlyout: (props: Omit) => { + return getAddConnectorFlyoutLazy({ + ...props, + actionTypeRegistry: this.actionTypeRegistry, + connectorServices: this.connectorServices!, + }); }, - getEditConnectorFlyout: (props: Omit) => { + getEditConnectorFlyout: (props: Omit) => { return getEditConnectorFlyoutLazy({ ...props, actionTypeRegistry: this.actionTypeRegistry, + connectorServices: this.connectorServices!, }); }, getAddAlertFlyout: (props: Omit) => { @@ -254,6 +277,7 @@ export class Plugin ...props, actionTypeRegistry: this.actionTypeRegistry, ruleTypeRegistry: this.ruleTypeRegistry, + connectorServices: this.connectorServices!, }); }, getEditAlertFlyout: ( @@ -263,6 +287,7 @@ export class Plugin ...props, actionTypeRegistry: this.actionTypeRegistry, ruleTypeRegistry: this.ruleTypeRegistry, + connectorServices: this.connectorServices!, }); }, getAlertsStateTable: (props: AlertsTableStateProps) => { @@ -290,7 +315,7 @@ export class Plugin return getRulesListNotifyBadgeLazy(props); }, getRulesList: () => { - return getRulesListLazy(); + return getRulesListLazy({ connectorServices: this.connectorServices! }); }, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index c83a4fd34dc9b..6923fe51d39d6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -5,9 +5,9 @@ * 2.0. */ +import type { ComponentType, ReactNode } from 'react'; import type { PublicMethodsOf } from '@kbn/utility-types'; import type { DocLinksStart } from '@kbn/core/public'; -import type { ComponentType } from 'react'; import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; @@ -44,12 +44,15 @@ import { RuleRegistrySearchRequestPagination } from '@kbn/rule-registry-plugin/c import { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common/search_strategy'; import { SortCombinations } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import React from 'react'; +import { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public'; import { TypeRegistry } from './application/type_registry'; import type { ComponentOpts as RuleStatusDropdownProps } from './application/sections/rules_list/components/rule_status_dropdown'; import type { RuleTagFilterProps } from './application/sections/rules_list/components/rule_tag_filter'; import type { RuleStatusFilterProps } from './application/sections/rules_list/components/rule_status_filter'; import type { RuleTagBadgeProps } from './application/sections/rules_list/components/rule_tag_badge'; import type { RuleEventLogListProps } from './application/sections/rule_details/components/rule_event_log_list'; +import type { CreateConnectorFlyoutProps } from './application/sections/action_connector_form/create_connector_flyout'; +import type { EditConnectorFlyoutProps } from './application/sections/action_connector_form/edit_connector_flyout'; import type { RulesListNotifyBadgeProps } from './application/sections/rules_list/components/rules_list_notify_badge'; // In Triggers and Actions we treat all `Alert`s as `SanitizedRule` @@ -87,6 +90,8 @@ export type { RuleStatusFilterProps, RuleTagBadgeProps, RuleEventLogListProps, + CreateConnectorFlyoutProps, + EditConnectorFlyoutProps, RulesListNotifyBadgeProps, }; export type { ActionType, AsApiContract }; @@ -108,23 +113,15 @@ export type AlertsTableConfigurationRegistryContract = PublicMethodsOf< TypeRegistry >; -export type ActionConnectorFieldsCallbacks = { - beforeActionConnectorSave?: () => Promise; - afterActionConnectorSave?: (connector: ActionConnector) => Promise; -} | null; -export type ActionConnectorFieldsSetCallbacks = React.Dispatch< - React.SetStateAction ->; +export interface ConnectorValidationError { + message: ReactNode; +} -export interface ActionConnectorFieldsProps { - action: TActionConnector; - editActionConfig: (property: string, value: unknown) => void; - editActionSecrets: (property: string, value: unknown) => void; - errors: IErrorObject; +export type ConnectorValidationFunc = () => Promise; +export interface ActionConnectorFieldsProps { readOnly: boolean; - consumer?: string; - setCallbacks: ActionConnectorFieldsSetCallbacks; isEdit: boolean; + registerPreSubmitValidator: (validator: ConnectorValidationFunc) => void; } export enum RuleFlyoutCloseReason { @@ -167,16 +164,11 @@ export interface ActionTypeModel - ) => Promise, Partial>>; validateParams: ( actionParams: ActionParams ) => Promise | unknown>>; actionConnectorFields: React.LazyExoticComponent< - ComponentType< - ActionConnectorFieldsProps> - > + ComponentType > | null; actionParamsFields: React.LazyExoticComponent>>; customConnectorSelectItem?: CustomConnectorSelectionItem; @@ -190,11 +182,6 @@ export interface ValidationResult { errors: Record; } -export interface ConnectorValidationResult { - config?: GenericValidationResult; - secrets?: GenericValidationResult; -} - export interface ActionConnectorProps { secrets: Secrets; id: string; @@ -319,28 +306,11 @@ export interface IErrorObject { [key: string]: string | string[] | IErrorObject; } -export interface ConnectorAddFlyoutProps { - onClose: () => void; - actionTypes?: ActionType[]; - onTestConnector?: (connector: ActionConnector) => void; - reloadConnectors?: () => Promise; - consumer?: string; - actionTypeRegistry: ActionTypeRegistryContract; -} -export enum EditConectorTabs { +export enum EditConnectorTabs { Configuration = 'configuration', Test = 'test', } -export interface ConnectorEditFlyoutProps { - initialConnector: ActionConnector; - onClose: () => void; - tab?: EditConectorTabs; - reloadConnectors?: () => Promise; - consumer?: string; - actionTypeRegistry: ActionTypeRegistryContract; -} - export interface RuleEditProps> { initialRule: Rule; ruleTypeRegistry: RuleTypeRegistryContract; @@ -466,3 +436,7 @@ export enum AlertsTableFlyoutState { } export type RuleStatus = 'enabled' | 'disabled' | 'snoozed'; + +export interface ConnectorServices { + validateEmailAddresses: ActionsPublicPluginSetup['validateEmailAddresses']; +} diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts index b51b889bf0437..5faae8c9bc2f9 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts @@ -138,7 +138,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('addNewActionConnectorButton-.slack'); const slackConnectorName = generateUniqueKey(); await testSubjects.setValue('nameInput', slackConnectorName); - await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); + await testSubjects.setValue('slackWebhookUrlInput', 'https://test.com'); await find.clickByCssSelector('[data-test-subj="saveActionButtonModal"]:not(disabled)'); const createdConnectorToastTitle = await pageObjects.common.closeToast(); expect(createdConnectorToastTitle).to.eql(`Created '${slackConnectorName}'`); @@ -192,7 +192,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('addNewActionConnectorButton-.slack'); const slackConnectorName = generateUniqueKey(); await testSubjects.setValue('nameInput', slackConnectorName); - await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); + await testSubjects.setValue('slackWebhookUrlInput', 'https://test.com'); await find.clickByCssSelector('[data-test-subj="saveActionButtonModal"]:not(disabled)'); const createdConnectorToastTitle = await pageObjects.common.closeToast(); expect(createdConnectorToastTitle).to.eql(`Created '${slackConnectorName}'`); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts index b2e27b30f0079..78287b57a5843 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts @@ -50,9 +50,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.setValue('nameInput', connectorName); - await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); + await testSubjects.setValue('slackWebhookUrlInput', 'https://test.com'); - await find.clickByCssSelector('[data-test-subj="saveNewActionButton"]:not(disabled)'); + await find.clickByCssSelector( + '[data-test-subj="create-connector-flyout-save-btn"]:not(disabled)' + ); const toastTitle = await pageObjects.common.closeToast(); expect(toastTitle).to.eql(`Created '${connectorName}'`); @@ -82,10 +84,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.setValue('nameInput', updatedConnectorName); - await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); + await testSubjects.setValue('slackWebhookUrlInput', 'https://test.com'); await find.clickByCssSelector( - '[data-test-subj="saveAndCloseEditedActionButton"]:not(disabled)' + '[data-test-subj="edit-connector-flyout-save-close-btn"]:not(disabled)' ); const toastTitle = await pageObjects.common.closeToast(); @@ -262,7 +264,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await find.clickByCssSelector('[data-test-subj="connectorsTableCell-name"] button'); expect(await testSubjects.exists('preconfiguredBadge')).to.be(true); - expect(await testSubjects.exists('saveAndCloseEditedActionButton')).to.be(false); + expect(await testSubjects.exists('edit-connector-flyout-save-close-btn')).to.be(false); }); }); @@ -273,9 +275,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.setValue('nameInput', connectorName); - await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); + await testSubjects.setValue('slackWebhookUrlInput', 'https://test.com'); - await find.clickByCssSelector('[data-test-subj="saveNewActionButton"]:not(disabled)'); + await find.clickByCssSelector( + '[data-test-subj="create-connector-flyout-save-btn"]:not(disabled)' + ); await pageObjects.common.closeToast(); } @@ -298,7 +302,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ).to.be(true); }); - await find.clickByCssSelector('[data-test-subj="saveNewActionButton"]:not(disabled)'); + await find.clickByCssSelector( + '[data-test-subj="create-connector-flyout-save-btn"]:not(disabled)' + ); await pageObjects.common.closeToast(); } }; From 6decdc3b8216bef54d9c8b7855423d26bb58c071 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Fri, 17 Jun 2022 11:14:58 +0200 Subject: [PATCH 13/30] ES Client: use `ClusterConnectionPool` (#134628) --- .../client/configure_client.test.ts | 16 ++++++++++++++++ .../elasticsearch/client/configure_client.ts | 4 +++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/core/server/elasticsearch/client/configure_client.test.ts b/src/core/server/elasticsearch/client/configure_client.test.ts index b17b308294a20..29750febd72c3 100644 --- a/src/core/server/elasticsearch/client/configure_client.test.ts +++ b/src/core/server/elasticsearch/client/configure_client.test.ts @@ -17,6 +17,7 @@ import { ClientMock, } from './configure_client.test.mocks'; import { loggingSystemMock } from '../../logging/logging_system.mock'; +import { ClusterConnectionPool } from '@elastic/elasticsearch'; import type { ElasticsearchClientConfig } from './client_config'; import { configureClient } from './configure_client'; import { instrumentEsQueryAndDeprecationLogger } from './log_query_and_deprecation'; @@ -112,6 +113,21 @@ describe('configureClient', () => { expect(client).toBe(ClientMock.mock.results[0].value); }); + it('constructs a client using `ClusterConnectionPool` for `ConnectionPool` ', () => { + const mockedTransport = { mockTransport: true }; + createTransportMock.mockReturnValue(mockedTransport); + + const client = configureClient(config, { logger, type: 'test', scoped: false }); + + expect(ClientMock).toHaveBeenCalledTimes(1); + expect(ClientMock).toHaveBeenCalledWith( + expect.objectContaining({ + ConnectionPool: ClusterConnectionPool, + }) + ); + expect(client).toBe(ClientMock.mock.results[0].value); + }); + it('calls instrumentEsQueryAndDeprecationLogger', () => { const client = configureClient(config, { logger, type: 'test', scoped: false }); diff --git a/src/core/server/elasticsearch/client/configure_client.ts b/src/core/server/elasticsearch/client/configure_client.ts index 020f3d66a42a5..248820ceb1f56 100644 --- a/src/core/server/elasticsearch/client/configure_client.ts +++ b/src/core/server/elasticsearch/client/configure_client.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { Client, HttpConnection } from '@elastic/elasticsearch'; +import { Client, HttpConnection, ClusterConnectionPool } from '@elastic/elasticsearch'; import type { Logger } from '@kbn/logging'; import { parseClientOptions, ElasticsearchClientConfig } from './client_config'; import { instrumentEsQueryAndDeprecationLogger } from './log_query_and_deprecation'; @@ -35,6 +35,8 @@ export const configureClient = ( ...clientOptions, Transport: KibanaTransport, Connection: HttpConnection, + // using ClusterConnectionPool until https://github.com/elastic/elasticsearch-js/issues/1714 is addressed + ConnectionPool: ClusterConnectionPool, }); instrumentEsQueryAndDeprecationLogger({ logger, client, type }); From 2dc48ff56ca1be56b5f7fa8e437ac143d0aabf0c Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Fri, 17 Jun 2022 11:20:22 +0200 Subject: [PATCH 14/30] [XY] Add xExtent validation within expression (#134546) * :sparkles: Add expression validation for xExtent * :white_check_mark: Add more tests --- .../expression_xy/common/__mocks__/index.ts | 4 - .../__snapshots__/xy_vis.test.ts.snap | 6 + .../common/expression_functions/validate.ts | 22 +++ .../expression_functions/xy_vis.test.ts | 125 ++++++++++++++++++ .../common/expression_functions/xy_vis_fn.ts | 2 + .../common/types/expression_functions.ts | 6 +- 6 files changed, 158 insertions(+), 7 deletions(-) diff --git a/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts b/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts index d5eaf3b086e65..e9810d2764025 100644 --- a/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts +++ b/src/plugins/chart_expressions/expression_xy/common/__mocks__/index.ts @@ -115,10 +115,6 @@ export const createArgsWithLayers = ( yLeft: false, yRight: false, }, - xExtent: { - mode: 'dataBounds', - type: 'axisExtentConfig', - }, yLeftExtent: { mode: 'full', type: 'axisExtentConfig', diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/__snapshots__/xy_vis.test.ts.snap b/src/plugins/chart_expressions/expression_xy/common/expression_functions/__snapshots__/xy_vis.test.ts.snap index 80e1d140be28d..6ffe660fa99af 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/__snapshots__/xy_vis.test.ts.snap +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/__snapshots__/xy_vis.test.ts.snap @@ -17,3 +17,9 @@ exports[`xyVis it should throw error if splitColumnAccessor is pointing to the a exports[`xyVis it should throw error if splitRowAccessor is pointing to the absent column 1`] = `"Provided column name or index is invalid: absent-accessor"`; exports[`xyVis throws the error if showLines is provided to the not line/area chart 1`] = `"Lines visibility can be controlled only at line charts"`; + +exports[`xyVis throws the error if the x axis extent is enabled for a date histogram 1`] = `"X axis extent is only supported for numeric histograms."`; + +exports[`xyVis throws the error if the x axis extent is enabled with the full mode 1`] = `"For x axis extent, the full mode is not supported."`; + +exports[`xyVis throws the error if the x axis extent is enabled without a histogram defined 1`] = `"X axis extent is only supported for numeric histograms."`; diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts index 2eec4ecaab950..6cf8f4c68d1db 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/validate.ts @@ -87,6 +87,14 @@ export const errors = { i18n.translate('expressionXY.reusable.function.xyVis.errors.dataBoundsForNotLineChartError', { defaultMessage: 'Only line charts can be fit to the data bounds', }), + extentFullModeIsInvalidError: () => + i18n.translate('expressionXY.reusable.function.xyVis.errors.extentFullModeIsInvalid', { + defaultMessage: 'For x axis extent, the full mode is not supported.', + }), + extentModeNotSupportedError: () => + i18n.translate('expressionXY.reusable.function.xyVis.errors.extentModeNotSupportedError', { + defaultMessage: 'X axis extent is only supported for numeric histograms.', + }), timeMarkerForNotTimeChartsError: () => i18n.translate('expressionXY.reusable.function.xyVis.errors.timeMarkerForNotTimeChartsError', { defaultMessage: 'Only time charts can have current time marker', @@ -136,6 +144,20 @@ export const validateExtentForDataBounds = ( } }; +export const validateXExtent = ( + extent: AxisExtentConfigResult | undefined, + dataLayers: Array +) => { + if (extent) { + if (extent.mode === AxisExtentModes.FULL) { + throw new Error(errors.extentFullModeIsInvalidError()); + } + if (isTimeChart(dataLayers) || dataLayers.every(({ isHistogram }) => !isHistogram)) { + throw new Error(errors.extentModeNotSupportedError()); + } + } +}; + export const validateExtent = ( extent: AxisExtentConfigResult, hasBarOrArea: boolean, diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts index ab8d243b72057..bbd906b724844 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis.test.ts @@ -215,4 +215,129 @@ describe('xyVis', () => { ) ).rejects.toThrowErrorMatchingSnapshot(); }); + + test('throws the error if the x axis extent is enabled for a date histogram', async () => { + const { + data, + args: { layers, ...rest }, + } = sampleArgs(); + const { layerId, layerType, table, type, ...restLayerArgs } = sampleLayer; + + expect( + xyVisFunction.fn( + data, + { + ...rest, + ...restLayerArgs, + referenceLines: [], + annotationLayers: [], + isHistogram: true, + xScaleType: 'time', + xExtent: { type: 'axisExtentConfig', mode: 'dataBounds' }, + }, + createMockExecutionContext() + ) + ).rejects.toThrowErrorMatchingSnapshot(); + }); + + test('throws the error if the x axis extent is enabled with the full mode', async () => { + const { + data, + args: { layers, ...rest }, + } = sampleArgs(); + const { layerId, layerType, table, type, ...restLayerArgs } = sampleLayer; + + expect( + xyVisFunction.fn( + data, + { + ...rest, + ...restLayerArgs, + referenceLines: [], + annotationLayers: [], + xExtent: { + type: 'axisExtentConfig', + mode: 'full', + lowerBound: undefined, + upperBound: undefined, + }, + }, + createMockExecutionContext() + ) + ).rejects.toThrowErrorMatchingSnapshot(); + }); + + test('throws the error if the x axis extent is enabled without a histogram defined', async () => { + const { + data, + args: { layers, ...rest }, + } = sampleArgs(); + const { layerId, layerType, table, type, ...restLayerArgs } = sampleLayer; + + expect( + xyVisFunction.fn( + data, + { + ...rest, + ...restLayerArgs, + referenceLines: [], + annotationLayers: [], + xExtent: { + type: 'axisExtentConfig', + mode: 'dataBounds', + }, + }, + createMockExecutionContext() + ) + ).rejects.toThrowErrorMatchingSnapshot(); + }); + + test('it renders with custom xExtent for a numeric histogram', async () => { + const { data, args } = sampleArgs(); + const { layers, ...rest } = args; + const { layerId, layerType, table, type, ...restLayerArgs } = sampleLayer; + const result = await xyVisFunction.fn( + data, + { + ...rest, + ...restLayerArgs, + referenceLines: [], + annotationLayers: [], + isHistogram: true, + xExtent: { + type: 'axisExtentConfig', + mode: 'custom', + lowerBound: 0, + upperBound: 10, + }, + }, + createMockExecutionContext() + ); + + expect(result).toEqual({ + type: 'render', + as: XY_VIS, + value: { + args: { + ...rest, + xExtent: { + type: 'axisExtentConfig', + mode: 'custom', + lowerBound: 0, + upperBound: 10, + }, + layers: [ + { + layerType, + table: data, + layerId: 'dataLayers-0', + type, + ...restLayerArgs, + isHistogram: true, + }, + ], + }, + }, + }); + }); }); diff --git a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts index 8e352a6f34b4b..c24f28f5ca173 100644 --- a/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts +++ b/src/plugins/chart_expressions/expression_xy/common/expression_functions/xy_vis_fn.ts @@ -33,6 +33,7 @@ import { validateLineWidthForChartType, validatePointsRadiusForChartType, validateLinesVisibilityForChartType, + validateXExtent, } from './validate'; const createDataLayer = (args: XYArgs, table: Datatable): DataLayerConfigResult => { @@ -120,6 +121,7 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => { const hasBar = hasBarLayer(dataLayers); const hasArea = hasAreaLayer(dataLayers); + validateXExtent(args.xExtent, dataLayers); validateExtent(args.yLeftExtent, hasBar || hasArea, dataLayers); validateExtent(args.yRightExtent, hasBar || hasArea, dataLayers); validateFillOpacity(args.fillOpacity, hasArea); diff --git a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts index e87704a919838..50e0439f757c0 100644 --- a/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_xy/common/types/expression_functions.ts @@ -196,7 +196,7 @@ export interface XYArgs extends DataLayerArgs { xTitle: string; yTitle: string; yRightTitle: string; - xExtent: AxisExtentConfigResult; + xExtent?: AxisExtentConfigResult; yLeftExtent: AxisExtentConfigResult; yRightExtent: AxisExtentConfigResult; yLeftScale: YScaleType; @@ -231,7 +231,7 @@ export interface LayeredXYArgs { xTitle: string; yTitle: string; yRightTitle: string; - xExtent: AxisExtentConfigResult; + xExtent?: AxisExtentConfigResult; yLeftExtent: AxisExtentConfigResult; yRightExtent: AxisExtentConfigResult; yLeftScale: YScaleType; @@ -263,7 +263,7 @@ export interface XYProps { xTitle: string; yTitle: string; yRightTitle: string; - xExtent: AxisExtentConfigResult; + xExtent?: AxisExtentConfigResult; yLeftExtent: AxisExtentConfigResult; yRightExtent: AxisExtentConfigResult; yLeftScale: YScaleType; From ea6b79dfbc9a98a2b4836b5a2d45cbca0d4314ca Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 17 Jun 2022 11:26:32 +0200 Subject: [PATCH 15/30] fix flaky test (#134634) --- test/functional/apps/visualize/group6/_tag_cloud.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/functional/apps/visualize/group6/_tag_cloud.ts b/test/functional/apps/visualize/group6/_tag_cloud.ts index fed99547d6eaf..da8bb3f02c751 100644 --- a/test/functional/apps/visualize/group6/_tag_cloud.ts +++ b/test/functional/apps/visualize/group6/_tag_cloud.ts @@ -29,8 +29,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'tagCloud', ]); - // Failing: See https://github.com/elastic/kibana/issues/134515 - describe.skip('tag cloud chart', function () { + describe('tag cloud chart', function () { const vizName1 = 'Visualization tagCloud'; const termsField = 'machine.ram'; @@ -146,7 +145,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickIndexPatternLogstash(); - await PageObjects.settings.filterField(termsField); await PageObjects.settings.openControlsByName(termsField); await ( await ( @@ -169,7 +167,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickIndexPatternLogstash(); - await PageObjects.settings.filterField(termsField); await PageObjects.settings.openControlsByName(termsField); await PageObjects.settings.setFieldFormat(''); await PageObjects.settings.controlChangeSave(); From afd25eb14f7760ddca83a5e2e99c4d99345c20a9 Mon Sep 17 00:00:00 2001 From: Gerard Soldevila Date: Fri, 17 Jun 2022 11:41:55 +0200 Subject: [PATCH 16/30] Telemetry plugin graceful setup() + stop() (#134236) * Telemetry plugin graceful setup() + stop() * Propagate abort signal to ES call * Import type instead * Document non-breaking updates to one of the core APIs * Await for current getOptInStatus call * Undo md changes * Undo md changes --- src/plugins/telemetry/server/plugin.ts | 11 ++++++++--- .../get_telemetry_saved_object.ts | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index 881407fb8e288..6a02750f10bbe 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -17,7 +17,6 @@ import { distinctUntilChanged, filter, } from 'rxjs'; - import { ElasticV3ServerShipper } from '@kbn/analytics-shippers-elastic-v3-server'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; @@ -90,6 +89,7 @@ export class TelemetryPlugin implements Plugin(undefined); private readonly isDev: boolean; private readonly fetcherTask: FetcherTask; + private optInPromise?: Promise; /** * @private Used to mark the completion of the old UI Settings migration */ @@ -111,7 +111,10 @@ export class TelemetryPlugin implements Plugin this.getOptInStatus()), + exhaustMap(() => { + this.optInPromise = this.getOptInStatus(); + return this.optInPromise; + }), distinctUntilChanged() ) .subscribe((isOptedIn) => this.isOptedIn$.next(isOptedIn)); @@ -207,9 +210,11 @@ export class TelemetryPlugin implements Plugin { diff --git a/src/plugins/telemetry/server/telemetry_repository/get_telemetry_saved_object.ts b/src/plugins/telemetry/server/telemetry_repository/get_telemetry_saved_object.ts index 56d13f3c6ad22..7d22758b5ccae 100644 --- a/src/plugins/telemetry/server/telemetry_repository/get_telemetry_saved_object.ts +++ b/src/plugins/telemetry/server/telemetry_repository/get_telemetry_saved_object.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { SavedObjectsErrorHelpers, SavedObjectsClientContract } from '@kbn/core/server'; -import { TelemetrySavedObject } from '.'; +import { SavedObjectsErrorHelpers, type SavedObjectsClientContract } from '@kbn/core/server'; +import type { TelemetrySavedObject } from '.'; type GetTelemetrySavedObject = ( repository: SavedObjectsClientContract From 5f7493180e5b36e3a11b3b3420baafa2301db4e3 Mon Sep 17 00:00:00 2001 From: Ioana Tagirta Date: Fri, 17 Jun 2022 13:17:00 +0200 Subject: [PATCH 17/30] Fix deployment settings URL (#134567) --- .../applications/shared/error_state/error_state_prompt.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx index 4e2ee38b8a464..25e00405f9a38 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx @@ -16,7 +16,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { HttpLogic } from '../http'; import { KibanaLogic } from '../kibana'; -import { EuiButtonTo, EuiLinkTo } from '../react_router_helpers'; +import { EuiButtonTo } from '../react_router_helpers'; import './error_state_prompt.scss'; @@ -95,14 +95,14 @@ const cloudError = (cloud: Partial) => { defaultMessage="Does your Cloud deployment have Enterprise Search nodes running? {deploymentSettingsLink}" values={{ deploymentSettingsLink: ( - + {i18n.translate( 'xpack.enterpriseSearch.errorConnectingState.cloudErrorMessageLinkText', { defaultMessage: 'Check your deployment settings', } )} - + ), }} /> From 5271982c81d48ca78dc277c63650f702d5f3b95d Mon Sep 17 00:00:00 2001 From: Shahzad Date: Fri, 17 Jun 2022 14:25:37 +0200 Subject: [PATCH 18/30] [Synthetics] Add learn more links (#134652) --- .../monitor_list/management_settings.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/management_settings.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/management_settings.tsx index 8b00f0d2cb326..1cbe6ae20ebe1 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/management_settings.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/management_settings.tsx @@ -13,6 +13,9 @@ import { ApiKeyBtn } from './api_key_btn'; import { fetchServiceAPIKey } from '../../../state/api'; import { useEnablement } from '../hooks/use_enablement'; +const syntheticsTestRunDocsLink = + 'https://www.elastic.co/guide/en/observability/current/synthetic-run-tests.html'; + export const ManagementSettings = () => { const { enablement: { canManageApiKeys }, @@ -63,7 +66,7 @@ export const ManagementSettings = () => { {GET_API_KEY_GENERATE} {GET_API_KEY_LABEL_DESCRIPTION}{' '} - + {LEARN_MORE_LABEL} @@ -74,7 +77,7 @@ export const ManagementSettings = () => { {GET_API_KEY_GENERATE} {GET_API_KEY_REDUCED_PERMISSIONS_LABEL}{' '} - + {LEARN_MORE_LABEL} From 1e5b0d4c8b1defce986363139a3de4c85eea4fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20FOUCRET?= Date: Fri, 17 Jun 2022 15:17:51 +0200 Subject: [PATCH 19/30] Enable adding BYOEI engines to Meta Engines via UI (#134428) --- .../meta_engine_creation_logic.test.ts | 5 +++- .../meta_engine_creation_logic.ts | 6 ++--- .../source_engines_logic.test.ts | 25 ++++--------------- .../source_engines/source_engines_logic.ts | 5 ++-- 4 files changed, 13 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.test.ts index 1080f7bd0dd3a..2c5a92b06d64b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.test.ts @@ -138,7 +138,10 @@ describe('MetaEngineCreationLogic', () => { ); MetaEngineCreationLogic.actions.fetchIndexedEngineNames(); await nextTick(); - expect(MetaEngineCreationLogic.actions.setIndexedEngineNames).toHaveBeenCalledWith(['foo']); + expect(MetaEngineCreationLogic.actions.setIndexedEngineNames).toHaveBeenCalledWith([ + 'foo', + 'elasticsearch-engine', + ]); }); it('if there are remaining pages it should call fetchIndexedEngineNames recursively with an incremented page', async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.ts index 82804db757ce5..5296676a38b36 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.ts @@ -16,7 +16,7 @@ import { HttpLogic } from '../../../shared/http'; import { KibanaLogic } from '../../../shared/kibana'; import { ENGINE_PATH } from '../../routes'; import { formatApiName } from '../../utils/format_api_name'; -import { EngineDetails, EngineTypes } from '../engine/types'; +import { EngineDetails } from '../engine/types'; import { META_ENGINE_CREATION_SUCCESS_MESSAGE } from './constants'; @@ -100,9 +100,7 @@ export const MetaEngineCreationLogic = kea< } if (response) { - const engineNames = response.results - .filter(({ type }) => type !== EngineTypes.elasticsearch) - .map((result) => result.name); + const engineNames = response.results.map((result) => result.name); actions.setIndexedEngineNames([...values.indexedEngineNames, ...engineNames]); if (page < response.meta.page.total_pages) { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.test.ts index 7002eb25f4668..b5bf839a0ae8f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.test.ts @@ -98,30 +98,15 @@ describe('SourceEnginesLogic', () => { SourceEnginesLogic.actions.setIndexedEngines([ { name: 'source-engine-1' }, - { name: 'source-engine-2' }, - ] as EngineDetails[]); - - expect(SourceEnginesLogic.values).toEqual({ - ...DEFAULT_VALUES, - indexedEngines: [{ name: 'source-engine-1' }, { name: 'source-engine-2' }], - // Selectors - indexedEngineNames: ['source-engine-1', 'source-engine-2'], - selectableEngineNames: ['source-engine-1', 'source-engine-2'], - }); - }); - - it('sets indexedEngines filters out elasticsearch type engines', () => { - mount(); - - SourceEnginesLogic.actions.setIndexedEngines([ - { name: 'source-engine-1' }, - { name: 'source-engine-2' }, - { name: 'source-engine-elasticsearch', type: 'elasticsearch' }, + { name: 'source-engine-2', type: 'elasticsearch' }, ] as EngineDetails[]); expect(SourceEnginesLogic.values).toEqual({ ...DEFAULT_VALUES, - indexedEngines: [{ name: 'source-engine-1' }, { name: 'source-engine-2' }], + indexedEngines: [ + { name: 'source-engine-1' }, + { name: 'source-engine-2', type: 'elasticsearch' }, + ], // Selectors indexedEngineNames: ['source-engine-1', 'source-engine-2'], selectableEngineNames: ['source-engine-1', 'source-engine-2'], diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.ts index 1f12af3f20b44..bae87f9370a34 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/source_engines/source_engines_logic.ts @@ -11,7 +11,7 @@ import { flashAPIErrors, flashSuccessToast } from '../../../shared/flash_message import { HttpLogic } from '../../../shared/http'; import { recursivelyFetchEngines } from '../../utils/recursively_fetch_engines'; import { EngineLogic } from '../engine'; -import { EngineDetails, EngineTypes } from '../engine/types'; +import { EngineDetails } from '../engine/types'; import { ADD_SOURCE_ENGINES_SUCCESS_MESSAGE, REMOVE_SOURCE_ENGINE_SUCCESS_MESSAGE } from './i18n'; @@ -88,8 +88,7 @@ export const SourceEnginesLogic = kea< indexedEngines: [ [], { - setIndexedEngines: (_, { indexedEngines }) => - indexedEngines.filter(({ type }) => type !== EngineTypes.elasticsearch), + setIndexedEngines: (_, { indexedEngines }) => indexedEngines, }, ], selectedEngineNamesToAdd: [ From f0f2a0529736fbcc14af186fa7ff6111595fb081 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Fri, 17 Jun 2022 10:41:20 -0300 Subject: [PATCH 20/30] [Discover] [Alerting] Make 'Test query' button pretty (#134605) * [Discover] [Alerting] Made 'Test query' button in 'Create rule' flyout pretty * [Discover] [Alerting] Fixed broken 'Test query' button tests --- .../es_query/expression/es_query_expression.test.tsx | 8 ++++---- .../alert_types/es_query/expression/test_query_row.tsx | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.test.tsx index 828a4c4e952b8..7e38fcf81c678 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.test.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.test.tsx @@ -179,7 +179,7 @@ describe('EsQueryAlertTypeExpression', () => { expect(wrapper.find('[data-test-subj="thresholdExpression"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="forLastExpression"]').exists()).toBeTruthy(); - const testQueryButton = wrapper.find('EuiButtonEmpty[data-test-subj="testQuery"]'); + const testQueryButton = wrapper.find('EuiButton[data-test-subj="testQuery"]'); expect(testQueryButton.exists()).toBeTruthy(); expect(testQueryButton.prop('disabled')).toBe(false); }); @@ -189,7 +189,7 @@ describe('EsQueryAlertTypeExpression', () => { ...defaultEsQueryExpressionParams, timeField: null, } as unknown as EsQueryAlertParams); - const testQueryButton = wrapper.find('EuiButtonEmpty[data-test-subj="testQuery"]'); + const testQueryButton = wrapper.find('EuiButton[data-test-subj="testQuery"]'); expect(testQueryButton.exists()).toBeTruthy(); expect(testQueryButton.prop('disabled')).toBe(true); }); @@ -204,7 +204,7 @@ describe('EsQueryAlertTypeExpression', () => { }); dataMock.search.search.mockImplementation(() => searchResponseMock$); const wrapper = await setup(defaultEsQueryExpressionParams); - const testQueryButton = wrapper.find('EuiButtonEmpty[data-test-subj="testQuery"]'); + const testQueryButton = wrapper.find('EuiButton[data-test-subj="testQuery"]'); testQueryButton.simulate('click'); expect(dataMock.search.search).toHaveBeenCalled(); @@ -225,7 +225,7 @@ describe('EsQueryAlertTypeExpression', () => { throw new Error('What is this query'); }); const wrapper = await setup(defaultEsQueryExpressionParams); - const testQueryButton = wrapper.find('EuiButtonEmpty[data-test-subj="testQuery"]'); + const testQueryButton = wrapper.find('EuiButton[data-test-subj="testQuery"]'); testQueryButton.simulate('click'); expect(dataMock.search.search).toHaveBeenCalled(); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/test_query_row.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/test_query_row.tsx index 6d18f2f0537ce..f78bae7cfcaf6 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/test_query_row.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/test_query_row.tsx @@ -5,7 +5,7 @@ * 2.0. */ import React from 'react'; -import { EuiButtonEmpty, EuiFormRow, EuiText } from '@elastic/eui'; +import { EuiButton, EuiFormRow, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { useTestQuery } from './use_test_query'; @@ -21,21 +21,21 @@ export function TestQueryRow({ return ( <> - - +
{testQueryLoading && ( From fb584d6f35ffdcfb391aed906677c1d3f97ead9c Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Fri, 17 Jun 2022 10:04:45 -0400 Subject: [PATCH 21/30] [Synthetics] reenable e2e tests (#134238) * synthetics - reenable e2e tests * renable alert tests * adjust alert test * Update x-pack/plugins/synthetics/e2e/journeys/index.ts * check enabled state * uncomment tests * add remote console * check that we're on the add monitor page * add error log * log html * add error * show experimental locations * update tests * comment out tests * Update x-pack/plugins/synthetics/e2e/journeys/monitor_management.journey.ts * Update x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/loader/loader.tsx * adjust add_monitor page * Update x-pack/plugins/synthetics/e2e/page_objects/monitor_management.tsx * enable more tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/synthetics/e2e/config.ts | 1 + .../status_alert_flyouts_in_alerting_app.ts | 4 ++-- .../alerts/tls_alert_flyouts_in_alerting_app.ts | 2 +- .../e2e/journeys/data_view_permissions.ts | 2 +- x-pack/plugins/synthetics/e2e/journeys/index.ts | 17 ++++++++--------- .../monitor_management_enablement.journey.ts | 3 +-- .../read_only_user/monitor_management.ts | 2 +- 7 files changed, 15 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/synthetics/e2e/config.ts b/x-pack/plugins/synthetics/e2e/config.ts index 42e97eb21e90a..8f55b497e61ca 100644 --- a/x-pack/plugins/synthetics/e2e/config.ts +++ b/x-pack/plugins/synthetics/e2e/config.ts @@ -63,6 +63,7 @@ async function config({ readConfigFile }: FtrConfigProviderContext) { : 'localKibanaIntegrationTestsUser' }`, `--xpack.uptime.service.password=${servicePassword}`, + `--xpack.uptime.service.showExperimentalLocations=${true}`, ], }, }; diff --git a/x-pack/plugins/synthetics/e2e/journeys/alerts/status_alert_flyouts_in_alerting_app.ts b/x-pack/plugins/synthetics/e2e/journeys/alerts/status_alert_flyouts_in_alerting_app.ts index 321c68a8c3350..e20482b33beed 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/alerts/status_alert_flyouts_in_alerting_app.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/alerts/status_alert_flyouts_in_alerting_app.ts @@ -27,7 +27,7 @@ journey('StatusFlyoutInAlertingApp', async ({ page, params }) => { step('Open monitor status flyout', async () => { await page.click(byTestId('createFirstRuleButton')); await waitForLoadingToFinish({ page }); - await page.click(byTestId('"xpack.synthetics.alerts.monitorStatus-SelectOption"')); + await page.click(byTestId('"xpack.uptime.alerts.monitorStatus-SelectOption"')); await waitForLoadingToFinish({ page }); await assertText({ page, text: 'This alert will apply to approximately 0 monitors.' }); }); @@ -61,7 +61,7 @@ journey('StatusFlyoutInAlertingApp', async ({ page, params }) => { step('Open tls alert flyout', async () => { await page.click(byTestId('createFirstRuleButton')); await waitForLoadingToFinish({ page }); - await page.click(byTestId('"xpack.synthetics.alerts.tlsCertificate-SelectOption"')); + await page.click(byTestId('"xpack.uptime.alerts.tlsCertificate-SelectOption"')); await waitForLoadingToFinish({ page }); await assertText({ page, text: 'has a certificate expiring within' }); }); diff --git a/x-pack/plugins/synthetics/e2e/journeys/alerts/tls_alert_flyouts_in_alerting_app.ts b/x-pack/plugins/synthetics/e2e/journeys/alerts/tls_alert_flyouts_in_alerting_app.ts index 5cf1d1a6c9ff5..873d10c0a21bb 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/alerts/tls_alert_flyouts_in_alerting_app.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/alerts/tls_alert_flyouts_in_alerting_app.ts @@ -27,7 +27,7 @@ journey('TlsFlyoutInAlertingApp', async ({ page, params }) => { step('Open tls alert flyout', async () => { await page.click(byTestId('createFirstRuleButton')); await waitForLoadingToFinish({ page }); - await page.click(byTestId('"xpack.synthetics.alerts.tlsCertificate-SelectOption"')); + await page.click(byTestId('"xpack.uptime.alerts.tlsCertificate-SelectOption"')); await waitForLoadingToFinish({ page }); await assertText({ page, text: 'has a certificate expiring within' }); }); diff --git a/x-pack/plugins/synthetics/e2e/journeys/data_view_permissions.ts b/x-pack/plugins/synthetics/e2e/journeys/data_view_permissions.ts index 303cd5d40db43..10e027f249104 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/data_view_permissions.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/data_view_permissions.ts @@ -38,7 +38,7 @@ journey('DataViewPermissions', async ({ page, params }) => { await page.goto(`${baseUrl}?${queryParams}`, { waitUntil: 'networkidle', }); - await login.loginToKibana('obs_read_user', 'changeme'); + await login.loginToKibana('viewer_user', 'changeme'); }); step('Click explore data button', async () => { diff --git a/x-pack/plugins/synthetics/e2e/journeys/index.ts b/x-pack/plugins/synthetics/e2e/journeys/index.ts index 8ebfb96262d98..1d9fe3e8f33d3 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/index.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/index.ts @@ -4,16 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +export * from './alerts'; // export * from './synthetics'; // TODO: Enable these in a follow up PR -// export * from './data_view_permissions'; // TODO: Enable these in a follow up PR +export * from './data_view_permissions'; export * from './uptime.journey'; export * from './step_duration.journey'; -// export * from './alerts'; // TODO: Enable these in a follow up PR -// export * from './read_only_user'; // TODO: Enable these in a follow up PR -// export * from './monitor_details.journey'; // TODO: Enable these in a follow up PR -// export * from './monitor_name.journey'; // TODO: Enable these in a follow up PR -// export * from './monitor_management.journey'; // TODO: Enable these in a follow up PR -// export * from './monitor_management_enablement.journey'; // TODO: Enable these in a follow up PR -// export * from './monitor_details'; // TODO: Enable these in a follow up PR +export * from './read_only_user'; +export * from './monitor_details.journey'; +export * from './monitor_name.journey'; +export * from './monitor_management.journey'; +export * from './monitor_management_enablement.journey'; +export * from './monitor_details'; export * from './locations'; diff --git a/x-pack/plugins/synthetics/e2e/journeys/monitor_management_enablement.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/monitor_management_enablement.journey.ts index 14232336799d4..b7308ece8af21 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/monitor_management_enablement.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/monitor_management_enablement.journey.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { journey, step, expect, before, after, Page } from '@elastic/synthetics'; import { monitorManagementPageProvider } from '../page_objects/monitor_management'; @@ -58,7 +57,7 @@ journey( }); step('login to Kibana', async () => { - await uptime.loginToKibana('obs_admin_user', 'changeme'); + await uptime.loginToKibana('editor_user', 'changeme'); const invalid = await page.locator( `text=Username or password is incorrect. Please try again.` ); diff --git a/x-pack/plugins/synthetics/e2e/journeys/read_only_user/monitor_management.ts b/x-pack/plugins/synthetics/e2e/journeys/read_only_user/monitor_management.ts index da2958c3775dd..33698961951da 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/read_only_user/monitor_management.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/read_only_user/monitor_management.ts @@ -23,7 +23,7 @@ journey( }); step('login to Kibana', async () => { - await uptime.loginToKibana('obs_read_user', 'changeme'); + await uptime.loginToKibana('viewer_user', 'changeme'); }); step('Adding monitor is disabled', async () => { From 581d6f8592a2c44d524d5ed3afd4b0be254068eb Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Fri, 17 Jun 2022 16:58:38 +0200 Subject: [PATCH 22/30] [Discover] Introduce hooks folder for React hooks (#134465) --- src/plugins/discover/README.md | 1 + .../public/application/context/context_app.tsx | 8 ++++---- .../public/application/context/context_app_content.tsx | 4 ++-- .../public/application/context/context_app_route.tsx | 6 +++--- .../{utils => hooks}/use_context_app_fetch.test.tsx | 0 .../context/{utils => hooks}/use_context_app_fetch.tsx | 4 ++-- .../context/{utils => hooks}/use_context_app_state.ts | 0 .../discover/public/application/doc/components/doc.tsx | 4 ++-- .../public/application/doc/single_doc_route.tsx | 6 +++--- .../main/components/chart/discover_chart.test.tsx | 2 +- .../main/components/chart/discover_chart.tsx | 4 ++-- .../main/components/chart/histogram.test.tsx | 4 ++-- .../application/main/components/chart/histogram.tsx | 6 +++--- .../document_explorer_callout.tsx | 2 +- .../document_explorer_update_callout.tsx | 2 +- .../components/field_stats_table/field_stats_table.tsx | 4 ++-- .../main/components/hits_counter/hits_counter.test.tsx | 2 +- .../main/components/hits_counter/hits_counter.tsx | 4 ++-- .../main/components/layout/discover_documents.test.tsx | 2 +- .../main/components/layout/discover_documents.tsx | 10 +++++----- .../main/components/layout/discover_layout.test.tsx | 2 +- .../main/components/layout/discover_layout.tsx | 8 ++++---- .../public/application/main/components/layout/types.ts | 2 +- .../main/components/sidebar/discover_field_search.tsx | 2 +- .../main/components/sidebar/discover_sidebar.test.tsx | 2 +- .../main/components/sidebar/discover_sidebar.tsx | 2 +- .../sidebar/discover_sidebar_responsive.test.tsx | 2 +- .../components/sidebar/discover_sidebar_responsive.tsx | 4 ++-- .../main/components/top_nav/discover_topnav.tsx | 2 +- .../main/components/top_nav/open_alerts_popover.tsx | 2 +- .../main/components/top_nav/open_options_popover.tsx | 2 +- .../main/components/top_nav/open_search_panel.test.tsx | 4 ++-- .../main/components/top_nav/open_search_panel.tsx | 2 +- .../public/application/main/discover_main_app.tsx | 6 +++--- .../public/application/main/discover_main_route.tsx | 2 +- .../main/{utils => hooks}/use_behavior_subject.ts | 0 .../main/{utils => hooks}/use_data_state.ts | 0 .../main/{utils => hooks}/use_discover_state.test.ts | 0 .../main/{utils => hooks}/use_discover_state.ts | 8 ++++---- .../main/{utils => hooks}/use_saved_search.test.ts | 0 .../main/{utils => hooks}/use_saved_search.ts | 6 +++--- .../{utils => hooks}/use_saved_search_messages.test.ts | 0 .../main/{utils => hooks}/use_saved_search_messages.ts | 0 .../main/{utils => hooks}/use_search_session.test.ts | 0 .../main/{utils => hooks}/use_search_session.ts | 0 .../application/main/{utils => hooks}/use_singleton.ts | 0 .../application/main/{utils => hooks}/use_url.test.ts | 0 .../application/main/{utils => hooks}/use_url.ts | 0 .../public/application/main/utils/fetch_all.test.ts | 2 +- .../public/application/main/utils/fetch_all.ts | 4 ++-- .../public/application/main/utils/fetch_chart.ts | 2 +- .../application/main/utils/get_fetch_observable.ts | 2 +- .../main/utils/get_fetch_observeable.test.ts | 2 +- .../main/utils/get_switch_index_pattern_app_state.ts | 2 +- .../public/application/not_found/not_found_route.tsx | 2 +- .../public/application/view_alert/view_alert_route.tsx | 2 +- .../public/components/discover_grid/discover_grid.tsx | 6 +++--- .../discover_grid/discover_grid_cell_actions.test.tsx | 4 ++-- .../discover_grid/discover_grid_cell_actions.tsx | 2 +- .../components/discover_grid/discover_grid_flyout.tsx | 4 ++-- .../discover_grid/get_render_cell_value.test.tsx | 4 ++-- .../components/discover_grid/get_render_cell_value.tsx | 2 +- .../discover_tour/discover_tour_provider.tsx | 2 +- .../doc_table/components/table_header/table_header.tsx | 4 ++-- .../components/doc_table/components/table_row.test.tsx | 4 ++-- .../components/doc_table/components/table_row.tsx | 6 +++--- .../doc_table/components/table_row_details.tsx | 2 +- .../components/doc_table/doc_table_embeddable.tsx | 4 ++-- .../public/components/doc_table/doc_table_infinite.tsx | 4 ++-- .../public/components/doc_table/doc_table_wrapper.tsx | 2 +- .../discover/public/components/doc_table/index.ts | 6 +++--- .../doc_table/{lib => utils}/get_default_sort.test.ts | 0 .../doc_table/{lib => utils}/get_default_sort.ts | 0 .../doc_table/{lib => utils}/get_sort.test.ts | 0 .../components/doc_table/{lib => utils}/get_sort.ts | 0 .../{lib => utils}/get_sort_for_search_source.test.ts | 0 .../{lib => utils}/get_sort_for_search_source.ts | 0 .../doc_table/{lib => utils}/row_formatter.scss | 0 .../doc_table/{lib => utils}/row_formatter.test.ts | 0 .../doc_table/{lib => utils}/row_formatter.tsx | 0 .../{lib => utils}/should_load_next_doc_patch.test.ts | 0 .../{lib => utils}/should_load_next_doc_patch.ts | 0 .../public/embeddable/utils/update_search_source.ts | 2 +- .../{utils => hooks}/use_data_grid_columns.test.tsx | 0 .../public/{utils => hooks}/use_data_grid_columns.ts | 0 .../public/{utils => hooks}/use_discover_services.ts | 0 .../public/{utils => hooks}/use_es_doc_search.test.tsx | 0 .../public/{utils => hooks}/use_es_doc_search.ts | 0 .../public/{utils => hooks}/use_index_pattern.test.tsx | 0 .../public/{utils => hooks}/use_index_pattern.tsx | 0 .../{utils => hooks}/use_navigation_props.test.tsx | 0 .../public/{utils => hooks}/use_navigation_props.tsx | 0 .../public/{utils => hooks}/use_pager.test.tsx | 0 .../discover/public/{utils => hooks}/use_pager.ts | 0 .../{utils => hooks}/use_row_heights_options.test.tsx | 0 .../public/{utils => hooks}/use_row_heights_options.ts | 8 ++++++-- src/plugins/discover/public/plugin.tsx | 2 +- .../components/doc_viewer/doc_viewer.test.tsx | 2 +- .../components/doc_viewer_source/source.test.tsx | 2 +- .../doc_views/components/doc_viewer_source/source.tsx | 4 ++-- .../components/doc_viewer_table/legacy/table.tsx | 2 +- .../doc_views/components/doc_viewer_table/table.tsx | 4 ++-- 102 files changed, 119 insertions(+), 114 deletions(-) rename src/plugins/discover/public/application/context/{utils => hooks}/use_context_app_fetch.test.tsx (100%) rename src/plugins/discover/public/application/context/{utils => hooks}/use_context_app_fetch.tsx (98%) rename src/plugins/discover/public/application/context/{utils => hooks}/use_context_app_state.ts (100%) rename src/plugins/discover/public/application/main/{utils => hooks}/use_behavior_subject.ts (100%) rename src/plugins/discover/public/application/main/{utils => hooks}/use_data_state.ts (100%) rename src/plugins/discover/public/application/main/{utils => hooks}/use_discover_state.test.ts (100%) rename src/plugins/discover/public/application/main/{utils => hooks}/use_discover_state.ts (96%) rename src/plugins/discover/public/application/main/{utils => hooks}/use_saved_search.test.ts (100%) rename src/plugins/discover/public/application/main/{utils => hooks}/use_saved_search.ts (97%) rename src/plugins/discover/public/application/main/{utils => hooks}/use_saved_search_messages.test.ts (100%) rename src/plugins/discover/public/application/main/{utils => hooks}/use_saved_search_messages.ts (100%) rename src/plugins/discover/public/application/main/{utils => hooks}/use_search_session.test.ts (100%) rename src/plugins/discover/public/application/main/{utils => hooks}/use_search_session.ts (100%) rename src/plugins/discover/public/application/main/{utils => hooks}/use_singleton.ts (100%) rename src/plugins/discover/public/application/main/{utils => hooks}/use_url.test.ts (100%) rename src/plugins/discover/public/application/main/{utils => hooks}/use_url.ts (100%) rename src/plugins/discover/public/components/doc_table/{lib => utils}/get_default_sort.test.ts (100%) rename src/plugins/discover/public/components/doc_table/{lib => utils}/get_default_sort.ts (100%) rename src/plugins/discover/public/components/doc_table/{lib => utils}/get_sort.test.ts (100%) rename src/plugins/discover/public/components/doc_table/{lib => utils}/get_sort.ts (100%) rename src/plugins/discover/public/components/doc_table/{lib => utils}/get_sort_for_search_source.test.ts (100%) rename src/plugins/discover/public/components/doc_table/{lib => utils}/get_sort_for_search_source.ts (100%) rename src/plugins/discover/public/components/doc_table/{lib => utils}/row_formatter.scss (100%) rename src/plugins/discover/public/components/doc_table/{lib => utils}/row_formatter.test.ts (100%) rename src/plugins/discover/public/components/doc_table/{lib => utils}/row_formatter.tsx (100%) rename src/plugins/discover/public/components/doc_table/{lib => utils}/should_load_next_doc_patch.test.ts (100%) rename src/plugins/discover/public/components/doc_table/{lib => utils}/should_load_next_doc_patch.ts (100%) rename src/plugins/discover/public/{utils => hooks}/use_data_grid_columns.test.tsx (100%) rename src/plugins/discover/public/{utils => hooks}/use_data_grid_columns.ts (100%) rename src/plugins/discover/public/{utils => hooks}/use_discover_services.ts (100%) rename src/plugins/discover/public/{utils => hooks}/use_es_doc_search.test.tsx (100%) rename src/plugins/discover/public/{utils => hooks}/use_es_doc_search.ts (100%) rename src/plugins/discover/public/{utils => hooks}/use_index_pattern.test.tsx (100%) rename src/plugins/discover/public/{utils => hooks}/use_index_pattern.tsx (100%) rename src/plugins/discover/public/{utils => hooks}/use_navigation_props.test.tsx (100%) rename src/plugins/discover/public/{utils => hooks}/use_navigation_props.tsx (100%) rename src/plugins/discover/public/{utils => hooks}/use_pager.test.tsx (100%) rename src/plugins/discover/public/{utils => hooks}/use_pager.ts (100%) rename src/plugins/discover/public/{utils => hooks}/use_row_heights_options.test.tsx (100%) rename src/plugins/discover/public/{utils => hooks}/use_row_heights_options.ts (94%) diff --git a/src/plugins/discover/README.md b/src/plugins/discover/README.md index 6537a830c46a9..f7dcc54880ead 100644 --- a/src/plugins/discover/README.md +++ b/src/plugins/discover/README.md @@ -17,6 +17,7 @@ One folder for every "route", each folder contains files and folders related onl * **[/view_alert](./public/application/view_alert)** (Forwarding links in alert notifications) * **[/components](./public/components)** (All React components used in more than just one app) * **[/embeddable](./public/embeddable)** (Code related to the saved search embeddable, rendered on dashboards) +* **[/hooks](./public/hooks)** (Code containing React hooks) * **[/services](./public/services)** (Services either for external or internal use) * **[/utils](./public/utils)** (All utility functions used across more than one application) diff --git a/src/plugins/discover/public/application/context/context_app.tsx b/src/plugins/discover/public/application/context/context_app.tsx index 1f886fdacac6b..482598ea40f38 100644 --- a/src/plugins/discover/public/application/context/context_app.tsx +++ b/src/plugins/discover/public/application/context/context_app.tsx @@ -19,14 +19,14 @@ import { DOC_TABLE_LEGACY, SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; import { ContextErrorMessage } from './components/context_error_message'; import { LoadingStatus } from './services/context_query_state'; import { AppState, isEqualFilters } from './services/context_state'; -import { useColumns } from '../../utils/use_data_grid_columns'; -import { useContextAppState } from './utils/use_context_app_state'; -import { useContextAppFetch } from './utils/use_context_app_fetch'; +import { useColumns } from '../../hooks/use_data_grid_columns'; +import { useContextAppState } from './hooks/use_context_app_state'; +import { useContextAppFetch } from './hooks/use_context_app_fetch'; import { popularizeField } from '../../utils/popularize_field'; import { ContextAppContent } from './context_app_content'; import { SurrDocType } from './services/context'; import { DocViewFilterFn } from '../../services/doc_views/doc_views_types'; -import { useDiscoverServices } from '../../utils/use_discover_services'; +import { useDiscoverServices } from '../../hooks/use_discover_services'; const ContextAppContentMemoized = memo(ContextAppContent); diff --git a/src/plugins/discover/public/application/context/context_app_content.tsx b/src/plugins/discover/public/application/context/context_app_content.tsx index dbc82061ab9a0..0c14c3bd62e34 100644 --- a/src/plugins/discover/public/application/context/context_app_content.tsx +++ b/src/plugins/discover/public/application/context/context_app_content.tsx @@ -21,9 +21,9 @@ import { SurrDocType } from './services/context'; import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from './services/constants'; import { DocTableContext } from '../../components/doc_table/doc_table_context'; import { EsHitRecordList } from '../types'; -import { SortPairArr } from '../../components/doc_table/lib/get_sort'; +import { SortPairArr } from '../../components/doc_table/utils/get_sort'; import { ElasticSearchHit } from '../../types'; -import { useDiscoverServices } from '../../utils/use_discover_services'; +import { useDiscoverServices } from '../../hooks/use_discover_services'; export interface ContextAppContentProps { columns: string[]; diff --git a/src/plugins/discover/public/application/context/context_app_route.tsx b/src/plugins/discover/public/application/context/context_app_route.tsx index 313769d98ae49..b8080788fd647 100644 --- a/src/plugins/discover/public/application/context/context_app_route.tsx +++ b/src/plugins/discover/public/application/context/context_app_route.tsx @@ -13,9 +13,9 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { ContextApp } from './context_app'; import { getRootBreadcrumbs } from '../../utils/breadcrumbs'; import { LoadingIndicator } from '../../components/common/loading_indicator'; -import { useIndexPattern } from '../../utils/use_index_pattern'; -import { useMainRouteBreadcrumb } from '../../utils/use_navigation_props'; -import { useDiscoverServices } from '../../utils/use_discover_services'; +import { useIndexPattern } from '../../hooks/use_index_pattern'; +import { useMainRouteBreadcrumb } from '../../hooks/use_navigation_props'; +import { useDiscoverServices } from '../../hooks/use_discover_services'; export interface ContextUrlParams { indexPatternId: string; diff --git a/src/plugins/discover/public/application/context/utils/use_context_app_fetch.test.tsx b/src/plugins/discover/public/application/context/hooks/use_context_app_fetch.test.tsx similarity index 100% rename from src/plugins/discover/public/application/context/utils/use_context_app_fetch.test.tsx rename to src/plugins/discover/public/application/context/hooks/use_context_app_fetch.test.tsx diff --git a/src/plugins/discover/public/application/context/utils/use_context_app_fetch.tsx b/src/plugins/discover/public/application/context/hooks/use_context_app_fetch.tsx similarity index 98% rename from src/plugins/discover/public/application/context/utils/use_context_app_fetch.tsx rename to src/plugins/discover/public/application/context/hooks/use_context_app_fetch.tsx index 88d3764a86982..a43b8b0ca4f9d 100644 --- a/src/plugins/discover/public/application/context/utils/use_context_app_fetch.tsx +++ b/src/plugins/discover/public/application/context/hooks/use_context_app_fetch.tsx @@ -20,9 +20,9 @@ import { LoadingStatus, } from '../services/context_query_state'; import { AppState } from '../services/context_state'; -import { getFirstSortableField } from './sorting'; +import { getFirstSortableField } from '../utils/sorting'; import { EsHitRecord } from '../../types'; -import { useDiscoverServices } from '../../../utils/use_discover_services'; +import { useDiscoverServices } from '../../../hooks/use_discover_services'; const createError = (statusKey: string, reason: FailureReason, error?: Error) => ({ [statusKey]: { value: LoadingStatus.FAILED, error, reason }, diff --git a/src/plugins/discover/public/application/context/utils/use_context_app_state.ts b/src/plugins/discover/public/application/context/hooks/use_context_app_state.ts similarity index 100% rename from src/plugins/discover/public/application/context/utils/use_context_app_state.ts rename to src/plugins/discover/public/application/context/hooks/use_context_app_state.ts diff --git a/src/plugins/discover/public/application/doc/components/doc.tsx b/src/plugins/discover/public/application/doc/components/doc.tsx index cd2b27c956b9d..5119d005edfde 100644 --- a/src/plugins/discover/public/application/doc/components/doc.tsx +++ b/src/plugins/discover/public/application/doc/components/doc.tsx @@ -12,8 +12,8 @@ import { EuiCallOut, EuiLink, EuiLoadingSpinner, EuiPageContent, EuiPage } from import type { DataView } from '@kbn/data-views-plugin/public'; import { DocViewer } from '../../../services/doc_views/components/doc_viewer'; import { ElasticRequestState } from '../types'; -import { useEsDocSearch } from '../../../utils/use_es_doc_search'; -import { useDiscoverServices } from '../../../utils/use_discover_services'; +import { useEsDocSearch } from '../../../hooks/use_es_doc_search'; +import { useDiscoverServices } from '../../../hooks/use_discover_services'; export interface DocProps { /** diff --git a/src/plugins/discover/public/application/doc/single_doc_route.tsx b/src/plugins/discover/public/application/doc/single_doc_route.tsx index c03ec2658c86d..f295ebac405a9 100644 --- a/src/plugins/discover/public/application/doc/single_doc_route.tsx +++ b/src/plugins/discover/public/application/doc/single_doc_route.tsx @@ -12,11 +12,11 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { useExecutionContext } from '@kbn/kibana-react-plugin/public'; import { getRootBreadcrumbs } from '../../utils/breadcrumbs'; import { LoadingIndicator } from '../../components/common/loading_indicator'; -import { useIndexPattern } from '../../utils/use_index_pattern'; +import { useIndexPattern } from '../../hooks/use_index_pattern'; import { withQueryParams } from '../../utils/with_query_params'; -import { useMainRouteBreadcrumb } from '../../utils/use_navigation_props'; +import { useMainRouteBreadcrumb } from '../../hooks/use_navigation_props'; import { Doc } from './components/doc'; -import { useDiscoverServices } from '../../utils/use_discover_services'; +import { useDiscoverServices } from '../../hooks/use_discover_services'; export interface SingleDocRouteProps { /** diff --git a/src/plugins/discover/public/application/main/components/chart/discover_chart.test.tsx b/src/plugins/discover/public/application/main/components/chart/discover_chart.test.tsx index 3f33b6cdbf032..e3fab643c5473 100644 --- a/src/plugins/discover/public/application/main/components/chart/discover_chart.test.tsx +++ b/src/plugins/discover/public/application/main/components/chart/discover_chart.test.tsx @@ -16,7 +16,7 @@ import { esHits } from '../../../../__mocks__/es_hits'; import { savedSearchMock } from '../../../../__mocks__/saved_search'; import { createSearchSourceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; import { GetStateReturn } from '../../services/discover_state'; -import { DataCharts$, DataTotalHits$ } from '../../utils/use_saved_search'; +import { DataCharts$, DataTotalHits$ } from '../../hooks/use_saved_search'; import { discoverServiceMock } from '../../../../__mocks__/services'; import { FetchStatus } from '../../../types'; import { Chart } from './point_series'; diff --git a/src/plugins/discover/public/application/main/components/chart/discover_chart.tsx b/src/plugins/discover/public/application/main/components/chart/discover_chart.tsx index 539220e17eb50..271f0c0b9f57d 100644 --- a/src/plugins/discover/public/application/main/components/chart/discover_chart.tsx +++ b/src/plugins/discover/public/application/main/components/chart/discover_chart.tsx @@ -22,11 +22,11 @@ import { HitsCounter } from '../hits_counter'; import { SavedSearch } from '../../../../services/saved_searches'; import { GetStateReturn } from '../../services/discover_state'; import { DiscoverHistogram } from './histogram'; -import { DataCharts$, DataTotalHits$ } from '../../utils/use_saved_search'; +import { DataCharts$, DataTotalHits$ } from '../../hooks/use_saved_search'; import { useChartPanels } from './use_chart_panels'; import { VIEW_MODE, DocumentViewModeToggle } from '../../../../components/view_mode_toggle'; import { SHOW_FIELD_STATISTICS } from '../../../../../common'; -import { useDiscoverServices } from '../../../../utils/use_discover_services'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { getVisualizeInformation, triggerVisualizeActions, diff --git a/src/plugins/discover/public/application/main/components/chart/histogram.test.tsx b/src/plugins/discover/public/application/main/components/chart/histogram.test.tsx index 80cd8562d3fa9..655925e3effd7 100644 --- a/src/plugins/discover/public/application/main/components/chart/histogram.test.tsx +++ b/src/plugins/discover/public/application/main/components/chart/histogram.test.tsx @@ -8,12 +8,12 @@ import { mountWithIntl } from '@kbn/test-jest-helpers'; import { BehaviorSubject } from 'rxjs'; import { FetchStatus } from '../../../types'; -import { DataCharts$ } from '../../utils/use_saved_search'; +import { DataCharts$ } from '../../hooks/use_saved_search'; import { discoverServiceMock } from '../../../../__mocks__/services'; import { Chart } from './point_series'; import { DiscoverHistogram } from './histogram'; import React from 'react'; -import * as hooks from '../../utils/use_data_state'; +import * as hooks from '../../hooks/use_data_state'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { GetStateReturn } from '../../services/discover_state'; diff --git a/src/plugins/discover/public/application/main/components/chart/histogram.tsx b/src/plugins/discover/public/application/main/components/chart/histogram.tsx index 4502060fb2458..b34aa1595f81e 100644 --- a/src/plugins/discover/public/application/main/components/chart/histogram.tsx +++ b/src/plugins/discover/public/application/main/components/chart/histogram.tsx @@ -41,10 +41,10 @@ import { renderEndzoneTooltip, } from '@kbn/charts-plugin/public'; import { LEGACY_TIME_AXIS, MULTILAYER_TIME_AXIS_STYLE } from '@kbn/charts-plugin/common'; -import { useDiscoverServices } from '../../../../utils/use_discover_services'; -import { DataCharts$, DataChartsMessage } from '../../utils/use_saved_search'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; +import { DataCharts$, DataChartsMessage } from '../../hooks/use_saved_search'; import { FetchStatus } from '../../../types'; -import { useDataState } from '../../utils/use_data_state'; +import { useDataState } from '../../hooks/use_data_state'; import { GetStateReturn } from '../../services/discover_state'; export interface DiscoverHistogramProps { diff --git a/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_callout.tsx b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_callout.tsx index 9087d380d8bf2..e17f8bad92816 100644 --- a/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_callout.tsx +++ b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_callout.tsx @@ -21,7 +21,7 @@ import { } from '@elastic/eui'; import { css } from '@emotion/react'; import { Storage } from '@kbn/kibana-utils-plugin/public'; -import { useDiscoverServices } from '../../../../utils/use_discover_services'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { DOC_TABLE_LEGACY } from '../../../../../common'; export const CALLOUT_STATE_KEY = 'discover:docExplorerCalloutClosed'; diff --git a/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.tsx b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.tsx index 2fe073946627a..617790c28907b 100644 --- a/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.tsx +++ b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.tsx @@ -19,7 +19,7 @@ import { EuiFlexItem, } from '@elastic/eui'; import { Storage } from '@kbn/kibana-utils-plugin/public'; -import { useDiscoverServices } from '../../../../utils/use_discover_services'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { useDiscoverTourContext } from '../../../../components/discover_tour'; export const CALLOUT_STATE_KEY = 'discover:docExplorerUpdateCalloutClosed'; diff --git a/src/plugins/discover/public/application/main/components/field_stats_table/field_stats_table.tsx b/src/plugins/discover/public/application/main/components/field_stats_table/field_stats_table.tsx index 9e51324aefc64..7838190608a97 100644 --- a/src/plugins/discover/public/application/main/components/field_stats_table/field_stats_table.tsx +++ b/src/plugins/discover/public/application/main/components/field_stats_table/field_stats_table.tsx @@ -18,11 +18,11 @@ import { IEmbeddable, isErrorEmbeddable, } from '@kbn/embeddable-plugin/public'; -import { useDiscoverServices } from '../../../../utils/use_discover_services'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { FIELD_STATISTICS_LOADED } from './constants'; import type { SavedSearch } from '../../../../services/saved_searches'; import type { GetStateReturn } from '../../services/discover_state'; -import { AvailableFields$, DataRefetch$ } from '../../utils/use_saved_search'; +import { AvailableFields$, DataRefetch$ } from '../../hooks/use_saved_search'; export interface DataVisualizerGridEmbeddableInput extends EmbeddableInput { dataView: DataView; diff --git a/src/plugins/discover/public/application/main/components/hits_counter/hits_counter.test.tsx b/src/plugins/discover/public/application/main/components/hits_counter/hits_counter.test.tsx index 4ba4eda45d279..b8111b25a6ef2 100644 --- a/src/plugins/discover/public/application/main/components/hits_counter/hits_counter.test.tsx +++ b/src/plugins/discover/public/application/main/components/hits_counter/hits_counter.test.tsx @@ -13,7 +13,7 @@ import { HitsCounter, HitsCounterProps } from './hits_counter'; import { findTestSubject } from '@elastic/eui/lib/test'; import { BehaviorSubject } from 'rxjs'; import { FetchStatus } from '../../../types'; -import { DataTotalHits$ } from '../../utils/use_saved_search'; +import { DataTotalHits$ } from '../../hooks/use_saved_search'; describe('hits counter', function () { let props: HitsCounterProps; diff --git a/src/plugins/discover/public/application/main/components/hits_counter/hits_counter.tsx b/src/plugins/discover/public/application/main/components/hits_counter/hits_counter.tsx index 1ae2b0942e248..bb7681c2efa36 100644 --- a/src/plugins/discover/public/application/main/components/hits_counter/hits_counter.tsx +++ b/src/plugins/discover/public/application/main/components/hits_counter/hits_counter.tsx @@ -17,9 +17,9 @@ import { } from '@elastic/eui'; import { FormattedMessage, FormattedNumber } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { DataTotalHits$, DataTotalHitsMsg } from '../../utils/use_saved_search'; +import { DataTotalHits$, DataTotalHitsMsg } from '../../hooks/use_saved_search'; import { FetchStatus } from '../../../types'; -import { useDataState } from '../../utils/use_data_state'; +import { useDataState } from '../../hooks/use_data_state'; export interface HitsCounterProps { /** diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx index 6e5b5a513f9bf..01d5aae129a72 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.test.tsx @@ -13,7 +13,7 @@ import { setHeaderActionMenuMounter } from '../../../../kibana_services'; import { esHits } from '../../../../__mocks__/es_hits'; import { savedSearchMock } from '../../../../__mocks__/saved_search'; import { GetStateReturn } from '../../services/discover_state'; -import { DataDocuments$ } from '../../utils/use_saved_search'; +import { DataDocuments$ } from '../../hooks/use_saved_search'; import { discoverServiceMock } from '../../../../__mocks__/services'; import { FetchStatus } from '../../../types'; import { DiscoverDocuments } from './discover_documents'; diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx index 1a84516fbdd8d..530118cb8f1c9 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx @@ -15,7 +15,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { DataView } from '@kbn/data-views-plugin/public'; -import { useDiscoverServices } from '../../../../utils/use_discover_services'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { DocViewFilterFn } from '../../../../services/doc_views/doc_views_types'; import { DiscoverGrid } from '../../../../components/discover_grid/discover_grid'; import { FetchStatus } from '../../../types'; @@ -25,13 +25,13 @@ import { SAMPLE_SIZE_SETTING, SEARCH_FIELDS_FROM_SOURCE, } from '../../../../../common'; -import { useColumns } from '../../../../utils/use_data_grid_columns'; +import { useColumns } from '../../../../hooks/use_data_grid_columns'; import { SavedSearch } from '../../../../services/saved_searches'; -import { DataDocumentsMsg, DataDocuments$ } from '../../utils/use_saved_search'; +import { DataDocumentsMsg, DataDocuments$ } from '../../hooks/use_saved_search'; import { AppState, GetStateReturn } from '../../services/discover_state'; -import { useDataState } from '../../utils/use_data_state'; +import { useDataState } from '../../hooks/use_data_state'; import { DocTableInfinite } from '../../../../components/doc_table/doc_table_infinite'; -import { SortPairArr } from '../../../../components/doc_table/lib/get_sort'; +import { SortPairArr } from '../../../../components/doc_table/utils/get_sort'; import { ElasticSearchHit } from '../../../../types'; import { DocumentExplorerCallout } from '../document_explorer_callout'; import { DocumentExplorerUpdateCallout } from '../document_explorer_callout/document_explorer_update_callout'; diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx index 84f96eeeab65f..1025270dab355 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx @@ -26,7 +26,7 @@ import { DataDocuments$, DataMain$, DataTotalHits$, -} from '../../utils/use_saved_search'; +} from '../../hooks/use_saved_search'; import { discoverServiceMock } from '../../../../__mocks__/services'; import { FetchStatus } from '../../../types'; import { RequestAdapter } from '@kbn/inspector-plugin'; diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx index d44f0e6c002ae..d68414f6e8478 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx @@ -24,7 +24,7 @@ import classNames from 'classnames'; import { generateFilters } from '@kbn/data-plugin/public'; import { DataView, DataViewField, DataViewType } from '@kbn/data-views-plugin/public'; import { InspectorSession } from '@kbn/inspector-plugin/public'; -import { useDiscoverServices } from '../../../../utils/use_discover_services'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { DiscoverNoResults } from '../no_results'; import { LoadingSpinner } from '../loading_spinner/loading_spinner'; import { DiscoverSidebarResponsive } from '../sidebar'; @@ -36,11 +36,11 @@ import { DocViewFilterFn } from '../../../../services/doc_views/doc_views_types' import { DiscoverChart } from '../chart'; import { getResultState } from '../../utils/get_result_state'; import { DiscoverUninitialized } from '../uninitialized/uninitialized'; -import { DataMainMsg } from '../../utils/use_saved_search'; -import { useColumns } from '../../../../utils/use_data_grid_columns'; +import { DataMainMsg } from '../../hooks/use_saved_search'; +import { useColumns } from '../../../../hooks/use_data_grid_columns'; import { DiscoverDocuments } from './discover_documents'; import { FetchStatus } from '../../../types'; -import { useDataState } from '../../utils/use_data_state'; +import { useDataState } from '../../hooks/use_data_state'; import { SavedSearchURLConflictCallout, useSavedSearchAliasMatchRedirect, diff --git a/src/plugins/discover/public/application/main/components/layout/types.ts b/src/plugins/discover/public/application/main/components/layout/types.ts index 295351f2cd318..52343c8611bdd 100644 --- a/src/plugins/discover/public/application/main/components/layout/types.ts +++ b/src/plugins/discover/public/application/main/components/layout/types.ts @@ -11,7 +11,7 @@ import type { DataView, DataViewAttributes } from '@kbn/data-views-plugin/public import { ISearchSource } from '@kbn/data-plugin/public'; import { RequestAdapter } from '@kbn/inspector-plugin'; import { AppState, GetStateReturn } from '../../services/discover_state'; -import { DataRefetch$, SavedSearchData } from '../../utils/use_saved_search'; +import { DataRefetch$, SavedSearchData } from '../../hooks/use_saved_search'; import { SavedSearch } from '../../../../services/saved_searches'; import { ElasticSearchHit } from '../../../../types'; diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.tsx index d6f88c0626e7f..0c0e88c8ca424 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.tsx @@ -37,7 +37,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { FieldIcon } from '@kbn/react-field'; import { getFieldTypeDescription } from './lib/get_field_type_description'; import { KNOWN_FIELD_TYPES } from '../../../../../common/field_types'; -import { useDiscoverServices } from '../../../../utils/use_discover_services'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; export interface State { searchable: string; diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx index c26a2c01f256a..5a553dc67596a 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.test.tsx @@ -27,7 +27,7 @@ import { ElasticSearchHit } from '../../../../types'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { BehaviorSubject } from 'rxjs'; import { FetchStatus } from '../../../types'; -import { AvailableFields$ } from '../../utils/use_saved_search'; +import { AvailableFields$ } from '../../hooks/use_saved_search'; function getCompProps(): DiscoverSidebarProps { const indexPattern = stubLogstashIndexPattern; diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx index 875e51d958552..23ba3e8e0cc71 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx @@ -29,7 +29,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { indexPatterns as indexPatternUtils } from '@kbn/data-plugin/public'; import { DataViewPicker } from '@kbn/unified-search-plugin/public'; import { DataViewField } from '@kbn/data-views-plugin/public'; -import { useDiscoverServices } from '../../../../utils/use_discover_services'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { DiscoverField } from './discover_field'; import { DiscoverFieldSearch } from './discover_field_search'; import { FIELDS_LIMIT_SETTING } from '../../../../../common'; diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx index d3af81c06c129..b10285383468c 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.test.tsx @@ -24,7 +24,7 @@ import { } from './discover_sidebar_responsive'; import { DiscoverServices } from '../../../../build_services'; import { FetchStatus } from '../../../types'; -import { AvailableFields$, DataDocuments$ } from '../../utils/use_saved_search'; +import { AvailableFields$, DataDocuments$ } from '../../hooks/use_saved_search'; import { stubLogstashIndexPattern } from '@kbn/data-plugin/common/stubs'; import { VIEW_MODE } from '../../../../components/view_mode_toggle'; import { ElasticSearchHit } from '../../../../types'; diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx index 785834785572b..a9121399c7e04 100644 --- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx @@ -24,11 +24,11 @@ import { } from '@elastic/eui'; import type { DataViewField, DataView, DataViewAttributes } from '@kbn/data-views-plugin/public'; import { SavedObject } from '@kbn/core/types'; -import { useDiscoverServices } from '../../../../utils/use_discover_services'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { getDefaultFieldFilter } from './lib/field_filter'; import { DiscoverSidebar } from './discover_sidebar'; import { AppState } from '../../services/discover_state'; -import { AvailableFields$, DataDocuments$ } from '../../utils/use_saved_search'; +import { AvailableFields$, DataDocuments$ } from '../../hooks/use_saved_search'; import { calcFieldCounts } from '../../utils/calc_field_counts'; import { VIEW_MODE } from '../../../../components/view_mode_toggle'; import { FetchStatus } from '../../../types'; diff --git a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx index e698f3756fb5f..265cec27d5344 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx @@ -9,7 +9,7 @@ import React, { useCallback, useMemo, useRef, useEffect } from 'react'; import { useHistory } from 'react-router-dom'; import { Query, TimeRange } from '@kbn/data-plugin/public'; import { DataViewType } from '@kbn/data-views-plugin/public'; -import { useDiscoverServices } from '../../../../utils/use_discover_services'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { DiscoverLayoutProps } from '../layout/types'; import { getTopNavLinks } from './get_top_nav_links'; import { getHeaderActionMenuMounter } from '../../../../kibana_services'; diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx index 48ba74c41cd2e..bfc1b28ed6eec 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/open_alerts_popover.tsx @@ -15,7 +15,7 @@ import { ISearchSource } from '@kbn/data-plugin/common'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { DiscoverServices } from '../../../../build_services'; import { updateSearchSource } from '../../utils/update_search_source'; -import { useDiscoverServices } from '../../../../utils/use_discover_services'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; const container = document.createElement('div'); let isOpen = false; diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_options_popover.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_options_popover.tsx index af976ccc7dca3..9a0e17b97c19b 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/open_options_popover.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/open_options_popover.tsx @@ -24,7 +24,7 @@ import { import './open_options_popover.scss'; import { Observable } from 'rxjs'; import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; -import { useDiscoverServices } from '../../../../utils/use_discover_services'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { DiscoverServices } from '../../../../build_services'; import { DOC_TABLE_LEGACY } from '../../../../../common'; diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_search_panel.test.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_search_panel.test.tsx index ad4c356ce63e0..859f265346d20 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/open_search_panel.test.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/open_search_panel.test.tsx @@ -15,7 +15,7 @@ describe('OpenSearchPanel', () => { }); test('render', async () => { - jest.doMock('../../../../utils/use_discover_services', () => ({ + jest.doMock('../../../../hooks/use_discover_services', () => ({ useDiscoverServices: jest.fn().mockImplementation(() => ({ core: { uiSettings: {}, savedObjects: {} }, addBasePath: (path: string) => path, @@ -31,7 +31,7 @@ describe('OpenSearchPanel', () => { }); test('should not render manage searches button without permissions', async () => { - jest.doMock('../../../../utils/use_discover_services', () => ({ + jest.doMock('../../../../hooks/use_discover_services', () => ({ useDiscoverServices: jest.fn().mockImplementation(() => ({ core: { uiSettings: {}, savedObjects: {} }, addBasePath: (path: string) => path, diff --git a/src/plugins/discover/public/application/main/components/top_nav/open_search_panel.tsx b/src/plugins/discover/public/application/main/components/top_nav/open_search_panel.tsx index 3e5a40dd94162..32668b99de0ed 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/open_search_panel.tsx +++ b/src/plugins/discover/public/application/main/components/top_nav/open_search_panel.tsx @@ -21,7 +21,7 @@ import { EuiTitle, } from '@elastic/eui'; import { SavedObjectFinderUi } from '@kbn/saved-objects-plugin/public'; -import { useDiscoverServices } from '../../../../utils/use_discover_services'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; const SEARCH_OBJECT_TYPE = 'search'; diff --git a/src/plugins/discover/public/application/main/discover_main_app.tsx b/src/plugins/discover/public/application/main/discover_main_app.tsx index fd1a438e70d04..ae477719af119 100644 --- a/src/plugins/discover/public/application/main/discover_main_app.tsx +++ b/src/plugins/discover/public/application/main/discover_main_app.tsx @@ -12,11 +12,11 @@ import type { SavedObject } from '@kbn/data-plugin/public'; import { DiscoverLayout } from './components/layout'; import { setBreadcrumbsTitle } from '../../utils/breadcrumbs'; import { addHelpMenuToAppChrome } from '../../components/help_menu/help_menu_util'; -import { useDiscoverState } from './utils/use_discover_state'; -import { useUrl } from './utils/use_url'; +import { useDiscoverState } from './hooks/use_discover_state'; +import { useUrl } from './hooks/use_url'; import { SavedSearch } from '../../services/saved_searches'; import { ElasticSearchHit } from '../../types'; -import { useDiscoverServices } from '../../utils/use_discover_services'; +import { useDiscoverServices } from '../../hooks/use_discover_services'; const DiscoverLayoutMemoized = React.memo(DiscoverLayout); diff --git a/src/plugins/discover/public/application/main/discover_main_route.tsx b/src/plugins/discover/public/application/main/discover_main_route.tsx index 4abaf18b566f7..213910bf37be2 100644 --- a/src/plugins/discover/public/application/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.tsx @@ -30,7 +30,7 @@ import { DiscoverMainApp } from './discover_main_app'; import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../../utils/breadcrumbs'; import { LoadingIndicator } from '../../components/common/loading_indicator'; import { DiscoverError } from '../../components/common/error_alert'; -import { useDiscoverServices } from '../../utils/use_discover_services'; +import { useDiscoverServices } from '../../hooks/use_discover_services'; import { getUrlTracker } from '../../kibana_services'; const DiscoverMainAppMemoized = memo(DiscoverMainApp); diff --git a/src/plugins/discover/public/application/main/utils/use_behavior_subject.ts b/src/plugins/discover/public/application/main/hooks/use_behavior_subject.ts similarity index 100% rename from src/plugins/discover/public/application/main/utils/use_behavior_subject.ts rename to src/plugins/discover/public/application/main/hooks/use_behavior_subject.ts diff --git a/src/plugins/discover/public/application/main/utils/use_data_state.ts b/src/plugins/discover/public/application/main/hooks/use_data_state.ts similarity index 100% rename from src/plugins/discover/public/application/main/utils/use_data_state.ts rename to src/plugins/discover/public/application/main/hooks/use_data_state.ts diff --git a/src/plugins/discover/public/application/main/utils/use_discover_state.test.ts b/src/plugins/discover/public/application/main/hooks/use_discover_state.test.ts similarity index 100% rename from src/plugins/discover/public/application/main/utils/use_discover_state.test.ts rename to src/plugins/discover/public/application/main/hooks/use_discover_state.test.ts diff --git a/src/plugins/discover/public/application/main/utils/use_discover_state.ts b/src/plugins/discover/public/application/main/hooks/use_discover_state.ts similarity index 96% rename from src/plugins/discover/public/application/main/utils/use_discover_state.ts rename to src/plugins/discover/public/application/main/hooks/use_discover_state.ts index 607aba88e033c..ab481f2f67a50 100644 --- a/src/plugins/discover/public/application/main/utils/use_discover_state.ts +++ b/src/plugins/discover/public/application/main/hooks/use_discover_state.ts @@ -9,10 +9,10 @@ import { useMemo, useEffect, useState, useCallback } from 'react'; import { isEqual } from 'lodash'; import { History } from 'history'; import { getState } from '../services/discover_state'; -import { getStateDefaults } from './get_state_defaults'; +import { getStateDefaults } from '../utils/get_state_defaults'; import { DiscoverServices } from '../../../build_services'; import { SavedSearch, getSavedSearch } from '../../../services/saved_searches'; -import { loadIndexPattern } from './resolve_index_pattern'; +import { loadIndexPattern } from '../utils/resolve_index_pattern'; import { useSavedSearch as useSavedSearchData } from './use_saved_search'; import { MODIFY_COLUMNS_ON_SWITCH, @@ -22,8 +22,8 @@ import { } from '../../../../common'; import { useSearchSession } from './use_search_session'; import { FetchStatus } from '../../types'; -import { getSwitchIndexPatternAppState } from './get_switch_index_pattern_app_state'; -import { SortPairArr } from '../../../components/doc_table/lib/get_sort'; +import { getSwitchIndexPatternAppState } from '../utils/get_switch_index_pattern_app_state'; +import { SortPairArr } from '../../../components/doc_table/utils/get_sort'; import { ElasticSearchHit } from '../../../types'; export function useDiscoverState({ diff --git a/src/plugins/discover/public/application/main/utils/use_saved_search.test.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search.test.ts similarity index 100% rename from src/plugins/discover/public/application/main/utils/use_saved_search.test.ts rename to src/plugins/discover/public/application/main/hooks/use_saved_search.test.ts diff --git a/src/plugins/discover/public/application/main/utils/use_saved_search.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search.ts similarity index 97% rename from src/plugins/discover/public/application/main/utils/use_saved_search.ts rename to src/plugins/discover/public/application/main/hooks/use_saved_search.ts index 5c9ea00d6558c..d4d6b869c7ee7 100644 --- a/src/plugins/discover/public/application/main/utils/use_saved_search.ts +++ b/src/plugins/discover/public/application/main/hooks/use_saved_search.ts @@ -13,15 +13,15 @@ import type { AutoRefreshDoneFn } from '@kbn/data-plugin/public'; import { DiscoverServices } from '../../../build_services'; import { DiscoverSearchSessionManager } from '../services/discover_search_session'; import { GetStateReturn } from '../services/discover_state'; -import { validateTimeRange } from './validate_time_range'; +import { validateTimeRange } from '../utils/validate_time_range'; import { Chart } from '../components/chart/point_series'; import { useSingleton } from './use_singleton'; import { FetchStatus } from '../../types'; -import { fetchAll } from './fetch_all'; +import { fetchAll } from '../utils/fetch_all'; import { useBehaviorSubject } from './use_behavior_subject'; import { sendResetMsg } from './use_saved_search_messages'; -import { getFetch$ } from './get_fetch_observable'; +import { getFetch$ } from '../utils/get_fetch_observable'; import { ElasticSearchHit } from '../../../types'; import { SavedSearch } from '../../../services/saved_searches'; diff --git a/src/plugins/discover/public/application/main/utils/use_saved_search_messages.test.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.test.ts similarity index 100% rename from src/plugins/discover/public/application/main/utils/use_saved_search_messages.test.ts rename to src/plugins/discover/public/application/main/hooks/use_saved_search_messages.test.ts diff --git a/src/plugins/discover/public/application/main/utils/use_saved_search_messages.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts similarity index 100% rename from src/plugins/discover/public/application/main/utils/use_saved_search_messages.ts rename to src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts diff --git a/src/plugins/discover/public/application/main/utils/use_search_session.test.ts b/src/plugins/discover/public/application/main/hooks/use_search_session.test.ts similarity index 100% rename from src/plugins/discover/public/application/main/utils/use_search_session.test.ts rename to src/plugins/discover/public/application/main/hooks/use_search_session.test.ts diff --git a/src/plugins/discover/public/application/main/utils/use_search_session.ts b/src/plugins/discover/public/application/main/hooks/use_search_session.ts similarity index 100% rename from src/plugins/discover/public/application/main/utils/use_search_session.ts rename to src/plugins/discover/public/application/main/hooks/use_search_session.ts diff --git a/src/plugins/discover/public/application/main/utils/use_singleton.ts b/src/plugins/discover/public/application/main/hooks/use_singleton.ts similarity index 100% rename from src/plugins/discover/public/application/main/utils/use_singleton.ts rename to src/plugins/discover/public/application/main/hooks/use_singleton.ts diff --git a/src/plugins/discover/public/application/main/utils/use_url.test.ts b/src/plugins/discover/public/application/main/hooks/use_url.test.ts similarity index 100% rename from src/plugins/discover/public/application/main/utils/use_url.test.ts rename to src/plugins/discover/public/application/main/hooks/use_url.test.ts diff --git a/src/plugins/discover/public/application/main/utils/use_url.ts b/src/plugins/discover/public/application/main/hooks/use_url.ts similarity index 100% rename from src/plugins/discover/public/application/main/utils/use_url.ts rename to src/plugins/discover/public/application/main/hooks/use_url.ts diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts index 03a414cd70702..d372f921f9976 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts @@ -22,7 +22,7 @@ import { DataMainMsg, DataTotalHitsMsg, SavedSearchData, -} from './use_saved_search'; +} from '../hooks/use_saved_search'; import { fetchDocuments } from './fetch_documents'; import { fetchChart } from './fetch_chart'; diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.ts b/src/plugins/discover/public/application/main/utils/fetch_all.ts index a897c2e18f384..1d5d4646445a5 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.ts @@ -17,7 +17,7 @@ import { sendNoResultsFoundMsg, sendPartialMsg, sendResetMsg, -} from './use_saved_search_messages'; +} from '../hooks/use_saved_search_messages'; import { updateSearchSource } from './update_search_source'; import type { SavedSearch, SortOrder } from '../../../services/saved_searches'; import { fetchDocuments } from './fetch_documents'; @@ -31,7 +31,7 @@ import { DataMain$, DataTotalHits$, SavedSearchData, -} from './use_saved_search'; +} from '../hooks/use_saved_search'; import { DiscoverServices } from '../../../build_services'; export interface FetchDeps { diff --git a/src/plugins/discover/public/application/main/utils/fetch_chart.ts b/src/plugins/discover/public/application/main/utils/fetch_chart.ts index 21270e2f94655..5117bffe5c9b8 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_chart.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_chart.ts @@ -17,7 +17,7 @@ import { } from '@kbn/data-plugin/public'; import { getChartAggConfigs, getDimensions } from '.'; import { buildPointSeriesData, Chart } from '../components/chart/point_series'; -import { TimechartBucketInterval } from './use_saved_search'; +import { TimechartBucketInterval } from '../hooks/use_saved_search'; import { FetchDeps } from './fetch_all'; interface Result { diff --git a/src/plugins/discover/public/application/main/utils/get_fetch_observable.ts b/src/plugins/discover/public/application/main/utils/get_fetch_observable.ts index 2265de776c065..984d0737dfb31 100644 --- a/src/plugins/discover/public/application/main/utils/get_fetch_observable.ts +++ b/src/plugins/discover/public/application/main/utils/get_fetch_observable.ts @@ -14,7 +14,7 @@ import type { ISearchSource, } from '@kbn/data-plugin/public'; import { FetchStatus } from '../../types'; -import { DataMain$, DataRefetch$ } from './use_saved_search'; +import { DataMain$, DataRefetch$ } from '../hooks/use_saved_search'; import { DiscoverSearchSessionManager } from '../services/discover_search_session'; /** diff --git a/src/plugins/discover/public/application/main/utils/get_fetch_observeable.test.ts b/src/plugins/discover/public/application/main/utils/get_fetch_observeable.test.ts index aa75a04b15bca..e39642ac2bce2 100644 --- a/src/plugins/discover/public/application/main/utils/get_fetch_observeable.test.ts +++ b/src/plugins/discover/public/application/main/utils/get_fetch_observeable.test.ts @@ -11,7 +11,7 @@ import { getFetch$ } from './get_fetch_observable'; import { FetchStatus } from '../../types'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { createSearchSessionMock } from '../../../__mocks__/search_session'; -import { DataRefetch$ } from './use_saved_search'; +import { DataRefetch$ } from '../hooks/use_saved_search'; import { savedSearchMock, savedSearchMockWithTimeField } from '../../../__mocks__/saved_search'; function createDataMock( diff --git a/src/plugins/discover/public/application/main/utils/get_switch_index_pattern_app_state.ts b/src/plugins/discover/public/application/main/utils/get_switch_index_pattern_app_state.ts index bf9aeee042fb8..59dbd79f157bf 100644 --- a/src/plugins/discover/public/application/main/utils/get_switch_index_pattern_app_state.ts +++ b/src/plugins/discover/public/application/main/utils/get_switch_index_pattern_app_state.ts @@ -7,7 +7,7 @@ */ import type { DataView } from '@kbn/data-views-plugin/public'; -import { getSortArray, SortPairArr } from '../../../components/doc_table/lib/get_sort'; +import { getSortArray, SortPairArr } from '../../../components/doc_table/utils/get_sort'; /** * Helper function to remove or adapt the currently selected columns/sort to be valid with the next diff --git a/src/plugins/discover/public/application/not_found/not_found_route.tsx b/src/plugins/discover/public/application/not_found/not_found_route.tsx index d4844c67b827e..e3a6e665e95ae 100644 --- a/src/plugins/discover/public/application/not_found/not_found_route.tsx +++ b/src/plugins/discover/public/application/not_found/not_found_route.tsx @@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { Redirect } from 'react-router-dom'; import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public'; import { getUrlTracker } from '../../kibana_services'; -import { useDiscoverServices } from '../../utils/use_discover_services'; +import { useDiscoverServices } from '../../hooks/use_discover_services'; let bannerId: string | undefined; diff --git a/src/plugins/discover/public/application/view_alert/view_alert_route.tsx b/src/plugins/discover/public/application/view_alert/view_alert_route.tsx index 41759a01b2b96..1e99b318cd55e 100644 --- a/src/plugins/discover/public/application/view_alert/view_alert_route.tsx +++ b/src/plugins/discover/public/application/view_alert/view_alert_route.tsx @@ -14,7 +14,7 @@ import { getTime } from '@kbn/data-plugin/common'; import type { DataView } from '@kbn/data-views-plugin/public'; import type { Filter } from '@kbn/data-plugin/public'; import { DiscoverAppLocatorParams } from '../../locator'; -import { useDiscoverServices } from '../../utils/use_discover_services'; +import { useDiscoverServices } from '../../hooks/use_discover_services'; import { getAlertUtils, QueryParams, SearchThresholdAlertParams } from './view_alert_utils'; type NonNullableEntry = { [K in keyof T]: NonNullable }; diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid.tsx index eafc150843ef7..d5d150fb68144 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid.tsx @@ -46,11 +46,11 @@ import { SHOW_MULTIFIELDS, } from '../../../common'; import { DiscoverGridDocumentToolbarBtn, getDocId } from './discover_grid_document_selection'; -import { SortPairArr } from '../doc_table/lib/get_sort'; +import { SortPairArr } from '../doc_table/utils/get_sort'; import { getFieldsToShow } from '../../utils/get_fields_to_show'; import type { ElasticSearchHit, ValueToStringConverter } from '../../types'; -import { useRowHeightsOptions } from '../../utils/use_row_heights_options'; -import { useDiscoverServices } from '../../utils/use_discover_services'; +import { useRowHeightsOptions } from '../../hooks/use_row_heights_options'; +import { useDiscoverServices } from '../../hooks/use_discover_services'; import { convertValueToString } from '../../utils/convert_value_to_string'; interface SortObj { diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.test.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.test.tsx index 3adab8ab14662..4a7e7cd323aa5 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.test.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.test.tsx @@ -14,13 +14,13 @@ jest.mock('@elastic/eui', () => { }; }); -jest.mock('../../utils/use_discover_services', () => { +jest.mock('../../hooks/use_discover_services', () => { const services = { toastNotifications: { addInfo: jest.fn(), }, }; - const originalModule = jest.requireActual('../../utils/use_discover_services'); + const originalModule = jest.requireActual('../../hooks/use_discover_services'); return { ...originalModule, useDiscoverServices: () => services, diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx index 8065474e49cb5..f7497dd5d459d 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_cell_actions.tsx @@ -11,7 +11,7 @@ import { EuiDataGridColumnCellActionProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DataViewField } from '@kbn/data-views-plugin/public'; import { DiscoverGridContext, GridContext } from './discover_grid_context'; -import { useDiscoverServices } from '../../utils/use_discover_services'; +import { useDiscoverServices } from '../../hooks/use_discover_services'; import { copyValueToClipboard } from '../../utils/copy_value_to_clipboard'; function onFilterCell( diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_flyout.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_flyout.tsx index 27720e72110ab..a34e6da654699 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_flyout.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_flyout.tsx @@ -27,9 +27,9 @@ import { } from '@elastic/eui'; import { DocViewer } from '../../services/doc_views/components/doc_viewer/doc_viewer'; import { DocViewFilterFn } from '../../services/doc_views/doc_views_types'; -import { useNavigationProps } from '../../utils/use_navigation_props'; +import { useNavigationProps } from '../../hooks/use_navigation_props'; import { ElasticSearchHit } from '../../types'; -import { useDiscoverServices } from '../../utils/use_discover_services'; +import { useDiscoverServices } from '../../hooks/use_discover_services'; export interface DiscoverGridFlyoutProps { columns: string[]; diff --git a/src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx b/src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx index 62b37225372dc..c706892c9f706 100644 --- a/src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx +++ b/src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx @@ -25,8 +25,8 @@ const mockServices = { }, }; -jest.mock('../../utils/use_discover_services', () => { - const originalModule = jest.requireActual('../../utils/use_discover_services'); +jest.mock('../../hooks/use_discover_services', () => { + const originalModule = jest.requireActual('../../hooks/use_discover_services'); return { ...originalModule, useDiscoverServices: () => mockServices, diff --git a/src/plugins/discover/public/components/discover_grid/get_render_cell_value.tsx b/src/plugins/discover/public/components/discover_grid/get_render_cell_value.tsx index 0446a7217974e..b129b57dbec9c 100644 --- a/src/plugins/discover/public/components/discover_grid/get_render_cell_value.tsx +++ b/src/plugins/discover/public/components/discover_grid/get_render_cell_value.tsx @@ -28,7 +28,7 @@ import { EsHitRecord } from '../../application/types'; import { formatFieldValue } from '../../utils/format_value'; import { formatHit } from '../../utils/format_hit'; import { ElasticSearchHit, HitsFlattened } from '../../types'; -import { useDiscoverServices } from '../../utils/use_discover_services'; +import { useDiscoverServices } from '../../hooks/use_discover_services'; import { MAX_DOC_FIELDS_DISPLAYED } from '../../../common'; const CELL_CLASS = 'dscDiscoverGrid__cellValue'; diff --git a/src/plugins/discover/public/components/discover_tour/discover_tour_provider.tsx b/src/plugins/discover/public/components/discover_tour/discover_tour_provider.tsx index 610fc0907ea5a..8b50261981f3d 100644 --- a/src/plugins/discover/public/components/discover_tour/discover_tour_provider.tsx +++ b/src/plugins/discover/public/components/discover_tour/discover_tour_provider.tsx @@ -26,7 +26,7 @@ import { EuiText, } from '@elastic/eui'; import { PLUGIN_ID } from '../../../common'; -import { useDiscoverServices } from '../../utils/use_discover_services'; +import { useDiscoverServices } from '../../hooks/use_discover_services'; import { DiscoverTourContext, DiscoverTourContextProps } from './discover_tour_context'; import { DISCOVER_TOUR_STEP_ANCHORS } from './discover_tour_anchors'; diff --git a/src/plugins/discover/public/components/doc_table/components/table_header/table_header.tsx b/src/plugins/discover/public/components/doc_table/components/table_header/table_header.tsx index e3fd1696497fa..878cbe17f50d2 100644 --- a/src/plugins/discover/public/components/doc_table/components/table_header/table_header.tsx +++ b/src/plugins/discover/public/components/doc_table/components/table_header/table_header.tsx @@ -11,8 +11,8 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import { FORMATS_UI_SETTINGS } from '@kbn/field-formats-plugin/common'; import { TableHeaderColumn } from './table_header_column'; import { SortOrder, getDisplayedColumns } from './helpers'; -import { getDefaultSort } from '../../lib/get_default_sort'; -import { useDiscoverServices } from '../../../../utils/use_discover_services'; +import { getDefaultSort } from '../../utils/get_default_sort'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; interface Props { diff --git a/src/plugins/discover/public/components/doc_table/components/table_row.test.tsx b/src/plugins/discover/public/components/doc_table/components/table_row.test.tsx index f2cfc9c6ad698..03a12428569f7 100644 --- a/src/plugins/discover/public/components/doc_table/components/table_row.test.tsx +++ b/src/plugins/discover/public/components/doc_table/components/table_row.test.tsx @@ -18,8 +18,8 @@ import { discoverServiceMock } from '../../../__mocks__/services'; import { DOC_HIDE_TIME_COLUMN_SETTING, MAX_DOC_FIELDS_DISPLAYED } from '../../../../common'; -jest.mock('../lib/row_formatter', () => { - const originalModule = jest.requireActual('../lib/row_formatter'); +jest.mock('../utils/row_formatter', () => { + const originalModule = jest.requireActual('../utils/row_formatter'); return { ...originalModule, formatRow: () => { diff --git a/src/plugins/discover/public/components/doc_table/components/table_row.tsx b/src/plugins/discover/public/components/doc_table/components/table_row.tsx index 17c448f35d2a5..e08843006020a 100644 --- a/src/plugins/discover/public/components/doc_table/components/table_row.tsx +++ b/src/plugins/discover/public/components/doc_table/components/table_row.tsx @@ -15,12 +15,12 @@ import { DataView } from '@kbn/data-views-plugin/public'; import { formatFieldValue } from '../../../utils/format_value'; import { DocViewer } from '../../../services/doc_views/components/doc_viewer'; import { TableCell } from './table_row/table_cell'; -import { formatRow, formatTopLevelObject } from '../lib/row_formatter'; -import { useNavigationProps } from '../../../utils/use_navigation_props'; +import { formatRow, formatTopLevelObject } from '../utils/row_formatter'; +import { useNavigationProps } from '../../../hooks/use_navigation_props'; import { DocViewFilterFn } from '../../../services/doc_views/doc_views_types'; import { ElasticSearchHit } from '../../../types'; import { TableRowDetails } from './table_row_details'; -import { useDiscoverServices } from '../../../utils/use_discover_services'; +import { useDiscoverServices } from '../../../hooks/use_discover_services'; import { DOC_HIDE_TIME_COLUMN_SETTING, MAX_DOC_FIELDS_DISPLAYED } from '../../../../common'; export type DocTableRow = ElasticSearchHit & { diff --git a/src/plugins/discover/public/components/doc_table/components/table_row_details.tsx b/src/plugins/discover/public/components/doc_table/components/table_row_details.tsx index eda652c96f95f..89c29c4846fc4 100644 --- a/src/plugins/discover/public/components/doc_table/components/table_row_details.tsx +++ b/src/plugins/discover/public/components/doc_table/components/table_row_details.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { DiscoverNavigationProps } from '../../../utils/use_navigation_props'; +import { DiscoverNavigationProps } from '../../../hooks/use_navigation_props'; interface TableRowDetailsProps { open: boolean; colLength: number; diff --git a/src/plugins/discover/public/components/doc_table/doc_table_embeddable.tsx b/src/plugins/discover/public/components/doc_table/doc_table_embeddable.tsx index aeba3f35aef47..8bd3c8477e19b 100644 --- a/src/plugins/discover/public/components/doc_table/doc_table_embeddable.tsx +++ b/src/plugins/discover/public/components/doc_table/doc_table_embeddable.tsx @@ -11,11 +11,11 @@ import './index.scss'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { SAMPLE_SIZE_SETTING } from '../../../common'; -import { usePager } from '../../utils/use_pager'; +import { usePager } from '../../hooks/use_pager'; import { ToolBarPagination } from './components/pager/tool_bar_pagination'; import { DocTableProps, DocTableRenderProps, DocTableWrapper } from './doc_table_wrapper'; import { TotalDocuments } from '../../application/main/components/total_documents/total_documents'; -import { useDiscoverServices } from '../../utils/use_discover_services'; +import { useDiscoverServices } from '../../hooks/use_discover_services'; export interface DocTableEmbeddableProps extends DocTableProps { totalHitCount: number; diff --git a/src/plugins/discover/public/components/doc_table/doc_table_infinite.tsx b/src/plugins/discover/public/components/doc_table/doc_table_infinite.tsx index 8cda3b421815c..f68e05290c6f9 100644 --- a/src/plugins/discover/public/components/doc_table/doc_table_infinite.tsx +++ b/src/plugins/discover/public/components/doc_table/doc_table_infinite.tsx @@ -14,8 +14,8 @@ import { EuiButtonEmpty } from '@elastic/eui'; import { SAMPLE_SIZE_SETTING } from '../../../common'; import { DocTableProps, DocTableRenderProps, DocTableWrapper } from './doc_table_wrapper'; import { SkipBottomButton } from '../../application/main/components/skip_bottom_button'; -import { shouldLoadNextDocPatch } from './lib/should_load_next_doc_patch'; -import { useDiscoverServices } from '../../utils/use_discover_services'; +import { shouldLoadNextDocPatch } from './utils/should_load_next_doc_patch'; +import { useDiscoverServices } from '../../hooks/use_discover_services'; const FOOTER_PADDING = { padding: 0 }; diff --git a/src/plugins/discover/public/components/doc_table/doc_table_wrapper.tsx b/src/plugins/discover/public/components/doc_table/doc_table_wrapper.tsx index d636ed37f7f27..3562337961417 100644 --- a/src/plugins/discover/public/components/doc_table/doc_table_wrapper.tsx +++ b/src/plugins/discover/public/components/doc_table/doc_table_wrapper.tsx @@ -16,7 +16,7 @@ import { SortOrder } from './components/table_header/helpers'; import { DocTableRow, TableRow } from './components/table_row'; import { DocViewFilterFn } from '../../services/doc_views/doc_views_types'; import { getFieldsToShow } from '../../utils/get_fields_to_show'; -import { useDiscoverServices } from '../../utils/use_discover_services'; +import { useDiscoverServices } from '../../hooks/use_discover_services'; export interface DocTableProps { /** diff --git a/src/plugins/discover/public/components/doc_table/index.ts b/src/plugins/discover/public/components/doc_table/index.ts index 513183cc99468..e79276b0c9687 100644 --- a/src/plugins/discover/public/components/doc_table/index.ts +++ b/src/plugins/discover/public/components/doc_table/index.ts @@ -6,6 +6,6 @@ * Side Public License, v 1. */ -export { getSort, getSortArray } from './lib/get_sort'; -export { getSortForSearchSource } from './lib/get_sort_for_search_source'; -export { getDefaultSort } from './lib/get_default_sort'; +export { getSort, getSortArray } from './utils/get_sort'; +export { getSortForSearchSource } from './utils/get_sort_for_search_source'; +export { getDefaultSort } from './utils/get_default_sort'; diff --git a/src/plugins/discover/public/components/doc_table/lib/get_default_sort.test.ts b/src/plugins/discover/public/components/doc_table/utils/get_default_sort.test.ts similarity index 100% rename from src/plugins/discover/public/components/doc_table/lib/get_default_sort.test.ts rename to src/plugins/discover/public/components/doc_table/utils/get_default_sort.test.ts diff --git a/src/plugins/discover/public/components/doc_table/lib/get_default_sort.ts b/src/plugins/discover/public/components/doc_table/utils/get_default_sort.ts similarity index 100% rename from src/plugins/discover/public/components/doc_table/lib/get_default_sort.ts rename to src/plugins/discover/public/components/doc_table/utils/get_default_sort.ts diff --git a/src/plugins/discover/public/components/doc_table/lib/get_sort.test.ts b/src/plugins/discover/public/components/doc_table/utils/get_sort.test.ts similarity index 100% rename from src/plugins/discover/public/components/doc_table/lib/get_sort.test.ts rename to src/plugins/discover/public/components/doc_table/utils/get_sort.test.ts diff --git a/src/plugins/discover/public/components/doc_table/lib/get_sort.ts b/src/plugins/discover/public/components/doc_table/utils/get_sort.ts similarity index 100% rename from src/plugins/discover/public/components/doc_table/lib/get_sort.ts rename to src/plugins/discover/public/components/doc_table/utils/get_sort.ts diff --git a/src/plugins/discover/public/components/doc_table/lib/get_sort_for_search_source.test.ts b/src/plugins/discover/public/components/doc_table/utils/get_sort_for_search_source.test.ts similarity index 100% rename from src/plugins/discover/public/components/doc_table/lib/get_sort_for_search_source.test.ts rename to src/plugins/discover/public/components/doc_table/utils/get_sort_for_search_source.test.ts diff --git a/src/plugins/discover/public/components/doc_table/lib/get_sort_for_search_source.ts b/src/plugins/discover/public/components/doc_table/utils/get_sort_for_search_source.ts similarity index 100% rename from src/plugins/discover/public/components/doc_table/lib/get_sort_for_search_source.ts rename to src/plugins/discover/public/components/doc_table/utils/get_sort_for_search_source.ts diff --git a/src/plugins/discover/public/components/doc_table/lib/row_formatter.scss b/src/plugins/discover/public/components/doc_table/utils/row_formatter.scss similarity index 100% rename from src/plugins/discover/public/components/doc_table/lib/row_formatter.scss rename to src/plugins/discover/public/components/doc_table/utils/row_formatter.scss diff --git a/src/plugins/discover/public/components/doc_table/lib/row_formatter.test.ts b/src/plugins/discover/public/components/doc_table/utils/row_formatter.test.ts similarity index 100% rename from src/plugins/discover/public/components/doc_table/lib/row_formatter.test.ts rename to src/plugins/discover/public/components/doc_table/utils/row_formatter.test.ts diff --git a/src/plugins/discover/public/components/doc_table/lib/row_formatter.tsx b/src/plugins/discover/public/components/doc_table/utils/row_formatter.tsx similarity index 100% rename from src/plugins/discover/public/components/doc_table/lib/row_formatter.tsx rename to src/plugins/discover/public/components/doc_table/utils/row_formatter.tsx diff --git a/src/plugins/discover/public/components/doc_table/lib/should_load_next_doc_patch.test.ts b/src/plugins/discover/public/components/doc_table/utils/should_load_next_doc_patch.test.ts similarity index 100% rename from src/plugins/discover/public/components/doc_table/lib/should_load_next_doc_patch.test.ts rename to src/plugins/discover/public/components/doc_table/utils/should_load_next_doc_patch.test.ts diff --git a/src/plugins/discover/public/components/doc_table/lib/should_load_next_doc_patch.ts b/src/plugins/discover/public/components/doc_table/utils/should_load_next_doc_patch.ts similarity index 100% rename from src/plugins/discover/public/components/doc_table/lib/should_load_next_doc_patch.ts rename to src/plugins/discover/public/components/doc_table/utils/should_load_next_doc_patch.ts diff --git a/src/plugins/discover/public/embeddable/utils/update_search_source.ts b/src/plugins/discover/public/embeddable/utils/update_search_source.ts index 7f383b910b990..cba88c63c806b 100644 --- a/src/plugins/discover/public/embeddable/utils/update_search_source.ts +++ b/src/plugins/discover/public/embeddable/utils/update_search_source.ts @@ -8,7 +8,7 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import { ISearchSource } from '@kbn/data-plugin/public'; import { getSortForSearchSource } from '../../components/doc_table'; -import { SortPairArr } from '../../components/doc_table/lib/get_sort'; +import { SortPairArr } from '../../components/doc_table/utils/get_sort'; export const updateSearchSource = ( searchSource: ISearchSource, diff --git a/src/plugins/discover/public/utils/use_data_grid_columns.test.tsx b/src/plugins/discover/public/hooks/use_data_grid_columns.test.tsx similarity index 100% rename from src/plugins/discover/public/utils/use_data_grid_columns.test.tsx rename to src/plugins/discover/public/hooks/use_data_grid_columns.test.tsx diff --git a/src/plugins/discover/public/utils/use_data_grid_columns.ts b/src/plugins/discover/public/hooks/use_data_grid_columns.ts similarity index 100% rename from src/plugins/discover/public/utils/use_data_grid_columns.ts rename to src/plugins/discover/public/hooks/use_data_grid_columns.ts diff --git a/src/plugins/discover/public/utils/use_discover_services.ts b/src/plugins/discover/public/hooks/use_discover_services.ts similarity index 100% rename from src/plugins/discover/public/utils/use_discover_services.ts rename to src/plugins/discover/public/hooks/use_discover_services.ts diff --git a/src/plugins/discover/public/utils/use_es_doc_search.test.tsx b/src/plugins/discover/public/hooks/use_es_doc_search.test.tsx similarity index 100% rename from src/plugins/discover/public/utils/use_es_doc_search.test.tsx rename to src/plugins/discover/public/hooks/use_es_doc_search.test.tsx diff --git a/src/plugins/discover/public/utils/use_es_doc_search.ts b/src/plugins/discover/public/hooks/use_es_doc_search.ts similarity index 100% rename from src/plugins/discover/public/utils/use_es_doc_search.ts rename to src/plugins/discover/public/hooks/use_es_doc_search.ts diff --git a/src/plugins/discover/public/utils/use_index_pattern.test.tsx b/src/plugins/discover/public/hooks/use_index_pattern.test.tsx similarity index 100% rename from src/plugins/discover/public/utils/use_index_pattern.test.tsx rename to src/plugins/discover/public/hooks/use_index_pattern.test.tsx diff --git a/src/plugins/discover/public/utils/use_index_pattern.tsx b/src/plugins/discover/public/hooks/use_index_pattern.tsx similarity index 100% rename from src/plugins/discover/public/utils/use_index_pattern.tsx rename to src/plugins/discover/public/hooks/use_index_pattern.tsx diff --git a/src/plugins/discover/public/utils/use_navigation_props.test.tsx b/src/plugins/discover/public/hooks/use_navigation_props.test.tsx similarity index 100% rename from src/plugins/discover/public/utils/use_navigation_props.test.tsx rename to src/plugins/discover/public/hooks/use_navigation_props.test.tsx diff --git a/src/plugins/discover/public/utils/use_navigation_props.tsx b/src/plugins/discover/public/hooks/use_navigation_props.tsx similarity index 100% rename from src/plugins/discover/public/utils/use_navigation_props.tsx rename to src/plugins/discover/public/hooks/use_navigation_props.tsx diff --git a/src/plugins/discover/public/utils/use_pager.test.tsx b/src/plugins/discover/public/hooks/use_pager.test.tsx similarity index 100% rename from src/plugins/discover/public/utils/use_pager.test.tsx rename to src/plugins/discover/public/hooks/use_pager.test.tsx diff --git a/src/plugins/discover/public/utils/use_pager.ts b/src/plugins/discover/public/hooks/use_pager.ts similarity index 100% rename from src/plugins/discover/public/utils/use_pager.ts rename to src/plugins/discover/public/hooks/use_pager.ts diff --git a/src/plugins/discover/public/utils/use_row_heights_options.test.tsx b/src/plugins/discover/public/hooks/use_row_heights_options.test.tsx similarity index 100% rename from src/plugins/discover/public/utils/use_row_heights_options.test.tsx rename to src/plugins/discover/public/hooks/use_row_heights_options.test.tsx diff --git a/src/plugins/discover/public/utils/use_row_heights_options.ts b/src/plugins/discover/public/hooks/use_row_heights_options.ts similarity index 94% rename from src/plugins/discover/public/utils/use_row_heights_options.ts rename to src/plugins/discover/public/hooks/use_row_heights_options.ts index 8935d655002c8..d932ef3f75ef1 100644 --- a/src/plugins/discover/public/utils/use_row_heights_options.ts +++ b/src/plugins/discover/public/hooks/use_row_heights_options.ts @@ -9,9 +9,13 @@ import type { EuiDataGridRowHeightOption, EuiDataGridRowHeightsOptions } from '@elastic/eui'; import { useMemo } from 'react'; import { ROW_HEIGHT_OPTION } from '../../common'; -import { isValidRowHeight } from './validate_row_height'; +import { isValidRowHeight } from '../utils/validate_row_height'; import { useDiscoverServices } from './use_discover_services'; -import { DataGridOptionsRecord, getStoredRowHeight, updateStoredRowHeight } from './row_heights'; +import { + DataGridOptionsRecord, + getStoredRowHeight, + updateStoredRowHeight, +} from '../utils/row_heights'; interface UseRowHeightProps { rowHeightState?: number; diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 25bc46bf90c2b..24da9869238b0 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -54,7 +54,7 @@ import { DeferredSpinner } from './components'; import { ViewSavedSearchAction } from './embeddable/view_saved_search_action'; import { injectTruncateStyles } from './utils/truncate_styles'; import { DOC_TABLE_LEGACY, TRUNCATE_MAX_HEIGHT } from '../common'; -import { useDiscoverServices } from './utils/use_discover_services'; +import { useDiscoverServices } from './hooks/use_discover_services'; import { initializeKbnUrlTracking } from './utils/initialize_kbn_url_tracking'; const DocViewerLegacyTable = React.lazy( diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer.test.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer.test.tsx index 2d946db20dd03..6d0018f695a33 100644 --- a/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer.test.tsx +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer/doc_viewer.test.tsx @@ -32,7 +32,7 @@ jest.mock('../../../../kibana_services', () => { }; }); -jest.mock('../../../../utils/use_discover_services', () => { +jest.mock('../../../../hooks/use_discover_services', () => { return { useDiscoverServices: { uiSettings: { diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/source.test.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/source.test.tsx index 5076813ec423f..537cdf6d3dc4a 100644 --- a/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/source.test.tsx +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/source.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import type { DataView } from '@kbn/data-views-plugin/public'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { DocViewerSource } from './source'; -import * as hooks from '../../../../utils/use_es_doc_search'; +import * as hooks from '../../../../hooks/use_es_doc_search'; import * as useUiSettingHook from '@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting'; import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner } from '@elastic/eui'; import { JsonCodeEditorCommon } from '../../../../components/json_code_editor/json_code_editor_common'; diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/source.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/source.tsx index 5aa813e25fe98..cbc14c9382e1c 100644 --- a/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/source.tsx +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer_source/source.tsx @@ -13,10 +13,10 @@ import { monaco } from '@kbn/monaco'; import { EuiButton, EuiEmptyPrompt, EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DataView } from '@kbn/data-views-plugin/public'; -import { useDiscoverServices } from '../../../../utils/use_discover_services'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { JSONCodeEditorCommonMemoized } from '../../../../components/json_code_editor/json_code_editor_common'; import { DOC_TABLE_LEGACY, SEARCH_FIELDS_FROM_SOURCE } from '../../../../../common'; -import { useEsDocSearch } from '../../../../utils/use_es_doc_search'; +import { useEsDocSearch } from '../../../../hooks/use_es_doc_search'; import { ElasticRequestState } from '../../../../application/doc/types'; import { getHeight } from './get_height'; diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table.tsx index 7c8ad53fe641a..152a5451e7760 100644 --- a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table.tsx +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/legacy/table.tsx @@ -11,7 +11,7 @@ import React, { useCallback, useMemo } from 'react'; import { EuiInMemoryTable } from '@elastic/eui'; import { flattenHit } from '@kbn/data-plugin/public'; import { getTypeForFieldIcon } from '../../../../../utils/get_type_for_field_icon'; -import { useDiscoverServices } from '../../../../../utils/use_discover_services'; +import { useDiscoverServices } from '../../../../../hooks/use_discover_services'; import { SHOW_MULTIFIELDS } from '../../../../../../common'; import { DocViewRenderProps, FieldRecordLegacy } from '../../../doc_views_types'; import { ACTIONS_COLUMN, MAIN_COLUMNS } from './table_columns'; diff --git a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table.tsx b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table.tsx index 56673acfb1821..48a869c86d3e8 100644 --- a/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table.tsx +++ b/src/plugins/discover/public/services/doc_views/components/doc_viewer_table/table.tsx @@ -30,8 +30,8 @@ import { debounce } from 'lodash'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import { flattenHit } from '@kbn/data-plugin/public'; import { getTypeForFieldIcon } from '../../../../utils/get_type_for_field_icon'; -import { useDiscoverServices } from '../../../../utils/use_discover_services'; -import { usePager } from '../../../../utils/use_pager'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; +import { usePager } from '../../../../hooks/use_pager'; import { FieldName } from '../../../../components/field_name/field_name'; import { SHOW_MULTIFIELDS } from '../../../../../common'; import { DocViewRenderProps, FieldRecordLegacy } from '../../doc_views_types'; From 2aac7f9250af7a11b93b0f54f38770f7b3622c64 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 17 Jun 2022 16:03:30 +0100 Subject: [PATCH 23/30] skip flaky suite (#132399) --- .../apps/ml/data_frame_analytics/results_view_content.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/results_view_content.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/results_view_content.ts index 2bddf0a7d9512..50bf465425f54 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/results_view_content.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/results_view_content.ts @@ -268,7 +268,8 @@ export default function ({ getService }: FtrProviderContext) { }); for (const testData of testDataList) { - describe(`${testData.suiteTitle}`, function () { + // FLAKY: https://github.com/elastic/kibana/issues/132399 + describe.skip(`${testData.suiteTitle}`, function () { before(async () => { await ml.navigation.navigateToMl(); await ml.navigation.navigateToDataFrameAnalytics(); From 28915d29224b6b0dfe10100d8b05a0409a4cded1 Mon Sep 17 00:00:00 2001 From: Julia Rechkunova Date: Fri, 17 Jun 2022 17:07:22 +0200 Subject: [PATCH 24/30] [Discover] Make footer visible under Document Explorer if sample size is less than hits number (#134231) * [Discover] Make footer visible under Document Explorer if sample size is less than hits number * [Discover] Simplify changes diff * [Discover] Update copy * [Discover] Update copy id * [Discover] Remove other translations * [Discover] Add link to Advanced settings and tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../discover_grid/discover_grid.scss | 14 +++ .../discover_grid/discover_grid.tsx | 86 +++++++++++-------- .../apps/discover/_data_grid_pagination.ts | 66 ++++++++++++++ test/functional/apps/discover/index.ts | 1 + .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 7 files changed, 132 insertions(+), 38 deletions(-) create mode 100644 test/functional/apps/discover/_data_grid_pagination.ts diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid.scss b/src/plugins/discover/public/components/discover_grid/discover_grid.scss index 113bb60924850..71335e968db4c 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid.scss +++ b/src/plugins/discover/public/components/discover_grid/discover_grid.scss @@ -39,7 +39,21 @@ font-size: $euiFontSizeS; } +.dscDiscoverGrid__inner { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + height: 100%; +} + +.dscDiscoverGrid__table { + flex-grow: 1; + flex-shrink: 1; + min-height: 0; +} + .dscDiscoverGrid__footer { + flex-shrink: 0; background-color: $euiColorLightShade; padding: $euiSize / 2 $euiSize; margin-top: $euiSize / 4; diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid.tsx index d5d150fb68144..48fd2ba656746 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid.tsx @@ -7,6 +7,7 @@ */ import React, { useCallback, useMemo, useState, useRef } from 'react'; +import classnames from 'classnames'; import { FormattedMessage } from '@kbn/i18n-react'; import './discover_grid.scss'; import { @@ -19,6 +20,7 @@ import { EuiLoadingSpinner, EuiIcon, EuiDataGridRefProps, + EuiLink, } from '@elastic/eui'; import type { DataView } from '@kbn/data-views-plugin/public'; import { flattenHit } from '@kbn/data-plugin/public'; @@ -42,6 +44,7 @@ import { import { getDisplayedColumns } from '../../utils/columns'; import { DOC_HIDE_TIME_COLUMN_SETTING, + SAMPLE_SIZE_SETTING, MAX_DOC_FIELDS_DISPLAYED, SHOW_MULTIFIELDS, } from '../../../common'; @@ -484,44 +487,57 @@ export const DiscoverGrid = ({ valueToStringConverter, }} > - - - + +
+ +
{showDisclaimer && ( -

+

+ + + ), + }} /> - - -

)} {searchTitle && ( diff --git a/test/functional/apps/discover/_data_grid_pagination.ts b/test/functional/apps/discover/_data_grid_pagination.ts new file mode 100644 index 0000000000000..da9faa24bb151 --- /dev/null +++ b/test/functional/apps/discover/_data_grid_pagination.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const dataGrid = getService('dataGrid'); + const PageObjects = getPageObjects(['settings', 'common', 'discover', 'header', 'timePicker']); + const defaultSettings = { defaultIndex: 'logstash-*', 'doc_table:legacy': false }; + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + + describe('discover data grid pagination', function describeIndexTests() { + before(async () => { + await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); + }); + + after(async () => { + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + await kibanaServer.uiSettings.replace({}); + }); + + beforeEach(async function () { + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await kibanaServer.uiSettings.update(defaultSettings); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + }); + + it('should show pagination', async () => { + const rows = await dataGrid.getDocTableRows(); + expect(rows.length).to.be.above(0); + // pagination is present + await testSubjects.existOrFail('pagination-button-0'); // first page + await testSubjects.existOrFail('pagination-button-4'); // last page + await testSubjects.missingOrFail('pagination-button-5'); + }); + + it('should show footer only for the last page', async () => { + // footer is not shown + await testSubjects.missingOrFail('discoverTableFooter'); + // go to next page + await testSubjects.click('pagination-button-next'); + // footer is not shown yet + await retry.try(async function () { + await testSubjects.missingOrFail('discoverTableFooter'); + }); + // go to the last page + await testSubjects.click('pagination-button-4'); + // footer is shown now + await retry.try(async function () { + await testSubjects.existOrFail('discoverTableFooter'); + await testSubjects.existOrFail('discoverTableSampleSizeSettingsLink'); + }); + }); + }); +} diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts index 0ed69e974d5e6..db72e270fd337 100644 --- a/test/functional/apps/discover/index.ts +++ b/test/functional/apps/discover/index.ts @@ -53,6 +53,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_data_grid_row_navigation')); loadTestFile(require.resolve('./_data_grid_doc_table')); loadTestFile(require.resolve('./_data_grid_copy_to_clipboard')); + loadTestFile(require.resolve('./_data_grid_pagination')); loadTestFile(require.resolve('./_indexpattern_with_unmapped_fields')); loadTestFile(require.resolve('./_runtime_fields_editor')); loadTestFile(require.resolve('./_huge_fields')); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 8ece3d7c60386..4a1b4c39f5548 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -2952,7 +2952,6 @@ "discover.hitCountSpinnerAriaLabel": "Nombre final de résultats toujours en chargement", "discover.hitsPluralTitle": "{formattedHits} {hits, plural, one {résultat} other {résultats}}", "discover.howToSeeOtherMatchingDocumentsDescription": "Voici les {sampleSize} premiers documents correspondant à votre recherche. Veuillez affiner celle-ci pour en voir plus.", - "discover.howToSeeOtherMatchingDocumentsDescriptionGrid": "Voici les {sampleSize} premiers documents correspondant à votre recherche. Veuillez affiner celle-ci pour en voir plus.", "discover.inspectorRequestDataTitleChart": "Données du graphique", "discover.inspectorRequestDataTitleDocuments": "Documents", "discover.inspectorRequestDataTitleTotalHits": "Nombre total de résultats", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4b1f81af84000..5e3448cfa19c6 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3046,7 +3046,6 @@ "discover.hitCountSpinnerAriaLabel": "読み込み中の最終一致件数", "discover.hitsPluralTitle": "{formattedHits} {hits, plural, other {一致}}", "discover.howToSeeOtherMatchingDocumentsDescription": "これらは検索条件に一致した初めの {sampleSize} 件のドキュメントです。他の結果を表示するには検索条件を絞ってください。", - "discover.howToSeeOtherMatchingDocumentsDescriptionGrid": "これらは検索条件に一致した初めの {sampleSize} 件のドキュメントです。他の結果を表示するには検索条件を絞ってください。", "discover.inspectorRequestDataTitleChart": "グラフデータ", "discover.inspectorRequestDataTitleDocuments": "ドキュメント", "discover.inspectorRequestDataTitleTotalHits": "総ヒット数", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index fa9b9677ccbe1..465a3d78860af 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3056,7 +3056,6 @@ "discover.hitCountSpinnerAriaLabel": "最终命中计数仍在加载", "discover.hitsPluralTitle": "{formattedHits} 个{hits, plural, other {命中}}", "discover.howToSeeOtherMatchingDocumentsDescription": "下面是与您的搜索匹配的前 {sampleSize} 个文档,请优化您的搜索以查看其他文档。", - "discover.howToSeeOtherMatchingDocumentsDescriptionGrid": "下面是与您的搜索匹配的前 {sampleSize} 个文档,请优化您的搜索以查看其他文档。", "discover.inspectorRequestDataTitleChart": "图表数据", "discover.inspectorRequestDataTitleDocuments": "文档", "discover.inspectorRequestDataTitleTotalHits": "总命中数", From 5a727332c93d21a969961e5f13c7207b2b4b8ae7 Mon Sep 17 00:00:00 2001 From: Davis McPhee Date: Fri, 17 Jun 2022 12:31:40 -0300 Subject: [PATCH 25/30] [Discover] Improve document explorer timestamp tooltip accessibility (#134411) * [Discover] Made document explorer timestamp tooltip accessible * [Discover] Fixed position of tooltip that broke functional tests, updated time field aria label, updated test snapshots --- .../discover_grid_columns.test.tsx | 30 +++++++++++-------- .../discover_grid/discover_grid_columns.tsx | 26 ++++++++-------- .../__snapshots__/table_header.test.tsx.snap | 2 +- .../table_header/table_header_column.tsx | 8 +++-- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 7 files changed, 38 insertions(+), 31 deletions(-) diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx index bdefa126f07d8..0f32075cc4e66 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx @@ -281,20 +281,24 @@ describe('Discover grid columns', function () { [Function], [Function], ], - "display": - timestamp - - + - , + delay="regular" + display="inlineBlock" + position="top" + > + + timestamp + + + + +
, "id": "timestamp", "initialWidth": 210, "isSortable": true, diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_columns.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_columns.tsx index 1d4feefaec992..b72c68c9914ba 100644 --- a/src/plugins/discover/public/components/discover_grid/discover_grid_columns.tsx +++ b/src/plugins/discover/public/components/discover_grid/discover_grid_columns.tsx @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import React, { Fragment } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiDataGridColumn, EuiIconTip, EuiScreenReaderOnly } from '@elastic/eui'; +import { EuiDataGridColumn, EuiIcon, EuiScreenReaderOnly, EuiToolTip } from '@elastic/eui'; import type { DataView } from '@kbn/data-views-plugin/public'; import { ExpandButton } from './discover_grid_expand_button'; import { DiscoverGridSettings } from './types'; @@ -111,9 +111,13 @@ export function buildEuiGridColumn({ }; if (column.id === indexPattern.timeFieldName) { + const timeFieldName = indexPatternField?.customLabel ?? indexPattern.timeFieldName; const primaryTimeAriaLabel = i18n.translate( 'discover.docTable.tableHeader.timeFieldIconTooltipAriaLabel', - { defaultMessage: 'Primary time field.' } + { + defaultMessage: '{timeFieldName} - this field represents the time that events occurred.', + values: { timeFieldName }, + } ); const primaryTimeTooltip = i18n.translate( 'discover.docTable.tableHeader.timeFieldIconTooltip', @@ -123,15 +127,13 @@ export function buildEuiGridColumn({ ); column.display = ( - - {indexPatternField?.customLabel ?? indexPattern.timeFieldName}{' '} - - +
+ + <> + {timeFieldName} + + +
); column.initialWidth = defaultTimeColumnWidth; } diff --git a/src/plugins/discover/public/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap b/src/plugins/discover/public/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap index 612973fe37a48..39f52a52138d0 100644 --- a/src/plugins/discover/public/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap +++ b/src/plugins/discover/public/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap @@ -23,7 +23,7 @@ exports[`TableHeader with time column renders correctly 1`] = ` data-euiicon-type="clock" tabindex="0" > - Primary time field. + time - this field represents the time that events occurred. pair[0] === name); const curColSortDir = (curColSort && curColSort[1]) || ''; + const fieldName = customLabel ?? displayName; const timeAriaLabel = i18n.translate( 'discover.docTable.tableHeader.timeFieldIconTooltipAriaLabel', - { defaultMessage: 'Primary time field.' } + { + defaultMessage: '{timeFieldName} - this field represents the time that events occurred.', + values: { timeFieldName: fieldName }, + } ); const timeTooltip = i18n.translate('discover.docTable.tableHeader.timeFieldIconTooltip', { defaultMessage: 'This field represents the time that events occurred.', @@ -195,7 +199,7 @@ export function TableHeaderColumn({ {showScoreSortWarning && } - {customLabel ?? displayName} + {fieldName} {isTimeColumn && ( Date: Fri, 17 Jun 2022 17:37:11 +0200 Subject: [PATCH 26/30] unskip test (#134549) --- .../synthetics/public/legacy_uptime/pages/overview.test.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/pages/overview.test.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/pages/overview.test.tsx index 30ea0e361580a..b3aa4714fa664 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/pages/overview.test.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/pages/overview.test.tsx @@ -9,8 +9,7 @@ import React from 'react'; import { OverviewPageComponent } from './overview'; import { render } from '../lib/helper/rtl_helpers'; -// FLAKY: https://github.com/elastic/kibana/issues/131346 -describe.skip('MonitorPage', () => { +describe('MonitorPage', () => { it('renders expected elements for valid props', async () => { const { findByText, findByPlaceholderText } = render(); From 182849fa7b931635624a49b93c1e99bf144e0c09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Fri, 17 Jun 2022 17:38:03 +0200 Subject: [PATCH 27/30] [DOCS] Adds important note about allowlisting email addresses to email connector docs (#134552) --- docs/management/connectors/action-types/email.asciidoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/management/connectors/action-types/email.asciidoc b/docs/management/connectors/action-types/email.asciidoc index 1c6c30526e0bb..f926d86e7767f 100644 --- a/docs/management/connectors/action-types/email.asciidoc +++ b/docs/management/connectors/action-types/email.asciidoc @@ -228,6 +228,10 @@ is considered `false`. Typically, `port: 465` uses `secure: true`, and [[elasticcloud]] ==== Sending email from Elastic Cloud +IMPORTANT: To receive notifications, the email addresses must be added to an + link:{cloud}/ec-watcher.html#ec-watcher-allowlist[allowlist] in the + Elasticsearch Service Console. + Use the preconfigured email connector (`Elastic-Cloud-SMTP`) to send emails from Elastic Cloud. From 0294ed4d3cec25ccc5cf9991eeff412850e9a0be Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Fri, 17 Jun 2022 17:30:18 +0100 Subject: [PATCH 28/30] [ML] Fix advanced wizard detector combo box (#134666) --- .../advanced_detector_modal.tsx | 28 ++++++------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx index fc82184341c02..94bface8317a2 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx @@ -240,7 +240,7 @@ export const AdvancedDetectorModal: FC = ({ @@ -251,7 +251,7 @@ export const AdvancedDetectorModal: FC = ({ = ({ = ({ = ({ = ({ Date: Fri, 17 Jun 2022 17:39:18 +0100 Subject: [PATCH 29/30] skip flaky suite (#134430) --- 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 3a33c95edba42..a86be2bbaaa8d 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 @@ -135,7 +135,8 @@ export default function ({ getService }: FtrProviderContext) { }); for (const testData of testDataList) { - describe(`${testData.suiteTitle}`, function () { + // FLAKY: https://github.com/elastic/kibana/issues/134430 + describe.skip(`${testData.suiteTitle}`, function () { const cloneJobId = `${testData.job.id}_clone`; const cloneDestIndex = `${testData.job!.dest!.index}_clone`; From 62e9b8b956bdbc17285d00db55cf98f4438ae55c Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Fri, 17 Jun 2022 17:59:50 +0100 Subject: [PATCH 30/30] [Security Solution] Use observable to update deepLinks when setting changes (#134577) * use observable to update deepLinks when setting changes * change advanced setting texts * test fix. app links needs always to be defined * fix home page bug --- .../public/app/deep_links/index.ts | 6 +- .../security_solution/public/plugin.tsx | 92 ++++++++----------- .../security_solution/server/ui_settings.ts | 7 +- 3 files changed, 43 insertions(+), 62 deletions(-) diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index 8b4b70d052898..da1405497e2bb 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -11,7 +11,7 @@ import { get } from 'lodash'; import { LicenseType } from '@kbn/licensing-plugin/common/types'; import { getCasesDeepLinks } from '@kbn/cases-plugin/public'; import { AppDeepLink, AppNavLinkStatus, AppUpdater, Capabilities } from '@kbn/core/public'; -import { Subject } from 'rxjs'; +import { Subject, Subscription } from 'rxjs'; import { SecurityPageName } from '../types'; import { OVERVIEW, @@ -561,8 +561,8 @@ const formatDeepLinks = (appLinks: AppLinkItems): AppDeepLink[] => /** * Registers any change in appLinks to be updated in app deepLinks */ -export const registerDeepLinksUpdater = (appUpdater$: Subject) => { - subscribeAppLinks((appLinks) => { +export const registerDeepLinksUpdater = (appUpdater$: Subject): Subscription => { + return subscribeAppLinks((appLinks) => { appUpdater$.next(() => ({ navLinkStatus: AppNavLinkStatus.hidden, // needed to prevent main security link to switch to visible after update deepLinks: formatDeepLinks(appLinks), diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 3f1d24d4f8bbf..72ba80e182547 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import reduceReducers from 'reduce-reducers'; import { BehaviorSubject, Subject, Subscription } from 'rxjs'; -import { pluck } from 'rxjs/operators'; +import { combineLatestWith, pluck } from 'rxjs/operators'; import { AnyAction, Reducer } from 'redux'; import { AppMountParameters, @@ -49,7 +49,7 @@ import { } from '../common/constants'; import { getDeepLinks, registerDeepLinksUpdater } from './app/deep_links'; -import { AppLinkItems, LinksPermissions, subscribeAppLinks, updateAppLinks } from './common/links'; +import { LinksPermissions, updateAppLinks } from './common/links'; import { getSubPluginRoutesByCapabilities, manageOldSiemRoutes } from './helpers'; import { SecurityAppStore } from './common/store/store'; import { licenseService } from './common/hooks/use_license'; @@ -81,7 +81,6 @@ export class Plugin implements IPlugin(); private storage = new Storage(localStorage); - private licensingSubscription: Subscription | null = null; /** * Lazily instantiated subPlugins. @@ -141,7 +140,8 @@ export class Plugin implements IPlugin