diff --git a/.sass-lint.yml b/.sass-lint.yml index d6eaaf391de1a2..9eed50602f5205 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -1,6 +1,7 @@ files: include: - 'src/legacy/core_plugins/metrics/**/*.s+(a|c)ss' + - 'src/plugins/index_pattern_management/**/*.s+(a|c)ss' - 'src/plugins/timelion/**/*.s+(a|c)ss' - 'src/plugins/vis_type_vislib/**/*.s+(a|c)ss' - 'src/plugins/vis_type_vega/**/*.s+(a|c)ss' diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index a03b1b74fc1ac2..842f90b7047c89 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -81,6 +81,7 @@ readonly links: { readonly loadingData: string; readonly introduction: string; }; + readonly addData: string; readonly kibana: string; readonly siem: { readonly guide: string; diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.abortcontroller.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.abortcontroller.md deleted file mode 100644 index 0451a2254dc403..00000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.abortcontroller.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [abortController](./kibana-plugin-plugins-data-public.searchinterceptor.abortcontroller.md) - -## SearchInterceptor.abortController property - -`abortController` used to signal all searches to abort. - -Signature: - -```typescript -protected abortController: AbortController; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.getpendingcount_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.getpendingcount_.md index db2c5d6957ad74..ef36b3f37b0c7c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.getpendingcount_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.getpendingcount_.md @@ -2,12 +2,16 @@ [Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [getPendingCount$](./kibana-plugin-plugins-data-public.searchinterceptor.getpendingcount_.md) -## SearchInterceptor.getPendingCount$ property +## SearchInterceptor.getPendingCount$() method Returns an `Observable` over the current number of pending searches. This could mean that one of the search requests is still in flight, or that it has only received partial responses. Signature: ```typescript -getPendingCount$: () => Observable; +getPendingCount$(): Observable; ``` +Returns: + +`Observable` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.hidetoast.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.hidetoast.md deleted file mode 100644 index 59938a755a99e4..00000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.hidetoast.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [hideToast](./kibana-plugin-plugins-data-public.searchinterceptor.hidetoast.md) - -## SearchInterceptor.hideToast property - -Signature: - -```typescript -protected hideToast: () => void; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.longrunningtoast.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.longrunningtoast.md deleted file mode 100644 index 5799039de91bc3..00000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.longrunningtoast.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [longRunningToast](./kibana-plugin-plugins-data-public.searchinterceptor.longrunningtoast.md) - -## SearchInterceptor.longRunningToast property - -The current long-running toast (if there is one). - -Signature: - -```typescript -protected longRunningToast?: Toast; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md index b3b7da05326d06..32954927504aea 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md @@ -20,22 +20,15 @@ export declare class SearchInterceptor | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| [abortController](./kibana-plugin-plugins-data-public.searchinterceptor.abortcontroller.md) | | AbortController | abortController used to signal all searches to abort. | | [deps](./kibana-plugin-plugins-data-public.searchinterceptor.deps.md) | | SearchInterceptorDeps | | -| [getPendingCount$](./kibana-plugin-plugins-data-public.searchinterceptor.getpendingcount_.md) | | () => Observable<number> | Returns an Observable over the current number of pending searches. This could mean that one of the search requests is still in flight, or that it has only received partial responses. | -| [hideToast](./kibana-plugin-plugins-data-public.searchinterceptor.hidetoast.md) | | () => void | | -| [longRunningToast](./kibana-plugin-plugins-data-public.searchinterceptor.longrunningtoast.md) | | Toast | The current long-running toast (if there is one). | -| [pendingCount](./kibana-plugin-plugins-data-public.searchinterceptor.pendingcount.md) | | number | The number of pending search requests. | -| [pendingCount$](./kibana-plugin-plugins-data-public.searchinterceptor.pendingcount_.md) | | BehaviorSubject<number> | Observable that emits when the number of pending requests changes. | | [requestTimeout](./kibana-plugin-plugins-data-public.searchinterceptor.requesttimeout.md) | | number | undefined | | -| [showToast](./kibana-plugin-plugins-data-public.searchinterceptor.showtoast.md) | | () => void | | -| [timeoutSubscriptions](./kibana-plugin-plugins-data-public.searchinterceptor.timeoutsubscriptions.md) | | Subscription | The subscriptions from scheduling the automatic timeout for each request. | ## Methods | Method | Modifiers | Description | | --- | --- | --- | +| [getPendingCount$()](./kibana-plugin-plugins-data-public.searchinterceptor.getpendingcount_.md) | | Returns an Observable over the current number of pending searches. This could mean that one of the search requests is still in flight, or that it has only received partial responses. | | [runSearch(request, signal, strategy)](./kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md) | | | -| [search(request, options)](./kibana-plugin-plugins-data-public.searchinterceptor.search.md) | | Searches using the given search method. Overrides the AbortSignal with one that will abort either when cancelPending is called, when the request times out, or when the original AbortSignal is aborted. Updates the pendingCount when the request is started/finalized. | +| [search(request, options)](./kibana-plugin-plugins-data-public.searchinterceptor.search.md) | | Searches using the given search method. Overrides the AbortSignal with one that will abort either when cancelPending is called, when the request times out, or when the original AbortSignal is aborted. Updates pendingCount$ when the request is started/finalized. | | [setupTimers(options)](./kibana-plugin-plugins-data-public.searchinterceptor.setuptimers.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.pendingcount.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.pendingcount.md deleted file mode 100644 index 7dd2bd3e6703fc..00000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.pendingcount.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [pendingCount](./kibana-plugin-plugins-data-public.searchinterceptor.pendingcount.md) - -## SearchInterceptor.pendingCount property - -The number of pending search requests. - -Signature: - -```typescript -protected pendingCount: number; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.pendingcount_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.pendingcount_.md deleted file mode 100644 index dad0fca0bfe08d..00000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.pendingcount_.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [pendingCount$](./kibana-plugin-plugins-data-public.searchinterceptor.pendingcount_.md) - -## SearchInterceptor.pendingCount$ property - -Observable that emits when the number of pending requests changes. - -Signature: - -```typescript -protected pendingCount$: BehaviorSubject; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.search.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.search.md index 38ddda7b4e184f..1752d183a87377 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.search.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.search.md @@ -4,7 +4,7 @@ ## SearchInterceptor.search() method -Searches using the given `search` method. Overrides the `AbortSignal` with one that will abort either when `cancelPending` is called, when the request times out, or when the original `AbortSignal` is aborted. Updates the `pendingCount` when the request is started/finalized. +Searches using the given `search` method. Overrides the `AbortSignal` with one that will abort either when `cancelPending` is called, when the request times out, or when the original `AbortSignal` is aborted. Updates `pendingCount$` when the request is started/finalized. Signature: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.showtoast.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.showtoast.md deleted file mode 100644 index e495c72b57215d..00000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.showtoast.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [showToast](./kibana-plugin-plugins-data-public.searchinterceptor.showtoast.md) - -## SearchInterceptor.showToast property - -Signature: - -```typescript -protected showToast: () => void; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.timeoutsubscriptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.timeoutsubscriptions.md deleted file mode 100644 index 12f200e0377844..00000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.timeoutsubscriptions.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [timeoutSubscriptions](./kibana-plugin-plugins-data-public.searchinterceptor.timeoutsubscriptions.md) - -## SearchInterceptor.timeoutSubscriptions property - -The subscriptions from scheduling the automatic timeout for each request. - -Signature: - -```typescript -protected timeoutSubscriptions: Subscription; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.http.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.http.md index 1146179c13d63b..66c31bb6fcf805 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.http.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.http.md @@ -7,5 +7,5 @@ Signature: ```typescript -http: CoreStart['http']; +http: CoreSetup['http']; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md index 1291af5359887d..63eb67ce48246c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md @@ -14,9 +14,9 @@ export interface SearchInterceptorDeps | Property | Type | Description | | --- | --- | --- | -| [application](./kibana-plugin-plugins-data-public.searchinterceptordeps.application.md) | ApplicationStart | | -| [http](./kibana-plugin-plugins-data-public.searchinterceptordeps.http.md) | CoreStart['http'] | | -| [toasts](./kibana-plugin-plugins-data-public.searchinterceptordeps.toasts.md) | ToastsStart | | -| [uiSettings](./kibana-plugin-plugins-data-public.searchinterceptordeps.uisettings.md) | CoreStart['uiSettings'] | | +| [http](./kibana-plugin-plugins-data-public.searchinterceptordeps.http.md) | CoreSetup['http'] | | +| [startServices](./kibana-plugin-plugins-data-public.searchinterceptordeps.startservices.md) | Promise<[CoreStart, any, unknown]> | | +| [toasts](./kibana-plugin-plugins-data-public.searchinterceptordeps.toasts.md) | ToastsSetup | | +| [uiSettings](./kibana-plugin-plugins-data-public.searchinterceptordeps.uisettings.md) | CoreSetup['uiSettings'] | | | [usageCollector](./kibana-plugin-plugins-data-public.searchinterceptordeps.usagecollector.md) | SearchUsageCollector | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.application.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.startservices.md similarity index 61% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.application.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.startservices.md index a8cd1b170a595e..855d0652058b87 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.application.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.startservices.md @@ -1,11 +1,11 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) > [application](./kibana-plugin-plugins-data-public.searchinterceptordeps.application.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) > [startServices](./kibana-plugin-plugins-data-public.searchinterceptordeps.startservices.md) -## SearchInterceptorDeps.application property +## SearchInterceptorDeps.startServices property Signature: ```typescript -application: ApplicationStart; +startServices: Promise<[CoreStart, any, unknown]>; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.toasts.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.toasts.md index 0023b34af10c37..1f560dfa5cf7c3 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.toasts.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.toasts.md @@ -7,5 +7,5 @@ Signature: ```typescript -toasts: ToastsStart; +toasts: ToastsSetup; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.uisettings.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.uisettings.md index 425e177ec9300a..a34d223c34ac2b 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.uisettings.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.uisettings.md @@ -7,5 +7,5 @@ Signature: ```typescript -uiSettings: CoreStart['uiSettings']; +uiSettings: CoreSetup['uiSettings']; ``` diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index bffb3f97cd1b9c..f750784c47043b 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -97,7 +97,7 @@ Using a wildcard is the more popular approach. comparisons. + Kibana reads the index mapping and lists all fields that contain a timestamp. If your -index doesn't have time-based data, choose *I don't want to use the Time Filter*. +index doesn't have time-based data, choose *I don't want to use the time filter*. + You must select a time field to use global time filters on your dashboards. diff --git a/examples/embeddable_examples/kibana.json b/examples/embeddable_examples/kibana.json index 771c19cfdbd3dd..0ac40ae1889de1 100644 --- a/examples/embeddable_examples/kibana.json +++ b/examples/embeddable_examples/kibana.json @@ -4,7 +4,7 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "requiredPlugins": ["embeddable", "uiActions"], + "requiredPlugins": ["embeddable", "uiActions", "dashboard"], "optionalPlugins": [], "extraPublicDirs": ["public/todo", "public/hello_world", "public/todo/todo_ref_embeddable"], "requiredBundles": ["kibanaReact"] diff --git a/examples/embeddable_examples/public/book/add_book_to_library_action.tsx b/examples/embeddable_examples/public/book/add_book_to_library_action.tsx new file mode 100644 index 00000000000000..b74a1d56429825 --- /dev/null +++ b/examples/embeddable_examples/public/book/add_book_to_library_action.tsx @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { createAction, IncompatibleActionError } from '../../../../src/plugins/ui_actions/public'; +import { BookEmbeddable, BOOK_EMBEDDABLE } from './book_embeddable'; +import { ViewMode, isReferenceOrValueEmbeddable } from '../../../../src/plugins/embeddable/public'; + +interface ActionContext { + embeddable: BookEmbeddable; +} + +export const ACTION_ADD_BOOK_TO_LIBRARY = 'ACTION_ADD_BOOK_TO_LIBRARY'; + +export const createAddBookToLibraryAction = () => + createAction({ + getDisplayName: () => + i18n.translate('embeddableExamples.book.addToLibrary', { + defaultMessage: 'Add Book To Library', + }), + type: ACTION_ADD_BOOK_TO_LIBRARY, + order: 100, + getIconType: () => 'folderCheck', + isCompatible: async ({ embeddable }: ActionContext) => { + return ( + embeddable.type === BOOK_EMBEDDABLE && + embeddable.getInput().viewMode === ViewMode.EDIT && + isReferenceOrValueEmbeddable(embeddable) && + !embeddable.inputIsRefType(embeddable.getInput()) + ); + }, + execute: async ({ embeddable }: ActionContext) => { + if (!isReferenceOrValueEmbeddable(embeddable)) { + throw new IncompatibleActionError(); + } + const newInput = await embeddable.getInputAsRefType(); + embeddable.updateInput(newInput); + }, + }); diff --git a/examples/embeddable_examples/public/book/book_component.tsx b/examples/embeddable_examples/public/book/book_component.tsx index 064e13c131a0a7..e46487641b913e 100644 --- a/examples/embeddable_examples/public/book/book_component.tsx +++ b/examples/embeddable_examples/public/book/book_component.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { EuiFlexItem, EuiFlexGroup, EuiIcon } from '@elastic/eui'; import { EuiText } from '@elastic/eui'; -import { EuiFlexGrid } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { withEmbeddableSubscription } from '../../../../src/plugins/embeddable/public'; import { BookEmbeddableInput, BookEmbeddableOutput, BookEmbeddable } from './book_embeddable'; @@ -44,26 +44,32 @@ function wrapSearchTerms(task?: string, search?: string) { ); } -export function BookEmbeddableComponentInner({ input: { search }, output: { attributes } }: Props) { +export function BookEmbeddableComponentInner({ + input: { search }, + output: { attributes }, + embeddable, +}: Props) { const title = attributes?.title; const author = attributes?.author; const readIt = attributes?.readIt; + const byReference = embeddable.inputIsRefType(embeddable.getInput()); + return ( - + {title ? ( -

{wrapSearchTerms(title, search)},

+

{wrapSearchTerms(title, search)}

) : null} {author ? ( -
-{wrapSearchTerms(author, search)}
+ -{wrapSearchTerms(author, search)}
) : null} @@ -76,7 +82,21 @@ export function BookEmbeddableComponentInner({ input: { search }, output: { attr
)} - +
+ + + + {' '} + + {byReference + ? i18n.translate('embeddableExamples.book.byReferenceLabel', { + defaultMessage: 'Book is By Reference', + }) + : i18n.translate('embeddableExamples.book.byValueLabel', { + defaultMessage: 'Book is By Value', + })} + + ); diff --git a/examples/embeddable_examples/public/book/book_embeddable.tsx b/examples/embeddable_examples/public/book/book_embeddable.tsx index d49bd3280d97d9..dd9418c0e8596d 100644 --- a/examples/embeddable_examples/public/book/book_embeddable.tsx +++ b/examples/embeddable_examples/public/book/book_embeddable.tsx @@ -25,10 +25,11 @@ import { IContainer, EmbeddableOutput, SavedObjectEmbeddableInput, - AttributeService, + ReferenceOrValueEmbeddable, } from '../../../../src/plugins/embeddable/public'; import { BookSavedObjectAttributes } from '../../common'; import { BookEmbeddableComponent } from './book_component'; +import { AttributeService } from '../../../../src/plugins/dashboard/public'; export const BOOK_EMBEDDABLE = 'book'; export type BookEmbeddableInput = BookByValueInput | BookByReferenceInput; @@ -59,7 +60,8 @@ function getHasMatch(search?: string, savedAttributes?: BookSavedObjectAttribute ); } -export class BookEmbeddable extends Embeddable { +export class BookEmbeddable extends Embeddable + implements ReferenceOrValueEmbeddable { public readonly type = BOOK_EMBEDDABLE; private subscription: Subscription; private node?: HTMLElement; @@ -96,6 +98,18 @@ export class BookEmbeddable extends Embeddable { + return this.attributeService.inputIsRefType(input); + }; + + getInputAsValueType = async (): Promise => { + return this.attributeService.getInputAsValueType(this.input); + }; + + getInputAsRefType = async (): Promise => { + return this.attributeService.getInputAsRefType(this.input, { showSaveModal: true }); + }; + public render(node: HTMLElement) { if (this.node) { ReactDOM.unmountComponentAtNode(this.node); @@ -113,6 +127,10 @@ export class BookEmbeddable extends Embeddable(this.type); } - return this.attributeService; + return this.attributeService!; } } diff --git a/examples/embeddable_examples/public/book/edit_book_action.tsx b/examples/embeddable_examples/public/book/edit_book_action.tsx index 222f70e0be60f7..b31d69696598e9 100644 --- a/examples/embeddable_examples/public/book/edit_book_action.tsx +++ b/examples/embeddable_examples/public/book/edit_book_action.tsx @@ -22,11 +22,7 @@ import { i18n } from '@kbn/i18n'; import { BookSavedObjectAttributes, BOOK_SAVED_OBJECT } from '../../common'; import { createAction } from '../../../../src/plugins/ui_actions/public'; import { toMountPoint } from '../../../../src/plugins/kibana_react/public'; -import { - ViewMode, - EmbeddableStart, - SavedObjectEmbeddableInput, -} from '../../../../src/plugins/embeddable/public'; +import { ViewMode, SavedObjectEmbeddableInput } from '../../../../src/plugins/embeddable/public'; import { BookEmbeddable, BOOK_EMBEDDABLE, @@ -34,10 +30,11 @@ import { BookByValueInput, } from './book_embeddable'; import { CreateEditBookComponent } from './create_edit_book_component'; +import { DashboardStart } from '../../../../src/plugins/dashboard/public'; interface StartServices { openModal: OverlayStart['openModal']; - getAttributeService: EmbeddableStart['getAttributeService']; + getAttributeService: DashboardStart['getAttributeService']; } interface ActionContext { diff --git a/examples/embeddable_examples/public/book/unlink_book_from_library_action.tsx b/examples/embeddable_examples/public/book/unlink_book_from_library_action.tsx new file mode 100644 index 00000000000000..cef77092a642ac --- /dev/null +++ b/examples/embeddable_examples/public/book/unlink_book_from_library_action.tsx @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { createAction, IncompatibleActionError } from '../../../../src/plugins/ui_actions/public'; +import { BookEmbeddable, BOOK_EMBEDDABLE } from './book_embeddable'; +import { ViewMode, isReferenceOrValueEmbeddable } from '../../../../src/plugins/embeddable/public'; + +interface ActionContext { + embeddable: BookEmbeddable; +} + +export const ACTION_UNLINK_BOOK_FROM_LIBRARY = 'ACTION_UNLINK_BOOK_FROM_LIBRARY'; + +export const createUnlinkBookFromLibraryAction = () => + createAction({ + getDisplayName: () => + i18n.translate('embeddableExamples.book.unlinkFromLibrary', { + defaultMessage: 'Unlink Book from Library Item', + }), + type: ACTION_UNLINK_BOOK_FROM_LIBRARY, + order: 100, + getIconType: () => 'folderExclamation', + isCompatible: async ({ embeddable }: ActionContext) => { + return ( + embeddable.type === BOOK_EMBEDDABLE && + embeddable.getInput().viewMode === ViewMode.EDIT && + isReferenceOrValueEmbeddable(embeddable) && + embeddable.inputIsRefType(embeddable.getInput()) + ); + }, + execute: async ({ embeddable }: ActionContext) => { + if (!isReferenceOrValueEmbeddable(embeddable)) { + throw new IncompatibleActionError(); + } + const newInput = await embeddable.getInputAsValueType(); + embeddable.updateInput(newInput); + }, + }); diff --git a/examples/embeddable_examples/public/plugin.ts b/examples/embeddable_examples/public/plugin.ts index 95f4f5b41e198b..0c6ed1eb3be488 100644 --- a/examples/embeddable_examples/public/plugin.ts +++ b/examples/embeddable_examples/public/plugin.ts @@ -58,6 +58,15 @@ import { BookEmbeddableFactoryDefinition, } from './book/book_embeddable_factory'; import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; +import { + ACTION_ADD_BOOK_TO_LIBRARY, + createAddBookToLibraryAction, +} from './book/add_book_to_library_action'; +import { DashboardStart } from '../../../src/plugins/dashboard/public'; +import { + ACTION_UNLINK_BOOK_FROM_LIBRARY, + createUnlinkBookFromLibraryAction, +} from './book/unlink_book_from_library_action'; export interface EmbeddableExamplesSetupDependencies { embeddable: EmbeddableSetup; @@ -66,6 +75,7 @@ export interface EmbeddableExamplesSetupDependencies { export interface EmbeddableExamplesStartDependencies { embeddable: EmbeddableStart; + dashboard: DashboardStart; } interface ExampleEmbeddableFactories { @@ -86,6 +96,8 @@ export interface EmbeddableExamplesStart { declare module '../../../src/plugins/ui_actions/public' { export interface ActionContextMapping { [ACTION_EDIT_BOOK]: { embeddable: BookEmbeddable }; + [ACTION_ADD_BOOK_TO_LIBRARY]: { embeddable: BookEmbeddable }; + [ACTION_UNLINK_BOOK_FROM_LIBRARY]: { embeddable: BookEmbeddable }; } } @@ -144,17 +156,25 @@ export class EmbeddableExamplesPlugin this.exampleEmbeddableFactories.getBookEmbeddableFactory = deps.embeddable.registerEmbeddableFactory( BOOK_EMBEDDABLE, new BookEmbeddableFactoryDefinition(async () => ({ - getAttributeService: (await core.getStartServices())[1].embeddable.getAttributeService, + getAttributeService: (await core.getStartServices())[1].dashboard.getAttributeService, openModal: (await core.getStartServices())[0].overlays.openModal, })) ); const editBookAction = createEditBookAction(async () => ({ - getAttributeService: (await core.getStartServices())[1].embeddable.getAttributeService, + getAttributeService: (await core.getStartServices())[1].dashboard.getAttributeService, openModal: (await core.getStartServices())[0].overlays.openModal, })); deps.uiActions.registerAction(editBookAction); deps.uiActions.attachAction(CONTEXT_MENU_TRIGGER, editBookAction.id); + + const addBookToLibraryAction = createAddBookToLibraryAction(); + deps.uiActions.registerAction(addBookToLibraryAction); + deps.uiActions.attachAction(CONTEXT_MENU_TRIGGER, addBookToLibraryAction.id); + + const unlinkBookFromLibraryAction = createUnlinkBookFromLibraryAction(); + deps.uiActions.registerAction(unlinkBookFromLibraryAction); + deps.uiActions.attachAction(CONTEXT_MENU_TRIGGER, unlinkBookFromLibraryAction.id); } public start( diff --git a/package.json b/package.json index becd670e4ddcf5..200aa41743f51c 100644 --- a/package.json +++ b/package.json @@ -276,7 +276,6 @@ "url-loader": "2.2.0", "uuid": "3.3.2", "val-loader": "^1.1.1", - "validate-npm-package-name": "2.2.2", "vega": "^5.13.0", "vega-lite": "^4.13.1", "vega-schema-url-parser": "^1.1.0", diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index ee141e1d8ab7a4..e411dcd472768d 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -97,8 +97,6 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(511); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["buildProductionProjects"]; }); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); - /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(145); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "getProjects", function() { return _utils_projects__WEBPACK_IMPORTED_MODULE_2__["getProjects"]; }); @@ -15108,7 +15106,7 @@ function getMarkerLines(loc, source, opts) { column: 0, line: -1 }, loc.start); - const endLoc = Object.assign({}, startLoc, {}, loc.end); + const endLoc = Object.assign({}, startLoc, loc.end); const { linesAbove = 2, linesBelow = 3 @@ -15530,7 +15528,7 @@ function isIdentifierName(name) { } } - return true; + return !isFirst; } /***/ }), @@ -59477,9 +59475,6 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(512); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _build_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildProductionProjects"]; }); -/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(748); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); - /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -59500,7 +59495,6 @@ __webpack_require__.r(__webpack_exports__); */ - /***/ }), /* 512 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { @@ -90331,71 +90325,5 @@ NestedError.prototype.name = 'NestedError'; module.exports = NestedError; -/***/ }), -/* 748 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return prepareExternalProjectDependencies; }); -/* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(164); -/* harmony import */ var _utils_project__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(163); -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - - -/** - * All external projects are located within `./plugins/{plugin}` relative - * to the Kibana root directory or `../kibana-extra/{plugin}` relative - * to Kibana itself. - */ - -const isKibanaDep = depVersion => // For ../kibana-extra/ directory (legacy only) -depVersion.includes('../../kibana/packages/') || // For plugins/ directory -depVersion.includes('../../packages/'); -/** - * This prepares the dependencies for an _external_ project. - */ - - -async function prepareExternalProjectDependencies(projectPath) { - const project = await _utils_project__WEBPACK_IMPORTED_MODULE_1__["Project"].fromPath(projectPath); - - if (!project.hasDependencies()) { - return; - } - - const deps = project.allDependencies; - - for (const depName of Object.keys(deps)) { - const depVersion = deps[depName]; // Kibana currently only supports `link:` dependencies on Kibana's own - // packages, as these are packaged into the `node_modules` folder when - // Kibana is built, so we don't need to take any action to enable - // `require(...)` to resolve for these packages. - - if (Object(_utils_package_json__WEBPACK_IMPORTED_MODULE_0__["isLinkDependency"])(depVersion) && !isKibanaDep(depVersion)) { - // For non-Kibana packages we need to set up symlinks during the - // installation process, but this is not something we support yet. - throw new Error('This plugin is using `link:` dependencies for non-Kibana packages'); - } - } -} - /***/ }) /******/ ]); \ No newline at end of file diff --git a/packages/kbn-pm/src/index.ts b/packages/kbn-pm/src/index.ts index 0aa58adb4382fc..27ce0a417fdebf 100644 --- a/packages/kbn-pm/src/index.ts +++ b/packages/kbn-pm/src/index.ts @@ -18,7 +18,7 @@ */ export { run } from './cli'; -export { buildProductionProjects, prepareExternalProjectDependencies } from './production'; +export { buildProductionProjects } from './production'; export { getProjects } from './utils/projects'; export { Project } from './utils/project'; export { copyWorkspacePackages } from './utils/workspaces'; diff --git a/packages/kbn-pm/src/production/index.ts b/packages/kbn-pm/src/production/index.ts index 493af2beb648d2..f74ab8a4484f11 100644 --- a/packages/kbn-pm/src/production/index.ts +++ b/packages/kbn-pm/src/production/index.ts @@ -18,4 +18,3 @@ */ export { buildProductionProjects } from './build_production_projects'; -export { prepareExternalProjectDependencies } from './prepare_project_dependencies'; diff --git a/packages/kbn-pm/src/production/prepare_project_dependencies.test.ts b/packages/kbn-pm/src/production/prepare_project_dependencies.test.ts deleted file mode 100644 index 13ab8d56e01901..00000000000000 --- a/packages/kbn-pm/src/production/prepare_project_dependencies.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { join, resolve } from 'path'; - -import { prepareExternalProjectDependencies } from './prepare_project_dependencies'; - -const packagesFixtures = resolve(__dirname, '__fixtures__/external_packages'); - -test('does nothing when Kibana `link:` dependencies', async () => { - const projectPath = join(packagesFixtures, 'with_kibana_link_deps'); - - // We're checking for undefined, but we don't really care about what's - // returned, we only care about it resolving. - await expect(prepareExternalProjectDependencies(projectPath)).resolves.toBeUndefined(); -}); - -test('throws if non-Kibana `link` dependencies', async () => { - const projectPath = join(packagesFixtures, 'with_other_link_deps'); - - await expect(prepareExternalProjectDependencies(projectPath)).rejects.toThrow( - 'This plugin is using `link:` dependencies for non-Kibana packages' - ); -}); diff --git a/packages/kbn-pm/src/production/prepare_project_dependencies.ts b/packages/kbn-pm/src/production/prepare_project_dependencies.ts deleted file mode 100644 index 9817770166480d..00000000000000 --- a/packages/kbn-pm/src/production/prepare_project_dependencies.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { isLinkDependency } from '../utils/package_json'; -import { Project } from '../utils/project'; - -/** - * All external projects are located within `./plugins/{plugin}` relative - * to the Kibana root directory or `../kibana-extra/{plugin}` relative - * to Kibana itself. - */ -const isKibanaDep = (depVersion: string) => - // For ../kibana-extra/ directory (legacy only) - depVersion.includes('../../kibana/packages/') || - // For plugins/ directory - depVersion.includes('../../packages/'); - -/** - * This prepares the dependencies for an _external_ project. - */ -export async function prepareExternalProjectDependencies(projectPath: string) { - const project = await Project.fromPath(projectPath); - - if (!project.hasDependencies()) { - return; - } - - const deps = project.allDependencies; - - for (const depName of Object.keys(deps)) { - const depVersion = deps[depName]; - - // Kibana currently only supports `link:` dependencies on Kibana's own - // packages, as these are packaged into the `node_modules` folder when - // Kibana is built, so we don't need to take any action to enable - // `require(...)` to resolve for these packages. - if (isLinkDependency(depVersion) && !isKibanaDep(depVersion)) { - // For non-Kibana packages we need to set up symlinks during the - // installation process, but this is not something we support yet. - throw new Error('This plugin is using `link:` dependencies for non-Kibana packages'); - } - } -} diff --git a/src/cli_keystore/add.js b/src/cli_keystore/add.js index 87266702a26cc9..44737e387c2d29 100644 --- a/src/cli_keystore/add.js +++ b/src/cli_keystore/add.js @@ -17,7 +17,7 @@ * under the License. */ -import Logger from '../cli_plugin/lib/logger'; +import { Logger } from '../cli_plugin/lib/logger'; import { confirm, question } from '../legacy/server/utils'; import { createPromiseFromStreams, createConcatStream } from '../legacy/utils'; diff --git a/src/cli_keystore/add.test.js b/src/cli_keystore/add.test.js index 320581b470c2b4..b5d5009667eb47 100644 --- a/src/cli_keystore/add.test.js +++ b/src/cli_keystore/add.test.js @@ -41,7 +41,7 @@ import { PassThrough } from 'stream'; import { Keystore } from '../legacy/server/keystore'; import { add } from './add'; -import Logger from '../cli_plugin/lib/logger'; +import { Logger } from '../cli_plugin/lib/logger'; import * as prompt from '../legacy/server/utils/prompt'; describe('Kibana keystore', () => { diff --git a/src/cli_keystore/create.js b/src/cli_keystore/create.js index 1af0959821f80d..8be1eb36882f10 100644 --- a/src/cli_keystore/create.js +++ b/src/cli_keystore/create.js @@ -17,7 +17,7 @@ * under the License. */ -import Logger from '../cli_plugin/lib/logger'; +import { Logger } from '../cli_plugin/lib/logger'; import { confirm } from '../legacy/server/utils'; export async function create(keystore, command, options) { diff --git a/src/cli_keystore/create.test.js b/src/cli_keystore/create.test.js index 33b5aa4bd07d8e..f48b3775ddfff7 100644 --- a/src/cli_keystore/create.test.js +++ b/src/cli_keystore/create.test.js @@ -40,7 +40,7 @@ import sinon from 'sinon'; import { Keystore } from '../legacy/server/keystore'; import { create } from './create'; -import Logger from '../cli_plugin/lib/logger'; +import { Logger } from '../cli_plugin/lib/logger'; import * as prompt from '../legacy/server/utils/prompt'; describe('Kibana keystore', () => { diff --git a/src/cli_keystore/get_keystore.js b/src/cli_keystore/get_keystore.js index c8ff2555563ad2..e181efe9196b8e 100644 --- a/src/cli_keystore/get_keystore.js +++ b/src/cli_keystore/get_keystore.js @@ -20,7 +20,7 @@ import { existsSync } from 'fs'; import { join } from 'path'; -import Logger from '../cli_plugin/lib/logger'; +import { Logger } from '../cli_plugin/lib/logger'; import { getConfigDirectory, getDataPath } from '../core/server/path'; export function getKeystore() { diff --git a/src/cli_keystore/get_keystore.test.js b/src/cli_keystore/get_keystore.test.js index 88102b8f51d572..b1c42fca2f73ca 100644 --- a/src/cli_keystore/get_keystore.test.js +++ b/src/cli_keystore/get_keystore.test.js @@ -18,7 +18,7 @@ */ import { getKeystore } from './get_keystore'; -import Logger from '../cli_plugin/lib/logger'; +import { Logger } from '../cli_plugin/lib/logger'; import fs from 'fs'; import sinon from 'sinon'; diff --git a/src/cli_keystore/list.js b/src/cli_keystore/list.js index e9158735a214f0..4a99de271bc6a0 100644 --- a/src/cli_keystore/list.js +++ b/src/cli_keystore/list.js @@ -17,7 +17,7 @@ * under the License. */ -import Logger from '../cli_plugin/lib/logger'; +import { Logger } from '../cli_plugin/lib/logger'; export function list(keystore, command, options = {}) { const logger = new Logger(options); diff --git a/src/cli_keystore/list.test.js b/src/cli_keystore/list.test.js index 857991b5ae3b9c..11c474f908216e 100644 --- a/src/cli_keystore/list.test.js +++ b/src/cli_keystore/list.test.js @@ -38,7 +38,7 @@ jest.mock('fs', () => ({ import sinon from 'sinon'; import { Keystore } from '../legacy/server/keystore'; import { list } from './list'; -import Logger from '../cli_plugin/lib/logger'; +import { Logger } from '../cli_plugin/lib/logger'; describe('Kibana keystore', () => { describe('list', () => { diff --git a/src/cli_plugin/cli.js b/src/cli_plugin/cli.js index da1068b54b4b55..e483385b5b9e8e 100644 --- a/src/cli_plugin/cli.js +++ b/src/cli_plugin/cli.js @@ -17,12 +17,11 @@ * under the License. */ -import _ from 'lodash'; import { pkg } from '../core/server/utils'; import Command from '../cli/command'; -import listCommand from './list'; -import installCommand from './install'; -import removeCommand from './remove'; +import { listCommand } from './list'; +import { installCommand } from './install'; +import { removeCommand } from './remove'; const argv = process.env.kbnWorkerArgv ? JSON.parse(process.env.kbnWorkerArgv) @@ -44,8 +43,12 @@ program .command('help ') .description('get the help for a specific command') .action(function (cmdName) { - const cmd = _.find(program.commands, { _name: cmdName }); - if (!cmd) return program.error(`unknown command ${cmdName}`); + const cmd = program.commands.find((c) => c._name === cmdName); + + if (!cmd) { + return program.error(`unknown command ${cmdName}`); + } + cmd.help(); }); diff --git a/src/cli_plugin/install/__fixtures__/replies/invalid_name.zip b/src/cli_plugin/install/__fixtures__/replies/invalid_name.zip index 5de9a0677b6cb6..4d77ba0d389a69 100644 Binary files a/src/cli_plugin/install/__fixtures__/replies/invalid_name.zip and b/src/cli_plugin/install/__fixtures__/replies/invalid_name.zip differ diff --git a/src/cli_plugin/install/__fixtures__/replies/package.no_version.json b/src/cli_plugin/install/__fixtures__/replies/package.no_version.json deleted file mode 100644 index 9c4f574d894600..00000000000000 --- a/src/cli_plugin/install/__fixtures__/replies/package.no_version.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "test-plugin" -} diff --git a/src/cli_plugin/install/__fixtures__/replies/test_plugin.zip b/src/cli_plugin/install/__fixtures__/replies/test_plugin.zip index 544abf86007e66..57f7455de10f84 100644 Binary files a/src/cli_plugin/install/__fixtures__/replies/test_plugin.zip and b/src/cli_plugin/install/__fixtures__/replies/test_plugin.zip differ diff --git a/src/cli_plugin/install/__fixtures__/replies/test_plugin_different_version.zip b/src/cli_plugin/install/__fixtures__/replies/test_plugin_different_version.zip index 12baa165fdb566..b84473cebf954a 100644 Binary files a/src/cli_plugin/install/__fixtures__/replies/test_plugin_different_version.zip and b/src/cli_plugin/install/__fixtures__/replies/test_plugin_different_version.zip differ diff --git a/src/cli_plugin/install/__fixtures__/replies/test_plugin_many.zip b/src/cli_plugin/install/__fixtures__/replies/test_plugin_many.zip index e4fc8d73feef86..bc58c2bdb9dd7e 100644 Binary files a/src/cli_plugin/install/__fixtures__/replies/test_plugin_many.zip and b/src/cli_plugin/install/__fixtures__/replies/test_plugin_many.zip differ diff --git a/src/cli_plugin/install/cleanup.js b/src/cli_plugin/install/cleanup.js index f31e028226c277..38354bac4a3dfc 100644 --- a/src/cli_plugin/install/cleanup.js +++ b/src/cli_plugin/install/cleanup.js @@ -45,6 +45,5 @@ export function cleanArtifacts(settings) { // At this point we're bailing, so swallow any errors on delete. try { del.sync(settings.workingPath); - del.sync(settings.plugins[0].path); } catch (e) {} // eslint-disable-line no-empty } diff --git a/src/cli_plugin/install/cleanup.test.js b/src/cli_plugin/install/cleanup.test.js index 46089f61d5e83e..1a4cabbc82b5dd 100644 --- a/src/cli_plugin/install/cleanup.test.js +++ b/src/cli_plugin/install/cleanup.test.js @@ -22,7 +22,7 @@ import fs from 'fs'; import del from 'del'; import { cleanPrevious, cleanArtifacts } from './cleanup'; -import Logger from '../lib/logger'; +import { Logger } from '../lib/logger'; describe('kibana cli', function () { describe('plugin installer', function () { diff --git a/src/cli_plugin/install/download.js b/src/cli_plugin/install/download.js index 10d20367c1b7ba..b7f5fbec46edc3 100644 --- a/src/cli_plugin/install/download.js +++ b/src/cli_plugin/install/download.js @@ -17,11 +17,12 @@ * under the License. */ -import downloadHttpFile from './downloaders/http'; -import downloadLocalFile from './downloaders/file'; -import { UnsupportedProtocolError } from '../lib/errors'; import { parse } from 'url'; +import { UnsupportedProtocolError } from '../lib/errors'; +import { downloadHttpFile } from './downloaders/http'; +import { downloadLocalFile } from './downloaders/file'; + function _isWindows() { return /^win/.test(process.platform); } diff --git a/src/cli_plugin/install/download.test.js b/src/cli_plugin/install/download.test.js index 93e5e414fed740..ae926b77f7d589 100644 --- a/src/cli_plugin/install/download.test.js +++ b/src/cli_plugin/install/download.test.js @@ -17,16 +17,18 @@ * under the License. */ +import Fs from 'fs'; +import { join } from 'path'; +import http from 'http'; + import sinon from 'sinon'; import nock from 'nock'; import glob from 'glob-all'; import del from 'del'; -import Fs from 'fs'; -import Logger from '../lib/logger'; + +import { Logger } from '../lib/logger'; import { UnsupportedProtocolError } from '../lib/errors'; import { download, _downloadSingle, _getFilePath, _checkFilePathDeprecation } from './download'; -import { join } from 'path'; -import http from 'http'; describe('kibana cli', function () { describe('plugin downloader', function () { diff --git a/src/cli_plugin/install/downloaders/file.js b/src/cli_plugin/install/downloaders/file.js index 56f83b03d5a907..c262f1010bbc82 100644 --- a/src/cli_plugin/install/downloaders/file.js +++ b/src/cli_plugin/install/downloaders/file.js @@ -17,9 +17,10 @@ * under the License. */ -import Progress from '../progress'; import { createWriteStream, createReadStream, statSync } from 'fs'; +import { Progress } from '../progress'; + function openSourceFile({ sourcePath }) { try { const fileInfo = statSync(sourcePath); @@ -58,7 +59,7 @@ async function copyFile({ readStream, writeStream, progress }) { /* // Responsible for managing local file transfers */ -export default async function copyLocalFile(logger, sourcePath, targetPath) { +export async function downloadLocalFile(logger, sourcePath, targetPath) { try { const { readStream, fileInfo } = openSourceFile({ sourcePath }); const writeStream = createWriteStream(targetPath); diff --git a/src/cli_plugin/install/downloaders/http.js b/src/cli_plugin/install/downloaders/http.js index 0fc01453f2b4c4..e9eafe3737ccb4 100644 --- a/src/cli_plugin/install/downloaders/http.js +++ b/src/cli_plugin/install/downloaders/http.js @@ -17,13 +17,15 @@ * under the License. */ -import Wreck from '@hapi/wreck'; -import Progress from '../progress'; import { createWriteStream } from 'fs'; + +import Wreck from '@hapi/wreck'; import HttpProxyAgent from 'http-proxy-agent'; import HttpsProxyAgent from 'https-proxy-agent'; import { getProxyForUrl } from 'proxy-from-env'; +import { Progress } from '../progress'; + function getProxyAgent(sourceUrl, logger) { const proxy = getProxyForUrl(sourceUrl); @@ -91,7 +93,7 @@ function downloadResponse({ resp, targetPath, progress }) { /* Responsible for managing http transfers */ -export default async function downloadUrl(logger, sourceUrl, targetPath, timeout) { +export async function downloadHttpFile(logger, sourceUrl, targetPath, timeout) { try { const { req, resp } = await sendRequest({ sourceUrl, timeout }, logger); diff --git a/src/cli_plugin/install/index.js b/src/cli_plugin/install/index.js index e3c465ea7a3f53..bc7e95b8489f0c 100644 --- a/src/cli_plugin/install/index.js +++ b/src/cli_plugin/install/index.js @@ -17,13 +17,12 @@ * under the License. */ -import { fromRoot, pkg } from '../../core/server/utils'; -import install from './install'; -import Logger from '../lib/logger'; +import { pkg } from '../../core/server/utils'; +import { install } from './install'; +import { Logger } from '../lib/logger'; import { getConfigPath } from '../../core/server/path'; import { parse, parseMilliseconds } from './settings'; -import logWarnings from '../lib/log_warnings'; -import { warnIfUsingPluginDirOption } from '../lib/warn_if_plugin_dir_option'; +import { logWarnings } from '../lib/log_warnings'; function processCommand(command, options) { let settings; @@ -37,12 +36,11 @@ function processCommand(command, options) { const logger = new Logger(settings); - warnIfUsingPluginDirOption(settings, fromRoot('plugins'), logger); logWarnings(settings, logger); install(settings, logger); } -export default function pluginInstall(program) { +export function installCommand(program) { program .command('install ') .option('-q, --quiet', 'disable all process messaging except errors') @@ -53,15 +51,9 @@ export default function pluginInstall(program) { 'length of time before failing; 0 for never fail', parseMilliseconds ) - .option( - '-d, --plugin-dir ', - 'path to the directory where plugins are stored (DEPRECATED, known to not work for all plugins)', - fromRoot('plugins') - ) .description( 'install a plugin', `Common examples: - install x-pack install file:///Path/to/my/x-pack.zip install https://path.to/my/x-pack.zip` ) diff --git a/src/cli_plugin/install/index.test.js b/src/cli_plugin/install/index.test.js index 39352f52f20fde..657ca0904041a7 100644 --- a/src/cli_plugin/install/index.test.js +++ b/src/cli_plugin/install/index.test.js @@ -18,7 +18,8 @@ */ import sinon from 'sinon'; -import index from './index'; + +import { installCommand } from './index'; describe('kibana cli', function () { describe('plugin installer', function () { @@ -41,7 +42,7 @@ describe('kibana cli', function () { it('should define the command', function () { sinon.spy(program, 'command'); - index(program); + installCommand(program); expect(program.command.calledWith('install ')).toBe(true); program.command.restore(); @@ -50,7 +51,7 @@ describe('kibana cli', function () { it('should define the description', function () { sinon.spy(program, 'description'); - index(program); + installCommand(program); expect(program.description.calledWith('install a plugin')).toBe(true); program.description.restore(); @@ -59,9 +60,9 @@ describe('kibana cli', function () { it('should define the command line options', function () { const spy = sinon.spy(program, 'option'); - const options = [/-q/, /-s/, /-c/, /-t/, /-d/]; + const options = [/-q/, /-s/, /-c/, /-t/]; - index(program); + installCommand(program); for (let i = 0; i < spy.callCount; i++) { const call = spy.getCall(i); @@ -80,7 +81,7 @@ describe('kibana cli', function () { it('should call the action function', function () { sinon.spy(program, 'action'); - index(program); + installCommand(program); expect(program.action.calledOnce).toBe(true); program.action.restore(); diff --git a/src/cli_plugin/install/install.js b/src/cli_plugin/install/install.js index 92be2ac2503202..b74b5894591120 100644 --- a/src/cli_plugin/install/install.js +++ b/src/cli_plugin/install/install.js @@ -19,20 +19,20 @@ import Fs from 'fs'; import { promisify } from 'util'; +import path from 'path'; + +import del from 'del'; import { download } from './download'; -import path from 'path'; import { cleanPrevious, cleanArtifacts } from './cleanup'; import { extract, getPackData } from './pack'; import { renamePlugin } from './rename'; -import del from 'del'; import { errorIfXPackInstall } from '../lib/error_if_x_pack'; import { existingInstall, assertVersion } from './kibana'; -import { prepareExternalProjectDependencies } from '@kbn/pm'; const mkdir = promisify(Fs.mkdir); -export default async function install(settings, logger) { +export async function install(settings, logger) { try { errorIfXPackInstall(settings, logger); @@ -52,12 +52,8 @@ export default async function install(settings, logger) { assertVersion(settings); - await prepareExternalProjectDependencies(settings.workingPath); - - await renamePlugin( - settings.workingPath, - path.join(settings.pluginDir, settings.plugins[0].name) - ); + const targetDir = path.join(settings.pluginDir, settings.plugins[0].id); + await renamePlugin(settings.workingPath, targetDir); logger.log('Plugin installation complete'); } catch (err) { diff --git a/src/cli_plugin/install/kibana.js b/src/cli_plugin/install/kibana.js index edbcef3e7fed0e..f093c1eee3db06 100644 --- a/src/cli_plugin/install/kibana.js +++ b/src/cli_plugin/install/kibana.js @@ -18,15 +18,16 @@ */ import path from 'path'; -import { versionSatisfies, cleanVersion } from '../../legacy/utils/version'; import { statSync } from 'fs'; +import { versionSatisfies, cleanVersion } from '../../legacy/utils/version'; + export function existingInstall(settings, logger) { try { - statSync(path.join(settings.pluginDir, settings.plugins[0].name)); + statSync(path.join(settings.pluginDir, settings.plugins[0].id)); logger.error( - `Plugin ${settings.plugins[0].name} already exists, please remove before installing a new version` + `Plugin ${settings.plugins[0].id} already exists, please remove before installing a new version` ); process.exit(70); } catch (e) { @@ -37,7 +38,7 @@ export function existingInstall(settings, logger) { export function assertVersion(settings) { if (!settings.plugins[0].kibanaVersion) { throw new Error( - `Plugin package.json is missing both a version property (required) and a kibana.version property (optional).` + `Plugin kibana.json is missing both a version property (required) and a kibanaVersion property (optional).` ); } @@ -45,7 +46,7 @@ export function assertVersion(settings) { const expected = cleanVersion(settings.version); if (!versionSatisfies(actual, expected)) { throw new Error( - `Plugin ${settings.plugins[0].name} [${actual}] is incompatible with Kibana [${expected}]` + `Plugin ${settings.plugins[0].id} [${actual}] is incompatible with Kibana [${expected}]` ); } } diff --git a/src/cli_plugin/install/kibana.test.js b/src/cli_plugin/install/kibana.test.js index 8c5dd00d099531..ef3400be730695 100644 --- a/src/cli_plugin/install/kibana.test.js +++ b/src/cli_plugin/install/kibana.test.js @@ -17,12 +17,14 @@ * under the License. */ -import sinon from 'sinon'; -import Logger from '../lib/logger'; import { join } from 'path'; -import del from 'del'; import fs from 'fs'; + +import sinon from 'sinon'; +import del from 'del'; + import { existingInstall, assertVersion } from './kibana'; +import { Logger } from '../lib/logger'; jest.spyOn(fs, 'statSync'); @@ -42,7 +44,7 @@ describe('kibana cli', function () { tempArchiveFile: tempArchiveFilePath, plugin: 'test-plugin', version: '1.0.0', - plugins: [{ name: 'foo' }], + plugins: [{ id: 'foo' }], pluginDir, }; @@ -69,7 +71,10 @@ describe('kibana cli', function () { plugin: 'test-plugin', version: '5.0.0-SNAPSHOT', plugins: [ - { name: 'foo', path: join(testWorkingPath, 'foo'), kibanaVersion: '5.0.0-SNAPSHOT' }, + { + id: 'foo', + kibanaVersion: '5.0.0-SNAPSHOT', + }, ], }; @@ -77,15 +82,17 @@ describe('kibana cli', function () { }); it('should throw an error if plugin is missing a kibana version.', function () { - expect(() => assertVersion(settings)).toThrow( - /plugin package\.json is missing both a version property/i + expect(() => assertVersion(settings)).toThrowErrorMatchingInlineSnapshot( + `"Plugin kibana.json is missing both a version property (required) and a kibanaVersion property (optional)."` ); }); it('should throw an error if plugin kibanaVersion does not match kibana version', function () { settings.plugins[0].kibanaVersion = '1.2.3.4'; - expect(() => assertVersion(settings)).toThrow(/incompatible with Kibana/i); + expect(() => assertVersion(settings)).toThrowErrorMatchingInlineSnapshot( + `"Plugin foo [1.2.3] is incompatible with Kibana [1.0.0]"` + ); }); it('should not throw an error if plugin kibanaVersion matches kibana version', function () { @@ -103,7 +110,9 @@ describe('kibana cli', function () { it('should ignore version info after the dash in checks on invalid version', function () { settings.plugins[0].kibanaVersion = '2.0.0-foo-bar-version-1.2.3'; - expect(() => assertVersion(settings)).toThrow(/incompatible with Kibana/i); + expect(() => assertVersion(settings)).toThrowErrorMatchingInlineSnapshot( + `"Plugin foo [2.0.0] is incompatible with Kibana [1.0.0]"` + ); }); }); diff --git a/src/cli_plugin/install/pack.js b/src/cli_plugin/install/pack.js index 87c94fce2b6777..56d7be78e44bc5 100644 --- a/src/cli_plugin/install/pack.js +++ b/src/cli_plugin/install/pack.js @@ -18,7 +18,11 @@ */ import { analyzeArchive, extractArchive } from './zip'; -import validate from 'validate-npm-package-name'; + +const CAMEL_CASE_REG_EXP = /^[a-z]{1}([a-zA-Z0-9]{1,})$/; +export function isCamelCase(candidate) { + return CAMEL_CASE_REG_EXP.test(candidate); +} /** * Checks the plugin name. Will throw an exception if it does not meet @@ -27,9 +31,10 @@ import validate from 'validate-npm-package-name'; * @param {object} plugin - a package object from listPackages() */ function assertValidPackageName(plugin) { - const validation = validate(plugin.name); - if (!validation.validForNewPackages) { - throw new Error(`Invalid plugin name [${plugin.name}] in package.json`); + if (!isCamelCase(plugin.id)) { + throw new Error( + `Invalid plugin name [${plugin.id}] in kibana.json, expected it to be valid camelCase` + ); } } @@ -60,17 +65,13 @@ export async function getPackData(settings, logger) { /** * Extracts files from a zip archive to a file path using a filter function - * - * @param {string} archive - file path to a zip archive - * @param {string} targetDir - directory path to where the files should - * extracted */ export async function extract(settings, logger) { try { const plugin = settings.plugins[0]; logger.log('Extracting plugin archive'); - await extractArchive(settings.tempArchiveFile, settings.workingPath, plugin.archivePath); + await extractArchive(settings.tempArchiveFile, settings.workingPath, plugin.stripPrefix); logger.log('Extraction complete'); } catch (err) { logger.error(err.stack); diff --git a/src/cli_plugin/install/pack.test.js b/src/cli_plugin/install/pack.test.js index 05a60107f80ff8..c31437e61bebff 100644 --- a/src/cli_plugin/install/pack.test.js +++ b/src/cli_plugin/install/pack.test.js @@ -18,14 +18,15 @@ */ import Fs from 'fs'; +import { join } from 'path'; import sinon from 'sinon'; import glob from 'glob-all'; import del from 'del'; -import Logger from '../lib/logger'; + +import { Logger } from '../lib/logger'; import { extract, getPackData } from './pack'; import { _downloadSingle } from './download'; -import { join } from 'path'; describe('kibana cli', function () { describe('pack', function () { @@ -73,133 +74,104 @@ describe('kibana cli', function () { return _downloadSingle(settings, logger, sourceUrl); } - function shouldReject() { - throw new Error('expected the promise to reject'); - } - describe('extract', function () { - //Also only extracts the content from the kibana folder. - //Ignores the others. - it('successfully extract a valid zip', function () { - return copyReplyFile('test_plugin.zip') - .then(() => { - return getPackData(settings, logger); - }) - .then(() => { - return extract(settings, logger); - }) - .then(() => { - const files = glob.sync('**/*', { cwd: testWorkingPath }); - const expected = [ - 'archive.part', - 'README.md', - 'index.js', - 'package.json', - 'public', - 'public/app.js', - 'extra file only in zip.txt', - ]; - expect(files.sort()).toEqual(expected.sort()); - }); + // Also only extracts the content from the kibana folder. + // Ignores the others. + it('successfully extract a valid zip', async () => { + await copyReplyFile('test_plugin.zip'); + await getPackData(settings, logger); + await extract(settings, logger); + + expect(glob.sync('**/*', { cwd: testWorkingPath })).toMatchInlineSnapshot(` + Array [ + "archive.part", + "bin", + "bin/executable", + "bin/not-executable", + "kibana.json", + "node_modules", + "node_modules/some-package", + "node_modules/some-package/index.js", + "node_modules/some-package/package.json", + "public", + "public/index.js", + ] + `); }); }); - describe('getPackData', function () { - it('populate settings.plugins', function () { - return copyReplyFile('test_plugin.zip') - .then(() => { - return getPackData(settings, logger); - }) - .then(() => { - expect(settings.plugins[0].name).toBe('test-plugin'); - expect(settings.plugins[0].archivePath).toBe('kibana/test-plugin'); - expect(settings.plugins[0].version).toBe('1.0.0'); - expect(settings.plugins[0].kibanaVersion).toBe('1.0.0'); - }); - }); - - it('populate settings.plugin.kibanaVersion', function () { - //kibana.version is defined in this package.json and is different than plugin version - return copyReplyFile('test_plugin_different_version.zip') - .then(() => { - return getPackData(settings, logger); - }) - .then(() => { - expect(settings.plugins[0].kibanaVersion).toBe('5.0.1'); - }); + describe('getPackData', () => { + it('populate settings.plugins', async () => { + await copyReplyFile('test_plugin.zip'); + await getPackData(settings, logger); + expect(settings.plugins).toMatchInlineSnapshot(` + Array [ + Object { + "id": "testPlugin", + "kibanaVersion": "1.0.0", + "stripPrefix": "kibana/test-plugin", + }, + ] + `); }); - it('populate settings.plugin.kibanaVersion (default to plugin version)', function () { - //kibana.version is not defined in this package.json, defaults to plugin version - return copyReplyFile('test_plugin.zip') - .then(() => { - return getPackData(settings, logger); - }) - .then(() => { - expect(settings.plugins[0].kibanaVersion).toBe('1.0.0'); - }); + it('populate settings.plugin.kibanaVersion', async () => { + await copyReplyFile('test_plugin_different_version.zip'); + await getPackData(settings, logger); + expect(settings.plugins).toMatchInlineSnapshot(` + Array [ + Object { + "id": "testPlugin", + "kibanaVersion": "5.0.1", + "stripPrefix": "kibana/test-plugin", + }, + ] + `); }); - it('populate settings.plugins with multiple plugins', function () { - return copyReplyFile('test_plugin_many.zip') - .then(() => { - return getPackData(settings, logger); - }) - .then(() => { - expect(settings.plugins[0].name).toBe('funger-plugin'); - expect(settings.plugins[0].archivePath).toBe('kibana/funger-plugin'); - expect(settings.plugins[0].version).toBe('1.0.0'); - - expect(settings.plugins[1].name).toBe('pdf'); - expect(settings.plugins[1].archivePath).toBe('kibana/pdf-linux'); - expect(settings.plugins[1].version).toBe('1.0.0'); - - expect(settings.plugins[2].name).toBe('pdf'); - expect(settings.plugins[2].archivePath).toBe('kibana/pdf-win32'); - expect(settings.plugins[2].version).toBe('1.0.0'); - - expect(settings.plugins[3].name).toBe('pdf'); - expect(settings.plugins[3].archivePath).toBe('kibana/pdf-win64'); - expect(settings.plugins[3].version).toBe('1.0.0'); - - expect(settings.plugins[4].name).toBe('pdf'); - expect(settings.plugins[4].archivePath).toBe('kibana/pdf'); - expect(settings.plugins[4].version).toBe('1.0.0'); - - expect(settings.plugins[5].name).toBe('test-plugin'); - expect(settings.plugins[5].archivePath).toBe('kibana/test-plugin'); - expect(settings.plugins[5].version).toBe('1.0.0'); - }); + it('populate settings.plugins with multiple plugins', async () => { + await copyReplyFile('test_plugin_many.zip'); + await getPackData(settings, logger); + expect(settings.plugins).toMatchInlineSnapshot(` + Array [ + Object { + "id": "fungerPlugin", + "kibanaVersion": "1.0.0", + "stripPrefix": "kibana/funger-plugin", + }, + Object { + "id": "pdf", + "kibanaVersion": "1.0.0", + "stripPrefix": "kibana/pdf", + }, + Object { + "id": "testPlugin", + "kibanaVersion": "1.0.0", + "stripPrefix": "kibana/test-plugin", + }, + ] + `); }); - it('throw an error if there is no kibana plugin', function () { - return copyReplyFile('test_plugin_no_kibana.zip') - .then(() => { - return getPackData(settings, logger); - }) - .then(shouldReject, (err) => { - expect(err.message).toMatch(/No kibana plugins found in archive/i); - }); + it('throw an error if there is no kibana plugin', async () => { + await copyReplyFile('test_plugin_no_kibana.zip'); + await expect(getPackData(settings, logger)).rejects.toThrowErrorMatchingInlineSnapshot( + `"No kibana plugins found in archive"` + ); }); - it('throw an error with a corrupt zip', function () { - return copyReplyFile('corrupt.zip') - .then(() => { - return getPackData(settings, logger); - }) - .then(shouldReject, (err) => { - expect(err.message).toMatch(/error retrieving/i); - }); + it('throw an error with a corrupt zip', async () => { + await copyReplyFile('corrupt.zip'); + await expect(getPackData(settings, logger)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Error retrieving metadata from plugin archive"` + ); }); - it('throw an error if there an invalid plugin name', function () { - return copyReplyFile('invalid_name.zip') - .then(() => { - return getPackData(settings, logger); - }) - .then(shouldReject, (err) => { - expect(err.message).toMatch(/invalid plugin name/i); - }); + it('throw an error if there an invalid plugin name', async () => { + await copyReplyFile('invalid_name.zip'); + await expect(getPackData(settings, logger)).rejects.toThrowErrorMatchingInlineSnapshot( + `"Invalid plugin name [invalid name] in kibana.json, expected it to be valid camelCase"` + ); }); }); }); diff --git a/src/cli_plugin/install/progress.js b/src/cli_plugin/install/progress.js index e58e4472150b90..5c7d5074603dcf 100644 --- a/src/cli_plugin/install/progress.js +++ b/src/cli_plugin/install/progress.js @@ -20,7 +20,7 @@ /** * Generates file transfer progress messages */ -export default class Progress { +export class Progress { constructor(logger) { const self = this; diff --git a/src/cli_plugin/install/progress.test.js b/src/cli_plugin/install/progress.test.js index 3b66e8b3dc86c9..ef948bafca8367 100644 --- a/src/cli_plugin/install/progress.test.js +++ b/src/cli_plugin/install/progress.test.js @@ -18,8 +18,9 @@ */ import sinon from 'sinon'; -import Progress from './progress'; -import Logger from '../lib/logger'; + +import { Progress } from './progress'; +import { Logger } from '../lib/logger'; describe('kibana cli', function () { describe('plugin installer', function () { diff --git a/src/cli_plugin/install/rename.js b/src/cli_plugin/install/rename.js index 1e5d94d4743753..897222a579a4af 100644 --- a/src/cli_plugin/install/rename.js +++ b/src/cli_plugin/install/rename.js @@ -18,6 +18,7 @@ */ import { rename } from 'fs'; + import { delay } from 'lodash'; export function renamePlugin(workingPath, finalPath) { @@ -31,8 +32,12 @@ export function renamePlugin(workingPath, finalPath) { // Retry for up to retryTime seconds const windowsEPERM = process.platform === 'win32' && err.code === 'EPERM'; const retryAvailable = Date.now() - start < retryTime; - if (windowsEPERM && retryAvailable) - return delay(rename, retryDelay, workingPath, finalPath, retry); + + if (windowsEPERM && retryAvailable) { + delay(rename, retryDelay, workingPath, finalPath, retry); + return; + } + reject(err); } resolve(); diff --git a/src/cli_plugin/install/rename.test.js b/src/cli_plugin/install/rename.test.js index 40df75adc5efaf..8525c367540f8f 100644 --- a/src/cli_plugin/install/rename.test.js +++ b/src/cli_plugin/install/rename.test.js @@ -17,9 +17,10 @@ * under the License. */ -import sinon from 'sinon'; import fs from 'fs'; +import sinon from 'sinon'; + import { renamePlugin } from './rename'; describe('plugin folder rename', function () { diff --git a/src/cli_plugin/install/settings.js b/src/cli_plugin/install/settings.js index 40c845fc37a9e6..20a11479321eec 100644 --- a/src/cli_plugin/install/settings.js +++ b/src/cli_plugin/install/settings.js @@ -17,9 +17,12 @@ * under the License. */ -import expiry from 'expiry-js'; import { resolve } from 'path'; +import expiry from 'expiry-js'; + +import { fromRoot } from '../../core/server/utils'; + function generateUrls({ version, plugin }) { return [ plugin, @@ -46,20 +49,14 @@ export function parse(command, options, kbnPackage) { quiet: options.quiet || false, silent: options.silent || false, config: options.config || '', - optimize: options.optimize, plugin: command, version: kbnPackage.version, - pluginDir: options.pluginDir || '', + pluginDir: fromRoot('plugins'), }; settings.urls = generateUrls(settings); settings.workingPath = resolve(settings.pluginDir, '.plugin.installing'); settings.tempArchiveFile = resolve(settings.workingPath, 'archive.part'); - settings.tempPackageFile = resolve(settings.workingPath, 'package.json'); - settings.setPlugin = function (plugin) { - settings.plugin = plugin; - settings.pluginPath = resolve(settings.pluginDir, settings.plugin.name); - }; return settings; } diff --git a/src/cli_plugin/install/settings.test.js b/src/cli_plugin/install/settings.test.js index 39ca07405ade25..54ad453de9ef8c 100644 --- a/src/cli_plugin/install/settings.test.js +++ b/src/cli_plugin/install/settings.test.js @@ -17,199 +17,82 @@ * under the License. */ +import { createAbsolutePathSerializer } from '@kbn/dev-utils'; + import { fromRoot } from '../../core/server/utils'; -import { resolve } from 'path'; import { parseMilliseconds, parse } from './settings'; -describe('kibana cli', function () { - describe('plugin installer', function () { - describe('command line option parsing', function () { - describe('parseMilliseconds function', function () { - it('should return 0 for an empty string', function () { - const value = ''; - const result = parseMilliseconds(value); - - expect(result).toBe(0); - }); - - it('should return 0 for a number with an invalid unit of measure', function () { - const result = parseMilliseconds('1gigablasts'); - expect(result).toBe(0); - }); - - it('should assume a number with no unit of measure is specified as milliseconds', function () { - const result = parseMilliseconds(1); - expect(result).toBe(1); - - const result2 = parseMilliseconds('1'); - expect(result2).toBe(1); - }); - - it('should interpret a number with "s" as the unit of measure as seconds', function () { - const result = parseMilliseconds('5s'); - expect(result).toBe(5 * 1000); - }); - - it('should interpret a number with "second" as the unit of measure as seconds', function () { - const result = parseMilliseconds('5second'); - expect(result).toBe(5 * 1000); - }); - - it('should interpret a number with "seconds" as the unit of measure as seconds', function () { - const result = parseMilliseconds('5seconds'); - expect(result).toBe(5 * 1000); - }); - - it('should interpret a number with "m" as the unit of measure as minutes', function () { - const result = parseMilliseconds('9m'); - expect(result).toBe(9 * 1000 * 60); - }); - - it('should interpret a number with "minute" as the unit of measure as minutes', function () { - const result = parseMilliseconds('9minute'); - expect(result).toBe(9 * 1000 * 60); - }); - - it('should interpret a number with "minutes" as the unit of measure as minutes', function () { - const result = parseMilliseconds('9minutes'); - expect(result).toBe(9 * 1000 * 60); - }); - }); - - describe('parse function', function () { - const command = 'plugin name'; - let options = {}; - const kbnPackage = { version: 1234 }; - beforeEach(function () { - options = { pluginDir: fromRoot('plugins') }; - }); - - describe('timeout option', function () { - it('should default to 0 (milliseconds)', function () { - const settings = parse(command, options, kbnPackage); - - expect(settings.timeout).toBe(0); - }); - - it('should set settings.timeout property', function () { - options.timeout = 1234; - const settings = parse(command, options, kbnPackage); - - expect(settings.timeout).toBe(1234); - }); - }); - - describe('quiet option', function () { - it('should default to false', function () { - const settings = parse(command, options, kbnPackage); - - expect(settings.quiet).toBe(false); - }); - - it('should set settings.quiet property to true', function () { - options.quiet = true; - const settings = parse(command, options, kbnPackage); - - expect(settings.quiet).toBe(true); - }); - }); - - describe('silent option', function () { - it('should default to false', function () { - const settings = parse(command, options, kbnPackage); - - expect(settings.silent).toBe(false); - }); - - it('should set settings.silent property to true', function () { - options.silent = true; - const settings = parse(command, options, kbnPackage); - - expect(settings.silent).toBe(true); - }); - }); - - describe('config option', function () { - it('should default to ZLS', function () { - const settings = parse(command, options, kbnPackage); - - expect(settings.config).toBe(''); - }); - - it('should set settings.config property', function () { - options.config = 'foo bar baz'; - const settings = parse(command, options, kbnPackage); - - expect(settings.config).toBe('foo bar baz'); - }); - }); - - describe('pluginDir option', function () { - it('should default to plugins', function () { - const settings = parse(command, options, kbnPackage); - - expect(settings.pluginDir).toBe(fromRoot('plugins')); - }); - - it('should set settings.config property', function () { - options.pluginDir = 'foo bar baz'; - const settings = parse(command, options, kbnPackage); - - expect(settings.pluginDir).toBe('foo bar baz'); - }); - }); - - describe('command value', function () { - it('should set settings.plugin property', function () { - const settings = parse(command, options, kbnPackage); - - expect(settings.plugin).toBe(command); - }); - }); - - describe('urls collection', function () { - it('should populate the settings.urls property', function () { - const settings = parse(command, options, kbnPackage); - - const expected = [ - command, - `https://artifacts.elastic.co/downloads/kibana-plugins/${command}/${command}-1234.zip`, - ]; - - expect(settings.urls).toEqual(expected); - }); - }); - - describe('workingPath value', function () { - it('should set settings.workingPath property', function () { - options.pluginDir = 'foo/bar/baz'; - const settings = parse(command, options, kbnPackage); - const expected = resolve('foo/bar/baz', '.plugin.installing'); - - expect(settings.workingPath).toBe(expected); - }); - }); - - describe('tempArchiveFile value', function () { - it('should set settings.tempArchiveFile property', function () { - options.pluginDir = 'foo/bar/baz'; - const settings = parse(command, options, kbnPackage); - const expected = resolve('foo/bar/baz', '.plugin.installing', 'archive.part'); - - expect(settings.tempArchiveFile).toBe(expected); - }); - }); +const SECOND = 1000; +const MINUTE = SECOND * 60; + +expect.addSnapshotSerializer(createAbsolutePathSerializer()); + +describe('parseMilliseconds function', function () { + it.each([ + ['', 0], + ['1gigablasts', 0], + [1, 1], + ['1', 1], + ['5s', 5 * SECOND], + ['5second', 5 * SECOND], + ['5seconds', 5 * SECOND], + ['9m', 9 * MINUTE], + ['9minute', 9 * MINUTE], + ['9minutes', 9 * MINUTE], + ])('should parse %j to %j', (input, result) => { + expect(parseMilliseconds(input)).toBe(result); + }); +}); - describe('tempPackageFile value', function () { - it('should set settings.tempPackageFile property', function () { - options.pluginDir = 'foo/bar/baz'; - const settings = parse(command, options, kbnPackage); - const expected = resolve('foo/bar/baz', '.plugin.installing', 'package.json'); +describe('parse function', function () { + const command = 'plugin name'; + const defaultOptions = { pluginDir: fromRoot('plugins') }; + const kbnPackage = { version: 1234 }; + + it('produces expected defaults', function () { + expect(parse(command, { ...defaultOptions }, kbnPackage)).toMatchInlineSnapshot(` + Object { + "config": "", + "plugin": "plugin name", + "pluginDir": /plugins, + "quiet": false, + "silent": false, + "tempArchiveFile": /plugins/.plugin.installing/archive.part, + "timeout": 0, + "urls": Array [ + "plugin name", + "https://artifacts.elastic.co/downloads/kibana-plugins/plugin name/plugin name-1234.zip", + ], + "version": 1234, + "workingPath": /plugins/.plugin.installing, + } + `); + }); - expect(settings.tempPackageFile).toBe(expected); - }); - }); - }); - }); + it('consumes overrides', function () { + const options = { + quiet: true, + silent: true, + config: 'foo bar baz', + ...defaultOptions, + }; + + expect(parse(command, options, kbnPackage)).toMatchInlineSnapshot(` + Object { + "config": "foo bar baz", + "plugin": "plugin name", + "pluginDir": /plugins, + "quiet": true, + "silent": true, + "tempArchiveFile": /plugins/.plugin.installing/archive.part, + "timeout": 0, + "urls": Array [ + "plugin name", + "https://artifacts.elastic.co/downloads/kibana-plugins/plugin name/plugin name-1234.zip", + ], + "version": 1234, + "workingPath": /plugins/.plugin.installing, + } + `); }); }); diff --git a/src/cli_plugin/install/zip.js b/src/cli_plugin/install/zip.js index 52eba2ea239a2c..b906dd59a302b1 100644 --- a/src/cli_plugin/install/zip.js +++ b/src/cli_plugin/install/zip.js @@ -17,21 +17,24 @@ * under the License. */ -import yauzl from 'yauzl'; import path from 'path'; import { createWriteStream, mkdir } from 'fs'; -import { get } from 'lodash'; + +import yauzl from 'yauzl'; + +const isDirectoryRegex = /(\/|\\)$/; +function isDirectory(filename) { + return isDirectoryRegex.test(filename); +} /** * Returns an array of package objects. There will be one for each of - * package.json files in the archive - * - * @param {string} archive - path to plugin archive zip file + * package.json files in the archive */ export function analyzeArchive(archive) { const plugins = []; - const regExp = new RegExp('(kibana[\\\\/][^\\\\/]+)[\\\\/]package.json', 'i'); + const regExp = new RegExp('(kibana[\\\\/][^\\\\/]+)[\\\\/]kibana.json', 'i'); return new Promise((resolve, reject) => { yauzl.open(archive, { lazyEntries: true }, function (err, zipfile) { @@ -47,31 +50,32 @@ export function analyzeArchive(archive) { return zipfile.readEntry(); } - zipfile.openReadStream(entry, function (err, readable) { + zipfile.openReadStream(entry, function (error, readable) { const chunks = []; - if (err) { - return reject(err); + if (error) { + return reject(error); } readable.on('data', (chunk) => chunks.push(chunk)); readable.on('end', function () { - const contents = Buffer.concat(chunks).toString(); - const pkg = JSON.parse(contents); - - plugins.push( - Object.assign(pkg, { - archivePath: match[1], - archive: archive, - - // Plugins must specify their version, and by default that version should match - // the version of kibana down to the patch level. If these two versions need - // to diverge, they can specify a kibana.version to indicate the version of - // kibana the plugin is intended to work with. - kibanaVersion: get(pkg, 'kibana.version', pkg.version), - }) - ); + const manifestJson = Buffer.concat(chunks).toString(); + const manifest = JSON.parse(manifestJson); + + plugins.push({ + id: manifest.id, + stripPrefix: match[1], + + // Plugins must specify their version, and by default that version in the plugin + // manifest should match the version of kibana down to the patch level. If these + // two versions need plugins can specify a kibanaVersion to indicate the version + // of kibana the plugin is intended to work with. + kibanaVersion: + typeof manifest.kibanaVersion === 'string' && manifest.kibanaVersion + ? manifest.kibanaVersion + : manifest.version, + }); zipfile.readEntry(); }); @@ -85,12 +89,7 @@ export function analyzeArchive(archive) { }); } -const isDirectoryRegex = /(\/|\\)$/; -export function _isDirectory(filename) { - return isDirectoryRegex.test(filename); -} - -export function extractArchive(archive, targetDir, extractPath) { +export function extractArchive(archive, targetDir, stripPrefix) { return new Promise((resolve, reject) => { yauzl.open(archive, { lazyEntries: true }, function (err, zipfile) { if (err) { @@ -102,8 +101,8 @@ export function extractArchive(archive, targetDir, extractPath) { zipfile.on('entry', function (entry) { let fileName = entry.fileName; - if (extractPath && fileName.startsWith(extractPath)) { - fileName = fileName.substring(extractPath.length); + if (stripPrefix && fileName.startsWith(stripPrefix)) { + fileName = fileName.substring(stripPrefix.length); } else { return zipfile.readEntry(); } @@ -112,30 +111,34 @@ export function extractArchive(archive, targetDir, extractPath) { fileName = path.join(targetDir, fileName); } - if (_isDirectory(fileName)) { - mkdir(fileName, { recursive: true }, function (err) { - if (err) { - return reject(err); + if (isDirectory(fileName)) { + mkdir(fileName, { recursive: true }, function (error) { + if (error) { + return reject(error); } zipfile.readEntry(); }); } else { // file entry - zipfile.openReadStream(entry, function (err, readStream) { - if (err) { - return reject(err); + zipfile.openReadStream(entry, function (error, readStream) { + if (error) { + return reject(error); } // ensure parent directory exists - mkdir(path.dirname(fileName), { recursive: true }, function (err) { - if (err) { - return reject(err); + mkdir(path.dirname(fileName), { recursive: true }, function (error2) { + if (error2) { + return reject(error2); } readStream.pipe( - createWriteStream(fileName, { mode: entry.externalFileAttributes >>> 16 }) + createWriteStream(fileName, { + // eslint-disable-next-line no-bitwise + mode: entry.externalFileAttributes >>> 16, + }) ); + readStream.on('end', function () { zipfile.readEntry(); }); diff --git a/src/cli_plugin/install/zip.test.js b/src/cli_plugin/install/zip.test.js index 28367e9e244531..0f56c0d0322aaa 100644 --- a/src/cli_plugin/install/zip.test.js +++ b/src/cli_plugin/install/zip.test.js @@ -17,12 +17,16 @@ * under the License. */ -import del from 'del'; import path from 'path'; import os from 'os'; -import glob from 'glob'; import fs from 'fs'; -import { analyzeArchive, extractArchive, _isDirectory } from './zip'; + +import del from 'del'; +import glob from 'glob'; + +import { analyzeArchive, extractArchive } from './zip'; + +const getMode = (path) => (fs.statSync(path).mode & parseInt('777', 8)).toString(8); describe('kibana cli', function () { describe('zip', function () { @@ -43,32 +47,37 @@ describe('kibana cli', function () { describe('analyzeArchive', function () { it('returns array of plugins', async () => { const packages = await analyzeArchive(archivePath); - const plugin = packages[0]; - - expect(packages).toBeInstanceOf(Array); - expect(plugin.name).toBe('test-plugin'); - expect(plugin.archivePath).toBe('kibana/test-plugin'); - expect(plugin.archive).toBe(archivePath); - expect(plugin.kibanaVersion).toBe('1.0.0'); + expect(packages).toMatchInlineSnapshot(` + Array [ + Object { + "id": "testPlugin", + "kibanaVersion": "1.0.0", + "stripPrefix": "kibana/test-plugin", + }, + ] + `); }); }); describe('extractArchive', () => { it('extracts files using the extractPath filter', async () => { - const archive = path.resolve(repliesPath, 'test_plugin_many.zip'); - + const archive = path.resolve(repliesPath, 'test_plugin.zip'); await extractArchive(archive, tempPath, 'kibana/test-plugin'); - const files = await glob.sync('**/*', { cwd: tempPath }); - - const expected = [ - 'extra file only in zip.txt', - 'index.js', - 'package.json', - 'public', - 'public/app.js', - 'README.md', - ]; - expect(files.sort()).toEqual(expected.sort()); + + expect(glob.sync('**/*', { cwd: tempPath })).toMatchInlineSnapshot(` + Array [ + "bin", + "bin/executable", + "bin/not-executable", + "kibana.json", + "node_modules", + "node_modules/some-package", + "node_modules/some-package/index.js", + "node_modules/some-package/package.json", + "public", + "public/index.js", + ] + `); }); }); @@ -76,49 +85,26 @@ describe('kibana cli', function () { it('verify consistency of modes of files', async () => { const archivePath = path.resolve(repliesPath, 'test_plugin.zip'); - await extractArchive(archivePath, tempPath, 'kibana/libs'); - const files = await glob.sync('**/*', { cwd: tempPath }); - - const expected = ['executable', 'unexecutable']; - expect(files.sort()).toEqual(expected.sort()); + await extractArchive(archivePath, tempPath, 'kibana/test-plugin/bin'); - const executableMode = - '0' + - (fs.statSync(path.resolve(tempPath, 'executable')).mode & parseInt('777', 8)).toString(8); - const unExecutableMode = - '0' + - (fs.statSync(path.resolve(tempPath, 'unexecutable')).mode & parseInt('777', 8)).toString( - 8 - ); + expect(glob.sync('**/*', { cwd: tempPath })).toMatchInlineSnapshot(` + Array [ + "executable", + "not-executable", + ] + `); - expect(executableMode).toEqual('0755'); - expect(unExecutableMode).toEqual('0644'); + expect(getMode(path.resolve(tempPath, 'executable'))).toEqual('755'); + expect(getMode(path.resolve(tempPath, 'not-executable'))).toEqual('644'); }); }); it('handles a corrupt zip archive', async () => { - try { - await extractArchive(path.resolve(repliesPath, 'corrupt.zip')); - throw new Error('This should have failed'); - } catch (e) { - return; - } - }); - }); - - describe('_isDirectory', () => { - it('should check for a forward slash', () => { - expect(_isDirectory('/foo/bar/')).toBe(true); - }); - - it('should check for a backslash', () => { - expect(_isDirectory('\\foo\\bar\\')).toBe(true); - }); - - it('should return false for files', () => { - expect(_isDirectory('foo.txt')).toBe(false); - expect(_isDirectory('\\path\\to\\foo.txt')).toBe(false); - expect(_isDirectory('/path/to/foo.txt')).toBe(false); + await expect( + extractArchive(path.resolve(repliesPath, 'corrupt.zip')) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"end of central directory record signature not found"` + ); }); }); }); diff --git a/src/cli_plugin/lib/error_if_x_pack.js b/src/cli_plugin/lib/error_if_x_pack.js index d6624f5308ec02..4ea7ceb37478ec 100644 --- a/src/cli_plugin/lib/error_if_x_pack.js +++ b/src/cli_plugin/lib/error_if_x_pack.js @@ -17,7 +17,7 @@ * under the License. */ -import { isOSS } from './is_oss'; +import { isOss } from './is_oss'; function isXPack(plugin) { return /x-pack/.test(plugin); @@ -25,7 +25,7 @@ function isXPack(plugin) { export function errorIfXPackInstall(settings) { if (isXPack(settings.plugin)) { - if (isOSS()) { + if (isOss()) { throw new Error( 'You are using the OSS-only distribution of Kibana. ' + 'As of version 6.3+ X-Pack is bundled in the standard distribution of this software by default; ' + @@ -40,7 +40,7 @@ export function errorIfXPackInstall(settings) { } export function errorIfXPackRemove(settings) { - if (isXPack(settings.plugin) && !isOSS()) { + if (isXPack(settings.plugin) && !isOss()) { throw new Error( 'You are using the standard distribution of Kibana. Please install the OSS-only distribution to remove X-Pack features.' ); diff --git a/src/cli_plugin/lib/is_oss.js b/src/cli_plugin/lib/is_oss.js index 3f2190d8346ec0..53f19a41228d68 100644 --- a/src/cli_plugin/lib/is_oss.js +++ b/src/cli_plugin/lib/is_oss.js @@ -20,6 +20,6 @@ import fs from 'fs'; import path from 'path'; -export function isOSS() { +export function isOss() { return !fs.existsSync(path.resolve(__dirname, '../../../x-pack')); } diff --git a/src/cli_plugin/lib/is_oss.test.js b/src/cli_plugin/lib/is_oss.test.js index a4673710c63ce9..636e1616e7d3ca 100644 --- a/src/cli_plugin/lib/is_oss.test.js +++ b/src/cli_plugin/lib/is_oss.test.js @@ -17,12 +17,12 @@ * under the License. */ -import { isOSS } from './is_oss'; +import { isOss } from './is_oss'; describe('is_oss', () => { describe('x-pack installed', () => { it('should return false', () => { - expect(isOSS()).toEqual(false); + expect(isOss()).toEqual(false); }); }); }); diff --git a/src/cli_plugin/lib/log_warnings.js b/src/cli_plugin/lib/log_warnings.js index b4542acecb3050..f31c3d4bd2e9ac 100644 --- a/src/cli_plugin/lib/log_warnings.js +++ b/src/cli_plugin/lib/log_warnings.js @@ -17,7 +17,7 @@ * under the License. */ -export default function (settings, logger) { +export function logWarnings(logger) { process.on('warning', (warning) => { // deprecation warnings do no reflect a current problem for // the user and therefor should be filtered out. diff --git a/src/cli_plugin/lib/logger.js b/src/cli_plugin/lib/logger.js index efd6130322c386..592618fbecfc87 100644 --- a/src/cli_plugin/lib/logger.js +++ b/src/cli_plugin/lib/logger.js @@ -20,7 +20,7 @@ /** * Logs messages and errors */ -export default class Logger { +export class Logger { constructor(settings = {}) { this.previousLineEnded = true; this.silent = !!settings.silent; diff --git a/src/cli_plugin/lib/logger.test.js b/src/cli_plugin/lib/logger.test.js index 00cad1a9bbb114..7ff683ea50c959 100644 --- a/src/cli_plugin/lib/logger.test.js +++ b/src/cli_plugin/lib/logger.test.js @@ -18,7 +18,8 @@ */ import sinon from 'sinon'; -import Logger from './logger'; + +import { Logger } from './logger'; describe('kibana cli', function () { describe('plugin installer', function () { diff --git a/src/cli_plugin/list/index.js b/src/cli_plugin/list/index.js index c0f708b8ccf833..a3cf1a54a8a624 100644 --- a/src/cli_plugin/list/index.js +++ b/src/cli_plugin/list/index.js @@ -18,37 +18,16 @@ */ import { fromRoot } from '../../core/server/utils'; -import list from './list'; -import Logger from '../lib/logger'; -import { parse } from './settings'; -import logWarnings from '../lib/log_warnings'; -import { warnIfUsingPluginDirOption } from '../lib/warn_if_plugin_dir_option'; +import { list } from './list'; +import { Logger } from '../lib/logger'; +import { logWarnings } from '../lib/log_warnings'; -function processCommand(command, options) { - let settings; - try { - settings = parse(command, options); - } catch (ex) { - //The logger has not yet been initialized. - console.error(ex.message); - process.exit(64); // eslint-disable-line no-process-exit - } - - const logger = new Logger(settings); - - warnIfUsingPluginDirOption(settings, fromRoot('plugins'), logger); - logWarnings(settings, logger); - list(settings, logger); +function processCommand() { + const logger = new Logger(); + logWarnings(logger); + list(fromRoot('plugins'), logger); } -export default function pluginList(program) { - program - .command('list') - .option( - '-d, --plugin-dir ', - 'path to the directory where plugins are stored (DEPRECATED, known to not work for all plugins)', - fromRoot('plugins') - ) - .description('list installed plugins') - .action(processCommand); +export function listCommand(program) { + program.command('list').description('list installed plugins').action(processCommand); } diff --git a/src/cli_plugin/list/list.js b/src/cli_plugin/list/list.js index b34631e5dfd085..bf6a082a91878f 100644 --- a/src/cli_plugin/list/list.js +++ b/src/cli_plugin/list/list.js @@ -20,19 +20,20 @@ import { statSync, readdirSync, readFileSync } from 'fs'; import { join } from 'path'; -export default function list(settings, logger) { - readdirSync(settings.pluginDir).forEach((filename) => { - const stat = statSync(join(settings.pluginDir, filename)); +export function list(pluginDir, logger) { + readdirSync(pluginDir).forEach((name) => { + const stat = statSync(join(pluginDir, name)); - if (stat.isDirectory() && filename[0] !== '.') { + if (stat.isDirectory() && name[0] !== '.') { try { - const packagePath = join(settings.pluginDir, filename, 'package.json'); - const { version } = JSON.parse(readFileSync(packagePath, 'utf8')); - logger.log(filename + '@' + version); + const packagePath = join(pluginDir, name, 'kibana.json'); + const pkg = JSON.parse(readFileSync(packagePath, 'utf8')); + logger.log(pkg.id + '@' + pkg.version); } catch (e) { - throw new Error('Unable to read package.json file for plugin ' + filename); + throw new Error('Unable to read kibana.json file for plugin ' + name); } } }); + logger.log(''); //intentional blank line for aesthetics } diff --git a/src/cli_plugin/list/list.test.js b/src/cli_plugin/list/list.test.js index 071a253fa87fe0..b1b5d1cde6a35d 100644 --- a/src/cli_plugin/list/list.test.js +++ b/src/cli_plugin/list/list.test.js @@ -17,78 +17,67 @@ * under the License. */ -import sinon from 'sinon'; -import del from 'del'; -import Logger from '../lib/logger'; -import list from './list'; import { join } from 'path'; -import { writeFileSync, appendFileSync, mkdirSync } from 'fs'; +import { writeFileSync, mkdirSync } from 'fs'; + +import del from 'del'; + +import { list } from './list'; function createPlugin(name, version, pluginBaseDir) { const pluginDir = join(pluginBaseDir, name); mkdirSync(pluginDir, { recursive: true }); - appendFileSync(join(pluginDir, 'package.json'), '{"version": "' + version + '"}'); + writeFileSync( + join(pluginDir, 'kibana.json'), + JSON.stringify({ + id: name, + version, + }) + ); } +const logger = { + messages: [], + log(msg) { + this.messages.push(`log: ${msg}`); + }, + error(msg) { + this.messages.push(`error: ${msg}`); + }, +}; + describe('kibana cli', function () { describe('plugin lister', function () { const pluginDir = join(__dirname, '.test.data.list'); - let logger; - - const settings = { - pluginDir: pluginDir, - }; beforeEach(function () { - logger = new Logger(settings); - sinon.stub(logger, 'log'); - sinon.stub(logger, 'error'); + logger.messages.length = 0; del.sync(pluginDir); mkdirSync(pluginDir, { recursive: true }); }); afterEach(function () { - logger.log.restore(); - logger.error.restore(); del.sync(pluginDir); }); - it('list all of the folders in the plugin folder', function () { - createPlugin('plugin1', '5.0.0-alpha2', pluginDir); - createPlugin('plugin2', '3.2.1', pluginDir); - createPlugin('plugin3', '1.2.3', pluginDir); - - list(settings, logger); - - expect(logger.log.calledWith('plugin1@5.0.0-alpha2')).toBe(true); - expect(logger.log.calledWith('plugin2@3.2.1')).toBe(true); - expect(logger.log.calledWith('plugin3@1.2.3')).toBe(true); - }); - - it('ignore folders that start with a period', function () { + it('list all of the folders in the plugin folder, ignoring dot prefixed plugins and regular files', function () { createPlugin('.foo', '1.0.0', pluginDir); createPlugin('plugin1', '5.0.0-alpha2', pluginDir); createPlugin('plugin2', '3.2.1', pluginDir); createPlugin('plugin3', '1.2.3', pluginDir); createPlugin('.bar', '1.0.0', pluginDir); - - list(settings, logger); - - expect(logger.log.calledWith('.foo@1.0.0')).toBe(false); - expect(logger.log.calledWith('.bar@1.0.0')).toBe(false); - }); - - it('list should only list folders', function () { - createPlugin('plugin1', '1.0.0', pluginDir); - createPlugin('plugin2', '1.0.0', pluginDir); - createPlugin('plugin3', '1.0.0', pluginDir); writeFileSync(join(pluginDir, 'plugin4'), 'This is a file, and not a folder.'); - list(settings, logger); + list(pluginDir, logger); - expect(logger.log.calledWith('plugin1@1.0.0')).toBe(true); - expect(logger.log.calledWith('plugin2@1.0.0')).toBe(true); - expect(logger.log.calledWith('plugin3@1.0.0')).toBe(true); + expect(logger.messages).toMatchInlineSnapshot(` + Array [ + "log: plugin1@5.0.0-alpha2", + "log: plugin2@3.2.1", + "log: plugin3@1.2.3", + "log: ", + ] + `); }); it('list should throw an exception if a plugin does not have a package.json', function () { @@ -96,19 +85,23 @@ describe('kibana cli', function () { mkdirSync(join(pluginDir, 'empty-plugin'), { recursive: true }); expect(function () { - list(settings, logger); - }).toThrowError('Unable to read package.json file for plugin empty-plugin'); + list(pluginDir, logger); + }).toThrowErrorMatchingInlineSnapshot( + `"Unable to read kibana.json file for plugin empty-plugin"` + ); }); it('list should throw an exception if a plugin have an empty package.json', function () { createPlugin('plugin1', '1.0.0', pluginDir); const invalidPluginDir = join(pluginDir, 'invalid-plugin'); mkdirSync(invalidPluginDir, { recursive: true }); - appendFileSync(join(invalidPluginDir, 'package.json'), ''); + writeFileSync(join(invalidPluginDir, 'package.json'), ''); expect(function () { - list(settings, logger); - }).toThrowError('Unable to read package.json file for plugin invalid-plugin'); + list(pluginDir, logger); + }).toThrowErrorMatchingInlineSnapshot( + `"Unable to read kibana.json file for plugin invalid-plugin"` + ); }); }); }); diff --git a/src/cli_plugin/list/settings.test.js b/src/cli_plugin/list/settings.test.js deleted file mode 100644 index 85e6cb88e82fd7..00000000000000 --- a/src/cli_plugin/list/settings.test.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { fromRoot } from '../../core/server/utils'; -import { parse } from './settings'; - -describe('kibana cli', function () { - describe('plugin installer', function () { - describe('command line option parsing', function () { - describe('parse function', function () { - let command; - const options = {}; - beforeEach(function () { - command = { pluginDir: fromRoot('plugins') }; - }); - - describe('pluginDir option', function () { - it('should default to plugins', function () { - const settings = parse(command, options); - - expect(settings.pluginDir).toBe(fromRoot('plugins')); - }); - - it('should set settings.config property', function () { - command.pluginDir = 'foo bar baz'; - const settings = parse(command, options); - - expect(settings.pluginDir).toBe('foo bar baz'); - }); - }); - }); - }); - }); -}); diff --git a/src/cli_plugin/remove/index.js b/src/cli_plugin/remove/index.js index 9ff06e0e760bfc..c3bd96086db9b9 100644 --- a/src/cli_plugin/remove/index.js +++ b/src/cli_plugin/remove/index.js @@ -17,46 +17,34 @@ * under the License. */ -import { fromRoot } from '../../core/server/utils'; -import remove from './remove'; -import Logger from '../lib/logger'; +import { remove } from './remove'; +import { Logger } from '../lib/logger'; import { parse } from './settings'; import { getConfigPath } from '../../core/server/path'; -import logWarnings from '../lib/log_warnings'; -import { warnIfUsingPluginDirOption } from '../lib/warn_if_plugin_dir_option'; +import { logWarnings } from '../lib/log_warnings'; function processCommand(command, options) { let settings; try { settings = parse(command, options); } catch (ex) { - //The logger has not yet been initialized. + // The logger has not yet been initialized. console.error(ex.message); process.exit(64); // eslint-disable-line no-process-exit } const logger = new Logger(settings); - warnIfUsingPluginDirOption(settings, fromRoot('plugins'), logger); logWarnings(settings, logger); remove(settings, logger); } -export default function pluginRemove(program) { +export function removeCommand(program) { program .command('remove ') .option('-q, --quiet', 'disable all process messaging except errors') .option('-s, --silent', 'disable all process messaging') .option('-c, --config ', 'path to the config file', getConfigPath()) - .option( - '-d, --plugin-dir ', - 'path to the directory where plugins are stored (DEPRECATED, known to not work for all plugins)', - fromRoot('plugins') - ) - .description( - 'remove a plugin', - `common examples: - remove x-pack` - ) + .description('remove a plugin') .action(processCommand); } diff --git a/src/cli_plugin/remove/remove.js b/src/cli_plugin/remove/remove.js index 353e592390ff40..0c218301242be7 100644 --- a/src/cli_plugin/remove/remove.js +++ b/src/cli_plugin/remove/remove.js @@ -18,11 +18,12 @@ */ import { statSync } from 'fs'; -import { errorIfXPackRemove } from '../lib/error_if_x_pack'; import del from 'del'; -export default function remove(settings, logger) { +import { errorIfXPackRemove } from '../lib/error_if_x_pack'; + +export function remove(settings, logger) { try { let stat; try { diff --git a/src/cli_plugin/remove/remove.test.js b/src/cli_plugin/remove/remove.test.js index 4bf061820aa050..44c66468bbb557 100644 --- a/src/cli_plugin/remove/remove.test.js +++ b/src/cli_plugin/remove/remove.test.js @@ -17,13 +17,15 @@ * under the License. */ +import { join } from 'path'; +import { writeFileSync, existsSync, mkdirSync } from 'fs'; + import sinon from 'sinon'; import glob from 'glob-all'; import del from 'del'; -import Logger from '../lib/logger'; -import remove from './remove'; -import { join } from 'path'; -import { writeFileSync, existsSync, mkdirSync } from 'fs'; + +import { Logger } from '../lib/logger'; +import { remove } from './remove'; describe('kibana cli', function () { describe('plugin remover', function () { diff --git a/src/cli_plugin/remove/settings.js b/src/cli_plugin/remove/settings.js index dc8d3c87e6eab3..8a7d41b35ae57e 100644 --- a/src/cli_plugin/remove/settings.js +++ b/src/cli_plugin/remove/settings.js @@ -19,12 +19,14 @@ import { resolve } from 'path'; +import { fromRoot } from '../../core/server/utils'; + export function parse(command, options) { const settings = { quiet: options.quiet || false, silent: options.silent || false, config: options.config || '', - pluginDir: options.pluginDir || '', + pluginDir: fromRoot('plugins'), plugin: command, }; diff --git a/src/cli_plugin/remove/settings.test.js b/src/cli_plugin/remove/settings.test.js index b3110e1ff0499a..9ae555d79cd1a8 100644 --- a/src/cli_plugin/remove/settings.test.js +++ b/src/cli_plugin/remove/settings.test.js @@ -17,88 +17,42 @@ * under the License. */ -import { fromRoot } from '../../core/server/utils'; -import { parse } from './settings'; - -describe('kibana cli', function () { - describe('plugin installer', function () { - describe('command line option parsing', function () { - describe('parse function', function () { - const command = 'plugin name'; - let options = {}; - const kbnPackage = { version: 1234 }; - beforeEach(function () { - options = { pluginDir: fromRoot('plugins') }; - }); - - describe('quiet option', function () { - it('should default to false', function () { - const settings = parse(command, options, kbnPackage); - - expect(settings.quiet).toBe(false); - }); - - it('should set settings.quiet property to true', function () { - options.quiet = true; - const settings = parse(command, options, kbnPackage); - - expect(settings.quiet).toBe(true); - }); - }); - - describe('silent option', function () { - it('should default to false', function () { - const settings = parse(command, options, kbnPackage); - - expect(settings.silent).toBe(false); - }); - - it('should set settings.silent property to true', function () { - options.silent = true; - const settings = parse(command, options, kbnPackage); - - expect(settings.silent).toBe(true); - }); - }); +import { createAbsolutePathSerializer } from '@kbn/dev-utils'; - describe('config option', function () { - it('should default to ZLS', function () { - const settings = parse(command, options, kbnPackage); - - expect(settings.config).toBe(''); - }); - - it('should set settings.config property', function () { - options.config = 'foo bar baz'; - const settings = parse(command, options, kbnPackage); - - expect(settings.config).toBe('foo bar baz'); - }); - }); - - describe('pluginDir option', function () { - it('should default to plugins', function () { - const settings = parse(command, options, kbnPackage); - - expect(settings.pluginDir).toBe(fromRoot('plugins')); - }); - - it('should set settings.config property', function () { - options.pluginDir = 'foo bar baz'; - const settings = parse(command, options, kbnPackage); - - expect(settings.pluginDir).toBe('foo bar baz'); - }); - }); +import { parse } from './settings'; - describe('command value', function () { - it('should set settings.plugin property', function () { - const settings = parse(command, options, kbnPackage); +const command = 'plugin name'; + +expect.addSnapshotSerializer(createAbsolutePathSerializer()); + +it('produces the defaults', () => { + expect(parse(command, {})).toMatchInlineSnapshot(` + Object { + "config": "", + "plugin": "plugin name", + "pluginDir": /plugins, + "pluginPath": /plugins/plugin name, + "quiet": false, + "silent": false, + } + `); +}); - expect(settings.plugin).toBe(command); - }); - }); - }); - }); - }); +it('overrides the defaults with the parsed cli options', () => { + expect( + parse(command, { + quiet: true, + silent: true, + config: 'foo/bar', + }) + ).toMatchInlineSnapshot(` + Object { + "config": "foo/bar", + "plugin": "plugin name", + "pluginDir": /plugins, + "pluginPath": /plugins/plugin name, + "quiet": true, + "silent": true, + } + `); }); diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index eb54983d0be135..8853d951819947 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -109,6 +109,7 @@ export class DocLinksService { loadingData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/tutorial-load-dataset.html`, introduction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-patterns.html`, }, + addData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/connect-to-elasticsearch.html`, kibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index.html`, siem: { guide: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/index.html`, @@ -209,6 +210,7 @@ export interface DocLinksStart { readonly loadingData: string; readonly introduction: string; }; + readonly addData: string; readonly kibana: string; readonly siem: { readonly guide: string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 9b421e0172df0c..0e879d16b4637a 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -576,6 +576,7 @@ export interface DocLinksStart { readonly loadingData: string; readonly introduction: string; }; + readonly addData: string; readonly kibana: string; readonly siem: { readonly guide: string; diff --git a/src/cli_plugin/list/settings.js b/src/dev/build/lib/__mocks__/get_build_number.ts similarity index 87% rename from src/cli_plugin/list/settings.js rename to src/dev/build/lib/__mocks__/get_build_number.ts index d17ce5deaec304..60cfd3d82557a3 100644 --- a/src/cli_plugin/list/settings.js +++ b/src/dev/build/lib/__mocks__/get_build_number.ts @@ -17,10 +17,6 @@ * under the License. */ -export function parse(command) { - const settings = { - pluginDir: command.pluginDir || '', - }; - - return settings; +export function getBuildNumber() { + return 12345; } diff --git a/src/dev/build/lib/get_build_number.ts b/src/dev/build/lib/get_build_number.ts new file mode 100644 index 00000000000000..c512042592002a --- /dev/null +++ b/src/dev/build/lib/get_build_number.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import os from 'os'; +import execa from 'execa'; + +export async function getBuildNumber() { + if (/^win/.test(os.platform())) { + // Windows does not have the wc process and `find /C /V ""` does not consistently work + const log = await execa('git', ['log', '--format="%h"']); + return log.stdout.split('\n').length; + } + + const wc = await execa.command('git log --format="%h" | wc -l', { + shell: true, + }); + return parseFloat(wc.stdout.trim()); +} diff --git a/src/dev/build/lib/version_info.test.ts b/src/dev/build/lib/version_info.test.ts index 1b0c71bf9220ee..ec8c363ddaccf1 100644 --- a/src/dev/build/lib/version_info.test.ts +++ b/src/dev/build/lib/version_info.test.ts @@ -20,6 +20,8 @@ import pkg from '../../../../package.json'; import { getVersionInfo } from './version_info'; +jest.mock('./get_build_number'); + describe('isRelease = true', () => { it('returns unchanged package.version, build sha, and build number', async () => { const versionInfo = await getVersionInfo({ diff --git a/src/dev/build/lib/version_info.ts b/src/dev/build/lib/version_info.ts index 958112c524bac7..fb3530b4c4edf2 100644 --- a/src/dev/build/lib/version_info.ts +++ b/src/dev/build/lib/version_info.ts @@ -17,22 +17,8 @@ * under the License. */ -import os from 'os'; - import execa from 'execa'; - -async function getBuildNumber() { - if (/^win/.test(os.platform())) { - // Windows does not have the wc process and `find /C /V ""` does not consistently work - const log = await execa('git', ['log', '--format="%h"']); - return log.stdout.split('\n').length; - } - - const wc = await execa.command('git log --format="%h" | wc -l', { - shell: true, - }); - return parseFloat(wc.stdout.trim()); -} +import { getBuildNumber } from './get_build_number'; interface Options { isRelease: boolean; diff --git a/src/dev/build/tasks/nodejs/download_node_builds_task.test.ts b/src/dev/build/tasks/nodejs/download_node_builds_task.test.ts index 6f08c8aa697506..413bf95cde8773 100644 --- a/src/dev/build/tasks/nodejs/download_node_builds_task.test.ts +++ b/src/dev/build/tasks/nodejs/download_node_builds_task.test.ts @@ -26,13 +26,10 @@ import { import { Config, Platform } from '../../lib'; import { DownloadNodeBuilds } from './download_node_builds_task'; -// import * as NodeShasumsNS from '../node_shasums'; -// import * as NodeDownloadInfoNS from '../node_download_info'; -// import * as DownloadNS from '../../../lib/download'; -// import { DownloadNodeBuilds } from '../download_node_builds_task'; jest.mock('./node_shasums'); jest.mock('./node_download_info'); jest.mock('../../lib/download'); +jest.mock('../../lib/get_build_number'); expect.addSnapshotSerializer(createAnyInstanceSerializer(ToolingLog)); diff --git a/src/dev/build/tasks/nodejs/extract_node_builds_task.test.ts b/src/dev/build/tasks/nodejs/extract_node_builds_task.test.ts index 94c421f7c9a626..f1700ef7b578c1 100644 --- a/src/dev/build/tasks/nodejs/extract_node_builds_task.test.ts +++ b/src/dev/build/tasks/nodejs/extract_node_builds_task.test.ts @@ -27,6 +27,7 @@ import { Config } from '../../lib'; import { ExtractNodeBuilds } from './extract_node_builds_task'; jest.mock('../../lib/fs'); +jest.mock('../../lib/get_build_number'); const Fs = jest.requireMock('../../lib/fs'); diff --git a/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts b/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts index f24b7ffc59c148..19416963d5eddc 100644 --- a/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts +++ b/src/dev/build/tasks/nodejs/verify_existing_node_builds_task.test.ts @@ -29,6 +29,7 @@ import { VerifyExistingNodeBuilds } from './verify_existing_node_builds_task'; jest.mock('./node_shasums'); jest.mock('./node_download_info'); jest.mock('../../lib/fs'); +jest.mock('../../lib/get_build_number'); const { getNodeShasums } = jest.requireMock('./node_shasums'); const { getNodeDownloadInfo } = jest.requireMock('./node_download_info'); diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx new file mode 100644 index 00000000000000..c2f529fe399f3f --- /dev/null +++ b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx @@ -0,0 +1,156 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EmbeddableInput, + SavedObjectEmbeddableInput, + isSavedObjectEmbeddableInput, + IEmbeddable, +} from '../embeddable_plugin'; +import { + SavedObjectsClientContract, + SimpleSavedObject, + I18nStart, + NotificationsStart, +} from '../../../../core/public'; +import { + SavedObjectSaveModal, + showSaveModal, + OnSaveProps, + SaveResult, +} from '../../../saved_objects/public'; + +/** + * The attribute service is a shared, generic service that embeddables can use to provide the functionality + * required to fulfill the requirements of the ReferenceOrValueEmbeddable interface. The attribute_service + * can also be used as a higher level wrapper to transform an embeddable input shape that references a saved object + * into an embeddable input shape that contains that saved object's attributes by value. + */ +export class AttributeService< + SavedObjectAttributes extends { title: string }, + ValType extends EmbeddableInput & { attributes: SavedObjectAttributes }, + RefType extends SavedObjectEmbeddableInput +> { + constructor( + private type: string, + private savedObjectsClient: SavedObjectsClientContract, + private i18nContext: I18nStart['Context'], + private toasts: NotificationsStart['toasts'] + ) {} + + public async unwrapAttributes(input: RefType | ValType): Promise { + if (this.inputIsRefType(input)) { + const savedObject: SimpleSavedObject = await this.savedObjectsClient.get< + SavedObjectAttributes + >(this.type, input.savedObjectId); + return savedObject.attributes; + } + return input.attributes; + } + + public async wrapAttributes( + newAttributes: SavedObjectAttributes, + useRefType: boolean, + embeddable?: IEmbeddable + ): Promise> { + const savedObjectId = + embeddable && isSavedObjectEmbeddableInput(embeddable.getInput()) + ? (embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId + : undefined; + if (!useRefType) { + return { attributes: newAttributes } as ValType; + } else { + try { + if (savedObjectId) { + await this.savedObjectsClient.update(this.type, savedObjectId, newAttributes); + return { savedObjectId } as RefType; + } else { + const savedItem = await this.savedObjectsClient.create(this.type, newAttributes); + return { savedObjectId: savedItem.id } as RefType; + } + } catch (error) { + this.toasts.addDanger({ + title: i18n.translate('dashboard.attributeService.saveToLibraryError', { + defaultMessage: `Panel was not saved to the library. Error: {errorMessage}`, + values: { + errorMessage: error.message, + }, + }), + 'data-test-subj': 'saveDashboardFailure', + }); + return Promise.reject({ error }); + } + } + } + + inputIsRefType = (input: ValType | RefType): input is RefType => { + return isSavedObjectEmbeddableInput(input); + }; + + getInputAsValueType = async (input: ValType | RefType): Promise => { + if (!this.inputIsRefType(input)) { + return input; + } + const attributes = await this.unwrapAttributes(input); + return { + ...input, + savedObjectId: undefined, + attributes, + }; + }; + + getInputAsRefType = async ( + input: ValType | RefType, + saveOptions?: { showSaveModal: boolean } | { title: string } + ): Promise => { + if (this.inputIsRefType(input)) { + return input; + } + + return new Promise((resolve, reject) => { + const onSave = async (props: OnSaveProps): Promise => { + try { + input.attributes.title = props.newTitle; + const wrappedInput = (await this.wrapAttributes(input.attributes, true)) as RefType; + resolve(wrappedInput); + return { id: wrappedInput.savedObjectId }; + } catch (error) { + reject(); + return { error }; + } + }; + + if (saveOptions && (saveOptions as { showSaveModal: boolean }).showSaveModal) { + showSaveModal( + reject()} + title={input.attributes.title} + showCopyOnSave={false} + objectType={this.type} + showDescription={false} + />, + this.i18nContext + ); + } + }); + }; +} diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts index dcfde67cd9f131..8a9954cc77a2e5 100644 --- a/src/plugins/dashboard/public/index.ts +++ b/src/plugins/dashboard/public/index.ts @@ -40,6 +40,7 @@ export { export { addEmbeddableToDashboardUrl } from './url_utils/url_helper'; export { SavedObjectDashboard } from './saved_dashboards'; export { SavedDashboardPanel } from './types'; +export { AttributeService } from './attribute_service/attribute_service'; export function plugin(initializerContext: PluginInitializerContext) { return new DashboardPlugin(initializerContext); diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index f1319665d258b4..3b0863a9f46516 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -34,7 +34,13 @@ import { ScopedHistory, } from 'src/core/public'; import { UsageCollectionSetup } from '../../usage_collection/public'; -import { CONTEXT_MENU_TRIGGER, EmbeddableSetup, EmbeddableStart } from '../../embeddable/public'; +import { + CONTEXT_MENU_TRIGGER, + EmbeddableSetup, + EmbeddableStart, + SavedObjectEmbeddableInput, + EmbeddableInput, +} from '../../embeddable/public'; import { DataPublicPluginSetup, DataPublicPluginStart, esFilters } from '../../data/public'; import { SharePluginSetup, SharePluginStart, UrlGeneratorContract } from '../../share/public'; import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public'; @@ -85,6 +91,7 @@ import { DashboardConstants } from './dashboard_constants'; import { addEmbeddableToDashboardUrl } from './url_utils/url_helper'; import { PlaceholderEmbeddableFactory } from './application/embeddable/placeholder'; import { UrlGeneratorState } from '../../share/public'; +import { AttributeService } from '.'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -131,6 +138,13 @@ export interface DashboardStart { dashboardUrlGenerator?: DashboardUrlGenerator; dashboardFeatureFlagConfig: DashboardFeatureFlagConfig; DashboardContainerByValueRenderer: ReturnType; + getAttributeService: < + A extends { title: string }, + V extends EmbeddableInput & { attributes: A }, + R extends SavedObjectEmbeddableInput + >( + type: string + ) => AttributeService; } declare module '../../../plugins/ui_actions/public' { @@ -420,6 +434,13 @@ export class DashboardPlugin DashboardContainerByValueRenderer: createDashboardContainerByValueRenderer({ factory: dashboardContainerFactory, }), + getAttributeService: (type: string) => + new AttributeService( + type, + core.savedObjects.client, + core.i18n.Context, + core.notifications.toasts + ), }; } diff --git a/src/plugins/data/public/index_patterns/index_patterns/redirect_no_index_pattern.tsx b/src/plugins/data/public/index_patterns/index_patterns/redirect_no_index_pattern.tsx index 7ed6525db6350e..ac015f8dd33afb 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/redirect_no_index_pattern.tsx +++ b/src/plugins/data/public/index_patterns/index_patterns/redirect_no_index_pattern.tsx @@ -40,7 +40,7 @@ export const onRedirectNoIndexPattern = ( const bannerMessage = i18n.translate('data.indexPatterns.ensureDefaultIndexPattern.bannerLabel', { defaultMessage: - "In order to visualize and explore data in Kibana, you'll need to create an index pattern to retrieve data from Elasticsearch.", + 'To visualize and explore data in Kibana, you must create an index pattern to retrieve data from Elasticsearch.', }); // Avoid being hostile to new users who don't have an index pattern setup yet diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 135b6121d1dd5e..3fc1e6454829d6 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -44,6 +44,7 @@ const createSetupContract = (): Setup => { search: searchServiceMock.createSetupContract(), fieldFormats: fieldFormatsServiceMock.createSetupContract(), query: querySetupMock, + __enhance: jest.fn(), }; }; diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 68c0f506f121d9..e950434b287a75 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -34,6 +34,7 @@ import { DataSetupDependencies, DataStartDependencies, InternalStartServices, + DataPublicPluginEnhancements, } from './types'; import { AutocompleteService } from './autocomplete'; import { SearchService } from './search/search_service'; @@ -156,16 +157,21 @@ export class DataPublicPlugin })) ); + const searchService = this.searchService.setup(core, { + expressions, + usageCollection, + getInternalStartServices, + packageInfo: this.packageInfo, + }); + return { autocomplete: this.autocomplete.setup(core), - search: this.searchService.setup(core, { - expressions, - usageCollection, - getInternalStartServices, - packageInfo: this.packageInfo, - }), + search: searchService, fieldFormats: this.fieldFormatsService.setup(core), query: queryService, + __enhance: (enhancements: DataPublicPluginEnhancements) => { + searchService.__enhance(enhancements.search); + }, }; } diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 6225d74fb1b310..a61334905e9f58 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -8,12 +8,12 @@ import { $Values } from '@kbn/utility-types'; import _ from 'lodash'; import { Action } from 'history'; import { ApiResponse } from '@elastic/elasticsearch/lib/Transport'; -import { ApplicationStart } from 'kibana/public'; import { Assign } from '@kbn/utility-types'; import { BehaviorSubject } from 'rxjs'; import Boom from 'boom'; import { Component } from 'react'; import { CoreSetup } from 'src/core/public'; +import { CoreSetup as CoreSetup_2 } from 'kibana/public'; import { CoreStart } from 'kibana/public'; import { CoreStart as CoreStart_2 } from 'src/core/public'; import { Ensure } from '@kbn/utility-types'; @@ -65,7 +65,7 @@ import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/ex import { Subscription } from 'rxjs'; import { Toast } from 'kibana/public'; import { ToastInputFields } from 'src/core/public/notifications'; -import { ToastsStart } from 'kibana/public'; +import { ToastsSetup } from 'kibana/public'; import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; @@ -222,6 +222,10 @@ export type CustomFilter = Filter & { // // @public (undocumented) export interface DataPublicPluginSetup { + // Warning: (ae-forgotten-export) The symbol "DataPublicPluginEnhancements" needs to be exported by the entry point index.d.ts + // + // @internal (undocumented) + __enhance: (enhancements: DataPublicPluginEnhancements) => void; // Warning: (ae-forgotten-export) The symbol "AutocompleteSetup" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1714,15 +1718,19 @@ export interface SearchError { // @public (undocumented) export class SearchInterceptor { constructor(deps: SearchInterceptorDeps, requestTimeout?: number | undefined); + // @internal protected abortController: AbortController; + // @internal (undocumented) + protected application: CoreStart['application']; // (undocumented) protected readonly deps: SearchInterceptorDeps; - getPendingCount$: () => Observable; - // (undocumented) + getPendingCount$(): Observable; + // @internal (undocumented) protected hideToast: () => void; + // @internal protected longRunningToast?: Toast; + // @internal protected pendingCount$: BehaviorSubject; - protected pendingCount: number; // (undocumented) protected readonly requestTimeout?: number | undefined; // (undocumented) @@ -1733,8 +1741,9 @@ export class SearchInterceptor { combinedSignal: AbortSignal; cleanup: () => void; }; - // (undocumented) + // @internal (undocumented) protected showToast: () => void; + // @internal protected timeoutSubscriptions: Subscription; } @@ -1743,13 +1752,13 @@ export class SearchInterceptor { // @public (undocumented) export interface SearchInterceptorDeps { // (undocumented) - application: ApplicationStart; + http: CoreSetup_2['http']; // (undocumented) - http: CoreStart['http']; + startServices: Promise<[CoreStart, any, unknown]>; // (undocumented) - toasts: ToastsStart; + toasts: ToastsSetup; // (undocumented) - uiSettings: CoreStart['uiSettings']; + uiSettings: CoreSetup_2['uiSettings']; // Warning: (ae-forgotten-export) The symbol "SearchUsageCollector" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1980,9 +1989,9 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:393:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:396:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:45:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/types.ts:54:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/types.ts:55:5 - (ae-forgotten-export) The symbol "createFiltersFromRangeSelectAction" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/types.ts:63:5 - (ae-forgotten-export) The symbol "IndexPatternSelectProps" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/types.ts:62:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/types.ts:63:5 - (ae-forgotten-export) The symbol "createFiltersFromRangeSelectAction" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/types.ts:71:5 - (ae-forgotten-export) The symbol "IndexPatternSelectProps" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index 96445e5367147d..ae028df31e401e 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -21,7 +21,14 @@ export * from './aggs'; export * from './expressions'; export * from './tabify'; -export { ISearch, ISearchOptions, ISearchGeneric, ISearchSetup, ISearchStart } from './types'; +export { + ISearch, + ISearchOptions, + ISearchGeneric, + ISearchSetup, + ISearchStart, + SearchEnhancements, +} from './types'; export { IEsSearchResponse, IEsSearchRequest, ES_SEARCH_STRATEGY } from '../../common/search'; diff --git a/src/plugins/data/public/search/mocks.ts b/src/plugins/data/public/search/mocks.ts index c56331baffed2e..8ccf46fe7c97d2 100644 --- a/src/plugins/data/public/search/mocks.ts +++ b/src/plugins/data/public/search/mocks.ts @@ -26,13 +26,13 @@ export * from './search_source/mocks'; function createSetupContract(): jest.Mocked { return { aggs: searchAggsSetupMock(), + __enhance: jest.fn(), }; } function createStartContract(): jest.Mocked { return { aggs: searchAggsStartMock(), - setInterceptor: jest.fn(), search: jest.fn(), searchSource: searchSourceMock, __LEGACY: { diff --git a/src/plugins/data/public/search/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor.test.ts index f4c5de2bcaf313..2eded17bda88c8 100644 --- a/src/plugins/data/public/search/search_interceptor.test.ts +++ b/src/plugins/data/public/search/search_interceptor.test.ts @@ -17,27 +17,27 @@ * under the License. */ -import { CoreStart } from '../../../../core/public'; +import { CoreSetup } from '../../../../core/public'; import { coreMock } from '../../../../core/public/mocks'; import { IEsSearchRequest } from '../../common/search'; import { SearchInterceptor } from './search_interceptor'; import { AbortError } from '../../common'; let searchInterceptor: SearchInterceptor; -let mockCoreStart: MockedKeys; +let mockCoreSetup: MockedKeys; const flushPromises = () => new Promise((resolve) => setImmediate(resolve)); jest.useFakeTimers(); describe('SearchInterceptor', () => { beforeEach(() => { - mockCoreStart = coreMock.createStart(); + mockCoreSetup = coreMock.createSetup(); searchInterceptor = new SearchInterceptor( { - toasts: mockCoreStart.notifications.toasts, - application: mockCoreStart.application, - uiSettings: mockCoreStart.uiSettings, - http: mockCoreStart.http, + toasts: mockCoreSetup.notifications.toasts, + startServices: mockCoreSetup.getStartServices(), + uiSettings: mockCoreSetup.uiSettings, + http: mockCoreSetup.http, }, 1000 ); @@ -46,7 +46,7 @@ describe('SearchInterceptor', () => { describe('search', () => { test('Observable should resolve if fetch is successful', async () => { const mockResponse: any = { result: 200 }; - mockCoreStart.http.fetch.mockResolvedValueOnce(mockResponse); + mockCoreSetup.http.fetch.mockResolvedValueOnce(mockResponse); const mockRequest: IEsSearchRequest = { params: {}, }; @@ -58,7 +58,7 @@ describe('SearchInterceptor', () => { test('Observable should fail if fetch has an error', async () => { const mockResponse: any = { result: 500 }; - mockCoreStart.http.fetch.mockRejectedValueOnce(mockResponse); + mockCoreSetup.http.fetch.mockRejectedValueOnce(mockResponse); const mockRequest: IEsSearchRequest = { params: {}, }; @@ -72,7 +72,7 @@ describe('SearchInterceptor', () => { }); test('Observable should fail if fetch times out (test merged signal)', async () => { - mockCoreStart.http.fetch.mockImplementationOnce((options: any) => { + mockCoreSetup.http.fetch.mockImplementationOnce((options: any) => { return new Promise((resolve, reject) => { options.signal.addEventListener('abort', () => { reject(new AbortError()); @@ -100,7 +100,7 @@ describe('SearchInterceptor', () => { test('Observable should fail if user aborts (test merged signal)', async () => { const abortController = new AbortController(); - mockCoreStart.http.fetch.mockImplementationOnce((options: any) => { + mockCoreSetup.http.fetch.mockImplementationOnce((options: any) => { return new Promise((resolve, reject) => { options.signal.addEventListener('abort', () => { reject(new AbortError()); @@ -136,7 +136,7 @@ describe('SearchInterceptor', () => { const error = (e: any) => { expect(e).toBeInstanceOf(AbortError); - expect(mockCoreStart.http.fetch).not.toBeCalled(); + expect(mockCoreSetup.http.fetch).not.toBeCalled(); done(); }; response.subscribe({ error }); @@ -150,7 +150,7 @@ describe('SearchInterceptor', () => { pendingCount$.subscribe(pendingNext); const mockResponse: any = { result: 200 }; - mockCoreStart.http.fetch.mockResolvedValue(mockResponse); + mockCoreSetup.http.fetch.mockResolvedValue(mockResponse); const mockRequest: IEsSearchRequest = { params: {}, }; @@ -169,7 +169,7 @@ describe('SearchInterceptor', () => { pendingCount$.subscribe(pendingNext); const mockResponse: any = { result: 500 }; - mockCoreStart.http.fetch.mockRejectedValue(mockResponse); + mockCoreSetup.http.fetch.mockRejectedValue(mockResponse); const mockRequest: IEsSearchRequest = { params: {}, }; diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index d6fcde8e986f36..99fccda7fddf35 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -20,7 +20,7 @@ import { trimEnd } from 'lodash'; import { BehaviorSubject, throwError, timer, Subscription, defer, from, Observable } from 'rxjs'; import { finalize, filter } from 'rxjs/operators'; -import { ApplicationStart, Toast, ToastsStart, CoreStart } from 'kibana/public'; +import { Toast, CoreStart, ToastsSetup, CoreSetup } from 'kibana/public'; import { getCombinedSignal, AbortError } from '../../common/utils'; import { IEsSearchRequest, IEsSearchResponse, ES_SEARCH_STRATEGY } from '../../common/search'; import { ISearchOptions } from './types'; @@ -30,39 +30,43 @@ import { SearchUsageCollector } from './collectors'; const LONG_QUERY_NOTIFICATION_DELAY = 10000; export interface SearchInterceptorDeps { - toasts: ToastsStart; - application: ApplicationStart; - http: CoreStart['http']; - uiSettings: CoreStart['uiSettings']; + toasts: ToastsSetup; + http: CoreSetup['http']; + uiSettings: CoreSetup['uiSettings']; + startServices: Promise<[CoreStart, any, unknown]>; usageCollector?: SearchUsageCollector; } export class SearchInterceptor { /** * `abortController` used to signal all searches to abort. + * @internal */ protected abortController = new AbortController(); - /** - * The number of pending search requests. - */ - protected pendingCount = 0; - /** * Observable that emits when the number of pending requests changes. + * @internal */ - protected pendingCount$ = new BehaviorSubject(this.pendingCount); + protected pendingCount$ = new BehaviorSubject(0); /** * The subscriptions from scheduling the automatic timeout for each request. + * @internal */ protected timeoutSubscriptions: Subscription = new Subscription(); /** * The current long-running toast (if there is one). + * @internal */ protected longRunningToast?: Toast; + /** + * @internal + */ + protected application!: CoreStart['application']; + /** * This class should be instantiated with a `requestTimeout` corresponding with how many ms after * requests are initiated that they should automatically cancel. @@ -76,6 +80,10 @@ export class SearchInterceptor { ) { this.deps.http.addLoadingCountSource(this.pendingCount$); + this.deps.startServices.then(([coreStart]) => { + this.application = coreStart.application; + }); + // When search requests go out, a notification is scheduled allowing users to continue the // request past the timeout. When all search requests complete, we remove the notification. this.getPendingCount$() @@ -87,9 +95,9 @@ export class SearchInterceptor { * Returns an `Observable` over the current number of pending searches. This could mean that one * of the search requests is still in flight, or that it has only received partial responses. */ - public getPendingCount$ = () => { + public getPendingCount$() { return this.pendingCount$.asObservable(); - }; + } protected runSearch( request: IEsSearchRequest, @@ -112,7 +120,7 @@ export class SearchInterceptor { /** * Searches using the given `search` method. Overrides the `AbortSignal` with one that will abort * either when `cancelPending` is called, when the request times out, or when the original - * `AbortSignal` is aborted. Updates the `pendingCount` when the request is started/finalized. + * `AbortSignal` is aborted. Updates `pendingCount$` when the request is started/finalized. */ public search( request: IEsSearchRequest, @@ -125,11 +133,11 @@ export class SearchInterceptor { } const { combinedSignal, cleanup } = this.setupTimers(options); - this.pendingCount$.next(++this.pendingCount); + this.pendingCount$.next(this.pendingCount$.getValue() + 1); return this.runSearch(request, combinedSignal, options?.strategy).pipe( finalize(() => { - this.pendingCount$.next(--this.pendingCount); + this.pendingCount$.next(this.pendingCount$.getValue() - 1); cleanup(); }) ); @@ -173,13 +181,16 @@ export class SearchInterceptor { }; } + /** + * @internal + */ protected showToast = () => { if (this.longRunningToast) return; this.longRunningToast = this.deps.toasts.addInfo( { title: 'Your query is taking a while', text: getLongQueryNotification({ - application: this.deps.application, + application: this.application, }), }, { @@ -188,6 +199,9 @@ export class SearchInterceptor { ); }; + /** + * @internal + */ protected hideToast = () => { if (this.longRunningToast) { this.deps.toasts.remove(this.longRunningToast); @@ -198,3 +212,5 @@ export class SearchInterceptor { } }; } + +export type ISearchInterceptor = PublicMethodsOf; diff --git a/src/plugins/data/public/search/search_service.test.ts b/src/plugins/data/public/search/search_service.test.ts index 55d31db1917338..f0a017847e06aa 100644 --- a/src/plugins/data/public/search/search_service.test.ts +++ b/src/plugins/data/public/search/search_service.test.ts @@ -41,6 +41,7 @@ describe('Search service', () => { expressions: expressionsPluginMock.createSetupContract(), } as any); expect(setup).toHaveProperty('aggs'); + expect(setup).toHaveProperty('__enhance'); }); }); @@ -49,7 +50,6 @@ describe('Search service', () => { const start = searchService.start(mockCoreStart, { indexPatterns: {}, } as any); - expect(start).toHaveProperty('setInterceptor'); expect(start).toHaveProperty('search'); }); }); diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 064e16014cb70a..4c94925b66d6e2 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -18,7 +18,7 @@ */ import { Plugin, CoreSetup, CoreStart, PackageInfo } from '../../../../core/public'; -import { ISearchSetup, ISearchStart } from './types'; +import { ISearchSetup, ISearchStart, SearchEnhancements } from './types'; import { ExpressionsSetup } from '../../../../plugins/expressions/public'; import { createSearchSource, SearchSource, SearchSourceDependencies } from './search_source'; @@ -28,7 +28,7 @@ import { calculateBounds, TimeRange } from '../../common/query'; import { IndexPatternsContract } from '../index_patterns/index_patterns'; import { GetInternalStartServicesFn } from '../types'; -import { SearchInterceptor } from './search_interceptor'; +import { ISearchInterceptor, SearchInterceptor } from './search_interceptor'; import { getAggTypes, getAggTypesFunctions, @@ -54,7 +54,7 @@ interface SearchServiceStartDependencies { export class SearchService implements Plugin { private esClient?: LegacyApiCaller; private readonly aggTypesRegistry = new AggTypesRegistry(); - private searchInterceptor!: SearchInterceptor; + private searchInterceptor!: ISearchInterceptor; private usageCollector?: SearchUsageCollector; /** @@ -91,15 +91,6 @@ export class SearchService implements Plugin { const aggFunctions = getAggTypesFunctions(); aggFunctions.forEach((fn) => expressions.registerFunction(fn)); - return { - aggs: { - calculateAutoTimeExpression: getCalculateAutoTimeExpression(core.uiSettings), - types: aggTypesSetup, - }, - }; - } - - public start(core: CoreStart, dependencies: SearchServiceStartDependencies): ISearchStart { /** * A global object that intercepts all searches and provides convenience methods for cancelling * all pending search requests, as well as getting the number of pending search requests. @@ -109,14 +100,27 @@ export class SearchService implements Plugin { this.searchInterceptor = new SearchInterceptor( { toasts: core.notifications.toasts, - application: core.application, http: core.http, uiSettings: core.uiSettings, + startServices: core.getStartServices(), usageCollector: this.usageCollector!, }, core.injectedMetadata.getInjectedVar('esRequestTimeout') as number ); + return { + usageCollector: this.usageCollector!, + __enhance: (enhancements: SearchEnhancements) => { + this.searchInterceptor = enhancements.searchInterceptor; + }, + aggs: { + calculateAutoTimeExpression: getCalculateAutoTimeExpression(core.uiSettings), + types: aggTypesSetup, + }, + }; + } + + public start(core: CoreStart, dependencies: SearchServiceStartDependencies): ISearchStart { const aggTypesStart = this.aggTypesRegistry.start(); const search: ISearchGeneric = (request, options) => { @@ -145,17 +149,12 @@ export class SearchService implements Plugin { types: aggTypesStart, }, search, - usageCollector: this.usageCollector!, searchSource: { create: createSearchSource(dependencies.indexPatterns, searchSourceDependencies), createEmpty: () => { return new SearchSource({}, searchSourceDependencies); }, }, - setInterceptor: (searchInterceptor: SearchInterceptor) => { - // TODO: should an intercepror have a destroy method? - this.searchInterceptor = searchInterceptor; - }, __LEGACY: legacySearch, }; } diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index f80a13d048a684..d85d4c4e5c9354 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -21,7 +21,7 @@ import { Observable } from 'rxjs'; import { PackageInfo } from 'kibana/server'; import { SearchAggsSetup, SearchAggsStart } from './aggs'; import { LegacyApiCaller } from './legacy/es_client'; -import { SearchInterceptor } from './search_interceptor'; +import { ISearchInterceptor } from './search_interceptor'; import { ISearchSource, SearchSourceFields } from './search_source'; import { SearchUsageCollector } from './collectors'; import { @@ -54,23 +54,33 @@ export interface ISearchStartLegacy { esClient: LegacyApiCaller; } +export interface SearchEnhancements { + searchInterceptor: ISearchInterceptor; +} /** * The setup contract exposed by the Search plugin exposes the search strategy extension * point. */ export interface ISearchSetup { aggs: SearchAggsSetup; + usageCollector?: SearchUsageCollector; + /** + * @internal + */ + __enhance: (enhancements: SearchEnhancements) => void; } export interface ISearchStart { aggs: SearchAggsStart; - setInterceptor: (searchInterceptor: SearchInterceptor) => void; search: ISearchGeneric; searchSource: { create: (fields?: SearchSourceFields) => Promise; createEmpty: () => ISearchSource; }; - usageCollector?: SearchUsageCollector; + /** + * @deprecated + * @internal + */ __LEGACY: ISearchStartLegacy; } diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index 6d671272514244..c39b7d355d4954 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -25,13 +25,17 @@ import { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public'; import { AutocompleteSetup, AutocompleteStart } from './autocomplete'; import { FieldFormatsSetup, FieldFormatsStart } from './field_formats'; import { createFiltersFromRangeSelectAction, createFiltersFromValueClickAction } from './actions'; -import { ISearchSetup, ISearchStart } from './search'; +import { ISearchSetup, ISearchStart, SearchEnhancements } from './search'; import { QuerySetup, QueryStart } from './query'; import { IndexPatternSelectProps } from './ui/index_pattern_select'; import { IndexPatternsContract } from './index_patterns'; import { StatefulSearchBarProps } from './ui/search_bar/create_search_bar'; import { UsageCollectionSetup } from '../../usage_collection/public'; +export interface DataPublicPluginEnhancements { + search: SearchEnhancements; +} + export interface DataSetupDependencies { expressions: ExpressionsSetup; uiActions: UiActionsSetup; @@ -47,6 +51,10 @@ export interface DataPublicPluginSetup { search: ISearchSetup; fieldFormats: FieldFormatsSetup; query: QuerySetup; + /** + * @internal + */ + __enhance: (enhancements: DataPublicPluginEnhancements) => void; } export interface DataPublicPluginStart { diff --git a/src/plugins/data/server/saved_objects/index_patterns.ts b/src/plugins/data/server/saved_objects/index_patterns.ts index 44d2813f6e3e81..6aabcdf7c1c01b 100644 --- a/src/plugins/data/server/saved_objects/index_patterns.ts +++ b/src/plugins/data/server/saved_objects/index_patterns.ts @@ -42,16 +42,10 @@ export const indexPatternSavedObjectType: SavedObjectsType = { }, }, mappings: { + dynamic: false, properties: { - fieldFormatMap: { type: 'text' }, - fields: { type: 'text' }, - intervalName: { type: 'keyword' }, - notExpandable: { type: 'boolean' }, - sourceFilters: { type: 'text' }, - timeFieldName: { type: 'keyword' }, title: { type: 'text' }, type: { type: 'keyword' }, - typeMeta: { type: 'keyword' }, }, }, migrations: indexPatternSavedObjectTypeMigrations as any, diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index fafbdda148de87..57253c1f741abc 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -28,7 +28,8 @@ export { ACTION_EDIT_PANEL, Adapters, AddPanelAction, - AttributeService, + ReferenceOrValueEmbeddable, + isReferenceOrValueEmbeddable, ChartActionContext, Container, ContainerInput, diff --git a/src/plugins/embeddable/public/lib/embeddables/attribute_service.ts b/src/plugins/embeddable/public/lib/embeddables/attribute_service.ts deleted file mode 100644 index a33f592350d9a8..00000000000000 --- a/src/plugins/embeddable/public/lib/embeddables/attribute_service.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { SavedObjectsClientContract } from '../../../../../core/public'; -import { - SavedObjectEmbeddableInput, - isSavedObjectEmbeddableInput, - EmbeddableInput, - IEmbeddable, -} from '.'; -import { SimpleSavedObject } from '../../../../../core/public'; - -export class AttributeService< - SavedObjectAttributes, - ValType extends EmbeddableInput & { attributes: SavedObjectAttributes }, - RefType extends SavedObjectEmbeddableInput -> { - constructor(private type: string, private savedObjectsClient: SavedObjectsClientContract) {} - - public async unwrapAttributes(input: RefType | ValType): Promise { - if (isSavedObjectEmbeddableInput(input)) { - const savedObject: SimpleSavedObject = await this.savedObjectsClient.get< - SavedObjectAttributes - >(this.type, input.savedObjectId); - return savedObject.attributes; - } - return input.attributes; - } - - public async wrapAttributes( - newAttributes: SavedObjectAttributes, - useRefType: boolean, - embeddable?: IEmbeddable - ): Promise> { - const savedObjectId = - embeddable && isSavedObjectEmbeddableInput(embeddable.getInput()) - ? (embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId - : undefined; - - if (useRefType) { - if (savedObjectId) { - await this.savedObjectsClient.update(this.type, savedObjectId, newAttributes); - return { savedObjectId } as RefType; - } else { - const savedItem = await this.savedObjectsClient.create(this.type, newAttributes); - return { savedObjectId: savedItem.id } as RefType; - } - } else { - return { attributes: newAttributes } as ValType; - } - } -} diff --git a/src/plugins/embeddable/public/lib/embeddables/index.ts b/src/plugins/embeddable/public/lib/embeddables/index.ts index 06cb6e322acf39..5bab5ac27f3cc9 100644 --- a/src/plugins/embeddable/public/lib/embeddables/index.ts +++ b/src/plugins/embeddable/public/lib/embeddables/index.ts @@ -25,5 +25,4 @@ export { ErrorEmbeddable, isErrorEmbeddable } from './error_embeddable'; export { withEmbeddableSubscription } from './with_subscription'; export { EmbeddableRoot } from './embeddable_root'; export * from './saved_object_embeddable'; -export { AttributeService } from './attribute_service'; export { EmbeddableRenderer, EmbeddableRendererProps } from './embeddable_renderer'; diff --git a/src/plugins/embeddable/public/lib/index.ts b/src/plugins/embeddable/public/lib/index.ts index b757fa59a7f3a6..aef4c33ee1078c 100644 --- a/src/plugins/embeddable/public/lib/index.ts +++ b/src/plugins/embeddable/public/lib/index.ts @@ -25,3 +25,4 @@ export * from './triggers'; export * from './containers'; export * from './panel'; export * from './state_transfer'; +export * from './reference_or_value_embeddable'; diff --git a/src/cli_plugin/lib/warn_if_plugin_dir_option.js b/src/plugins/embeddable/public/lib/reference_or_value_embeddable/index.ts similarity index 72% rename from src/cli_plugin/lib/warn_if_plugin_dir_option.js rename to src/plugins/embeddable/public/lib/reference_or_value_embeddable/index.ts index eb85ba063c3c95..e9b8521a35ba5b 100644 --- a/src/cli_plugin/lib/warn_if_plugin_dir_option.js +++ b/src/plugins/embeddable/public/lib/reference_or_value_embeddable/index.ts @@ -17,11 +17,4 @@ * under the License. */ -export function warnIfUsingPluginDirOption(options, defaultValue, logger) { - if (options && options.pluginDir !== defaultValue) { - logger.log( - 'Warning: Using the -d, --plugin-dir option is deprecated, and is ' + - 'known to not work for all plugins, including X-Pack.' - ); - } -} +export { ReferenceOrValueEmbeddable, isReferenceOrValueEmbeddable } from './types'; diff --git a/src/plugins/embeddable/public/lib/reference_or_value_embeddable/types.ts b/src/plugins/embeddable/public/lib/reference_or_value_embeddable/types.ts new file mode 100644 index 00000000000000..eaf5c94a09138d --- /dev/null +++ b/src/plugins/embeddable/public/lib/reference_or_value_embeddable/types.ts @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EmbeddableInput, SavedObjectEmbeddableInput } from '..'; + +/** + * Any embeddable that implements this interface will be able to use input that is + * either by reference (backed by a saved object) OR by value, (provided + * by the container). + * @public + */ +export interface ReferenceOrValueEmbeddable< + ValTypeInput extends EmbeddableInput = EmbeddableInput, + RefTypeInput extends SavedObjectEmbeddableInput = SavedObjectEmbeddableInput +> { + /** + * determines whether the input is by value or by reference. + */ + inputIsRefType: (input: ValTypeInput | RefTypeInput) => input is RefTypeInput; + + /** + * Gets the embeddable's current input as its Value type + */ + getInputAsValueType: () => Promise; + + /** + * Gets the embeddable's current input as its Reference type + */ + getInputAsRefType: () => Promise; +} + +export function isReferenceOrValueEmbeddable( + incoming: unknown +): incoming is ReferenceOrValueEmbeddable { + return ( + !!(incoming as ReferenceOrValueEmbeddable).inputIsRefType && + !!(incoming as ReferenceOrValueEmbeddable).getInputAsValueType && + !!(incoming as ReferenceOrValueEmbeddable).getInputAsRefType + ); +} diff --git a/src/plugins/embeddable/public/mocks.tsx b/src/plugins/embeddable/public/mocks.tsx index 94aa980e446ca4..fa79af909a4279 100644 --- a/src/plugins/embeddable/public/mocks.tsx +++ b/src/plugins/embeddable/public/mocks.tsx @@ -97,7 +97,6 @@ const createStartContract = (): Start => { getEmbeddableFactories: jest.fn(), getEmbeddableFactory: jest.fn(), EmbeddablePanel: jest.fn(), - getAttributeService: jest.fn(), getEmbeddablePanel: jest.fn(), getStateTransfer: jest.fn(() => createEmbeddableStateTransferMock() as EmbeddableStateTransfer), }; diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index 319cbf8ec44b47..3cbd49279564f3 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -37,10 +37,8 @@ import { defaultEmbeddableFactoryProvider, IEmbeddable, EmbeddablePanel, - SavedObjectEmbeddableInput, } from './lib'; import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition'; -import { AttributeService } from './lib/embeddables/attribute_service'; import { EmbeddableStateTransfer } from './lib/state_transfer'; export interface EmbeddableSetupDependencies { @@ -75,14 +73,6 @@ export interface EmbeddableStart { embeddableFactoryId: string ) => EmbeddableFactory | undefined; getEmbeddableFactories: () => IterableIterator; - getAttributeService: < - A, - V extends EmbeddableInput & { attributes: A }, - R extends SavedObjectEmbeddableInput - >( - type: string - ) => AttributeService; - EmbeddablePanel: EmbeddablePanelHOC; getEmbeddablePanel: (stateTransfer?: EmbeddableStateTransfer) => EmbeddablePanelHOC; getStateTransfer: (history?: ScopedHistory) => EmbeddableStateTransfer; @@ -159,7 +149,6 @@ export class EmbeddablePublicPlugin implements Plugin new AttributeService(type, core.savedObjects.client), getStateTransfer: (history?: ScopedHistory) => { return history ? new EmbeddableStateTransfer(core.application.navigateToApp, history) diff --git a/src/plugins/index_pattern_management/public/_templates.scss b/src/plugins/index_pattern_management/public/_templates.scss new file mode 100644 index 00000000000000..5303537bddabcb --- /dev/null +++ b/src/plugins/index_pattern_management/public/_templates.scss @@ -0,0 +1,11 @@ +%inp-empty-state-footer { + background: $euiColorLightestShade; + margin: 0 (-$euiSizeL) (-$euiSizeL); + padding: $euiSizeL; + border-radius: 0 0 $euiBorderRadius $euiBorderRadius; + + // sass-lint:disable-block mixins-before-declarations + @include euiBreakpoint('xs', 's') { + text-align: center; + } +} diff --git a/src/plugins/index_pattern_management/public/_variables.scss b/src/plugins/index_pattern_management/public/_variables.scss new file mode 100644 index 00000000000000..5da25a91bd77c5 --- /dev/null +++ b/src/plugins/index_pattern_management/public/_variables.scss @@ -0,0 +1 @@ +$inpEmptyStateMaxWidth: $euiSizeXXL * 19; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_prompt/index.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_prompt/index.tsx deleted file mode 100644 index ab3b90177bcfda..00000000000000 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_prompt/index.tsx +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { - EuiDescriptionList, - EuiDescriptionListDescription, - EuiDescriptionListTitle, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiHorizontalRule, - EuiSpacer, - EuiText, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; - -export const CreateIndexPatternPrompt = ({ onClose }: { onClose: () => void }) => ( - - - -

- -

-
-
- - -

- -

-
- - -

- -

-
- - - - - - - - - - - - - - - - - - - - - -
-
-); diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap index 6d79515c172fe1..0e5fc0582f72c5 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap @@ -62,10 +62,37 @@ exports[`CreateIndexPatternWizard renders index pattern step when there are indi exports[`CreateIndexPatternWizard renders the empty state when there are no indices 1`] = ` - + +
+ + + - + +
+ + + - - } - > -

- - - , - "learnHowLink": - - , - "needToIndex": - - , - } - } - /> -

- - - -
- -`; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/empty_state/empty_state.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/empty_state/empty_state.tsx deleted file mode 100644 index 43c3bf79026feb..00000000000000 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/empty_state/empty_state.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; - -import { EuiCallOut, EuiTextColor, EuiLink, EuiButton } from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n/react'; -import { IBasePath } from 'kibana/public'; - -export const EmptyState = ({ - onRefresh, - prependBasePath, -}: { - onRefresh: () => void; - prependBasePath: IBasePath['prepend']; -}) => ( -
- - } - > -

- - - - ), - learnHowLink: ( - - - - ), - getStartedLink: ( - - - - ), - }} - /> -

- - - - -
-
-); diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/__snapshots__/header.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/__snapshots__/header.test.tsx.snap index c4f735558b1f21..851e5cc4c2a762 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/__snapshots__/header.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/__snapshots__/header.test.tsx.snap @@ -7,7 +7,7 @@ exports[`Header should mark the input as invalid 1`] = ` >

@@ -119,7 +119,7 @@ exports[`Header should render normally 1`] = ` >

diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/header.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/header.tsx index f1bf0d54a1cbf7..8efa44decf3c60 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/header.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/header/header.tsx @@ -66,7 +66,7 @@ export const Header: React.FC = ({

@@ -127,6 +127,7 @@ export const Header: React.FC = ({ id="checkboxShowSystemIndices" checked={isIncludingSystemIndices} onChange={onChangeIncludingSystemIndices} + data-test-subj="showSystemAndHiddenIndices" /> ) : null} diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/__snapshots__/indices_list.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/__snapshots__/indices_list.test.tsx.snap index 598de4d90e8932..6631a9bbd1d02b 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/__snapshots__/indices_list.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/indices_list/__snapshots__/indices_list.test.tsx.snap @@ -2,7 +2,10 @@ exports[`IndicesList should change pages 1`] = `
- + - + - + - + - + - + {rows} diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/loading_indices/__snapshots__/loading_indices.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/loading_indices/__snapshots__/loading_indices.test.tsx.snap index 9d67ca913d415d..a5517f6d4b6167 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/loading_indices/__snapshots__/loading_indices.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/loading_indices/__snapshots__/loading_indices.test.tsx.snap @@ -3,47 +3,33 @@ exports[`LoadingIndices should render normally 1`] = ` - - - - - - - - - - - - +

+ + + + `; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/loading_indices/loading_indices.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/loading_indices/loading_indices.tsx index 16e8d1a9f3e986..82603fd5985961 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/loading_indices/loading_indices.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/loading_indices/loading_indices.tsx @@ -19,34 +19,30 @@ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTextColor, EuiLoadingSpinner } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiLoadingSpinner } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; export const LoadingIndices = ({ ...rest }) => ( - + - - - - - - + +

- - - - - - - - +

+
+
+ +
); diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/status_message/status_message.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/status_message/status_message.tsx index 22b75071b93bb9..c2c4c84b516836 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/status_message/status_message.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/components/status_message/status_message.tsx @@ -132,7 +132,7 @@ export const StatusMessage: React.FC = ({ /> ); - } else if (allIndicesLength) { + } else { statusIcon = undefined; statusColor = 'warning'; statusMessage = ( diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx index fab638509313dd..d8555d71d6ec01 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_index_pattern/step_index_pattern.tsx @@ -159,7 +159,7 @@ export class StepIndexPattern extends Component indexPatternCreationType.getIndexTags(indexName), query, this.state.isIncludingSystemIndices ) @@ -175,13 +175,13 @@ export class StepIndexPattern extends Component indexPatternCreationType.getIndexTags(indexName), `${query}*`, this.state.isIncludingSystemIndices ), getIndices( this.context.services.http, - indexPatternCreationType, + (indexName: string) => indexPatternCreationType.getIndexTags(indexName), query, this.state.isIncludingSystemIndices ), @@ -227,7 +227,13 @@ export class StepIndexPattern extends Component; + return ( + <> + + + + + ); } renderStatusMessage(matchedIndices: { diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/advanced_options/__snapshots__/advanced_options.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/advanced_options/__snapshots__/advanced_options.test.tsx.snap index d1b10fb532020a..a2d2023ea06016 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/advanced_options/__snapshots__/advanced_options.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/advanced_options/__snapshots__/advanced_options.test.tsx.snap @@ -7,7 +7,7 @@ exports[`AdvancedOptions should hide if not showing 1`] = ` onClick={[Function]} > @@ -25,7 +25,7 @@ exports[`AdvancedOptions should render normally 1`] = ` onClick={[Function]} > diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/advanced_options/advanced_options.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/advanced_options/advanced_options.tsx index b8f34095743ba0..752fcbcd42b5c4 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/advanced_options/advanced_options.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/advanced_options/advanced_options.tsx @@ -45,12 +45,12 @@ export const AdvancedOptions: React.FC = ({ {isVisible ? ( ) : ( )} diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/header/__snapshots__/header.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/header/__snapshots__/header.test.tsx.snap index 2ac243780b31db..9efda4fdac7f91 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/header/__snapshots__/header.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/header/__snapshots__/header.test.tsx.snap @@ -17,9 +17,18 @@ exports[`Header should render normally 1`] = ` size="m" /> - - ki* - + + ki* + , + "indexPatternName": "ki*", + } + } + /> `; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/header/header.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/header/header.tsx index c17b356e159f6a..530d0688b61ca6 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/header/header.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/header/header.tsx @@ -40,7 +40,14 @@ export const Header: React.FC = ({ indexPattern, indexPatternName } - {indexPattern} + {indexPattern}, + indexPatternName, + }} + /> ); diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/time_field.scss b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/time_field.scss deleted file mode 100644 index 5bd60e8b76afc4..00000000000000 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/time_field.scss +++ /dev/null @@ -1,7 +0,0 @@ -/** - * 1. Bring the line-height down or else this link expands its container when it becomes visible. - */ - -.timeFieldRefreshButton { - line-height: 1 !important; /* 1 */ -} diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/time_field.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/time_field.tsx index 7a3d72551f4641..1eae1055ac5efa 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/time_field.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/components/time_field/time_field.tsx @@ -17,8 +17,6 @@ * under the License. */ -import './time_field.scss'; - import React from 'react'; import { diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.test.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.test.tsx index b14cd526d7e276..af5618424bbc08 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.test.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.test.tsx @@ -27,7 +27,6 @@ jest.mock('./components/step_index_pattern', () => ({ StepIndexPattern: 'StepInd jest.mock('./components/step_time_field', () => ({ StepTimeField: 'StepTimeField' })); jest.mock('./components/header', () => ({ Header: 'Header' })); jest.mock('./components/loading_state', () => ({ LoadingState: 'LoadingState' })); -jest.mock('./components/empty_state', () => ({ EmptyState: 'EmptyState' })); jest.mock('./lib/get_indices', () => ({ getIndices: () => { return [{ name: 'kibana' }]; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx index cd76ca09ccb741..a789ebbfadbcee 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx @@ -33,7 +33,6 @@ import { StepIndexPattern } from './components/step_index_pattern'; import { StepTimeField } from './components/step_time_field'; import { Header } from './components/header'; import { LoadingState } from './components/loading_state'; -import { EmptyState } from './components/empty_state'; import { context as contextType } from '../../../../kibana_react/public'; import { getCreateBreadcrumbs } from '../breadcrumbs'; @@ -125,7 +124,13 @@ export class CreateIndexPatternWizard extends Component< // query local and remote indices, updating state independently ensureMinimumTime( this.catchAndWarn( - getIndices(this.context.services.http, this.state.indexPatternCreationType, `*`, false), + getIndices( + this.context.services.http, + (indexName: string) => this.state.indexPatternCreationType.getIndexTags(indexName), + `*`, + false + ), + [], indicesFailMsg ) @@ -136,7 +141,13 @@ export class CreateIndexPatternWizard extends Component< this.catchAndWarn( // if we get an error from remote cluster query, supply fallback value that allows user entry. // ['a'] is fallback value - getIndices(this.context.services.http, this.state.indexPatternCreationType, `*:*`, false), + getIndices( + this.context.services.http, + (indexName: string) => this.state.indexPatternCreationType.getIndexTags(indexName), + `*:*`, + false + ), + ['a'], clustersFailMsg ).then((remoteIndices: string[] | MatchedItem[]) => @@ -200,39 +211,24 @@ export class CreateIndexPatternWizard extends Component< }; renderHeader() { + const { docLinks, indexPatternCreationType } = this.state; return (
); } renderContent() { - const { - allIndices, - isInitiallyLoadingIndices, - step, - indexPattern, - remoteClustersExist, - } = this.state; + const { allIndices, isInitiallyLoadingIndices, step, indexPattern } = this.state; if (isInitiallyLoadingIndices) { return ; } - const hasDataIndices = allIndices.some(({ name }: MatchedItem) => !name.startsWith('.')); - if (!hasDataIndices && !remoteClustersExist) { - return ( - - ); - } - const header = this.renderHeader(); if (step === 1) { diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.test.ts b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.test.ts index 4cd28090420a7e..8f3765b7b5dcc5 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.test.ts +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.test.ts @@ -37,7 +37,7 @@ describe('extractTimeFields', () => { expect(extractTimeFields(fields)).toEqual([ { display: '@timestamp', fieldName: '@timestamp' }, { isDisabled: true, display: '───', fieldName: '' }, - { display: `I don't want to use the Time Filter`, fieldName: undefined }, + { display: `I don't want to use the time filter`, fieldName: undefined }, ]); }); }); diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.ts b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.ts index b7056ad17343af..efac21245c2578 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.ts +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.ts @@ -45,7 +45,7 @@ export function extractTimeFields(fields: IFieldType[]) { const noTimeFieldLabel = i18n.translate( 'indexPatternManagement.createIndexPattern.stepTime.noTimeFieldOptionLabel', { - defaultMessage: "I don't want to use the Time Filter", + defaultMessage: "I don't want to use the time filter", } ); const noTimeFieldOption = { diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.test.ts b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.test.ts index 8e4dd37284333c..44a2d1a3be0d05 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.test.ts +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.test.ts @@ -18,7 +18,6 @@ */ import { getIndices, responseToItemArray } from './get_indices'; -import { IndexPatternCreationConfig } from '../../../../../index_pattern_management/public'; import { httpServiceMock } from '../../../../../../core/public/mocks'; import { ResolveIndexResponseItemIndexAttrs } from '../types'; @@ -44,33 +43,27 @@ export const successfulResponse = { ], }; -const mockIndexPatternCreationType = new IndexPatternCreationConfig({ - type: 'default', - name: 'name', - showSystemIndices: false, - httpClient: {}, - isBeta: false, -}); +const mockGetTags = () => []; const http = httpServiceMock.createStartContract(); http.get.mockResolvedValue(successfulResponse); describe('getIndices', () => { it('should work in a basic case', async () => { - const result = await getIndices(http, mockIndexPatternCreationType, 'kibana', false); + const result = await getIndices(http, mockGetTags, 'kibana', false); expect(result.length).toBe(3); expect(result[0].name).toBe('f-alias'); expect(result[1].name).toBe('foo'); }); it('should ignore ccs query-all', async () => { - expect((await getIndices(http, mockIndexPatternCreationType, '*:', false)).length).toBe(0); + expect((await getIndices(http, mockGetTags, '*:', false)).length).toBe(0); }); it('should ignore a single comma', async () => { - expect((await getIndices(http, mockIndexPatternCreationType, ',', false)).length).toBe(0); - expect((await getIndices(http, mockIndexPatternCreationType, ',*', false)).length).toBe(0); - expect((await getIndices(http, mockIndexPatternCreationType, ',foobar', false)).length).toBe(0); + expect((await getIndices(http, mockGetTags, ',', false)).length).toBe(0); + expect((await getIndices(http, mockGetTags, ',*', false)).length).toBe(0); + expect((await getIndices(http, mockGetTags, ',foobar', false)).length).toBe(0); }); it('response object to item array', () => { @@ -98,8 +91,8 @@ describe('getIndices', () => { }, ], }; - expect(responseToItemArray(result, mockIndexPatternCreationType)).toMatchSnapshot(); - expect(responseToItemArray({}, mockIndexPatternCreationType)).toEqual([]); + expect(responseToItemArray(result, mockGetTags)).toMatchSnapshot(); + expect(responseToItemArray({}, mockGetTags)).toEqual([]); }); describe('errors', () => { @@ -107,7 +100,7 @@ describe('getIndices', () => { http.get.mockImplementationOnce(() => { throw new Error('Test error'); }); - const result = await getIndices(http, mockIndexPatternCreationType, 'kibana', false); + const result = await getIndices(http, mockGetTags, 'kibana', false); expect(result.length).toBe(0); }); }); diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.ts b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.ts index c6a11de1bc4fc0..6844e90316e226 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.ts +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/get_indices.ts @@ -38,7 +38,7 @@ const frozenLabel = i18n.translate('indexPatternManagement.frozenLabel', { export async function getIndices( http: HttpStart, - indexPatternCreationType: IndexPatternCreationConfig, + getIndexTags: IndexPatternCreationConfig['getIndexTags'], rawPattern: string, showAllIndices: boolean ): Promise { @@ -73,7 +73,7 @@ export async function getIndices( return []; } - return responseToItemArray(response, indexPatternCreationType); + return responseToItemArray(response, getIndexTags); } catch { return []; } @@ -81,7 +81,7 @@ export async function getIndices( export const responseToItemArray = ( response: ResolveIndexResponse, - indexPatternCreationType: IndexPatternCreationConfig + getIndexTags: IndexPatternCreationConfig['getIndexTags'] ): MatchedItem[] => { const source: MatchedItem[] = []; @@ -89,7 +89,7 @@ export const responseToItemArray = ( const tags: MatchedItem['tags'] = [{ key: 'index', name: indexLabel, color: 'default' }]; const isFrozen = (index.attributes || []).includes(ResolveIndexResponseItemIndexAttrs.FROZEN); - tags.push(...indexPatternCreationType.getIndexTags(index.name)); + tags.push(...getIndexTags(index.name)); if (isFrozen) { tags.push({ name: frozenLabel, key: 'frozen', color: 'danger' }); } diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx index 4b538af7c5fe79..987e8f0dae3a0c 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -27,7 +27,6 @@ import { EuiBadge, EuiText, EuiLink, - EuiIcon, EuiCallOut, EuiPanel, } from '@elastic/eui'; @@ -162,7 +161,7 @@ export const EditIndexPattern = withRouter( const timeFilterHeader = i18n.translate( 'indexPatternManagement.editIndexPattern.timeFilterHeader', { - defaultMessage: "Time Filter field name: '{timeFieldName}'", + defaultMessage: "Time field: '{timeFieldName}'", values: { timeFieldName: indexPattern.timeFieldName }, } ); @@ -187,62 +186,55 @@ export const EditIndexPattern = withRouter( return (
- - - - - {showTagsSection && ( - - {Boolean(indexPattern.timeFieldName) && ( - - {timeFilterHeader} - - )} - {tags.map((tag: any) => ( - - {tag.name} - - ))} - + + + {showTagsSection && ( + + {Boolean(indexPattern.timeFieldName) && ( + + {timeFilterHeader} + )} - - -

- {indexPattern.title} }} - />{' '} - - {mappingAPILink} - - -

-
- {conflictedFields.length > 0 && ( - -

{mappingConflictLabel}

-
- )} -
- - - -
+ {tags.map((tag: any) => ( + + {tag.name} + + ))} + + )} + + +

+ {indexPattern.title} }} + />{' '} + + {mappingAPILink} + +

+
+ {conflictedFields.length > 0 && ( + <> + + +

{mappingConflictLabel}

+
+ + )} + +
); diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx index 4cf43d63d58397..8ca8c6453c7e96 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx @@ -19,14 +19,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { - EuiFlexGroup, - EuiToolTip, - EuiFlexItem, - EuiIcon, - EuiTitle, - EuiButtonIcon, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiToolTip, EuiFlexItem, EuiTitle, EuiButtonIcon } from '@elastic/eui'; import { IIndexPattern } from 'src/plugins/data/public'; interface IndexHeaderProps { @@ -77,22 +70,13 @@ export function IndexHeader({ return ( - - {defaultIndex === indexPattern.id && ( - - - - )} - - -

{indexPattern.title}

-
-
-
+ +

{indexPattern.title}

+
- - {setDefault && ( + + {defaultIndex !== indexPattern.id && setDefault && ( setFieldFilter(e.target.value)} diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_help/test_script.scss b/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_help/test_script.scss index 34e8a60d070745..ca230711827dc7 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_help/test_script.scss +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/scripting_help/test_script.scss @@ -1,5 +1,5 @@ .testScript__searchBar { - .globalQueryBar { + .globalQueryBar { padding: $euiSize 0 0; } } diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/__snapshots__/empty_index_pattern_prompt.test.tsx.snap b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/__snapshots__/empty_index_pattern_prompt.test.tsx.snap new file mode 100644 index 00000000000000..c5e6d1220d8bf8 --- /dev/null +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/__snapshots__/empty_index_pattern_prompt.test.tsx.snap @@ -0,0 +1,99 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EmptyIndexPatternPrompt should render normally 1`] = ` + + + + + + + +

+ +
+ +

+

+ +

+ + + +
+
+
+ + + + + + + + + + + +
+`; diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/assets/index_pattern_illustration.scss b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/assets/index_pattern_illustration.scss new file mode 100644 index 00000000000000..cd0477aba7adf2 --- /dev/null +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/assets/index_pattern_illustration.scss @@ -0,0 +1,9 @@ +.indexPatternIllustration { + &__verticalStripes { + fill: $euiColorFullShade; + } + + &__dots { + fill: $euiColorLightShade; + } +} diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/assets/index_pattern_illustration.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/assets/index_pattern_illustration.tsx new file mode 100644 index 00000000000000..2461c0f5df9194 --- /dev/null +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/assets/index_pattern_illustration.tsx @@ -0,0 +1,551 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import './index_pattern_illustration.scss'; +import React from 'react'; + +const IndexPatternIllustration = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); + +export const Illustration = IndexPatternIllustration; diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/empty_index_pattern_prompt.scss b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/empty_index_pattern_prompt.scss new file mode 100644 index 00000000000000..11ac55b098a57c --- /dev/null +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/empty_index_pattern_prompt.scss @@ -0,0 +1,31 @@ +@import '../../../variables'; +@import '../../../templates'; + +.inpEmptyIndexPatternPrompt { + // override EUI specificity + max-width: $inpEmptyStateMaxWidth !important; // sass-lint:disable-line no-important +} + +.inpEmptyIndexPatternPrompt__footer { + @extend %inp-empty-state-footer; + // override EUI specificity + align-items: baseline !important; // sass-lint:disable-line no-important +} + +.inpEmptyIndexPatternPrompt__title { + // override EUI specificity + width: auto !important; // sass-lint:disable-line no-important +} + +@include euiBreakpoint('xs', 's') { + .inpEmptyIndexPatternPrompt__illustration > svg { + width: $euiSize * 12; + height: auto; + margin: 0 auto; + } + + .inpEmptyIndexPatternPrompt__text { + text-align: center; + align-items: center; + } +} diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/empty_state/empty_state.test.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/empty_index_pattern_prompt.test.tsx similarity index 57% rename from src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/empty_state/empty_state.test.tsx rename to src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/empty_index_pattern_prompt.test.tsx index 7fa39363e3ef72..83eb803333afcf 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/empty_state/empty_state.test.tsx +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/empty_index_pattern_prompt.test.tsx @@ -18,30 +18,20 @@ */ import React from 'react'; -import { EmptyState } from '../empty_state'; -import { shallow } from 'enzyme'; -import sinon from 'sinon'; +import { EmptyIndexPatternPrompt } from '../empty_index_pattern_prompt'; +import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; -describe('EmptyState', () => { +describe('EmptyIndexPatternPrompt', () => { it('should render normally', () => { - const component = shallow( {}} prependBasePath={(x) => x} />); + const component = shallowWithI18nProvider( + {} }]} + docLinksIndexPatternIntro={'testUrl'} + setBreadcrumbs={() => {}} + /> + ); expect(component).toMatchSnapshot(); }); - - describe('props', () => { - describe('onRefresh', () => { - it('is called when refresh button is clicked', () => { - const onRefreshHandler = sinon.stub(); - - const component = shallow( - x} /> - ); - - component.find('[data-test-subj="refreshIndicesButton"]').simulate('click'); - - sinon.assert.calledOnce(onRefreshHandler); - }); - }); - }); }); diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/empty_index_pattern_prompt.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/empty_index_pattern_prompt.tsx new file mode 100644 index 00000000000000..de389097fd4ba5 --- /dev/null +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/empty_index_pattern_prompt.tsx @@ -0,0 +1,111 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import './empty_index_pattern_prompt.scss'; + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { EuiPageContent, EuiSpacer, EuiText, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { EuiDescriptionListTitle } from '@elastic/eui'; +import { EuiDescriptionListDescription, EuiDescriptionList } from '@elastic/eui'; +import { EuiLink } from '@elastic/eui'; +import { getListBreadcrumbs } from '../../breadcrumbs'; +import { IndexPatternCreationOption } from '../../types'; +import { CreateButton } from '../../create_button'; +import { Illustration } from './assets/index_pattern_illustration'; +import { ManagementAppMountParams } from '../../../../../management/public'; + +interface Props { + canSave: boolean; + creationOptions: IndexPatternCreationOption[]; + docLinksIndexPatternIntro: string; + setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; +} + +export const EmptyIndexPatternPrompt = ({ + canSave, + creationOptions, + docLinksIndexPatternIntro, + setBreadcrumbs, +}: Props) => { + setBreadcrumbs(getListBreadcrumbs()); + + return ( + + + + + + + +

+ +
+ +

+

+ +

+ {canSave && ( + + + + )} +
+
+
+ + + + + + + + + + + +
+ ); +}; diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/index.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/index.tsx new file mode 100644 index 00000000000000..239bb272b23abc --- /dev/null +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_index_pattern_prompt/index.tsx @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { EmptyIndexPatternPrompt } from './empty_index_pattern_prompt'; diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap new file mode 100644 index 00000000000000..645694371f9059 --- /dev/null +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/__snapshots__/empty_state.test.tsx.snap @@ -0,0 +1,216 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`EmptyState should render normally 1`] = ` + + + + + +

+ +

+
+
+
+ + + + + + } + icon={ + + } + onClick={[Function]} + title={ + + } + /> + + + + } + icon={ + + } + isDisabled={false} + onClick={[Function]} + title={ + + } + /> + + + + } + icon={ + + } + onClick={[Function]} + title={ + + } + /> + + + +
+ + + + + , + "title": , + }, + ] + } + /> + + + + + + + , + "title": , + }, + ] + } + /> + + +
+
+
+ + + + + , + } + } + /> + +
+`; diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.scss b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.scss new file mode 100644 index 00000000000000..37889b9d7c4834 --- /dev/null +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.scss @@ -0,0 +1,23 @@ +@import '../../../variables'; +@import '../../../templates'; + +.inpEmptyState { + // override EUI specificity + max-width: $inpEmptyStateMaxWidth !important; // sass-lint:disable-line no-important +} + +.inpEmptyState__cardGrid { + justify-content: center; +} + +.inpEmptyState__card { + min-width: $euiSizeXL * 6; +} + +.inpEmptyState__footer { + @extend %inp-empty-state-footer; +} + +.inpEmptyState__footerFlexItem { + min-width: $euiSizeXL * 7; +} diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.test.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.test.tsx new file mode 100644 index 00000000000000..7b2cc0f4c3c60e --- /dev/null +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.test.tsx @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EmptyState } from '../empty_state'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; +// @ts-expect-error +import { findTestSubject } from '@elastic/eui/lib/test'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { docLinksServiceMock } from '../../../../../../core/public/mocks'; +import { MlCardState } from '../../../types'; + +const docLinks = docLinksServiceMock.createStartContract(); + +jest.mock('react-router-dom', () => ({ + useHistory: () => ({ + createHref: jest.fn(), + }), +})); + +describe('EmptyState', () => { + it('should render normally', () => { + const component = shallow( + {}} + navigateToApp={async () => {}} + getMlCardState={() => MlCardState.ENABLED} + canSave={true} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + describe('props', () => { + describe('onRefresh', () => { + it('is called when refresh button is clicked', () => { + const onRefreshHandler = sinon.stub(); + + const component = mountWithIntl( + {}} + getMlCardState={() => MlCardState.ENABLED} + canSave={true} + /> + ); + + findTestSubject(component, 'refreshIndicesButton').simulate('click'); + + sinon.assert.calledOnce(onRefreshHandler); + }); + }); + }); +}); diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx new file mode 100644 index 00000000000000..e758184f0f14b7 --- /dev/null +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/empty_state.tsx @@ -0,0 +1,234 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import './empty_state.scss'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { DocLinksStart, ApplicationStart } from 'kibana/public'; +import { + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiTitle, + EuiPageContentBody, + EuiPageContent, + EuiIcon, + EuiSpacer, + EuiFlexItem, + EuiDescriptionList, + EuiFlexGrid, + EuiCard, + EuiLink, + EuiText, +} from '@elastic/eui'; +import { useHistory } from 'react-router-dom'; +import { reactRouterNavigate } from '../../../../../../plugins/kibana_react/public'; +import { MlCardState } from '../../../types'; + +export const EmptyState = ({ + onRefresh, + navigateToApp, + docLinks, + getMlCardState, + canSave, +}: { + onRefresh: () => void; + navigateToApp: ApplicationStart['navigateToApp']; + docLinks: DocLinksStart; + getMlCardState: () => MlCardState; + canSave: boolean; +}) => { + const mlCard = ( + + navigateToApp('ml', { path: '#/filedatavisualizer' })} + className="inpEmptyState__card" + betaBadgeLabel={ + getMlCardState() === MlCardState.ENABLED + ? undefined + : i18n.translate( + 'indexPatternManagement.createIndexPattern.emptyState.basicLicenseLabel', + { + defaultMessage: 'Basic', + } + ) + } + betaBadgeTooltipContent={i18n.translate( + 'indexPatternManagement.createIndexPattern.emptyState.basicLicenseDescription', + { + defaultMessage: 'This feature requires a Basic license.', + } + )} + isDisabled={getMlCardState() === MlCardState.DISABLED} + icon={} + title={ + + } + description={ + + } + /> + + ); + + const createAnyway = ( + + + + + ), + }} + /> + + ); + + return ( + <> + + + + +

+ +

+
+
+
+ + + + + navigateToApp('home', { path: '#/tutorial_directory' })} + icon={} + title={ + + } + description={ + + } + /> + + {getMlCardState() !== MlCardState.HIDDEN ? mlCard : <>} + + navigateToApp('home', { path: '#/tutorial_directory/sampleData' })} + icon={} + title={ + + } + description={ + + } + /> + + + +
+ + + + ), + description: ( + + + + ), + }, + ]} + /> + + + + ), + description: ( + + {' '} + + + ), + }, + ]} + /> + + +
+
+
+ + {canSave && createAnyway} + + ); +}; diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/empty_state/index.ts b/src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/empty_state/index.ts rename to src/plugins/index_pattern_management/public/components/index_pattern_table/empty_state/index.ts diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx index 947882b8df495b..2768314a758604 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx @@ -20,14 +20,14 @@ import { EuiBadge, EuiButtonEmpty, - EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, - EuiPanel, EuiSpacer, EuiText, EuiBadgeGroup, + EuiPageContent, + EuiTitle, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { withRouter, RouteComponentProps } from 'react-router-dom'; @@ -36,10 +36,13 @@ import { i18n } from '@kbn/i18n'; import { reactRouterNavigate, useKibana } from '../../../../../plugins/kibana_react/public'; import { IndexPatternManagmentContext } from '../../types'; import { CreateButton } from '../create_button'; -import { CreateIndexPatternPrompt } from '../create_index_pattern_prompt'; import { IndexPatternTableItem, IndexPatternCreationOption } from '../types'; import { getIndexPatterns } from '../utils'; import { getListBreadcrumbs } from '../breadcrumbs'; +import { EmptyState } from './empty_state'; +import { MatchedItem, ResolveIndexResponseItemAlias } from '../create_index_pattern_wizard/types'; +import { EmptyIndexPatternPrompt } from './empty_index_pattern_prompt'; +import { getIndices } from '../create_index_pattern_wizard/lib'; const pagination = { initialPageSize: 10, @@ -81,13 +84,19 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { uiSettings, indexPatternManagementStart, chrome, + docLinks, + application, + http, + getMlCardState, } = useKibana().services; - const [showFlyout, setShowFlyout] = useState(false); const [indexPatterns, setIndexPatterns] = useState([]); const [creationOptions, setCreationOptions] = useState([]); + const [sources, setSources] = useState([]); + const [remoteClustersExist, setRemoteClustersExist] = useState(false); + const [isLoadingSources, setIsLoadingSources] = useState(true); + const [isLoadingIndexPatterns, setIsLoadingIndexPatterns] = useState(true); setBreadcrumbs(getListBreadcrumbs()); - useEffect(() => { (async function () { const options = await indexPatternManagementStart.creation.getIndexPatternCreationOptions( @@ -98,9 +107,9 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { uiSettings.get('defaultIndex'), indexPatternManagementStart ); + setIsLoadingIndexPatterns(false); setCreationOptions(options); setIndexPatterns(gettedIndexPatterns); - setShowFlyout(gettedIndexPatterns.length === 0); })(); }, [ history.push, @@ -110,6 +119,28 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { savedObjects.client, ]); + const removeAliases = (item: MatchedItem) => + !((item as unknown) as ResolveIndexResponseItemAlias).indices; + + const loadSources = () => { + getIndices(http, () => [], '*', false).then((dataSources) => + setSources(dataSources.filter(removeAliases)) + ); + getIndices(http, () => [], '*:*', false).then((dataSources) => + setRemoteClustersExist(!!dataSources.filter(removeAliases).length) + ); + }; + + useEffect(() => { + getIndices(http, () => [], '*', false).then((dataSources) => { + setSources(dataSources.filter(removeAliases)); + setIsLoadingSources(false); + }); + getIndices(http, () => [], '*:*', false).then((dataSources) => + setRemoteClustersExist(!!dataSources.filter(removeAliases).length) + ); + }, [http, creationOptions]); + chrome.docTitle.change(title); const columns = [ @@ -130,12 +161,11 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { {name} +   {index.tags && index.tags.map(({ key: tagKey, name: tagName }) => ( - - {tagName} - + {tagName} ))} @@ -156,31 +186,51 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { <> ); + if (isLoadingSources || isLoadingIndexPatterns) { + return <>; + } + + const hasDataIndices = sources.some(({ name }: MatchedItem) => !name.startsWith('.')); + + if (!indexPatterns.length) { + if (!hasDataIndices && !remoteClustersExist) { + return ( + + ); + } else { + return ( + + ); + } + } + return ( - - {showFlyout && setShowFlyout(false)} />} + - - - - -

{title}

-
-
- - setShowFlyout(true)} - aria-label="Help" + + +

{title}

+
+ + +

+ - - +

+
{createButton}
@@ -195,7 +245,7 @@ export const IndexPatternTable = ({ canSave, history }: Props) => { sorting={sorting} search={search} /> -
+ ); }; diff --git a/src/plugins/index_pattern_management/public/index.ts b/src/plugins/index_pattern_management/public/index.ts index 2d6db13757eea5..9a0fd39fb4fd91 100644 --- a/src/plugins/index_pattern_management/public/index.ts +++ b/src/plugins/index_pattern_management/public/index.ts @@ -41,3 +41,5 @@ export { IndexPatternCreationOption, IndexPatternListConfig, } from './service'; + +export { MlCardState } from './types'; diff --git a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx index bcabd55c879750..add45a07e0c5fb 100644 --- a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx +++ b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx @@ -34,7 +34,7 @@ import { CreateIndexPatternWizardWithRouter, } from '../components'; import { IndexPatternManagementStartDependencies, IndexPatternManagementStart } from '../plugin'; -import { IndexPatternManagmentContext } from '../types'; +import { IndexPatternManagmentContext, MlCardState } from '../types'; const readOnlyBadge = { text: i18n.translate('indexPatternManagement.indexPatterns.badge.readOnly.text', { @@ -48,7 +48,8 @@ const readOnlyBadge = { export async function mountManagementSection( getStartServices: StartServicesAccessor, - params: ManagementAppMountParams + params: ManagementAppMountParams, + getMlCardState: () => MlCardState ) { const [ { chrome, application, savedObjects, uiSettings, notifications, overlays, http, docLinks }, @@ -73,6 +74,7 @@ export async function mountManagementSection( data, indexPatternManagementStart: indexPatternManagementStart as IndexPatternManagementStart, setBreadcrumbs: params.setBreadcrumbs, + getMlCardState, }; ReactDOM.render( diff --git a/src/plugins/index_pattern_management/public/mocks.ts b/src/plugins/index_pattern_management/public/mocks.ts index ec8100db420851..6a9ef23e3732e6 100644 --- a/src/plugins/index_pattern_management/public/mocks.ts +++ b/src/plugins/index_pattern_management/public/mocks.ts @@ -39,6 +39,9 @@ const createSetupContract = (): IndexPatternManagementSetup => ({ getAll: jest.fn(), getById: jest.fn(), } as any, + environment: { + update: jest.fn(), + }, }); const createStartContract = (): IndexPatternManagementStart => ({ diff --git a/src/plugins/index_pattern_management/public/plugin.ts b/src/plugins/index_pattern_management/public/plugin.ts index fe680eff8657e2..ee1e00fcafd980 100644 --- a/src/plugins/index_pattern_management/public/plugin.ts +++ b/src/plugins/index_pattern_management/public/plugin.ts @@ -86,7 +86,9 @@ export class IndexPatternManagementPlugin mount: async (params) => { const { mountManagementSection } = await import('./management_app'); - return mountManagementSection(core.getStartServices, params); + return mountManagementSection(core.getStartServices, params, () => + this.indexPatternManagementService.environmentService.getEnvironment().ml() + ); }, }); diff --git a/src/plugins/index_pattern_management/public/service/environment/environment.mock.ts b/src/plugins/index_pattern_management/public/service/environment/environment.mock.ts new file mode 100644 index 00000000000000..2c2c68b8ead2d6 --- /dev/null +++ b/src/plugins/index_pattern_management/public/service/environment/environment.mock.ts @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EnvironmentService, EnvironmentServiceSetup } from './environment'; +import { MlCardState } from '../../types'; + +const createSetupMock = (): jest.Mocked => { + const setup = { + update: jest.fn(), + }; + return setup; +}; + +const createMock = (): jest.Mocked> => { + const service = { + setup: jest.fn(), + getEnvironment: jest.fn(() => ({ + ml: () => MlCardState.ENABLED, + })), + }; + service.setup.mockImplementation(createSetupMock); + return service; +}; + +export const environmentServiceMock = { + createSetup: createSetupMock, + create: createMock, +}; diff --git a/src/plugins/index_pattern_management/public/service/environment/environment.test.ts b/src/plugins/index_pattern_management/public/service/environment/environment.test.ts new file mode 100644 index 00000000000000..1aa67ba751b811 --- /dev/null +++ b/src/plugins/index_pattern_management/public/service/environment/environment.test.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EnvironmentService } from './environment'; +import { MlCardState } from '../../types'; + +describe('EnvironmentService', () => { + describe('setup', () => { + test('allows multiple update calls', () => { + const setup = new EnvironmentService().setup(); + expect(() => { + setup.update({ ml: () => MlCardState.ENABLED }); + }).not.toThrow(); + }); + }); + + describe('getEnvironment', () => { + test('returns default values', () => { + const service = new EnvironmentService(); + expect(service.getEnvironment().ml()).toEqual(MlCardState.DISABLED); + }); + + test('returns last state of update calls', () => { + let cardState = MlCardState.DISABLED; + const service = new EnvironmentService(); + const setup = service.setup(); + setup.update({ ml: () => cardState }); + expect(service.getEnvironment().ml()).toEqual(MlCardState.DISABLED); + cardState = MlCardState.ENABLED; + expect(service.getEnvironment().ml()).toEqual(MlCardState.ENABLED); + }); + }); +}); diff --git a/src/plugins/index_pattern_management/public/service/environment/environment.ts b/src/plugins/index_pattern_management/public/service/environment/environment.ts new file mode 100644 index 00000000000000..f40ce3589fa76a --- /dev/null +++ b/src/plugins/index_pattern_management/public/service/environment/environment.ts @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MlCardState } from '../../types'; + +/** @public */ +export interface Environment { + /** + * Flag whether ml features should be advertised + */ + readonly ml: () => MlCardState; +} + +export class EnvironmentService { + private environment = { + ml: () => MlCardState.DISABLED, + }; + + public setup() { + return { + /** + * Update the environment to influence how available features are presented. + * @param update + */ + update: (update: Partial) => { + this.environment = Object.assign({}, this.environment, update); + }, + }; + } + + public getEnvironment() { + return this.environment; + } +} + +export type EnvironmentServiceSetup = ReturnType; diff --git a/src/plugins/index_pattern_management/public/service/environment/index.ts b/src/plugins/index_pattern_management/public/service/environment/index.ts new file mode 100644 index 00000000000000..91d14c358e7db8 --- /dev/null +++ b/src/plugins/index_pattern_management/public/service/environment/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { EnvironmentService, Environment, EnvironmentServiceSetup } from './environment'; diff --git a/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts index d4cc9c95e76a7e..06b9b83b1b6016 100644 --- a/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts +++ b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts @@ -21,6 +21,7 @@ import { HttpSetup } from '../../../../core/public'; import { IndexPatternCreationManager, IndexPatternCreationConfig } from './creation'; import { IndexPatternListManager, IndexPatternListConfig } from './list'; import { FieldFormatEditors } from './field_format_editors'; +import { EnvironmentService } from './environment'; import { BytesFormatEditor, @@ -49,11 +50,13 @@ export class IndexPatternManagementService { indexPatternCreationManager: IndexPatternCreationManager; indexPatternListConfig: IndexPatternListManager; fieldFormatEditors: FieldFormatEditors; + environmentService: EnvironmentService; constructor() { this.indexPatternCreationManager = new IndexPatternCreationManager(); this.indexPatternListConfig = new IndexPatternListManager(); this.fieldFormatEditors = new FieldFormatEditors(); + this.environmentService = new EnvironmentService(); } public setup({ httpClient }: SetupDependencies) { @@ -83,6 +86,7 @@ export class IndexPatternManagementService { creation: creationManagerSetup, list: indexPatternListConfigSetup, fieldFormatEditors: fieldFormatEditorsSetup, + environment: this.environmentService.setup(), }; } diff --git a/src/plugins/index_pattern_management/public/types.ts b/src/plugins/index_pattern_management/public/types.ts index 97941687e652de..2876bd62273507 100644 --- a/src/plugins/index_pattern_management/public/types.ts +++ b/src/plugins/index_pattern_management/public/types.ts @@ -44,8 +44,15 @@ export interface IndexPatternManagmentContext { data: DataPublicPluginStart; indexPatternManagementStart: IndexPatternManagementStart; setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; + getMlCardState: () => MlCardState; } export type IndexPatternManagmentContextValue = KibanaReactContextValue< IndexPatternManagmentContext >; + +export enum MlCardState { + HIDDEN, + DISABLED, + ENABLED, +} diff --git a/src/plugins/vis_type_vega/public/_vega_vis.scss b/src/plugins/vis_type_vega/public/_vega_vis.scss index f9468d677eeed9..12108c7ba3de0b 100644 --- a/src/plugins/vis_type_vega/public/_vega_vis.scss +++ b/src/plugins/vis_type_vega/public/_vega_vis.scss @@ -107,6 +107,7 @@ .vgaVis__tooltip { max-width: 100%; + position: fixed; h2 { margin-bottom: $euiSizeS; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js b/src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js index 01386fd91abc5a..7b0274478cea2d 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js @@ -85,12 +85,12 @@ export class TooltipHandler { let anchorBounds; if (item.bounds.width() > this.centerOnMark || item.bounds.height() > this.centerOnMark) { // I would expect clientX/Y, but that shows incorrectly - anchorBounds = createRect(event.pageX, event.pageY, 0, 0); + anchorBounds = createRect(event.clientX, event.clientY, 0, 0); } else { const containerBox = this.container.getBoundingClientRect(); anchorBounds = createRect( - containerBox.left + view._origin[0] + item.bounds.x1 + window.pageXOffset, - containerBox.top + view._origin[1] + item.bounds.y1 + window.pageYOffset, + containerBox.left + view._origin[0] + item.bounds.x1, + containerBox.top + view._origin[1] + item.bounds.y1, item.bounds.width(), item.bounds.height() ); diff --git a/test/functional/apps/management/_index_patterns_empty.ts b/test/functional/apps/management/_index_patterns_empty.ts new file mode 100644 index 00000000000000..4ae2e7836ac376 --- /dev/null +++ b/test/functional/apps/management/_index_patterns_empty.ts @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['common', 'settings']); + const testSubjects = getService('testSubjects'); + const globalNav = getService('globalNav'); + const es = getService('legacyEs'); + + describe('index pattern empty view', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + await kibanaServer.uiSettings.replace({}); + await PageObjects.settings.navigateTo(); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + await esArchiver.loadIfNeeded('makelogs'); + }); + + // create index pattern and return to verify list + it(`shows empty views`, async () => { + // @ts-expect-error + await es.transport.request({ + path: '/_all', + method: 'DELETE', + }); + await PageObjects.settings.clickKibanaIndexPatterns(); + await testSubjects.existOrFail('createAnyway'); + // @ts-expect-error + await es.transport.request({ + path: '/logstash-a/_doc', + method: 'POST', + body: { user: 'matt', message: 20 }, + }); + await testSubjects.click('refreshIndicesButton'); + await testSubjects.existOrFail('createIndexPatternButton', { timeout: 5000 }); + await PageObjects.settings.createIndexPattern('logstash-*', ''); + }); + + it(`doesn't show read-only badge`, async () => { + await globalNav.badgeMissingOrFail(); + }); + }); +} diff --git a/test/functional/apps/management/_kibana_settings.js b/test/functional/apps/management/_kibana_settings.js index 2a488a94c68896..e2b20bacc0b397 100644 --- a/test/functional/apps/management/_kibana_settings.js +++ b/test/functional/apps/management/_kibana_settings.js @@ -28,7 +28,7 @@ export default function ({ getService, getPageObjects }) { before(async function () { // delete .kibana index and then wait for Kibana to re-create it await kibanaServer.uiSettings.replace({}); - await PageObjects.settings.createIndexPattern(); + await PageObjects.settings.createIndexPattern('logstash-*'); await PageObjects.settings.navigateTo(); }); diff --git a/test/functional/apps/management/index.js b/test/functional/apps/management/index.js index 97e7314f9678e7..d5f0c286af7a55 100644 --- a/test/functional/apps/management/index.js +++ b/test/functional/apps/management/index.js @@ -43,6 +43,7 @@ export default function ({ getService, loadTestFile }) { loadTestFile(require.resolve('./_scripted_fields')); loadTestFile(require.resolve('./_scripted_fields_preview')); loadTestFile(require.resolve('./_mgmt_import_saved_objects')); + loadTestFile(require.resolve('./_index_patterns_empty')); }); describe('', function () { diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 4b80647c8749dd..a4285a5f94d51e 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -55,15 +55,6 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider await testSubjects.click('indexPatterns'); await PageObjects.header.waitUntilLoadingHasFinished(); - - // check for the index pattern info flyout that covers the - // create index pattern button on smaller screens - // @ts-ignore - await retry.waitFor('index pattern info flyout', async () => { - if (await testSubjects.exists('CreateIndexPatternPrompt')) { - await testSubjects.click('CreateIndexPatternPrompt > euiFlyoutCloseButton'); - } else return true; - }); } async getAdvancedSettings(propertyName: string) { @@ -311,9 +302,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider } async isIndexPatternListEmpty() { - await testSubjects.existOrFail('indexPatternTable', { timeout: 5000 }); - const indexPatternList = await this.getIndexPatternList(); - return indexPatternList.length === 0; + return !(await testSubjects.exists('indexPatternTable', { timeout: 5000 })); } async removeLogstashIndexPatternIfExist() { diff --git a/x-pack/package.json b/x-pack/package.json index b426e790c2d47d..2b52646e0f7483 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -255,8 +255,8 @@ "cronstrue": "^1.51.0", "cytoscape": "^3.10.0", "d3": "3.5.17", - "d3-scale": "1.0.7", "d3-array": "1.2.4", + "d3-scale": "1.0.7", "dedent": "^0.7.0", "del": "^5.1.0", "dragselect": "1.13.1", @@ -267,7 +267,7 @@ "font-awesome": "4.7.0", "formsy-react": "^1.1.5", "fp-ts": "^2.3.1", - "get-port": "4.2.0", + "get-port": "^4.2.0", "getos": "^3.1.0", "git-url-parse": "11.1.2", "github-markdown-css": "^2.10.0", diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts index bdf3f6a0acf90c..7f6e3feac0671b 100644 --- a/x-pack/plugins/data_enhanced/public/plugin.ts +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -31,20 +31,26 @@ export class DataEnhancedPlugin KUERY_LANGUAGE_NAME, setupKqlQuerySuggestionProvider(core) ); - } - public start(core: CoreStart, plugins: DataEnhancedStartDependencies) { - setAutocompleteService(plugins.data.autocomplete); const enhancedSearchInterceptor = new EnhancedSearchInterceptor( { toasts: core.notifications.toasts, - application: core.application, http: core.http, uiSettings: core.uiSettings, - usageCollector: plugins.data.search.usageCollector, + startServices: core.getStartServices(), + usageCollector: data.search.usageCollector, }, core.injectedMetadata.getInjectedVar('esRequestTimeout') as number ); - plugins.data.search.setInterceptor(enhancedSearchInterceptor); + + data.__enhance({ + search: { + searchInterceptor: enhancedSearchInterceptor, + }, + }); + } + + public start(core: CoreStart, plugins: DataEnhancedStartDependencies) { + setAutocompleteService(plugins.data.autocomplete); } } diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts index d004511fa46749..fe954f1602cd31 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts @@ -6,7 +6,7 @@ import { coreMock } from '../../../../../src/core/public/mocks'; import { EnhancedSearchInterceptor } from './search_interceptor'; -import { CoreStart } from 'kibana/public'; +import { CoreSetup, CoreStart } from 'kibana/public'; import { AbortError } from '../../../../../src/plugins/data/common'; const timeTravel = (msToRun = 0) => { @@ -19,13 +19,14 @@ const error = jest.fn(); const complete = jest.fn(); let searchInterceptor: EnhancedSearchInterceptor; +let mockCoreSetup: MockedKeys; let mockCoreStart: MockedKeys; jest.useFakeTimers(); function mockFetchImplementation(responses: any[]) { let i = 0; - mockCoreStart.http.fetch.mockImplementation(() => { + mockCoreSetup.http.fetch.mockImplementation(() => { const { time = 0, value = {}, isError = false } = responses[i++]; return new Promise((resolve, reject) => setTimeout(() => { @@ -39,6 +40,7 @@ describe('EnhancedSearchInterceptor', () => { let mockUsageCollector: any; beforeEach(() => { + mockCoreSetup = coreMock.createSetup(); mockCoreStart = coreMock.createStart(); next.mockClear(); @@ -54,12 +56,20 @@ describe('EnhancedSearchInterceptor', () => { trackLongQueryRunBeyondTimeout: jest.fn(), }; + const mockPromise = new Promise((resolve) => { + resolve([ + { + application: mockCoreStart.application, + }, + ]); + }); + searchInterceptor = new EnhancedSearchInterceptor( { - toasts: mockCoreStart.notifications.toasts, - application: mockCoreStart.application, - http: mockCoreStart.http, - uiSettings: mockCoreStart.uiSettings, + toasts: mockCoreSetup.notifications.toasts, + startServices: mockPromise as any, + http: mockCoreSetup.http, + uiSettings: mockCoreSetup.uiSettings, usageCollector: mockUsageCollector, }, 1000 @@ -229,8 +239,8 @@ describe('EnhancedSearchInterceptor', () => { expect(error).toHaveBeenCalled(); expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError); - expect(mockCoreStart.http.fetch).toHaveBeenCalledTimes(2); - expect(mockCoreStart.http.delete).toHaveBeenCalled(); + expect(mockCoreSetup.http.fetch).toHaveBeenCalledTimes(2); + expect(mockCoreSetup.http.delete).toHaveBeenCalled(); }); test('should not DELETE a running async search on async timeout prior to first response', async () => { @@ -253,8 +263,8 @@ describe('EnhancedSearchInterceptor', () => { expect(error).toHaveBeenCalled(); expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError); - expect(mockCoreStart.http.fetch).toHaveBeenCalled(); - expect(mockCoreStart.http.delete).not.toHaveBeenCalled(); + expect(mockCoreSetup.http.fetch).toHaveBeenCalled(); + expect(mockCoreSetup.http.delete).not.toHaveBeenCalled(); }); test('should DELETE a running async search on async timeout after first response', async () => { @@ -285,16 +295,16 @@ describe('EnhancedSearchInterceptor', () => { expect(next).toHaveBeenCalled(); expect(error).not.toHaveBeenCalled(); - expect(mockCoreStart.http.fetch).toHaveBeenCalled(); - expect(mockCoreStart.http.delete).not.toHaveBeenCalled(); + expect(mockCoreSetup.http.fetch).toHaveBeenCalled(); + expect(mockCoreSetup.http.delete).not.toHaveBeenCalled(); // Long enough to reach the timeout but not long enough to reach the next response await timeTravel(1000); expect(error).toHaveBeenCalled(); expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError); - expect(mockCoreStart.http.fetch).toHaveBeenCalledTimes(2); - expect(mockCoreStart.http.delete).toHaveBeenCalled(); + expect(mockCoreSetup.http.fetch).toHaveBeenCalledTimes(2); + expect(mockCoreSetup.http.delete).toHaveBeenCalled(); }); test('should DELETE a running async search on async timeout on error from fetch', async () => { @@ -327,16 +337,16 @@ describe('EnhancedSearchInterceptor', () => { expect(next).toHaveBeenCalled(); expect(error).not.toHaveBeenCalled(); - expect(mockCoreStart.http.fetch).toHaveBeenCalled(); - expect(mockCoreStart.http.delete).not.toHaveBeenCalled(); + expect(mockCoreSetup.http.fetch).toHaveBeenCalled(); + expect(mockCoreSetup.http.delete).not.toHaveBeenCalled(); // Long enough to reach the timeout but not long enough to reach the next response await timeTravel(10); expect(error).toHaveBeenCalled(); expect(error.mock.calls[0][0]).toBe(responses[1].value); - expect(mockCoreStart.http.fetch).toHaveBeenCalledTimes(2); - expect(mockCoreStart.http.delete).toHaveBeenCalled(); + expect(mockCoreSetup.http.fetch).toHaveBeenCalledTimes(2); + expect(mockCoreSetup.http.delete).toHaveBeenCalled(); }); }); @@ -367,7 +377,7 @@ describe('EnhancedSearchInterceptor', () => { await timeTravel(); - const areAllRequestsAborted = mockCoreStart.http.fetch.mock.calls.every( + const areAllRequestsAborted = mockCoreSetup.http.fetch.mock.calls.every( ([{ signal }]) => signal?.aborted ); expect(areAllRequestsAborted).toBe(true); diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts index bff9e2cb9048c8..ae6dddf33536f0 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -20,8 +20,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { /** * This class should be instantiated with a `requestTimeout` corresponding with how many ms after * requests are initiated that they should automatically cancel. - * @param toasts The `core.notifications.toasts` service - * @param application The `core.application` service + * @param deps `SearchInterceptorDeps` * @param requestTimeout Usually config value `elasticsearch.requestTimeout` */ constructor(deps: SearchInterceptorDeps, requestTimeout?: number) { @@ -78,7 +77,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { const { combinedSignal, cleanup } = this.setupTimers(options); const aborted$ = from(toPromise(combinedSignal)); - this.pendingCount$.next(++this.pendingCount); + this.pendingCount$.next(this.pendingCount$.getValue() + 1); return this.runSearch(request, combinedSignal, options?.strategy).pipe( expand((response) => { @@ -113,7 +112,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { }, }), finalize(() => { - this.pendingCount$.next(--this.pendingCount); + this.pendingCount$.next(this.pendingCount$.getValue() - 1); cleanup(); }) ); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/index.ts index 6f7b55a3ea4b02..6ce9eefd26445a 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/index.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { OnXJsonEditorUpdateHandler, XJsonEditor } from './xjson_editor'; +export { XJsonEditor } from './xjson_editor'; +export { TextEditor } from './text_editor'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/text_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/text_editor.tsx new file mode 100644 index 00000000000000..1d0e36c0d526c7 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/text_editor.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiPanel } from '@elastic/eui'; +import React, { FunctionComponent } from 'react'; +import { EuiFormRow } from '@elastic/eui'; +import { + CodeEditor, + FieldHook, + getFieldValidityAndErrorMessage, +} from '../../../../../../shared_imports'; + +interface Props { + field: FieldHook; + editorProps: { [key: string]: any }; +} + +export const TextEditor: FunctionComponent = ({ field, editorProps }) => { + const { value, helpText, setValue, label } = field; + const { errorMessage } = getFieldValidityAndErrorMessage(field); + + return ( + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/xjson_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/xjson_editor.tsx index a8456ad0ffd72c..228094c0dfac58 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/xjson_editor.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/xjson_editor.tsx @@ -4,25 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiPanel } from '@elastic/eui'; import { XJsonLang } from '@kbn/monaco'; import React, { FunctionComponent, useCallback } from 'react'; -import { EuiFormRow } from '@elastic/eui'; -import { - CodeEditor, - FieldHook, - getFieldValidityAndErrorMessage, - Monaco, -} from '../../../../../../shared_imports'; +import { FieldHook, Monaco } from '../../../../../../shared_imports'; -export type OnXJsonEditorUpdateHandler = (arg: { - data: { - raw: string; - format(): T; - }; - validate(): boolean; - isValid: boolean | undefined; -}) => void; +import { TextEditor } from './text_editor'; interface Props { field: FieldHook; @@ -30,9 +16,8 @@ interface Props { } export const XJsonEditor: FunctionComponent = ({ field, editorProps }) => { - const { value, helpText, setValue, label } = field; + const { value, setValue } = field; const { xJson, setXJson, convertToJson } = Monaco.useXJsonMode(value); - const { errorMessage } = getFieldValidityAndErrorMessage(field); const onChange = useCallback( (s) => { @@ -42,25 +27,18 @@ export const XJsonEditor: FunctionComponent = ({ field, editorProps }) => [setValue, setXJson, convertToJson] ); return ( - - - { - XJsonLang.registerGrammarChecker(m); - }} - options={{ minimap: { enabled: false } }} - onChange={onChange} - {...(editorProps as any)} - /> - - + { + XJsonLang.registerGrammarChecker(m); + }, + onChange, + ...editorProps, + }} + /> ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/manage_processor_form.container.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/manage_processor_form.container.tsx index ea137b87e66d5d..84551ce152099a 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/manage_processor_form.container.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/manage_processor_form.container.tsx @@ -40,18 +40,19 @@ export const ManageProcessorForm: FunctionComponent = ({ const handleSubmit = useCallback( async (data: FormData, isValid: boolean) => { if (isValid) { - const { type, customOptions, ...options } = data; + const { type, customOptions, fields } = data; onSubmit({ type, - options: customOptions ? customOptions : options, + options: customOptions ? customOptions : fields, }); } }, [onSubmit] ); + const maybeProcessorOptions = processor?.options; const { form } = useForm({ - defaultValue: processor?.options, + defaultValue: { fields: maybeProcessorOptions ?? {} }, onSubmit: handleSubmit, }); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/manage_processor_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/manage_processor_form.tsx index 4e172cce630276..ad6d191be802df 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/manage_processor_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/manage_processor_form.tsx @@ -14,12 +14,12 @@ import { EuiFlyoutHeader, EuiFlyoutBody, EuiFlyoutFooter, - EuiSpacer, EuiTabs, EuiTab, EuiTitle, EuiFlexGroup, EuiFlexItem, + EuiSpacer, } from '@elastic/eui'; import { Form, FormDataProvider, FormHook } from '../../../../../shared_imports'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx index 6b2568bad3afc1..a6447bc30ac00f 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx @@ -5,7 +5,7 @@ */ import React, { FunctionComponent } from 'react'; -import { EuiHorizontalRule } from '@elastic/eui'; +import { EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; import { FormDataProvider } from '../../../../../shared_imports'; import { ProcessorInternal } from '../../types'; @@ -36,6 +36,7 @@ export const ProcessorSettingsFields: FunctionComponent = ({ processor }) return ( <> + ); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/append.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/append.tsx new file mode 100644 index 00000000000000..8eb484b56bafe0 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/append.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { + FIELD_TYPES, + fieldValidators, + UseField, + ComboBoxField, +} from '../../../../../../shared_imports'; + +import { FieldsConfig } from './shared'; +import { FieldNameField } from './common_fields/field_name_field'; + +const { emptyField } = fieldValidators; + +const fieldsConfig: FieldsConfig = { + value: { + type: FIELD_TYPES.COMBO_BOX, + deserializer: (v) => (Array.isArray(v) ? v : [String(v)]), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.appendForm.valueFieldLabel', { + defaultMessage: 'Value', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.appendForm.valueFieldHelpText', { + defaultMessage: 'The value to be appended by this processor.', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.appendForm.valueRequiredError', { + defaultMessage: 'A value to set is required.', + }) + ), + }, + ], + }, +}; + +export const Append: FunctionComponent = () => { + return ( + <> + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/bytes.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/bytes.tsx new file mode 100644 index 00000000000000..64a501f03d454e --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/bytes.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { FIELD_TYPES, UseField, Field } from '../../../../../../shared_imports'; + +import { FieldsConfig } from './shared'; +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; +import { FieldNameField } from './common_fields/field_name_field'; + +const fieldsConfig: FieldsConfig = { + target_field: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.bytesForm.targetFieldLabel', { + defaultMessage: 'Target field (optional)', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.bytesForm.targetFieldHelpText', { + defaultMessage: 'The field to assign the converted value to', + }), + }, +}; + +export const Bytes: FunctionComponent = () => { + return ( + <> + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/circle.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/circle.tsx new file mode 100644 index 00000000000000..3a39e597cb8dce --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/circle.tsx @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { + FIELD_TYPES, + fieldValidators, + UseField, + Field, + SelectField, + NumericField, +} from '../../../../../../shared_imports'; + +import { FieldsConfig } from './shared'; +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; +import { FieldNameField } from './common_fields/field_name_field'; + +const { emptyField } = fieldValidators; + +const fieldsConfig: FieldsConfig = { + target_field: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.circleForm.targetFieldLabel', { + defaultMessage: 'Target field (optional)', + }), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.circleForm.targetFieldHelpText', + { + defaultMessage: 'By default field is updated in-place.', + } + ), + }, + error_distance: { + type: FIELD_TYPES.NUMBER, + deserializer: (v) => (typeof v === 'number' && !isNaN(v) ? v : 1.0), + serializer: Number, + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceFieldLabel', + { + defaultMessage: 'Error distance', + } + ), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceHelpText', + { + defaultMessage: + 'The difference between the resulting inscribed distance from center to side and the circle’s radius (measured in meters for geo_shape, unit-less for shape).', + } + ), + validations: [ + { + validator: ({ value }) => { + return isNaN(Number(value)) + ? { + message: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.circleForm.errorDistanceError', + { + defaultMessage: 'An error distance value is required.', + } + ), + } + : undefined; + }, + }, + ], + }, + shape_type: { + type: FIELD_TYPES.SELECT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeFieldLabel', { + defaultMessage: 'Shape type', + }), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeFieldHelpText', + { defaultMessage: 'Which field mapping type is to be used.' } + ), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.circleForm.shapeTypeRequiredError', { + defaultMessage: 'A shape type value is required.', + }) + ), + }, + ], + }, +}; + +export const Circle: FunctionComponent = () => { + return ( + <> + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/common_processor_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/common_processor_fields.tsx index 4802653f9e6807..7ae7b82c31a43e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/common_processor_fields.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/common_processor_fields.tsx @@ -15,41 +15,64 @@ import { ToggleField, } from '../../../../../../../shared_imports'; +import { TextEditor } from '../../field_components'; + const ignoreFailureConfig: FieldConfig = { defaultValue: false, + serializer: (v) => (v === false ? undefined : v), + deserializer: (v) => (typeof v === 'boolean' ? v : undefined), label: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.commonFields.ignoreFailureFieldLabel', { defaultMessage: 'Ignore failure', } ), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.commonFields.ignoreFailureHelpText', + { defaultMessage: 'Ignore failures for this processor.' } + ), type: FIELD_TYPES.TOGGLE, }; const ifConfig: FieldConfig = { - defaultValue: undefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.ifFieldLabel', { defaultMessage: 'Condition (optional)', }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.ifFieldHelpText', { + defaultMessage: 'Conditionally execute this processor.', + }), type: FIELD_TYPES.TEXT, }; const tagConfig: FieldConfig = { - defaultValue: undefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.tagFieldLabel', { defaultMessage: 'Tag (optional)', }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.tagFieldHelpText', { + defaultMessage: 'An identifier for this processor. Useful for debugging and metrics.', + }), type: FIELD_TYPES.TEXT, }; export const CommonProcessorFields: FunctionComponent = () => { return ( - <> - +
+ - + - - + +
); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/field_name_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/field_name_field.tsx new file mode 100644 index 00000000000000..7ef5ba6768c190 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/field_name_field.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + FIELD_TYPES, + UseField, + Field, + fieldValidators, + ValidationConfig, +} from '../../../../../../../shared_imports'; + +import { FieldsConfig } from '../shared'; + +const { emptyField } = fieldValidators; + +export const fieldsConfig: FieldsConfig = { + field: { + type: FIELD_TYPES.TEXT, + deserializer: String, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.fieldFieldLabel', { + defaultMessage: 'Field', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.fieldRequiredError', { + defaultMessage: 'A field value is required.', + }) + ), + }, + ], + }, +}; + +interface Props { + helpText?: React.ReactNode; + /** + * The field name requires a value. Processor specific validation + * checks can be added here. + */ + additionalValidations?: ValidationConfig[]; +} + +export const FieldNameField: FunctionComponent = ({ helpText, additionalValidations }) => ( + +); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx new file mode 100644 index 00000000000000..08eb0a425ef338 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FIELD_TYPES, UseField, ToggleField } from '../../../../../../../shared_imports'; + +import { FieldsConfig } from '../shared'; + +export const fieldsConfig: FieldsConfig = { + ignore_missing: { + type: FIELD_TYPES.TOGGLE, + defaultValue: false, + serializer: (v) => (v === false ? undefined : v), + deserializer: (v) => (typeof v === 'boolean' ? v : undefined), + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.commonFields.ignoreMissingFieldLabel', + { + defaultMessage: 'Ignore missing', + } + ), + }, +}; + +interface Props { + helpText?: string; +} + +export const IgnoreMissingField: FunctionComponent = ({ helpText }) => ( + +); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/processor_type_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/processor_type_field.tsx index 71ee4a714a28e3..e4ad90f61af0a1 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/processor_type_field.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/processor_type_field.tsx @@ -46,20 +46,12 @@ interface Props { const { emptyField } = fieldValidators; -const typeConfig: FieldConfig = { +const typeConfig: FieldConfig = { type: FIELD_TYPES.COMBO_BOX, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.typeField.typeFieldLabel', { defaultMessage: 'Processor', }), - deserializer: (value: string | undefined) => { - if (value) { - return [value]; - } - return []; - }, - serializer: (value: string[]) => { - return value[0]; - }, + deserializer: String, validations: [ { validator: emptyField( @@ -73,11 +65,11 @@ const typeConfig: FieldConfig = { export const ProcessorTypeField: FunctionComponent = ({ initialType }) => { return ( - + config={typeConfig} defaultValue={initialType} path="type"> {(typeField) => { let selectedOptions: ProcessorTypeAndLabel[]; - if ((typeField.value as string[]).length) { - const [type] = typeField.value as string[]; + if (typeField.value?.length) { + const type = typeField.value; const descriptor = getProcessorDescriptor(type); selectedOptions = descriptor ? [{ label: descriptor.label, value: type }] @@ -103,9 +95,7 @@ export const ProcessorTypeField: FunctionComponent = ({ initialType }) => return false; } - const newValue = [...(typeField.value as string[]), value]; - - typeField.setValue(newValue); + typeField.setValue(value); }; return ( @@ -131,8 +121,9 @@ export const ProcessorTypeField: FunctionComponent = ({ initialType }) => options={processorTypesAndLabels} selectedOptions={selectedOptions} onCreateOption={onCreateComboOption} - onChange={(options: EuiComboBoxOptionOption[]) => { - typeField.setValue(options.map(({ value }) => value)); + onChange={(options: Array>) => { + const [selection] = options; + typeField.setValue(selection?.value! ?? ''); }} noSuggestions={false} singleSelection={{ diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/convert.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/convert.tsx new file mode 100644 index 00000000000000..b45f589bf0f922 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/convert.tsx @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { + FIELD_TYPES, + fieldValidators, + UseField, + Field, + SelectField, +} from '../../../../../../shared_imports'; + +import { FieldsConfig } from './shared'; +import { FieldNameField } from './common_fields/field_name_field'; +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; + +const { emptyField } = fieldValidators; + +const fieldsConfig: FieldsConfig = { + /* Required fields config */ + type: { + type: FIELD_TYPES.TEXT, + defaultValue: '', + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.convertForm.typeFieldLabel', { + defaultMessage: 'Type', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.convertForm.typeFieldHelpText', { + defaultMessage: 'The type to convert the existing value to.', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.convertForm.typeRequiredError', { + defaultMessage: 'A type value is required.', + }) + ), + }, + ], + }, + /* Optional fields config */ + target_field: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.convertForm.targetFieldLabel', { + defaultMessage: 'Target field (optional)', + }), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.convertForm.targetFieldHelpText', + { + defaultMessage: 'The field to assign the converted value to.', + } + ), + }, +}; + +export const Convert: FunctionComponent = () => { + return ( + <> + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/csv.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/csv.tsx new file mode 100644 index 00000000000000..3ac0179ca02a69 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/csv.tsx @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiCode } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + FIELD_TYPES, + fieldValidators, + UseField, + Field, + ToggleField, + ComboBoxField, + ValidationFunc, +} from '../../../../../../shared_imports'; + +import { FieldsConfig } from './shared'; +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; +import { FieldNameField } from './common_fields/field_name_field'; + +import { isArrayOfStrings } from './shared'; + +const { minLengthField } = fieldValidators; + +/** + * Allow empty strings ('') to pass this validation. + */ +const isStringLengthOne: ValidationFunc = ({ value }) => { + return typeof value === 'string' && value !== '' && value.length !== 1 + ? { + message: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.convertForm.separatorLengthError', + { + defaultMessage: 'A separator value must be 1 character.', + } + ), + } + : undefined; +}; + +const fieldsConfig: FieldsConfig = { + /* Required fields config */ + target_fields: { + type: FIELD_TYPES.COMBO_BOX, + deserializer: (v) => { + return isArrayOfStrings(v) ? v : []; + }, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.csvForm.targetFieldsFieldLabel', { + defaultMessage: 'Target fields', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.csvForm.targetFieldsHelpText', { + defaultMessage: 'The array of fields to assign extracted values to.', + }), + validations: [ + { + validator: minLengthField({ + length: 1, + message: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.csvForm.targetFieldRequiredError', + { + defaultMessage: 'A target fields value is required.', + } + ), + }), + }, + ], + }, + /* Optional fields config */ + separator: { + type: FIELD_TYPES.TEXT, + serializer: (v) => (v ? v : undefined), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.convertForm.separatorFieldLabel', { + defaultMessage: 'Separator (optional)', + }), + validations: [ + { + validator: isStringLengthOne, + }, + ], + helpText: ( + {','} }} + /> + ), + }, + quote: { + type: FIELD_TYPES.TEXT, + serializer: (v) => (v ? v : undefined), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.convertForm.quoteFieldLabel', { + defaultMessage: 'Quote (optional)', + }), + validations: [ + { + validator: isStringLengthOne, + }, + ], + helpText: ( + {'"'} }} + /> + ), + }, + trim: { + type: FIELD_TYPES.TOGGLE, + defaultValue: false, + deserializer: (v) => (typeof v === 'boolean' ? v : undefined), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.csvForm.trimFieldLabel', { + defaultMessage: 'Trim', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.csvForm.trimFieldHelpText', { + defaultMessage: 'Trim whitespaces in unquoted fields', + }), + }, + empty_value: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.convertForm.emptyValueFieldLabel', { + defaultMessage: 'Empty value (optional)', + }), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.convertForm.emptyValueFieldHelpText', + { + defaultMessage: + 'Value used to fill empty fields, empty fields will be skipped if this is not provided.', + } + ), + }, +}; + +export const CSV: FunctionComponent = () => { + return ( + <> + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date.tsx new file mode 100644 index 00000000000000..424e84058ac3fc --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date.tsx @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiCode } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + FIELD_TYPES, + fieldValidators, + UseField, + Field, + ComboBoxField, +} from '../../../../../../shared_imports'; + +import { FieldsConfig, isArrayOfStrings } from './shared'; +import { FieldNameField } from './common_fields/field_name_field'; + +const { minLengthField } = fieldValidators; + +const fieldsConfig: FieldsConfig = { + /* Required fields config */ + formats: { + type: FIELD_TYPES.COMBO_BOX, + deserializer: (v) => { + return isArrayOfStrings(v) ? v : []; + }, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.dateForm.formatsFieldLabel', { + defaultMessage: 'Formats', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.dateForm.formatsFieldHelpText', { + defaultMessage: + 'An array of the expected date formats. Can be a java time pattern or one of the following formats: ISO8601, UNIX, UNIX_MS, or TAI64N.', + }), + validations: [ + { + validator: minLengthField({ + length: 1, + message: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dateForm.formatsRequiredError', + { + defaultMessage: 'A value for formats is required.', + } + ), + }), + }, + ], + }, + /* Optional fields config */ + target_field: { + type: FIELD_TYPES.TEXT, + serializer: (v) => (v ? undefined : v), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.dateForm.targetFieldFieldLabel', { + defaultMessage: 'Target field (optional)', + }), + helpText: ( + {'@timestamp'}, + }} + /> + ), + }, + timezone: { + type: FIELD_TYPES.TEXT, + serializer: (v) => (v ? v : undefined), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.dateForm.timezoneFieldLabel', { + defaultMessage: 'Timezone (optional)', + }), + helpText: ( + {'UTC'} }} + /> + ), + }, + locale: { + type: FIELD_TYPES.TEXT, + serializer: (v) => (v ? v : undefined), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.dateForm.localeFieldLabel', { + defaultMessage: 'Locale (optional)', + }), + helpText: ( + {'ENGLISH'} }} + /> + ), + }, +}; + +/** + * Disambiguate from global Date object + */ +export const DateProcessor: FunctionComponent = () => { + return ( + <> + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date_index_name.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date_index_name.tsx new file mode 100644 index 00000000000000..387c9ff4e0b46a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date_index_name.tsx @@ -0,0 +1,242 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiCode } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + FIELD_TYPES, + fieldValidators, + UseField, + Field, + ComboBoxField, + SelectField, +} from '../../../../../../shared_imports'; + +import { FieldsConfig, isArrayOfStrings } from './shared'; +import { FieldNameField } from './common_fields/field_name_field'; + +const { emptyField } = fieldValidators; + +const fieldsConfig: FieldsConfig = { + /* Required fields config */ + date_rounding: { + type: FIELD_TYPES.SELECT, + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRoundingFieldLabel', + { + defaultMessage: 'Date rounding', + } + ), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRoundingFieldHelpText', + { + defaultMessage: 'How to round the date when formatting the date into the index name.', + } + ), + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateRoundingRequiredError', + { + defaultMessage: 'A field value is required.', + } + ) + ), + }, + ], + }, + /* Optional fields config */ + index_name_prefix: { + type: FIELD_TYPES.TEXT, + serializer: (v) => (v ? v : undefined), + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.indexNamePrefixFieldLabel', + { + defaultMessage: 'Index name prefix (optional)', + } + ), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.indexNamePrefixFieldHelpText', + { defaultMessage: 'A prefix of the index name to be prepended before the printed date.' } + ), + }, + index_name_format: { + type: FIELD_TYPES.TEXT, + serializer: (v) => (v ? v : undefined), + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.indexNameFormatFieldLabel', + { + defaultMessage: 'Index name format (optional)', + } + ), + helpText: ( + {'yyyy-MM-dd'} }} + /> + ), + }, + date_formats: { + type: FIELD_TYPES.COMBO_BOX, + serializer: (v: string[]) => { + return v.length ? v : undefined; + }, + deserializer: (v) => { + return isArrayOfStrings(v) ? v : []; + }, + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.dateFormatsFieldLabel', + { + defaultMessage: 'Date formats (optional)', + } + ), + helpText: ( + {"yyyy-MM-dd'T'HH:mm:ss.SSSXX"} }} + /> + ), + }, + timezone: { + type: FIELD_TYPES.TEXT, + serializer: (v) => (v ? v : undefined), + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.timezoneFieldLabel', + { + defaultMessage: 'Timezone (optional)', + } + ), + helpText: ( + {'UTC'} }} + /> + ), + }, + locale: { + type: FIELD_TYPES.TEXT, + serializer: (v) => (v ? v : undefined), + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.localeFieldLabel', + { + defaultMessage: 'Locale (optional)', + } + ), + helpText: ( + {'ENGLISH'} }} + /> + ), + }, +}; + +/** + * Disambiguate from global Date object + */ +export const DateIndexName: FunctionComponent = () => { + return ( + <> + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dissect.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dissect.tsx new file mode 100644 index 00000000000000..5f9f55ced1a256 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dissect.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiCode } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { TextEditor } from '../field_components'; + +import { + FieldConfig, + FIELD_TYPES, + fieldValidators, + UseField, + Field, +} from '../../../../../../shared_imports'; + +import { FieldNameField } from './common_fields/field_name_field'; +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; + +const { emptyField } = fieldValidators; + +const fieldsConfig: Record = { + /* Required field config */ + pattern: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.dissectForm.patternFieldLabel', { + defaultMessage: 'Pattern', + }), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dissectForm.patternFieldHelpText', + { + defaultMessage: 'The pattern to apply to the field.', + } + ), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.dissectForm.patternRequiredError', { + defaultMessage: 'A pattern value is required.', + }) + ), + }, + ], + }, + /* Optional field config */ + append_separator: { + type: FIELD_TYPES.TEXT, + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dissectForm.appendSeparatorparaotrFieldLabel', + { + defaultMessage: 'Append separator (optional)', + } + ), + helpText: ( + {'""'} }} + /> + ), + }, +}; + +export const Dissect: FunctionComponent = () => { + return ( + <> + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dot_expander.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dot_expander.tsx new file mode 100644 index 00000000000000..4e50c61ac930c3 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dot_expander.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { FieldConfig, FIELD_TYPES, UseField, Field } from '../../../../../../shared_imports'; + +import { FieldNameField } from './common_fields/field_name_field'; + +const fieldsConfig: Record = { + path: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.dotExpanderForm.pathFieldLabel', { + defaultMessage: 'Path', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.dotExpanderForm.pathHelpText', { + defaultMessage: 'Only required if the field to expand is part another object field.', + }), + }, +}; + +export const DotExpander: FunctionComponent = () => { + return ( + <> + { + if (typeof value === 'string' && value.length) { + return !value.includes('.') + ? { + message: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dotExpanderForm.fieldNameRequiresDotError', + { defaultMessage: 'A field value requires at least one dot character.' } + ), + } + : undefined; + } + }, + }, + ]} + /> + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/drop.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/drop.tsx new file mode 100644 index 00000000000000..87b6cb76cdccec --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/drop.tsx @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FunctionComponent } from 'react'; + +/** + * This fields component has no unique fields + */ +export const Drop: FunctionComponent = () => { + return null; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx index 77f85e61eff6b4..3148022adaa980 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx @@ -15,6 +15,7 @@ import { UseField, Field, } from '../../../../../../shared_imports'; +import { TextEditor } from '../field_components'; const { emptyField } = fieldValidators; @@ -84,15 +85,25 @@ const ignoreMissingConfig: FieldConfig = { export const Gsub: FunctionComponent = () => { return ( <> - + - + - + - + - + ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/index.ts new file mode 100644 index 00000000000000..6996deb2d861c5 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { Append } from './append'; +export { Bytes } from './bytes'; +export { Circle } from './circle'; +export { Convert } from './convert'; +export { CSV } from './csv'; +export { DateProcessor } from './date'; +export { DateIndexName } from './date_index_name'; +export { Dissect } from './dissect'; +export { DotExpander } from './dot_expander'; +export { Drop } from './drop'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx index 1ba6a14d0448d8..88cea620ae804e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx @@ -64,11 +64,11 @@ const overrideConfig: FieldConfig = { export const SetProcessor: FunctionComponent = () => { return ( <> - + - + - + ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/shared.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/shared.ts new file mode 100644 index 00000000000000..a0a31dd3a8e934 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/shared.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; +import { isRight } from 'fp-ts/lib/Either'; +import { flow } from 'fp-ts/lib/function'; + +import { FieldConfig } from '../../../../../../shared_imports'; + +export const arrayOfStrings = rt.array(rt.string); +export const isArrayOfStrings = flow(arrayOfStrings.decode, isRight); + +export type FieldsConfig = Record; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx index 7055721fc8b07c..502045084b24de 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx @@ -7,6 +7,19 @@ import { i18n } from '@kbn/i18n'; import { FunctionComponent } from 'react'; +import { + Append, + Bytes, + Circle, + Convert, + CSV, + DateProcessor, + DateIndexName, + Dissect, + DotExpander, + Drop, +} from '../manage_processor_form/processors'; + // import { SetProcessor } from './processors/set'; // import { Gsub } from './processors/gsub'; @@ -23,70 +36,70 @@ type MapProcessorTypeToDescriptor = Record; export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { append: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Append, docLinkPath: '/append-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.append', { defaultMessage: 'Append', }), }, bytes: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Bytes, docLinkPath: '/bytes-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.bytes', { defaultMessage: 'Bytes', }), }, circle: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Circle, docLinkPath: '/ingest-circle-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.circle', { defaultMessage: 'Circle', }), }, convert: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Convert, docLinkPath: '/convert-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.convert', { defaultMessage: 'Convert', }), }, csv: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: CSV, docLinkPath: '/csv-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.csv', { defaultMessage: 'CSV', }), }, date: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: DateProcessor, docLinkPath: '/date-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.date', { defaultMessage: 'Date', }), }, date_index_name: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: DateIndexName, docLinkPath: '/date-index-name-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.dateIndexName', { defaultMessage: 'Date Index Name', }), }, dissect: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Dissect, docLinkPath: '/dissect-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.dissect', { defaultMessage: 'Dissect', }), }, dot_expander: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: DotExpander, docLinkPath: '/dot-expand-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.dotExpander', { defaultMessage: 'Dot Expander', }), }, drop: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Drop, docLinkPath: '/drop-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.drop', { defaultMessage: 'Drop', diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts index d2c4b73a487679..936db37f0c6292 100644 --- a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -43,6 +43,8 @@ export { FieldConfig, FieldHook, getFieldValidityAndErrorMessage, + ValidationFunc, + ValidationConfig, } from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; export { @@ -57,6 +59,9 @@ export { FormRow, ToggleField, ComboBoxField, + RadioGroupField, + NumericField, + SelectField, } from '../../../../src/plugins/es_ui_shared/static/forms/components'; export { diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.mock.ts index 0450849931b300..da22e33dc7b524 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_item_schema.mock.ts @@ -8,6 +8,7 @@ import { COMMENTS, DESCRIPTION, ENTRIES, + ITEM_ID, ITEM_TYPE, LIST_ID, META, @@ -32,3 +33,26 @@ export const getCreateExceptionListItemSchemaMock = (): CreateExceptionListItemS tags: TAGS, type: ITEM_TYPE, }); + +/** + * Useful for end to end testing + */ +export const getCreateExceptionListItemMinimalSchemaMock = (): CreateExceptionListItemSchema => ({ + description: DESCRIPTION, + entries: ENTRIES, + item_id: ITEM_ID, + list_id: LIST_ID, + name: NAME, + type: ITEM_TYPE, +}); + +/** + * Useful for end to end testing + */ +export const getCreateExceptionListItemMinimalSchemaMockWithoutId = (): CreateExceptionListItemSchema => ({ + description: DESCRIPTION, + entries: ENTRIES, + list_id: LIST_ID, + name: NAME, + type: ITEM_TYPE, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.mock.ts index d9c04746103690..f8431fcce1bf76 100644 --- a/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/request/create_exception_list_schema.mock.ts @@ -7,6 +7,7 @@ import { DESCRIPTION, ENDPOINT_TYPE, + LIST_ID, META, NAME, NAMESPACE_TYPE, @@ -26,3 +27,22 @@ export const getCreateExceptionListSchemaMock = (): CreateExceptionListSchema => type: ENDPOINT_TYPE, version: VERSION, }); + +/** + * Useful for end to end testing + */ +export const getCreateExceptionListMinimalSchemaMock = (): CreateExceptionListSchema => ({ + description: DESCRIPTION, + list_id: LIST_ID, + name: NAME, + type: ENDPOINT_TYPE, +}); + +/** + * Useful for end to end testing + */ +export const getCreateExceptionListMinimalSchemaMockWithoutId = (): CreateExceptionListSchema => ({ + description: DESCRIPTION, + name: NAME, + type: ENDPOINT_TYPE, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.mock.ts index 90d70c273f490e..4673c0fe7629d2 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_item_schema.mock.ts @@ -9,6 +9,7 @@ import { DESCRIPTION, ENTRIES, ID, + ITEM_ID, ITEM_TYPE, LIST_ITEM_ID, META, @@ -34,3 +35,15 @@ export const getUpdateExceptionListItemSchemaMock = (): UpdateExceptionListItemS tags: TAGS, type: ITEM_TYPE, }); + +/** + * Useful for end to end tests and other mechanisms which want to fill in the values + * after doing a get of the structure. + */ +export const getUpdateMinimalExceptionListItemSchemaMock = (): UpdateExceptionListItemSchema => ({ + description: DESCRIPTION, + entries: ENTRIES, + item_id: ITEM_ID, + name: NAME, + type: ITEM_TYPE, +}); diff --git a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.mock.ts index 22af29e6af0b78..b7dc2d9e0c9487 100644 --- a/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/request/update_exception_list_schema.mock.ts @@ -20,3 +20,14 @@ export const getUpdateExceptionListSchemaMock = (): UpdateExceptionListSchema => tags: ['malware'], type: 'endpoint', }); + +/** + * Useful for end to end tests and other mechanisms which want to fill in the values + * after doing a get of the structure. + */ +export const getUpdateMinimalExceptionListSchemaMock = (): UpdateExceptionListSchema => ({ + description: DESCRIPTION, + list_id: LIST_ID, + name: NAME, + type: 'endpoint', +}); diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts index c0d04c9823ca3b..1a8f21a5232f80 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_item_schema.mock.ts @@ -7,8 +7,11 @@ import { COMMENTS, DATE_NOW, DESCRIPTION, + ELASTIC_USER, ENTRIES, + ITEM_ID, ITEM_TYPE, + LIST_ID, META, NAME, NAMESPACE_TYPE, @@ -38,3 +41,24 @@ export const getExceptionListItemSchemaMock = (): ExceptionListItemSchema => ({ updated_at: DATE_NOW, updated_by: USER, }); + +/** + * This is useful for end to end tests where we remove the auto generated parts for comparisons + * such as created_at, updated_at, and id. + */ +export const getExceptionListItemResponseMockWithoutAutoGeneratedValues = (): Partial< + ExceptionListItemSchema +> => ({ + _tags: [], + comments: [], + created_by: ELASTIC_USER, + description: DESCRIPTION, + entries: ENTRIES, + item_id: ITEM_ID, + list_id: LIST_ID, + name: NAME, + namespace_type: 'single', + tags: [], + type: ITEM_TYPE, + updated_by: ELASTIC_USER, +}); diff --git a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts index 2655b09631b23c..e2f0a7c06b4004 100644 --- a/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/response/exception_list_schema.mock.ts @@ -7,9 +7,12 @@ import { DATE_NOW, DESCRIPTION, + ELASTIC_USER, ENDPOINT_TYPE, IMMUTABLE, + LIST_ID, META, + NAME, TIE_BREAKER, USER, VERSION, @@ -18,6 +21,7 @@ import { import { ENDPOINT_LIST_ID } from '../..'; import { ExceptionListSchema } from './exception_list_schema'; + export const getExceptionListSchemaMock = (): ExceptionListSchema => ({ _tags: ['endpoint', 'process', 'malware', 'os:linux'], _version: _VERSION, @@ -37,3 +41,23 @@ export const getExceptionListSchemaMock = (): ExceptionListSchema => ({ updated_by: 'user_name', version: VERSION, }); + +/** + * This is useful for end to end tests where we remove the auto generated parts for comparisons + * such as created_at, updated_at, and id. + */ +export const getExceptionResponseMockWithoutAutoGeneratedValues = (): Partial< + ExceptionListSchema +> => ({ + _tags: [], + created_by: ELASTIC_USER, + description: DESCRIPTION, + immutable: IMMUTABLE, + list_id: LIST_ID, + name: NAME, + namespace_type: 'single', + tags: [], + type: ENDPOINT_TYPE, + updated_by: ELASTIC_USER, + version: VERSION, +}); diff --git a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts index fc0473b2b37040..f092aec82a8f3e 100644 --- a/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/create_exception_list_item_route.ts @@ -57,7 +57,7 @@ export const createExceptionListItemRoute = (router: IRouter): void => { }); if (exceptionList == null) { return siemResponse.error({ - body: `list id: "${listId}" does not exist`, + body: `exception list id: "${listId}" does not exist`, statusCode: 404, }); } else { diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts index 88643e53ff0a72..103cba700013f9 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts @@ -62,7 +62,7 @@ export const findExceptionListItemRoute = (router: IRouter): void => { }); if (exceptionListItems == null) { return siemResponse.error({ - body: `list id: "${listId}" does not exist`, + body: `exception list id: "${listId}" does not exist`, statusCode: 404, }); } diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts index 7e15f694aee13c..745ad0735a1747 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_item_route.ts @@ -54,39 +54,46 @@ export const updateExceptionListItemRoute = (router: IRouter): void => { namespace_type: namespaceType, tags, } = request.body; - const exceptionLists = getExceptionListClient(context); - const exceptionListItem = await exceptionLists.updateExceptionListItem({ - _tags, - _version, - comments, - description, - entries, - id, - itemId, - meta, - name, - namespaceType, - tags, - type, - }); - if (exceptionListItem == null) { - if (id != null) { - return siemResponse.error({ - body: `list item id: "${id}" not found`, - statusCode: 404, - }); - } else { - return siemResponse.error({ - body: `list item item_id: "${itemId}" not found`, - statusCode: 404, - }); - } + if (id == null && itemId == null) { + return siemResponse.error({ + body: 'either id or item_id need to be defined', + statusCode: 404, + }); } else { - const [validated, errors] = validate(exceptionListItem, exceptionListItemSchema); - if (errors != null) { - return siemResponse.error({ body: errors, statusCode: 500 }); + const exceptionLists = getExceptionListClient(context); + const exceptionListItem = await exceptionLists.updateExceptionListItem({ + _tags, + _version, + comments, + description, + entries, + id, + itemId, + meta, + name, + namespaceType, + tags, + type, + }); + if (exceptionListItem == null) { + if (id != null) { + return siemResponse.error({ + body: `exception list item id: "${id}" does not exist`, + statusCode: 404, + }); + } else { + return siemResponse.error({ + body: `exception list item item_id: "${itemId}" does not exist`, + statusCode: 404, + }); + } } else { - return response.ok({ body: validated ?? {} }); + const [validated, errors] = validate(exceptionListItem, exceptionListItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } } } } catch (err) { diff --git a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts index 8102210b8430d7..1903d0f601d1d1 100644 --- a/x-pack/plugins/lists/server/routes/update_exception_list_route.ts +++ b/x-pack/plugins/lists/server/routes/update_exception_list_route.ts @@ -15,7 +15,7 @@ import { updateExceptionListSchema, } from '../../common/schemas'; -import { getExceptionListClient } from './utils'; +import { getErrorMessageExceptionList, getExceptionListClient } from './utils'; export const updateExceptionListRoute = (router: IRouter): void => { router.put( @@ -50,7 +50,7 @@ export const updateExceptionListRoute = (router: IRouter): void => { const exceptionLists = getExceptionListClient(context); if (id == null && listId == null) { return siemResponse.error({ - body: `either id or list_id need to be defined`, + body: 'either id or list_id need to be defined', statusCode: 404, }); } else { @@ -69,7 +69,7 @@ export const updateExceptionListRoute = (router: IRouter): void => { }); if (list == null) { return siemResponse.error({ - body: `exception list id: "${id}" not found`, + body: getErrorMessageExceptionList({ id, listId }), statusCode: 404, }); } else { diff --git a/x-pack/plugins/lists/server/routes/utils/get_error_message_exception_list.ts b/x-pack/plugins/lists/server/routes/utils/get_error_message_exception_list.ts index 665a7540184a03..7db3bedd9ec84c 100644 --- a/x-pack/plugins/lists/server/routes/utils/get_error_message_exception_list.ts +++ b/x-pack/plugins/lists/server/routes/utils/get_error_message_exception_list.ts @@ -12,10 +12,10 @@ export const getErrorMessageExceptionList = ({ listId: string | undefined; }): string => { if (id != null) { - return `Exception list id: "${id}" does not exist`; + return `exception list id: "${id}" does not exist`; } else if (listId != null) { - return `Exception list list_id: "${listId}" does not exist`; + return `exception list list_id: "${listId}" does not exist`; } else { - return 'Exception list does not exist'; + return 'exception list does not exist'; } }; diff --git a/x-pack/plugins/lists/server/routes/utils/get_error_message_exception_list_item.ts b/x-pack/plugins/lists/server/routes/utils/get_error_message_exception_list_item.ts index 8e6730ef3db5cd..efb6c0e59ade56 100644 --- a/x-pack/plugins/lists/server/routes/utils/get_error_message_exception_list_item.ts +++ b/x-pack/plugins/lists/server/routes/utils/get_error_message_exception_list_item.ts @@ -12,10 +12,10 @@ export const getErrorMessageExceptionListItem = ({ itemId: string | undefined; }): string => { if (id != null) { - return `Exception list item id: "${id}" does not exist`; + return `exception list item id: "${id}" does not exist`; } else if (itemId != null) { - return `Exception list item list_id: "${itemId}" does not exist`; + return `exception list item item_id: "${itemId}" does not exist`; } else { - return 'Exception list item does not exist'; + return 'exception list item does not exist'; } }; diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts index 2102673060273f..d0bce8508e82e7 100644 --- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts @@ -36,8 +36,8 @@ export interface MlSummaryJob { export interface AuditMessage { job_id: string; msgTime: number; - level: number; - highestLevel: number; + level: string; + highestLevel: string; highestLevelText: string; text: string; } diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index c61db9fb1ad8da..7b4ea5458f4a61 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -16,7 +16,8 @@ "share", "embeddable", "uiActions", - "kibanaLegacy" + "kibanaLegacy", + "indexPatternManagement" ], "optionalPlugins": [ "security", diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index 42c462fa9d8697..c281dc4e9ae059 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -21,7 +21,8 @@ import { MlRouter } from './routing'; import { mlApiServicesProvider } from './services/ml_api_service'; import { HttpService } from './services/http_service'; -export type MlDependencies = Omit & MlStartDependencies; +export type MlDependencies = Omit & + MlStartDependencies; interface AppProps { coreStart: CoreStart; diff --git a/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js b/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js index 1b33d680422958..329863fdc99862 100644 --- a/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js +++ b/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js @@ -62,7 +62,10 @@ describe('FieldTitleBar', () => { expect(hasClassName).toBeTruthy(); }); - test(`tooltip hovering`, (done) => { + test(`tooltip hovering`, () => { + // Use fake timers so we don't have to wait for the EuiToolTip timeout + jest.useFakeTimers(); + const props = { card: { fieldName: 'foo', type: 'bar' } }; const wrapper = mountWithIntl(); const container = wrapper.find({ className: 'field-name' }); @@ -70,14 +73,22 @@ describe('FieldTitleBar', () => { expect(wrapper.find('EuiToolTip').children()).toHaveLength(1); container.simulate('mouseover'); - // EuiToolTip mounts children after a 250ms delay - setTimeout(() => { - wrapper.update(); - expect(wrapper.find('EuiToolTip').children()).toHaveLength(2); - container.simulate('mouseout'); - wrapper.update(); - expect(wrapper.find('EuiToolTip').children()).toHaveLength(1); - done(); - }, 250); + + // Run the timers so the EuiTooltip will be visible + jest.runAllTimers(); + + wrapper.update(); + expect(wrapper.find('EuiToolTip').children()).toHaveLength(2); + + container.simulate('mouseout'); + + // Run the timers so the EuiTooltip will be hidden again + jest.runAllTimers(); + + wrapper.update(); + expect(wrapper.find('EuiToolTip').children()).toHaveLength(1); + + // Clearing all mocks will also reset fake timers. + jest.clearAllMocks(); }); }); diff --git a/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js b/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js index 7e37dc10ade33b..d4200c2f8366b4 100644 --- a/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js +++ b/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js @@ -27,6 +27,9 @@ describe('FieldTypeIcon', () => { }); test(`render with tooltip and test hovering`, () => { + // Use fake timers so we don't have to wait for the EuiToolTip timeout + jest.useFakeTimers(); + const typeIconComponent = mount( ); @@ -35,11 +38,23 @@ describe('FieldTypeIcon', () => { expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(1); container.simulate('mouseover'); - // EuiToolTip mounts children after a 250ms delay - setTimeout(() => expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(2), 250); + + // Run the timers so the EuiTooltip will be visible + jest.runAllTimers(); + + typeIconComponent.update(); + expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(2); container.simulate('mouseout'); + + // Run the timers so the EuiTooltip will be hidden again + jest.runAllTimers(); + + typeIconComponent.update(); expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(1); + + // Clearing all mocks will also reset fake timers. + jest.clearAllMocks(); }); test(`update component`, () => { diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index aa6163379f9c0a..ff59d46de758d9 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -20,8 +20,10 @@ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { HomePublicPluginSetup } from 'src/plugins/home/public'; +import { IndexPatternManagementSetup } from 'src/plugins/index_pattern_management/public'; import { EmbeddableSetup } from 'src/plugins/embeddable/public'; import { AppStatus, AppUpdater, DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; +import { MlCardState } from '../../../../src/plugins/index_pattern_management/public'; import { SecurityPluginSetup } from '../../security/public'; import { LicensingPluginSetup } from '../../licensing/public'; import { registerManagementSection } from './application/management'; @@ -53,6 +55,7 @@ export interface MlSetupDependencies { uiActions: UiActionsSetup; kibanaVersion: string; share: SharePluginSetup; + indexPatternManagement: IndexPatternManagementSetup; } export type MlCoreSetup = CoreSetup; @@ -104,11 +107,20 @@ export class MlPlugin implements Plugin { }); const licensing = pluginsSetup.licensing.license$.pipe(take(1)); - licensing.subscribe((license) => { + licensing.subscribe(async (license) => { + const [coreStart] = await core.getStartServices(); if (isMlEnabled(license)) { // add ML to home page registerFeature(pluginsSetup.home); + // register ML for the index pattern management no data screen. + pluginsSetup.indexPatternManagement.environment.update({ + ml: () => + coreStart.application.capabilities.ml.canFindFileStructure + ? MlCardState.ENABLED + : MlCardState.HIDDEN, + }); + // register various ML plugin features which require a full license if (isFullLicense(license)) { registerManagementSection(pluginsSetup.management, core); diff --git a/x-pack/plugins/ml/public/shared.ts b/x-pack/plugins/ml/public/shared.ts index 4b1d7ee733dcfd..ec884bfac5351c 100644 --- a/x-pack/plugins/ml/public/shared.ts +++ b/x-pack/plugins/ml/public/shared.ts @@ -9,6 +9,7 @@ export * from '../common/constants/anomalies'; export * from '../common/types/data_recognizer'; export * from '../common/types/capabilities'; export * from '../common/types/anomalies'; +export * from '../common/types/anomaly_detection_jobs'; export * from '../common/types/modules'; export * from '../common/types/audit_message'; diff --git a/x-pack/plugins/observability/public/components/app/ingest_manager_panel/index.tsx b/x-pack/plugins/observability/public/components/app/ingest_manager_panel/index.tsx index 1ab9f75632d9dc..5d0c8a40ed3de9 100644 --- a/x-pack/plugins/observability/public/components/app/ingest_manager_panel/index.tsx +++ b/x-pack/plugins/observability/public/components/app/ingest_manager_panel/index.tsx @@ -11,13 +11,16 @@ import { EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EuiText } from '@elastic/eui'; import { EuiLink } from '@elastic/eui'; +import { usePluginContext } from '../../../hooks/use_plugin_context'; export function IngestManagerPanel() { + const { core } = usePluginContext(); + return ( @@ -25,7 +28,7 @@ export function IngestManagerPanel() {

- {i18n.translate('xpack.observability.ingestManafer.title', { + {i18n.translate('xpack.observability.ingestManager.title', { defaultMessage: 'Have you seen our new Ingest Manager?', })}

@@ -33,15 +36,15 @@ export function IngestManagerPanel() {
- {i18n.translate('xpack.observability.ingestManafer.text', { + {i18n.translate('xpack.observability.ingestManager.text', { defaultMessage: 'The Elastic Agent provides a simple, unified way to add monitoring for logs, metrics, and other types of data to your hosts. You no longer need to install multiple Beats and other agents, making it easier and faster to deploy configurations across your infrastructure.', })} - - {i18n.translate('xpack.observability.ingestManafer.button', { + + {i18n.translate('xpack.observability.ingestManager.button', { defaultMessage: 'Try Ingest Manager Beta', })} diff --git a/x-pack/plugins/reporting/server/routes/jobs.ts b/x-pack/plugins/reporting/server/routes/jobs.ts index e8eac9e577beb5..db62c0cc403fc0 100644 --- a/x-pack/plugins/reporting/server/routes/jobs.ts +++ b/x-pack/plugins/reporting/server/routes/jobs.ts @@ -15,11 +15,6 @@ import { downloadJobResponseHandlerFactory, } from './lib/job_response_handler'; -interface ListQuery { - page: string; - size: string; - ids?: string; // optional field forbids us from extending RequestQuery -} const MAIN_ENTRY = `${API_BASE_URL}/jobs`; const handleUnavailable = (res: any) => { @@ -52,11 +47,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { const { management: { jobTypes = [] }, } = await reporting.getLicenseInfo(); - const { - page: queryPage = '0', - size: querySize = '10', - ids: queryIds = null, - } = req.query as ListQuery; // NOTE: type inference is not working here. userHandler breaks it? + const { page: queryPage = '0', size: querySize = '10', ids: queryIds = null } = req.query; const page = parseInt(queryPage, 10) || 0; const size = Math.min(100, parseInt(querySize, 10) || 10); const jobIds = queryIds ? queryIds.split(',') : null; @@ -116,7 +107,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { return handleUnavailable(res); } - const { docId } = req.params as { docId: string }; + const { docId } = req.params; const { management: { jobTypes = [] }, } = await reporting.getLicenseInfo(); @@ -161,7 +152,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { return res.custom({ statusCode: 503 }); } - const { docId } = req.params as { docId: string }; + const { docId } = req.params; const { management: { jobTypes = [] }, } = await reporting.getLicenseInfo(); @@ -213,7 +204,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { return handleUnavailable(res); } - const { docId } = req.params as { docId: string }; + const { docId } = req.params; const { management: { jobTypes = [] }, } = await reporting.getLicenseInfo(); @@ -239,7 +230,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { return handleUnavailable(res); } - const { docId } = req.params as { docId: string }; + const { docId } = req.params; const { management: { jobTypes = [] }, } = await reporting.getLicenseInfo(); diff --git a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts index 3758eafc6d718c..e2f393aad57d23 100644 --- a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts +++ b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts @@ -12,7 +12,7 @@ import { getUserFactory } from './get_user'; type ReportingUser = AuthenticatedUser | null; const superuserRole = 'superuser'; -export type RequestHandlerUser = RequestHandler extends (...a: infer U) => infer R +export type RequestHandlerUser = RequestHandler extends (...a: infer U) => infer R ? (user: ReportingUser, ...a: U) => R : never; @@ -21,7 +21,7 @@ export const authorizedUserPreRoutingFactory = function authorizedUserPreRouting ) { const setupDeps = reporting.getPluginSetupDeps(); const getUser = getUserFactory(setupDeps.security); - return (handler: RequestHandlerUser): RequestHandler => { + return (handler: RequestHandlerUser): RequestHandler => { return (context, req, res) => { let user: ReportingUser = null; if (setupDeps.security && setupDeps.security.license.isEnabled()) { diff --git a/x-pack/plugins/security_solution/common/machine_learning/has_ml_license.test.ts b/x-pack/plugins/security_solution/common/machine_learning/has_ml_license.test.ts new file mode 100644 index 00000000000000..1ffc2e16b78f75 --- /dev/null +++ b/x-pack/plugins/security_solution/common/machine_learning/has_ml_license.test.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { emptyMlCapabilities } from './empty_ml_capabilities'; +import { hasMlLicense } from './has_ml_license'; + +describe('hasMlLicense', () => { + test('it returns false when license is not platinum or trial', () => { + const capabilities = { ...emptyMlCapabilities, isPlatinumOrTrialLicense: false }; + expect(hasMlLicense(capabilities)).toEqual(false); + }); + + test('it returns true when license is platinum or trial', () => { + const capabilities = { ...emptyMlCapabilities, isPlatinumOrTrialLicense: true }; + expect(hasMlLicense(capabilities)).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/common/machine_learning/has_ml_license.ts b/x-pack/plugins/security_solution/common/machine_learning/has_ml_license.ts new file mode 100644 index 00000000000000..c0b6862ac30fe5 --- /dev/null +++ b/x-pack/plugins/security_solution/common/machine_learning/has_ml_license.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MlCapabilitiesResponse } from '../../../ml/common/types/capabilities'; + +export const hasMlLicense = (capabilities: MlCapabilitiesResponse): boolean => + capabilities.isPlatinumOrTrialLicense; diff --git a/x-pack/plugins/security_solution/common/machine_learning/is_security_job.ts b/x-pack/plugins/security_solution/common/machine_learning/is_security_job.ts index 43cfa4ad599640..f5783fc9b3973a 100644 --- a/x-pack/plugins/security_solution/common/machine_learning/is_security_job.ts +++ b/x-pack/plugins/security_solution/common/machine_learning/is_security_job.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MlSummaryJob } from '../../../ml/common/types/anomaly_detection_jobs'; import { ML_GROUP_IDS } from '../constants'; -export const isSecurityJob = (job: MlSummaryJob): boolean => +export const isSecurityJob = (job: { groups: string[] }): boolean => job.groups.some((group) => ML_GROUP_IDS.includes(group)); diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.test.tsx index 6fb693e47560d6..56daa9a8364f6b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.test.tsx @@ -80,6 +80,9 @@ describe('Configuration button', () => { }); test('it shows the tooltip when hovering the button', () => { + // Use fake timers so we don't have to wait for the EuiToolTip timeout + jest.useFakeTimers(); + const msgTooltip = 'My message tooltip'; const titleTooltip = 'My title'; @@ -96,11 +99,14 @@ describe('Configuration button', () => { ); newWrapper.find('[data-test-subj="configure-case-button"]').first().simulate('mouseOver'); - // EuiToolTip mounts children after a 250ms delay - setTimeout( - () => - expect(newWrapper.find('.euiToolTipPopover').text()).toBe(`${titleTooltip}${msgTooltip}`), - 250 - ); + + // Run the timers so the EuiTooltip will be visible + jest.runAllTimers(); + + newWrapper.update(); + expect(newWrapper.find('.euiToolTipPopover').text()).toBe(`${titleTooltip}${msgTooltip}`); + + // Clearing all mocks will also reset fake timers. + jest.clearAllMocks(); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts index 6fbb308672e5d7..e6597de892bff3 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts @@ -9,13 +9,11 @@ import { useState, useEffect, useMemo } from 'react'; import { DEFAULT_ANOMALY_SCORE } from '../../../../../common/constants'; import { anomaliesTableData } from '../api/anomalies_table_data'; import { InfluencerInput, Anomalies, CriteriaFields } from '../types'; -import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; -import { useSiemJobs } from '../../ml_popover/hooks/use_siem_jobs'; -import { useMlCapabilities } from '../../ml_popover/hooks/use_ml_capabilities'; -import { useStateToaster, errorToToaster } from '../../toasters'; import * as i18n from './translations'; import { useTimeZone, useUiSetting$ } from '../../../lib/kibana'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; +import { useInstalledSecurityJobs } from '../hooks/use_installed_security_jobs'; interface Args { influencers?: InfluencerInput[]; @@ -58,15 +56,13 @@ export const useAnomaliesTableData = ({ skip = false, }: Args): Return => { const [tableData, setTableData] = useState(null); - const [, siemJobs] = useSiemJobs(true); + const { isMlUser, jobs } = useInstalledSecurityJobs(); const [loading, setLoading] = useState(true); - const capabilities = useMlCapabilities(); - const userPermissions = hasMlUserPermissions(capabilities); - const [, dispatchToaster] = useStateToaster(); + const { addError } = useAppToasts(); const timeZone = useTimeZone(); const [anomalyScore] = useUiSetting$(DEFAULT_ANOMALY_SCORE); - const siemJobIds = siemJobs.filter((job) => job.isInstalled).map((job) => job.id); + const jobIds = jobs.map((job) => job.id); const startDateMs = useMemo(() => new Date(startDate).getTime(), [startDate]); const endDateMs = useMemo(() => new Date(endDate).getTime(), [endDate]); @@ -81,11 +77,11 @@ export const useAnomaliesTableData = ({ earliestMs: number, latestMs: number ) { - if (userPermissions && !skip && siemJobIds.length > 0) { + if (isMlUser && !skip && jobIds.length > 0) { try { const data = await anomaliesTableData( { - jobIds: siemJobIds, + jobIds, criteriaFields: criteriaFieldsInput, aggregationInterval: 'auto', threshold: getThreshold(anomalyScore, threshold), @@ -104,13 +100,13 @@ export const useAnomaliesTableData = ({ } } catch (error) { if (isSubscribed) { - errorToToaster({ title: i18n.SIEM_TABLE_FETCH_FAILURE, error, dispatchToaster }); + addError(error, { title: i18n.SIEM_TABLE_FETCH_FAILURE }); setLoading(false); } } - } else if (!userPermissions && isSubscribed) { + } else if (!isMlUser && isSubscribed) { setLoading(false); - } else if (siemJobIds.length === 0 && isSubscribed) { + } else if (jobIds.length === 0 && isSubscribed) { setLoading(false); } else if (isSubscribed) { setTableData(null); @@ -132,9 +128,9 @@ export const useAnomaliesTableData = ({ startDateMs, endDateMs, skip, - userPermissions, + isMlUser, // eslint-disable-next-line react-hooks/exhaustive-deps - siemJobIds.sort().join(), + jobIds.sort().join(), ]); return [loading, tableData]; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/api/get_jobs_summary.ts b/x-pack/plugins/security_solution/public/common/components/ml/api/get_jobs_summary.ts new file mode 100644 index 00000000000000..15f823814d7fc8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/api/get_jobs_summary.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpSetup } from '../../../../../../../../src/core/public'; +import { MlSummaryJob } from '../../../../../../ml/public'; + +export interface GetJobsSummaryArgs { + http: HttpSetup; + jobIds?: string[]; + signal: AbortSignal; +} + +/** + * Fetches a summary of all ML jobs currently installed + * + * @param http HTTP Service + * @param jobIds Array of job IDs to filter against + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const getJobsSummary = async ({ + http, + jobIds, + signal, +}: GetJobsSummaryArgs): Promise => + http.fetch('/api/ml/jobs/jobs_summary', { + method: 'POST', + body: JSON.stringify({ jobIds: jobIds ?? [] }), + asSystemRequest: true, + signal, + }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/api/get_ml_capabilities.ts b/x-pack/plugins/security_solution/public/common/components/ml/api/get_ml_capabilities.ts index 32f6f888ab8d71..8ee765c1dea476 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/api/get_ml_capabilities.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/api/get_ml_capabilities.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { HttpSetup } from '../../../../../../../../src/core/public'; import { MlCapabilitiesResponse } from '../../../../../../ml/public'; -import { KibanaServices } from '../../../lib/kibana'; import { InfluencerInput } from '../types'; export interface Body { @@ -21,10 +21,15 @@ export interface Body { maxExamples: number; } -export const getMlCapabilities = async (signal: AbortSignal): Promise => { - return KibanaServices.get().http.fetch('/api/ml/ml_capabilities', { +export const getMlCapabilities = async ({ + http, + signal, +}: { + http: HttpSetup; + signal: AbortSignal; +}): Promise => + http.fetch('/api/ml/ml_capabilities', { method: 'GET', asSystemRequest: true, signal, }); -}; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_jobs_summary.ts b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_jobs_summary.ts new file mode 100644 index 00000000000000..a80bfb59649cb4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_jobs_summary.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useAsync, withOptionalSignal } from '../../../../shared_imports'; +import { getJobsSummary } from '../api/get_jobs_summary'; + +const _getJobsSummary = withOptionalSignal(getJobsSummary); + +export const useGetJobsSummary = () => useAsync(_getJobsSummary); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_ml_capabilities.ts b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_ml_capabilities.ts new file mode 100644 index 00000000000000..aabd8c7b62e85f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_get_ml_capabilities.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getMlCapabilities } from '../api/get_ml_capabilities'; +import { useAsync, withOptionalSignal } from '../../../../shared_imports'; + +const _getMlCapabilities = withOptionalSignal(getMlCapabilities); + +export const useGetMlCapabilities = () => useAsync(_getMlCapabilities); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.test.ts new file mode 100644 index 00000000000000..72690a17739266 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.test.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook } from '@testing-library/react-hooks'; + +import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; +import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license'; +import { isSecurityJob } from '../../../../../common/machine_learning/is_security_job'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; +import { useAppToastsMock } from '../../../hooks/use_app_toasts.mock'; +import { mockJobsSummaryResponse } from '../../ml_popover/api.mock'; +import { getJobsSummary } from '../api/get_jobs_summary'; +import { useInstalledSecurityJobs } from './use_installed_security_jobs'; + +jest.mock('../../../../../common/machine_learning/has_ml_user_permissions'); +jest.mock('../../../../../common/machine_learning/has_ml_license'); +jest.mock('../../../hooks/use_app_toasts'); +jest.mock('../api/get_jobs_summary'); + +describe('useInstalledSecurityJobs', () => { + let appToastsMock: jest.Mocked>; + + beforeEach(() => { + appToastsMock = useAppToastsMock.create(); + (useAppToasts as jest.Mock).mockReturnValue(appToastsMock); + (getJobsSummary as jest.Mock).mockResolvedValue(mockJobsSummaryResponse); + }); + + describe('when the user has permissions', () => { + beforeEach(() => { + (hasMlUserPermissions as jest.Mock).mockReturnValue(true); + (hasMlLicense as jest.Mock).mockReturnValue(true); + }); + + it('returns jobs and permissions', async () => { + const { result, waitForNextUpdate } = renderHook(() => useInstalledSecurityJobs()); + await waitForNextUpdate(); + + expect(result.current.jobs).toHaveLength(3); + expect(result.current.jobs).toEqual( + expect.arrayContaining([ + { + datafeedId: 'datafeed-siem-api-rare_process_linux_ecs', + datafeedIndices: ['auditbeat-*'], + datafeedState: 'stopped', + description: 'SIEM Auditbeat: Detect unusually rare processes on Linux (beta)', + earliestTimestampMs: 1557353420495, + groups: ['siem'], + hasDatafeed: true, + id: 'siem-api-rare_process_linux_ecs', + isSingleMetricViewerJob: true, + jobState: 'closed', + latestTimestampMs: 1557434782207, + memory_status: 'hard_limit', + processed_record_count: 582251, + }, + ]) + ); + expect(result.current.isMlUser).toEqual(true); + expect(result.current.isLicensed).toEqual(true); + }); + + it('filters out non-security jobs', async () => { + const { result, waitForNextUpdate } = renderHook(() => useInstalledSecurityJobs()); + await waitForNextUpdate(); + + expect(result.current.jobs.length).toBeGreaterThan(0); + expect(result.current.jobs.every(isSecurityJob)).toEqual(true); + }); + + it('renders a toast error if the ML call fails', async () => { + (getJobsSummary as jest.Mock).mockRejectedValue('whoops'); + const { waitForNextUpdate } = renderHook(() => useInstalledSecurityJobs()); + await waitForNextUpdate(); + + expect(appToastsMock.addError).toHaveBeenCalledWith('whoops', { + title: 'Security job fetch failure', + }); + }); + }); + + describe('when the user does not have valid permissions', () => { + beforeEach(() => { + (hasMlUserPermissions as jest.Mock).mockReturnValue(false); + (hasMlLicense as jest.Mock).mockReturnValue(false); + }); + + it('returns empty jobs and false predicates', () => { + const { result } = renderHook(() => useInstalledSecurityJobs()); + + expect(result.current.jobs).toEqual([]); + expect(result.current.isMlUser).toEqual(false); + expect(result.current.isLicensed).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts new file mode 100644 index 00000000000000..a9a728f81cc6cd --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_installed_security_jobs.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState } from 'react'; + +import { MlSummaryJob } from '../../../../../../ml/public'; +import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; +import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license'; +import { isSecurityJob } from '../../../../../common/machine_learning/is_security_job'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; +import { useHttp } from '../../../lib/kibana'; +import { useMlCapabilities } from './use_ml_capabilities'; +import * as i18n from '../translations'; +import { useGetJobsSummary } from './use_get_jobs_summary'; + +export interface UseInstalledSecurityJobsReturn { + loading: boolean; + jobs: MlSummaryJob[]; + isMlUser: boolean; + isLicensed: boolean; +} + +/** + * Returns a collection of installed ML jobs (MlSummaryJob) relevant to + * Security Solution, i.e. all installed jobs in the `security` ML group. + * Use the corresponding helper functions to filter the job list as + * necessary (running jobs, etc). + * + */ +export const useInstalledSecurityJobs = (): UseInstalledSecurityJobsReturn => { + const [jobs, setJobs] = useState([]); + const { addError } = useAppToasts(); + const mlCapabilities = useMlCapabilities(); + const http = useHttp(); + const { error, loading, result, start } = useGetJobsSummary(); + + const isMlUser = hasMlUserPermissions(mlCapabilities); + const isLicensed = hasMlLicense(mlCapabilities); + + useEffect(() => { + if (isMlUser && isLicensed) { + start({ http }); + } + }, [http, isMlUser, isLicensed, start]); + + useEffect(() => { + if (result) { + const securityJobs = result.filter(isSecurityJob); + setJobs(securityJobs); + } + }, [result]); + + useEffect(() => { + if (error) { + addError(error, { title: i18n.SIEM_JOB_FETCH_FAILURE }); + } + }, [addError, error]); + + return { isLicensed, isMlUser, jobs, loading }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_ml_capabilities.tsx b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_ml_capabilities.ts similarity index 80% rename from x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_ml_capabilities.tsx rename to x-pack/plugins/security_solution/public/common/components/ml/hooks/use_ml_capabilities.ts index d897b2554b4fdd..4f804a355e4b55 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_ml_capabilities.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/hooks/use_ml_capabilities.ts @@ -6,6 +6,6 @@ import { useContext } from 'react'; -import { MlCapabilitiesContext } from '../../ml/permissions/ml_capabilities_provider'; +import { MlCapabilitiesContext } from '../permissions/ml_capabilities_provider'; export const useMlCapabilities = () => useContext(MlCapabilitiesContext); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/permissions/ml_capabilities_provider.tsx b/x-pack/plugins/security_solution/public/common/components/ml/permissions/ml_capabilities_provider.tsx index c83271a56be5a8..c12c8d78da714e 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/permissions/ml_capabilities_provider.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/permissions/ml_capabilities_provider.tsx @@ -8,9 +8,9 @@ import React, { useState, useEffect } from 'react'; import { MlCapabilitiesResponse } from '../../../../../../ml/public'; import { emptyMlCapabilities } from '../../../../../common/machine_learning/empty_ml_capabilities'; -import { getMlCapabilities } from '../api/get_ml_capabilities'; -import { errorToToaster, useStateToaster } from '../../toasters'; - +import { useAppToasts } from '../../../hooks/use_app_toasts'; +import { useHttp } from '../../../lib/kibana'; +import { useGetMlCapabilities } from '../hooks/use_get_ml_capabilities'; import * as i18n from './translations'; interface MlCapabilitiesProvider extends MlCapabilitiesResponse { @@ -32,36 +32,27 @@ export const MlCapabilitiesProvider = React.memo<{ children: JSX.Element }>(({ c const [capabilities, setCapabilities] = useState( emptyMlCapabilitiesProvider ); - const [, dispatchToaster] = useStateToaster(); + const http = useHttp(); + const { addError } = useAppToasts(); + const { start, result, error } = useGetMlCapabilities(); useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); + start({ http }); + }, [http, start]); - async function fetchMlCapabilities() { - try { - const mlCapabilities = await getMlCapabilities(abortCtrl.signal); - if (isSubscribed) { - setCapabilities({ ...mlCapabilities, capabilitiesFetched: true }); - } - } catch (error) { - if (isSubscribed) { - errorToToaster({ - title: i18n.MACHINE_LEARNING_PERMISSIONS_FAILURE, - error, - dispatchToaster, - }); - } - } + useEffect(() => { + if (result) { + setCapabilities({ ...result, capabilitiesFetched: true }); } + }, [result]); - fetchMlCapabilities(); - return () => { - isSubscribed = false; - abortCtrl.abort(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + useEffect(() => { + if (error) { + addError(error, { + title: i18n.MACHINE_LEARNING_PERMISSIONS_FAILURE, + }); + } + }, [addError, error]); return ( {children} diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx index 9bfae686b1a594..7fdf41e6b6500d 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx @@ -16,7 +16,7 @@ import { convertAnomaliesToHosts } from './convert_anomalies_to_hosts'; import { Loader } from '../../loader'; import { getIntervalFromAnomalies } from '../anomaly/get_interval_from_anomalies'; import { AnomaliesHostTableProps } from '../types'; -import { useMlCapabilities } from '../../ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../hooks/use_ml_capabilities'; import { BasicTable } from './basic_table'; import { hostEquality } from './host_equality'; import { getCriteriaFromHostType } from '../criteria/get_criteria_from_host_type'; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx index af27d411b990d9..124d8d9a794c1a 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx @@ -14,7 +14,7 @@ import { convertAnomaliesToNetwork } from './convert_anomalies_to_network'; import { Loader } from '../../loader'; import { AnomaliesNetworkTableProps } from '../types'; import { getAnomaliesNetworkTableColumnsCurated } from './get_anomalies_network_table_columns'; -import { useMlCapabilities } from '../../ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../hooks/use_ml_capabilities'; import { BasicTable } from './basic_table'; import { networkEquality } from './network_equality'; import { getCriteriaFromNetworkType } from '../criteria/get_criteria_from_network_type'; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/translations.ts b/x-pack/plugins/security_solution/public/common/components/ml/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/translations.ts rename to x-pack/plugins/security_solution/public/common/components/ml/translations.ts diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/__mocks__/api.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/api.mock.ts similarity index 99% rename from x-pack/plugins/security_solution/public/common/components/ml_popover/__mocks__/api.tsx rename to x-pack/plugins/security_solution/public/common/components/ml_popover/api.mock.ts index 54bb0a96207e14..0e8f033ff0cf35 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/__mocks__/api.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/api.mock.ts @@ -4,16 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ +import { MlSummaryJob } from '../../../../../ml/public'; import { Group, - JobSummary, Module, RecognizerModule, SetupMlResponse, - SiemJob, + SecurityJob, StartDatafeedResponse, StopDatafeedResponse, -} from '../types'; +} from './types'; export const mockGroupsResponse: Group[] = [ { @@ -31,7 +31,7 @@ export const mockGroupsResponse: Group[] = [ { id: 'suricata', jobIds: ['suricata_alert_rate'], calendarIds: [] }, ]; -export const mockOpenedJob: JobSummary = { +export const mockOpenedJob: MlSummaryJob = { datafeedId: 'datafeed-siem-api-rare_process_linux_ecs', datafeedIndices: ['auditbeat-*'], datafeedState: 'started', @@ -48,7 +48,7 @@ export const mockOpenedJob: JobSummary = { processed_record_count: 3425264, }; -export const mockJobsSummaryResponse: JobSummary[] = [ +export const mockJobsSummaryResponse: MlSummaryJob[] = [ { id: 'rc-rare-process-windows-5', description: @@ -491,7 +491,7 @@ export const mockStopDatafeedsSuccess: StopDatafeedResponse = { 'datafeed-linux_anomalous_network_service': { stopped: true }, }; -export const mockSiemJobs: SiemJob[] = [ +export const mockSecurityJobs: SecurityJob[] = [ { id: 'linux_anomalous_network_activity_ecs', description: diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/api.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/api.ts similarity index 89% rename from x-pack/plugins/security_solution/public/common/components/ml_popover/api.tsx rename to x-pack/plugins/security_solution/public/common/components/ml_popover/api.ts index 7c72098209a066..dd0fb33fd2bc67 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/api.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/api.ts @@ -9,7 +9,6 @@ import { CloseJobsResponse, ErrorResponse, GetModulesProps, - JobSummary, MlSetupArgs, Module, RecognizerModule, @@ -165,21 +164,3 @@ export const stopDatafeeds = async ({ return [stopDatafeedsResponse, closeJobsResponse]; }; - -/** - * Fetches a summary of all ML jobs currently installed - * - * NOTE: If not sending jobIds in the body, you must at least send an empty body or the server will - * return a 500 - * - * @param signal to cancel request - * - * @throws An error if response is not OK - */ -export const getJobsSummary = async (signal: AbortSignal): Promise => - KibanaServices.get().http.fetch('/api/ml/jobs/jobs_summary', { - method: 'POST', - body: JSON.stringify({}), - asSystemRequest: true, - signal, - }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.test.tsx index 0b8da6be57e1b5..2a2db46d423077 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.test.tsx @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mockSiemJobs } from './__mocks__/api'; +import { mockSecurityJobs } from './api.mock'; import { filterJobs, getStablePatternTitles, searchFilter } from './helpers'; describe('helpers', () => { describe('filterJobs', () => { test('returns all jobs when no filter is suplied', () => { const filteredJobs = filterJobs({ - jobs: mockSiemJobs, + jobs: mockSecurityJobs, selectedGroups: [], showCustomJobs: false, showElasticJobs: false, @@ -23,17 +23,17 @@ describe('helpers', () => { describe('searchFilter', () => { test('returns all jobs when nullfilterQuery is provided', () => { - const jobsToDisplay = searchFilter(mockSiemJobs); - expect(jobsToDisplay.length).toEqual(mockSiemJobs.length); + const jobsToDisplay = searchFilter(mockSecurityJobs); + expect(jobsToDisplay.length).toEqual(mockSecurityJobs.length); }); test('returns correct DisplayJobs when filterQuery matches job.id', () => { - const jobsToDisplay = searchFilter(mockSiemJobs, 'rare_process'); + const jobsToDisplay = searchFilter(mockSecurityJobs, 'rare_process'); expect(jobsToDisplay.length).toEqual(2); }); test('returns correct DisplayJobs when filterQuery matches job.description', () => { - const jobsToDisplay = searchFilter(mockSiemJobs, 'Detect unusually'); + const jobsToDisplay = searchFilter(mockSecurityJobs, 'Detect unusually'); expect(jobsToDisplay.length).toEqual(2); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.tsx index 5989d052f7cd25..daf9da855c0f94 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/helpers.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SiemJob } from './types'; +import { SecurityJob } from './types'; /** * Returns a filtered array of Jobs according to JobsTableFilters selections @@ -22,12 +22,12 @@ export const filterJobs = ({ showElasticJobs, filterQuery, }: { - jobs: SiemJob[]; + jobs: SecurityJob[]; selectedGroups: string[]; showCustomJobs: boolean; showElasticJobs: boolean; filterQuery: string; -}): SiemJob[] => +}): SecurityJob[] => searchFilter( jobs .filter((job) => !showCustomJobs || (showCustomJobs && !job.isElasticJob)) @@ -44,7 +44,7 @@ export const filterJobs = ({ * @param jobs to filter * @param filterQuery user-provided search string to filter for occurrence in job names/description */ -export const searchFilter = (jobs: SiemJob[], filterQuery?: string): SiemJob[] => +export const searchFilter = (jobs: SecurityJob[], filterQuery?: string): SecurityJob[] => jobs.filter((job) => filterQuery == null ? true diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.test.ts b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.test.ts new file mode 100644 index 00000000000000..80f50912a84f28 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.test.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook } from '@testing-library/react-hooks'; + +import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions'; +import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; +import { useAppToastsMock } from '../../../hooks/use_app_toasts.mock'; +import { getJobsSummary } from '../../ml/api/get_jobs_summary'; +import { checkRecognizer, getModules } from '../api'; +import { SecurityJob } from '../types'; +import { + mockJobsSummaryResponse, + mockGetModuleResponse, + checkRecognizerSuccess, +} from '../api.mock'; +import { useSecurityJobs } from './use_security_jobs'; + +jest.mock('../../../../../common/machine_learning/has_ml_admin_permissions'); +jest.mock('../../../../../common/machine_learning/has_ml_license'); +jest.mock('../../../lib/kibana'); +jest.mock('../../../hooks/use_app_toasts'); +jest.mock('../../ml/hooks/use_ml_capabilities'); +jest.mock('../../ml/api/get_jobs_summary'); +jest.mock('../api'); + +describe('useSecurityJobs', () => { + let appToastsMock: jest.Mocked>; + + beforeEach(() => { + appToastsMock = useAppToastsMock.create(); + (useAppToasts as jest.Mock).mockReturnValue(appToastsMock); + }); + + describe('when user has valid permissions', () => { + beforeEach(() => { + (hasMlAdminPermissions as jest.Mock).mockReturnValue(true); + (hasMlLicense as jest.Mock).mockReturnValue(true); + (getJobsSummary as jest.Mock).mockResolvedValue(mockJobsSummaryResponse); + (getModules as jest.Mock).mockResolvedValue(mockGetModuleResponse); + (checkRecognizer as jest.Mock).mockResolvedValue(checkRecognizerSuccess); + }); + + it('combines multiple ML calls into an array of SecurityJobs', async () => { + const expectedSecurityJob: SecurityJob = { + datafeedId: 'datafeed-siem-api-rare_process_linux_ecs', + datafeedIndices: ['auditbeat-*'], + datafeedState: 'stopped', + defaultIndexPattern: '', + description: 'SIEM Auditbeat: Detect unusually rare processes on Linux (beta)', + earliestTimestampMs: 1557353420495, + groups: ['siem'], + hasDatafeed: true, + id: 'siem-api-rare_process_linux_ecs', + isCompatible: true, + isElasticJob: false, + isInstalled: true, + isSingleMetricViewerJob: true, + jobState: 'closed', + latestTimestampMs: 1557434782207, + memory_status: 'hard_limit', + moduleId: '', + processed_record_count: 582251, + }; + + const { result, waitForNextUpdate } = renderHook(() => useSecurityJobs(false)); + await waitForNextUpdate(); + + expect(result.current.jobs).toHaveLength(6); + expect(result.current.jobs).toEqual(expect.arrayContaining([expectedSecurityJob])); + }); + + it('returns those permissions', async () => { + const { result, waitForNextUpdate } = renderHook(() => useSecurityJobs(false)); + await waitForNextUpdate(); + + expect(result.current.isMlAdmin).toEqual(true); + expect(result.current.isLicensed).toEqual(true); + }); + + it('renders a toast error if an ML call fails', async () => { + (getModules as jest.Mock).mockRejectedValue('whoops'); + const { waitForNextUpdate } = renderHook(() => useSecurityJobs(false)); + await waitForNextUpdate(); + + expect(appToastsMock.addError).toHaveBeenCalledWith('whoops', { + title: 'Security job fetch failure', + }); + }); + }); + + describe('when the user does not have valid permissions', () => { + beforeEach(() => { + (hasMlAdminPermissions as jest.Mock).mockReturnValue(false); + (hasMlLicense as jest.Mock).mockReturnValue(false); + }); + + it('returns empty jobs and false predicates', () => { + const { result } = renderHook(() => useSecurityJobs(false)); + + expect(result.current.jobs).toEqual([]); + expect(result.current.isMlAdmin).toEqual(false); + expect(result.current.isLicensed).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.ts b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.ts new file mode 100644 index 00000000000000..e8809e8366eed4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState } from 'react'; + +import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; +import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions'; +import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license'; +import { useAppToasts } from '../../../hooks/use_app_toasts'; +import { useUiSetting$, useHttp } from '../../../lib/kibana'; +import { checkRecognizer, getModules } from '../api'; +import { SecurityJob } from '../types'; +import { createSecurityJobs } from './use_security_jobs_helpers'; +import { useMlCapabilities } from '../../ml/hooks/use_ml_capabilities'; +import * as i18n from '../../ml/translations'; +import { getJobsSummary } from '../../ml/api/get_jobs_summary'; + +export interface UseSecurityJobsReturn { + loading: boolean; + jobs: SecurityJob[]; + isMlAdmin: boolean; + isLicensed: boolean; +} + +/** + * Compiles a collection of SecurityJobs, which are a list of all jobs relevant to the Security Solution App. This + * includes all installed jobs in the `Security` ML group, and all jobs within ML Modules defined in + * ml_module (whether installed or not). Use the corresponding helper functions to filter the job + * list as necessary. E.g. installed jobs, running jobs, etc. + * + * NOTE: If the user is not an ml admin, jobs will be empty and isMlAdmin will be false. + * + * @param refetchData + */ +export const useSecurityJobs = (refetchData: boolean): UseSecurityJobsReturn => { + const [jobs, setJobs] = useState([]); + const [loading, setLoading] = useState(true); + const mlCapabilities = useMlCapabilities(); + const [siemDefaultIndex] = useUiSetting$(DEFAULT_INDEX_KEY); + const http = useHttp(); + const { addError } = useAppToasts(); + + const isMlAdmin = hasMlAdminPermissions(mlCapabilities); + const isLicensed = hasMlLicense(mlCapabilities); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + setLoading(true); + + async function fetchSecurityJobIdsFromGroupsData() { + if (isMlAdmin && isLicensed) { + try { + // Batch fetch all installed jobs, ML modules, and check which modules are compatible with siemDefaultIndex + const [jobSummaryData, modulesData, compatibleModules] = await Promise.all([ + getJobsSummary({ http, signal: abortCtrl.signal }), + getModules({ signal: abortCtrl.signal }), + checkRecognizer({ + indexPatternName: siemDefaultIndex, + signal: abortCtrl.signal, + }), + ]); + + const compositeSecurityJobs = createSecurityJobs( + jobSummaryData, + modulesData, + compatibleModules + ); + + if (isSubscribed) { + setJobs(compositeSecurityJobs); + } + } catch (error) { + if (isSubscribed) { + addError(error, { title: i18n.SIEM_JOB_FETCH_FAILURE }); + } + } + } + if (isSubscribed) { + setLoading(false); + } + } + + fetchSecurityJobIdsFromGroupsData(); + return () => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [refetchData, isMlAdmin, isLicensed, siemDefaultIndex, addError, http]); + + return { isLicensed, isMlAdmin, jobs, loading }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs_helpers.test.tsx similarity index 83% rename from x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.test.tsx rename to x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs_helpers.test.tsx index fc9f369a305aa7..7fb4e6f59d9f7f 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs_helpers.test.tsx @@ -6,29 +6,29 @@ import { composeModuleAndInstalledJobs, - createSiemJobs, + createSecurityJobs, getAugmentedFields, getInstalledJobs, getModuleJobs, - moduleToSiemJob, -} from './use_siem_jobs_helpers'; + moduleToSecurityJob, +} from './use_security_jobs_helpers'; import { checkRecognizerSuccess, mockGetModuleResponse, mockJobsSummaryResponse, -} from '../__mocks__/api'; +} from '../api.mock'; // TODO: Expand test coverage -describe('useSiemJobsHelpers', () => { - describe('moduleToSiemJob', () => { - test('correctly converts module to SiemJob', () => { - const siemJob = moduleToSiemJob( +describe('useSecurityJobsHelpers', () => { + describe('moduleToSecurityJob', () => { + test('correctly converts module to SecurityJob', () => { + const securityJob = moduleToSecurityJob( mockGetModuleResponse[0], mockGetModuleResponse[0].jobs[0], false ); - expect(siemJob).toEqual({ + expect(securityJob).toEqual({ datafeedId: '', datafeedIndices: [], datafeedState: '', @@ -86,19 +86,19 @@ describe('useSiemJobsHelpers', () => { const installedJobs = getInstalledJobs(mockJobsSummaryResponse, moduleJobs, [ 'siem_auditbeat', ]); - const siemJobs = composeModuleAndInstalledJobs(installedJobs, moduleJobs); - expect(siemJobs.length).toEqual(6); + const securityJobs = composeModuleAndInstalledJobs(installedJobs, moduleJobs); + expect(securityJobs.length).toEqual(6); }); }); - describe('createSiemJobs', () => { + describe('createSecurityJobs', () => { test('returns correct number of jobs when creating jobs with successful responses', () => { - const siemJobs = createSiemJobs( + const securityJobs = createSecurityJobs( mockJobsSummaryResponse, mockGetModuleResponse, checkRecognizerSuccess ); - expect(siemJobs.length).toEqual(6); + expect(securityJobs.length).toEqual(6); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs_helpers.tsx similarity index 59% rename from x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.tsx rename to x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs_helpers.tsx index adbd712ffeb3e0..d0109fd29b5fb8 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_security_jobs_helpers.tsx @@ -5,26 +5,26 @@ */ import { - AugmentedSiemJobFields, - JobSummary, + AugmentedSecurityJobFields, Module, ModuleJob, RecognizerModule, - SiemJob, + SecurityJob, } from '../types'; import { mlModules } from '../ml_modules'; +import { MlSummaryJob } from '../../../../../../ml/public'; /** - * Helper function for converting from ModuleJob -> SiemJob + * Helper function for converting from ModuleJob -> SecurityJob * @param module * @param moduleJob * @param isCompatible */ -export const moduleToSiemJob = ( +export const moduleToSecurityJob = ( module: Module, moduleJob: ModuleJob, isCompatible: boolean -): SiemJob => { +): SecurityJob => { return { datafeedId: '', datafeedIndices: [], @@ -46,7 +46,7 @@ export const moduleToSiemJob = ( }; /** - * Returns fields necessary to augment a ModuleJob to a SiemJob + * Returns fields necessary to augment a ModuleJob to a SecurityJob * * @param jobId * @param moduleJobs @@ -54,9 +54,9 @@ export const moduleToSiemJob = ( */ export const getAugmentedFields = ( jobId: string, - moduleJobs: SiemJob[], + moduleJobs: SecurityJob[], compatibleModuleIds: string[] -): AugmentedSiemJobFields => { +): AugmentedSecurityJobFields => { const moduleJob = moduleJobs.find((mj) => mj.id === jobId); return moduleJob !== undefined ? { @@ -74,24 +74,27 @@ export const getAugmentedFields = ( }; /** - * Process Modules[] from the `get_module` ML API into SiemJobs[] by filtering to SIEM specific + * Process Modules[] from the `get_module` ML API into SecurityJobs[] by filtering to Security specific * modules and unpacking jobs from each module * * @param modulesData * @param compatibleModuleIds */ -export const getModuleJobs = (modulesData: Module[], compatibleModuleIds: string[]): SiemJob[] => +export const getModuleJobs = ( + modulesData: Module[], + compatibleModuleIds: string[] +): SecurityJob[] => modulesData .filter((module) => mlModules.includes(module.id)) .map((module) => [ ...module.jobs.map((moduleJob) => - moduleToSiemJob(module, moduleJob, compatibleModuleIds.includes(module.id)) + moduleToSecurityJob(module, moduleJob, compatibleModuleIds.includes(module.id)) ), ]) .flat(); /** - * Process JobSummary[] from the `jobs_summary` ML API into SiemJobs[] by filtering to to SIEM jobs + * Process data from the `jobs_summary` ML API into SecurityJobs[] by filtering to Security jobs * and augmenting with moduleId/defaultIndexPattern/isCompatible * * @param jobSummaryData @@ -99,57 +102,57 @@ export const getModuleJobs = (modulesData: Module[], compatibleModuleIds: string * @param compatibleModuleIds */ export const getInstalledJobs = ( - jobSummaryData: JobSummary[], - moduleJobs: SiemJob[], + jobSummaryData: MlSummaryJob[], + moduleJobs: SecurityJob[], compatibleModuleIds: string[] -): SiemJob[] => +): SecurityJob[] => jobSummaryData .filter(({ groups }) => groups.includes('siem') || groups.includes('security')) - .map((jobSummary) => ({ + .map((jobSummary) => ({ ...jobSummary, ...getAugmentedFields(jobSummary.id, moduleJobs, compatibleModuleIds), isInstalled: true, })); /** - * Combines installed jobs + moduleSiemJobs that don't overlap and sorts by name asc + * Combines installed jobs + moduleSecurityJobs that don't overlap and sorts by name asc * * @param installedJobs - * @param moduleSiemJobs + * @param moduleSecurityJobs */ export const composeModuleAndInstalledJobs = ( - installedJobs: SiemJob[], - moduleSiemJobs: SiemJob[] -): SiemJob[] => { + installedJobs: SecurityJob[], + moduleSecurityJobs: SecurityJob[] +): SecurityJob[] => { const installedJobsIds = installedJobs.map((installedJob) => installedJob.id); return [ ...installedJobs, - ...moduleSiemJobs.filter((mj) => !installedJobsIds.includes(mj.id)), + ...moduleSecurityJobs.filter((mj) => !installedJobsIds.includes(mj.id)), ].sort((a, b) => a.id.localeCompare(b.id)); }; /** - * Creates a list of SiemJobs by composing JobSummary jobs (installed jobs) and Module - * jobs (pre-packaged SIEM jobs) into a single job object that can be used throughout the SIEM app + * Creates a list of SecurityJobs by composing jobs summaries (installed jobs) and Module + * jobs (pre-packaged Security jobs) into a single job object that can be used throughout the Security app * * @param jobSummaryData * @param modulesData * @param compatibleModules */ -export const createSiemJobs = ( - jobSummaryData: JobSummary[], +export const createSecurityJobs = ( + jobSummaryData: MlSummaryJob[], modulesData: Module[], compatibleModules: RecognizerModule[] -): SiemJob[] => { +): SecurityJob[] => { // Create lookup of compatible modules const compatibleModuleIds = compatibleModules.map((module) => module.id); - // Process modulesData: Filter to SIEM specific modules, and unpack jobs from modules - const moduleSiemJobs = getModuleJobs(modulesData, compatibleModuleIds); + // Process modulesData: Filter to Security specific modules, and unpack jobs from modules + const moduleSecurityJobs = getModuleJobs(modulesData, compatibleModuleIds); - // Process jobSummaryData: Filter to SIEM jobs, and augment with moduleId/defaultIndexPattern/isCompatible - const installedJobs = getInstalledJobs(jobSummaryData, moduleSiemJobs, compatibleModuleIds); + // Process jobSummaryData: Filter to Security jobs, and augment with moduleId/defaultIndexPattern/isCompatible + const installedJobs = getInstalledJobs(jobSummaryData, moduleSecurityJobs, compatibleModuleIds); - // Combine installed jobs + moduleSiemJobs that don't overlap, and sort by name asc - return composeModuleAndInstalledJobs(installedJobs, moduleSiemJobs); + // Combine installed jobs + moduleSecurityJobs that don't overlap, and sort by name asc + return composeModuleAndInstalledJobs(installedJobs, moduleSecurityJobs); }; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs.tsx deleted file mode 100644 index 7f0a8dea1913e4..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/hooks/use_siem_jobs.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { useEffect, useState } from 'react'; - -import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; -import { checkRecognizer, getJobsSummary, getModules } from '../api'; -import { SiemJob } from '../types'; -import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; -import { errorToToaster, useStateToaster } from '../../toasters'; -import { useUiSetting$ } from '../../../lib/kibana'; - -import * as i18n from './translations'; -import { createSiemJobs } from './use_siem_jobs_helpers'; -import { useMlCapabilities } from './use_ml_capabilities'; - -type Return = [boolean, SiemJob[]]; - -/** - * Compiles a collection of SiemJobs, which are a list of all jobs relevant to the SIEM App. This - * includes all installed jobs in the `SIEM` ML group, and all jobs within ML Modules defined in - * ml_module (whether installed or not). Use the corresponding helper functions to filter the job - * list as necessary. E.g. installed jobs, running jobs, etc. - * - * @param refetchData - */ -export const useSiemJobs = (refetchData: boolean): Return => { - const [siemJobs, setSiemJobs] = useState([]); - const [loading, setLoading] = useState(true); - const mlCapabilities = useMlCapabilities(); - const userPermissions = hasMlUserPermissions(mlCapabilities); - const [siemDefaultIndex] = useUiSetting$(DEFAULT_INDEX_KEY); - const [, dispatchToaster] = useStateToaster(); - - useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); - setLoading(true); - - async function fetchSiemJobIdsFromGroupsData() { - if (userPermissions) { - try { - // Batch fetch all installed jobs, ML modules, and check which modules are compatible with siemDefaultIndex - const [jobSummaryData, modulesData, compatibleModules] = await Promise.all([ - getJobsSummary(abortCtrl.signal), - getModules({ signal: abortCtrl.signal }), - checkRecognizer({ - indexPatternName: siemDefaultIndex, - signal: abortCtrl.signal, - }), - ]); - - const compositeSiemJobs = createSiemJobs(jobSummaryData, modulesData, compatibleModules); - - if (isSubscribed) { - setSiemJobs(compositeSiemJobs); - } - } catch (error) { - if (isSubscribed) { - errorToToaster({ title: i18n.SIEM_JOB_FETCH_FAILURE, error, dispatchToaster }); - } - } - } - if (isSubscribed) { - setLoading(false); - } - } - - fetchSiemJobIdsFromGroupsData(); - return () => { - isSubscribed = false; - abortCtrl.abort(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [refetchData, userPermissions]); - - return [loading, siemJobs]; -}; diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap index 747ac63551b55b..9bee321e9fbde4 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap @@ -25,7 +25,7 @@ exports[`JobsTableFilters renders correctly against snapshot 1`] = ` { - let siemJobs: SiemJob[]; + let securityJobs: SecurityJob[]; beforeEach(() => { - siemJobs = cloneDeep(mockSiemJobs); + securityJobs = cloneDeep(mockSecurityJobs); }); test('renders correctly against snapshot', () => { const wrapper = shallow( - + ); expect(wrapper).toMatchSnapshot(); }); @@ -29,7 +32,7 @@ describe('GroupsFilterPopover', () => { const mockOnSelectedGroupsChanged = jest.fn(); const wrapper = mount( ); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx index d879942b8b1014..362fb94dc1ec4f 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx @@ -15,30 +15,30 @@ import { EuiSpacer, } from '@elastic/eui'; import * as i18n from './translations'; -import { SiemJob } from '../../types'; +import { SecurityJob } from '../../types'; import { toggleSelectedGroup } from './toggle_selected_group'; interface GroupsFilterPopoverProps { - siemJobs: SiemJob[]; + securityJobs: SecurityJob[]; onSelectedGroupsChanged: Dispatch>; } /** - * Popover for selecting which SiemJob groups to filter on. Component extracts unique groups and - * their counts from the provided SiemJobs. The 'siem' & 'security' groups are filtered out as all jobs will be + * Popover for selecting which SecurityJob groups to filter on. Component extracts unique groups and + * their counts from the provided SecurityJobs. The 'siem' & 'security' groups are filtered out as all jobs will be * siem/security jobs * - * @param siemJobs jobs to fetch groups from to display for filtering + * @param securityJobs jobs to fetch groups from to display for filtering * @param onSelectedGroupsChanged change listener to be notified when group selection changes */ export const GroupsFilterPopoverComponent = ({ - siemJobs, + securityJobs, onSelectedGroupsChanged, }: GroupsFilterPopoverProps) => { const [isGroupPopoverOpen, setIsGroupPopoverOpen] = useState(false); const [selectedGroups, setSelectedGroups] = useState([]); - const groups = siemJobs + const groups = securityJobs .map((j) => j.groups) .flat() .filter((g) => g !== 'siem' && g !== 'security'); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx index 5b656adc3e5817..6b7699d57aedf9 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx @@ -7,20 +7,20 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; import { JobsTableFiltersComponent } from './jobs_table_filters'; -import { SiemJob } from '../../types'; +import { SecurityJob } from '../../types'; import { cloneDeep } from 'lodash/fp'; -import { mockSiemJobs } from '../../__mocks__/api'; +import { mockSecurityJobs } from '../../api.mock'; describe('JobsTableFilters', () => { - let siemJobs: SiemJob[]; + let securityJobs: SecurityJob[]; beforeEach(() => { - siemJobs = cloneDeep(mockSiemJobs); + securityJobs = cloneDeep(mockSecurityJobs); }); test('renders correctly against snapshot', () => { const wrapper = shallow( - + ); expect(wrapper).toMatchSnapshot(); }); @@ -28,7 +28,7 @@ describe('JobsTableFilters', () => { test('when you click Elastic Jobs filter, state is updated and it is selected', () => { const onFilterChanged = jest.fn(); const wrapper = mount( - + ); wrapper.find('[data-test-subj="show-elastic-jobs-filter-button"]').first().simulate('click'); @@ -45,7 +45,7 @@ describe('JobsTableFilters', () => { test('when you click Custom Jobs filter, state is updated and it is selected', () => { const onFilterChanged = jest.fn(); const wrapper = mount( - + ); wrapper.find('[data-test-subj="show-custom-jobs-filter-button"]').first().simulate('click'); @@ -62,7 +62,7 @@ describe('JobsTableFilters', () => { test('when you click Custom Jobs filter once, then Elastic Jobs filter, state is updated and selected changed', () => { const onFilterChanged = jest.fn(); const wrapper = mount( - + ); wrapper.find('[data-test-subj="show-custom-jobs-filter-button"]').first().simulate('click'); @@ -88,7 +88,7 @@ describe('JobsTableFilters', () => { test('when you click Custom Jobs filter twice, state is updated and it is revert', () => { const onFilterChanged = jest.fn(); const wrapper = mount( - + ); wrapper.find('[data-test-subj="show-custom-jobs-filter-button"]').first().simulate('click'); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx index 4cfb7f8ad2b5bb..f25ea667b34118 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx @@ -15,11 +15,11 @@ import { } from '@elastic/eui'; import { EuiSearchBarQuery } from '../../../../../timelines/components/open_timeline/types'; import * as i18n from './translations'; -import { JobsFilters, SiemJob } from '../../types'; +import { JobsFilters, SecurityJob } from '../../types'; import { GroupsFilterPopover } from './groups_filter_popover'; interface JobsTableFiltersProps { - siemJobs: SiemJob[]; + securityJobs: SecurityJob[]; onFilterChanged: Dispatch>; } @@ -27,10 +27,13 @@ interface JobsTableFiltersProps { * Collection of filters for filtering data within the JobsTable. Contains search bar, Elastic/Custom * Jobs filter button toggle, and groups selection * - * @param siemJobs jobs to fetch groups from to display for filtering + * @param securityJobs jobs to fetch groups from to display for filtering * @param onFilterChanged change listener to be notified on filter changes */ -export const JobsTableFiltersComponent = ({ siemJobs, onFilterChanged }: JobsTableFiltersProps) => { +export const JobsTableFiltersComponent = ({ + securityJobs, + onFilterChanged, +}: JobsTableFiltersProps) => { const [filterQuery, setFilterQuery] = useState(''); const [selectedGroups, setSelectedGroups] = useState([]); const [showCustomJobs, setShowCustomJobs] = useState(false); @@ -71,7 +74,10 @@ export const JobsTableFiltersComponent = ({ siemJobs, onFilterChanged }: JobsTab - + diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.test.tsx index ade8c6fe805257..e58d76bd1dde00 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.test.tsx @@ -9,22 +9,22 @@ import React from 'react'; import { JobSwitchComponent } from './job_switch'; import { cloneDeep } from 'lodash/fp'; -import { mockSiemJobs } from '../__mocks__/api'; -import { SiemJob } from '../types'; +import { mockSecurityJobs } from '../api.mock'; +import { SecurityJob } from '../types'; describe('JobSwitch', () => { - let siemJobs: SiemJob[]; + let securityJobs: SecurityJob[]; let onJobStateChangeMock = jest.fn(); beforeEach(() => { - siemJobs = cloneDeep(mockSiemJobs); + securityJobs = cloneDeep(mockSecurityJobs); onJobStateChangeMock = jest.fn(); }); test('renders correctly against snapshot', () => { const wrapper = shallow( ); @@ -34,8 +34,8 @@ describe('JobSwitch', () => { test('should call onJobStateChange when the switch is clicked to be true/open', () => { const wrapper = mount( ); @@ -57,8 +57,8 @@ describe('JobSwitch', () => { test('should have a switch when it is not in the loading state', () => { const wrapper = mount( ); @@ -68,8 +68,8 @@ describe('JobSwitch', () => { test('should not have a switch when it is in the loading state', () => { const wrapper = mount( ); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.tsx index d370d475bd6e57..3ad71ee6b6919c 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/job_switch.tsx @@ -12,7 +12,7 @@ import { isJobFailed, isJobStarted, } from '../../../../../common/machine_learning/helpers'; -import { SiemJob } from '../types'; +import { SecurityJob } from '../types'; const StaticSwitch = styled(EuiSwitch)` .euiSwitch__thumb, @@ -24,14 +24,14 @@ const StaticSwitch = styled(EuiSwitch)` StaticSwitch.displayName = 'StaticSwitch'; export interface JobSwitchProps { - job: SiemJob; - isSiemJobsLoading: boolean; - onJobStateChange: (job: SiemJob, latestTimestampMs: number, enable: boolean) => Promise; + job: SecurityJob; + isSecurityJobsLoading: boolean; + onJobStateChange: (job: SecurityJob, latestTimestampMs: number, enable: boolean) => Promise; } export const JobSwitchComponent = ({ job, - isSiemJobsLoading, + isSecurityJobsLoading, onJobStateChange, }: JobSwitchProps) => { const [isLoading, setIsLoading] = useState(false); @@ -47,7 +47,7 @@ export const JobSwitchComponent = ({ return ( - {isSiemJobsLoading || isLoading || isJobLoading(job.jobState, job.datafeedState) ? ( + {isSecurityJobsLoading || isLoading || isJobLoading(job.jobState, job.datafeedState) ? ( ) : ( { - let siemJobs: SiemJob[]; + let securityJobs: SecurityJob[]; let onJobStateChangeMock = jest.fn(); beforeEach(() => { - siemJobs = cloneDeep(mockSiemJobs); + securityJobs = cloneDeep(mockSecurityJobs); onJobStateChangeMock = jest.fn(); }); @@ -25,7 +25,7 @@ describe('JobsTableComponent', () => { const wrapper = shallow( ); @@ -36,7 +36,7 @@ describe('JobsTableComponent', () => { const wrapper = mount( ); @@ -46,11 +46,11 @@ describe('JobsTableComponent', () => { }); test('should render the hyperlink with URI encodings which points specifically to the job id', () => { - siemJobs[0].id = 'job id with spaces'; + securityJobs[0].id = 'job id with spaces'; const wrapper = mount( ); @@ -63,7 +63,7 @@ describe('JobsTableComponent', () => { const wrapper = mount( ); @@ -73,14 +73,14 @@ describe('JobsTableComponent', () => { .simulate('click', { target: { checked: true }, }); - expect(onJobStateChangeMock.mock.calls[0]).toEqual([siemJobs[0], 1571022859393, true]); + expect(onJobStateChangeMock.mock.calls[0]).toEqual([securityJobs[0], 1571022859393, true]); }); test('should have a switch when it is not in the loading state', () => { const wrapper = mount( ); @@ -91,7 +91,7 @@ describe('JobsTableComponent', () => { const wrapper = mount( ); diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx index f28a99c9947d54..be911a1cd85378 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.tsx @@ -25,7 +25,7 @@ import styled from 'styled-components'; import { useBasePath } from '../../../lib/kibana'; import * as i18n from './translations'; import { JobSwitch } from './job_switch'; -import { SiemJob } from '../types'; +import { SecurityJob } from '../types'; const JobNameWrapper = styled.div` margin: 5px 0; @@ -38,12 +38,12 @@ const truncateThreshold = 200; const getJobsTableColumns = ( isLoading: boolean, - onJobStateChange: (job: SiemJob, latestTimestampMs: number, enable: boolean) => Promise, + onJobStateChange: (job: SecurityJob, latestTimestampMs: number, enable: boolean) => Promise, basePath: string ) => [ { name: i18n.COLUMN_JOB_NAME, - render: ({ id, description }: SiemJob) => ( + render: ({ id, description }: SecurityJob) => ( ( + render: ({ groups }: SecurityJob) => ( {groups.map((group) => ( @@ -76,9 +76,13 @@ const getJobsTableColumns = ( { name: i18n.COLUMN_RUN_JOB, - render: (job: SiemJob) => + render: (job: SecurityJob) => job.isCompatible ? ( - + ) : ( ), @@ -87,13 +91,16 @@ const getJobsTableColumns = ( } as const, ]; -const getPaginatedItems = (items: SiemJob[], pageIndex: number, pageSize: number): SiemJob[] => - items.slice(pageIndex * pageSize, pageIndex * pageSize + pageSize); +const getPaginatedItems = ( + items: SecurityJob[], + pageIndex: number, + pageSize: number +): SecurityJob[] => items.slice(pageIndex * pageSize, pageIndex * pageSize + pageSize); export interface JobTableProps { isLoading: boolean; - jobs: SiemJob[]; - onJobStateChange: (job: SiemJob, latestTimestampMs: number, enable: boolean) => Promise; + jobs: SecurityJob[]; + onJobStateChange: (job: SecurityJob, latestTimestampMs: number, enable: boolean) => Promise; } export const JobsTableComponent = ({ isLoading, jobs, onJobStateChange }: JobTableProps) => { diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx index 0ebf3674718482..f2bf2273c4b3fb 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/ml_popover.tsx @@ -12,19 +12,17 @@ import styled from 'styled-components'; import { useKibana } from '../../lib/kibana'; import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../lib/telemetry'; -import { hasMlAdminPermissions } from '../../../../common/machine_learning/has_ml_admin_permissions'; import { errorToToaster, useStateToaster, ActionToaster } from '../toasters'; import { setupMlJob, startDatafeeds, stopDatafeeds } from './api'; import { filterJobs } from './helpers'; -import { useSiemJobs } from './hooks/use_siem_jobs'; import { JobsTableFilters } from './jobs_table/filters/jobs_table_filters'; import { JobsTable } from './jobs_table/jobs_table'; import { ShowingCount } from './jobs_table/showing_count'; import { PopoverDescription } from './popover_description'; import * as i18n from './translations'; -import { JobsFilters, SiemJob } from './types'; +import { JobsFilters, SecurityJob } from './types'; import { UpgradeContents } from './upgrade_contents'; -import { useMlCapabilities } from './hooks/use_ml_capabilities'; +import { useSecurityJobs } from './hooks/use_security_jobs'; const PopoverContentsDiv = styled.div` max-width: 684px; @@ -87,24 +85,25 @@ export const MlPopover = React.memo(() => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [filterProperties, setFilterProperties] = useState(defaultFilterProps); - const [isLoadingSiemJobs, siemJobs] = useSiemJobs(refreshToggle); + const { isMlAdmin, isLicensed, loading: isLoadingSecurityJobs, jobs } = useSecurityJobs( + refreshToggle + ); const [, dispatchToaster] = useStateToaster(); - const capabilities = useMlCapabilities(); const docLinks = useKibana().services.docLinks; const handleJobStateChange = useCallback( - (job: SiemJob, latestTimestampMs: number, enable: boolean) => + (job: SecurityJob, latestTimestampMs: number, enable: boolean) => enableDatafeed(job, latestTimestampMs, enable, dispatch, dispatchToaster), [dispatch, dispatchToaster] ); const filteredJobs = filterJobs({ - jobs: siemJobs, + jobs, ...filterProperties, }); - const incompatibleJobCount = siemJobs.filter((j) => !j.isCompatible).length; + const incompatibleJobCount = jobs.filter((j) => !j.isCompatible).length; - if (!capabilities.isPlatinumOrTrialLicense) { + if (!isLicensed) { // If the user does not have platinum show upgrade UI return ( { ); - } else if (hasMlAdminPermissions(capabilities)) { + } else if (isMlAdmin) { // If the user has Platinum License & ML Admin Permissions, show Anomaly Detection button & full config UI return ( { - + @@ -194,7 +193,7 @@ export const MlPopover = React.memo(() => { )} @@ -209,7 +208,7 @@ export const MlPopover = React.memo(() => { // Enable/Disable Job & Datafeed -- passed to JobsTable for use as callback on JobSwitch const enableDatafeed = async ( - job: SiemJob, + job: SecurityJob, latestTimestampMs: number, enable: boolean, dispatch: Dispatch, @@ -257,7 +256,7 @@ const enableDatafeed = async ( dispatch({ type: 'refresh' }); }; -const submitTelemetry = (job: SiemJob, enabled: boolean) => { +const submitTelemetry = (job: SecurityJob, enabled: boolean) => { // Report type of job enabled/disabled track( METRIC_TYPE.COUNT, diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/types.ts b/x-pack/plugins/security_solution/public/common/components/ml_popover/types.ts index f39daa0b9a7fbe..c839f5110fe7fd 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/types.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AuditMessageBase } from '../../../../../ml/public'; import { MlError } from '../ml/types'; +import { MlSummaryJob } from '../../../../../ml/public'; export interface Group { id: string; @@ -98,28 +98,6 @@ export interface MlSetupArgs { prefix?: string; } -/** - * Representation of an ML Job as returned from the `ml/jobs/jobs_summary` API - */ -export interface JobSummary { - auditMessage?: AuditMessageBase; - datafeedId: string; - datafeedIndices: string[]; - datafeedState: string; - description: string; - earliestTimestampMs?: number; - latestResultsTimestampMs?: number; - groups: string[]; - hasDatafeed: boolean; - id: string; - isSingleMetricViewerJob: boolean; - jobState: string; - latestTimestampMs?: number; - memory_status: string; - nodeName?: string; - processed_record_count: number; -} - export interface Detector { detector_description: string; function: string; @@ -133,10 +111,10 @@ export interface CustomURL { } /** - * Representation of an ML Job as used by the SIEM App -- a composition of ModuleJob and JobSummary + * Representation of an ML Job as used by the SIEM App -- a composition of ModuleJob and MlSummaryJob * that includes necessary metadata like moduleName, defaultIndexPattern, etc. */ -export interface SiemJob extends JobSummary { +export interface SecurityJob extends MlSummaryJob { moduleId: string; defaultIndexPattern: string; isCompatible: boolean; @@ -144,7 +122,7 @@ export interface SiemJob extends JobSummary { isElasticJob: boolean; } -export interface AugmentedSiemJobFields { +export interface AugmentedSecurityJobFields { moduleId: string; defaultIndexPattern: string; isCompatible: boolean; diff --git a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx index 76270a7c08cd63..94019b26c180b0 100644 --- a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx @@ -9,7 +9,7 @@ import React, { useEffect } from 'react'; import { DEFAULT_ANOMALY_SCORE } from '../../../../../common/constants'; import { AnomaliesQueryTabBodyProps } from './types'; import { getAnomaliesFilterQuery } from './utils'; -import { useSiemJobs } from '../../../components/ml_popover/hooks/use_siem_jobs'; +import { useInstalledSecurityJobs } from '../../../components/ml/hooks/use_installed_security_jobs'; import { useUiSetting$ } from '../../../lib/kibana'; import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; import { histogramConfigs } from './histogram_configs'; @@ -38,13 +38,13 @@ export const AnomaliesQueryTabBody = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const [, siemJobs] = useSiemJobs(true); + const { jobs } = useInstalledSecurityJobs(); const [anomalyScore] = useUiSetting$(DEFAULT_ANOMALY_SCORE); const mergedFilterQuery = getAnomaliesFilterQuery( filterQuery, anomaliesFilterQuery, - siemJobs, + jobs, anomalyScore, flowTarget, ip diff --git a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/utils.ts b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/utils.ts index 10d5d1c60a6c2b..5248801d723b60 100644 --- a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/utils.ts +++ b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/utils.ts @@ -6,21 +6,20 @@ import deepmerge from 'deepmerge'; +import { MlSummaryJob } from '../../../../../../ml/public'; import { ESTermQuery } from '../../../../../common/typed_json'; import { createFilter } from '../../helpers'; -import { SiemJob } from '../../../components/ml_popover/types'; import { FlowTarget } from '../../../../graphql/types'; export const getAnomaliesFilterQuery = ( filterQuery: string | ESTermQuery | undefined, anomaliesFilterQuery: object = {}, - siemJobs: SiemJob[] = [], + securityJobs: MlSummaryJob[] = [], anomalyScore: number, flowTarget?: FlowTarget, ip?: string ): string => { - const siemJobIds = siemJobs - .filter((job) => job.isInstalled) + const securityJobIds = securityJobs .map((job) => job.id) .map((jobId) => ({ match_phrase: { @@ -38,7 +37,7 @@ export const getAnomaliesFilterQuery = ( filter: [ { bool: { - should: siemJobIds, + should: securityJobIds, minimum_should_match: 1, }, }, diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts new file mode 100644 index 00000000000000..1af4ba3ba9233c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_app_toasts.mock.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +const createAppToastsMock = () => ({ + addError: jest.fn(), + addSuccess: jest.fn(), +}); + +export const useAppToastsMock = { + create: createAppToastsMock, +}; diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts index 2c52acd3ec747c..5f4285f2747ae1 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts @@ -17,6 +17,7 @@ export const KibanaServices = { get: jest.fn(), getKibanaVersion: jest.fn(() => export const useKibana = jest.fn(createUseKibanaMock()); export const useUiSetting = jest.fn(createUseUiSettingMock()); export const useUiSetting$ = jest.fn(createUseUiSetting$Mock()); +export const useHttp = jest.fn(() => useKibana().services.http); export const useTimeZone = jest.fn(); export const useDateFormat = jest.fn(); export const useBasePath = jest.fn(() => '/test/base/path'); diff --git a/x-pack/plugins/security_solution/public/common/mock/kibana_core.ts b/x-pack/plugins/security_solution/public/common/mock/kibana_core.ts index 13b3c4b249bfef..f8eed75cf9bf1a 100644 --- a/x-pack/plugins/security_solution/public/common/mock/kibana_core.ts +++ b/x-pack/plugins/security_solution/public/common/mock/kibana_core.ts @@ -6,8 +6,10 @@ import { coreMock } from '../../../../../../src/core/public/mocks'; import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; +import { securityMock } from '../../../../../plugins/security/public/mocks'; export const createKibanaCoreStartMock = () => coreMock.createStart(); export const createKibanaPluginsStartMock = () => ({ data: dataPluginMock.createStartContract(), + security: securityMock.createSetup(), }); diff --git a/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts b/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts index c5d50e1379482b..bdb8ca85b0d777 100644 --- a/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts +++ b/x-pack/plugins/security_solution/public/common/mock/kibana_react.ts @@ -96,28 +96,10 @@ export const createUseKibanaMock = () => { export const createStartServices = () => { const core = createKibanaCoreStartMock(); const plugins = createKibanaPluginsStartMock(); - const security = { - authc: { - getCurrentUser: jest.fn(), - areAPIKeysEnabled: jest.fn(), - }, - sessionTimeout: { - start: jest.fn(), - stop: jest.fn(), - extend: jest.fn(), - }, - license: { - isEnabled: jest.fn(), - getFeatures: jest.fn(), - features$: jest.fn(), - }, - __legacyCompat: { logoutUrl: 'logoutUrl', tenant: 'tenant' }, - }; const services = ({ ...core, ...plugins, - security, } as unknown) as StartServices; return services; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx index 47c12d19341740..00141c9a453d82 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx @@ -38,7 +38,7 @@ import { buildRuleTypeDescription, buildThresholdDescription, } from './helpers'; -import { useSiemJobs } from '../../../../common/components/ml_popover/hooks/use_siem_jobs'; +import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/use_security_jobs'; import { buildMlJobDescription } from './ml_job_description'; import { buildActionsDescription } from './actions_description'; import { buildThrottleDescription } from './throttle_description'; @@ -67,7 +67,7 @@ export const StepRuleDescriptionComponent: React.FC = }) => { const kibana = useKibana(); const [filterManager] = useState(new FilterManager(kibana.services.uiSettings)); - const [, siemJobs] = useSiemJobs(true); + const { jobs } = useSecurityJobs(false); const keys = Object.keys(schema); const listItems = keys.reduce((acc: ListItems[], key: string) => { @@ -77,7 +77,7 @@ export const StepRuleDescriptionComponent: React.FC = buildMlJobDescription( get(key, data) as string, (get(key, schema) as { label: string }).label, - siemJobs + jobs ), ]; } diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx index c82a465f08c3a7..3152fef12c6523 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.test.tsx @@ -7,31 +7,14 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { mockOpenedJob } from '../../../../common/components/ml_popover/api.mock'; import { MlJobDescription, AuditIcon, JobStatusBadge } from './ml_job_description'; -jest.mock('../../../../common/lib/kibana'); -const job = { - moduleId: 'moduleId', - defaultIndexPattern: 'defaultIndexPattern', - isCompatible: true, - isInstalled: true, - isElasticJob: true, - datafeedId: 'datafeedId', - datafeedIndices: [], - datafeedState: 'datafeedState', - description: 'description', - groups: [], - hasDatafeed: true, - id: 'id', - isSingleMetricViewerJob: false, - jobState: 'jobState', - memory_status: 'memory_status', - processed_record_count: 0, -}; +jest.mock('../../../../common/lib/kibana'); describe('MlJobDescription', () => { it('renders correctly', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find('[data-test-subj="machineLearningJobId"]')).toHaveLength(1); }); @@ -47,7 +30,7 @@ describe('AuditIcon', () => { describe('JobStatusBadge', () => { it('renders correctly', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find('EuiBadge')).toHaveLength(1); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx index d7e06511e79373..6baa2abab33d1a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/ml_job_description.tsx @@ -8,9 +8,9 @@ import React from 'react'; import styled from 'styled-components'; import { EuiBadge, EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; +import { MlSummaryJob } from '../../../../../../ml/public'; import { isJobStarted } from '../../../../../common/machine_learning/helpers'; import { useKibana } from '../../../../common/lib/kibana'; -import { SiemJob } from '../../../../common/components/ml_popover/types'; import { ListItems } from './types'; import { ML_JOB_STARTED, ML_JOB_STOPPED } from './translations'; @@ -21,7 +21,7 @@ enum MessageLevels { } const AuditIconComponent: React.FC<{ - message: SiemJob['auditMessage']; + message: MlSummaryJob['auditMessage']; }> = ({ message }) => { if (!message) { return null; @@ -47,7 +47,7 @@ const AuditIconComponent: React.FC<{ export const AuditIcon = React.memo(AuditIconComponent); -const JobStatusBadgeComponent: React.FC<{ job: SiemJob }> = ({ job }) => { +const JobStatusBadgeComponent: React.FC<{ job: MlSummaryJob }> = ({ job }) => { const isStarted = isJobStarted(job.jobState, job.datafeedState); const color = isStarted ? 'secondary' : 'danger'; const text = isStarted ? ML_JOB_STARTED : ML_JOB_STOPPED; @@ -69,7 +69,7 @@ const Wrapper = styled.div` overflow: hidden; `; -const MlJobDescriptionComponent: React.FC<{ job: SiemJob }> = ({ job }) => { +const MlJobDescriptionComponent: React.FC<{ job: MlSummaryJob }> = ({ job }) => { const jobUrl = useKibana().services.application.getUrlForApp( `ml#/jobs?mlManagement=(jobId:${encodeURI(job.id)})` ); @@ -92,12 +92,12 @@ export const MlJobDescription = React.memo(MlJobDescriptionComponent); export const buildMlJobDescription = ( jobId: string, label: string, - siemJobs: SiemJob[] + jobs: MlSummaryJob[] ): ListItems => { - const siemJob = siemJobs.find((job) => job.id === jobId); + const job = jobs.find(({ id }) => id === jobId); return { title: label, - description: siemJob ? : jobId, + description: job ? : jobId, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.test.tsx index 6f6581e4de1c37..4a08adbedab3f6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.test.tsx @@ -8,14 +8,14 @@ import React from 'react'; import { shallow } from 'enzyme'; import { MlJobSelect } from './index'; -import { useSiemJobs } from '../../../../common/components/ml_popover/hooks/use_siem_jobs'; +import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/use_security_jobs'; import { useFormFieldMock } from '../../../../common/mock'; -jest.mock('../../../../common/components/ml_popover/hooks/use_siem_jobs'); +jest.mock('../../../../common/components/ml_popover/hooks/use_security_jobs'); jest.mock('../../../../common/lib/kibana'); describe('MlJobSelect', () => { beforeAll(() => { - (useSiemJobs as jest.Mock).mockReturnValue([false, []]); + (useSecurityJobs as jest.Mock).mockReturnValue({ loading: false, jobs: [] }); }); it('renders correctly', () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.tsx index cdfdf4ca6b66bf..b0aa0329fe8f40 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/ml_job_select/index.tsx @@ -19,7 +19,7 @@ import { import styled from 'styled-components'; import { isJobStarted } from '../../../../../common/machine_learning/helpers'; import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; -import { useSiemJobs } from '../../../../common/components/ml_popover/hooks/use_siem_jobs'; +import { useSecurityJobs } from '../../../../common/components/ml_popover/hooks/use_security_jobs'; import { useKibana } from '../../../../common/lib/kibana'; import { ML_JOB_SELECT_PLACEHOLDER_TEXT, @@ -81,7 +81,7 @@ interface MlJobSelectProps { export const MlJobSelect: React.FC = ({ describedByIds = [], field }) => { const jobId = field.value as string; const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - const [isLoading, siemJobs] = useSiemJobs(false); + const { loading, jobs } = useSecurityJobs(false); const mlUrl = useKibana().services.application.getUrlForApp('ml'); const handleJobChange = useCallback( (machineLearningJobId: string) => { @@ -96,7 +96,7 @@ export const MlJobSelect: React.FC = ({ describedByIds = [], f disabled: true, }; - const jobOptions = siemJobs.map((job) => ({ + const jobOptions = jobs.map((job) => ({ value: job.id, inputDisplay: job.id, dropdownDisplay: , @@ -107,9 +107,9 @@ export const MlJobSelect: React.FC = ({ describedByIds = [], f const isJobRunning = useMemo(() => { // If the selected job is not found in the list, it means the placeholder is selected // and so we don't want to show the warning, thus isJobRunning will be true when 'job == null' - const job = siemJobs.find((j) => j.id === jobId); + const job = jobs.find(({ id }) => id === jobId); return job == null || isJobStarted(job.jobState, job.datafeedState); - }, [siemJobs, jobId]); + }, [jobs, jobId]); return ( @@ -126,7 +126,7 @@ export const MlJobSelect: React.FC = ({ describedByIds = [], f = ({ componentProps={{ describedByIds: ['detectionEngineStepDefineRuleType'], isReadOnly: isUpdateView, - hasValidLicense: mlCapabilities.isPlatinumOrTrialLicense, + hasValidLicense: hasMlLicense(mlCapabilities), isMlAdmin: hasMlAdminPermissions(mlCapabilities), }} /> diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx index 85dce907084e8a..110691328b13b5 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx @@ -47,8 +47,9 @@ import { getColumns, getMonitoringColumns } from './columns'; import { showRulesTable } from './helpers'; import { allRulesReducer, State } from './reducer'; import { RulesTableFilters } from './rules_table_filters/rules_table_filters'; -import { useMlCapabilities } from '../../../../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../../../../../common/components/ml/hooks/use_ml_capabilities'; import { hasMlAdminPermissions } from '../../../../../../common/machine_learning/has_ml_admin_permissions'; +import { hasMlLicense } from '../../../../../../common/machine_learning/has_ml_license'; import { SecurityPageName } from '../../../../../app/types'; import { useFormatUrl } from '../../../../../common/components/link_to'; @@ -145,8 +146,7 @@ export const AllRules = React.memo( const { formatUrl } = useFormatUrl(SecurityPageName.detections); // TODO: Refactor license check + hasMlAdminPermissions to common check - const hasMlPermissions = - mlCapabilities.isPlatinumOrTrialLicense && hasMlAdminPermissions(mlCapabilities); + const hasMlPermissions = hasMlLicense(mlCapabilities) && hasMlAdminPermissions(mlCapabilities); const setRules = useCallback((newRules: Rule[], newPagination: Partial) => { dispatch({ diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 016d0c7c67a9e8..8a969a4cf098cb 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -71,8 +71,9 @@ import { RuleActionsOverflow } from '../../../../components/rules/rule_actions_o import { RuleStatusFailedCallOut } from './status_failed_callout'; import { FailureHistory } from './failure_history'; import { RuleStatus } from '../../../../components/rules//rule_status'; -import { useMlCapabilities } from '../../../../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../../../../../common/components/ml/hooks/use_ml_capabilities'; import { hasMlAdminPermissions } from '../../../../../../common/machine_learning/has_ml_admin_permissions'; +import { hasMlLicense } from '../../../../../../common/machine_learning/has_ml_license'; import { SecurityPageName } from '../../../../../app/types'; import { LinkButton } from '../../../../../common/components/links'; import { useFormatUrl } from '../../../../../common/components/link_to'; @@ -161,8 +162,7 @@ export const RuleDetailsPageComponent: FC = ({ const { globalFullScreen } = useFullScreen(); // TODO: Refactor license check + hasMlAdminPermissions to common check - const hasMlPermissions = - mlCapabilities.isPlatinumOrTrialLicense && hasMlAdminPermissions(mlCapabilities); + const hasMlPermissions = hasMlLicense(mlCapabilities) && hasMlAdminPermissions(mlCapabilities); const ruleDetailTabs = getRuleDetailsTabs(rule); // persist rule until refresh is complete diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index 34840b28266268..67f563e944f42f 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -17,7 +17,7 @@ import { LastEventTime } from '../../../common/components/last_event_time'; import { AnomalyTableProvider } from '../../../common/components/ml/anomaly/anomaly_table_provider'; import { hostToCriteria } from '../../../common/components/ml/criteria/host_to_criteria'; import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml_user_permissions'; -import { useMlCapabilities } from '../../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities'; import { scoreIntervalToDateTime } from '../../../common/components/ml/score/score_interval_to_datetime'; import { SiemNavigation } from '../../../common/components/navigation'; import { KpiHostsComponent } from '../../components/kpi_hosts'; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index e4e69443c510d2..2b19249dc426fd 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -34,7 +34,7 @@ import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from import { SpyRoute } from '../../common/utils/route/spy_routes'; import { esQuery } from '../../../../../../src/plugins/data/public'; -import { useMlCapabilities } from '../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../../common/components/ml/hooks/use_ml_capabilities'; import { OverviewEmpty } from '../../overview/components/overview_empty'; import { Display } from './display'; import { HostsTabs } from './hosts_tabs'; diff --git a/x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx b/x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx index cf08b084d21979..d6dfe1769308e9 100644 --- a/x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx @@ -30,7 +30,7 @@ import { DescriptionListStyled, OverviewWrapper } from '../../../common/componen import { Loader } from '../../../common/components/loader'; import { Anomalies, NarrowDateRange } from '../../../common/components/ml/types'; import { AnomalyScores } from '../../../common/components/ml/score/anomaly_scores'; -import { useMlCapabilities } from '../../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities'; import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml_user_permissions'; import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect'; diff --git a/x-pack/plugins/security_solution/public/network/pages/index.tsx b/x-pack/plugins/security_solution/public/network/pages/index.tsx index 9ac05cc98bb454..07abe7bc8c209f 100644 --- a/x-pack/plugins/security_solution/public/network/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/index.tsx @@ -7,7 +7,7 @@ import React, { useMemo } from 'react'; import { Route, Switch, RouteComponentProps, useHistory } from 'react-router-dom'; -import { useMlCapabilities } from '../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../../common/components/ml/hooks/use_ml_capabilities'; import { hasMlUserPermissions } from '../../../common/machine_learning/has_ml_user_permissions'; import { FlowTarget } from '../../graphql/types'; diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx index 0a15b039b96af2..c7aba6fcc8a5b3 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx @@ -23,7 +23,7 @@ import { HostItem } from '../../../graphql/types'; import { Loader } from '../../../common/components/loader'; import { IPDetailsLink } from '../../../common/components/links'; import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml_user_permissions'; -import { useMlCapabilities } from '../../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities'; import { AnomalyScores } from '../../../common/components/ml/score/anomaly_scores'; import { Anomalies, NarrowDateRange } from '../../../common/components/ml/types'; import { DescriptionListStyled, OverviewWrapper } from '../../../common/components/page'; diff --git a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx index 9f310bb1cc0d65..061dfce64b4e44 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx @@ -127,7 +127,6 @@ const EdgeLineComponent = React.memo( return ( void) => { - const disabled = selectedItems?.some((item) => item.status === TimelineStatus.immutable); + const disabled = selectedItems == null || selectedItems.length === 0; return ( <> , ); }, - // eslint-disable-next-line react-hooks/exhaustive-deps [ selectedItems, deleteTimelines, diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx index 9de3242c5e3038..3d5c5f60d1d9b0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx @@ -9,6 +9,7 @@ import { cloneDeep } from 'lodash/fp'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ThemeProvider } from 'styled-components'; +import { act } from '@testing-library/react'; import '../../../common/mock/match_media'; import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines_page'; @@ -51,7 +52,7 @@ describe('OpenTimeline', () => { title, timelineType: TimelineType.default, timelineStatus: TimelineStatus.active, - templateTimelineFilter: [
], + templateTimelineFilter: [
,
], totalSearchResultsCount: mockSearchResults.length, }); @@ -279,6 +280,86 @@ describe('OpenTimeline', () => { expect(wrapper.find('[data-test-subj="utility-bar-action"]').exists()).toEqual(true); }); + test('it should disable export-timeline if no timeline is selected', async () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: null, + selectedItems: [], + }; + const wrapper = mountWithIntl( + + + + ); + + wrapper.find('[data-test-subj="utility-bar-action"]').find('EuiLink').simulate('click'); + await act(async () => { + expect( + wrapper.find('[data-test-subj="export-timeline-action"]').first().prop('disabled') + ).toEqual(true); + }); + }); + + test('it should disable delete timeline if no timeline is selected', async () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: null, + selectedItems: [], + }; + const wrapper = mountWithIntl( + + + + ); + + wrapper.find('[data-test-subj="utility-bar-action"]').find('EuiLink').simulate('click'); + await act(async () => { + expect( + wrapper.find('[data-test-subj="delete-timeline-action"]').first().prop('disabled') + ).toEqual(true); + }); + }); + + test('it should enable export-timeline if a timeline is selected', async () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: null, + selectedItems: [{}], + }; + const wrapper = mountWithIntl( + + + + ); + + wrapper.find('[data-test-subj="utility-bar-action"]').find('EuiLink').simulate('click'); + await act(async () => { + expect( + wrapper.find('[data-test-subj="export-timeline-action"]').first().prop('disabled') + ).toEqual(false); + }); + }); + + test('it should enable delete timeline if a timeline is selected', async () => { + const defaultProps = { + ...getDefaultTestProps(mockResults), + timelineStatus: null, + selectedItems: [{}], + }; + const wrapper = mountWithIntl( + + + + ); + + wrapper.find('[data-test-subj="utility-bar-action"]').find('EuiLink').simulate('click'); + await act(async () => { + expect( + wrapper.find('[data-test-subj="delete-timeline-action"]').first().prop('disabled') + ).toEqual(false); + }); + }); + test("it should render a selectable timeline table if timelineStatus is active (selecting custom templates' tab)", () => { const defaultProps = { ...getDefaultTestProps(mockResults), diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx index c9495c46d4acf3..1f5f0ccca3b708 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.tsx @@ -160,7 +160,6 @@ export const OpenTimeline = React.memo( }, [onDeleteSelected, deleteTimelines, timelineStatus]); const SearchRowContent = useMemo(() => <>{templateTimelineFilter}, [templateTimelineFilter]); - return ( <> { + test('it filters out running tasks', async () => { const taskManagerId = uuid.v1(); const claimOwnershipUntil = new Date(Date.now()); const runAt = new Date(); @@ -641,7 +641,7 @@ if (doc['task.runAt'].size()!=0) { taskType: 'foo', schedule: undefined, attempts: 0, - status: 'idle', + status: 'claiming', params: '{ "hello": "world" }', state: '{ "baby": "Henhen" }', user: 'jimbo', @@ -715,7 +715,103 @@ if (doc['task.runAt'].size()!=0) { runAt, scope: ['reporting'], state: { baby: 'Henhen' }, - status: 'idle', + status: 'claiming', + taskType: 'foo', + user: 'jimbo', + ownerId: taskManagerId, + }, + ]); + }); + + test('it returns task objects', async () => { + const taskManagerId = uuid.v1(); + const claimOwnershipUntil = new Date(Date.now()); + const runAt = new Date(); + const tasks = [ + { + _id: 'aaa', + _source: { + type: 'task', + task: { + runAt, + taskType: 'foo', + schedule: undefined, + attempts: 0, + status: 'claiming', + params: '{ "hello": "world" }', + state: '{ "baby": "Henhen" }', + user: 'jimbo', + scope: ['reporting'], + ownerId: taskManagerId, + }, + }, + _seq_no: 1, + _primary_term: 2, + sort: ['a', 1], + }, + { + _id: 'bbb', + _source: { + type: 'task', + task: { + runAt, + taskType: 'bar', + schedule: { interval: '5m' }, + attempts: 2, + status: 'claiming', + params: '{ "shazm": 1 }', + state: '{ "henry": "The 8th" }', + user: 'dabo', + scope: ['reporting', 'ceo'], + ownerId: taskManagerId, + }, + }, + _seq_no: 3, + _primary_term: 4, + sort: ['b', 2], + }, + ]; + const { + result: { docs }, + args: { + search: { + body: { query }, + }, + }, + } = await testClaimAvailableTasks({ + opts: { + taskManagerId, + }, + claimingOpts: { + claimOwnershipUntil, + size: 10, + }, + hits: tasks, + }); + + expect(query.bool.must).toContainEqual({ + bool: { + must: [ + { + term: { + 'task.ownerId': taskManagerId, + }, + }, + { term: { 'task.status': 'claiming' } }, + ], + }, + }); + + expect(docs).toMatchObject([ + { + attempts: 0, + id: 'aaa', + schedule: undefined, + params: { hello: 'world' }, + runAt, + scope: ['reporting'], + state: { baby: 'Henhen' }, + status: 'claiming', taskType: 'foo', user: 'jimbo', ownerId: taskManagerId, @@ -728,7 +824,7 @@ if (doc['task.runAt'].size()!=0) { runAt, scope: ['reporting', 'ceo'], state: { henry: 'The 8th' }, - status: 'running', + status: 'claiming', taskType: 'bar', user: 'dabo', ownerId: taskManagerId, diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index a18fb57b35b3d7..f2da41053e6ab6 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -217,48 +217,39 @@ export class TaskStore { claimTasksByIdWithRawIds, size ); + const docs = numberOfTasksClaimed > 0 ? await this.sweepForClaimedTasks(claimTasksByIdWithRawIds, size) : []; - // emit success/fail events for claimed tasks by id - if (claimTasksById && claimTasksById.length) { - const [documentsReturnedById, documentsClaimedBySchedule] = partition(docs, (doc) => - claimTasksById.includes(doc.id) - ); - - const [documentsClaimedById, documentsRequestedButNotClaimed] = partition( - documentsReturnedById, - // we filter the schduled tasks down by status is 'claiming' in the esearch, - // but we do not apply this limitation on tasks claimed by ID so that we can - // provide more detailed error messages when we fail to claim them - (doc) => doc.status === TaskStatus.Claiming - ); - - const documentsRequestedButNotReturned = difference( - claimTasksById, - map(documentsReturnedById, 'id') - ); + const [documentsReturnedById, documentsClaimedBySchedule] = partition(docs, (doc) => + claimTasksById.includes(doc.id) + ); - this.emitEvents( - [...documentsClaimedById, ...documentsClaimedBySchedule].map((doc) => - asTaskClaimEvent(doc.id, asOk(doc)) - ) - ); + const [documentsClaimedById, documentsRequestedButNotClaimed] = partition( + documentsReturnedById, + // we filter the schduled tasks down by status is 'claiming' in the esearch, + // but we do not apply this limitation on tasks claimed by ID so that we can + // provide more detailed error messages when we fail to claim them + (doc) => doc.status === TaskStatus.Claiming + ); - this.emitEvents( - documentsRequestedButNotClaimed.map((doc) => asTaskClaimEvent(doc.id, asErr(some(doc)))) - ); + const documentsRequestedButNotReturned = difference( + claimTasksById, + map(documentsReturnedById, 'id') + ); - this.emitEvents( - documentsRequestedButNotReturned.map((id) => asTaskClaimEvent(id, asErr(none))) - ); - } + this.emitEvents([ + ...documentsClaimedById.map((doc) => asTaskClaimEvent(doc.id, asOk(doc))), + ...documentsClaimedBySchedule.map((doc) => asTaskClaimEvent(doc.id, asOk(doc))), + ...documentsRequestedButNotClaimed.map((doc) => asTaskClaimEvent(doc.id, asErr(some(doc)))), + ...documentsRequestedButNotReturned.map((id) => asTaskClaimEvent(id, asErr(none))), + ]); return { - claimedTasks: numberOfTasksClaimed, - docs, + claimedTasks: documentsClaimedById.length + documentsClaimedBySchedule.length, + docs: docs.filter((doc) => doc.status === TaskStatus.Claiming), }; }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e5cd46b330ca8b..81c06cf5c381f6 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2388,11 +2388,6 @@ "indexPatternManagement.createIndexPattern.description": "インデックスパターンは、{single}または{multiple}データソース、{star}と一致します。", "indexPatternManagement.createIndexPattern.documentation": "ドキュメンテーションを表示", "indexPatternManagement.createIndexPattern.emptyState.checkDataButton": "新規データを確認", - "indexPatternManagement.createIndexPattern.emptyStateHeader": "Elasticsearchデータが見つかりませんでした", - "indexPatternManagement.createIndexPattern.emptyStateLabel.emptyStateDetail": "{needToIndex} {learnHowLink}または{getStartedLink}", - "indexPatternManagement.createIndexPattern.emptyStateLabel.getStartedLink": "サンプルデータで始めましょう。", - "indexPatternManagement.createIndexPattern.emptyStateLabel.learnHowLink": "方法を学習", - "indexPatternManagement.createIndexPattern.emptyStateLabel.needToIndexLabel": "インデックスパターンを作成する前に、Elasticsearchへのデータのインデックスが必要です。", "indexPatternManagement.createIndexPattern.includeSystemIndicesToggleSwitchLabel": "システムと非表示のインデックスを含める", "indexPatternManagement.createIndexPattern.loadClustersFailMsg": "リモートクラスターの読み込みに失敗", "indexPatternManagement.createIndexPattern.loadIndicesFailMsg": "インデックスの読み込みに失敗", @@ -2403,7 +2398,6 @@ "indexPatternManagement.createIndexPattern.step.indexPatternPlaceholder": "index-name-*", "indexPatternManagement.createIndexPattern.step.invalidCharactersErrorMessage": "{indexPatternName}にはスペースや{characterList}は使えません。", "indexPatternManagement.createIndexPattern.step.loadingHeader": "一致するインデックスを検索中…", - "indexPatternManagement.createIndexPattern.step.loadingLabel": "お待ちください…", "indexPatternManagement.createIndexPattern.step.nextStepButton": "次のステップ", "indexPatternManagement.createIndexPattern.step.pagingLabel": "ページごとの行数: {perPage}", "indexPatternManagement.createIndexPattern.step.status.matchAnyLabel.matchAnyDetail": "インデックスパターンは、{sourceCount, plural, one {個のソース} other {個のソース} }と一致します。", @@ -2553,15 +2547,6 @@ "indexPatternManagement.indexPattern.sectionsHeader": "インデックスパターン", "indexPatternManagement.indexPattern.titleExistsLabel": "「{title}」というタイトルのインデックスパターンがすでに存在します。", "indexPatternManagement.indexPatternList.createButton.betaLabel": "ベータ", - "indexPatternManagement.indexPatternPrompt.exampleOne": "チャートを作成したりコンテンツを素早くクエリできるように log-west-001 という名前の単一のデータソースをインデックスします。", - "indexPatternManagement.indexPatternPrompt.exampleOneTitle": "単一のデータソース", - "indexPatternManagement.indexPatternPrompt.examplesTitle": "インデックスパターンの例", - "indexPatternManagement.indexPatternPrompt.exampleThree": "比較目的に履歴の動向を集約できるよう、これらのログのアーカイブされた月々のロールアップメトリックスを指定どおりに別々のインデックスパターンにグループ分けします。", - "indexPatternManagement.indexPatternPrompt.exampleThreeTitle": "カスタムグルーピング", - "indexPatternManagement.indexPatternPrompt.exampleTwo": "すべての西海岸のサーバーログに対してクエリを実行できるように、頭に「log-west」の付いたすべての受信データソースをグループ化します。", - "indexPatternManagement.indexPatternPrompt.exampleTwoTitle": "複数データソース", - "indexPatternManagement.indexPatternPrompt.subtitle": "インデックスパターンは、Kibanaで共有フィールドにクエリを実行できるよう、種類の異なるデータソースをバケットにまとめることができます。", - "indexPatternManagement.indexPatternPrompt.title": "インデックスパターンについて", "indexPatternManagement.indexPatterns.badge.readOnly.text": "読み取り専用", "indexPatternManagement.indexPatterns.badge.readOnly.tooltip": "インデックスパターンを保存できません", "indexPatternManagement.indexPatterns.createBreadcrumb": "インデックスパターンを作成", @@ -14062,10 +14047,6 @@ "xpack.observability.home.sectionsubtitle": "ログ、メトリック、トレースを大規模に、1つのスタックにまとめて、環境内のあらゆる場所で生じるイベントの監視、分析、対応を行います。", "xpack.observability.home.sectionTitle": "エコシステム全体の一元的な可視性", "xpack.observability.home.title": "オブザーバビリティ", - "xpack.observability.ingestManafer.beta": "ベータ", - "xpack.observability.ingestManafer.button": "Ingest Managerベータを試す", - "xpack.observability.ingestManafer.text": "Elasticエージェントでは、シンプルかつ統合された方法で、ログ、メトリック、他の種類のデータの監視をホストに追加することができます。複数のBeatsと他のエージェントをインストールする必要はありません。このため、インフラストラクチャ全体での構成のデプロイが簡単で高速になりました。", - "xpack.observability.ingestManafer.title": "新しいIngest Managerをご覧になりましたか?", "xpack.observability.landing.breadcrumb": "はじめて使う", "xpack.observability.news.readFullStory": "詳細なストーリーを読む", "xpack.observability.news.title": "新機能", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b273f6cc81baf6..af5e68b7e44d71 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2389,11 +2389,6 @@ "indexPatternManagement.createIndexPattern.description": "索引模式可以匹配单个源,例如 {single} 或 {multiple} 个数据源、{star}。", "indexPatternManagement.createIndexPattern.documentation": "阅读文档", "indexPatternManagement.createIndexPattern.emptyState.checkDataButton": "检查新数据", - "indexPatternManagement.createIndexPattern.emptyStateHeader": "找不到任何 Elasticsearch 数据", - "indexPatternManagement.createIndexPattern.emptyStateLabel.emptyStateDetail": "{needToIndex}{learnHowLink}或{getStartedLink}", - "indexPatternManagement.createIndexPattern.emptyStateLabel.getStartedLink": "开始使用一些样例数据集。", - "indexPatternManagement.createIndexPattern.emptyStateLabel.learnHowLink": "了解操作方法", - "indexPatternManagement.createIndexPattern.emptyStateLabel.needToIndexLabel": "您需要在 Elasticsearch 中索引一些数据后,才能创建索引模式。", "indexPatternManagement.createIndexPattern.includeSystemIndicesToggleSwitchLabel": "包括系统和隐藏索引", "indexPatternManagement.createIndexPattern.loadClustersFailMsg": "无法加载远程集群", "indexPatternManagement.createIndexPattern.loadIndicesFailMsg": "无法加载索引", @@ -2404,7 +2399,6 @@ "indexPatternManagement.createIndexPattern.step.indexPatternPlaceholder": "index-name-*", "indexPatternManagement.createIndexPattern.step.invalidCharactersErrorMessage": "{indexPatternName} 不能包含空格或字符:{characterList}", "indexPatternManagement.createIndexPattern.step.loadingHeader": "正在寻找匹配的索引......", - "indexPatternManagement.createIndexPattern.step.loadingLabel": "请稍候......", "indexPatternManagement.createIndexPattern.step.nextStepButton": "下一步", "indexPatternManagement.createIndexPattern.step.pagingLabel": "每页行数:{perPage}", "indexPatternManagement.createIndexPattern.step.status.matchAnyLabel.matchAnyDetail": "您的索引模式可以匹配{sourceCount, plural, one {您的 # 个源} other {您的 # 个源中的任何一个} }。", @@ -2554,15 +2548,6 @@ "indexPatternManagement.indexPattern.sectionsHeader": "索引模式", "indexPatternManagement.indexPattern.titleExistsLabel": "具有标题“{title}”的索引模式已存在。", "indexPatternManagement.indexPatternList.createButton.betaLabel": "公测版", - "indexPatternManagement.indexPatternPrompt.exampleOne": "索引单个称作 log-west-001 的数据源,以便可以快速地构建图表或查询其内容。", - "indexPatternManagement.indexPatternPrompt.exampleOneTitle": "单数据源", - "indexPatternManagement.indexPatternPrompt.examplesTitle": "索引模式示例", - "indexPatternManagement.indexPatternPrompt.exampleThree": "具体而言,将这些日志每月存档的汇总/打包指标分组成不同的索引模式,从而可以聚合历史趋势以进行比较。", - "indexPatternManagement.indexPatternPrompt.exampleThreeTitle": "定制分组", - "indexPatternManagement.indexPatternPrompt.exampleTwo": "分组以 log-west* 开头的所有传入数据源,以便可以查询所有西海岸服务器日志。", - "indexPatternManagement.indexPatternPrompt.exampleTwoTitle": "多数据源", - "indexPatternManagement.indexPatternPrompt.subtitle": "索引模式允许您将异类的数据源一起装入存储桶,从而可以在 Kibana 中查询它们共享的字段。", - "indexPatternManagement.indexPatternPrompt.title": "关于索引模式", "indexPatternManagement.indexPatterns.badge.readOnly.text": "只读", "indexPatternManagement.indexPatterns.badge.readOnly.tooltip": "无法保存索引模式", "indexPatternManagement.indexPatterns.createBreadcrumb": "创建索引模式", @@ -14067,10 +14052,6 @@ "xpack.observability.home.sectionsubtitle": "通过根据需要将日志、指标和跟踪都置于单个堆栈上,来监测、分析和响应环境中任何位置发生的事件。", "xpack.observability.home.sectionTitle": "整个生态系统的统一可见性", "xpack.observability.home.title": "可观测性", - "xpack.observability.ingestManafer.beta": "公测版", - "xpack.observability.ingestManafer.button": "试用采集管理器公测版", - "xpack.observability.ingestManafer.text": "通过 Elastic 代理,您能够以简单统一的方式将日志、指标和其他类型数据的监测添加到主机。不再需要安装多个 Beats 和其他代理,这样在整个基础设施中部署配置会更轻松更快速。", - "xpack.observability.ingestManafer.title": "是否了解我们全新的采集管理器?", "xpack.observability.landing.breadcrumb": "入门", "xpack.observability.news.readFullStory": "详细了解", "xpack.observability.news.title": "最近的新闻", diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/slack.ts index 90660cc99507d3..91511b508aca67 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/slack.ts @@ -4,26 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ +import http from 'http'; +import getPort from 'get-port'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { - getExternalServiceSimulatorPath, - ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { getSlackServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function slackTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const kibanaServer = getService('kibanaServer'); describe('slack action', () => { - let slackSimulatorURL: string = ''; + let slackSimulatorURL: string = ''; + let slackServer: http.Server; - // need to wait for kibanaServer to settle ... - before(() => { - slackSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SLACK) - ); + before(async () => { + slackServer = await getSlackServer(); + const availablePort = await getPort({ port: 9000 }); + slackServer.listen(availablePort); + slackSimulatorURL = `http://localhost:${availablePort}`; }); it('should return 403 when creating a slack action', async () => { @@ -44,5 +43,9 @@ export default function slackTest({ getService }: FtrProviderContext) { 'Action type .slack is disabled because your basic license does not support it. Please upgrade your license.', }); }); + + after(() => { + slackServer.close(); + }); }); } diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/webhook.ts index af1d413ff3c462..039f1d4dd3275d 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/webhook.ts @@ -4,25 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ +import http from 'http'; +import getPort from 'get-port'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { - getExternalServiceSimulatorPath, - ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { getWebhookServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function webhookTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const kibanaServer = getService('kibanaServer'); describe('webhook action', () => { - let webhookSimulatorURL: string = ''; - + let webhookSimulatorURL: string = ''; + let webhookServer: http.Server; // need to wait for kibanaServer to settle ... - before(() => { - webhookSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK) - ); + before(async () => { + webhookServer = await getWebhookServer(); + const availablePort = await getPort({ port: 9000 }); + webhookServer.listen(availablePort); + webhookSimulatorURL = `http://localhost:${availablePort}`; }); it('should return 403 when creating a webhook action', async () => { @@ -47,5 +46,9 @@ export default function webhookTest({ getService }: FtrProviderContext) { 'Action type .webhook is disabled because your basic license does not support it. Please upgrade your license.', }); }); + + after(() => { + webhookServer.close(); + }); }); } diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts index cb1271494c294e..0f7acf5ead1a18 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import http from 'http'; import { Plugin, CoreSetup, IRouter } from 'kibana/server'; import { EncryptedSavedObjectsPluginStart } from '../../../../../../../plugins/encrypted_saved_objects/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../../plugins/features/server'; @@ -13,6 +14,8 @@ import { initPlugin as initPagerduty } from './pagerduty_simulation'; import { initPlugin as initServiceNow } from './servicenow_simulation'; import { initPlugin as initJira } from './jira_simulation'; import { initPlugin as initResilient } from './resilient_simulation'; +import { initPlugin as initSlack } from './slack_simulation'; +import { initPlugin as initWebhook } from './webhook_simulation'; export const NAME = 'actions-FTS-external-service-simulators'; @@ -39,6 +42,14 @@ export function getAllExternalServiceSimulatorPaths(): string[] { return allPaths; } +export async function getWebhookServer(): Promise { + return await initWebhook(); +} + +export async function getSlackServer(): Promise { + return await initSlack(); +} + interface FixtureSetupDeps { actions: ActionsPluginSetupContract; features: FeaturesPluginSetup; diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/slack_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/slack_simulation.ts new file mode 100644 index 00000000000000..5032112e702e25 --- /dev/null +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/slack_simulation.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import http from 'http'; + +export async function initPlugin() { + return http.createServer((request, response) => { + if (request.method === 'POST') { + let data = ''; + request.on('data', (chunk) => { + data += chunk; + }); + request.on('end', () => { + const body = JSON.parse(data); + const text = body && body.text; + + if (text == null) { + response.statusCode = 400; + response.end('bad request to slack simulator'); + return; + } + + switch (text) { + case 'success': { + response.statusCode = 200; + response.end('ok'); + return; + } + case 'no_text': + response.statusCode = 400; + response.end('no_text'); + return; + + case 'invalid_payload': + response.statusCode = 400; + response.end('invalid_payload'); + return; + + case 'invalid_token': + response.statusCode = 403; + response.end('invalid_token'); + return; + + case 'status_500': + response.statusCode = 500; + response.end('simulated slack 500 response'); + return; + + case 'rate_limit': + const res = { + retry_after: 1, + ok: false, + error: 'rate_limited', + }; + + response.writeHead(429, { 'Content-Type': 'application/json', 'Retry-After': '1' }); + response.write(JSON.stringify(res)); + response.end(); + return; + } + response.statusCode = 400; + response.end('unknown request to slack simulator'); + }); + } + }); +} diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/webhook_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/webhook_simulation.ts new file mode 100644 index 00000000000000..44d8ea0c2da20c --- /dev/null +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/webhook_simulation.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import http from 'http'; +import { fromNullable, map, filter, getOrElse } from 'fp-ts/lib/Option'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { constant } from 'fp-ts/lib/function'; + +export async function initPlugin() { + return http.createServer((request, response) => { + const credentials = pipe( + fromNullable(request.headers.authorization), + map((authorization) => authorization.split(/\s+/)), + filter((parts) => parts.length > 1), + map((parts) => Buffer.from(parts[1], 'base64').toString()), + filter((credentialsPart) => credentialsPart.indexOf(':') !== -1), + map((credentialsPart) => { + const [username, password] = credentialsPart.split(':'); + return { username, password }; + }), + getOrElse(constant({ username: '', password: '' })) + ); + + if (request.method === 'POST' || request.method === 'PUT') { + let data = ''; + request.on('data', (chunk) => { + data += chunk; + }); + request.on('end', () => { + switch (data) { + case 'success': + response.statusCode = 200; + response.end('OK'); + return; + case 'authenticate': + return validateAuthentication(credentials, response); + case 'success_post_method': + return validateRequestUsesMethod(request.method ?? '', 'post', response); + case 'success_put_method': + return validateRequestUsesMethod(request.method ?? '', 'put', response); + case 'failure': + response.statusCode = 500; + response.end('Error'); + return; + } + response.statusCode = 400; + response.end( + `unknown request to webhook simulator [${data ? `content: ${data}` : `no content`}]` + ); + return; + }); + } else { + request.on('end', () => { + response.statusCode = 400; + response.end('unknown request to webhook simulator [no content]'); + return; + }); + } + }); +} + +function validateAuthentication(credentials: any, res: any) { + try { + expect(credentials).to.eql({ + username: 'elastic', + password: 'changeme', + }); + res.statusCode = 200; + res.end('OK'); + } catch (ex) { + res.statusCode = 403; + res.end(`the validateAuthentication operation failed. ${ex.message}`); + } +} + +function validateRequestUsesMethod(requestMethod: string, method: string, res: any) { + try { + expect(requestMethod.toLowerCase()).to.eql(method); + res.statusCode = 200; + res.end('OK'); + } catch (ex) { + res.statusCode = 403; + res.end(`the validateAuthentication operation failed. ${ex.message}`); + } +} diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators_legacy/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators_legacy/index.ts deleted file mode 100644 index 43e6a73673556c..00000000000000 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators_legacy/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import Hapi from 'hapi'; -import { - getExternalServiceSimulatorPath, - NAME, - ExternalServiceSimulator, -} from '../actions_simulators/server/plugin'; - -import { initPlugin as initWebhook } from './webhook_simulation'; -import { initPlugin as initSlack } from './slack_simulation'; - -// eslint-disable-next-line import/no-default-export -export default function (kibana: any) { - return new kibana.Plugin({ - require: ['xpack_main'], - name: `${NAME}-legacy`, - init: (server: Hapi.Server) => { - initWebhook(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK)); - initSlack(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.SLACK)); - }, - }); -} diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators_legacy/package.json b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators_legacy/package.json deleted file mode 100644 index 644cd77d3be753..00000000000000 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators_legacy/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "actions-fixtures-legacy", - "version": "0.0.0", - "kibana": { - "version": "kibana" - } -} diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators_legacy/slack_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators_legacy/slack_simulation.ts deleted file mode 100644 index b914386b136ccc..00000000000000 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators_legacy/slack_simulation.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Joi from 'joi'; -import Hapi from 'hapi'; - -interface SlackRequest extends Hapi.Request { - payload: { - text: string; - }; -} -export function initPlugin(server: Hapi.Server, path: string) { - server.route({ - method: 'POST', - path, - options: { - auth: false, - validate: { - options: { abortEarly: false }, - payload: Joi.object().keys({ - text: Joi.string(), - }), - }, - }, - handler: slackHandler as Hapi.Lifecycle.Method, - }); -} -// Slack simulator: create a slack action pointing here, and you can get -// different responses based on the message posted. See the README.md for -// more info. - -function slackHandler(request: SlackRequest, h: any) { - const body = request.payload; - const text = body && body.text; - - if (text == null) { - return htmlResponse(h, 400, 'bad request to slack simulator'); - } - - switch (text) { - case 'success': - return htmlResponse(h, 200, 'ok'); - - case 'no_text': - return htmlResponse(h, 400, 'no_text'); - - case 'invalid_payload': - return htmlResponse(h, 400, 'invalid_payload'); - - case 'invalid_token': - return htmlResponse(h, 403, 'invalid_token'); - - case 'status_500': - return htmlResponse(h, 500, 'simulated slack 500 response'); - - case 'rate_limit': - const response = { - retry_after: 1, - ok: false, - error: 'rate_limited', - }; - - return h.response(response).type('application/json').header('retry-after', '1').code(429); - } - - return htmlResponse(h, 400, 'unknown request to slack simulator'); -} - -function htmlResponse(h: any, code: number, text: string) { - return h.response(text).type('text/html').code(code); -} diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators_legacy/webhook_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators_legacy/webhook_simulation.ts deleted file mode 100644 index 44e1aff162c922..00000000000000 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators_legacy/webhook_simulation.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import expect from '@kbn/expect'; -import Joi from 'joi'; -import Hapi, { Util } from 'hapi'; -import { fromNullable, map, filter, getOrElse } from 'fp-ts/lib/Option'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { constant } from 'fp-ts/lib/function'; - -interface WebhookRequest extends Hapi.Request { - payload: string; -} - -export async function initPlugin(server: Hapi.Server, path: string) { - server.auth.scheme('identifyCredentialsIfPresent', function identifyCredentialsIfPresent( - s: Hapi.Server, - options?: Hapi.ServerAuthSchemeOptions - ) { - const scheme = { - async authenticate(request: Hapi.Request, h: Hapi.ResponseToolkit) { - const credentials = pipe( - fromNullable(request.headers.authorization), - map((authorization) => authorization.split(/\s+/)), - filter((parts) => parts.length > 1), - map((parts) => Buffer.from(parts[1], 'base64').toString()), - filter((credentialsPart) => credentialsPart.indexOf(':') !== -1), - map((credentialsPart) => { - const [username, password] = credentialsPart.split(':'); - return { username, password }; - }), - getOrElse(constant({ username: '', password: '' })) - ); - - return h.authenticated({ credentials }); - }, - }; - - return scheme; - }); - server.auth.strategy('simple', 'identifyCredentialsIfPresent'); - - server.route({ - method: ['POST', 'PUT'], - path, - options: { - auth: 'simple', - validate: { - options: { abortEarly: false }, - payload: Joi.string(), - }, - }, - handler: webhookHandler as Hapi.Lifecycle.Method, - }); -} - -function webhookHandler(request: WebhookRequest, h: any) { - const body = request.payload; - - switch (body) { - case 'success': - return htmlResponse(h, 200, `OK`); - case 'authenticate': - return validateAuthentication(request, h); - case 'success_post_method': - return validateRequestUsesMethod(request, h, 'post'); - case 'success_put_method': - return validateRequestUsesMethod(request, h, 'put'); - case 'failure': - return htmlResponse(h, 500, `Error`); - } - - return htmlResponse( - h, - 400, - `unknown request to webhook simulator [${body ? `content: ${body}` : `no content`}]` - ); -} - -function validateAuthentication(request: WebhookRequest, h: any) { - const { - auth: { credentials }, - } = request; - try { - expect(credentials).to.eql({ - username: 'elastic', - password: 'changeme', - }); - return htmlResponse(h, 200, `OK`); - } catch (ex) { - return htmlResponse(h, 403, `the validateAuthentication operation failed. ${ex.message}`); - } -} - -function validateRequestUsesMethod( - request: WebhookRequest, - h: any, - method: Util.HTTP_METHODS_PARTIAL -) { - try { - expect(request.method).to.eql(method); - return htmlResponse(h, 200, `OK`); - } catch (ex) { - return htmlResponse(h, 403, `the validateAuthentication operation failed. ${ex.message}`); - } -} - -function htmlResponse(h: any, code: number, text: string) { - return h.response(text).type('text/html').code(code); -} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts index f21bc8edeef1e4..c68bcaa0ad4e8d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts @@ -5,28 +5,27 @@ */ import expect from '@kbn/expect'; - +import http from 'http'; +import getPort from 'get-port'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { - getExternalServiceSimulatorPath, - ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { getSlackServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function slackTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const kibanaServer = getService('kibanaServer'); describe('slack action', () => { let simulatedActionId = ''; - let slackSimulatorURL: string = ''; + let slackSimulatorURL: string = ''; + let slackServer: http.Server; // need to wait for kibanaServer to settle ... - before(() => { - slackSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SLACK) - ); + before(async () => { + slackServer = await getSlackServer(); + const availablePort = await getPort({ port: 9000 }); + slackServer.listen(availablePort); + slackSimulatorURL = `http://localhost:${availablePort}`; }); it('should return 200 when creating a slack action successfully', async () => { @@ -220,5 +219,9 @@ export default function slackTest({ getService }: FtrProviderContext) { expect(result.message).to.match(/error posting a slack message, retry later/); expect(result.retry).to.equal(true); }); + + after(() => { + slackServer.close(); + }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts index 7eba753d7e98ba..8f17ab54184b5b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts @@ -4,12 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import http from 'http'; +import getPort from 'get-port'; import expect from '@kbn/expect'; import { URL, format as formatUrl } from 'url'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, + getWebhookServer, } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; const defaultValues: Record = { @@ -30,11 +33,13 @@ export default function webhookTest({ getService }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); async function createWebhookAction( - urlWithCreds: string, - config: Record> = {} + webhookSimulatorURL: string, + config: Record> = {}, + kibanaUrlWithCreds: string ): Promise { - const { url: fullUrl, user, password } = extractCredentialsFromUrl(urlWithCreds); - const url = config.url && typeof config.url === 'object' ? parsePort(config.url) : fullUrl; + const { user, password } = extractCredentialsFromUrl(kibanaUrlWithCreds); + const url = + config.url && typeof config.url === 'object' ? parsePort(config.url) : webhookSimulatorURL; const composedConfig = { headers: { 'Content-Type': 'text/plain', @@ -61,11 +66,17 @@ export default function webhookTest({ getService }: FtrProviderContext) { } describe('webhook action', () => { - let webhookSimulatorURL: string = ''; - + let webhookSimulatorURL: string = ''; + let webhookServer: http.Server; + let kibanaURL: string = ''; // need to wait for kibanaServer to settle ... - before(() => { - webhookSimulatorURL = kibanaServer.resolveUrl( + before(async () => { + webhookServer = await getWebhookServer(); + const availablePort = await getPort({ port: 9000 }); + webhookServer.listen(availablePort); + webhookSimulatorURL = `http://localhost:${availablePort}`; + + kibanaURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK) ); }); @@ -117,7 +128,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { }); it('should send authentication to the webhook target', async () => { - const webhookActionId = await createWebhookAction(webhookSimulatorURL); + const webhookActionId = await createWebhookAction(webhookSimulatorURL, {}, kibanaURL); const { body: result } = await supertest .post(`/api/actions/action/${webhookActionId}/_execute`) .set('kbn-xsrf', 'test') @@ -132,7 +143,11 @@ export default function webhookTest({ getService }: FtrProviderContext) { }); it('should support the POST method against webhook target', async () => { - const webhookActionId = await createWebhookAction(webhookSimulatorURL, { method: 'post' }); + const webhookActionId = await createWebhookAction( + webhookSimulatorURL, + { method: 'post' }, + kibanaURL + ); const { body: result } = await supertest .post(`/api/actions/action/${webhookActionId}/_execute`) .set('kbn-xsrf', 'test') @@ -147,7 +162,11 @@ export default function webhookTest({ getService }: FtrProviderContext) { }); it('should support the PUT method against webhook target', async () => { - const webhookActionId = await createWebhookAction(webhookSimulatorURL, { method: 'put' }); + const webhookActionId = await createWebhookAction( + webhookSimulatorURL, + { method: 'put' }, + kibanaURL + ); const { body: result } = await supertest .post(`/api/actions/action/${webhookActionId}/_execute`) .set('kbn-xsrf', 'test') @@ -183,7 +202,11 @@ export default function webhookTest({ getService }: FtrProviderContext) { }); it('should handle unreachable webhook targets', async () => { - const webhookActionId = await createWebhookAction('http://some.non.existent.com/endpoint'); + const webhookActionId = await createWebhookAction( + 'http://some.non.existent.com/endpoint', + {}, + kibanaURL + ); const { body: result } = await supertest .post(`/api/actions/action/${webhookActionId}/_execute`) .set('kbn-xsrf', 'test') @@ -199,7 +222,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { }); it('should handle failing webhook targets', async () => { - const webhookActionId = await createWebhookAction(webhookSimulatorURL); + const webhookActionId = await createWebhookAction(webhookSimulatorURL, {}, kibanaURL); const { body: result } = await supertest .post(`/api/actions/action/${webhookActionId}/_execute`) .set('kbn-xsrf', 'test') @@ -214,6 +237,10 @@ export default function webhookTest({ getService }: FtrProviderContext) { expect(result.message).to.match(/error calling webhook, retry later/); expect(result.serviceMessage).to.eql('[500] Internal Server Error'); }); + + after(() => { + webhookServer.close(); + }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts index cac6355409ac93..ab3a92d0b3f706 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/update.ts @@ -31,8 +31,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) { .then((response: SupertestResponse) => response.body); } - // FLAKY: https://github.com/elastic/kibana/issues/72803 - describe.skip('update', () => { + describe('update', () => { const objectRemover = new ObjectRemover(supertest); after(() => objectRemover.removeAll()); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts index b3572978cee703..acfbad007d7220 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts @@ -4,24 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ +import http from 'http'; +import getPort from 'get-port'; import expect from '@kbn/expect'; import { URL, format as formatUrl } from 'url'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { - getExternalServiceSimulatorPath, - ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { getWebhookServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default function webhookTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const kibanaServer = getService('kibanaServer'); async function createWebhookAction( - urlWithCreds: string, + webhookSimulatorURL: string, config: Record> = {} ): Promise { - const url = formatUrl(new URL(urlWithCreds), { auth: false }); + const url = formatUrl(new URL(webhookSimulatorURL), { auth: false }); const composedConfig = { headers: { 'Content-Type': 'text/plain', @@ -45,13 +43,13 @@ export default function webhookTest({ getService }: FtrProviderContext) { } describe('webhook action', () => { - let webhookSimulatorURL: string = ''; - - // need to wait for kibanaServer to settle ... - before(() => { - webhookSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK) - ); + let webhookSimulatorURL: string = ''; + let webhookServer: http.Server; + before(async () => { + webhookServer = await getWebhookServer(); + const availablePort = await getPort({ port: 9000 }); + webhookServer.listen(availablePort); + webhookSimulatorURL = `http://localhost:${availablePort}`; }); it('webhook can be executed without username and password', async () => { @@ -68,5 +66,9 @@ export default function webhookTest({ getService }: FtrProviderContext) { expect(result.status).to.eql('ok'); }); + + after(() => { + webhookServer.close(); + }); }); } diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts index bc36f70df3641d..cedd96f147c2b2 100644 --- a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts @@ -130,7 +130,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it(`index pattern listing doesn't show create button`, async () => { await PageObjects.settings.clickKibanaIndexPatterns(); - await testSubjects.existOrFail('indexPatternTable'); + await testSubjects.existOrFail('emptyIndexPatternPrompt'); await testSubjects.missingOrFail('createIndexPatternButton'); }); diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/create_exception_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/create_exception_list_items.ts new file mode 100644 index 00000000000000..6148dbcc7090e6 --- /dev/null +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/create_exception_list_items.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { ExceptionListItemSchema } from '../../../../plugins/lists/common'; +import { getExceptionListItemResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_item_schema.mock'; +import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; +import { + getCreateExceptionListItemMinimalSchemaMock, + getCreateExceptionListItemMinimalSchemaMockWithoutId, +} from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock'; +import { + EXCEPTION_LIST_ITEM_URL, + EXCEPTION_LIST_URL, +} from '../../../../plugins/lists/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +import { + removeListItemServerGeneratedProperties, + removeExceptionListItemServerGeneratedProperties, +} from '../../utils'; + +import { deleteAllExceptions } from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('create_exception_list_items', () => { + describe('validation errors', () => { + it('should give a 404 error that the exception list must exist first before being able to add a list item to the exception list', async () => { + const { body } = await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMock()) + .expect(404); + + expect(body).to.eql({ + message: 'exception list id: "some-list-id" does not exist', + status_code: 404, + }); + }); + }); + + describe('creating exception list items', () => { + afterEach(async () => { + await deleteAllExceptions(es); + }); + + it('should create a simple exception list item with a list item id', async () => { + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + const { body } = await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMock()) + .expect(200); + + const bodyToCompare = removeExceptionListItemServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getExceptionListItemResponseMockWithoutAutoGeneratedValues()); + }); + + it('should create a simple exception list item without an id', async () => { + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + const { body } = await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMockWithoutId()) + .expect(200); + + const bodyToCompare = removeListItemServerGeneratedProperties(body); + const outputList: Partial = { + ...getExceptionListItemResponseMockWithoutAutoGeneratedValues(), + item_id: body.item_id, + }; + expect(bodyToCompare).to.eql(outputList); + }); + + it('should cause a 409 conflict if we attempt to create the same exception list item twice', async () => { + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMock()) + .expect(200); + + const { body } = await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMock()) + .expect(409); + + expect(body).to.eql({ + message: 'exception list item id: "some-list-item-id" already exists', + status_code: 409, + }); + }); + }); + }); +}; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/create_exception_lists.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/create_exception_lists.ts new file mode 100644 index 00000000000000..2b654c72ae282e --- /dev/null +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/create_exception_lists.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { ExceptionListSchema } from '../../../../plugins/lists/common'; +import { EXCEPTION_LIST_URL } from '../../../../plugins/lists/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { getExceptionResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_schema.mock'; +import { + getCreateExceptionListMinimalSchemaMock, + getCreateExceptionListMinimalSchemaMockWithoutId, +} from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; + +import { deleteAllExceptions, removeExceptionListServerGeneratedProperties } from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('create_exception_lists', () => { + describe('creating exception lists', () => { + afterEach(async () => { + await deleteAllExceptions(es); + }); + + it('should create a simple exception list', async () => { + const { body } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getExceptionResponseMockWithoutAutoGeneratedValues()); + }); + + it('should create a simple exception list without a list_id', async () => { + const { body } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMockWithoutId()) + .expect(200); + + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + const outputtedList: Partial = { + ...getExceptionResponseMockWithoutAutoGeneratedValues(), + list_id: bodyToCompare.list_id, + }; + expect(bodyToCompare).to.eql(outputtedList); + }); + + it('should cause a 409 conflict if we attempt to create the same list_id twice', async () => { + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + const { body } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(409); + + expect(body).to.eql({ + message: 'exception list id: "some-list-id" already exists', + status_code: 409, + }); + }); + }); + }); +}; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_exception_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_exception_list_items.ts new file mode 100644 index 00000000000000..16bdd264dc5464 --- /dev/null +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_exception_list_items.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { ExceptionListItemSchema } from '../../../../plugins/lists/common'; +import { getExceptionListItemResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_item_schema.mock'; +import { + getCreateExceptionListItemMinimalSchemaMock, + getCreateExceptionListItemMinimalSchemaMockWithoutId, +} from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock'; +import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + EXCEPTION_LIST_URL, + EXCEPTION_LIST_ITEM_URL, +} from '../../../../plugins/lists/common/constants'; + +import { deleteAllExceptions, removeExceptionListItemServerGeneratedProperties } from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('delete_exception_list_items', () => { + describe('delete exception list items', () => { + afterEach(async () => { + await deleteAllExceptions(es); + }); + + it('should delete a single exception list item by its item_id', async () => { + // create an exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // create an exception list item + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMock()) + .expect(200); + + // delete the exception list item by its item_id + const { body } = await supertest + .delete( + `${EXCEPTION_LIST_ITEM_URL}?item_id=${ + getCreateExceptionListItemMinimalSchemaMock().item_id + }` + ) + .set('kbn-xsrf', 'true') + .expect(200); + + const bodyToCompare = removeExceptionListItemServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getExceptionListItemResponseMockWithoutAutoGeneratedValues()); + }); + + it('should delete a single exception list item using an auto generated id', async () => { + // create an exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // create an exception list item + const { body: bodyWithCreatedList } = await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMockWithoutId()) + .expect(200); + + // delete that exception list item by its auto-generated id + const { body } = await supertest + .delete(`${EXCEPTION_LIST_ITEM_URL}?id=${bodyWithCreatedList.id}`) + .set('kbn-xsrf', 'true') + .expect(200); + const outputtedList: Partial = { + ...getExceptionListItemResponseMockWithoutAutoGeneratedValues(), + item_id: body.item_id, + }; + + const bodyToCompare = removeExceptionListItemServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputtedList); + }); + + it('should return an error if the id does not exist when trying to delete it', async () => { + const { body } = await supertest + .delete(`${EXCEPTION_LIST_ITEM_URL}?id=c1e1b359-7ac1-4e96-bc81-c683c092436f`) + .set('kbn-xsrf', 'true') + .expect(404); + + expect(body).to.eql({ + message: 'exception list item id: "c1e1b359-7ac1-4e96-bc81-c683c092436f" does not exist', + status_code: 404, + }); + }); + + it('should return an error if the item_id does not exist when trying to delete it', async () => { + const { body } = await supertest + .delete(`${EXCEPTION_LIST_ITEM_URL}?item_id=c1e1b359-7ac1-4e96-bc81-c683c092436f`) + .set('kbn-xsrf', 'true') + .expect(404); + + expect(body).to.eql({ + message: + 'exception list item item_id: "c1e1b359-7ac1-4e96-bc81-c683c092436f" does not exist', + status_code: 404, + }); + }); + }); + }); +}; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_exception_lists.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_exception_lists.ts new file mode 100644 index 00000000000000..56e4bcd9641cfe --- /dev/null +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/delete_exception_lists.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { ExceptionListSchema } from '../../../../plugins/lists/common'; +import { getExceptionResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_schema.mock'; +import { + getCreateExceptionListMinimalSchemaMock, + getCreateExceptionListMinimalSchemaMockWithoutId, +} from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { EXCEPTION_LIST_URL } from '../../../../plugins/lists/common/constants'; + +import { deleteAllExceptions, removeExceptionListServerGeneratedProperties } from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('delete_exception_lists', () => { + describe('delete exception lists', () => { + afterEach(async () => { + await deleteAllExceptions(es); + }); + + it('should delete a single exception list by its list_id', async () => { + // create an exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // delete the exception list by its list id + const { body } = await supertest + .delete( + `${EXCEPTION_LIST_URL}?list_id=${getCreateExceptionListMinimalSchemaMock().list_id}` + ) + .set('kbn-xsrf', 'true') + .expect(200); + + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getExceptionResponseMockWithoutAutoGeneratedValues()); + }); + + it('should delete a single exception list using an auto generated id', async () => { + // create an exception list + const { body: bodyWithCreatedList } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMockWithoutId()) + .expect(200); + + // delete that list by its auto-generated id + const { body } = await supertest + .delete(`${EXCEPTION_LIST_URL}?id=${bodyWithCreatedList.id}`) + .set('kbn-xsrf', 'true') + .expect(200); + + const outputtedList: Partial = { + ...getExceptionResponseMockWithoutAutoGeneratedValues(), + list_id: body.list_id, + }; + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputtedList); + }); + + it('should return an error if the id does not exist when trying to delete it', async () => { + const { body } = await supertest + .delete(`${EXCEPTION_LIST_URL}?id=c1e1b359-7ac1-4e96-bc81-c683c092436f`) + .set('kbn-xsrf', 'true') + .expect(404); + + expect(body).to.eql({ + message: 'exception list id: "c1e1b359-7ac1-4e96-bc81-c683c092436f" does not exist', + status_code: 404, + }); + }); + + it('should return an error if the list_id does not exist when trying to delete it', async () => { + const { body } = await supertest + .delete(`${EXCEPTION_LIST_URL}?list_id=c1e1b359-7ac1-4e96-bc81-c683c092436f`) + .set('kbn-xsrf', 'true') + .expect(404); + + expect(body).to.eql({ + message: 'exception list list_id: "c1e1b359-7ac1-4e96-bc81-c683c092436f" does not exist', + status_code: 404, + }); + }); + }); + }); +}; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/export_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/export_list_items.ts index 6fe783fc497f2c..74c28f5abdfc1d 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/export_list_items.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/export_list_items.ts @@ -96,8 +96,9 @@ export default ({ getService }: FtrProviderContext): void => { .set('kbn-xsrf', 'true') .expect(200) .parse(binaryToString); - - expect(body.toString()).to.eql('127.0.0.2\n127.0.0.1\n'); + const bodyString = body.toString(); + expect(bodyString.includes('127.0.0.1')).to.be(true); + expect(bodyString.includes('127.0.0.2')).to.be(true); }); }); }); diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_list_items.ts new file mode 100644 index 00000000000000..a65e9f344986fa --- /dev/null +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_list_items.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { getExceptionListItemResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_item_schema.mock'; +import { getCreateExceptionListItemMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock'; +import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + EXCEPTION_LIST_URL, + EXCEPTION_LIST_ITEM_URL, +} from '../../../../plugins/lists/common/constants'; + +import { deleteAllExceptions, removeExceptionListItemServerGeneratedProperties } from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('find_exception_list_items', () => { + describe('find exception list items', () => { + afterEach(async () => { + await deleteAllExceptions(es); + }); + + it('should return an empty find body correctly if no exception list items are loaded', async () => { + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + const { body } = await supertest + .get( + `${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${ + getCreateExceptionListMinimalSchemaMock().list_id + }` + ) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).to.eql({ + data: [], + page: 1, + per_page: 20, + total: 0, + }); + }); + + it('should return 404 if given a list_id that does not exist', async () => { + const { body } = await supertest + .get(`${EXCEPTION_LIST_ITEM_URL}/_find?list_id=non_exist`) + .set('kbn-xsrf', 'true') + .send() + .expect(404); + + expect(body).to.eql({ + message: 'exception list id: "non_exist" does not exist', + status_code: 404, + }); + }); + + it('should return a single exception list item when a single exception list item is loaded from a find with defaults added', async () => { + // add the exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // add a single exception list item + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMock()) + .expect(200); + + // query the single exception list from _find + const { body } = await supertest + .get( + `${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${ + getCreateExceptionListMinimalSchemaMock().list_id + }` + ) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + body.data = [removeExceptionListItemServerGeneratedProperties(body.data[0])]; + expect(body).to.eql({ + data: [getExceptionListItemResponseMockWithoutAutoGeneratedValues()], + page: 1, + per_page: 20, + total: 1, + }); + }); + }); + }); +}; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_lists.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_lists.ts new file mode 100644 index 00000000000000..c2328a7d112f43 --- /dev/null +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_lists.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { getExceptionResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_schema.mock'; +import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { EXCEPTION_LIST_URL } from '../../../../plugins/lists/common/constants'; + +import { deleteAllExceptions, removeExceptionListServerGeneratedProperties } from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('find_exception_lists', () => { + describe('find exception lists', () => { + afterEach(async () => { + await deleteAllExceptions(es); + }); + + it('should return an empty find body correctly if no exception lists are loaded', async () => { + const { body } = await supertest + .get(`${EXCEPTION_LIST_URL}/_find`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).to.eql({ + data: [], + page: 1, + per_page: 20, + total: 0, + }); + }); + + it('should return a single exception list when a single exception list is loaded from a find with defaults added', async () => { + // add a single exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // query the single exception list from _find + const { body } = await supertest + .get(`${EXCEPTION_LIST_URL}/_find`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + body.data = [removeExceptionListServerGeneratedProperties(body.data[0])]; + expect(body).to.eql({ + data: [getExceptionResponseMockWithoutAutoGeneratedValues()], + page: 1, + per_page: 20, + total: 1, + }); + }); + }); + }); +}; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/import_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/import_list_items.ts index 4befb6bbaf0500..7b7a6173fb4081 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/import_list_items.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/import_list_items.ts @@ -45,7 +45,7 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - describe('importing rules with an index', () => { + describe('importing lists with an index', () => { beforeEach(async () => { await createListsIndex(supertest); }); diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/index.ts index 302877a680aa66..5458b4a9a7db25 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/index.ts @@ -23,5 +23,15 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./find_list_items')); loadTestFile(require.resolve('./import_list_items')); loadTestFile(require.resolve('./export_list_items')); + loadTestFile(require.resolve('./create_exception_lists')); + loadTestFile(require.resolve('./create_exception_list_items')); + loadTestFile(require.resolve('./read_exception_lists')); + loadTestFile(require.resolve('./read_exception_list_items')); + loadTestFile(require.resolve('./update_exception_lists')); + loadTestFile(require.resolve('./update_exception_list_items')); + loadTestFile(require.resolve('./delete_exception_lists')); + loadTestFile(require.resolve('./delete_exception_list_items')); + loadTestFile(require.resolve('./find_exception_lists')); + loadTestFile(require.resolve('./find_exception_list_items')); }); }; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/read_exception_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/read_exception_list_items.ts new file mode 100644 index 00000000000000..26b969e940a2b3 --- /dev/null +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/read_exception_list_items.ts @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { getExceptionListItemResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_item_schema.mock'; +import { + getCreateExceptionListItemMinimalSchemaMock, + getCreateExceptionListItemMinimalSchemaMockWithoutId, +} from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock'; +import { ExceptionListItemSchema } from '../../../../plugins/lists/common'; +import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + EXCEPTION_LIST_URL, + EXCEPTION_LIST_ITEM_URL, +} from '../../../../plugins/lists/common/constants'; + +import { deleteAllExceptions, removeExceptionListItemServerGeneratedProperties } from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('read_exception_list_items', () => { + describe('reading exception list items', () => { + afterEach(async () => { + await deleteAllExceptions(es); + }); + + it('should be able to read a single exception list items using item_id', async () => { + // create a simple exception list to read + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + const { body } = await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMock()) + .expect(200); + + const bodyToCompare = removeExceptionListItemServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getExceptionListItemResponseMockWithoutAutoGeneratedValues()); + }); + + it('should be able to read a single exception list item using id', async () => { + // create a simple exception list to read + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // create a simple exception list item to read + const { body: createListBody } = await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMock()) + .expect(200); + + const { body } = await supertest + .get(`${EXCEPTION_LIST_ITEM_URL}?id=${createListBody.id}`) + .set('kbn-xsrf', 'true') + .expect(200); + + const bodyToCompare = removeExceptionListItemServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getExceptionListItemResponseMockWithoutAutoGeneratedValues()); + }); + + it('should be able to read a single list item with an auto-generated id', async () => { + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // create a simple exception list item to read + const { body: createListBody } = await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMockWithoutId()) + .expect(200); + + const { body } = await supertest + .get(`${EXCEPTION_LIST_ITEM_URL}?id=${createListBody.id}`) + .set('kbn-xsrf', 'true') + .expect(200); + + const outputtedList: Partial = { + ...getExceptionListItemResponseMockWithoutAutoGeneratedValues(), + item_id: body.item_id, + }; + + const bodyToCompare = removeExceptionListItemServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputtedList); + }); + + it('should be able to read a single list item with an auto-generated item_id', async () => { + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // create a simple exception list item to read + const { body: createListBody } = await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMockWithoutId()) + .expect(200); + + const { body } = await supertest + .get(`${EXCEPTION_LIST_ITEM_URL}?item_id=${createListBody.item_id}`) + .set('kbn-xsrf', 'true') + .expect(200); + + const outputtedList: Partial = { + ...getExceptionListItemResponseMockWithoutAutoGeneratedValues(), + item_id: body.item_id, + }; + + const bodyToCompare = removeExceptionListItemServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputtedList); + }); + + it('should return 404 if given a fake id', async () => { + const { body } = await supertest + .get(`${EXCEPTION_LIST_ITEM_URL}?id=c1e1b359-7ac1-4e96-bc81-c683c092436f`) + .set('kbn-xsrf', 'true') + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'exception list item id: "c1e1b359-7ac1-4e96-bc81-c683c092436f" does not exist', + }); + }); + + it('should return 404 if given a fake list_id', async () => { + const { body } = await supertest + .get(`${EXCEPTION_LIST_ITEM_URL}?item_id=c1e1b359-7ac1-4e96-bc81-c683c092436f`) + .set('kbn-xsrf', 'true') + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: + 'exception list item item_id: "c1e1b359-7ac1-4e96-bc81-c683c092436f" does not exist', + }); + }); + }); + }); +}; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/read_exception_lists.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/read_exception_lists.ts new file mode 100644 index 00000000000000..ee6bef3200f5c7 --- /dev/null +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/read_exception_lists.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { ExceptionListSchema } from '../../../../plugins/lists/common'; +import { getExceptionResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_schema.mock'; +import { + getCreateExceptionListMinimalSchemaMock, + getCreateExceptionListMinimalSchemaMockWithoutId, +} from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { EXCEPTION_LIST_URL } from '../../../../plugins/lists/common/constants'; + +import { deleteAllExceptions, removeExceptionListServerGeneratedProperties } from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('read_exception_lists', () => { + describe('reading exception lists', () => { + afterEach(async () => { + await deleteAllExceptions(es); + }); + + it('should be able to read a single exception list using list_id', async () => { + // create a simple exception list to read + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + const { body } = await supertest + .get(`${EXCEPTION_LIST_URL}?list_id=${getCreateExceptionListMinimalSchemaMock().list_id}`) + .set('kbn-xsrf', 'true') + .expect(200); + + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getExceptionResponseMockWithoutAutoGeneratedValues()); + }); + + it('should be able to read a single exception list using id', async () => { + // create a simple exception list to read + const { body: createListBody } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + const { body } = await supertest + .get(`${EXCEPTION_LIST_URL}?id=${createListBody.id}`) + .set('kbn-xsrf', 'true') + .expect(200); + + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getExceptionResponseMockWithoutAutoGeneratedValues()); + }); + + it('should be able to read a single list with an auto-generated list_id', async () => { + // create a simple exception list to read + const { body: createListBody } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMockWithoutId()) + .expect(200); + + const { body } = await supertest + .get(`${EXCEPTION_LIST_URL}?list_id=${createListBody.list_id}`) + .set('kbn-xsrf', 'true') + .expect(200); + + const outputtedList: Partial = { + ...getExceptionResponseMockWithoutAutoGeneratedValues(), + list_id: body.list_id, + }; + + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputtedList); + }); + + it('should return 404 if given a fake id', async () => { + const { body } = await supertest + .get(`${EXCEPTION_LIST_URL}?id=c1e1b359-7ac1-4e96-bc81-c683c092436f`) + .set('kbn-xsrf', 'true') + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'exception list id: "c1e1b359-7ac1-4e96-bc81-c683c092436f" does not exist', + }); + }); + + it('should return 404 if given a fake list_id', async () => { + const { body } = await supertest + .get(`${EXCEPTION_LIST_URL}?list_id=c1e1b359-7ac1-4e96-bc81-c683c092436f`) + .set('kbn-xsrf', 'true') + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'exception list list_id: "c1e1b359-7ac1-4e96-bc81-c683c092436f" does not exist', + }); + }); + }); + }); +}; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/update_exception_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/update_exception_list_items.ts new file mode 100644 index 00000000000000..40fb705620a196 --- /dev/null +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/update_exception_list_items.ts @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { getExceptionListItemResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_item_schema.mock'; +import { getCreateExceptionListItemMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_item_schema.mock'; +import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + EXCEPTION_LIST_URL, + EXCEPTION_LIST_ITEM_URL, +} from '../../../../plugins/lists/common/constants'; + +import { deleteAllExceptions, removeExceptionListServerGeneratedProperties } from '../../utils'; +import { + UpdateExceptionListItemSchema, + ExceptionListItemSchema, +} from '../../../../plugins/lists/common/schemas'; + +import { getUpdateMinimalExceptionListItemSchemaMock } from '../../../../plugins/lists/common/schemas/request/update_exception_list_item_schema.mock'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('update_exception_list_items', () => { + describe('update exception list items', () => { + afterEach(async () => { + await deleteAllExceptions(es); + }); + + it('should update a single exception list item property of name using an id', async () => { + // create a simple exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // create a simple exception list item + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListItemMinimalSchemaMock()) + .expect(200); + + // update a exception list item's name + const updatedList: UpdateExceptionListItemSchema = { + ...getUpdateMinimalExceptionListItemSchemaMock(), + name: 'some other name', + }; + + const { body } = await supertest + .put(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(updatedList) + .expect(200); + + const outputList: Partial = { + ...getExceptionListItemResponseMockWithoutAutoGeneratedValues(), + name: 'some other name', + }; + + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputList); + }); + + it('should update a single exception list item property of name using an auto-generated item_id', async () => { + // create a simple exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // eslint-disable-next-line @typescript-eslint/naming-convention + const { item_id, ...itemNoId } = getCreateExceptionListItemMinimalSchemaMock(); + + // create a simple exception list item + const { body: createListBody } = await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(itemNoId) + .expect(200); + + // update a exception list item's name + const updatedList: UpdateExceptionListItemSchema = { + ...getUpdateMinimalExceptionListItemSchemaMock(), + item_id: createListBody.item_id, + name: 'some other name', + }; + + const { body } = await supertest + .put(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(updatedList) + .expect(200); + + const outputList: Partial = { + ...getExceptionListItemResponseMockWithoutAutoGeneratedValues(), + name: 'some other name', + item_id: body.item_id, + }; + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputList); + }); + + it('should give a 404 if it is given a fake exception list item id', async () => { + const updatedList: UpdateExceptionListItemSchema = { + ...getUpdateMinimalExceptionListItemSchemaMock(), + id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d', + }; + delete updatedList.item_id; + + const { body } = await supertest + .put(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(updatedList) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'exception list item id: "5096dec6-b6b9-4d8d-8f93-6c2602079d9d" does not exist', + }); + }); + + it('should give a 404 if it is given a fake item_id', async () => { + const updatedList: UpdateExceptionListItemSchema = { + ...getUpdateMinimalExceptionListItemSchemaMock(), + item_id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d', + }; + + const { body } = await supertest + .put(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(updatedList) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: + 'exception list item item_id: "5096dec6-b6b9-4d8d-8f93-6c2602079d9d" does not exist', + }); + }); + + it('should give a 404 if both id and list_id is null', async () => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { item_id, ...listNoId } = getUpdateMinimalExceptionListItemSchemaMock(); + + const { body } = await supertest + .put(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send(listNoId) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'either id or item_id need to be defined', + }); + }); + }); + }); +}; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/update_exception_lists.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/update_exception_lists.ts new file mode 100644 index 00000000000000..bd30dd87963eda --- /dev/null +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/update_exception_lists.ts @@ -0,0 +1,182 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { getExceptionResponseMockWithoutAutoGeneratedValues } from '../../../../plugins/lists/common/schemas/response/exception_list_schema.mock'; +import { getCreateExceptionListMinimalSchemaMock } from '../../../../plugins/lists/common/schemas/request/create_exception_list_schema.mock'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { EXCEPTION_LIST_URL } from '../../../../plugins/lists/common/constants'; + +import { deleteAllExceptions, removeExceptionListServerGeneratedProperties } from '../../utils'; +import { + UpdateExceptionListSchema, + ExceptionListSchema, +} from '../../../../plugins/lists/common/schemas'; + +import { getUpdateMinimalExceptionListSchemaMock } from '../../../../plugins/lists/common/schemas/request/update_exception_list_schema.mock'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('update_exception_lists', () => { + describe('update exception lists', () => { + afterEach(async () => { + await deleteAllExceptions(es); + }); + + it('should update a single exception list property of name using an id', async () => { + // create a simple exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // update a exception list's name + const updatedList: UpdateExceptionListSchema = { + ...getUpdateMinimalExceptionListSchemaMock(), + name: 'some other name', + }; + + const { body } = await supertest + .put(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(updatedList) + .expect(200); + + const outputList: Partial = { + ...getExceptionResponseMockWithoutAutoGeneratedValues(), + name: 'some other name', + version: 2, + }; + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputList); + }); + + it('should update a single exception list property of name using an auto-generated list_id', async () => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { list_id, ...listNoId } = getCreateExceptionListMinimalSchemaMock(); + + // create a simple exception list + const { body: createListBody } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(listNoId) + .expect(200); + + // update a exception list's name + const updatedList: UpdateExceptionListSchema = { + ...getUpdateMinimalExceptionListSchemaMock(), + id: createListBody.id, + name: 'some other name', + }; + + const { body } = await supertest + .put(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(updatedList) + .expect(200); + + const outputList: Partial = { + ...getExceptionResponseMockWithoutAutoGeneratedValues(), + name: 'some other name', + list_id: body.list_id, + version: 2, + }; + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputList); + }); + + it('should change the version of a list when it updates two properties', async () => { + // create a simple exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListMinimalSchemaMock()) + .expect(200); + + // update a simple exception list property of name and description + // update a exception list's name + const updatedList: UpdateExceptionListSchema = { + ...getUpdateMinimalExceptionListSchemaMock(), + name: 'some other name', + description: 'some other description', + }; + + const { body } = await supertest + .put(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(updatedList) + .expect(200); + + const outputList: Partial = { + ...getExceptionResponseMockWithoutAutoGeneratedValues(), + name: 'some other name', + description: 'some other description', + version: 2, + }; + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(outputList); + }); + + it('should give a 404 if it is given a fake id', async () => { + const updatedList: UpdateExceptionListSchema = { + ...getUpdateMinimalExceptionListSchemaMock(), + id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d', + }; + delete updatedList.list_id; + + const { body } = await supertest + .put(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(updatedList) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'exception list id: "5096dec6-b6b9-4d8d-8f93-6c2602079d9d" does not exist', + }); + }); + + it('should give a 404 if it is given a fake list_id', async () => { + const updatedList: UpdateExceptionListSchema = { + ...getUpdateMinimalExceptionListSchemaMock(), + list_id: '5096dec6-b6b9-4d8d-8f93-6c2602079d9d', + }; + + const { body } = await supertest + .put(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(updatedList) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'exception list list_id: "5096dec6-b6b9-4d8d-8f93-6c2602079d9d" does not exist', + }); + }); + + it('should give a 404 if both id and list_id is null', async () => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { list_id, ...listNoId } = getUpdateMinimalExceptionListSchemaMock(); + + const { body } = await supertest + .put(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(listNoId) + .expect(404); + + expect(body).to.eql({ + status_code: 404, + message: 'either id or list_id need to be defined', + }); + }); + }); + }); +}; diff --git a/x-pack/test/lists_api_integration/utils.ts b/x-pack/test/lists_api_integration/utils.ts index 272768fdf50b30..54a13fc027c99d 100644 --- a/x-pack/test/lists_api_integration/utils.ts +++ b/x-pack/test/lists_api_integration/utils.ts @@ -6,8 +6,13 @@ import { SuperTest } from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; +import { Client } from '@elastic/elasticsearch'; -import { ListItemSchema } from '../../plugins/lists/common/schemas'; +import { + ListItemSchema, + ExceptionListSchema, + ExceptionListItemSchema, +} from '../../plugins/lists/common/schemas'; import { ListSchema } from '../../plugins/lists/common'; import { LIST_INDEX } from '../../plugins/lists/common/constants'; @@ -83,6 +88,30 @@ export const removeListItemServerGeneratedProperties = ( return removedProperties; }; +/** + * This will remove server generated properties such as date times, etc... + * @param list List to pass in to remove typical server generated properties + */ +export const removeExceptionListItemServerGeneratedProperties = ( + list: Partial +): Partial => { + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + const { created_at, updated_at, id, tie_breaker_id, _version, ...removedProperties } = list; + return removedProperties; +}; + +/** + * This will remove server generated properties such as date times, etc... + * @param list List to pass in to remove typical server generated properties + */ +export const removeExceptionListServerGeneratedProperties = ( + list: Partial +): Partial => { + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + const { created_at, updated_at, id, tie_breaker_id, _version, ...removedProperties } = list; + return removedProperties; +}; + // Similar to ReactJs's waitFor from here: https://testing-library.com/docs/dom-testing-library/api-async#waitfor export const waitFor = async ( functionToTest: () => Promise, @@ -124,3 +153,32 @@ export const binaryToString = (res: any, callback: any): void => { callback(null, Buffer.from(res.data)); }); }; + +/** + * Remove all exceptions from the .kibana index + * This will retry 20 times before giving up and hopefully still not interfere with other tests + * @param es The ElasticSearch handle + */ +export const deleteAllExceptions = async (es: Client, retryCount = 20): Promise => { + if (retryCount > 0) { + try { + await es.deleteByQuery({ + index: '.kibana', + q: 'type:exception-list or type:exception-list-agnostic', + wait_for_completion: true, + refresh: true, + body: {}, + }); + } catch (err) { + // eslint-disable-next-line no-console + console.log( + `Failure trying to deleteAllExceptions, retries left are: ${retryCount - 1}`, + err + ); + await deleteAllExceptions(es, retryCount - 1); + } + } else { + // eslint-disable-next-line no-console + console.log('Could not deleteAllExceptions, no retries are left'); + } +}; diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js index ea95eb42dd6ff5..c87a5039360b8f 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js @@ -28,8 +28,7 @@ export default function ({ getService }) { const testHistoryIndex = '.kibana_task_manager_test_result'; const supertest = supertestAsPromised(url.format(config.get('servers.kibana'))); - // FLAKY: https://github.com/elastic/kibana/issues/71390 - describe.skip('scheduling and running tasks', () => { + describe('scheduling and running tasks', () => { beforeEach( async () => await supertest.delete('/api/sample_tasks').set('kbn-xsrf', 'xxx').expect(200) ); diff --git a/yarn.lock b/yarn.lock index 7731d2f7a8ea19..42c4b800e6b0c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25,14 +25,7 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" - integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g== - dependencies: - "@babel/highlight" "^7.8.3" - -"@babel/code-frame@^7.10.4": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== @@ -48,38 +41,7 @@ invariant "^2.2.4" semver "^5.5.0" -"@babel/compat-data@^7.8.6", "@babel/compat-data@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.9.0.tgz#04815556fc90b0c174abd2c0c1bb966faa036a6c" - integrity sha512-zeFQrr+284Ekvd9e7KAX954LkapWiOmQtsfHirhxqfdlX6MEC32iRE+pqUGlYIBchdevaCwvzxWGSy/YBNI85g== - dependencies: - browserslist "^4.9.1" - invariant "^2.2.4" - semver "^5.5.0" - -"@babel/core@^7.0.1", "@babel/core@^7.1.0", "@babel/core@^7.4.3", "@babel/core@^7.7.5": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.9.0.tgz#ac977b538b77e132ff706f3b8a4dbad09c03c56e" - integrity sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w== - dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.9.0" - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helpers" "^7.9.0" - "@babel/parser" "^7.9.0" - "@babel/template" "^7.8.6" - "@babel/traverse" "^7.9.0" - "@babel/types" "^7.9.0" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.1" - json5 "^2.1.2" - lodash "^4.17.13" - resolve "^1.3.2" - semver "^5.4.1" - source-map "^0.5.0" - -"@babel/core@^7.11.1": +"@babel/core@^7.0.1", "@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.4.3", "@babel/core@^7.7.5": version "7.11.1" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.1.tgz#2c55b604e73a40dc21b0e52650b11c65cf276643" integrity sha512-XqF7F6FWQdKGGWAzGELL+aCO1p+lRY5Tj5/tbT3St1G8NaH70jhhDIKknIZaDans0OQBG5wRAldROLHSt44BgQ== @@ -101,17 +63,7 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.0.0", "@babel/generator@^7.9.0": - version "7.9.4" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.4.tgz#12441e90c3b3c4159cdecf312075bf1a8ce2dbce" - integrity sha512-rjP8ahaDy/ouhrvCoU1E5mqaitWrxwuNGU+dy1EpaoK48jZay4MdkskKGIMHLZNewg8sAsqpGSREJwP0zH3YQA== - dependencies: - "@babel/types" "^7.9.0" - jsesc "^2.5.1" - lodash "^4.17.13" - source-map "^0.5.0" - -"@babel/generator@^7.11.0": +"@babel/generator@^7.0.0", "@babel/generator@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.0.tgz#4b90c78d8c12825024568cbe83ee6c9af193585c" integrity sha512-fEm3Uzw7Mc9Xi//qU20cBKatTfs2aOtKqmvy/Vm7RkJEGFQ4xc9myCfbXxqK//ZS8MR/ciOHw6meGASJuKmDfQ== @@ -120,24 +72,7 @@ jsesc "^2.5.1" source-map "^0.5.0" -"@babel/generator@^7.9.5": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.5.tgz#27f0917741acc41e6eaaced6d68f96c3fa9afaf9" - integrity sha512-GbNIxVB3ZJe3tLeDm1HSn2AhuD/mVcyLDpgtLXa5tplmWrJdF/elxB56XNqCuD6szyNkDi6wuoKXln3QeBmCHQ== - dependencies: - "@babel/types" "^7.9.5" - jsesc "^2.5.1" - lodash "^4.17.13" - source-map "^0.5.0" - -"@babel/helper-annotate-as-pure@^7.0.0", "@babel/helper-annotate-as-pure@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz#60bc0bc657f63a0924ff9a4b4a0b24a13cf4deee" - integrity sha512-6o+mJrZBxOoEX77Ezv9zwW7WV8DdluouRKNY/IR5u/YTMuKHgugHOzYWlYvYLpLA9nPsQCAAASpCIbjI9Mv+Uw== - dependencies: - "@babel/types" "^7.8.3" - -"@babel/helper-annotate-as-pure@^7.10.4": +"@babel/helper-annotate-as-pure@^7.0.0", "@babel/helper-annotate-as-pure@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3" integrity sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA== @@ -152,14 +87,6 @@ "@babel/helper-explode-assignable-expression" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-builder-binary-assignment-operator-visitor@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.8.3.tgz#c84097a427a061ac56a1c30ebf54b7b22d241503" - integrity sha512-5eFOm2SyFPK4Rh3XMMRDjN7lBH0orh3ss0g3rTYZnBQ+r6YPj7lgDyCvPphynHvUrobJmeMignBr6Acw9mAPlw== - dependencies: - "@babel/helper-explode-assignable-expression" "^7.8.3" - "@babel/types" "^7.8.3" - "@babel/helper-builder-react-jsx-experimental@^7.10.4": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.10.5.tgz#f35e956a19955ff08c1258e44a515a6d6248646b" @@ -169,15 +96,6 @@ "@babel/helper-module-imports" "^7.10.4" "@babel/types" "^7.10.5" -"@babel/helper-builder-react-jsx-experimental@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.9.0.tgz#066d80262ade488f9c1b1823ce5db88a4cedaa43" - integrity sha512-3xJEiyuYU4Q/Ar9BsHisgdxZsRlsShMe90URZ0e6przL26CCs8NJbDoxH94kKT17PcxlMhsCAwZd90evCo26VQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.8.3" - "@babel/helper-module-imports" "^7.8.3" - "@babel/types" "^7.9.0" - "@babel/helper-builder-react-jsx@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.10.4.tgz#8095cddbff858e6fa9c326daee54a2f2732c1d5d" @@ -186,14 +104,6 @@ "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-builder-react-jsx@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.9.0.tgz#16bf391990b57732700a3278d4d9a81231ea8d32" - integrity sha512-weiIo4gaoGgnhff54GQ3P5wsUQmnSwpkvU0r6ZHq6TzoSzKy4JxHEgnxNytaKbov2a9z/CVNyzliuCOUPEX3Jw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.8.3" - "@babel/types" "^7.9.0" - "@babel/helper-compilation-targets@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.4.tgz#804ae8e3f04376607cc791b9d47d540276332bd2" @@ -205,17 +115,6 @@ levenary "^1.1.1" semver "^5.5.0" -"@babel/helper-compilation-targets@^7.8.7": - version "7.8.7" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.8.7.tgz#dac1eea159c0e4bd46e309b5a1b04a66b53c1dde" - integrity sha512-4mWm8DCK2LugIS+p1yArqvG1Pf162upsIsjE7cNBjez+NjliQpVhj20obE520nao0o14DaTnFJv+Fw5a0JpoUw== - dependencies: - "@babel/compat-data" "^7.8.6" - browserslist "^4.9.1" - invariant "^2.2.4" - levenary "^1.1.1" - semver "^5.5.0" - "@babel/helper-create-class-features-plugin@^7.10.4", "@babel/helper-create-class-features-plugin@^7.10.5": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.5.tgz#9f61446ba80e8240b0a5c85c6fdac8459d6f259d" @@ -228,18 +127,6 @@ "@babel/helper-replace-supers" "^7.10.4" "@babel/helper-split-export-declaration" "^7.10.4" -"@babel/helper-create-class-features-plugin@^7.8.3": - version "7.8.6" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.8.6.tgz#243a5b46e2f8f0f674dc1387631eb6b28b851de0" - integrity sha512-klTBDdsr+VFFqaDHm5rR69OpEQtO2Qv8ECxHS1mNhJJvaHArR6a1xTf5K/eZW7eZpJbhCx3NW1Yt/sKsLXLblg== - dependencies: - "@babel/helper-function-name" "^7.8.3" - "@babel/helper-member-expression-to-functions" "^7.8.3" - "@babel/helper-optimise-call-expression" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-replace-supers" "^7.8.6" - "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/helper-create-regexp-features-plugin@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz#fdd60d88524659a0b6959c0579925e425714f3b8" @@ -249,15 +136,6 @@ "@babel/helper-regex" "^7.10.4" regexpu-core "^4.7.0" -"@babel/helper-create-regexp-features-plugin@^7.8.3", "@babel/helper-create-regexp-features-plugin@^7.8.8": - version "7.8.8" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.8.tgz#5d84180b588f560b7864efaeea89243e58312087" - integrity sha512-LYVPdwkrQEiX9+1R29Ld/wTrmQu1SSKYnuOk3g0CkcZMA1p0gsNxJFj/3gBdaJ7Cg0Fnek5z0DsMULePP7Lrqg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.8.3" - "@babel/helper-regex" "^7.8.3" - regexpu-core "^4.7.0" - "@babel/helper-define-map@^7.10.4": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz#b53c10db78a640800152692b13393147acb9bb30" @@ -267,15 +145,6 @@ "@babel/types" "^7.10.5" lodash "^4.17.19" -"@babel/helper-define-map@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.8.3.tgz#a0655cad5451c3760b726eba875f1cd8faa02c15" - integrity sha512-PoeBYtxoZGtct3md6xZOCWPcKuMuk3IHhgxsRRNtnNShebf4C8YonTSblsK4tvDbm+eJAw2HAPOfCr+Q/YRG/g== - dependencies: - "@babel/helper-function-name" "^7.8.3" - "@babel/types" "^7.8.3" - lodash "^4.17.13" - "@babel/helper-explode-assignable-expression@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.10.4.tgz#40a1cd917bff1288f699a94a75b37a1a2dbd8c7c" @@ -284,14 +153,6 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-explode-assignable-expression@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.8.3.tgz#a728dc5b4e89e30fc2dfc7d04fa28a930653f982" - integrity sha512-N+8eW86/Kj147bO9G2uclsg5pwfs/fqqY5rwgIL7eTBklgXjcOJ3btzS5iM6AitJcftnY7pm2lGsrJVYLGjzIw== - dependencies: - "@babel/traverse" "^7.8.3" - "@babel/types" "^7.8.3" - "@babel/helper-function-name@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" @@ -301,24 +162,6 @@ "@babel/template" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-function-name@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz#eeeb665a01b1f11068e9fb86ad56a1cb1a824cca" - integrity sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA== - dependencies: - "@babel/helper-get-function-arity" "^7.8.3" - "@babel/template" "^7.8.3" - "@babel/types" "^7.8.3" - -"@babel/helper-function-name@^7.9.5": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz#2b53820d35275120e1874a82e5aabe1376920a5c" - integrity sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw== - dependencies: - "@babel/helper-get-function-arity" "^7.8.3" - "@babel/template" "^7.8.3" - "@babel/types" "^7.9.5" - "@babel/helper-get-function-arity@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" @@ -326,13 +169,6 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-get-function-arity@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5" - integrity sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA== - dependencies: - "@babel/types" "^7.8.3" - "@babel/helper-hoist-variables@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz#d49b001d1d5a68ca5e6604dda01a6297f7c9381e" @@ -340,13 +176,6 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-hoist-variables@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.8.3.tgz#1dbe9b6b55d78c9b4183fc8cdc6e30ceb83b7134" - integrity sha512-ky1JLOjcDUtSc+xkt0xhYff7Z6ILTAHKmZLHPxAhOP0Nd77O+3nCsd6uSVYur6nJnCI029CrNbYlc0LoPfAPQg== - dependencies: - "@babel/types" "^7.8.3" - "@babel/helper-member-expression-to-functions@^7.10.4", "@babel/helper-member-expression-to-functions@^7.10.5": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz#ae69c83d84ee82f4b42f96e2a09410935a8f26df" @@ -354,21 +183,7 @@ dependencies: "@babel/types" "^7.11.0" -"@babel/helper-member-expression-to-functions@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz#659b710498ea6c1d9907e0c73f206eee7dadc24c" - integrity sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA== - dependencies: - "@babel/types" "^7.8.3" - -"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz#7fe39589b39c016331b6b8c3f441e8f0b1419498" - integrity sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg== - dependencies: - "@babel/types" "^7.8.3" - -"@babel/helper-module-imports@^7.10.4": +"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620" integrity sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw== @@ -388,19 +203,6 @@ "@babel/types" "^7.11.0" lodash "^4.17.19" -"@babel/helper-module-transforms@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz#43b34dfe15961918707d247327431388e9fe96e5" - integrity sha512-0FvKyu0gpPfIQ8EkxlrAydOWROdHpBmiCiRwLkUiBGhCUPRRbVD2/tm3sFr/c/GWFrQ/ffutGUAnx7V0FzT2wA== - dependencies: - "@babel/helper-module-imports" "^7.8.3" - "@babel/helper-replace-supers" "^7.8.6" - "@babel/helper-simple-access" "^7.8.3" - "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/template" "^7.8.6" - "@babel/types" "^7.9.0" - lodash "^4.17.13" - "@babel/helper-optimise-call-expression@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" @@ -408,19 +210,7 @@ dependencies: "@babel/types" "^7.10.4" -"@babel/helper-optimise-call-expression@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz#7ed071813d09c75298ef4f208956006b6111ecb9" - integrity sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ== - dependencies: - "@babel/types" "^7.8.3" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670" - integrity sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ== - -"@babel/helper-plugin-utils@^7.10.4": +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== @@ -432,13 +222,6 @@ dependencies: lodash "^4.17.19" -"@babel/helper-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.8.3.tgz#139772607d51b93f23effe72105b319d2a4c6965" - integrity sha512-BWt0QtYv/cg/NecOAZMdcn/waj/5P26DR4mVLXfFtDokSR6fyuG0Pj+e2FqtSME+MqED1khnSMulkmGl8qWiUQ== - dependencies: - lodash "^4.17.13" - "@babel/helper-remap-async-to-generator@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.10.4.tgz#fce8bea4e9690bbe923056ded21e54b4e8b68ed5" @@ -450,17 +233,6 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-remap-async-to-generator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.8.3.tgz#273c600d8b9bf5006142c1e35887d555c12edd86" - integrity sha512-kgwDmw4fCg7AVgS4DukQR/roGp+jP+XluJE5hsRZwxCYGg+Rv9wSGErDWhlI90FODdYfd4xG4AQRiMDjjN0GzA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.8.3" - "@babel/helper-wrap-function" "^7.8.3" - "@babel/template" "^7.8.3" - "@babel/traverse" "^7.8.3" - "@babel/types" "^7.8.3" - "@babel/helper-replace-supers@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf" @@ -471,16 +243,6 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-replace-supers@^7.8.3", "@babel/helper-replace-supers@^7.8.6": - version "7.8.6" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz#5ada744fd5ad73203bf1d67459a27dcba67effc8" - integrity sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.8.3" - "@babel/helper-optimise-call-expression" "^7.8.3" - "@babel/traverse" "^7.8.6" - "@babel/types" "^7.8.6" - "@babel/helper-simple-access@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461" @@ -489,14 +251,6 @@ "@babel/template" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-simple-access@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz#7f8109928b4dab4654076986af575231deb639ae" - integrity sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw== - dependencies: - "@babel/template" "^7.8.3" - "@babel/types" "^7.8.3" - "@babel/helper-skip-transparent-expression-wrappers@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.11.0.tgz#eec162f112c2f58d3af0af125e3bb57665146729" @@ -511,28 +265,11 @@ dependencies: "@babel/types" "^7.11.0" -"@babel/helper-split-export-declaration@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz#31a9f30070f91368a7182cf05f831781065fc7a9" - integrity sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA== - dependencies: - "@babel/types" "^7.8.3" - "@babel/helper-validator-identifier@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== -"@babel/helper-validator-identifier@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz#ad53562a7fc29b3b9a91bbf7d10397fd146346ed" - integrity sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw== - -"@babel/helper-validator-identifier@^7.9.5": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz#90977a8e6fbf6b431a7dc31752eee233bf052d80" - integrity sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g== - "@babel/helper-wrap-function@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz#8a6f701eab0ff39f765b5a1cfef409990e624b87" @@ -543,16 +280,6 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-wrap-function@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz#9dbdb2bb55ef14aaa01fe8c99b629bd5352d8610" - integrity sha512-LACJrbUET9cQDzb6kG7EeD7+7doC3JNvUgTEQOx2qaO1fKlzE/Bf05qs9w1oXQMmXlPO65lC3Tq9S6gZpTErEQ== - dependencies: - "@babel/helper-function-name" "^7.8.3" - "@babel/template" "^7.8.3" - "@babel/traverse" "^7.8.3" - "@babel/types" "^7.8.3" - "@babel/helpers@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.4.tgz#2abeb0d721aff7c0a97376b9e1f6f65d7a475044" @@ -562,25 +289,7 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helpers@^7.9.0": - version "7.9.2" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.9.2.tgz#b42a81a811f1e7313b88cba8adc66b3d9ae6c09f" - integrity sha512-JwLvzlXVPjO8eU9c/wF9/zOIN7X6h8DYf7mG4CiFRZRvZNKEF5dQ3H3V+ASkHoIB3mWhatgl5ONhyqHRI6MppA== - dependencies: - "@babel/template" "^7.8.3" - "@babel/traverse" "^7.9.0" - "@babel/types" "^7.9.0" - -"@babel/highlight@^7.0.0", "@babel/highlight@^7.8.3": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.9.0.tgz#4e9b45ccb82b79607271b2979ad82c7b68163079" - integrity sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ== - dependencies: - "@babel/helper-validator-identifier" "^7.9.0" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/highlight@^7.10.4": +"@babel/highlight@^7.0.0", "@babel/highlight@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== @@ -589,15 +298,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.2.0", "@babel/parser@^7.7.5", "@babel/parser@^7.8.6", "@babel/parser@^7.9.0": - version "7.9.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8" - integrity sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA== - -"@babel/parser@^7.10.4", "@babel/parser@^7.11.0", "@babel/parser@^7.11.1", "@babel/parser@^7.11.2": - version "7.11.2" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.2.tgz#0882ab8a455df3065ea2dcb4c753b2460a24bead" - integrity sha512-Vuj/+7vLo6l1Vi7uuO+1ngCDNeVmNbTngcJFKCR/oEtz8tKz0CJxZEGmPt9KcIloZhOZ3Zit6xbpXT2MDlS9Vw== +"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.11.0", "@babel/parser@^7.11.1", "@babel/parser@^7.11.2", "@babel/parser@^7.2.0", "@babel/parser@^7.7.5": + version "7.11.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.3.tgz#9e1eae46738bcd08e23e867bab43e7b95299a8f9" + integrity sha512-REo8xv7+sDxkKvoxEywIdsNFiZLybwdI7hcT5uEPyQrSMB4YQ973BfC9OOrD/81MaIjh6UxdulIQXkjmiH3PcA== "@babel/plugin-proposal-async-generator-functions@^7.10.4": version "7.10.5" @@ -608,16 +312,7 @@ "@babel/helper-remap-async-to-generator" "^7.10.4" "@babel/plugin-syntax-async-generators" "^7.8.0" -"@babel/plugin-proposal-async-generator-functions@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.8.3.tgz#bad329c670b382589721b27540c7d288601c6e6f" - integrity sha512-NZ9zLv848JsV3hs8ryEh7Uaz/0KsmPLqv0+PdkDJL1cJy0K4kOCFa8zc1E3mp+RHPQcpdfb/6GovEsW4VDrOMw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-remap-async-to-generator" "^7.8.3" - "@babel/plugin-syntax-async-generators" "^7.8.0" - -"@babel/plugin-proposal-class-properties@^7.10.4": +"@babel/plugin-proposal-class-properties@^7.10.4", "@babel/plugin-proposal-class-properties@^7.7.0": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz#a33bf632da390a59c7a8c570045d1115cd778807" integrity sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg== @@ -625,14 +320,6 @@ "@babel/helper-create-class-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-proposal-class-properties@^7.7.0": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.8.3.tgz#5e06654af5cd04b608915aada9b2a6788004464e" - integrity sha512-EqFhbo7IosdgPgZggHaNObkmO1kNUe3slaKu54d5OWvy+p9QIKOzK1GAEpAIsZtWVtPXUHSMcT4smvDrCfY4AA== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-proposal-dynamic-import@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.4.tgz#ba57a26cb98b37741e9d5bca1b8b0ddf8291f17e" @@ -641,14 +328,6 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-dynamic-import" "^7.8.0" -"@babel/plugin-proposal-dynamic-import@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.8.3.tgz#38c4fe555744826e97e2ae930b0fb4cc07e66054" - integrity sha512-NyaBbyLFXFLT9FP+zk0kYlUlA8XtCUbehs67F0nnEg7KICgMc2mNkIeu9TYhKzyXMkrapZFwAhXLdnt4IYHy1w== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-dynamic-import" "^7.8.0" - "@babel/plugin-proposal-export-namespace-from@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.10.4.tgz#570d883b91031637b3e2958eea3c438e62c05f54" @@ -665,14 +344,6 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-json-strings" "^7.8.0" -"@babel/plugin-proposal-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.8.3.tgz#da5216b238a98b58a1e05d6852104b10f9a70d6b" - integrity sha512-KGhQNZ3TVCQG/MjRbAUwuH+14y9q0tpxs1nWWs3pbSleRdDro9SAMMDyye8HhY1gqZ7/NqIc8SKhya0wRDgP1Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-json-strings" "^7.8.0" - "@babel/plugin-proposal-logical-assignment-operators@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.11.0.tgz#9f80e482c03083c87125dee10026b58527ea20c8" @@ -689,14 +360,6 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" -"@babel/plugin-proposal-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.8.3.tgz#e4572253fdeed65cddeecfdab3f928afeb2fd5d2" - integrity sha512-TS9MlfzXpXKt6YYomudb/KU7nQI6/xnapG6in1uZxoxDghuSMZsPb6D2fyUwNYSAp4l1iR7QtFOjkqcRYcUsfw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" - "@babel/plugin-proposal-numeric-separator@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.4.tgz#ce1590ff0a65ad12970a609d78855e9a4c1aef06" @@ -705,15 +368,7 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-numeric-separator" "^7.10.4" -"@babel/plugin-proposal-numeric-separator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.8.3.tgz#5d6769409699ec9b3b68684cd8116cedff93bad8" - integrity sha512-jWioO1s6R/R+wEHizfaScNsAx+xKgwTLNXSh7tTC4Usj3ItsPEhYkEpU4h+lpnBwq7NBVOJXfO6cRFYcX69JUQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.8.3" - -"@babel/plugin-proposal-object-rest-spread@^7.11.0": +"@babel/plugin-proposal-object-rest-spread@^7.11.0", "@babel/plugin-proposal-object-rest-spread@^7.6.2": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.11.0.tgz#bd81f95a1f746760ea43b6c2d3d62b11790ad0af" integrity sha512-wzch41N4yztwoRw0ak+37wxwJM2oiIiy6huGCoqkvSTA9acYWcPfn9Y4aJqmFFJ70KTJUu29f3DQ43uJ9HXzEA== @@ -722,14 +377,6 @@ "@babel/plugin-syntax-object-rest-spread" "^7.8.0" "@babel/plugin-transform-parameters" "^7.10.4" -"@babel/plugin-proposal-object-rest-spread@^7.6.2", "@babel/plugin-proposal-object-rest-spread@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.9.0.tgz#a28993699fc13df165995362693962ba6b061d6f" - integrity sha512-UgqBv6bjq4fDb8uku9f+wcm1J7YxJ5nT7WO/jBr0cl0PLKb7t1O6RNR1kZbjgx2LQtsDI9hwoQVmn0yhXeQyow== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-object-rest-spread" "^7.8.0" - "@babel/plugin-proposal-optional-catch-binding@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.4.tgz#31c938309d24a78a49d68fdabffaa863758554dd" @@ -738,14 +385,6 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" -"@babel/plugin-proposal-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.8.3.tgz#9dee96ab1650eed88646ae9734ca167ac4a9c5c9" - integrity sha512-0gkX7J7E+AtAw9fcwlVQj8peP61qhdg/89D5swOkjYbkboA2CVckn3kiyum1DE0wskGb7KJJxBdyEBApDLLVdw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" - "@babel/plugin-proposal-optional-chaining@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.11.0.tgz#de5866d0646f6afdaab8a566382fe3a221755076" @@ -755,14 +394,6 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" "@babel/plugin-syntax-optional-chaining" "^7.8.0" -"@babel/plugin-proposal-optional-chaining@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.9.0.tgz#31db16b154c39d6b8a645292472b98394c292a58" - integrity sha512-NDn5tu3tcv4W30jNhmc2hyD5c56G6cXx4TesJubhxrJeCvuuMpttxr0OnNCqbZGhFjLrg+NIhxxC+BK5F6yS3w== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.0" - "@babel/plugin-proposal-private-methods@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.4.tgz#b160d972b8fdba5c7d111a145fc8c421fc2a6909" @@ -771,7 +402,7 @@ "@babel/helper-create-class-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-proposal-unicode-property-regex@^7.10.4": +"@babel/plugin-proposal-unicode-property-regex@^7.10.4", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz#4483cda53041ce3413b7fe2f00022665ddfaa75d" integrity sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA== @@ -779,14 +410,6 @@ "@babel/helper-create-regexp-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-proposal-unicode-property-regex@^7.4.4", "@babel/plugin-proposal-unicode-property-regex@^7.8.3": - version "7.8.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.8.tgz#ee3a95e90cdc04fe8cd92ec3279fa017d68a0d1d" - integrity sha512-EVhjVsMpbhLw9ZfHWSx2iy13Q8Z/eg8e8ccVWt23sWQK5l1UdkoLJPN5w69UA4uITGBnEZD2JOe4QOHycYKv8A== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.8.8" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-async-generators@^7.8.0", "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" @@ -801,20 +424,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-class-properties@^7.10.4": +"@babel/plugin-syntax-class-properties@^7.10.4", "@babel/plugin-syntax-class-properties@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz#6644e6a0baa55a61f9e3231f6c9eeb6ee46c124c" integrity sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-class-properties@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.8.3.tgz#6cb933a8872c8d359bfde69bbeaae5162fd1e8f7" - integrity sha512-UcAyQWg2bAN647Q+O811tG9MrJ38Z10jjhQdKNAL8fsyPzE3cCN/uT+f55cFVY4aGO4jqJAvmqsuY3GQDwAoXg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-dynamic-import@^7.2.0", "@babel/plugin-syntax-dynamic-import@^7.8.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" @@ -829,12 +445,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-syntax-flow@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.8.3.tgz#f2c883bd61a6316f2c89380ae5122f923ba4527f" - integrity sha512-innAx3bUbA0KSYj2E2MNFSn9hiCeowOFLxlsuhXzw8hMQnzkDomUr9QCD7E9VF60NmnG1sNTuuv6Qf4f8INYsg== +"@babel/plugin-syntax-flow@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.10.4.tgz#53351dd7ae01995e567d04ce42af1a6e0ba846a6" + integrity sha512-yxQsX1dJixF4qEEdzVbst3SZQ58Nrooz8NV9Z9GL4byTE25BvJgl5lf0RECUf0fh28rZBb/RYTWn/eeKwCMrZQ== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-json-strings@^7.8.0", "@babel/plugin-syntax-json-strings@^7.8.3": version "7.8.3" @@ -850,27 +466,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-jsx@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.8.3.tgz#521b06c83c40480f1e58b4fd33b92eceb1d6ea94" - integrity sha512-WxdW9xyLgBdefoo0Ynn3MRSkhe5tFVxxKNVdnZSh318WrG2e2jH+E9wd/++JsqcLJZPfz87njQJ8j2Upjm0M0A== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-logical-assignment-operators@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.8.3.tgz#3995d7d7ffff432f6ddc742b47e730c054599897" - integrity sha512-Zpg2Sgc++37kuFl6ppq2Q7Awc6E6AIW671x5PY8E/f7MCIyPPGK/EoeZXvvY3P42exZ3Q4/t3YOzP/HiN79jDg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0", "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" @@ -878,20 +480,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-numeric-separator@^7.10.4": +"@babel/plugin-syntax-numeric-separator@^7.10.4", "@babel/plugin-syntax-numeric-separator@^7.8.3": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-numeric-separator@^7.8.0", "@babel/plugin-syntax-numeric-separator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.8.3.tgz#0e3fb63e09bea1b11e96467271c8308007e7c41f" - integrity sha512-H7dCMAdN83PcCmqmkHB5dtp+Xa9a6LKSvA2hiFBC/5alSHxM5VgWZXFqDi0YFe8XNGT6iCa+z4V4zSt/PdZ7Dw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-object-rest-spread@^7.8.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" @@ -920,13 +515,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-top-level-await@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.8.3.tgz#3acdece695e6b13aaf57fc291d1a800950c71391" - integrity sha512-kwj1j9lL/6Wd0hROD3b/OZZ7MSrZLqqn9RAZ5+cYYsflQ9HZBIKCUkr3+uL1MEJ1NePiUbf98jjiMQSv0NMR9g== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-typescript@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.10.4.tgz#2f55e770d3501e83af217d782cb7517d7bb34d25" @@ -941,13 +529,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-arrow-functions@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.8.3.tgz#82776c2ed0cd9e1a49956daeb896024c9473b8b6" - integrity sha512-0MRF+KC8EqH4dbuITCWwPSzsyO3HIWWlm30v8BbbpOrS1B++isGxPnnuq/IZvOX5J2D/p7DQalQm+/2PnlKGxg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-async-to-generator@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.4.tgz#41a5017e49eb6f3cda9392a51eef29405b245a37" @@ -957,15 +538,6 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-remap-async-to-generator" "^7.10.4" -"@babel/plugin-transform-async-to-generator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.8.3.tgz#4308fad0d9409d71eafb9b1a6ee35f9d64b64086" - integrity sha512-imt9tFLD9ogt56Dd5CI/6XgpukMwd/fLGSrix2httihVe7LOGVPhyhMh1BU5kDM7iHD08i8uUtmV2sWaBFlHVQ== - dependencies: - "@babel/helper-module-imports" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-remap-async-to-generator" "^7.8.3" - "@babel/plugin-transform-block-scoped-functions@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.4.tgz#1afa595744f75e43a91af73b0d998ecfe4ebc2e8" @@ -973,13 +545,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-block-scoped-functions@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.8.3.tgz#437eec5b799b5852072084b3ae5ef66e8349e8a3" - integrity sha512-vo4F2OewqjbB1+yaJ7k2EJFHlTP3jR634Z9Cj9itpqNjuLXvhlVxgnjsHsdRgASR8xYDrx6onw4vW5H6We0Jmg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-block-scoping@^7.10.4": version "7.11.1" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.11.1.tgz#5b7efe98852bef8d652c0b28144cd93a9e4b5215" @@ -987,14 +552,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-block-scoping@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.8.3.tgz#97d35dab66857a437c166358b91d09050c868f3a" - integrity sha512-pGnYfm7RNRgYRi7bids5bHluENHqJhrV4bCZRwc5GamaWIIs07N4rZECcmJL6ZClwjDz1GbdMZFtPs27hTB06w== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - lodash "^4.17.13" - "@babel/plugin-transform-classes@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.4.tgz#405136af2b3e218bc4a1926228bc917ab1a0adc7" @@ -1009,20 +566,6 @@ "@babel/helper-split-export-declaration" "^7.10.4" globals "^11.1.0" -"@babel/plugin-transform-classes@^7.9.0": - version "7.9.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.9.2.tgz#8603fc3cc449e31fdbdbc257f67717536a11af8d" - integrity sha512-TC2p3bPzsfvSsqBZo0kJnuelnoK9O3welkUpqSqBQuBF6R5MN2rysopri8kNvtlGIb2jmUO7i15IooAZJjZuMQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.8.3" - "@babel/helper-define-map" "^7.8.3" - "@babel/helper-function-name" "^7.8.3" - "@babel/helper-optimise-call-expression" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-replace-supers" "^7.8.6" - "@babel/helper-split-export-declaration" "^7.8.3" - globals "^11.1.0" - "@babel/plugin-transform-computed-properties@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.4.tgz#9ded83a816e82ded28d52d4b4ecbdd810cdfc0eb" @@ -1030,13 +573,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-computed-properties@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.8.3.tgz#96d0d28b7f7ce4eb5b120bb2e0e943343c86f81b" - integrity sha512-O5hiIpSyOGdrQZRQ2ccwtTVkgUDBBiCuK//4RJ6UfePllUTCENOzKxfh6ulckXKc0DixTFLCfb2HVkNA7aDpzA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-destructuring@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.4.tgz#70ddd2b3d1bea83d01509e9bb25ddb3a74fc85e5" @@ -1044,14 +580,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-destructuring@^7.8.3": - version "7.8.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.8.8.tgz#fadb2bc8e90ccaf5658de6f8d4d22ff6272a2f4b" - integrity sha512-eRJu4Vs2rmttFCdhPUM3bV0Yo/xPSdPw6ML9KHs/bjB4bLA5HXlbvYXPOD5yASodGod+krjYx21xm1QmL8dCJQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-transform-dotall-regex@^7.10.4": +"@babel/plugin-transform-dotall-regex@^7.10.4", "@babel/plugin-transform-dotall-regex@^7.4.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz#469c2062105c1eb6a040eaf4fac4b488078395ee" integrity sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA== @@ -1059,14 +588,6 @@ "@babel/helper-create-regexp-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-dotall-regex@^7.4.4", "@babel/plugin-transform-dotall-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.8.3.tgz#c3c6ec5ee6125c6993c5cbca20dc8621a9ea7a6e" - integrity sha512-kLs1j9Nn4MQoBYdRXH6AeaXMbEJFaFu/v1nQkvib6QzTj8MZI5OQzqmD83/2jEM1z0DLilra5aWO5YpyC0ALIw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-duplicate-keys@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.4.tgz#697e50c9fee14380fe843d1f306b295617431e47" @@ -1074,13 +595,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-duplicate-keys@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.8.3.tgz#8d12df309aa537f272899c565ea1768e286e21f1" - integrity sha512-s8dHiBUbcbSgipS4SMFuWGqCvyge5V2ZeAWzR6INTVC3Ltjig/Vw1G2Gztv0vU/hRG9X8IvKvYdoksnUfgXOEQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-exponentiation-operator@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.4.tgz#5ae338c57f8cf4001bdb35607ae66b92d665af2e" @@ -1089,21 +603,13 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-exponentiation-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.8.3.tgz#581a6d7f56970e06bf51560cd64f5e947b70d7b7" - integrity sha512-zwIpuIymb3ACcInbksHaNcR12S++0MDLKkiqXHl3AzpgdKlFNhog+z/K0+TGW+b0w5pgTq4H6IwV/WhxbGYSjQ== - dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-transform-flow-strip-types@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.9.0.tgz#8a3538aa40434e000b8f44a3c5c9ac7229bd2392" - integrity sha512-7Qfg0lKQhEHs93FChxVLAvhBshOPQDtJUTVHr/ZwQNRccCm4O9D79r9tVSoV8iNwjP1YgfD+e/fgHcPkN1qEQg== +"@babel/plugin-transform-flow-strip-types@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.10.4.tgz#c497957f09e86e3df7296271e9eb642876bf7788" + integrity sha512-XTadyuqNst88UWBTdLjM+wEY7BFnY2sYtPyAidfC7M/QaZnSuIZpMvLxqGT7phAcnGyWh/XQFLKcGf04CnvxSQ== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-flow" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-flow" "^7.10.4" "@babel/plugin-transform-for-of@^7.10.4": version "7.10.4" @@ -1112,13 +618,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-for-of@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.9.0.tgz#0f260e27d3e29cd1bb3128da5e76c761aa6c108e" - integrity sha512-lTAnWOpMwOXpyDx06N+ywmF3jNbafZEqZ96CGYabxHrxNX8l5ny7dt4bK/rGwAh9utyP2b2Hv7PlZh1AAS54FQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-function-name@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.4.tgz#6a467880e0fc9638514ba369111811ddbe2644b7" @@ -1127,14 +626,6 @@ "@babel/helper-function-name" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-function-name@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.8.3.tgz#279373cb27322aaad67c2683e776dfc47196ed8b" - integrity sha512-rO/OnDS78Eifbjn5Py9v8y0aR+aSYhDhqAwVfsTl0ERuMZyr05L1aFSCJnbv2mmsLkit/4ReeQ9N2BgLnOcPCQ== - dependencies: - "@babel/helper-function-name" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-literals@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.4.tgz#9f42ba0841100a135f22712d0e391c462f571f3c" @@ -1142,13 +633,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-literals@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.8.3.tgz#aef239823d91994ec7b68e55193525d76dbd5dc1" - integrity sha512-3Tqf8JJ/qB7TeldGl+TT55+uQei9JfYaregDcEAyBZ7akutriFrt6C/wLYIer6OYhleVQvH/ntEhjE/xMmy10A== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-member-expression-literals@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.4.tgz#b1ec44fcf195afcb8db2c62cd8e551c881baf8b7" @@ -1156,13 +640,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-member-expression-literals@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.8.3.tgz#963fed4b620ac7cbf6029c755424029fa3a40410" - integrity sha512-3Wk2EXhnw+rP+IDkK6BdtPKsUE5IeZ6QOGrPYvw52NwBStw9V1ZVzxgK6fSKSxqUvH9eQPR3tm3cOq79HlsKYA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-modules-amd@^7.10.4": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.5.tgz#1b9cddaf05d9e88b3aad339cb3e445c4f020a9b1" @@ -1172,15 +649,6 @@ "@babel/helper-plugin-utils" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-amd@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.9.0.tgz#19755ee721912cf5bb04c07d50280af3484efef4" - integrity sha512-vZgDDF003B14O8zJy0XXLnPH4sg+9X5hFBBGN1V+B2rgrB+J2xIypSN6Rk9imB2hSTHQi5OHLrFWsZab1GMk+Q== - dependencies: - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helper-plugin-utils" "^7.8.3" - babel-plugin-dynamic-import-node "^2.3.0" - "@babel/plugin-transform-modules-commonjs@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.4.tgz#66667c3eeda1ebf7896d41f1f16b17105a2fbca0" @@ -1191,16 +659,6 @@ "@babel/helper-simple-access" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.9.0.tgz#e3e72f4cbc9b4a260e30be0ea59bdf5a39748940" - integrity sha512-qzlCrLnKqio4SlgJ6FMMLBe4bySNis8DFn1VkGmOcxG9gqEyPIOzeQrA//u0HAKrWpJlpZbZMPB1n/OPa4+n8g== - dependencies: - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-simple-access" "^7.8.3" - babel-plugin-dynamic-import-node "^2.3.0" - "@babel/plugin-transform-modules-systemjs@^7.10.4": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.5.tgz#6270099c854066681bae9e05f87e1b9cadbe8c85" @@ -1211,16 +669,6 @@ "@babel/helper-plugin-utils" "^7.10.4" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-systemjs@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.9.0.tgz#e9fd46a296fc91e009b64e07ddaa86d6f0edeb90" - integrity sha512-FsiAv/nao/ud2ZWy4wFacoLOm5uxl0ExSQ7ErvP7jpoihLR6Cq90ilOFyX9UXct3rbtKsAiZ9kFt5XGfPe/5SQ== - dependencies: - "@babel/helper-hoist-variables" "^7.8.3" - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helper-plugin-utils" "^7.8.3" - babel-plugin-dynamic-import-node "^2.3.0" - "@babel/plugin-transform-modules-umd@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.4.tgz#9a8481fe81b824654b3a0b65da3df89f3d21839e" @@ -1229,14 +677,6 @@ "@babel/helper-module-transforms" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-modules-umd@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.9.0.tgz#e909acae276fec280f9b821a5f38e1f08b480697" - integrity sha512-uTWkXkIVtg/JGRSIABdBoMsoIeoHQHPTL0Y2E7xf5Oj7sLqwVsNXOkNk0VJc7vF0IMBsPeikHxFjGe+qmwPtTQ== - dependencies: - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-named-capturing-groups-regex@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.4.tgz#78b4d978810b6f3bcf03f9e318f2fc0ed41aecb6" @@ -1244,13 +684,6 @@ dependencies: "@babel/helper-create-regexp-features-plugin" "^7.10.4" -"@babel/plugin-transform-named-capturing-groups-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.8.3.tgz#a2a72bffa202ac0e2d0506afd0939c5ecbc48c6c" - integrity sha512-f+tF/8UVPU86TrCb06JoPWIdDpTNSGGcAtaD9mLP0aYGA0OS0j7j7DHJR0GTFrUZPUU6loZhbsVZgTh0N+Qdnw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.8.3" - "@babel/plugin-transform-new-target@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.4.tgz#9097d753cb7b024cb7381a3b2e52e9513a9c6888" @@ -1258,13 +691,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-new-target@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.8.3.tgz#60cc2ae66d85c95ab540eb34babb6434d4c70c43" - integrity sha512-QuSGysibQpyxexRyui2vca+Cmbljo8bcRckgzYV4kRIsHpVeyeC3JDO63pY+xFZ6bWOBn7pfKZTqV4o/ix9sFw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-object-super@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.4.tgz#d7146c4d139433e7a6526f888c667e314a093894" @@ -1273,14 +699,6 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-replace-supers" "^7.10.4" -"@babel/plugin-transform-object-super@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.8.3.tgz#ebb6a1e7a86ffa96858bd6ac0102d65944261725" - integrity sha512-57FXk+gItG/GejofIyLIgBKTas4+pEU47IXKDBWFTxdPd7F80H8zybyAY7UoblVfBhBGs2EKM+bJUu2+iUYPDQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-replace-supers" "^7.8.3" - "@babel/plugin-transform-parameters@^7.10.4": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.5.tgz#59d339d58d0b1950435f4043e74e2510005e2c4a" @@ -1289,14 +707,6 @@ "@babel/helper-get-function-arity" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-parameters@^7.8.7": - version "7.9.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.9.3.tgz#3028d0cc20ddc733166c6e9c8534559cee09f54a" - integrity sha512-fzrQFQhp7mIhOzmOtPiKffvCYQSK10NR8t6BBz2yPbeUHb9OLW8RZGtgDRBn8z2hGcwvKDL3vC7ojPTLNxmqEg== - dependencies: - "@babel/helper-get-function-arity" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-property-literals@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.4.tgz#f6fe54b6590352298785b83edd815d214c42e3c0" @@ -1304,19 +714,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-property-literals@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.8.3.tgz#33194300d8539c1ed28c62ad5087ba3807b98263" - integrity sha512-uGiiXAZMqEoQhRWMK17VospMZh5sXWg+dlh2soffpkAl96KAm+WZuJfa6lcELotSRmooLqg0MWdH6UUq85nmmg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-react-constant-elements@^7.0.0", "@babel/plugin-transform-react-constant-elements@^7.2.0", "@babel/plugin-transform-react-constant-elements@^7.6.3": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.9.0.tgz#a75abc936a3819edec42d3386d9f1c93f28d9d9e" - integrity sha512-wXMXsToAUOxJuBBEHajqKLFWcCkOSLshTI2ChCFFj1zDd7od4IOxiwLCOObNUvOpkxLpjIuaIdBMmNt6ocCPAw== + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.10.4.tgz#0f485260bf1c29012bb973e7e404749eaac12c9e" + integrity sha512-cYmQBW1pXrqBte1raMkAulXmi7rjg3VI6ZLg9QIic8Hq7BtYXaWuZSxsr2siOMI6SWwpxjWfnwhTUrd7JlAV7g== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-react-display-name@^7.10.4": version "7.10.4" @@ -1325,13 +728,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-react-display-name@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.8.3.tgz#70ded987c91609f78353dd76d2fb2a0bb991e8e5" - integrity sha512-3Jy/PCw8Fe6uBKtEgz3M82ljt+lTg+xJaM4og+eyu83qLT87ZUSckn0wy7r31jflURWLO83TW6Ylf7lyXj3m5A== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-react-jsx-development@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.10.4.tgz#6ec90f244394604623880e15ebc3c34c356258ba" @@ -1341,15 +737,6 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-jsx" "^7.10.4" -"@babel/plugin-transform-react-jsx-development@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.9.0.tgz#3c2a130727caf00c2a293f0aed24520825dbf754" - integrity sha512-tK8hWKrQncVvrhvtOiPpKrQjfNX3DtkNLSX4ObuGcpS9p0QrGetKmlySIGR07y48Zft8WVgPakqd/bk46JrMSw== - dependencies: - "@babel/helper-builder-react-jsx-experimental" "^7.9.0" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-jsx" "^7.8.3" - "@babel/plugin-transform-react-jsx-self@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.10.4.tgz#cd301a5fed8988c182ed0b9d55e9bd6db0bd9369" @@ -1358,14 +745,6 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-jsx" "^7.10.4" -"@babel/plugin-transform-react-jsx-self@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.9.0.tgz#f4f26a325820205239bb915bad8e06fcadabb49b" - integrity sha512-K2ObbWPKT7KUTAoyjCsFilOkEgMvFG+y0FqOl6Lezd0/13kMkkjHskVsZvblRPj1PHA44PrToaZANrryppzTvQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-jsx" "^7.8.3" - "@babel/plugin-transform-react-jsx-source@^7.10.4": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.10.5.tgz#34f1779117520a779c054f2cdd9680435b9222b4" @@ -1374,14 +753,6 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-jsx" "^7.10.4" -"@babel/plugin-transform-react-jsx-source@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.9.0.tgz#89ef93025240dd5d17d3122294a093e5e0183de0" - integrity sha512-K6m3LlSnTSfRkM6FcRk8saNEeaeyG5k7AVkBU2bZK3+1zdkSED3qNdsWrUgQBeTVD2Tp3VMmerxVO2yM5iITmw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-jsx" "^7.8.3" - "@babel/plugin-transform-react-jsx@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.10.4.tgz#673c9f913948764a4421683b2bef2936968fddf2" @@ -1392,16 +763,6 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-jsx" "^7.10.4" -"@babel/plugin-transform-react-jsx@^7.9.4": - version "7.9.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.9.4.tgz#86f576c8540bd06d0e95e0b61ea76d55f6cbd03f" - integrity sha512-Mjqf3pZBNLt854CK0C/kRuXAnE6H/bo7xYojP+WGtX8glDGSibcwnsWwhwoSuRg0+EBnxPC1ouVnuetUIlPSAw== - dependencies: - "@babel/helper-builder-react-jsx" "^7.9.0" - "@babel/helper-builder-react-jsx-experimental" "^7.9.0" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-jsx" "^7.8.3" - "@babel/plugin-transform-react-pure-annotations@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.10.4.tgz#3eefbb73db94afbc075f097523e445354a1c6501" @@ -1417,13 +778,6 @@ dependencies: regenerator-transform "^0.14.2" -"@babel/plugin-transform-regenerator@^7.8.7": - version "7.8.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.8.7.tgz#5e46a0dca2bee1ad8285eb0527e6abc9c37672f8" - integrity sha512-TIg+gAl4Z0a3WmD3mbYSk+J9ZUH6n/Yc57rtKRnlA/7rcCvpekHXe0CMZHP1gYp7/KLe9GHTuIba0vXmls6drA== - dependencies: - regenerator-transform "^0.14.2" - "@babel/plugin-transform-reserved-words@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.4.tgz#8f2682bcdcef9ed327e1b0861585d7013f8a54dd" @@ -1431,13 +785,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-reserved-words@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.8.3.tgz#9a0635ac4e665d29b162837dd3cc50745dfdf1f5" - integrity sha512-mwMxcycN3omKFDjDQUl+8zyMsBfjRFr0Zn/64I41pmjv4NJuqcYlEtezwYtw9TFd9WR1vN5kiM+O0gMZzO6L0A== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-runtime@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.11.0.tgz#e27f78eb36f19448636e05c33c90fd9ad9b8bccf" @@ -1455,13 +802,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-shorthand-properties@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.8.3.tgz#28545216e023a832d4d3a1185ed492bcfeac08c8" - integrity sha512-I9DI6Odg0JJwxCHzbzW08ggMdCezoWcuQRz3ptdudgwaHxTjxw5HgdFJmZIkIMlRymL6YiZcped4TTCB0JcC8w== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-spread@^7.11.0": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.11.0.tgz#fa84d300f5e4f57752fe41a6d1b3c554f13f17cc" @@ -1470,13 +810,6 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-skip-transparent-expression-wrappers" "^7.11.0" -"@babel/plugin-transform-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.8.3.tgz#9c8ffe8170fdfb88b114ecb920b82fb6e95fe5e8" - integrity sha512-CkuTU9mbmAoFOI1tklFWYYbzX5qCIZVXPVy0jpXgGwkplCndQAa58s2jr66fTeQnA64bDox0HL4U56CFYoyC7g== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-sticky-regex@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.4.tgz#8f3889ee8657581130a29d9cc91d7c73b7c4a28d" @@ -1485,14 +818,6 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-regex" "^7.10.4" -"@babel/plugin-transform-sticky-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.8.3.tgz#be7a1290f81dae767475452199e1f76d6175b100" - integrity sha512-9Spq0vGCD5Bb4Z/ZXXSK5wbbLFMG085qd2vhL1JYu1WcQ5bXqZBAYRzU1d+p79GcHs2szYv5pVQCX13QgldaWw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-regex" "^7.8.3" - "@babel/plugin-transform-template-literals@^7.10.4": version "7.10.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.5.tgz#78bc5d626a6642db3312d9d0f001f5e7639fde8c" @@ -1501,14 +826,6 @@ "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-template-literals@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.8.3.tgz#7bfa4732b455ea6a43130adc0ba767ec0e402a80" - integrity sha512-820QBtykIQOLFT8NZOcTRJ1UNuztIELe4p9DCgvj4NK+PwluSJ49we7s9FB1HIGNIYT7wFUJ0ar2QpCDj0escQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-typeof-symbol@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.4.tgz#9509f1a7eec31c4edbffe137c16cc33ff0bc5bfc" @@ -1516,13 +833,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-typeof-symbol@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.8.4.tgz#ede4062315ce0aaf8a657a920858f1a2f35fc412" - integrity sha512-2QKyfjGdvuNfHsb7qnBBlKclbD4CfshH2KvDabiijLMGXPHJXGxtDzwIF7bQP+T0ysw8fYTtxPafgfs/c1Lrqg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-typescript@^7.10.4": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.11.0.tgz#2b4879676af37342ebb278216dd090ac67f13abb" @@ -1547,81 +857,7 @@ "@babel/helper-create-regexp-features-plugin" "^7.10.4" "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-unicode-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.8.3.tgz#0cef36e3ba73e5c57273effb182f46b91a1ecaad" - integrity sha512-+ufgJjYdmWfSQ+6NS9VGUR2ns8cjJjYbrbi11mZBTaWm+Fui/ncTLFF28Ei1okavY+xkojGr1eJxNsWYeA5aZw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/preset-env@^7.0.0", "@babel/preset-env@^7.4.3", "@babel/preset-env@^7.4.5": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.9.0.tgz#a5fc42480e950ae8f5d9f8f2bbc03f52722df3a8" - integrity sha512-712DeRXT6dyKAM/FMbQTV/FvRCms2hPCx+3weRjZ8iQVQWZejWWk1wwG6ViWMyqb/ouBbGOl5b6aCk0+j1NmsQ== - dependencies: - "@babel/compat-data" "^7.9.0" - "@babel/helper-compilation-targets" "^7.8.7" - "@babel/helper-module-imports" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-proposal-async-generator-functions" "^7.8.3" - "@babel/plugin-proposal-dynamic-import" "^7.8.3" - "@babel/plugin-proposal-json-strings" "^7.8.3" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-proposal-numeric-separator" "^7.8.3" - "@babel/plugin-proposal-object-rest-spread" "^7.9.0" - "@babel/plugin-proposal-optional-catch-binding" "^7.8.3" - "@babel/plugin-proposal-optional-chaining" "^7.9.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.8.3" - "@babel/plugin-syntax-async-generators" "^7.8.0" - "@babel/plugin-syntax-dynamic-import" "^7.8.0" - "@babel/plugin-syntax-json-strings" "^7.8.0" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" - "@babel/plugin-syntax-numeric-separator" "^7.8.0" - "@babel/plugin-syntax-object-rest-spread" "^7.8.0" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" - "@babel/plugin-syntax-optional-chaining" "^7.8.0" - "@babel/plugin-syntax-top-level-await" "^7.8.3" - "@babel/plugin-transform-arrow-functions" "^7.8.3" - "@babel/plugin-transform-async-to-generator" "^7.8.3" - "@babel/plugin-transform-block-scoped-functions" "^7.8.3" - "@babel/plugin-transform-block-scoping" "^7.8.3" - "@babel/plugin-transform-classes" "^7.9.0" - "@babel/plugin-transform-computed-properties" "^7.8.3" - "@babel/plugin-transform-destructuring" "^7.8.3" - "@babel/plugin-transform-dotall-regex" "^7.8.3" - "@babel/plugin-transform-duplicate-keys" "^7.8.3" - "@babel/plugin-transform-exponentiation-operator" "^7.8.3" - "@babel/plugin-transform-for-of" "^7.9.0" - "@babel/plugin-transform-function-name" "^7.8.3" - "@babel/plugin-transform-literals" "^7.8.3" - "@babel/plugin-transform-member-expression-literals" "^7.8.3" - "@babel/plugin-transform-modules-amd" "^7.9.0" - "@babel/plugin-transform-modules-commonjs" "^7.9.0" - "@babel/plugin-transform-modules-systemjs" "^7.9.0" - "@babel/plugin-transform-modules-umd" "^7.9.0" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.8.3" - "@babel/plugin-transform-new-target" "^7.8.3" - "@babel/plugin-transform-object-super" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.8.7" - "@babel/plugin-transform-property-literals" "^7.8.3" - "@babel/plugin-transform-regenerator" "^7.8.7" - "@babel/plugin-transform-reserved-words" "^7.8.3" - "@babel/plugin-transform-shorthand-properties" "^7.8.3" - "@babel/plugin-transform-spread" "^7.8.3" - "@babel/plugin-transform-sticky-regex" "^7.8.3" - "@babel/plugin-transform-template-literals" "^7.8.3" - "@babel/plugin-transform-typeof-symbol" "^7.8.4" - "@babel/plugin-transform-unicode-regex" "^7.8.3" - "@babel/preset-modules" "^0.1.3" - "@babel/types" "^7.9.0" - browserslist "^4.9.1" - core-js-compat "^3.6.2" - invariant "^2.2.2" - levenary "^1.1.1" - semver "^5.5.0" - -"@babel/preset-env@^7.11.0": +"@babel/preset-env@^7.0.0", "@babel/preset-env@^7.11.0", "@babel/preset-env@^7.4.3", "@babel/preset-env@^7.4.5": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.11.0.tgz#860ee38f2ce17ad60480c2021ba9689393efb796" integrity sha512-2u1/k7rG/gTh02dylX2kL3S0IJNF+J6bfDSp4DI2Ma8QN6Y9x9pmAax59fsCk6QUQG0yqH47yJWA+u1I1LccAg== @@ -1696,12 +932,12 @@ semver "^5.5.0" "@babel/preset-flow@^7.0.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.9.0.tgz#fee847c3e090b0b2d9227c1949e4da1d1379280d" - integrity sha512-88uSmlshIrlmPkNkEcx3UpSZ6b8n0UGBq0/0ZMZCF/uxAW0XIAUuDHBhIOAh0pvweafH4RxOwi/H3rWhtqOYPA== + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.10.4.tgz#e0d9c72f8cb02d1633f6a5b7b16763aa2edf659f" + integrity sha512-XI6l1CptQCOBv+ZKYwynyswhtOKwpZZp5n0LG1QKCo8erRhqjoQV6nvx61Eg30JHpysWQSBwA2AWRU3pBbSY5g== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-flow-strip-types" "^7.9.0" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-flow-strip-types" "^7.10.4" "@babel/preset-modules@^0.1.3": version "0.1.3" @@ -1714,19 +950,7 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" -"@babel/preset-react@^7.0.0": - version "7.9.4" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.9.4.tgz#c6c97693ac65b6b9c0b4f25b948a8f665463014d" - integrity sha512-AxylVB3FXeOTQXNXyiuAQJSvss62FEotbX2Pzx3K/7c+MKJMdSg6Ose6QYllkdCFA8EInCJVw7M/o5QbLuA4ZQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-transform-react-display-name" "^7.8.3" - "@babel/plugin-transform-react-jsx" "^7.9.4" - "@babel/plugin-transform-react-jsx-development" "^7.9.0" - "@babel/plugin-transform-react-jsx-self" "^7.9.0" - "@babel/plugin-transform-react-jsx-source" "^7.9.0" - -"@babel/preset-react@^7.10.4": +"@babel/preset-react@^7.0.0", "@babel/preset-react@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.10.4.tgz#92e8a66d816f9911d11d4cc935be67adfc82dbcf" integrity sha512-BrHp4TgOIy4M19JAfO1LhycVXOPWdDbTRep7eVyatf174Hff+6Uk53sDyajqZPu8W1qXRBiYOfIamek6jA7YVw== @@ -1759,9 +983,9 @@ source-map-support "^0.5.16" "@babel/runtime-corejs2@^7.2.0", "@babel/runtime-corejs2@^7.6.3": - version "7.9.2" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.9.2.tgz#f11d074ff99b9b4319b5ecf0501f12202bf2bf4d" - integrity sha512-ayjSOxuK2GaSDJFCtLgHnYjuMyIpViNujWrZo8GUpN60/n7juzJKK5yOo6RFVb0zdU9ACJFK+MsZrUnj3OmXMw== + version "7.11.2" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.11.2.tgz#700a03945ebad0d31ba6690fc8a6bcc9040faa47" + integrity sha512-AC/ciV28adSSpEkBglONBWq4/Lvm6GAZuxIoyVtsnUpZMl0bxLtoChEnYAkP+47KyOCayZanojtflUEUJtR/6Q== dependencies: core-js "^2.6.5" regenerator-runtime "^0.13.4" @@ -1773,42 +997,19 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": - version "7.9.2" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06" - integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/runtime@^7.11.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.11.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.3.1", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.7": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.5.tgz#303d8bd440ecd5a491eae6117fd3367698674c5c" - integrity sha512-otddXKhdNn7d0ptoFRHtMLa8LqDxLYwTjB4nYgM1yy5N6gU/MUf8zqyyLltCH3yAVitBzmwK4us+DD0l/MauAg== - dependencies: - regenerator-runtime "^0.13.4" - "@babel/standalone@^7.4.5": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.10.5.tgz#4ee38dc79fda10a2a0da0897f09e270310151314" - integrity sha512-PERGHqhQ7H3TrdglvSW4pEHULywMJEdytnzaR0VPF1HN45aS+3FcE62efb90XPKS9TlgrEUkYDvYMt+0m6G0YA== - -"@babel/template@^7.0.0", "@babel/template@^7.3.3", "@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6": - version "7.8.6" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" - integrity sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg== - dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/parser" "^7.8.6" - "@babel/types" "^7.8.6" + version "7.11.3" + resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.11.3.tgz#043e6ff3b12226e41ed2013418b9a4c055d9c21e" + integrity sha512-rcoT32Hw0faYhmCDR0P84ODKL5kpEdhYPgdzlTKs7+v9oJaVLsGvq0xlkmLRj01F6LrItH3tY9eEoRsPLie4RQ== -"@babel/template@^7.10.4": +"@babel/template@^7.0.0", "@babel/template@^7.10.4", "@babel/template@^7.3.3", "@babel/template@^7.7.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== @@ -1817,22 +1018,7 @@ "@babel/parser" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.6", "@babel/traverse@^7.4.5", "@babel/traverse@^7.8.3", "@babel/traverse@^7.8.6", "@babel/traverse@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.0.tgz#d3882c2830e513f4fe4cec9fe76ea1cc78747892" - integrity sha512-jAZQj0+kn4WTHO5dUZkZKhbFrqZE7K5LAQ5JysMnmvGij+wOdr+8lWqPeW0BcF4wFwrEXXtdGO7wcV6YPJcf3w== - dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.9.0" - "@babel/helper-function-name" "^7.8.3" - "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/parser" "^7.9.0" - "@babel/types" "^7.9.0" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.13" - -"@babel/traverse@^7.10.4", "@babel/traverse@^7.11.0": +"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.6", "@babel/traverse@^7.10.4", "@babel/traverse@^7.11.0", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.4": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.0.tgz#9b996ce1b98f53f7c3e4175115605d56ed07dd24" integrity sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg== @@ -1847,31 +1033,7 @@ globals "^11.1.0" lodash "^4.17.19" -"@babel/traverse@^7.7.4": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.5.tgz#6e7c56b44e2ac7011a948c21e283ddd9d9db97a2" - integrity sha512-c4gH3jsvSuGUezlP6rzSJ6jf8fYjLj3hsMZRx/nX0h+fmHN0w+ekubRrHPqnMec0meycA2nwCsJ7dC8IPem2FQ== - dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.9.5" - "@babel/helper-function-name" "^7.9.5" - "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/parser" "^7.9.0" - "@babel/types" "^7.9.5" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.13" - -"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.0.tgz#00b064c3df83ad32b2dbf5ff07312b15c7f1efb5" - integrity sha512-BS9JKfXkzzJl8RluW4JGknzpiUV7ZrvTayM6yfqLTVBEnFtyowVIOu6rqxRd5cVO6yGoWf4T8u8dgK9oB+GCng== - dependencies: - "@babel/helper-validator-identifier" "^7.9.0" - lodash "^4.17.13" - to-fast-properties "^2.0.0" - -"@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0": +"@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4", "@babel/types@^7.4.0", "@babel/types@^7.4.4": version "7.11.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.0.tgz#2ae6bf1ba9ae8c3c43824e5861269871b206e90d" integrity sha512-O53yME4ZZI0jO1EVGtF1ePGl0LHirG4P1ibcD80XyzZcKhcMFeCXmh4Xb1ifGBIV233Qg12x4rBfQgA+tmOukA== @@ -1880,24 +1042,6 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" -"@babel/types@^7.3.3": - version "7.9.6" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.6.tgz#2c5502b427251e9de1bd2dff95add646d95cc9f7" - integrity sha512-qxXzvBO//jO9ZnoasKF1uJzHd2+M6Q2ZPIVfnFps8JJvXy0ZBbwbNOmE6SGIY5XOY6d1Bo5lb9d9RJ8nv3WSeA== - dependencies: - "@babel/helper-validator-identifier" "^7.9.5" - lodash "^4.17.13" - to-fast-properties "^2.0.0" - -"@babel/types@^7.4", "@babel/types@^7.9.5": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.5.tgz#89231f82915a8a566a703b3b20133f73da6b9444" - integrity sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg== - dependencies: - "@babel/helper-validator-identifier" "^7.9.5" - lodash "^4.17.13" - to-fast-properties "^2.0.0" - "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -7695,13 +6839,6 @@ babel-plugin-add-react-displayname@^0.0.5: resolved "https://registry.yarnpkg.com/babel-plugin-add-react-displayname/-/babel-plugin-add-react-displayname-0.0.5.tgz#339d4cddb7b65fd62d1df9db9fe04de134122bd5" integrity sha1-M51M3be2X9YtHfnbn+BN4TQSK9U= -babel-plugin-dynamic-import-node@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f" - integrity sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ== - dependencies: - object.assign "^4.1.0" - babel-plugin-dynamic-import-node@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" @@ -8653,16 +7790,6 @@ browserslist@^4.8.3: electron-to-chromium "^1.3.338" node-releases "^1.1.46" -browserslist@^4.9.1: - version "4.11.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.11.0.tgz#aef4357b10a8abda00f97aac7cd587b2082ba1ad" - integrity sha512-WqEC7Yr5wUH5sg6ruR++v2SGOQYpyUdYYd4tZoAq1F7y+QXoLoYGXVbxhtaIqWmAJjtNTRjVD3HuJc1OXTel2A== - dependencies: - caniuse-lite "^1.0.30001035" - electron-to-chromium "^1.3.380" - node-releases "^1.1.52" - pkg-up "^3.1.0" - bser@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719" @@ -8751,11 +7878,6 @@ builtin-status-codes@^3.0.0: resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= -builtins@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/builtins/-/builtins-0.0.7.tgz#355219cd6cf18dbe7c01cc7fd2dce765cfdc549a" - integrity sha1-NVIZzWzxjb58Acx/0tznZc/cVJo= - bytes@1: version "1.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-1.0.0.tgz#3569ede8ba34315fab99c3e92cb04c7220de1fa8" @@ -9089,7 +8211,7 @@ can-use-dom@^0.1.0: resolved "https://registry.yarnpkg.com/can-use-dom/-/can-use-dom-0.1.0.tgz#22cc4a34a0abc43950f42c6411024a3f6366b45a" integrity sha1-IsxKNKCrxDlQ9CxkEQJKP2NmtFo= -caniuse-lite@^1.0.30000984, caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001022, caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001043: +caniuse-lite@^1.0.30000984, caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001022, caniuse-lite@^1.0.30001043: version "1.0.30001094" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001094.tgz#0b11d02e1cdc201348dbd8e3e57bd9b6ce82b175" integrity sha512-ufHZNtMaDEuRBpTbqD93tIQnngmJ+oBknjvr0IbFympSdtFpAUFmNv4mVKbb53qltxFx0nK3iy32S9AqkLzUNA== @@ -12470,11 +11592,6 @@ electron-to-chromium@^1.3.191, electron-to-chromium@^1.3.338: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.340.tgz#5d4fe78e984d4211194cf5a52e08069543da146f" integrity sha512-hRFBAglhcj5iVYH+o8QU0+XId1WGoc0VGowJB1cuJAt3exHGrivZvWeAO5BRgBZqwZtwxjm8a5MQeGoT/Su3ww== -electron-to-chromium@^1.3.380: - version "1.3.382" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.382.tgz#cad02da655c33f7a3d6ca7525bd35c17e90f3a8f" - integrity sha512-gJfxOcgnBlXhfnUUObsq3n3ReU8CT6S8je97HndYRkKsNZMJJ38zO/pI5aqO7L3Myfq+E3pqPyKK/ynyLEQfBA== - electron-to-chromium@^1.3.413: version "1.3.465" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.465.tgz#d692e5c383317570c2bd82092a24a0308c6ccf29" @@ -14977,7 +14094,7 @@ get-own-enumerable-property-symbols@^3.0.0: resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.0.tgz#b877b49a5c16aefac3655f2ed2ea5b684df8d203" integrity sha512-CIJYJC4GGF06TakLg8z4GQKvDsx9EMspVxOYih7LerEL/WosUnFIww45CGfxfeKHqlg3twgUrYRT1O3WQqjGCg== -get-port@4.2.0: +get-port@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/get-port/-/get-port-4.2.0.tgz#e37368b1e863b7629c43c5a323625f95cf24b119" integrity sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw== @@ -22121,13 +21238,6 @@ node-releases@^1.1.25, node-releases@^1.1.46: dependencies: semver "^6.3.0" -node-releases@^1.1.52: - version "1.1.52" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.52.tgz#bcffee3e0a758e92e44ecfaecd0a47554b0bcba9" - integrity sha512-snSiT1UypkgGt2wxPqS6ImEUICbNCMb31yaxWrOLXjhlt2z2/IBpaOxzONExqSm4y5oLnAqjjRWu+wsDzK5yNQ== - dependencies: - semver "^6.3.0" - node-releases@^1.1.53: version "1.1.58" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.58.tgz#8ee20eef30fa60e52755fcc0942def5a734fe935" @@ -30844,13 +29954,6 @@ validate-npm-package-license@^3.0.1: spdx-correct "~1.0.0" spdx-expression-parse "~1.0.0" -validate-npm-package-name@2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-2.2.2.tgz#f65695b22f7324442019a3c7fa39a6e7fd299085" - integrity sha1-9laVsi9zJEQgGaPH+jmm5/0pkIU= - dependencies: - builtins "0.0.7" - validator@^10.11.0: version "10.11.0" resolved "https://registry.yarnpkg.com/validator/-/validator-10.11.0.tgz#003108ea6e9a9874d31ccc9e5006856ccd76b228"