From 771f3ae098bab04e9d179ee462729d2d5052ee86 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 29 Jun 2020 20:34:33 -0400 Subject: [PATCH 01/23] [ILM] Fix bug when clearing priority field (#70154) --- .../edit_policy/{contants.ts => constants.ts} | 3 +++ .../edit_policy/edit_policy.helpers.tsx | 2 +- .../client_integration/edit_policy/edit_policy.test.ts | 2 +- .../public/application/store/selectors/policies.js | 9 ++++++++- .../server/routes/api/policies/register_create_route.ts | 2 +- 5 files changed, 14 insertions(+), 4 deletions(-) rename x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/{contants.ts => constants.ts} (92%) diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/contants.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts similarity index 92% rename from x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/contants.ts rename to x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts index a58aad6dc6bc2..225432375dc75 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/contants.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts @@ -14,6 +14,9 @@ export const DELETE_PHASE_POLICY = { hot: { min_age: '0ms', actions: { + set_priority: { + priority: null, + }, rollover: { max_size: '50gb', }, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index a36cd7e35c36f..d6c955e0c0813 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -8,7 +8,7 @@ import { act } from 'react-dom/test-utils'; import { registerTestBed, TestBed, TestBedConfig } from '../../../../../test_utils'; -import { POLICY_NAME } from './contants'; +import { POLICY_NAME } from './constants'; import { TestSubjects } from '../helpers'; import { EditPolicy } from '../../../public/application/sections/edit_policy'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts index cc04749af3205..8753f01376d42 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts @@ -9,7 +9,7 @@ import { act } from 'react-dom/test-utils'; import { setupEnvironment } from '../helpers/setup_environment'; import { EditPolicyTestBed, setup } from './edit_policy.helpers'; -import { DELETE_PHASE_POLICY } from './contants'; +import { DELETE_PHASE_POLICY } from './constants'; import { API_BASE_PATH } from '../../../common/constants'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js index 32c6d93383c22..5bea22f0b3a76 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/selectors/policies.js @@ -193,8 +193,11 @@ const phaseFromES = (phase, phaseName, defaultEmptyPolicy) => { } if (actions.set_priority) { - policy[PHASE_INDEX_PRIORITY] = actions.set_priority.priority; + const { priority } = actions.set_priority; + + policy[PHASE_INDEX_PRIORITY] = priority ?? ''; } + if (actions.wait_for_snapshot) { policy[PHASE_WAIT_FOR_SNAPSHOT_POLICY] = actions.wait_for_snapshot.policy; } @@ -311,6 +314,10 @@ export const phaseToES = (phase, originalEsPhase) => { esPhase.actions.set_priority = { priority: phase[PHASE_INDEX_PRIORITY], }; + } else if (phase[PHASE_INDEX_PRIORITY] === '') { + esPhase.actions.set_priority = { + priority: null, + }; } if (phase[PHASE_WAIT_FOR_SNAPSHOT_POLICY]) { diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts index c09e56d236f7f..2d02802119e47 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts @@ -34,7 +34,7 @@ const minAgeSchema = schema.maybe(schema.string()); const setPrioritySchema = schema.maybe( schema.object({ - priority: schema.number(), + priority: schema.nullable(schema.number()), }) ); From 590fc8d2ffa325cbe9fd44465ec5a82052a7a3a5 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Mon, 29 Jun 2020 20:02:39 -0500 Subject: [PATCH 02/23] [Security][Lists] Add API functions and react hooks for value list APIs (#69603) * Add pure API functions and react hooks for value list APIs This also adds a generic hook, useAsyncTask, that wraps an async function to provide basic utilities: * loading state * error state * abort/cancel function * Fix type errors in hook tests These were not caught locally as I was accidentally running typescript without the full project. * Document current limitations of useAsyncTask * Defines a new validation function that returns an Either instead of a tuple This allows callers to further leverage fp-ts functions as needed. * Remove duplicated copyright comment * WIP: Perform request/response validations in the FP style * leverages new validateEither fn which returns an Either * constructs a pipeline that: * validates the payload * performs the API call * validates the response and short-circuits if any of those produce a Left value. It then converts the Either into a promise that either rejects with the Left or resolves with the Right. * Adds helper function to convert a TaskEither back to a Promise This cleans up our validation pipeline considerably. * Adds request/response validations to findLists * refactors private API functions to accept the encoded request schema (i.e. snake cased) * refactors validateEither to use `schema.validate` instead of `schema.decode` since we don't actually want the decoded value, we just want to verify that it'll be able to be decoded on the backend. * Refactor our API types * Add request/response validation to import/export functions * Fix type errors * Continue to export decoded types without a qualifier * pull types used by hooks from their new location * Fix errors with usage of act() * Attempting to reduce plugin bundle size By pulling from the module directly instead of an index, we can hopefully narrow down our dependencies until tree-shaking does this for us. * useAsyncFn's initiator does not return a promise Rather than returning a promise and requiring the caller to handle a rejection, we instead return nothing and require the user to watch the hook's state. * success can be handled with a useEffect on state.result * errors can be handled with a useEffect on state.error * Fix failing test Assertion count wasn't updated following interface changes; we've now got two inline expectations so this isn't needed. Co-authored-by: Elastic Machine --- .../schemas/request/delete_list_schema.ts | 1 + .../request/export_list_item_query_schema.ts | 1 + .../schemas/request/find_list_schema.ts | 6 +- .../request/import_list_item_query_schema.ts | 8 +- .../request/import_list_item_schema.ts | 1 + .../plugins/lists/common/siem_common_deps.ts | 2 +- .../lists/public/common/fp_utils.test.ts | 23 ++ .../plugins/lists/public/common/fp_utils.ts | 18 + .../common/hooks/use_async_task.test.ts | 93 +++++ .../public/common/hooks/use_async_task.ts | 48 +++ x-pack/plugins/lists/public/index.tsx | 5 + x-pack/plugins/lists/public/lists/api.test.ts | 331 ++++++++++++++++++ x-pack/plugins/lists/public/lists/api.ts | 173 +++++++++ .../lists/hooks/use_delete_list.test.ts | 36 ++ .../public/lists/hooks/use_delete_list.ts | 19 + .../lists/hooks/use_export_list.test.ts | 35 ++ .../public/lists/hooks/use_export_list.ts | 19 + .../public/lists/hooks/use_find_lists.test.ts | 36 ++ .../public/lists/hooks/use_find_lists.ts | 19 + .../lists/hooks/use_import_list.test.ts | 91 +++++ .../public/lists/hooks/use_import_list.ts | 19 + x-pack/plugins/lists/public/lists/types.ts | 33 ++ .../server/routes/export_list_item_route.ts | 2 +- .../security_solution/common/validate.test.ts | 26 +- .../security_solution/common/validate.ts | 12 +- 25 files changed, 1040 insertions(+), 17 deletions(-) create mode 100644 x-pack/plugins/lists/public/common/fp_utils.test.ts create mode 100644 x-pack/plugins/lists/public/common/fp_utils.ts create mode 100644 x-pack/plugins/lists/public/common/hooks/use_async_task.test.ts create mode 100644 x-pack/plugins/lists/public/common/hooks/use_async_task.ts create mode 100644 x-pack/plugins/lists/public/lists/api.test.ts create mode 100644 x-pack/plugins/lists/public/lists/api.ts create mode 100644 x-pack/plugins/lists/public/lists/hooks/use_delete_list.test.ts create mode 100644 x-pack/plugins/lists/public/lists/hooks/use_delete_list.ts create mode 100644 x-pack/plugins/lists/public/lists/hooks/use_export_list.test.ts create mode 100644 x-pack/plugins/lists/public/lists/hooks/use_export_list.ts create mode 100644 x-pack/plugins/lists/public/lists/hooks/use_find_lists.test.ts create mode 100644 x-pack/plugins/lists/public/lists/hooks/use_find_lists.ts create mode 100644 x-pack/plugins/lists/public/lists/hooks/use_import_list.test.ts create mode 100644 x-pack/plugins/lists/public/lists/hooks/use_import_list.ts create mode 100644 x-pack/plugins/lists/public/lists/types.ts diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts index fd6aa5b85f81a..6f6fc7a9ea33c 100644 --- a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts @@ -17,3 +17,4 @@ export const deleteListSchema = t.exact( ); export type DeleteListSchema = t.TypeOf; +export type DeleteListSchemaEncoded = t.OutputOf; diff --git a/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.ts b/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.ts index 14b201bf8089d..58092ffc563b1 100644 --- a/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.ts @@ -18,3 +18,4 @@ export const exportListItemQuerySchema = t.exact( ); export type ExportListItemQuerySchema = t.TypeOf; +export type ExportListItemQuerySchemaEncoded = t.OutputOf; diff --git a/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts index c29ab4f5360dd..212232f6bc9c1 100644 --- a/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/find_list_schema.ts @@ -9,7 +9,6 @@ import * as t from 'io-ts'; import { cursor, filter, sort_field, sort_order } from '../common/schemas'; -import { RequiredKeepUndefined } from '../../types'; import { StringToPositiveNumber } from '../types/string_to_positive_number'; export const findListSchema = t.exact( @@ -23,6 +22,5 @@ export const findListSchema = t.exact( }) ); -export type FindListSchemaPartial = t.TypeOf; - -export type FindListSchema = RequiredKeepUndefined>; +export type FindListSchema = t.TypeOf; +export type FindListSchemaEncoded = t.OutputOf; diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts index 73d9a53a41e4f..b37de61d0c2c3 100644 --- a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts @@ -9,11 +9,11 @@ import * as t from 'io-ts'; import { list_id, type } from '../common/schemas'; -import { Identity, RequiredKeepUndefined } from '../../types'; +import { Identity } from '../../types'; export const importListItemQuerySchema = t.exact(t.partial({ list_id, type })); export type ImportListItemQuerySchemaPartial = Identity>; -export type ImportListItemQuerySchema = RequiredKeepUndefined< - t.TypeOf ->; + +export type ImportListItemQuerySchema = t.TypeOf; +export type ImportListItemQuerySchemaEncoded = t.OutputOf; diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts index ee6a2aa0b339a..7370eecf690c7 100644 --- a/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts @@ -17,3 +17,4 @@ export const importListItemSchema = t.exact( ); export type ImportListItemSchema = t.TypeOf; +export type ImportListItemSchemaEncoded = t.OutputOf; diff --git a/x-pack/plugins/lists/common/siem_common_deps.ts b/x-pack/plugins/lists/common/siem_common_deps.ts index b1bb7d8aace36..dccc548985e77 100644 --- a/x-pack/plugins/lists/common/siem_common_deps.ts +++ b/x-pack/plugins/lists/common/siem_common_deps.ts @@ -9,5 +9,5 @@ export { DefaultUuid } from '../../security_solution/common/detection_engine/sch export { DefaultStringArray } from '../../security_solution/common/detection_engine/schemas/types/default_string_array'; export { exactCheck } from '../../security_solution/common/exact_check'; export { getPaths, foldLeftRight } from '../../security_solution/common/test_utils'; -export { validate } from '../../security_solution/common/validate'; +export { validate, validateEither } from '../../security_solution/common/validate'; export { formatErrors } from '../../security_solution/common/format_errors'; diff --git a/x-pack/plugins/lists/public/common/fp_utils.test.ts b/x-pack/plugins/lists/public/common/fp_utils.test.ts new file mode 100644 index 0000000000000..79042f4f9a72f --- /dev/null +++ b/x-pack/plugins/lists/public/common/fp_utils.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { tryCatch } from 'fp-ts/lib/TaskEither'; + +import { toPromise } from './fp_utils'; + +describe('toPromise', () => { + it('rejects with left if TaskEither is left', async () => { + const task = tryCatch(() => Promise.reject(new Error('whoops')), String); + + await expect(toPromise(task)).rejects.toEqual('Error: whoops'); + }); + + it('resolves with right if TaskEither is right', async () => { + const task = tryCatch(() => Promise.resolve('success'), String); + + await expect(toPromise(task)).resolves.toEqual('success'); + }); +}); diff --git a/x-pack/plugins/lists/public/common/fp_utils.ts b/x-pack/plugins/lists/public/common/fp_utils.ts new file mode 100644 index 0000000000000..04e1033879476 --- /dev/null +++ b/x-pack/plugins/lists/public/common/fp_utils.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { TaskEither } from 'fp-ts/lib/TaskEither'; +import { fold } from 'fp-ts/lib/Either'; + +export const toPromise = async (taskEither: TaskEither): Promise => + pipe( + await taskEither(), + fold( + (e) => Promise.reject(e), + (a) => Promise.resolve(a) + ) + ); diff --git a/x-pack/plugins/lists/public/common/hooks/use_async_task.test.ts b/x-pack/plugins/lists/public/common/hooks/use_async_task.test.ts new file mode 100644 index 0000000000000..af3aa60cfa506 --- /dev/null +++ b/x-pack/plugins/lists/public/common/hooks/use_async_task.test.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; + +import { useAsyncTask } from './use_async_task'; + +describe('useAsyncTask', () => { + let task: jest.Mock; + + beforeEach(() => { + task = jest.fn().mockResolvedValue('resolved value'); + }); + + it('does not invoke task if start was not called', () => { + renderHook(() => useAsyncTask(task)); + expect(task).not.toHaveBeenCalled(); + }); + + it('invokes the task when start is called', async () => { + const { result, waitForNextUpdate } = renderHook(() => useAsyncTask(task)); + + act(() => { + result.current.start({}); + }); + await waitForNextUpdate(); + + expect(task).toHaveBeenCalled(); + }); + + it('invokes the task with a signal and start args', async () => { + const { result, waitForNextUpdate } = renderHook(() => useAsyncTask(task)); + + act(() => { + result.current.start({ + arg1: 'value1', + arg2: 'value2', + }); + }); + await waitForNextUpdate(); + + expect(task).toHaveBeenCalledWith(expect.any(AbortController), { + arg1: 'value1', + arg2: 'value2', + }); + }); + + it('populates result with the resolved value of the task', async () => { + const { result, waitForNextUpdate } = renderHook(() => useAsyncTask(task)); + + act(() => { + result.current.start({}); + }); + await waitForNextUpdate(); + + expect(result.current.result).toEqual('resolved value'); + expect(result.current.error).toBeUndefined(); + }); + + it('populates error if task rejects', async () => { + task.mockRejectedValue(new Error('whoops')); + const { result, waitForNextUpdate } = renderHook(() => useAsyncTask(task)); + + act(() => { + result.current.start({}); + }); + await waitForNextUpdate(); + + expect(result.current.result).toBeUndefined(); + expect(result.current.error).toEqual(new Error('whoops')); + }); + + it('populates the loading state while the task is pending', async () => { + let resolve: () => void; + task.mockImplementation(() => new Promise((_resolve) => (resolve = _resolve))); + + const { result, waitForNextUpdate } = renderHook(() => useAsyncTask(task)); + + act(() => { + result.current.start({}); + }); + + expect(result.current.loading).toBe(true); + + act(() => resolve()); + await waitForNextUpdate(); + + expect(result.current.loading).toBe(false); + }); +}); diff --git a/x-pack/plugins/lists/public/common/hooks/use_async_task.ts b/x-pack/plugins/lists/public/common/hooks/use_async_task.ts new file mode 100644 index 0000000000000..f767e9333c234 --- /dev/null +++ b/x-pack/plugins/lists/public/common/hooks/use_async_task.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCallback, useRef } from 'react'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; + +// Params can be generalized to a ...rest parameter extending unknown[] once https://github.com/microsoft/TypeScript/pull/39094 is available. +// for now, the task must still receive unknown as a second argument, and an argument must be passed to start() +export type UseAsyncTask = ( + task: (...args: [AbortController, Params]) => Promise +) => AsyncTask; + +export interface AsyncTask { + start: (params: Params) => void; + abort: () => void; + loading: boolean; + error: Error | undefined; + result: Result | undefined; +} + +/** + * + * @param task Async function receiving an AbortController and optional arguments + * + * @returns An {@link AsyncTask} containing the underlying task's state along with start/abort helpers + */ +export const useAsyncTask: UseAsyncTask = (task) => { + const ctrl = useRef(new AbortController()); + const abort = useCallback((): void => { + ctrl.current.abort(); + }, []); + + // @ts-ignore typings are incorrect, see: https://github.com/streamich/react-use/pull/589 + const [state, initiator] = useAsyncFn(task, [task]); + + const start = useCallback( + (args) => { + ctrl.current = new AbortController(); + initiator(ctrl.current, args); + }, + [initiator] + ); + + return { abort, error: state.error, loading: state.loading, result: state.value, start }; +}; diff --git a/x-pack/plugins/lists/public/index.tsx b/x-pack/plugins/lists/public/index.tsx index 71187273c731c..1ea24123ccb9a 100644 --- a/x-pack/plugins/lists/public/index.tsx +++ b/x-pack/plugins/lists/public/index.tsx @@ -3,11 +3,16 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + // Exports to be shared with plugins export { useApi } from './exceptions/hooks/use_api'; export { usePersistExceptionItem } from './exceptions/hooks/persist_exception_item'; export { usePersistExceptionList } from './exceptions/hooks/persist_exception_list'; export { useExceptionList } from './exceptions/hooks/use_exception_list'; +export { useFindLists } from './lists/hooks/use_find_lists'; +export { useImportList } from './lists/hooks/use_import_list'; +export { useDeleteList } from './lists/hooks/use_delete_list'; +export { useExportList } from './lists/hooks/use_export_list'; export { ExceptionList, ExceptionIdentifiers, diff --git a/x-pack/plugins/lists/public/lists/api.test.ts b/x-pack/plugins/lists/public/lists/api.test.ts new file mode 100644 index 0000000000000..38556e2eabc18 --- /dev/null +++ b/x-pack/plugins/lists/public/lists/api.test.ts @@ -0,0 +1,331 @@ +/* + * 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 { HttpFetchOptions } from '../../../../../src/core/public'; +import { httpServiceMock } from '../../../../../src/core/public/mocks'; +import { getListResponseMock } from '../../common/schemas/response/list_schema.mock'; +import { getFoundListSchemaMock } from '../../common/schemas/response/found_list_schema.mock'; + +import { deleteList, exportList, findLists, importList } from './api'; +import { + ApiPayload, + DeleteListParams, + ExportListParams, + FindListsParams, + ImportListParams, +} from './types'; + +describe('Value Lists API', () => { + let httpMock: ReturnType; + + beforeEach(() => { + httpMock = httpServiceMock.createStartContract(); + }); + + describe('deleteList', () => { + beforeEach(() => { + httpMock.fetch.mockResolvedValue(getListResponseMock()); + }); + + it('DELETEs specifying the id as a query parameter', async () => { + const abortCtrl = new AbortController(); + const payload: ApiPayload = { id: 'list-id' }; + await deleteList({ + http: httpMock, + ...payload, + signal: abortCtrl.signal, + }); + + expect(httpMock.fetch).toHaveBeenCalledWith( + '/api/lists', + expect.objectContaining({ + method: 'DELETE', + query: { id: 'list-id' }, + }) + ); + }); + + it('rejects with an error if request payload is invalid (and does not make API call)', async () => { + const abortCtrl = new AbortController(); + const payload: Omit, 'id'> & { + id: number; + } = { id: 23 }; + + await expect( + deleteList({ + http: httpMock, + ...((payload as unknown) as ApiPayload), + signal: abortCtrl.signal, + }) + ).rejects.toEqual('Invalid value "23" supplied to "id"'); + expect(httpMock.fetch).not.toHaveBeenCalled(); + }); + + it('rejects with an error if response payload is invalid', async () => { + const abortCtrl = new AbortController(); + const payload: ApiPayload = { id: 'list-id' }; + const badResponse = { ...getListResponseMock(), id: undefined }; + httpMock.fetch.mockResolvedValue(badResponse); + + await expect( + deleteList({ + http: httpMock, + ...payload, + signal: abortCtrl.signal, + }) + ).rejects.toEqual('Invalid value "undefined" supplied to "id"'); + }); + }); + + describe('findLists', () => { + beforeEach(() => { + httpMock.fetch.mockResolvedValue(getFoundListSchemaMock()); + }); + + it('GETs from the lists endpoint', async () => { + const abortCtrl = new AbortController(); + await findLists({ + http: httpMock, + pageIndex: 1, + pageSize: 10, + signal: abortCtrl.signal, + }); + + expect(httpMock.fetch).toHaveBeenCalledWith( + '/api/lists/_find', + expect.objectContaining({ + method: 'GET', + }) + ); + }); + + it('sends pagination as query parameters', async () => { + const abortCtrl = new AbortController(); + await findLists({ + http: httpMock, + pageIndex: 1, + pageSize: 10, + signal: abortCtrl.signal, + }); + + expect(httpMock.fetch).toHaveBeenCalledWith( + '/api/lists/_find', + expect.objectContaining({ + query: { page: 1, per_page: 10 }, + }) + ); + }); + + it('rejects with an error if request payload is invalid (and does not make API call)', async () => { + const abortCtrl = new AbortController(); + const payload: ApiPayload = { pageIndex: 10, pageSize: 0 }; + + await expect( + findLists({ + http: httpMock, + ...payload, + signal: abortCtrl.signal, + }) + ).rejects.toEqual('Invalid value "0" supplied to "per_page"'); + expect(httpMock.fetch).not.toHaveBeenCalled(); + }); + + it('rejects with an error if response payload is invalid', async () => { + const abortCtrl = new AbortController(); + const payload: ApiPayload = { pageIndex: 1, pageSize: 10 }; + const badResponse = { ...getFoundListSchemaMock(), cursor: undefined }; + httpMock.fetch.mockResolvedValue(badResponse); + + await expect( + findLists({ + http: httpMock, + ...payload, + signal: abortCtrl.signal, + }) + ).rejects.toEqual('Invalid value "undefined" supplied to "cursor"'); + }); + }); + + describe('importList', () => { + beforeEach(() => { + httpMock.fetch.mockResolvedValue(getListResponseMock()); + }); + + it('POSTs the file', async () => { + const abortCtrl = new AbortController(); + const file = new File([], 'name'); + + await importList({ + file, + http: httpMock, + listId: 'my_list', + signal: abortCtrl.signal, + type: 'keyword', + }); + expect(httpMock.fetch).toHaveBeenCalledWith( + '/api/lists/items/_import', + expect.objectContaining({ + method: 'POST', + }) + ); + + // httpmock's fetch signature is inferred incorrectly + const [[, { body }]] = (httpMock.fetch.mock.calls as unknown) as Array< + [unknown, HttpFetchOptions] + >; + const actualFile = (body as FormData).get('file'); + expect(actualFile).toEqual(file); + }); + + it('sends type and id as query parameters', async () => { + const abortCtrl = new AbortController(); + const file = new File([], 'name'); + + await importList({ + file, + http: httpMock, + listId: 'my_list', + signal: abortCtrl.signal, + type: 'keyword', + }); + + expect(httpMock.fetch).toHaveBeenCalledWith( + '/api/lists/items/_import', + expect.objectContaining({ + query: { list_id: 'my_list', type: 'keyword' }, + }) + ); + }); + + it('rejects with an error if request body is invalid (and does not make API call)', async () => { + const abortCtrl = new AbortController(); + const payload: ApiPayload = { + file: (undefined as unknown) as File, + listId: 'list-id', + type: 'ip', + }; + + await expect( + importList({ + http: httpMock, + ...payload, + signal: abortCtrl.signal, + }) + ).rejects.toEqual('Invalid value "undefined" supplied to "file"'); + expect(httpMock.fetch).not.toHaveBeenCalled(); + }); + + it('rejects with an error if request params are invalid (and does not make API call)', async () => { + const abortCtrl = new AbortController(); + const file = new File([], 'name'); + const payload: ApiPayload = { + file, + listId: 'list-id', + type: 'other' as 'ip', + }; + + await expect( + importList({ + http: httpMock, + ...payload, + signal: abortCtrl.signal, + }) + ).rejects.toEqual('Invalid value "other" supplied to "type"'); + expect(httpMock.fetch).not.toHaveBeenCalled(); + }); + + it('rejects with an error if response payload is invalid', async () => { + const abortCtrl = new AbortController(); + const file = new File([], 'name'); + const payload: ApiPayload = { + file, + listId: 'list-id', + type: 'ip', + }; + const badResponse = { ...getListResponseMock(), id: undefined }; + httpMock.fetch.mockResolvedValue(badResponse); + + await expect( + importList({ + http: httpMock, + ...payload, + signal: abortCtrl.signal, + }) + ).rejects.toEqual('Invalid value "undefined" supplied to "id"'); + }); + }); + + describe('exportList', () => { + beforeEach(() => { + httpMock.fetch.mockResolvedValue(getListResponseMock()); + }); + + it('POSTs to the export endpoint', async () => { + const abortCtrl = new AbortController(); + + await exportList({ + http: httpMock, + listId: 'my_list', + signal: abortCtrl.signal, + }); + expect(httpMock.fetch).toHaveBeenCalledWith( + '/api/lists/items/_export', + expect.objectContaining({ + method: 'POST', + }) + ); + }); + + it('sends type and id as query parameters', async () => { + const abortCtrl = new AbortController(); + + await exportList({ + http: httpMock, + listId: 'my_list', + signal: abortCtrl.signal, + }); + expect(httpMock.fetch).toHaveBeenCalledWith( + '/api/lists/items/_export', + expect.objectContaining({ + query: { list_id: 'my_list' }, + }) + ); + }); + + it('rejects with an error if request params are invalid (and does not make API call)', async () => { + const abortCtrl = new AbortController(); + const payload: ApiPayload = { + listId: (23 as unknown) as string, + }; + + await expect( + exportList({ + http: httpMock, + ...payload, + signal: abortCtrl.signal, + }) + ).rejects.toEqual('Invalid value "23" supplied to "list_id"'); + expect(httpMock.fetch).not.toHaveBeenCalled(); + }); + + it('rejects with an error if response payload is invalid', async () => { + const abortCtrl = new AbortController(); + const payload: ApiPayload = { + listId: 'list-id', + }; + const badResponse = { ...getListResponseMock(), id: undefined }; + httpMock.fetch.mockResolvedValue(badResponse); + + await expect( + exportList({ + http: httpMock, + ...payload, + signal: abortCtrl.signal, + }) + ).rejects.toEqual('Invalid value "undefined" supplied to "id"'); + }); + }); +}); diff --git a/x-pack/plugins/lists/public/lists/api.ts b/x-pack/plugins/lists/public/lists/api.ts new file mode 100644 index 0000000000000..d615239f4eb01 --- /dev/null +++ b/x-pack/plugins/lists/public/lists/api.ts @@ -0,0 +1,173 @@ +/* + * 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 { chain, fromEither, map, tryCatch } from 'fp-ts/lib/TaskEither'; +import { flow } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { + DeleteListSchemaEncoded, + ExportListItemQuerySchemaEncoded, + FindListSchemaEncoded, + FoundListSchema, + ImportListItemQuerySchemaEncoded, + ImportListItemSchemaEncoded, + ListSchema, + deleteListSchema, + exportListItemQuerySchema, + findListSchema, + foundListSchema, + importListItemQuerySchema, + importListItemSchema, + listSchema, +} from '../../common/schemas'; +import { LIST_ITEM_URL, LIST_URL } from '../../common/constants'; +import { validateEither } from '../../common/siem_common_deps'; +import { toPromise } from '../common/fp_utils'; + +import { + ApiParams, + DeleteListParams, + ExportListParams, + FindListsParams, + ImportListParams, +} from './types'; + +const findLists = async ({ + http, + cursor, + page, + per_page, + signal, +}: ApiParams & FindListSchemaEncoded): Promise => { + return http.fetch(`${LIST_URL}/_find`, { + method: 'GET', + query: { + cursor, + page, + per_page, + }, + signal, + }); +}; + +const findListsWithValidation = async ({ + http, + pageIndex, + pageSize, + signal, +}: FindListsParams): Promise => + pipe( + { + page: String(pageIndex), + per_page: String(pageSize), + }, + (payload) => fromEither(validateEither(findListSchema, payload)), + chain((payload) => tryCatch(() => findLists({ http, signal, ...payload }), String)), + chain((response) => fromEither(validateEither(foundListSchema, response))), + flow(toPromise) + ); + +export { findListsWithValidation as findLists }; + +const importList = async ({ + file, + http, + list_id, + type, + signal, +}: ApiParams & ImportListItemSchemaEncoded & ImportListItemQuerySchemaEncoded): Promise< + ListSchema +> => { + const formData = new FormData(); + formData.append('file', file as Blob); + + return http.fetch(`${LIST_ITEM_URL}/_import`, { + body: formData, + headers: { 'Content-Type': undefined }, + method: 'POST', + query: { list_id, type }, + signal, + }); +}; + +const importListWithValidation = async ({ + file, + http, + listId, + type, + signal, +}: ImportListParams): Promise => + pipe( + { + list_id: listId, + type, + }, + (query) => fromEither(validateEither(importListItemQuerySchema, query)), + chain((query) => + pipe( + fromEither(validateEither(importListItemSchema, { file })), + map((body) => ({ ...body, ...query })) + ) + ), + chain((payload) => tryCatch(() => importList({ http, signal, ...payload }), String)), + chain((response) => fromEither(validateEither(listSchema, response))), + flow(toPromise) + ); + +export { importListWithValidation as importList }; + +const deleteList = async ({ + http, + id, + signal, +}: ApiParams & DeleteListSchemaEncoded): Promise => + http.fetch(LIST_URL, { + method: 'DELETE', + query: { id }, + signal, + }); + +const deleteListWithValidation = async ({ + http, + id, + signal, +}: DeleteListParams): Promise => + pipe( + { id }, + (payload) => fromEither(validateEither(deleteListSchema, payload)), + chain((payload) => tryCatch(() => deleteList({ http, signal, ...payload }), String)), + chain((response) => fromEither(validateEither(listSchema, response))), + flow(toPromise) + ); + +export { deleteListWithValidation as deleteList }; + +const exportList = async ({ + http, + list_id, + signal, +}: ApiParams & ExportListItemQuerySchemaEncoded): Promise => + http.fetch(`${LIST_ITEM_URL}/_export`, { + method: 'POST', + query: { list_id }, + signal, + }); + +const exportListWithValidation = async ({ + http, + listId, + signal, +}: ExportListParams): Promise => + pipe( + { list_id: listId }, + (payload) => fromEither(validateEither(exportListItemQuerySchema, payload)), + chain((payload) => tryCatch(() => exportList({ http, signal, ...payload }), String)), + chain((response) => fromEither(validateEither(listSchema, response))), + flow(toPromise) + ); + +export { exportListWithValidation as exportList }; diff --git a/x-pack/plugins/lists/public/lists/hooks/use_delete_list.test.ts b/x-pack/plugins/lists/public/lists/hooks/use_delete_list.test.ts new file mode 100644 index 0000000000000..6262c553dfd52 --- /dev/null +++ b/x-pack/plugins/lists/public/lists/hooks/use_delete_list.test.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; + +import * as Api from '../api'; +import { httpServiceMock } from '../../../../../../src/core/public/mocks'; +import { getListResponseMock } from '../../../common/schemas/response/list_schema.mock'; + +import { useDeleteList } from './use_delete_list'; + +jest.mock('../api'); + +describe('useDeleteList', () => { + let httpMock: ReturnType; + + beforeEach(() => { + httpMock = httpServiceMock.createStartContract(); + (Api.deleteList as jest.Mock).mockResolvedValue(getListResponseMock()); + }); + + it('invokes Api.deleteList', async () => { + const { result, waitForNextUpdate } = renderHook(() => useDeleteList()); + act(() => { + result.current.start({ http: httpMock, id: 'list' }); + }); + await waitForNextUpdate(); + + expect(Api.deleteList).toHaveBeenCalledWith( + expect.objectContaining({ http: httpMock, id: 'list' }) + ); + }); +}); diff --git a/x-pack/plugins/lists/public/lists/hooks/use_delete_list.ts b/x-pack/plugins/lists/public/lists/hooks/use_delete_list.ts new file mode 100644 index 0000000000000..0f1f6facdd7c4 --- /dev/null +++ b/x-pack/plugins/lists/public/lists/hooks/use_delete_list.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useAsyncTask } from '../../common/hooks/use_async_task'; +import { DeleteListParams } from '../types'; +import { deleteList } from '../api'; + +export type DeleteListTaskArgs = Omit; + +const deleteListsTask = ( + { signal }: AbortController, + args: DeleteListTaskArgs +): ReturnType => deleteList({ signal, ...args }); + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const useDeleteList = () => useAsyncTask(deleteListsTask); diff --git a/x-pack/plugins/lists/public/lists/hooks/use_export_list.test.ts b/x-pack/plugins/lists/public/lists/hooks/use_export_list.test.ts new file mode 100644 index 0000000000000..2eca0fd11b21a --- /dev/null +++ b/x-pack/plugins/lists/public/lists/hooks/use_export_list.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; + +import * as Api from '../api'; +import { httpServiceMock } from '../../../../../../src/core/public/mocks'; + +import { useExportList } from './use_export_list'; + +jest.mock('../api'); + +describe('useExportList', () => { + let httpMock: ReturnType; + + beforeEach(() => { + httpMock = httpServiceMock.createStartContract(); + (Api.exportList as jest.Mock).mockResolvedValue(new Blob()); + }); + + it('invokes Api.exportList', async () => { + const { result, waitForNextUpdate } = renderHook(() => useExportList()); + act(() => { + result.current.start({ http: httpMock, listId: 'list' }); + }); + await waitForNextUpdate(); + + expect(Api.exportList).toHaveBeenCalledWith( + expect.objectContaining({ http: httpMock, listId: 'list' }) + ); + }); +}); diff --git a/x-pack/plugins/lists/public/lists/hooks/use_export_list.ts b/x-pack/plugins/lists/public/lists/hooks/use_export_list.ts new file mode 100644 index 0000000000000..41efde939ead4 --- /dev/null +++ b/x-pack/plugins/lists/public/lists/hooks/use_export_list.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useAsyncTask } from '../../common/hooks/use_async_task'; +import { ExportListParams } from '../types'; +import { exportList } from '../api'; + +export type ExportListTaskArgs = Omit; + +const exportListTask = ( + { signal }: AbortController, + args: ExportListTaskArgs +): ReturnType => exportList({ signal, ...args }); + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const useExportList = () => useAsyncTask(exportListTask); diff --git a/x-pack/plugins/lists/public/lists/hooks/use_find_lists.test.ts b/x-pack/plugins/lists/public/lists/hooks/use_find_lists.test.ts new file mode 100644 index 0000000000000..0d63acbe0bd2c --- /dev/null +++ b/x-pack/plugins/lists/public/lists/hooks/use_find_lists.test.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; + +import * as Api from '../api'; +import { httpServiceMock } from '../../../../../../src/core/public/mocks'; +import { getFoundListSchemaMock } from '../../../common/schemas/response/found_list_schema.mock'; + +import { useFindLists } from './use_find_lists'; + +jest.mock('../api'); + +describe('useFindLists', () => { + let httpMock: ReturnType; + + beforeEach(() => { + httpMock = httpServiceMock.createStartContract(); + (Api.findLists as jest.Mock).mockResolvedValue(getFoundListSchemaMock()); + }); + + it('invokes Api.findLists', async () => { + const { result, waitForNextUpdate } = renderHook(() => useFindLists()); + act(() => { + result.current.start({ http: httpMock, pageIndex: 1, pageSize: 10 }); + }); + await waitForNextUpdate(); + + expect(Api.findLists).toHaveBeenCalledWith( + expect.objectContaining({ http: httpMock, pageIndex: 1, pageSize: 10 }) + ); + }); +}); diff --git a/x-pack/plugins/lists/public/lists/hooks/use_find_lists.ts b/x-pack/plugins/lists/public/lists/hooks/use_find_lists.ts new file mode 100644 index 0000000000000..d50a16855a547 --- /dev/null +++ b/x-pack/plugins/lists/public/lists/hooks/use_find_lists.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useAsyncTask } from '../../common/hooks/use_async_task'; +import { FindListsParams } from '../types'; +import { findLists } from '../api'; + +export type FindListsTaskArgs = Omit; + +const findListsTask = ( + { signal }: AbortController, + args: FindListsTaskArgs +): ReturnType => findLists({ signal, ...args }); + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const useFindLists = () => useAsyncTask(findListsTask); diff --git a/x-pack/plugins/lists/public/lists/hooks/use_import_list.test.ts b/x-pack/plugins/lists/public/lists/hooks/use_import_list.test.ts new file mode 100644 index 0000000000000..00a8b7f3206b0 --- /dev/null +++ b/x-pack/plugins/lists/public/lists/hooks/use_import_list.test.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; + +import { httpServiceMock } from '../../../../../../src/core/public/mocks'; +import { getListResponseMock } from '../../../common/schemas/response/list_schema.mock'; +import * as Api from '../api'; + +import { useImportList } from './use_import_list'; + +jest.mock('../api'); + +describe('useImportList', () => { + let httpMock: ReturnType; + + beforeEach(() => { + httpMock = httpServiceMock.createStartContract(); + (Api.importList as jest.Mock).mockResolvedValue(getListResponseMock()); + }); + + it('does not invoke importList if start was not called', () => { + renderHook(() => useImportList()); + expect(Api.importList).not.toHaveBeenCalled(); + }); + + it('invokes Api.importList', async () => { + const fileMock = ('my file' as unknown) as File; + + const { result, waitForNextUpdate } = renderHook(() => useImportList()); + + act(() => { + result.current.start({ + file: fileMock, + http: httpMock, + listId: 'my_list_id', + type: 'keyword', + }); + }); + await waitForNextUpdate(); + + expect(Api.importList).toHaveBeenCalledWith( + expect.objectContaining({ + file: fileMock, + listId: 'my_list_id', + type: 'keyword', + }) + ); + }); + + it('populates result with the response of Api.importList', async () => { + const fileMock = ('my file' as unknown) as File; + + const { result, waitForNextUpdate } = renderHook(() => useImportList()); + + act(() => { + result.current.start({ + file: fileMock, + http: httpMock, + listId: 'my_list_id', + type: 'keyword', + }); + }); + await waitForNextUpdate(); + + expect(result.current.result).toEqual(getListResponseMock()); + }); + + it('error is populated if importList rejects', async () => { + const fileMock = ('my file' as unknown) as File; + (Api.importList as jest.Mock).mockRejectedValue(new Error('whoops')); + const { result, waitForNextUpdate } = renderHook(() => useImportList()); + + act(() => { + result.current.start({ + file: fileMock, + http: httpMock, + listId: 'my_list_id', + type: 'keyword', + }); + }); + + await waitForNextUpdate(); + + expect(result.current.result).toBeUndefined(); + expect(result.current.error).toEqual(new Error('whoops')); + }); +}); diff --git a/x-pack/plugins/lists/public/lists/hooks/use_import_list.ts b/x-pack/plugins/lists/public/lists/hooks/use_import_list.ts new file mode 100644 index 0000000000000..2854acd6e522e --- /dev/null +++ b/x-pack/plugins/lists/public/lists/hooks/use_import_list.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useAsyncTask } from '../../common/hooks/use_async_task'; +import { ImportListParams } from '../types'; +import { importList } from '../api'; + +export type ImportListTaskArgs = Omit; + +const importListTask = ( + { signal }: AbortController, + args: ImportListTaskArgs +): ReturnType => importList({ signal, ...args }); + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const useImportList = () => useAsyncTask(importListTask); diff --git a/x-pack/plugins/lists/public/lists/types.ts b/x-pack/plugins/lists/public/lists/types.ts new file mode 100644 index 0000000000000..6421ad174d4d9 --- /dev/null +++ b/x-pack/plugins/lists/public/lists/types.ts @@ -0,0 +1,33 @@ +/* + * 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 { HttpStart } from '../../../../../src/core/public'; +import { Type } from '../../common/schemas'; + +export interface ApiParams { + http: HttpStart; + signal: AbortSignal; +} +export type ApiPayload = Omit; + +export interface FindListsParams extends ApiParams { + pageSize: number | undefined; + pageIndex: number | undefined; +} + +export interface ImportListParams extends ApiParams { + file: File; + listId: string | undefined; + type: Type | undefined; +} + +export interface DeleteListParams extends ApiParams { + id: string; +} + +export interface ExportListParams extends ApiParams { + listId: string; +} diff --git a/x-pack/plugins/lists/server/routes/export_list_item_route.ts b/x-pack/plugins/lists/server/routes/export_list_item_route.ts index 32b99bfc512bf..8b50f4666085a 100644 --- a/x-pack/plugins/lists/server/routes/export_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/export_list_item_route.ts @@ -47,7 +47,7 @@ export const exportListItemRoute = (router: IRouter): void => { body: stream, headers: { 'Content-Disposition': `attachment; filename="${fileName}"`, - 'Content-Type': 'text/plain', + 'Content-Type': 'application/ndjson', }, }); } diff --git a/x-pack/plugins/security_solution/common/validate.test.ts b/x-pack/plugins/security_solution/common/validate.test.ts index 032f6d9590168..b2217099fca19 100644 --- a/x-pack/plugins/security_solution/common/validate.test.ts +++ b/x-pack/plugins/security_solution/common/validate.test.ts @@ -3,15 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -/* - * 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 { left, right } from 'fp-ts/lib/Either'; import * as t from 'io-ts'; -import { validate } from './validate'; +import { validate, validateEither } from './validate'; describe('validate', () => { test('it should do a validation correctly', () => { @@ -32,3 +28,21 @@ describe('validate', () => { expect(errors).toEqual('Invalid value "some other value" supplied to "a"'); }); }); + +describe('validateEither', () => { + it('returns the ORIGINAL payload as right if valid', () => { + const schema = t.exact(t.type({ a: t.number })); + const payload = { a: 1 }; + const result = validateEither(schema, payload); + + expect(result).toEqual(right(payload)); + }); + + it('returns an error string if invalid', () => { + const schema = t.exact(t.type({ a: t.number })); + const payload = { a: 'some other value' }; + const result = validateEither(schema, payload); + + expect(result).toEqual(left('Invalid value "some other value" supplied to "a"')); + }); +}); diff --git a/x-pack/plugins/security_solution/common/validate.ts b/x-pack/plugins/security_solution/common/validate.ts index db9e286e2ebc2..f36df38c2a90d 100644 --- a/x-pack/plugins/security_solution/common/validate.ts +++ b/x-pack/plugins/security_solution/common/validate.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fold } from 'fp-ts/lib/Either'; +import { fold, Either, mapLeft } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import * as t from 'io-ts'; import { exactCheck } from './exact_check'; @@ -23,3 +23,13 @@ export const validate = ( const right = (output: T): [T | null, string | null] => [output, null]; return pipe(checked, fold(left, right)); }; + +export const validateEither = ( + schema: T, + obj: A +): Either => + pipe( + obj, + (a) => schema.validate(a, t.getDefaultContext(schema.asDecoder())), + mapLeft((errors) => formatErrors(errors).join(',')) + ); From b3f19dad7435677a66a0d2de0f626860a6e12863 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Mon, 29 Jun 2020 21:43:47 -0400 Subject: [PATCH 03/23] [Ingest Manager][SECURITY SOLUTION] adjust config reassign link and add roundtrip to Reassignment flow (#70208) --- .../components/actions_menu.tsx | 18 +++++++-- .../fleet/agent_details_page/index.tsx | 40 ++++++++++++++++--- .../types/intra_app_route_state.ts | 11 ++++- .../view/details/host_details.tsx | 34 ++++++++++++++-- .../pages/endpoint_hosts/view/hooks.ts | 17 ++++++++ .../pages/endpoint_hosts/view/index.test.tsx | 29 ++++++++++++++ 6 files changed, 135 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/actions_menu.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/actions_menu.tsx index 27e17f6b3df61..75a67fb9288e5 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/actions_menu.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/actions_menu.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, useState } from 'react'; +import React, { memo, useState, useMemo } from 'react'; import { EuiPortal, EuiContextMenuItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { Agent } from '../../../../types'; @@ -14,16 +14,26 @@ import { useAgentRefresh } from '../hooks'; export const AgentDetailsActionMenu: React.FunctionComponent<{ agent: Agent; -}> = memo(({ agent }) => { + assignFlyoutOpenByDefault?: boolean; + onCancelReassign?: () => void; +}> = memo(({ agent, assignFlyoutOpenByDefault = false, onCancelReassign }) => { const hasWriteCapabilites = useCapabilities().write; const refreshAgent = useAgentRefresh(); - const [isReassignFlyoutOpen, setIsReassignFlyoutOpen] = useState(false); + const [isReassignFlyoutOpen, setIsReassignFlyoutOpen] = useState(assignFlyoutOpenByDefault); + + const onClose = useMemo(() => { + if (onCancelReassign) { + return onCancelReassign; + } else { + return () => setIsReassignFlyoutOpen(false); + } + }, [onCancelReassign, setIsReassignFlyoutOpen]); return ( <> {isReassignFlyoutOpen && ( - setIsReassignFlyoutOpen(false)} /> + )} { sendRequest: sendAgentConfigRequest, } = useGetOneAgentConfig(agentData?.item?.config_id); + const { + application: { navigateToApp }, + } = useCore(); + const routeState = useIntraAppState(); + const queryParams = new URLSearchParams(useLocation().search); + const openReassignFlyoutOpenByDefault = queryParams.get('openReassignFlyout') === 'true'; + + const reassignCancelClickHandler = useCallback(() => { + if (routeState && routeState.onDoneNavigateTo) { + navigateToApp(routeState.onDoneNavigateTo[0], routeState.onDoneNavigateTo[1]); + } + }, [routeState, navigateToApp]); + const headerLeftContent = useMemo( () => ( @@ -124,7 +144,17 @@ export const AgentDetailsPage: React.FunctionComponent = () => { }, { isDivider: true }, { - content: , + content: ( + + ), }, ].map((item, index) => ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/intra_app_route_state.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/intra_app_route_state.ts index b2948686ff6e5..c5833adcded5f 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/intra_app_route_state.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/intra_app_route_state.ts @@ -29,9 +29,18 @@ export interface AgentConfigDetailsDeployAgentAction { onDoneNavigateTo?: Parameters; } +/** + * Supported routing state for the agent config details page routes with deploy agents action + */ +export interface AgentDetailsReassignConfigAction { + /** On done, navigate to the given app */ + onDoneNavigateTo?: Parameters; +} + /** * All possible Route states. */ export type AnyIntraAppRouteState = | CreateDatasourceRouteState - | AgentConfigDetailsDeployAgentAction; + | AgentConfigDetailsDeployAgentAction + | AgentDetailsReassignConfigAction; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx index b7e90c19799c7..80c4e2f379c7c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx @@ -19,7 +19,8 @@ import React, { memo, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { HostMetadata } from '../../../../../../common/endpoint/types'; -import { useHostSelector, useHostLogsUrl, useHostIngestUrl } from '../hooks'; +import { useHostSelector, useHostLogsUrl, useAgentDetailsIngestUrl } from '../hooks'; +import { useNavigateToAppEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; import { policyResponseStatus, uiQueryParams } from '../../store/selectors'; import { POLICY_STATUS_TO_HEALTH_COLOR } from '../host_constants'; import { FormattedDateAndTime } from '../../../../../common/components/endpoint/formatted_date_time'; @@ -28,6 +29,7 @@ import { LinkToApp } from '../../../../../common/components/endpoint/link_to_app import { getEndpointDetailsPath, getPolicyDetailPath } from '../../../../common/routing'; import { SecurityPageName } from '../../../../../app/types'; import { useFormatUrl } from '../../../../../common/components/link_to'; +import { AgentDetailsReassignConfigAction } from '../../../../../../../ingest_manager/public'; const HostIds = styled(EuiListGroupItem)` margin-top: 0; @@ -46,9 +48,16 @@ const LinkToExternalApp = styled.div` } `; +const openReassignFlyoutSearch = '?openReassignFlyout=true'; + export const HostDetails = memo(({ details }: { details: HostMetadata }) => { const { url: logsUrl, appId: logsAppId, appPath: logsAppPath } = useHostLogsUrl(details.host.id); - const { url: ingestUrl, appId: ingestAppId, appPath: ingestAppPath } = useHostIngestUrl(); + const agentId = details.elastic.agent.id; + const { + url: agentDetailsUrl, + appId: ingestAppId, + appPath: agentDetailsAppPath, + } = useAgentDetailsIngestUrl(agentId); const queryParams = useHostSelector(uiQueryParams); const policyStatus = useHostSelector( policyResponseStatus @@ -96,6 +105,22 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { ]; }, [details.host.id, formatUrl, queryParams]); + const agentDetailsWithFlyoutPath = `${agentDetailsAppPath}${openReassignFlyoutSearch}`; + const agentDetailsWithFlyoutUrl = `${agentDetailsUrl}${openReassignFlyoutSearch}`; + const handleReassignEndpointsClick = useNavigateToAppEventHandler< + AgentDetailsReassignConfigAction + >(ingestAppId, { + path: agentDetailsWithFlyoutPath, + state: { + onDoneNavigateTo: [ + 'securitySolution:management', + { + path: getEndpointDetailsPath({ name: 'endpointDetails', selected_host: details.host.id }), + }, + ], + }, + }); + const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseRoutePath); const [policyDetailsRoutePath, policyDetailsRouteUrl] = useMemo(() => { @@ -207,8 +232,9 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts index 51aaea20df843..c072c812edbb5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts @@ -51,3 +51,20 @@ export const useHostIngestUrl = (): { url: string; appId: string; appPath: strin }; }, [services.application]); }; + +/** + * Returns an object that contains Ingest app and URL information + */ +export const useAgentDetailsIngestUrl = ( + agentId: string +): { url: string; appId: string; appPath: string } => { + const { services } = useKibana(); + return useMemo(() => { + const appPath = `#/fleet/agents/${agentId}/activity`; + return { + url: `${services.application.getUrlForApp('ingestManager')}${appPath}`, + appId: 'ingestManager', + appPath, + }; + }, [services.application, agentId]); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 9690ac5c1b9bf..68943797ea07e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -210,6 +210,7 @@ describe('when on the hosts page', () => { describe('when there is a selected host in the url', () => { let hostDetails: HostInfo; + let agentId: string; const dispatchServerReturnedHostPolicyResponse = ( overallStatus: HostPolicyResponseActionStatus = HostPolicyResponseActionStatus.success ) => { @@ -274,6 +275,8 @@ describe('when on the hosts page', () => { }, }; + agentId = hostDetails.metadata.elastic.agent.id; + coreStart.http.get.mockReturnValue(Promise.resolve(hostDetails)); coreStart.application.getUrlForApp.mockReturnValue('/app/logs'); @@ -404,6 +407,32 @@ describe('when on the hosts page', () => { ).not.toBeNull(); }); + it('should include the link to reassignment in Ingest', async () => { + coreStart.application.getUrlForApp.mockReturnValue('/app/ingestManager'); + const renderResult = render(); + const linkToReassign = await renderResult.findByTestId('hostDetailsLinkToIngest'); + expect(linkToReassign).not.toBeNull(); + expect(linkToReassign.textContent).toEqual('Reassign Policy'); + expect(linkToReassign.getAttribute('href')).toEqual( + `/app/ingestManager#/fleet/agents/${agentId}/activity?openReassignFlyout=true` + ); + }); + + describe('when link to reassignment in Ingest is clicked', () => { + beforeEach(async () => { + coreStart.application.getUrlForApp.mockReturnValue('/app/ingestManager'); + const renderResult = render(); + const linkToReassign = await renderResult.findByTestId('hostDetailsLinkToIngest'); + reactTestingLibrary.act(() => { + reactTestingLibrary.fireEvent.click(linkToReassign); + }); + }); + + it('should navigate to Ingest without full page refresh', () => { + expect(coreStart.application.navigateToApp.mock.calls).toHaveLength(1); + }); + }); + it('should include the link to logs', async () => { const renderResult = render(); const linkToLogs = await renderResult.findByTestId('hostDetailsLinkToLogs'); From 7144db201f5959b7d62636103fd37e8e65e15418 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Mon, 29 Jun 2020 20:44:27 -0600 Subject: [PATCH 04/23] [SIEM][Detection Engine][Lists] Moves getQueryFilter to common folder for use by both front and backend ## Summary * Moves querying and tests from server to common The function we are interested using on the front end is: ```ts export const getQueryFilter = ( query: Query, language: Language, filters: Array>, index: Index, lists: ExceptionListItemSchema[] ) => { ``` ### Checklist - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../build_exceptions_query.test.ts | 8 +- .../build_exceptions_query.ts | 8 +- .../detection_engine/get_query_filter.test.ts | 650 ++++++++++++++++++ .../detection_engine/get_query_filter.ts | 41 ++ .../signals/get_filter.test.ts | 642 +---------------- .../detection_engine/signals/get_filter.ts | 39 +- 6 files changed, 701 insertions(+), 687 deletions(-) rename x-pack/plugins/security_solution/{server/lib/detection_engine/signals => common/detection_engine}/build_exceptions_query.test.ts (99%) rename x-pack/plugins/security_solution/{server/lib/detection_engine/signals => common/detection_engine}/build_exceptions_query.ts (95%) create mode 100644 x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.test.ts b/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts similarity index 99% rename from x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.test.ts rename to x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts index ce7cc50e81d67..ed0344207d18f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts @@ -17,13 +17,13 @@ import { buildNested, } from './build_exceptions_query'; import { - EntriesArray, + EntryNested, EntryExists, EntryMatch, EntryMatchAny, - EntryNested, -} from '../../../../../lists/common/schemas'; -import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; + EntriesArray, +} from '../../../lists/common/schemas'; +import { getExceptionListItemSchemaMock } from '../../../lists/common/schemas/response/exception_list_item_schema.mock'; describe('build_exceptions_query', () => { describe('getLanguageBooleanOperator', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts b/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.ts similarity index 95% rename from x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts rename to x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.ts index ba0d9dec7d1b0..36353d42d26b7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_exceptions_query.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.ts @@ -3,11 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Language, Query } from '../../../../common/detection_engine/schemas/common/schemas'; -import { Query as DataQuery } from '../../../../../../../src/plugins/data/server'; +import { Query as DataQuery } from '../../../../../src/plugins/data/common'; import { Entry, - ExceptionListItemSchema, EntryMatch, EntryMatchAny, EntryNested, @@ -19,7 +17,9 @@ import { entriesMatch, entriesNested, entriesList, -} from '../../../../../lists/common/schemas'; + ExceptionListItemSchema, +} from '../../../lists/common/schemas'; +import { Language, Query } from './schemas/common/schemas'; type Operators = 'and' | 'or' | 'not'; type LuceneOperators = 'AND' | 'OR' | 'NOT'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts new file mode 100644 index 0000000000000..6edd2489e90c9 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts @@ -0,0 +1,650 @@ +/* + * 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 { getQueryFilter } from './get_query_filter'; +import { Filter } from 'src/plugins/data/public'; +import { getExceptionListItemSchemaMock } from '../../../lists/common/schemas/response/exception_list_item_schema.mock'; + +describe('get_filter', () => { + describe('getQueryFilter', () => { + test('it should work with an empty filter as kuery', () => { + const esQuery = getQueryFilter('host.name: linux', 'kuery', [], ['auditbeat-*'], []); + expect(esQuery).toEqual({ + bool: { + must: [], + filter: [ + { + bool: { + should: [ + { + match: { + 'host.name': 'linux', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + should: [], + must_not: [], + }, + }); + }); + + test('it should work with an empty filter as lucene', () => { + const esQuery = getQueryFilter('host.name: linux', 'lucene', [], ['auditbeat-*'], []); + expect(esQuery).toEqual({ + bool: { + must: [ + { + query_string: { + query: 'host.name: linux', + analyze_wildcard: true, + time_zone: 'Zulu', + }, + }, + ], + filter: [], + should: [], + must_not: [], + }, + }); + }); + + test('it should work with a simple filter as a kuery', () => { + const esQuery = getQueryFilter( + 'host.name: windows', + 'kuery', + [ + { + meta: { + alias: 'custom label here', + disabled: false, + key: 'host.name', + negate: false, + params: { + query: 'siem-windows', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + }, + ], + ['auditbeat-*'], + [] + ); + expect(esQuery).toEqual({ + bool: { + must: [], + filter: [ + { + bool: { + should: [ + { + match: { + 'host.name': 'windows', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + ], + should: [], + must_not: [], + }, + }); + }); + + test('it should work with a simple filter as a kuery without meta information', () => { + const esQuery = getQueryFilter( + 'host.name: windows', + 'kuery', + [ + { + query: { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + }, + ], + ['auditbeat-*'], + [] + ); + expect(esQuery).toEqual({ + bool: { + must: [], + filter: [ + { + bool: { + should: [ + { + match: { + 'host.name': 'windows', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + ], + should: [], + must_not: [], + }, + }); + }); + + test('it should work with a simple filter as a kuery without meta information with an exists', () => { + const query: Partial = { + query: { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + }; + + const exists: Partial = { + exists: { + field: 'host.hostname', + }, + } as Partial; + + const esQuery = getQueryFilter( + 'host.name: windows', + 'kuery', + [query, exists], + ['auditbeat-*'], + [] + ); + expect(esQuery).toEqual({ + bool: { + must: [], + filter: [ + { + bool: { + should: [ + { + match: { + 'host.name': 'windows', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + { + exists: { + field: 'host.hostname', + }, + }, + ], + should: [], + must_not: [], + }, + }); + }); + + test('it should work with a simple filter that is disabled as a kuery', () => { + const esQuery = getQueryFilter( + 'host.name: windows', + 'kuery', + [ + { + meta: { + alias: 'custom label here', + disabled: true, + key: 'host.name', + negate: false, + params: { + query: 'siem-windows', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + }, + ], + ['auditbeat-*'], + [] + ); + expect(esQuery).toEqual({ + bool: { + must: [], + filter: [ + { + bool: { + should: [ + { + match: { + 'host.name': 'windows', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + should: [], + must_not: [], + }, + }); + }); + + test('it should work with a simple filter as a lucene', () => { + const esQuery = getQueryFilter( + 'host.name: windows', + 'lucene', + [ + { + meta: { + alias: 'custom label here', + disabled: false, + key: 'host.name', + negate: false, + params: { + query: 'siem-windows', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + }, + ], + ['auditbeat-*'], + [] + ); + expect(esQuery).toEqual({ + bool: { + must: [ + { + query_string: { + query: 'host.name: windows', + analyze_wildcard: true, + time_zone: 'Zulu', + }, + }, + ], + filter: [ + { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + ], + should: [], + must_not: [], + }, + }); + }); + + test('it should work with a simple filter that is disabled as a lucene', () => { + const esQuery = getQueryFilter( + 'host.name: windows', + 'lucene', + [ + { + meta: { + alias: 'custom label here', + disabled: true, + key: 'host.name', + negate: false, + params: { + query: 'siem-windows', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + }, + ], + ['auditbeat-*'], + [] + ); + expect(esQuery).toEqual({ + bool: { + must: [ + { + query_string: { + query: 'host.name: windows', + analyze_wildcard: true, + time_zone: 'Zulu', + }, + }, + ], + filter: [], + should: [], + must_not: [], + }, + }); + }); + + test('it should work with a list', () => { + const esQuery = getQueryFilter( + 'host.name: linux', + 'kuery', + [], + ['auditbeat-*'], + [getExceptionListItemSchemaMock()] + ); + expect(esQuery).toEqual({ + bool: { + filter: [ + { + bool: { + filter: [ + { + bool: { + minimum_should_match: 1, + should: [ + { + match: { + 'host.name': 'linux', + }, + }, + ], + }, + }, + { + bool: { + filter: [ + { + nested: { + path: 'some.parentField', + query: { + bool: { + minimum_should_match: 1, + should: [ + { + match: { + 'some.parentField.nested.field': 'some value', + }, + }, + ], + }, + }, + score_mode: 'none', + }, + }, + { + bool: { + must_not: { + bool: { + minimum_should_match: 1, + should: [ + { + match: { + 'some.not.nested.field': 'some value', + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + ], + }, + }, + ], + must: [], + must_not: [], + should: [], + }, + }); + }); + + test('it should work with an empty list', () => { + const esQuery = getQueryFilter('host.name: linux', 'kuery', [], ['auditbeat-*'], []); + expect(esQuery).toEqual({ + bool: { + filter: [ + { bool: { minimum_should_match: 1, should: [{ match: { 'host.name': 'linux' } }] } }, + ], + must: [], + must_not: [], + should: [], + }, + }); + }); + + test('it should work when lists has value undefined', () => { + const esQuery = getQueryFilter('host.name: linux', 'kuery', [], ['auditbeat-*'], []); + expect(esQuery).toEqual({ + bool: { + filter: [ + { bool: { minimum_should_match: 1, should: [{ match: { 'host.name': 'linux' } }] } }, + ], + must: [], + must_not: [], + should: [], + }, + }); + }); + + test('it should work with a nested object queries', () => { + const esQuery = getQueryFilter( + 'category:{ name:Frank and trusted:true }', + 'kuery', + [], + ['auditbeat-*'], + [] + ); + expect(esQuery).toEqual({ + bool: { + must: [], + filter: [ + { + nested: { + path: 'category', + query: { + bool: { + filter: [ + { + bool: { + should: [ + { + match: { + 'category.name': 'Frank', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + match: { + 'category.trusted': true, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + score_mode: 'none', + }, + }, + ], + should: [], + must_not: [], + }, + }); + }); + + test('it works with references and does not add indexes', () => { + const esQuery = getQueryFilter( + '(event.module:suricata and event.kind:alert) and suricata.eve.alert.signature_id: (2610182 or 2610183 or 2610184 or 2610185 or 2610186 or 2610187)', + 'kuery', + [], + ['my custom index'], + [] + ); + expect(esQuery).toEqual({ + bool: { + must: [], + filter: [ + { + bool: { + filter: [ + { + bool: { + filter: [ + { + bool: { + should: [{ match: { 'event.module': 'suricata' } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [{ match: { 'event.kind': 'alert' } }], + minimum_should_match: 1, + }, + }, + ], + }, + }, + { + bool: { + should: [ + { + bool: { + should: [{ match: { 'suricata.eve.alert.signature_id': 2610182 } }], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + bool: { + should: [ + { match: { 'suricata.eve.alert.signature_id': 2610183 } }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + bool: { + should: [ + { match: { 'suricata.eve.alert.signature_id': 2610184 } }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + bool: { + should: [ + { + match: { + 'suricata.eve.alert.signature_id': 2610185, + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + bool: { + should: [ + { + match: { + 'suricata.eve.alert.signature_id': 2610186, + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + match: { + 'suricata.eve.alert.signature_id': 2610187, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + ], + should: [], + must_not: [], + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts new file mode 100644 index 0000000000000..ef390c3b44939 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts @@ -0,0 +1,41 @@ +/* + * 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 { + Filter, + IIndexPattern, + isFilterDisabled, + buildEsQuery, + Query as DataQuery, +} from '../../../../../src/plugins/data/common'; +import { ExceptionListItemSchema } from '../../../lists/common/schemas'; +import { buildQueryExceptions } from './build_exceptions_query'; +import { Query, Language, Index } from './schemas/common/schemas'; + +export const getQueryFilter = ( + query: Query, + language: Language, + filters: Array>, + index: Index, + lists: ExceptionListItemSchema[] +) => { + const indexPattern: IIndexPattern = { + fields: [], + title: index.join(), + }; + + const queries: DataQuery[] = buildQueryExceptions({ query, language, lists }); + + const config = { + allowLeadingWildcards: true, + queryStringOptions: { analyze_wildcard: true }, + ignoreFilterIfFieldNotInIndex: false, + dateFormatTZ: 'Zulu', + }; + + const enabledFilters = ((filters as unknown) as Filter[]).filter((f) => !isFilterDisabled(f)); + return buildEsQuery(indexPattern, queries, enabledFilters, config); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts index 9b3a446bc666d..f34879781e0b0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getQueryFilter, getFilter } from './get_filter'; -import { PartialFilter } from '../types'; +import { getFilter } from './get_filter'; import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; @@ -34,509 +33,6 @@ describe('get_filter', () => { jest.resetAllMocks(); }); - describe('getQueryFilter', () => { - test('it should work with an empty filter as kuery', () => { - const esQuery = getQueryFilter('host.name: linux', 'kuery', [], ['auditbeat-*'], []); - expect(esQuery).toEqual({ - bool: { - must: [], - filter: [ - { - bool: { - should: [ - { - match: { - 'host.name': 'linux', - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - should: [], - must_not: [], - }, - }); - }); - - test('it should work with an empty filter as lucene', () => { - const esQuery = getQueryFilter('host.name: linux', 'lucene', [], ['auditbeat-*'], []); - expect(esQuery).toEqual({ - bool: { - must: [ - { - query_string: { - query: 'host.name: linux', - analyze_wildcard: true, - time_zone: 'Zulu', - }, - }, - ], - filter: [], - should: [], - must_not: [], - }, - }); - }); - - test('it should work with a simple filter as a kuery', () => { - const esQuery = getQueryFilter( - 'host.name: windows', - 'kuery', - [ - { - meta: { - alias: 'custom label here', - disabled: false, - key: 'host.name', - negate: false, - params: { - query: 'siem-windows', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'host.name': 'siem-windows', - }, - }, - }, - ], - ['auditbeat-*'], - [] - ); - expect(esQuery).toEqual({ - bool: { - must: [], - filter: [ - { - bool: { - should: [ - { - match: { - 'host.name': 'windows', - }, - }, - ], - minimum_should_match: 1, - }, - }, - { - match_phrase: { - 'host.name': 'siem-windows', - }, - }, - ], - should: [], - must_not: [], - }, - }); - }); - - test('it should work with a simple filter as a kuery without meta information', () => { - const esQuery = getQueryFilter( - 'host.name: windows', - 'kuery', - [ - { - query: { - match_phrase: { - 'host.name': 'siem-windows', - }, - }, - }, - ], - ['auditbeat-*'], - [] - ); - expect(esQuery).toEqual({ - bool: { - must: [], - filter: [ - { - bool: { - should: [ - { - match: { - 'host.name': 'windows', - }, - }, - ], - minimum_should_match: 1, - }, - }, - { - match_phrase: { - 'host.name': 'siem-windows', - }, - }, - ], - should: [], - must_not: [], - }, - }); - }); - - test('it should work with a simple filter as a kuery without meta information with an exists', () => { - const query: PartialFilter = { - query: { - match_phrase: { - 'host.name': 'siem-windows', - }, - }, - } as PartialFilter; - - const exists: PartialFilter = { - exists: { - field: 'host.hostname', - }, - } as PartialFilter; - - const esQuery = getQueryFilter( - 'host.name: windows', - 'kuery', - [query, exists], - ['auditbeat-*'], - [] - ); - expect(esQuery).toEqual({ - bool: { - must: [], - filter: [ - { - bool: { - should: [ - { - match: { - 'host.name': 'windows', - }, - }, - ], - minimum_should_match: 1, - }, - }, - { - match_phrase: { - 'host.name': 'siem-windows', - }, - }, - { - exists: { - field: 'host.hostname', - }, - }, - ], - should: [], - must_not: [], - }, - }); - }); - - test('it should work with a simple filter that is disabled as a kuery', () => { - const esQuery = getQueryFilter( - 'host.name: windows', - 'kuery', - [ - { - meta: { - alias: 'custom label here', - disabled: true, - key: 'host.name', - negate: false, - params: { - query: 'siem-windows', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'host.name': 'siem-windows', - }, - }, - }, - ], - ['auditbeat-*'], - [] - ); - expect(esQuery).toEqual({ - bool: { - must: [], - filter: [ - { - bool: { - should: [ - { - match: { - 'host.name': 'windows', - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - should: [], - must_not: [], - }, - }); - }); - - test('it should work with a simple filter as a lucene', () => { - const esQuery = getQueryFilter( - 'host.name: windows', - 'lucene', - [ - { - meta: { - alias: 'custom label here', - disabled: false, - key: 'host.name', - negate: false, - params: { - query: 'siem-windows', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'host.name': 'siem-windows', - }, - }, - }, - ], - ['auditbeat-*'], - [] - ); - expect(esQuery).toEqual({ - bool: { - must: [ - { - query_string: { - query: 'host.name: windows', - analyze_wildcard: true, - time_zone: 'Zulu', - }, - }, - ], - filter: [ - { - match_phrase: { - 'host.name': 'siem-windows', - }, - }, - ], - should: [], - must_not: [], - }, - }); - }); - - test('it should work with a simple filter that is disabled as a lucene', () => { - const esQuery = getQueryFilter( - 'host.name: windows', - 'lucene', - [ - { - meta: { - alias: 'custom label here', - disabled: true, - key: 'host.name', - negate: false, - params: { - query: 'siem-windows', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'host.name': 'siem-windows', - }, - }, - }, - ], - ['auditbeat-*'], - [] - ); - expect(esQuery).toEqual({ - bool: { - must: [ - { - query_string: { - query: 'host.name: windows', - analyze_wildcard: true, - time_zone: 'Zulu', - }, - }, - ], - filter: [], - should: [], - must_not: [], - }, - }); - }); - - test('it should work with a list', () => { - const esQuery = getQueryFilter( - 'host.name: linux', - 'kuery', - [], - ['auditbeat-*'], - [getExceptionListItemSchemaMock()] - ); - expect(esQuery).toEqual({ - bool: { - filter: [ - { - bool: { - filter: [ - { - bool: { - minimum_should_match: 1, - should: [ - { - match: { - 'host.name': 'linux', - }, - }, - ], - }, - }, - { - bool: { - filter: [ - { - nested: { - path: 'some.parentField', - query: { - bool: { - minimum_should_match: 1, - should: [ - { - match: { - 'some.parentField.nested.field': 'some value', - }, - }, - ], - }, - }, - score_mode: 'none', - }, - }, - { - bool: { - must_not: { - bool: { - minimum_should_match: 1, - should: [ - { - match: { - 'some.not.nested.field': 'some value', - }, - }, - ], - }, - }, - }, - }, - ], - }, - }, - ], - }, - }, - ], - must: [], - must_not: [], - should: [], - }, - }); - }); - - test('it should work with an empty list', () => { - const esQuery = getQueryFilter('host.name: linux', 'kuery', [], ['auditbeat-*'], []); - expect(esQuery).toEqual({ - bool: { - filter: [ - { bool: { minimum_should_match: 1, should: [{ match: { 'host.name': 'linux' } }] } }, - ], - must: [], - must_not: [], - should: [], - }, - }); - }); - - test('it should work when lists has value undefined', () => { - const esQuery = getQueryFilter('host.name: linux', 'kuery', [], ['auditbeat-*'], []); - expect(esQuery).toEqual({ - bool: { - filter: [ - { bool: { minimum_should_match: 1, should: [{ match: { 'host.name': 'linux' } }] } }, - ], - must: [], - must_not: [], - should: [], - }, - }); - }); - - test('it should work with a nested object queries', () => { - const esQuery = getQueryFilter( - 'category:{ name:Frank and trusted:true }', - 'kuery', - [], - ['auditbeat-*'], - [] - ); - expect(esQuery).toEqual({ - bool: { - must: [], - filter: [ - { - nested: { - path: 'category', - query: { - bool: { - filter: [ - { - bool: { - should: [ - { - match: { - 'category.name': 'Frank', - }, - }, - ], - minimum_should_match: 1, - }, - }, - { - bool: { - should: [ - { - match: { - 'category.trusted': true, - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - }, - }, - score_mode: 'none', - }, - }, - ], - should: [], - must_not: [], - }, - }); - }); - }); - describe('getFilter', () => { test('returns a query if given a type of query', async () => { const filter = await getFilter({ @@ -685,142 +181,6 @@ describe('get_filter', () => { ).rejects.toThrow('Unsupported Rule of type "machine_learning" supplied to getFilter'); }); - test('it works with references and does not add indexes', () => { - const esQuery = getQueryFilter( - '(event.module:suricata and event.kind:alert) and suricata.eve.alert.signature_id: (2610182 or 2610183 or 2610184 or 2610185 or 2610186 or 2610187)', - 'kuery', - [], - ['my custom index'], - [] - ); - expect(esQuery).toEqual({ - bool: { - must: [], - filter: [ - { - bool: { - filter: [ - { - bool: { - filter: [ - { - bool: { - should: [{ match: { 'event.module': 'suricata' } }], - minimum_should_match: 1, - }, - }, - { - bool: { - should: [{ match: { 'event.kind': 'alert' } }], - minimum_should_match: 1, - }, - }, - ], - }, - }, - { - bool: { - should: [ - { - bool: { - should: [{ match: { 'suricata.eve.alert.signature_id': 2610182 } }], - minimum_should_match: 1, - }, - }, - { - bool: { - should: [ - { - bool: { - should: [ - { match: { 'suricata.eve.alert.signature_id': 2610183 } }, - ], - minimum_should_match: 1, - }, - }, - { - bool: { - should: [ - { - bool: { - should: [ - { match: { 'suricata.eve.alert.signature_id': 2610184 } }, - ], - minimum_should_match: 1, - }, - }, - { - bool: { - should: [ - { - bool: { - should: [ - { - match: { - 'suricata.eve.alert.signature_id': 2610185, - }, - }, - ], - minimum_should_match: 1, - }, - }, - { - bool: { - should: [ - { - bool: { - should: [ - { - match: { - 'suricata.eve.alert.signature_id': 2610186, - }, - }, - ], - minimum_should_match: 1, - }, - }, - { - bool: { - should: [ - { - match: { - 'suricata.eve.alert.signature_id': 2610187, - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - }, - }, - ], - should: [], - must_not: [], - }, - }); - }); - test('returns a query when given a list', async () => { const filter = await getFilter({ type: 'query', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts index 50ce01aaa6f74..4bd9de734f448 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getQueryFilter } from '../../../../common/detection_engine/get_query_filter'; import { LanguageOrUndefined, QueryOrUndefined, @@ -11,50 +12,12 @@ import { SavedIdOrUndefined, IndexOrUndefined, Language, - Index, - Query, } from '../../../../common/detection_engine/schemas/common/schemas'; import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { AlertServices } from '../../../../../alerts/server'; import { assertUnreachable } from '../../../utils/build_query'; -import { - Filter, - Query as DataQuery, - esQuery, - esFilters, - IIndexPattern, -} from '../../../../../../../src/plugins/data/server'; import { PartialFilter } from '../types'; import { BadRequestError } from '../errors/bad_request_error'; -import { buildQueryExceptions } from './build_exceptions_query'; - -export const getQueryFilter = ( - query: Query, - language: Language, - filters: PartialFilter[], - index: Index, - lists: ExceptionListItemSchema[] -) => { - const indexPattern = { - fields: [], - title: index.join(), - } as IIndexPattern; - - const queries: DataQuery[] = buildQueryExceptions({ query, language, lists }); - - const config = { - allowLeadingWildcards: true, - queryStringOptions: { analyze_wildcard: true }, - ignoreFilterIfFieldNotInIndex: false, - dateFormatTZ: 'Zulu', - }; - - const enabledFilters = ((filters as unknown) as Filter[]).filter( - (f) => f && !esFilters.isFilterDisabled(f) - ); - - return esQuery.buildEsQuery(indexPattern, queries, enabledFilters, config); -}; interface GetFilterArgs { type: Type; From 159369b71999b1f9d8df5645c0935044d25bb8bc Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 30 Jun 2020 08:37:42 +0300 Subject: [PATCH 05/23] Use ts-expect-error in platform code (#69883) * ts-ignore --> ts-expect-error * fix error with mutable array * fix errors in consumers code * update SOM * fix FeatureConfig & Feature compatibility * do not re-export from code. it breaks built version * update docs * add eslint rule for platform team code * remove test. this is covered by ts-expect-error in unit tests Co-authored-by: Elastic Machine --- .eslintrc.js | 17 +++++++ .../core/public/kibana-plugin-core-public.md | 1 - ...na-plugin-core-public.recursivereadonly.md | 14 ------ .../core/server/kibana-plugin-core-server.md | 1 - ...na-plugin-core-server.recursivereadonly.md | 14 ------ ...in-plugins-data-public.querystringinput.md | 2 +- packages/kbn-utility-types/index.ts | 3 +- .../capabilities/capabilities_service.test.ts | 4 +- .../capabilities/capabilities_service.tsx | 3 +- src/core/public/application/types.ts | 2 +- .../recently_accessed/persisted_log.test.ts | 2 +- .../recently_accessed_service.test.ts | 4 +- .../chrome/ui/header/header_help_menu.tsx | 1 - .../public/chrome/ui/header/recent_links.tsx | 1 - src/core/public/http/fetch.test.ts | 4 +- src/core/public/http/http_service.test.ts | 2 +- src/core/public/index.ts | 1 - .../injected_metadata_service.test.ts | 7 ++- .../integrations/styles/styles_service.ts | 2 +- src/core/public/kbn_bootstrap.ts | 1 - src/core/public/public.api.md | 8 +-- .../saved_objects_client.test.ts | 2 +- .../ui_settings/ui_settings_api.test.ts | 2 +- .../legacy/retry_call_cluster.ts | 2 +- .../server/http/cookie_session_storage.ts | 2 +- src/core/server/http/router/request.ts | 5 +- src/core/server/index.ts | 1 - .../legacy/config/get_unused_config_keys.ts | 2 +- src/core/server/legacy/legacy_service.test.ts | 1 - .../legacy/logging/legacy_logging_server.ts | 4 +- .../plugins/find_legacy_plugin_specs.ts | 2 +- src/core/server/plugins/types.ts | 2 +- .../service/lib/decorate_es_error.ts | 6 +-- src/core/server/server.api.md | 13 ++--- src/core/utils/deep_freeze.test.ts | 11 ++-- src/core/utils/deep_freeze.ts | 15 +----- .../frozen_object_mutation/index.ts | 44 ---------------- .../frozen_object_mutation/tsconfig.json | 12 ----- .../integration_tests/deep_freeze.test.ts | 40 --------------- src/plugins/data/public/public.api.md | 3 +- src/plugins/data/server/server.api.md | 2 +- .../object_view/components/field.tsx | 9 +--- .../objects_table/components/flyout.tsx | 1 - .../objects_table/components/table.test.tsx | 2 +- .../objects_table/components/table.tsx | 1 - .../objects_table/saved_objects_table.tsx | 2 +- .../vis_type_timelion/server/plugin.ts | 8 ++- .../lib/adapters/framework/adapter_types.ts | 4 +- .../lib/adapters/framework/adapter_types.ts | 2 +- ...dashboard_mode_request_interceptor.test.ts | 4 +- x-pack/plugins/features/common/feature.ts | 14 +++--- .../common/feature_kibana_privileges.ts | 14 +++--- x-pack/plugins/features/common/sub_feature.ts | 4 +- .../plugins/features/server/feature_schema.ts | 6 +-- x-pack/plugins/features/server/plugin.ts | 3 +- .../embeddable/embeddable_factory.ts | 8 +-- x-pack/plugins/security/common/model/user.ts | 2 +- .../role_combo_box/role_combo_box.tsx | 2 +- .../users/edit_user/edit_user_page.tsx | 2 +- .../authorization/app_authorization.test.ts | 9 ++-- .../feature_privilege_iterator.ts | 2 +- x-pack/plugins/security/server/index.ts | 2 +- .../management/lib/feature_utils.test.ts | 4 +- .../on_post_auth_interceptor.test.ts | 50 +++++++++---------- 64 files changed, 134 insertions(+), 286 deletions(-) delete mode 100644 docs/development/core/public/kibana-plugin-core-public.recursivereadonly.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.recursivereadonly.md delete mode 100644 src/core/utils/integration_tests/__fixtures__/frozen_object_mutation/index.ts delete mode 100644 src/core/utils/integration_tests/__fixtures__/frozen_object_mutation/tsconfig.json delete mode 100644 src/core/utils/integration_tests/deep_freeze.test.ts diff --git a/.eslintrc.js b/.eslintrc.js index ffc49a60d5bca..32f59c4d6b3db 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1039,5 +1039,22 @@ module.exports = { ...require('eslint-config-prettier/@typescript-eslint').rules, }, }, + + { + files: [ + // platform-team owned code + 'src/core/**', + 'x-pack/plugins/features/**', + 'x-pack/plugins/licensing/**', + 'x-pack/plugins/global_search/**', + 'x-pack/plugins/cloud/**', + 'packages/kbn-config-schema', + 'src/plugins/status_page/**', + 'src/plugins/saved_objects_management/**', + ], + rules: { + '@typescript-eslint/prefer-ts-expect-error': 'error', + }, + }, ], }; diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index b0612ff4d5b65..8f2bde3856019 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -172,7 +172,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PublicAppInfo](./kibana-plugin-core-public.publicappinfo.md) | Public information about a registered [application](./kibana-plugin-core-public.app.md) | | [PublicLegacyAppInfo](./kibana-plugin-core-public.publiclegacyappinfo.md) | Information about a registered [legacy application](./kibana-plugin-core-public.legacyapp.md) | | [PublicUiSettingsParams](./kibana-plugin-core-public.publicuisettingsparams.md) | A sub-set of [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) exposed to the client-side. | -| [RecursiveReadonly](./kibana-plugin-core-public.recursivereadonly.md) | | | [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-core-public.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | | [SavedObjectsClientContract](./kibana-plugin-core-public.savedobjectsclientcontract.md) | SavedObjectsClientContract as implemented by the [SavedObjectsClient](./kibana-plugin-core-public.savedobjectsclient.md) | diff --git a/docs/development/core/public/kibana-plugin-core-public.recursivereadonly.md b/docs/development/core/public/kibana-plugin-core-public.recursivereadonly.md deleted file mode 100644 index 2f47ef1086d71..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.recursivereadonly.md +++ /dev/null @@ -1,14 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [RecursiveReadonly](./kibana-plugin-core-public.recursivereadonly.md) - -## RecursiveReadonly type - - -Signature: - -```typescript -export declare type RecursiveReadonly = T extends (...args: any[]) => any ? T : T extends any[] ? RecursiveReadonlyArray : T extends object ? Readonly<{ - [K in keyof T]: RecursiveReadonly; -}> : T; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 7e777d51f147f..f73595ea0a8ff 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -257,7 +257,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginName](./kibana-plugin-core-server.pluginname.md) | Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays that use it as a key or value more obvious. | | [PluginOpaqueId](./kibana-plugin-core-server.pluginopaqueid.md) | | | [PublicUiSettingsParams](./kibana-plugin-core-server.publicuisettingsparams.md) | A sub-set of [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) exposed to the client-side. | -| [RecursiveReadonly](./kibana-plugin-core-server.recursivereadonly.md) | | | [RedirectResponseOptions](./kibana-plugin-core-server.redirectresponseoptions.md) | HTTP response parameters for redirection response | | [RequestHandler](./kibana-plugin-core-server.requesthandler.md) | A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) functions. | | [RequestHandlerContextContainer](./kibana-plugin-core-server.requesthandlercontextcontainer.md) | An object that handles registration of http request context providers. | diff --git a/docs/development/core/server/kibana-plugin-core-server.recursivereadonly.md b/docs/development/core/server/kibana-plugin-core-server.recursivereadonly.md deleted file mode 100644 index bc9cd4680b17d..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.recursivereadonly.md +++ /dev/null @@ -1,14 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [RecursiveReadonly](./kibana-plugin-core-server.recursivereadonly.md) - -## RecursiveReadonly type - - -Signature: - -```typescript -export declare type RecursiveReadonly = T extends (...args: any[]) => any ? T : T extends any[] ? RecursiveReadonlyArray : T extends object ? Readonly<{ - [K in keyof T]: RecursiveReadonly; -}> : T; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md index 85eb4825bc2e3..a25f4a0c373b2 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md @@ -7,5 +7,5 @@ Signature: ```typescript -QueryStringInput: React.FC> +QueryStringInput: React.FC> ``` diff --git a/packages/kbn-utility-types/index.ts b/packages/kbn-utility-types/index.ts index 9a8a81460f410..6ccfeb8ab052c 100644 --- a/packages/kbn-utility-types/index.ts +++ b/packages/kbn-utility-types/index.ts @@ -61,7 +61,8 @@ export type Ensure = T extends X ? T : never; // If we define this inside RecursiveReadonly TypeScript complains. // eslint-disable-next-line @typescript-eslint/no-empty-interface -interface RecursiveReadonlyArray extends Array> {} +export interface RecursiveReadonlyArray extends ReadonlyArray> {} + export type RecursiveReadonly = T extends (...args: any) => any ? T : T extends any[] diff --git a/src/core/public/application/capabilities/capabilities_service.test.ts b/src/core/public/application/capabilities/capabilities_service.test.ts index dfbb449b4d58e..286a93fdc2398 100644 --- a/src/core/public/application/capabilities/capabilities_service.test.ts +++ b/src/core/public/application/capabilities/capabilities_service.test.ts @@ -48,7 +48,7 @@ describe('#start', () => { appIds: ['app1', 'app2', 'legacyApp1', 'legacyApp2'], }); - // @ts-ignore TypeScript knows this shouldn't be possible + // @ts-expect-error TypeScript knows this shouldn't be possible expect(() => (capabilities.foo = 'foo')).toThrowError(); }); @@ -59,7 +59,7 @@ describe('#start', () => { appIds: ['app1', 'app2', 'legacyApp1', 'legacyApp2'], }); - // @ts-ignore TypeScript knows this shouldn't be possible + // @ts-expect-error TypeScript knows this shouldn't be possible expect(() => (capabilities.foo = 'foo')).toThrowError(); }); }); diff --git a/src/core/public/application/capabilities/capabilities_service.tsx b/src/core/public/application/capabilities/capabilities_service.tsx index d602422c14634..7304a8e5a66bc 100644 --- a/src/core/public/application/capabilities/capabilities_service.tsx +++ b/src/core/public/application/capabilities/capabilities_service.tsx @@ -16,9 +16,10 @@ * specific language governing permissions and limitations * under the License. */ +import { RecursiveReadonly } from '@kbn/utility-types'; import { Capabilities } from '../../../types/capabilities'; -import { deepFreeze, RecursiveReadonly } from '../../../utils'; +import { deepFreeze } from '../../../utils'; import { HttpStart } from '../../http'; interface StartDeps { diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index cd2dd99c30c11..0fe97431b1569 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -19,6 +19,7 @@ import { Observable } from 'rxjs'; import { History } from 'history'; +import { RecursiveReadonly } from '@kbn/utility-types'; import { Capabilities } from './capabilities'; import { ChromeStart } from '../chrome'; @@ -30,7 +31,6 @@ import { NotificationsStart } from '../notifications'; import { OverlayStart } from '../overlays'; import { PluginOpaqueId } from '../plugins'; import { IUiSettingsClient } from '../ui_settings'; -import { RecursiveReadonly } from '../../utils'; import { SavedObjectsStart } from '../saved_objects'; import { AppCategory } from '../../types'; import { ScopedHistory } from './scoped_history'; diff --git a/src/core/public/chrome/recently_accessed/persisted_log.test.ts b/src/core/public/chrome/recently_accessed/persisted_log.test.ts index 9b307a2d25faf..4229efdf7ca9d 100644 --- a/src/core/public/chrome/recently_accessed/persisted_log.test.ts +++ b/src/core/public/chrome/recently_accessed/persisted_log.test.ts @@ -59,7 +59,7 @@ describe('PersistedLog', () => { describe('internal functionality', () => { test('reads from storage', () => { - // @ts-ignore + // @ts-expect-error const log = new PersistedLog(historyName, { maxLength: 10 }, storage); expect(storage.getItem).toHaveBeenCalledTimes(1); diff --git a/src/core/public/chrome/recently_accessed/recently_accessed_service.test.ts b/src/core/public/chrome/recently_accessed/recently_accessed_service.test.ts index 3c9713a93144a..14c3c581f9f17 100644 --- a/src/core/public/chrome/recently_accessed/recently_accessed_service.test.ts +++ b/src/core/public/chrome/recently_accessed/recently_accessed_service.test.ts @@ -55,11 +55,11 @@ describe('RecentlyAccessed#start()', () => { let originalLocalStorage: Storage; beforeAll(() => { originalLocalStorage = window.localStorage; - // @ts-ignore + // @ts-expect-error window.localStorage = new LocalStorageMock(); }); beforeEach(() => localStorage.clear()); - // @ts-ignore + // @ts-expect-error afterAll(() => (window.localStorage = originalLocalStorage)); const getStart = async () => { diff --git a/src/core/public/chrome/ui/header/header_help_menu.tsx b/src/core/public/chrome/ui/header/header_help_menu.tsx index 1023a561a0fe3..6d2938e3345a6 100644 --- a/src/core/public/chrome/ui/header/header_help_menu.tsx +++ b/src/core/public/chrome/ui/header/header_help_menu.tsx @@ -312,7 +312,6 @@ class HeaderHelpMenuUI extends Component { ); return ( - // @ts-ignore repositionOnScroll doesn't exist in EuiPopover { fetchMock.get('*', {}); await expect( fetchInstance.fetch( - // @ts-ignore + // @ts-expect-error { path: '/', headers: { hello: 'world' } }, { headers: { hello: 'mars' } } ) diff --git a/src/core/public/http/http_service.test.ts b/src/core/public/http/http_service.test.ts index 78220af9cc83b..0afea5aaa506a 100644 --- a/src/core/public/http/http_service.test.ts +++ b/src/core/public/http/http_service.test.ts @@ -17,7 +17,7 @@ * under the License. */ -// @ts-ignore +// @ts-expect-error import fetchMock from 'fetch-mock/es5/client'; import { loadingServiceMock } from './http_service.test.mocks'; diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 41af0f1b8395f..3e4e70fb99508 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -81,7 +81,6 @@ import { export { CoreContext, CoreSystem } from './core_system'; export { - RecursiveReadonly, DEFAULT_APP_CATEGORIES, getFlattenedObject, URLMeaningfulParts, diff --git a/src/core/public/injected_metadata/injected_metadata_service.test.ts b/src/core/public/injected_metadata/injected_metadata_service.test.ts index cf4b72114d5ac..1a8b4d14ee249 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.test.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.test.ts @@ -58,7 +58,6 @@ describe('setup.getCspConfig()', () => { const csp = injectedMetadata.setup().getCspConfig(); expect(() => { - // @ts-ignore TS knows this shouldn't be possible csp.warnLegacyBrowsers = false; }).toThrowError(); }); @@ -100,11 +99,11 @@ describe('setup.getPlugins()', () => { plugins.push({ id: 'new-plugin', plugin: {} as DiscoveredPlugin }); }).toThrowError(); expect(() => { - // @ts-ignore TS knows this shouldn't be possible + // @ts-expect-error TS knows this shouldn't be possible plugins[0].name = 'changed'; }).toThrowError(); expect(() => { - // @ts-ignore TS knows this shouldn't be possible + // @ts-expect-error TS knows this shouldn't be possible plugins[0].newProp = 'changed'; }).toThrowError(); }); @@ -136,7 +135,7 @@ describe('setup.getLegacyMetadata()', () => { foo: true, }); expect(() => { - // @ts-ignore TS knows this shouldn't be possible + // @ts-expect-error TS knows this shouldn't be possible legacyMetadata.foo = false; }).toThrowError(); }); diff --git a/src/core/public/integrations/styles/styles_service.ts b/src/core/public/integrations/styles/styles_service.ts index 41fc861d6cb39..d1d7f2170fde3 100644 --- a/src/core/public/integrations/styles/styles_service.ts +++ b/src/core/public/integrations/styles/styles_service.ts @@ -21,7 +21,7 @@ import { Subscription } from 'rxjs'; import { IUiSettingsClient } from '../../ui_settings'; import { CoreService } from '../../../types'; -// @ts-ignore +// @ts-expect-error import disableAnimationsCss from '!!raw-loader!./disable_animations.css'; interface StartDeps { diff --git a/src/core/public/kbn_bootstrap.ts b/src/core/public/kbn_bootstrap.ts index 0f86061816701..a108b5aaa47ec 100644 --- a/src/core/public/kbn_bootstrap.ts +++ b/src/core/public/kbn_bootstrap.ts @@ -42,7 +42,6 @@ export function __kbnBootstrap__() { const APM_ENABLED = process.env.IS_KIBANA_DISTRIBUTABLE !== 'true' && apmConfig != null; if (APM_ENABLED) { - // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-var-requires const { init, apm } = require('@elastic/apm-rum'); if (apmConfig.globalLabels) { diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index a65b9dd9d242a..86e281a49b744 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -116,6 +116,7 @@ import { PublicUiSettingsParams as PublicUiSettingsParams_2 } from 'src/core/ser import { PutScriptParams } from 'elasticsearch'; import { PutTemplateParams } from 'elasticsearch'; import React from 'react'; +import { RecursiveReadonly } from '@kbn/utility-types'; import { ReindexParams } from 'elasticsearch'; import { ReindexRethrottleParams } from 'elasticsearch'; import { RenderSearchTemplateParams } from 'elasticsearch'; @@ -1158,13 +1159,6 @@ export type PublicLegacyAppInfo = Omit & { // @public export type PublicUiSettingsParams = Omit; -// Warning: (ae-forgotten-export) The symbol "RecursiveReadonlyArray" needs to be exported by the entry point index.d.ts -// -// @public (undocumented) -export type RecursiveReadonly = T extends (...args: any[]) => any ? T : T extends any[] ? RecursiveReadonlyArray : T extends object ? Readonly<{ - [K in keyof T]: RecursiveReadonly; -}> : T; - // Warning: (ae-missing-release-tag) "SavedObject" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/src/core/public/saved_objects/saved_objects_client.test.ts b/src/core/public/saved_objects/saved_objects_client.test.ts index 0c34a16c68e99..20824af38af0f 100644 --- a/src/core/public/saved_objects/saved_objects_client.test.ts +++ b/src/core/public/saved_objects/saved_objects_client.test.ts @@ -432,7 +432,7 @@ describe('SavedObjectsClient', () => { sortOrder: 'sort', // Not currently supported by API }; - // @ts-ignore + // @ts-expect-error savedObjectsClient.find(options); expect(http.fetch.mock.calls).toMatchInlineSnapshot(` Array [ diff --git a/src/core/public/ui_settings/ui_settings_api.test.ts b/src/core/public/ui_settings/ui_settings_api.test.ts index bab7081509d53..14791407d2550 100644 --- a/src/core/public/ui_settings/ui_settings_api.test.ts +++ b/src/core/public/ui_settings/ui_settings_api.test.ts @@ -17,7 +17,7 @@ * under the License. */ -// @ts-ignore +// @ts-expect-error import fetchMock from 'fetch-mock/es5/client'; import * as Rx from 'rxjs'; import { takeUntil, toArray } from 'rxjs/operators'; diff --git a/src/core/server/elasticsearch/legacy/retry_call_cluster.ts b/src/core/server/elasticsearch/legacy/retry_call_cluster.ts index b12ecc889eb2d..475a76d406017 100644 --- a/src/core/server/elasticsearch/legacy/retry_call_cluster.ts +++ b/src/core/server/elasticsearch/legacy/retry_call_cluster.ts @@ -67,7 +67,7 @@ export function migrationsRetryCallCluster( error instanceof esErrors.RequestTimeout || error instanceof esErrors.AuthenticationException || error instanceof esErrors.AuthorizationException || - // @ts-ignore + // @ts-expect-error error instanceof esErrors.Gone || error?.body?.error?.type === 'snapshot_in_progress_exception' ); diff --git a/src/core/server/http/cookie_session_storage.ts b/src/core/server/http/cookie_session_storage.ts index 13f498233f695..5ca70045f81db 100644 --- a/src/core/server/http/cookie_session_storage.ts +++ b/src/core/server/http/cookie_session_storage.ts @@ -19,7 +19,7 @@ import { Request, Server } from 'hapi'; import hapiAuthCookie from 'hapi-auth-cookie'; -// @ts-ignore no TS definitions +// @ts-expect-error no TS definitions import Statehood from 'statehood'; import { KibanaRequest, ensureRawRequest } from './router'; diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index f266677c1a172..fefd75ad9710e 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -21,8 +21,9 @@ import { Url } from 'url'; import { Request, ApplicationState } from 'hapi'; import { Observable, fromEvent, merge } from 'rxjs'; import { shareReplay, first, takeUntil } from 'rxjs/operators'; +import { RecursiveReadonly } from '@kbn/utility-types'; -import { deepFreeze, RecursiveReadonly } from '../../../utils'; +import { deepFreeze } from '../../../utils'; import { Headers } from './headers'; import { RouteMethod, RouteConfigOptions, validBodyOutput, isSafeMethod } from './route'; import { KibanaSocket, IKibanaSocket } from './socket'; @@ -156,7 +157,7 @@ export class KibanaRequest< public readonly params: Params, public readonly query: Query, public readonly body: Body, - // @ts-ignore we will use this flag as soon as http request proxy is supported in the core + // @ts-expect-error we will use this flag as soon as http request proxy is supported in the core // until that time we have to expose all the headers private readonly withoutSecretHeaders: boolean ) { diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 91c33fac41646..35aabab4a0b26 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -307,7 +307,6 @@ export { } from './metrics'; export { - RecursiveReadonly, DEFAULT_APP_CATEGORIES, getFlattenedObject, URLMeaningfulParts, diff --git a/src/core/server/legacy/config/get_unused_config_keys.ts b/src/core/server/legacy/config/get_unused_config_keys.ts index 6cd193d896109..8e53178142180 100644 --- a/src/core/server/legacy/config/get_unused_config_keys.ts +++ b/src/core/server/legacy/config/get_unused_config_keys.ts @@ -18,7 +18,7 @@ */ import { difference, get, set } from 'lodash'; -// @ts-ignore +// @ts-expect-error import { getTransform } from '../../../../legacy/deprecation/index'; import { unset } from '../../../../legacy/utils'; import { getFlattenedObject } from '../../../utils'; diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index ccadae757fe54..ffe3b2375bc90 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -29,7 +29,6 @@ import { import { BehaviorSubject, throwError } from 'rxjs'; -// @ts-ignore: implicit any for JS file import { ClusterManager as MockClusterManager } from '../../../cli/cluster/cluster_manager'; import KbnServer from '../../../legacy/server/kbn_server'; import { Config, Env, ObjectToConfigAdapter } from '../config'; diff --git a/src/core/server/legacy/logging/legacy_logging_server.ts b/src/core/server/legacy/logging/legacy_logging_server.ts index 85a8686b4eded..4a7fea87cf69f 100644 --- a/src/core/server/legacy/logging/legacy_logging_server.ts +++ b/src/core/server/legacy/logging/legacy_logging_server.ts @@ -19,9 +19,9 @@ import { ServerExtType } from 'hapi'; import Podium from 'podium'; -// @ts-ignore: implicit any for JS file +// @ts-expect-error: implicit any for JS file import { Config } from '../../../../legacy/server/config'; -// @ts-ignore: implicit any for JS file +// @ts-expect-error: implicit any for JS file import { setupLogging } from '../../../../legacy/server/logging'; import { LogLevel } from '../../logging/log_level'; import { LogRecord } from '../../logging/log_record'; diff --git a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts index 5039b3a55cc58..f3ec2ed8335c5 100644 --- a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts +++ b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts @@ -23,7 +23,7 @@ import { toArray, tap, distinct, map } from 'rxjs/operators'; import { findPluginSpecs, defaultConfig, - // @ts-ignore + // @ts-expect-error } from '../../../../legacy/plugin_discovery/find_plugin_specs.js'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { collectUiExports as collectLegacyUiExports } from '../../../../legacy/ui/ui_exports/collect_ui_exports'; diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index 2ca5c9f6ed3c5..9e86ee22c607b 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -19,8 +19,8 @@ import { Observable } from 'rxjs'; import { Type } from '@kbn/config-schema'; +import { RecursiveReadonly } from '@kbn/utility-types'; -import { RecursiveReadonly } from 'kibana/public'; import { ConfigPath, EnvironmentMode, PackageInfo, ConfigDeprecationProvider } from '../config'; import { LoggerFactory } from '../logging'; import { KibanaConfigType } from '../kibana_config'; diff --git a/src/core/server/saved_objects/service/lib/decorate_es_error.ts b/src/core/server/saved_objects/service/lib/decorate_es_error.ts index e57f08aa7a527..7d1575798c357 100644 --- a/src/core/server/saved_objects/service/lib/decorate_es_error.ts +++ b/src/core/server/saved_objects/service/lib/decorate_es_error.ts @@ -26,11 +26,11 @@ const { NoConnections, RequestTimeout, Conflict, - // @ts-ignore + // @ts-expect-error 401: NotAuthorized, - // @ts-ignore + // @ts-expect-error 403: Forbidden, - // @ts-ignore + // @ts-expect-error 413: RequestEntityTooLarge, NotFound, BadRequest, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 5973e300e098e..9cc5a8a386b0b 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -108,7 +108,7 @@ import { PingParams } from 'elasticsearch'; import { PutScriptParams } from 'elasticsearch'; import { PutTemplateParams } from 'elasticsearch'; import { Readable } from 'stream'; -import { RecursiveReadonly as RecursiveReadonly_2 } from 'kibana/public'; +import { RecursiveReadonly } from '@kbn/utility-types'; import { ReindexParams } from 'elasticsearch'; import { ReindexRethrottleParams } from 'elasticsearch'; import { RenderSearchTemplateParams } from 'elasticsearch'; @@ -299,7 +299,7 @@ export const config: { startupTimeout: import("@kbn/config-schema").Type; logQueries: import("@kbn/config-schema").Type; ssl: import("@kbn/config-schema").ObjectType<{ - verificationMode: import("@kbn/config-schema").Type<"none" | "full" | "certificate">; + verificationMode: import("@kbn/config-schema").Type<"none" | "certificate" | "full">; certificateAuthorities: import("@kbn/config-schema").Type; certificate: import("@kbn/config-schema").Type; key: import("@kbn/config-schema").Type; @@ -1663,13 +1663,6 @@ export interface PluginsServiceStart { // @public export type PublicUiSettingsParams = Omit; -// Warning: (ae-forgotten-export) The symbol "RecursiveReadonlyArray" needs to be exported by the entry point index.d.ts -// -// @public (undocumented) -export type RecursiveReadonly = T extends (...args: any[]) => any ? T : T extends any[] ? RecursiveReadonlyArray : T extends object ? Readonly<{ - [K in keyof T]: RecursiveReadonly; -}> : T; - // @public export type RedirectResponseOptions = HttpResponseOptions & { headers: { @@ -2550,7 +2543,7 @@ export interface SessionStorageFactory { } // @public (undocumented) -export type SharedGlobalConfig = RecursiveReadonly_2<{ +export type SharedGlobalConfig = RecursiveReadonly<{ kibana: Pick; elasticsearch: Pick; path: Pick; diff --git a/src/core/utils/deep_freeze.test.ts b/src/core/utils/deep_freeze.test.ts index b4531d80d0252..58aa9c9b8c92b 100644 --- a/src/core/utils/deep_freeze.test.ts +++ b/src/core/utils/deep_freeze.test.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - import { deepFreeze } from './deep_freeze'; it('returns the first argument with all original references', () => { @@ -33,7 +32,7 @@ it('returns the first argument with all original references', () => { it('prevents adding properties to argument', () => { const frozen = deepFreeze({}); expect(() => { - // @ts-ignore ts knows this shouldn't be possible, but just making sure + // @ts-expect-error ts knows this shouldn't be possible, but just making sure frozen.foo = true; }).toThrowError(`object is not extensible`); }); @@ -41,7 +40,7 @@ it('prevents adding properties to argument', () => { it('prevents changing properties on argument', () => { const frozen = deepFreeze({ foo: false }); expect(() => { - // @ts-ignore ts knows this shouldn't be possible, but just making sure + // @ts-expect-error ts knows this shouldn't be possible, but just making sure frozen.foo = true; }).toThrowError(`read only property 'foo'`); }); @@ -49,7 +48,7 @@ it('prevents changing properties on argument', () => { it('prevents changing properties on nested children of argument', () => { const frozen = deepFreeze({ foo: { bar: { baz: { box: 1 } } } }); expect(() => { - // @ts-ignore ts knows this shouldn't be possible, but just making sure + // @ts-expect-error ts knows this shouldn't be possible, but just making sure frozen.foo.bar.baz.box = 2; }).toThrowError(`read only property 'box'`); }); @@ -57,7 +56,7 @@ it('prevents changing properties on nested children of argument', () => { it('prevents adding items to a frozen array', () => { const frozen = deepFreeze({ foo: [1] }); expect(() => { - // @ts-ignore ts knows this shouldn't be possible, but just making sure + // @ts-expect-error ts knows this shouldn't be possible, but just making sure frozen.foo.push(2); }).toThrowError(`object is not extensible`); }); @@ -65,7 +64,7 @@ it('prevents adding items to a frozen array', () => { it('prevents reassigning items in a frozen array', () => { const frozen = deepFreeze({ foo: [1] }); expect(() => { - // @ts-ignore ts knows this shouldn't be possible, but just making sure + // @ts-expect-error ts knows this shouldn't be possible, but just making sure frozen.foo[0] = 2; }).toThrowError(`read only property '0'`); }); diff --git a/src/core/utils/deep_freeze.ts b/src/core/utils/deep_freeze.ts index b0f283c60d0fc..fbc35acb45b0f 100644 --- a/src/core/utils/deep_freeze.ts +++ b/src/core/utils/deep_freeze.ts @@ -16,19 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - -// if we define this inside RecursiveReadonly TypeScript complains -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface RecursiveReadonlyArray extends Array> {} - -/** @public */ -export type RecursiveReadonly = T extends (...args: any[]) => any - ? T - : T extends any[] - ? RecursiveReadonlyArray - : T extends object - ? Readonly<{ [K in keyof T]: RecursiveReadonly }> - : T; +import { RecursiveReadonly } from '@kbn/utility-types'; /** @public */ export type Freezable = { [k: string]: any } | any[]; @@ -47,6 +35,5 @@ export function deepFreeze(object: T) { deepFreeze(value); } } - return Object.freeze(object) as RecursiveReadonly; } diff --git a/src/core/utils/integration_tests/__fixtures__/frozen_object_mutation/index.ts b/src/core/utils/integration_tests/__fixtures__/frozen_object_mutation/index.ts deleted file mode 100644 index d4f001a914d34..0000000000000 --- a/src/core/utils/integration_tests/__fixtures__/frozen_object_mutation/index.ts +++ /dev/null @@ -1,44 +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 { deepFreeze } from '../../../../utils/deep_freeze'; - -deepFreeze({ - foo: { - bar: { - baz: 1, - }, - }, -}).foo.bar.baz = 2; - -deepFreeze({ - foo: [ - { - bar: 1, - }, - ], -}).foo[0].bar = 2; - -deepFreeze({ - foo: [1], -}).foo[0] = 2; - -deepFreeze({ - foo: [1], -}).foo.push(2); diff --git a/src/core/utils/integration_tests/__fixtures__/frozen_object_mutation/tsconfig.json b/src/core/utils/integration_tests/__fixtures__/frozen_object_mutation/tsconfig.json deleted file mode 100644 index 12307c46b95fa..0000000000000 --- a/src/core/utils/integration_tests/__fixtures__/frozen_object_mutation/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "skipLibCheck": true, - "lib": [ - "es2018" - ] - }, - "files": [ - "index.ts" - ] -} diff --git a/src/core/utils/integration_tests/deep_freeze.test.ts b/src/core/utils/integration_tests/deep_freeze.test.ts deleted file mode 100644 index f58e298fecfbf..0000000000000 --- a/src/core/utils/integration_tests/deep_freeze.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 { resolve } from 'path'; - -import execa from 'execa'; - -const MINUTE = 60 * 1000; - -it( - 'types return values to prevent mutations in typescript', - async () => { - await expect( - execa('tsc', ['--noEmit'], { - cwd: resolve(__dirname, '__fixtures__/frozen_object_mutation'), - preferLocal: true, - }).catch((err) => err.stdout) - ).resolves.toMatchInlineSnapshot(` - "index.ts(28,12): error TS2540: Cannot assign to 'baz' because it is a read-only property. - index.ts(36,11): error TS2540: Cannot assign to 'bar' because it is a read-only property." - `); - }, - MINUTE -); diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index b12ad94017fbb..0bb3fc3a3bf16 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -142,6 +142,7 @@ import { PutScriptParams } from 'elasticsearch'; import { PutTemplateParams } from 'elasticsearch'; import React from 'react'; import * as React_2 from 'react'; +import { RecursiveReadonly } from '@kbn/utility-types'; import { ReindexParams } from 'elasticsearch'; import { ReindexRethrottleParams } from 'elasticsearch'; import { RenderSearchTemplateParams } from 'elasticsearch'; @@ -1531,7 +1532,7 @@ export interface QueryState { // Warning: (ae-missing-release-tag) "QueryStringInput" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const QueryStringInput: React.FC>; +export const QueryStringInput: React.FC>; // @public (undocumented) export type QuerySuggestion = QuerySuggestionBasic | QuerySuggestionField; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 1eaab2550645f..24ce42e2c20ae 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -109,7 +109,7 @@ import { PeerCertificate } from 'tls'; import { PingParams } from 'elasticsearch'; import { PutScriptParams } from 'elasticsearch'; import { PutTemplateParams } from 'elasticsearch'; -import { RecursiveReadonly } from 'kibana/public'; +import { RecursiveReadonly } from '@kbn/utility-types'; import { ReindexParams } from 'elasticsearch'; import { ReindexRethrottleParams } from 'elasticsearch'; import { RenderSearchTemplateParams } from 'elasticsearch'; diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/field.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/field.tsx index fd7967f4128c3..50358c17e058c 100644 --- a/src/plugins/saved_objects_management/public/management_section/object_view/components/field.tsx +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/field.tsx @@ -18,14 +18,7 @@ */ import React, { PureComponent } from 'react'; -import { - EuiFieldNumber, - EuiFieldText, - EuiFormRow, - EuiSwitch, - // @ts-ignore - EuiCodeEditor, -} from '@elastic/eui'; +import { EuiFieldNumber, EuiFieldText, EuiFormRow, EuiSwitch, EuiCodeEditor } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { FieldState, FieldType } from '../../types'; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx index 6e7397d1058bf..aac799da6ea67 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx @@ -31,7 +31,6 @@ import { EuiForm, EuiFormRow, EuiSwitch, - // @ts-ignore EuiFilePicker, EuiInMemoryTable, EuiSelect, diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx index 6b25a1b0c1f25..6b209a62e1b98 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { shallowWithI18nProvider, mountWithI18nProvider } from 'test_utils/enzyme_helpers'; -// @ts-ignore +// @ts-expect-error import { findTestSubject } from '@elastic/eui/lib/test'; import { keyCodes } from '@elastic/eui'; import { httpServiceMock } from '../../../../../../core/public/mocks'; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx index 51e7525d0e00a..719729cee2602 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx @@ -20,7 +20,6 @@ import { IBasePath } from 'src/core/public'; import React, { PureComponent, Fragment } from 'react'; import { - // @ts-ignore EuiSearchBar, EuiBasicTable, EuiButton, diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index 54bc649c33b60..340c0e3237f91 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -19,7 +19,7 @@ import React, { Component } from 'react'; import { debounce } from 'lodash'; -// @ts-ignore +// @ts-expect-error import { saveAs } from '@elastic/filesaver'; import { EuiSpacer, diff --git a/src/plugins/vis_type_timelion/server/plugin.ts b/src/plugins/vis_type_timelion/server/plugin.ts index 435ec9027eef2..605c6be0a85df 100644 --- a/src/plugins/vis_type_timelion/server/plugin.ts +++ b/src/plugins/vis_type_timelion/server/plugin.ts @@ -20,11 +20,9 @@ import { i18n } from '@kbn/i18n'; import { first } from 'rxjs/operators'; import { TypeOf } from '@kbn/config-schema'; -import { - CoreSetup, - PluginInitializerContext, - RecursiveReadonly, -} from '../../../../src/core/server'; +import { RecursiveReadonly } from '@kbn/utility-types'; + +import { CoreSetup, PluginInitializerContext } from '../../../../src/core/server'; import { deepFreeze } from '../../../../src/core/server'; import { configSchema } from '../config'; import loadFunctions from './lib/load_functions'; diff --git a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts index e2703cb5786dd..85a8618be5d18 100644 --- a/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/legacy/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts @@ -126,7 +126,7 @@ export interface KibanaServerRequest extends t.TypeOf { kind: 'authenticated'; [internalAuthData]: AuthDataType; username: string; - roles: string[]; + roles: readonly string[]; full_name: string | null; email: string | null; enabled: boolean; diff --git a/x-pack/plugins/beats_management/public/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/framework/adapter_types.ts index a5904d687b37e..ce663650409fa 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/framework/adapter_types.ts @@ -43,7 +43,7 @@ export interface FrameworkInfo extends t.TypeOf {} export const RuntimeFrameworkUser = t.interface( { username: t.string, - roles: t.array(t.string), + roles: t.readonlyArray(t.string), full_name: t.union([t.null, t.string]), email: t.union([t.null, t.string]), enabled: t.boolean, diff --git a/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.test.ts b/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.test.ts index 2978c48af7414..67fc1a98ad4d1 100644 --- a/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.test.ts +++ b/x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.test.ts @@ -85,9 +85,9 @@ describe('DashboardOnlyModeRequestInterceptor', () => { security.authc.getCurrentUser = jest.fn( (r: KibanaRequest) => - ({ + (({ roles: [DASHBOARD_ONLY_MODE_ROLE], - } as AuthenticatedUser) + } as unknown) as AuthenticatedUser) ); uiSettingsMock = [DASHBOARD_ONLY_MODE_ROLE]; diff --git a/x-pack/plugins/features/common/feature.ts b/x-pack/plugins/features/common/feature.ts index 1b405094d9eda..4a293e0c962cc 100644 --- a/x-pack/plugins/features/common/feature.ts +++ b/x-pack/plugins/features/common/feature.ts @@ -49,7 +49,9 @@ export interface FeatureConfig { * This does not restrict access to your feature based on license. * Its only purpose is to inform the space and roles UIs on which features to display. */ - validLicenses?: Array<'basic' | 'standard' | 'gold' | 'platinum' | 'enterprise' | 'trial'>; + validLicenses?: ReadonlyArray< + 'basic' | 'standard' | 'gold' | 'platinum' | 'enterprise' | 'trial' + >; /** * An optional EUI Icon to be used when displaying your feature. @@ -66,7 +68,7 @@ export interface FeatureConfig { * An array of app ids that are enabled when this feature is enabled. * Apps specified here will automatically cascade to the privileges defined below, unless specified differently there. */ - app: string[]; + app: readonly string[]; /** * If this feature includes management sections, you can specify them here to control visibility of those @@ -83,14 +85,14 @@ export interface FeatureConfig { * ``` */ management?: { - [sectionId: string]: string[]; + [sectionId: string]: readonly string[]; }; /** * If this feature includes a catalogue entry, you can specify them here to control visibility based on the current space. * * Items specified here will automatically cascade to the privileges defined below, unless specified differently there. */ - catalogue?: string[]; + catalogue?: readonly string[]; /** * Feature privilege definition. @@ -112,7 +114,7 @@ export interface FeatureConfig { /** * Optional sub-feature privilege definitions. This can only be specified if `privileges` are are also defined. */ - subFeatures?: SubFeatureConfig[]; + subFeatures?: readonly SubFeatureConfig[]; /** * Optional message to display on the Role Management screen when configuring permissions for this feature. @@ -124,7 +126,7 @@ export interface FeatureConfig { */ reserved?: { description: string; - privileges: ReservedKibanaPrivilege[]; + privileges: readonly ReservedKibanaPrivilege[]; }; } diff --git a/x-pack/plugins/features/common/feature_kibana_privileges.ts b/x-pack/plugins/features/common/feature_kibana_privileges.ts index 768c8c6ae1088..a9ba38e36f20b 100644 --- a/x-pack/plugins/features/common/feature_kibana_privileges.ts +++ b/x-pack/plugins/features/common/feature_kibana_privileges.ts @@ -26,13 +26,13 @@ export interface FeatureKibanaPrivileges { * ``` */ management?: { - [sectionId: string]: string[]; + [sectionId: string]: readonly string[]; }; /** * If this feature includes a catalogue entry, you can specify them here to control visibility based on user permissions. */ - catalogue?: string[]; + catalogue?: readonly string[]; /** * If your feature includes server-side APIs, you can tag those routes to secure access based on user permissions. @@ -60,7 +60,7 @@ export interface FeatureKibanaPrivileges { * A generic tag name like "access:read" could be used elsewhere, and access to that API endpoint would also * extend to any routes you have also tagged with that name. */ - api?: string[]; + api?: readonly string[]; /** * If your feature exposes a client-side application (most of them do!), then you can control access to them here. @@ -73,7 +73,7 @@ export interface FeatureKibanaPrivileges { * ``` * */ - app?: string[]; + app?: readonly string[]; /** * If your feature requires access to specific saved objects, then specify your access needs here. @@ -88,7 +88,7 @@ export interface FeatureKibanaPrivileges { * } * ``` */ - all: string[]; + all: readonly string[]; /** * List of saved object types which users should have read-only access to when granted this privilege. @@ -99,7 +99,7 @@ export interface FeatureKibanaPrivileges { * } * ``` */ - read: string[]; + read: readonly string[]; }; /** * A list of UI Capabilities that should be granted to users with this privilege. @@ -121,5 +121,5 @@ export interface FeatureKibanaPrivileges { * * @see UICapabilities */ - ui: string[]; + ui: readonly string[]; } diff --git a/x-pack/plugins/features/common/sub_feature.ts b/x-pack/plugins/features/common/sub_feature.ts index 121bb8514c8a2..0651bad883ea5 100644 --- a/x-pack/plugins/features/common/sub_feature.ts +++ b/x-pack/plugins/features/common/sub_feature.ts @@ -15,7 +15,7 @@ export interface SubFeatureConfig { name: string; /** Collection of privilege groups */ - privilegeGroups: SubFeaturePrivilegeGroupConfig[]; + privilegeGroups: readonly SubFeaturePrivilegeGroupConfig[]; } /** @@ -45,7 +45,7 @@ export interface SubFeaturePrivilegeGroupConfig { /** * The privileges which belong to this group. */ - privileges: SubFeaturePrivilegeConfig[]; + privileges: readonly SubFeaturePrivilegeConfig[]; } /** diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index 7497548cf8904..c45788b511cde 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -126,7 +126,7 @@ export function validateFeature(feature: FeatureConfig) { const unseenCatalogue = new Set(catalogue); - function validateAppEntry(privilegeId: string, entry: string[] = []) { + function validateAppEntry(privilegeId: string, entry: readonly string[] = []) { entry.forEach((privilegeApp) => unseenApps.delete(privilegeApp)); const unknownAppEntries = difference(entry, app); @@ -139,7 +139,7 @@ export function validateFeature(feature: FeatureConfig) { } } - function validateCatalogueEntry(privilegeId: string, entry: string[] = []) { + function validateCatalogueEntry(privilegeId: string, entry: readonly string[] = []) { entry.forEach((privilegeCatalogue) => unseenCatalogue.delete(privilegeCatalogue)); const unknownCatalogueEntries = difference(entry || [], catalogue); @@ -154,7 +154,7 @@ export function validateFeature(feature: FeatureConfig) { function validateManagementEntry( privilegeId: string, - managementEntry: Record = {} + managementEntry: Record = {} ) { Object.entries(managementEntry).forEach(([managementSectionId, managementSectionEntry]) => { if (unseenManagement.has(managementSectionId)) { diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index bfae416471c2f..149c1acfb5086 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -3,14 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { RecursiveReadonly } from '@kbn/utility-types'; import { CoreSetup, CoreStart, SavedObjectsServiceStart, Logger, PluginInitializerContext, - RecursiveReadonly, } from '../../../../src/core/server'; import { Capabilities as UICapabilities } from '../../../../src/core/server'; import { deepFreeze } from '../../../../src/core/server'; diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts index c23d44aa8e4b6..f9685dac32e23 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts @@ -4,13 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - Capabilities, - HttpSetup, - RecursiveReadonly, - SavedObjectsClientContract, -} from 'kibana/public'; +import { Capabilities, HttpSetup, SavedObjectsClientContract } from 'kibana/public'; import { i18n } from '@kbn/i18n'; +import { RecursiveReadonly } from '@kbn/utility-types'; import { IndexPatternsContract, IndexPattern, diff --git a/x-pack/plugins/security/common/model/user.ts b/x-pack/plugins/security/common/model/user.ts index 5c852e7a8f03d..ae6de7f4c5fbc 100644 --- a/x-pack/plugins/security/common/model/user.ts +++ b/x-pack/plugins/security/common/model/user.ts @@ -8,7 +8,7 @@ export interface User { username: string; email: string; full_name: string; - roles: string[]; + roles: readonly string[]; enabled: boolean; metadata?: { _reserved: boolean; diff --git a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.tsx b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.tsx index 689830e2845ce..5b24b296b299f 100644 --- a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.tsx +++ b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.tsx @@ -12,7 +12,7 @@ import { RoleComboBoxOption } from './role_combo_box_option'; interface Props { availableRoles: Role[]; - selectedRoleNames: string[]; + selectedRoleNames: readonly string[]; onChange: (selectedRoleNames: string[]) => void; placeholder?: string; isLoading?: boolean; diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx index eea7edd62fbfa..9eb2616cebb18 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx @@ -57,7 +57,7 @@ interface State { showDeleteConfirmation: boolean; user: EditUser; roles: Role[]; - selectedRoles: string[]; + selectedRoles: readonly string[]; formError: UserValidationResult | null; } diff --git a/x-pack/plugins/security/server/authorization/app_authorization.test.ts b/x-pack/plugins/security/server/authorization/app_authorization.test.ts index 2d3a981fb3247..1dc072ab2e6e9 100644 --- a/x-pack/plugins/security/server/authorization/app_authorization.test.ts +++ b/x-pack/plugins/security/server/authorization/app_authorization.test.ts @@ -5,6 +5,7 @@ */ import { PluginSetupContract as FeaturesSetupContract } from '../../../features/server'; +import { featuresPluginMock } from '../../../features/server/mocks'; import { initAppAuthorization } from './app_authorization'; import { @@ -16,9 +17,11 @@ import { import { authorizationMock } from './index.mock'; const createFeaturesSetupContractMock = (): FeaturesSetupContract => { - return { - getFeatures: () => [{ id: 'foo', name: 'Foo', app: ['foo'], privileges: {} }], - } as FeaturesSetupContract; + const mock = featuresPluginMock.createSetup(); + mock.getFeatures.mockReturnValue([ + { id: 'foo', name: 'Foo', app: ['foo'], privileges: {} } as any, + ]); + return mock; }; describe('initAppAuthorization', () => { diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts index e239a6e280aec..029b2e77f7812 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts @@ -76,7 +76,7 @@ function mergeWithSubFeatures( return mergedConfig; } -function mergeArrays(input1: string[] | undefined, input2: string[] | undefined) { +function mergeArrays(input1: readonly string[] | undefined, input2: readonly string[] | undefined) { const first = input1 ?? []; const second = input2 ?? []; return Array.from(new Set([...first, ...second])); diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index a0a06b537213d..d357519c5ccce 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -5,11 +5,11 @@ */ import { TypeOf } from '@kbn/config-schema'; +import { RecursiveReadonly } from '@kbn/utility-types'; import { PluginConfigDescriptor, PluginInitializer, PluginInitializerContext, - RecursiveReadonly, } from '../../../../src/core/server'; import { ConfigSchema } from './config'; import { Plugin, SecurityPluginSetup, PluginSetupDependencies } from './plugin'; diff --git a/x-pack/plugins/spaces/public/management/lib/feature_utils.test.ts b/x-pack/plugins/spaces/public/management/lib/feature_utils.test.ts index a3360969fb3f2..20d419e5c90e4 100644 --- a/x-pack/plugins/spaces/public/management/lib/feature_utils.test.ts +++ b/x-pack/plugins/spaces/public/management/lib/feature_utils.test.ts @@ -5,7 +5,7 @@ */ import { getEnabledFeatures } from './feature_utils'; -import { Feature } from '../../../../features/public'; +import { FeatureConfig } from '../../../../features/public'; const buildFeatures = () => [ @@ -25,7 +25,7 @@ const buildFeatures = () => id: 'feature4', name: 'feature 4', }, - ] as Feature[]; + ] as FeatureConfig[]; const buildSpace = (disabledFeatures = [] as string[]) => ({ id: 'space', diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts index 17a1fbcca73bd..8375296d869e6 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts @@ -21,7 +21,6 @@ import { coreMock, } from '../../../../../../src/core/server/mocks'; import * as kbnTestServer from '../../../../../../src/test_utils/kbn_server'; -import { PluginsSetup } from '../../plugin'; import { SpacesService } from '../../spaces_service'; import { SpacesAuditLogger } from '../audit_logger'; import { convertSavedObjectToSpace } from '../../routes/lib'; @@ -29,6 +28,7 @@ import { initSpacesOnPostAuthRequestInterceptor } from './on_post_auth_intercept import { Feature } from '../../../../features/server'; import { spacesConfig } from '../__fixtures__'; import { securityMock } from '../../../../security/server/mocks'; +import { featuresPluginMock } from '../../../../features/server/mocks'; // FLAKY: https://github.com/elastic/kibana/issues/55953 describe.skip('onPostAuthInterceptor', () => { @@ -123,31 +123,29 @@ describe.skip('onPostAuthInterceptor', () => { const loggingMock = loggingSystemMock.create().asLoggerFactory().get('xpack', 'spaces'); - const featuresPlugin = { - getFeatures: () => - [ - { - id: 'feature-1', - name: 'feature 1', - app: ['app-1'], - }, - { - id: 'feature-2', - name: 'feature 2', - app: ['app-2'], - }, - { - id: 'feature-4', - name: 'feature 4', - app: ['app-1', 'app-4'], - }, - { - id: 'feature-5', - name: 'feature 4', - app: ['kibana'], - }, - ] as Feature[], - } as PluginsSetup['features']; + const featuresPlugin = featuresPluginMock.createSetup(); + featuresPlugin.getFeatures.mockReturnValue(([ + { + id: 'feature-1', + name: 'feature 1', + app: ['app-1'], + }, + { + id: 'feature-2', + name: 'feature 2', + app: ['app-2'], + }, + { + id: 'feature-4', + name: 'feature 4', + app: ['app-1', 'app-4'], + }, + { + id: 'feature-5', + name: 'feature 4', + app: ['kibana'], + }, + ] as unknown) as Feature[]); const mockRepository = jest.fn().mockImplementation(() => { return { From 8485d2fbacf366eb6ef0334eb903a6db048ab844 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Tue, 30 Jun 2020 07:51:12 +0200 Subject: [PATCH 06/23] Implement recursive plugin discovery (#68811) * implements recursive scanning in plugin discovery system * update optimizer to find plugins in sub-directories * update renovate * update optimizer IT snapshot * refactor processPluginSearchPaths$ and add test for inaccessible manifest * add symlink test * add maxDepth to the optimizer * adapt mockFs definitions * remove `flat` usage --- package.json | 2 + .../plugins/{ => nested}/baz/kibana.json | 0 .../plugins/{ => nested}/baz/server/index.ts | 0 .../plugins/{ => nested}/baz/server/lib.ts | 0 .../basic_optimization.test.ts.snap | 12 +- .../optimizer/kibana_platform_plugins.test.ts | 12 +- .../src/optimizer/kibana_platform_plugins.ts | 13 +- renovate.json5 | 8 + .../discovery/plugins_discovery.test.mocks.ts | 9 - .../discovery/plugins_discovery.test.ts | 571 +++++++++++------- .../plugins/discovery/plugins_discovery.ts | 107 +++- yarn.lock | 12 + 12 files changed, 486 insertions(+), 260 deletions(-) rename packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/{ => nested}/baz/kibana.json (100%) rename packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/{ => nested}/baz/server/index.ts (100%) rename packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/{ => nested}/baz/server/lib.ts (100%) diff --git a/package.json b/package.json index c225435b4e4ff..b520be4df6969 100644 --- a/package.json +++ b/package.json @@ -361,6 +361,7 @@ "@types/markdown-it": "^0.0.7", "@types/minimatch": "^2.0.29", "@types/mocha": "^7.0.2", + "@types/mock-fs": "^4.10.0", "@types/moment-timezone": "^0.5.12", "@types/mustache": "^0.8.31", "@types/node": ">=10.17.17 <10.20.0", @@ -473,6 +474,7 @@ "listr": "^0.14.1", "load-grunt-config": "^3.0.1", "mocha": "^7.1.1", + "mock-fs": "^4.12.0", "mock-http-server": "1.3.0", "ms-chromium-edge-driver": "^0.2.3", "multistream": "^2.1.1", diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/nested/baz/kibana.json similarity index 100% rename from packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/kibana.json rename to packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/nested/baz/kibana.json diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/server/index.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/nested/baz/server/index.ts similarity index 100% rename from packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/server/index.ts rename to packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/nested/baz/server/index.ts diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/server/lib.ts b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/nested/baz/server/lib.ts similarity index 100% rename from packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz/server/lib.ts rename to packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/nested/baz/server/lib.ts diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 2265bad9f6afa..b6b0973f0d539 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -43,18 +43,18 @@ OptimizerConfig { "id": "bar", "isUiPlugin": true, }, - Object { - "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/baz, - "extraPublicDirs": Array [], - "id": "baz", - "isUiPlugin": false, - }, Object { "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo, "extraPublicDirs": Array [], "id": "foo", "isUiPlugin": true, }, + Object { + "directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/nested/baz, + "extraPublicDirs": Array [], + "id": "baz", + "isUiPlugin": false, + }, ], "profileWebpack": false, "repoRoot": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo, diff --git a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.test.ts b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.test.ts index 0961881df461c..f7b457ca42c6d 100644 --- a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.test.ts +++ b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.test.ts @@ -41,18 +41,18 @@ it('parses kibana.json files of plugins found in pluginDirs', () => { "id": "bar", "isUiPlugin": true, }, - Object { - "directory": /packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/baz, - "extraPublicDirs": Array [], - "id": "baz", - "isUiPlugin": false, - }, Object { "directory": /packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo, "extraPublicDirs": Array [], "id": "foo", "isUiPlugin": true, }, + Object { + "directory": /packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/nested/baz, + "extraPublicDirs": Array [], + "id": "baz", + "isUiPlugin": false, + }, Object { "directory": /packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz, "extraPublicDirs": Array [], diff --git a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts index bfc60a29efa27..83637691004f4 100644 --- a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts +++ b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts @@ -37,7 +37,7 @@ export function findKibanaPlatformPlugins(scanDirs: string[], paths: string[]) { .sync( Array.from( new Set([ - ...scanDirs.map((dir) => `${dir}/*/kibana.json`), + ...scanDirs.map(nestedScanDirPaths).reduce((dirs, current) => [...dirs, ...current], []), ...paths.map((path) => `${path}/kibana.json`), ]) ), @@ -51,6 +51,17 @@ export function findKibanaPlatformPlugins(scanDirs: string[], paths: string[]) { ); } +function nestedScanDirPaths(dir: string): string[] { + // down to 5 level max + return [ + `${dir}/*/kibana.json`, + `${dir}/*/*/kibana.json`, + `${dir}/*/*/*/kibana.json`, + `${dir}/*/*/*/*/kibana.json`, + `${dir}/*/*/*/*/*/kibana.json`, + ]; +} + function readKibanaPlatformPlugin(manifestPath: string): KibanaPlatformPlugin { if (!Path.isAbsolute(manifestPath)) { throw new TypeError('expected new platform manifest path to be absolute'); diff --git a/renovate.json5 b/renovate.json5 index 1af155fcc645e..49a255d60f29e 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -636,6 +636,14 @@ '(\\b|_)mocha(\\b|_)', ], }, + { + groupSlug: 'mock-fs', + groupName: 'mock-fs related packages', + packageNames: [ + 'mock-fs', + '@types/mock-fs', + ], + }, { groupSlug: 'moment', groupName: 'moment related packages', diff --git a/src/core/server/plugins/discovery/plugins_discovery.test.mocks.ts b/src/core/server/plugins/discovery/plugins_discovery.test.mocks.ts index d92465e4dd497..83accc06cb995 100644 --- a/src/core/server/plugins/discovery/plugins_discovery.test.mocks.ts +++ b/src/core/server/plugins/discovery/plugins_discovery.test.mocks.ts @@ -17,14 +17,5 @@ * under the License. */ -export const mockReaddir = jest.fn(); -export const mockReadFile = jest.fn(); -export const mockStat = jest.fn(); -jest.mock('fs', () => ({ - readdir: mockReaddir, - readFile: mockReadFile, - stat: mockStat, -})); - export const mockPackage = new Proxy({ raw: {} as any }, { get: (obj, prop) => obj.raw[prop] }); jest.mock('../../../../../package.json', () => mockPackage); diff --git a/src/core/server/plugins/discovery/plugins_discovery.test.ts b/src/core/server/plugins/discovery/plugins_discovery.test.ts index 1c42f5dcfc7a7..70413757de9da 100644 --- a/src/core/server/plugins/discovery/plugins_discovery.test.ts +++ b/src/core/server/plugins/discovery/plugins_discovery.test.ts @@ -17,251 +17,384 @@ * under the License. */ -import { mockPackage, mockReaddir, mockReadFile, mockStat } from './plugins_discovery.test.mocks'; -import { rawConfigServiceMock } from '../../config/raw_config_service.mock'; +import { mockPackage } from './plugins_discovery.test.mocks'; +import mockFs from 'mock-fs'; import { loggingSystemMock } from '../../logging/logging_system.mock'; -import { resolve } from 'path'; import { first, map, toArray } from 'rxjs/operators'; - +import { resolve } from 'path'; import { ConfigService, Env } from '../../config'; import { getEnvOptions } from '../../config/__mocks__/env'; -import { PluginWrapper } from '../plugin'; import { PluginsConfig, PluginsConfigType, config } from '../plugins_config'; import { discover } from './plugins_discovery'; +import { rawConfigServiceMock } from '../../config/raw_config_service.mock'; +import { CoreContext } from '../../core_context'; -const TEST_PLUGIN_SEARCH_PATHS = { - nonEmptySrcPlugins: resolve(process.cwd(), 'src', 'plugins'), - emptyPlugins: resolve(process.cwd(), 'plugins'), - nonExistentKibanaExtra: resolve(process.cwd(), '..', 'kibana-extra'), +const KIBANA_ROOT = process.cwd(); + +const Plugins = { + invalid: () => ({ + 'kibana.json': 'not-json', + }), + incomplete: () => ({ + 'kibana.json': JSON.stringify({ version: '1' }), + }), + incompatible: () => ({ + 'kibana.json': JSON.stringify({ id: 'plugin', version: '1' }), + }), + missingManifest: () => ({}), + inaccessibleManifest: () => ({ + 'kibana.json': mockFs.file({ + mode: 0, // 0000, + content: JSON.stringify({ id: 'plugin', version: '1' }), + }), + }), + valid: (id: string) => ({ + 'kibana.json': JSON.stringify({ + id, + configPath: ['plugins', id], + version: '1', + kibanaVersion: '1.2.3', + requiredPlugins: [], + optionalPlugins: [], + server: true, + }), + }), +}; + +const packageMock = { + branch: 'master', + version: '1.2.3', + build: { + distributable: true, + number: 1, + sha: '', + }, }; -const TEST_EXTRA_PLUGIN_PATH = resolve(process.cwd(), 'my-extra-plugin'); - -const logger = loggingSystemMock.create(); - -beforeEach(() => { - mockReaddir.mockImplementation((path, cb) => { - if (path === TEST_PLUGIN_SEARCH_PATHS.nonEmptySrcPlugins) { - cb(null, [ - '1', - '2-no-manifest', - '3', - '4-incomplete-manifest', - '5-invalid-manifest', - '6', - '7-non-dir', - '8-incompatible-manifest', - '9-inaccessible-dir', - ]); - } else if (path === TEST_PLUGIN_SEARCH_PATHS.nonExistentKibanaExtra) { - cb(new Error('ENOENT')); - } else { - cb(null, []); - } + +const manifestPath = (...pluginPath: string[]) => + resolve(KIBANA_ROOT, 'src', 'plugins', ...pluginPath, 'kibana.json'); + +describe('plugins discovery system', () => { + let logger: ReturnType; + let env: Env; + let configService: ConfigService; + let pluginConfig: PluginsConfigType; + let coreContext: CoreContext; + + beforeEach(async () => { + logger = loggingSystemMock.create(); + + mockPackage.raw = packageMock; + + env = Env.createDefault( + getEnvOptions({ + cliArgs: { envName: 'development' }, + }) + ); + + configService = new ConfigService( + rawConfigServiceMock.create({ rawConfig: { plugins: { paths: [] } } }), + env, + logger + ); + await configService.setSchema(config.path, config.schema); + + coreContext = { + coreId: Symbol(), + configService, + env, + logger, + }; + + pluginConfig = await configService + .atPath('plugins') + .pipe(first()) + .toPromise(); + + // jest relies on the filesystem to get sourcemaps when using console.log + // which breaks with the mocked FS, see https://github.com/tschaub/mock-fs/issues/234 + // hijacking logging to process.stdout as a workaround for this suite. + jest.spyOn(console, 'log').mockImplementation((...args) => { + process.stdout.write(args + '\n'); + }); }); - mockStat.mockImplementation((path, cb) => { - if (path.includes('9-inaccessible-dir')) { - cb(new Error(`ENOENT (disappeared between "readdir" and "stat").`)); - } else { - cb(null, { isDirectory: () => !path.includes('non-dir') }); - } + afterEach(() => { + mockFs.restore(); + // restore the console.log behavior + jest.restoreAllMocks(); }); - mockReadFile.mockImplementation((path, cb) => { - if (path.includes('no-manifest')) { - cb(new Error('ENOENT')); - } else if (path.includes('invalid-manifest')) { - cb(null, Buffer.from('not-json')); - } else if (path.includes('incomplete-manifest')) { - cb(null, Buffer.from(JSON.stringify({ version: '1' }))); - } else if (path.includes('incompatible-manifest')) { - cb(null, Buffer.from(JSON.stringify({ id: 'plugin', version: '1' }))); - } else { - cb( - null, - Buffer.from( - JSON.stringify({ - id: 'plugin', - configPath: ['core', 'config'], - version: '1', - kibanaVersion: '1.2.3', - requiredPlugins: ['a', 'b'], - optionalPlugins: ['c', 'd'], - server: true, - }) - ) - ); - } + it('discovers plugins in the search locations', async () => { + const { plugin$ } = discover(new PluginsConfig(pluginConfig, env), coreContext); + + mockFs( + { + [`${KIBANA_ROOT}/src/plugins/plugin_a`]: Plugins.valid('pluginA'), + [`${KIBANA_ROOT}/plugins/plugin_b`]: Plugins.valid('pluginB'), + [`${KIBANA_ROOT}/x-pack/plugins/plugin_c`]: Plugins.valid('pluginC'), + }, + { createCwd: false } + ); + + const plugins = await plugin$.pipe(toArray()).toPromise(); + const pluginNames = plugins.map((plugin) => plugin.name); + + expect(pluginNames).toHaveLength(3); + expect(pluginNames).toEqual(expect.arrayContaining(['pluginA', 'pluginB', 'pluginC'])); }); -}); -afterEach(() => { - jest.clearAllMocks(); -}); + it('return errors when the manifest is invalid or incompatible', async () => { + const { plugin$, error$ } = discover(new PluginsConfig(pluginConfig, env), coreContext); + + mockFs( + { + [`${KIBANA_ROOT}/src/plugins/plugin_a`]: Plugins.invalid(), + [`${KIBANA_ROOT}/src/plugins/plugin_b`]: Plugins.incomplete(), + [`${KIBANA_ROOT}/src/plugins/plugin_c`]: Plugins.incompatible(), + [`${KIBANA_ROOT}/src/plugins/plugin_ad`]: Plugins.missingManifest(), + }, + { createCwd: false } + ); + + const plugins = await plugin$.pipe(toArray()).toPromise(); + expect(plugins).toHaveLength(0); + + const errors = await error$ + .pipe( + map((error) => error.toString()), + toArray() + ) + .toPromise(); -test('properly iterates through plugin search locations', async () => { - mockPackage.raw = { - branch: 'master', - version: '1.2.3', - build: { - distributable: true, - number: 1, - sha: '', - }, - }; - - const env = Env.createDefault( - getEnvOptions({ - cliArgs: { envName: 'development' }, - }) - ); - const configService = new ConfigService( - rawConfigServiceMock.create({ rawConfig: { plugins: { paths: [TEST_EXTRA_PLUGIN_PATH] } } }), - env, - logger - ); - await configService.setSchema(config.path, config.schema); - - const rawConfig = await configService - .atPath('plugins') - .pipe(first()) - .toPromise(); - const { plugin$, error$ } = discover(new PluginsConfig(rawConfig, env), { - coreId: Symbol(), - configService, - env, - logger, + expect(errors).toEqual( + expect.arrayContaining([ + `Error: Unexpected token o in JSON at position 1 (invalid-manifest, ${manifestPath( + 'plugin_a' + )})`, + `Error: Plugin manifest must contain an "id" property. (invalid-manifest, ${manifestPath( + 'plugin_b' + )})`, + `Error: Plugin "plugin" is only compatible with Kibana version "1", but used Kibana version is "1.2.3". (incompatible-version, ${manifestPath( + 'plugin_c' + )})`, + ]) + ); }); - const plugins = await plugin$.pipe(toArray()).toPromise(); - expect(plugins).toHaveLength(4); - - for (const path of [ - resolve(TEST_PLUGIN_SEARCH_PATHS.nonEmptySrcPlugins, '1'), - resolve(TEST_PLUGIN_SEARCH_PATHS.nonEmptySrcPlugins, '3'), - resolve(TEST_PLUGIN_SEARCH_PATHS.nonEmptySrcPlugins, '6'), - TEST_EXTRA_PLUGIN_PATH, - ]) { - const discoveredPlugin = plugins.find((plugin) => plugin.path === path)!; - expect(discoveredPlugin).toBeInstanceOf(PluginWrapper); - expect(discoveredPlugin.configPath).toEqual(['core', 'config']); - expect(discoveredPlugin.requiredPlugins).toEqual(['a', 'b']); - expect(discoveredPlugin.optionalPlugins).toEqual(['c', 'd']); - } - - await expect( - error$ + it('return errors when the plugin search path is not accessible', async () => { + const { plugin$, error$ } = discover(new PluginsConfig(pluginConfig, env), coreContext); + + mockFs( + { + [`${KIBANA_ROOT}/src/plugins`]: mockFs.directory({ + mode: 0, // 0000 + items: { + plugin_a: Plugins.valid('pluginA'), + }, + }), + }, + { createCwd: false } + ); + + const plugins = await plugin$.pipe(toArray()).toPromise(); + expect(plugins).toHaveLength(0); + + const errors = await error$ .pipe( map((error) => error.toString()), toArray() ) - .toPromise() - ).resolves.toEqual([ - `Error: ENOENT (disappeared between "readdir" and "stat"). (invalid-plugin-path, ${resolve( - TEST_PLUGIN_SEARCH_PATHS.nonEmptySrcPlugins, - '9-inaccessible-dir' - )})`, - `Error: ENOENT (invalid-search-path, ${TEST_PLUGIN_SEARCH_PATHS.nonExistentKibanaExtra})`, - `Error: ENOENT (missing-manifest, ${resolve( - TEST_PLUGIN_SEARCH_PATHS.nonEmptySrcPlugins, - '2-no-manifest', - 'kibana.json' - )})`, - `Error: Plugin manifest must contain an "id" property. (invalid-manifest, ${resolve( - TEST_PLUGIN_SEARCH_PATHS.nonEmptySrcPlugins, - '4-incomplete-manifest', - 'kibana.json' - )})`, - `Error: Unexpected token o in JSON at position 1 (invalid-manifest, ${resolve( - TEST_PLUGIN_SEARCH_PATHS.nonEmptySrcPlugins, - '5-invalid-manifest', - 'kibana.json' - )})`, - `Error: Plugin "plugin" is only compatible with Kibana version "1", but used Kibana version is "1.2.3". (incompatible-version, ${resolve( - TEST_PLUGIN_SEARCH_PATHS.nonEmptySrcPlugins, - '8-incompatible-manifest', - 'kibana.json' - )})`, - ]); -}); + .toPromise(); -test('logs a warning about --plugin-path when used in development', async () => { - mockPackage.raw = { - branch: 'master', - version: '1.2.3', - build: { - distributable: true, - number: 1, - sha: '', - }, - }; - - const env = Env.createDefault( - getEnvOptions({ - cliArgs: { dev: false, envName: 'development' }, - }) - ); - const configService = new ConfigService( - rawConfigServiceMock.create({ rawConfig: { plugins: { paths: [TEST_EXTRA_PLUGIN_PATH] } } }), - env, - logger - ); - await configService.setSchema(config.path, config.schema); - - const rawConfig = await configService - .atPath('plugins') - .pipe(first()) - .toPromise(); - - discover(new PluginsConfig(rawConfig, env), { - coreId: Symbol(), - configService, - env, - logger, + const srcPluginsPath = resolve(KIBANA_ROOT, 'src', 'plugins'); + const xpackPluginsPath = resolve(KIBANA_ROOT, 'x-pack', 'plugins'); + expect(errors).toEqual( + expect.arrayContaining([ + `Error: EACCES, permission denied '${srcPluginsPath}' (invalid-search-path, ${srcPluginsPath})`, + `Error: ENOENT, no such file or directory '${xpackPluginsPath}' (invalid-search-path, ${xpackPluginsPath})`, + ]) + ); }); - expect(loggingSystemMock.collect(logger).warn).toEqual([ - [ - `Explicit plugin paths [${TEST_EXTRA_PLUGIN_PATH}] should only be used in development. Relative imports may not work properly in production.`, - ], - ]); -}); + it('return an error when the manifest file is not accessible', async () => { + const { plugin$, error$ } = discover(new PluginsConfig(pluginConfig, env), coreContext); + + mockFs( + { + [`${KIBANA_ROOT}/src/plugins/plugin_a`]: { + ...Plugins.inaccessibleManifest(), + nested_plugin: Plugins.valid('nestedPlugin'), + }, + }, + { createCwd: false } + ); -test('does not log a warning about --plugin-path when used in production', async () => { - mockPackage.raw = { - branch: 'master', - version: '1.2.3', - build: { - distributable: true, - number: 1, - sha: '', - }, - }; - - const env = Env.createDefault( - getEnvOptions({ - cliArgs: { dev: false, envName: 'production' }, - }) - ); - const configService = new ConfigService( - rawConfigServiceMock.create({ rawConfig: { plugins: { paths: [TEST_EXTRA_PLUGIN_PATH] } } }), - env, - logger - ); - await configService.setSchema(config.path, config.schema); - - const rawConfig = await configService - .atPath('plugins') - .pipe(first()) - .toPromise(); - - discover(new PluginsConfig(rawConfig, env), { - coreId: Symbol(), - configService, - env, - logger, + const plugins = await plugin$.pipe(toArray()).toPromise(); + expect(plugins).toHaveLength(0); + + const errors = await error$ + .pipe( + map((error) => error.toString()), + toArray() + ) + .toPromise(); + + const errorPath = manifestPath('plugin_a'); + expect(errors).toEqual( + expect.arrayContaining([ + `Error: EACCES, permission denied '${errorPath}' (missing-manifest, ${errorPath})`, + ]) + ); + }); + + it('discovers plugins in nested directories', async () => { + const { plugin$, error$ } = discover(new PluginsConfig(pluginConfig, env), coreContext); + + mockFs( + { + [`${KIBANA_ROOT}/src/plugins/plugin_a`]: Plugins.valid('pluginA'), + [`${KIBANA_ROOT}/src/plugins/sub1/plugin_b`]: Plugins.valid('pluginB'), + [`${KIBANA_ROOT}/src/plugins/sub1/sub2/plugin_c`]: Plugins.valid('pluginC'), + [`${KIBANA_ROOT}/src/plugins/sub1/sub2/plugin_d`]: Plugins.incomplete(), + }, + { createCwd: false } + ); + + const plugins = await plugin$.pipe(toArray()).toPromise(); + const pluginNames = plugins.map((plugin) => plugin.name); + + expect(pluginNames).toHaveLength(3); + expect(pluginNames).toEqual(expect.arrayContaining(['pluginA', 'pluginB', 'pluginC'])); + + const errors = await error$ + .pipe( + map((error) => error.toString()), + toArray() + ) + .toPromise(); + + expect(errors).toEqual( + expect.arrayContaining([ + `Error: Plugin manifest must contain an "id" property. (invalid-manifest, ${manifestPath( + 'sub1', + 'sub2', + 'plugin_d' + )})`, + ]) + ); + }); + + it('does not discover plugins nested inside another plugin', async () => { + const { plugin$ } = discover(new PluginsConfig(pluginConfig, env), coreContext); + + mockFs( + { + [`${KIBANA_ROOT}/src/plugins/plugin_a`]: { + ...Plugins.valid('pluginA'), + nested_plugin: Plugins.valid('nestedPlugin'), + }, + }, + { createCwd: false } + ); + + const plugins = await plugin$.pipe(toArray()).toPromise(); + const pluginNames = plugins.map((plugin) => plugin.name); + + expect(pluginNames).toEqual(['pluginA']); + }); + + it('stops scanning when reaching `maxDepth`', async () => { + const { plugin$ } = discover(new PluginsConfig(pluginConfig, env), coreContext); + + mockFs( + { + [`${KIBANA_ROOT}/src/plugins/sub1/plugin`]: Plugins.valid('plugin1'), + [`${KIBANA_ROOT}/src/plugins/sub1/sub2/plugin`]: Plugins.valid('plugin2'), + [`${KIBANA_ROOT}/src/plugins/sub1/sub2/sub3/plugin`]: Plugins.valid('plugin3'), + [`${KIBANA_ROOT}/src/plugins/sub1/sub2/sub3/sub4/plugin`]: Plugins.valid('plugin4'), + [`${KIBANA_ROOT}/src/plugins/sub1/sub2/sub3/sub4/sub5/plugin`]: Plugins.valid('plugin5'), + [`${KIBANA_ROOT}/src/plugins/sub1/sub2/sub3/sub4/sub5/sub6/plugin`]: Plugins.valid( + 'plugin6' + ), + }, + { createCwd: false } + ); + + const plugins = await plugin$.pipe(toArray()).toPromise(); + const pluginNames = plugins.map((plugin) => plugin.name); + + expect(pluginNames).toHaveLength(5); + expect(pluginNames).toEqual( + expect.arrayContaining(['plugin1', 'plugin2', 'plugin3', 'plugin4', 'plugin5']) + ); + }); + + it('works with symlinks', async () => { + const { plugin$ } = discover(new PluginsConfig(pluginConfig, env), coreContext); + + const pluginFolder = resolve(KIBANA_ROOT, '..', 'ext-plugins'); + + mockFs( + { + [`${KIBANA_ROOT}/plugins`]: mockFs.symlink({ + path: '../ext-plugins', + }), + [pluginFolder]: { + plugin_a: Plugins.valid('pluginA'), + plugin_b: Plugins.valid('pluginB'), + }, + }, + { createCwd: false } + ); + + const plugins = await plugin$.pipe(toArray()).toPromise(); + const pluginNames = plugins.map((plugin) => plugin.name); + + expect(pluginNames).toHaveLength(2); + expect(pluginNames).toEqual(expect.arrayContaining(['pluginA', 'pluginB'])); }); - expect(loggingSystemMock.collect(logger).warn).toEqual([]); + it('logs a warning about --plugin-path when used in development', async () => { + const extraPluginTestPath = resolve(process.cwd(), 'my-extra-plugin'); + + env = Env.createDefault( + getEnvOptions({ + cliArgs: { dev: false, envName: 'development' }, + }) + ); + + discover(new PluginsConfig({ ...pluginConfig, paths: [extraPluginTestPath] }, env), { + coreId: Symbol(), + configService, + env, + logger, + }); + + expect(loggingSystemMock.collect(logger).warn).toEqual([ + [ + `Explicit plugin paths [${extraPluginTestPath}] should only be used in development. Relative imports may not work properly in production.`, + ], + ]); + }); + + test('does not log a warning about --plugin-path when used in production', async () => { + const extraPluginTestPath = resolve(process.cwd(), 'my-extra-plugin'); + + env = Env.createDefault( + getEnvOptions({ + cliArgs: { dev: false, envName: 'production' }, + }) + ); + + discover(new PluginsConfig({ ...pluginConfig, paths: [extraPluginTestPath] }, env), { + coreId: Symbol(), + configService, + env, + logger, + }); + + expect(loggingSystemMock.collect(logger).warn).toEqual([]); + }); }); diff --git a/src/core/server/plugins/discovery/plugins_discovery.ts b/src/core/server/plugins/discovery/plugins_discovery.ts index 1910483211e34..5e765a9632e55 100644 --- a/src/core/server/plugins/discovery/plugins_discovery.ts +++ b/src/core/server/plugins/discovery/plugins_discovery.ts @@ -19,7 +19,7 @@ import { readdir, stat } from 'fs'; import { resolve } from 'path'; -import { bindNodeCallback, from, merge } from 'rxjs'; +import { bindNodeCallback, from, merge, Observable } from 'rxjs'; import { catchError, filter, map, mergeMap, shareReplay } from 'rxjs/operators'; import { CoreContext } from '../../core_context'; import { Logger } from '../../logging'; @@ -32,6 +32,13 @@ import { parseManifest } from './plugin_manifest_parser'; const fsReadDir$ = bindNodeCallback(readdir); const fsStat$ = bindNodeCallback(stat); +const maxScanDepth = 5; + +interface PluginSearchPathEntry { + dir: string; + depth: number; +} + /** * Tries to discover all possible plugins based on the provided plugin config. * Discovery result consists of two separate streams, the one (`plugin$`) is @@ -75,34 +82,96 @@ export function discover(config: PluginsConfig, coreContext: CoreContext) { } /** - * Iterates over every plugin search path and returns a merged stream of all - * sub-directories. If directory cannot be read or it's impossible to get stat + * Recursively iterates over every plugin search path and returns a merged stream of all + * sub-directories containing a manifest file. If directory cannot be read or it's impossible to get stat * for any of the nested entries then error is added into the stream instead. + * * @param pluginDirs List of the top-level directories to process. * @param log Plugin discovery logger instance. */ -function processPluginSearchPaths$(pluginDirs: readonly string[], log: Logger) { - return from(pluginDirs).pipe( - mergeMap((dir) => { - log.debug(`Scanning "${dir}" for plugin sub-directories...`); +function processPluginSearchPaths$( + pluginDirs: readonly string[], + log: Logger +): Observable { + function recursiveScanFolder( + ent: PluginSearchPathEntry + ): Observable { + return from([ent]).pipe( + mergeMap((entry) => { + return findManifestInFolder(entry.dir, () => { + if (entry.depth > maxScanDepth) { + return []; + } + return mapSubdirectories(entry.dir, (subDir) => + recursiveScanFolder({ dir: subDir, depth: entry.depth + 1 }) + ); + }); + }) + ); + } - return fsReadDir$(dir).pipe( - mergeMap((subDirs: string[]) => subDirs.map((subDir) => resolve(dir, subDir))), - mergeMap((path) => - fsStat$(path).pipe( - // Filter out non-directory entries from target directories, it's expected that - // these directories may contain files (e.g. `README.md` or `package.json`). - // We shouldn't silently ignore the entries we couldn't get stat for though. - mergeMap((pathStat) => (pathStat.isDirectory() ? [path] : [])), - catchError((err) => [PluginDiscoveryError.invalidPluginPath(path, err)]) - ) - ), - catchError((err) => [PluginDiscoveryError.invalidSearchPath(dir, err)]) + return from(pluginDirs.map((dir) => ({ dir, depth: 0 }))).pipe( + mergeMap((entry) => { + log.debug(`Scanning "${entry.dir}" for plugin sub-directories...`); + return fsReadDir$(entry.dir).pipe( + mergeMap(() => recursiveScanFolder(entry)), + catchError((err) => [PluginDiscoveryError.invalidSearchPath(entry.dir, err)]) ); }) ); } +/** + * Attempts to read manifest file in specified directory or calls `notFound` and returns results if not found. For any + * manifest files that cannot be read, a PluginDiscoveryError is added. + * @param dir + * @param notFound + */ +function findManifestInFolder( + dir: string, + notFound: () => never[] | Observable +): string[] | Observable { + return fsStat$(resolve(dir, 'kibana.json')).pipe( + mergeMap((stats) => { + // `kibana.json` exists in given directory, we got a plugin + if (stats.isFile()) { + return [dir]; + } + return []; + }), + catchError((manifestStatError) => { + // did not find manifest. recursively process sub directories until we reach max depth. + if (manifestStatError.code !== 'ENOENT') { + return [PluginDiscoveryError.invalidPluginPath(dir, manifestStatError)]; + } + return notFound(); + }) + ); +} + +/** + * Finds all subdirectories in `dir` and executed `mapFunc` for each one. For any directories that cannot be read, + * a PluginDiscoveryError is added. + * @param dir + * @param mapFunc + */ +function mapSubdirectories( + dir: string, + mapFunc: (subDir: string) => Observable +): Observable { + return fsReadDir$(dir).pipe( + mergeMap((subDirs: string[]) => subDirs.map((subDir) => resolve(dir, subDir))), + mergeMap((subDir) => + fsStat$(subDir).pipe( + mergeMap((pathStat) => (pathStat.isDirectory() ? mapFunc(subDir) : [])), + catchError((subDirStatError) => [ + PluginDiscoveryError.invalidPluginPath(subDir, subDirStatError), + ]) + ) + ) + ); +} + /** * Tries to load and parse the plugin manifest file located at the provided plugin * directory path and produces an error result if it fails to do so or plugin manifest diff --git a/yarn.lock b/yarn.lock index d40a3abfa8939..ee61303e85f4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5436,6 +5436,13 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-7.0.2.tgz#b17f16cf933597e10d6d78eae3251e692ce8b0ce" integrity sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w== +"@types/mock-fs@^4.10.0": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@types/mock-fs/-/mock-fs-4.10.0.tgz#460061b186993d76856f669d5317cda8a007c24b" + integrity sha512-FQ5alSzmHMmliqcL36JqIA4Yyn9jyJKvRSGV3mvPh108VFatX7naJDzSG4fnFQNZFq9dIx0Dzoe6ddflMB2Xkg== + dependencies: + "@types/node" "*" + "@types/moment-timezone@^0.5.12": version "0.5.12" resolved "https://registry.yarnpkg.com/@types/moment-timezone/-/moment-timezone-0.5.12.tgz#0fb680c03db194fe8ff4551eaeb1eec8d3d80e9f" @@ -22016,6 +22023,11 @@ mochawesome@^4.1.0: strip-ansi "^5.0.0" uuid "^3.3.2" +mock-fs@^4.12.0: + version "4.12.0" + resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.12.0.tgz#a5d50b12d2d75e5bec9dac3b67ffe3c41d31ade4" + integrity sha512-/P/HtrlvBxY4o/PzXY9cCNBrdylDNxg7gnrv2sMNxj+UJ2m8jSpl0/A6fuJeNAWr99ZvGWH8XCbE0vmnM5KupQ== + mock-http-server@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/mock-http-server/-/mock-http-server-1.3.0.tgz#d2c2ffe65f77d3a4da8302c91d3bf687e5b51519" From a40e58e898d1126fa361688a2eb1ca484963f2e9 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 30 Jun 2020 10:59:14 +0200 Subject: [PATCH 07/23] [Discover] Deangularize Skip to bottom button (#69811) Co-authored-by: Elastic Machine --- .../public/application/_discover.scss | 12 +---- .../public/application/angular/discover.html | 15 +----- .../public/application/angular/discover.js | 27 ++++++---- .../components/skip_bottom_button/index.ts | 21 ++++++++ .../skip_bottom_button.test.tsx | 41 ++++++++++++++ .../skip_bottom_button/skip_bottom_button.tsx | 53 +++++++++++++++++++ .../skip_bottom_button_directive.ts | 23 ++++++++ .../discover/public/get_inner_angular.ts | 2 + 8 files changed, 159 insertions(+), 35 deletions(-) create mode 100644 src/plugins/discover/public/application/components/skip_bottom_button/index.ts create mode 100644 src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.test.tsx create mode 100644 src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.tsx create mode 100644 src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button_directive.ts diff --git a/src/plugins/discover/public/application/_discover.scss b/src/plugins/discover/public/application/_discover.scss index b0f3dfaf96c4f..1aaa0a24357ed 100644 --- a/src/plugins/discover/public/application/_discover.scss +++ b/src/plugins/discover/public/application/_discover.scss @@ -100,16 +100,6 @@ discover-app { .dscSkipButton { position: absolute; - left: -10000px; + right: $euiSizeM; top: $euiSizeXS; - width: 1px; - height: 1px; - overflow: hidden; - - &:focus { - left: initial; - right: $euiSize; - width: auto; - height: auto; - } } diff --git a/src/plugins/discover/public/application/angular/discover.html b/src/plugins/discover/public/application/angular/discover.html index 022c39afff27f..3c16e4a6d9dee 100644 --- a/src/plugins/discover/public/application/angular/discover.html +++ b/src/plugins/discover/public/application/angular/discover.html @@ -65,18 +65,7 @@

{{screenTitle}}

- + {{screenTitle}} on-remove-column="removeColumn" > - +
{ + bottomMarker.focus(); + // The anchor tag is not technically empty (it's a hack to make Safari scroll) + // so the browser will show a highlight: remove the focus once scrolled + $timeout(() => { + bottomMarker.blur(); + }, 0); + }, 0); + }; + $scope.newQuery = function () { history.push('/'); }; @@ -1007,17 +1023,6 @@ function discoverController( $window.scrollTo(0, 0); }; - $scope.scrollToBottom = function () { - // delay scrolling to after the rows have been rendered - $timeout(() => { - $element.find('#discoverBottomMarker').focus(); - }, 0); - }; - - $scope.showAllRows = function () { - $scope.minimumVisibleRows = $scope.hits; - }; - async function setupVisualization() { // If no timefield has been specified we don't create a histogram of messages if (!getTimeField()) return; diff --git a/src/plugins/discover/public/application/components/skip_bottom_button/index.ts b/src/plugins/discover/public/application/components/skip_bottom_button/index.ts new file mode 100644 index 0000000000000..2feaa35e0d61f --- /dev/null +++ b/src/plugins/discover/public/application/components/skip_bottom_button/index.ts @@ -0,0 +1,21 @@ +/* + * 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 { SkipBottomButton } from './skip_bottom_button'; +export { createSkipBottomButtonDirective } from './skip_bottom_button_directive'; diff --git a/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.test.tsx b/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.test.tsx new file mode 100644 index 0000000000000..bf417f9f1890b --- /dev/null +++ b/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.test.tsx @@ -0,0 +1,41 @@ +/* + * 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 { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { ReactWrapper } from 'enzyme'; +import { SkipBottomButton, SkipBottomButtonProps } from './skip_bottom_button'; +// @ts-ignore +import { findTestSubject } from '@elastic/eui/lib/test'; + +describe('Skip to Bottom Button', function () { + let props: SkipBottomButtonProps; + let component: ReactWrapper; + + beforeAll(() => { + props = { + onClick: jest.fn(), + }; + }); + + it('should be clickable', function () { + component = mountWithIntl(); + component.simulate('click'); + expect(props.onClick).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.tsx b/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.tsx new file mode 100644 index 0000000000000..ccf05ca031a8d --- /dev/null +++ b/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button.tsx @@ -0,0 +1,53 @@ +/* + * 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 { EuiSkipLink } from '@elastic/eui'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; + +export interface SkipBottomButtonProps { + /** + * Action to perform on click + */ + onClick: () => void; +} + +export function SkipBottomButton({ onClick }: SkipBottomButtonProps) { + return ( + + { + // prevent the anchor to reload the page on click + event.preventDefault(); + // The destinationId prop cannot be leveraged here as the table needs + // to be updated first (angular logic) + onClick(); + }} + className="dscSkipButton" + destinationId="" + > + + + + ); +} diff --git a/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button_directive.ts b/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button_directive.ts new file mode 100644 index 0000000000000..27f17b25fd447 --- /dev/null +++ b/src/plugins/discover/public/application/components/skip_bottom_button/skip_bottom_button_directive.ts @@ -0,0 +1,23 @@ +/* + * 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 { SkipBottomButton } from './skip_bottom_button'; + +export function createSkipBottomButtonDirective(reactDirective: any) { + return reactDirective(SkipBottomButton, [['onClick', { watchDepth: 'reference' }]]); +} diff --git a/src/plugins/discover/public/get_inner_angular.ts b/src/plugins/discover/public/get_inner_angular.ts index 2b4705645cfcc..05513eef93624 100644 --- a/src/plugins/discover/public/get_inner_angular.ts +++ b/src/plugins/discover/public/get_inner_angular.ts @@ -63,6 +63,7 @@ import { createLoadingSpinnerDirective } from '././application/components/loadin import { createTimechartHeaderDirective } from './application/components/timechart_header'; import { DiscoverStartPlugins } from './plugin'; import { getScopedHistory } from './kibana_services'; +import { createSkipBottomButtonDirective } from './application/components/skip_bottom_button'; /** * returns the main inner angular module, it contains all the parts of Angular Discover @@ -155,6 +156,7 @@ export function initializeInnerAngularModule( .directive('fixedScroll', FixedScrollProvider) .directive('renderComplete', createRenderCompleteDirective) .directive('discoverSidebar', createDiscoverSidebarDirective) + .directive('skipBottomButton', createSkipBottomButtonDirective) .directive('hitsCounter', createHitsCounterDirective) .directive('loadingSpinner', createLoadingSpinnerDirective) .directive('timechartHeader', createTimechartHeaderDirective) From ceb8595151768601f5257ec8d7bb2163328acf59 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Tue, 30 Jun 2020 10:28:54 +0100 Subject: [PATCH 08/23] [Logs UI] [Alerting] "Group by" functionality (#68250) - Add "group by" functionality to logs alerts --- .../infra/common/alerting/logs/types.ts | 103 +++- .../utils/elasticsearch_runtime_types.ts | 18 + .../logs/expression_editor/editor.tsx | 26 +- .../alerting/logs/log_threshold_alert_type.ts | 2 +- .../group_by_expression.tsx | 85 +++ .../shared/group_by_expression/selector.tsx | 56 ++ .../lib/adapters/framework/adapter_types.ts | 1 + .../log_threshold_executor.test.ts | 572 ++++++++++++------ .../log_threshold/log_threshold_executor.ts | 292 +++++++-- .../register_log_threshold_alert_type.ts | 9 + 10 files changed, 918 insertions(+), 246 deletions(-) create mode 100644 x-pack/plugins/infra/common/utils/elasticsearch_runtime_types.ts create mode 100644 x-pack/plugins/infra/public/components/alerting/shared/group_by_expression/group_by_expression.tsx create mode 100644 x-pack/plugins/infra/public/components/alerting/shared/group_by_expression/selector.tsx diff --git a/x-pack/plugins/infra/common/alerting/logs/types.ts b/x-pack/plugins/infra/common/alerting/logs/types.ts index cbfffbfd8f940..884a813d74c86 100644 --- a/x-pack/plugins/infra/common/alerting/logs/types.ts +++ b/x-pack/plugins/infra/common/alerting/logs/types.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; +import * as rt from 'io-ts'; +import { commonSearchSuccessResponseFieldsRT } from '../../utils/elasticsearch_runtime_types'; export const LOG_DOCUMENT_COUNT_ALERT_TYPE_ID = 'logs.alert.document.count'; @@ -20,6 +22,19 @@ export enum Comparator { NOT_MATCH_PHRASE = 'does not match phrase', } +const ComparatorRT = rt.keyof({ + [Comparator.GT]: null, + [Comparator.GT_OR_EQ]: null, + [Comparator.LT]: null, + [Comparator.LT_OR_EQ]: null, + [Comparator.EQ]: null, + [Comparator.NOT_EQ]: null, + [Comparator.MATCH]: null, + [Comparator.NOT_MATCH]: null, + [Comparator.MATCH_PHRASE]: null, + [Comparator.NOT_MATCH_PHRASE]: null, +}); + // Maps our comparators to i18n strings, some comparators have more specific wording // depending on the field type the comparator is being used with. export const ComparatorToi18nMap = { @@ -74,22 +89,78 @@ export enum AlertStates { ERROR, } -export interface DocumentCount { - comparator: Comparator; - value: number; -} +const DocumentCountRT = rt.type({ + comparator: ComparatorRT, + value: rt.number, +}); -export interface Criterion { - field: string; - comparator: Comparator; - value: string | number; -} +export type DocumentCount = rt.TypeOf; -export interface LogDocumentCountAlertParams { - count: DocumentCount; - criteria: Criterion[]; - timeUnit: 's' | 'm' | 'h' | 'd'; - timeSize: number; -} +const CriterionRT = rt.type({ + field: rt.string, + comparator: ComparatorRT, + value: rt.union([rt.string, rt.number]), +}); + +export type Criterion = rt.TypeOf; + +const TimeUnitRT = rt.union([rt.literal('s'), rt.literal('m'), rt.literal('h'), rt.literal('d')]); +export type TimeUnit = rt.TypeOf; + +export const LogDocumentCountAlertParamsRT = rt.intersection([ + rt.type({ + count: DocumentCountRT, + criteria: rt.array(CriterionRT), + timeUnit: TimeUnitRT, + timeSize: rt.number, + }), + rt.partial({ + groupBy: rt.array(rt.string), + }), +]); + +export type LogDocumentCountAlertParams = rt.TypeOf; + +export const UngroupedSearchQueryResponseRT = rt.intersection([ + commonSearchSuccessResponseFieldsRT, + rt.type({ + hits: rt.type({ + total: rt.type({ + value: rt.number, + }), + }), + }), +]); + +export type UngroupedSearchQueryResponse = rt.TypeOf; + +export const GroupedSearchQueryResponseRT = rt.intersection([ + commonSearchSuccessResponseFieldsRT, + rt.type({ + aggregations: rt.type({ + groups: rt.intersection([ + rt.type({ + buckets: rt.array( + rt.type({ + key: rt.record(rt.string, rt.string), + doc_count: rt.number, + filtered_results: rt.type({ + doc_count: rt.number, + }), + }) + ), + }), + rt.partial({ + after_key: rt.record(rt.string, rt.string), + }), + ]), + }), + hits: rt.type({ + total: rt.type({ + value: rt.number, + }), + }), + }), +]); -export type TimeUnit = 's' | 'm' | 'h' | 'd'; +export type GroupedSearchQueryResponse = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/utils/elasticsearch_runtime_types.ts b/x-pack/plugins/infra/common/utils/elasticsearch_runtime_types.ts new file mode 100644 index 0000000000000..a48c65d648b25 --- /dev/null +++ b/x-pack/plugins/infra/common/utils/elasticsearch_runtime_types.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +export const commonSearchSuccessResponseFieldsRT = rt.type({ + _shards: rt.type({ + total: rt.number, + successful: rt.number, + skipped: rt.number, + failed: rt.number, + }), + timed_out: rt.boolean, + took: rt.number, +}); diff --git a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx index 9e4e78ca392fd..295e60552cce5 100644 --- a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx +++ b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx @@ -22,6 +22,7 @@ import { DocumentCount } from './document_count'; import { Criteria } from './criteria'; import { useSourceId } from '../../../../containers/source_id'; import { LogSourceProvider, useLogSourceContext } from '../../../../containers/logs/log_source'; +import { GroupByExpression } from '../../shared/group_by_expression/group_by_expression'; export interface ExpressionCriteria { field?: string; @@ -121,7 +122,6 @@ export const Editor: React.FC = (props) => { const { setAlertParams, alertParams, errors } = props; const [hasSetDefaults, setHasSetDefaults] = useState(false); const { sourceStatus } = useLogSourceContext(); - useMount(() => { for (const [key, value] of Object.entries({ ...DEFAULT_EXPRESSION, ...alertParams })) { setAlertParams(key, value); @@ -140,6 +140,17 @@ export const Editor: React.FC = (props) => { /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [sourceStatus]); + const groupByFields = useMemo(() => { + if (sourceStatus?.logIndexFields) { + return sourceStatus.logIndexFields.filter((field) => { + return field.type === 'string' && field.aggregatable; + }); + } else { + return []; + } + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + }, [sourceStatus]); + const updateCount = useCallback( (countParams) => { const nextCountParams = { ...alertParams.count, ...countParams }; @@ -172,6 +183,13 @@ export const Editor: React.FC = (props) => { [setAlertParams] ); + const updateGroupBy = useCallback( + (groups: string[]) => { + setAlertParams('groupBy', groups); + }, + [setAlertParams] + ); + const addCriterion = useCallback(() => { const nextCriteria = alertParams?.criteria ? [...alertParams.criteria, DEFAULT_CRITERIA] @@ -219,6 +237,12 @@ export const Editor: React.FC = (props) => { errors={errors as { [key: string]: string[] }} /> + +
void; + label?: string; +} + +const DEFAULT_GROUP_BY_LABEL = i18n.translate('xpack.infra.alerting.alertFlyout.groupByLabel', { + defaultMessage: 'Group By', +}); + +const EVERYTHING_PLACEHOLDER = i18n.translate( + 'xpack.infra.alerting.alertFlyout.groupBy.placeholder', + { + defaultMessage: 'Nothing (ungrouped)', + } +); + +export const GroupByExpression: React.FC = ({ + selectedGroups = [], + fields, + label, + onChange, +}) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const expressionValue = useMemo(() => { + return selectedGroups.length > 0 ? selectedGroups.join(', ') : EVERYTHING_PLACEHOLDER; + }, [selectedGroups]); + + const labelProp = label ?? DEFAULT_GROUP_BY_LABEL; + + return ( + + + setIsPopoverOpen(true)} + /> + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + ownFocus + panelPaddingSize="s" + anchorPosition="downLeft" + > +
+ {labelProp} + +
+
+
+
+ ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/shared/group_by_expression/selector.tsx b/x-pack/plugins/infra/public/components/alerting/shared/group_by_expression/selector.tsx new file mode 100644 index 0000000000000..7a6a7ff77335b --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/shared/group_by_expression/selector.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 { EuiComboBox } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { IFieldType } from 'src/plugins/data/public'; + +interface Props { + selectedGroups?: string[]; + onChange: (groupBy: string[]) => void; + fields: IFieldType[]; + label: string; + placeholder: string; +} + +export const GroupBySelector = ({ + onChange, + fields, + selectedGroups = [], + label, + placeholder, +}: Props) => { + const handleChange = useCallback( + (selectedOptions: Array<{ label: string }>) => { + const groupBy = selectedOptions.map((option) => option.label); + onChange(groupBy); + }, + [onChange] + ); + + const formattedSelectedGroups = useMemo(() => { + return selectedGroups.map((group) => ({ label: group })); + }, [selectedGroups]); + + const options = useMemo(() => { + return fields.filter((field) => field.aggregatable).map((field) => ({ label: field.name })); + }, [fields]); + + return ( +
+ +
+ ); +}; diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 905b7dfa314bd..018e5098a4291 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -60,6 +60,7 @@ export interface InfraDatabaseSearchResponse skipped: number; failed: number; }; + timed_out: boolean; aggregations?: Aggregations; hits: { total: { diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts index a3b9e85458416..4f1e81e0b2c40 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts @@ -55,7 +55,7 @@ services.alertInstanceFactory.mockImplementation((instanceId: string) => { * Helper functions */ function getAlertState(instanceId: string): AlertStates { - const alert = alertInstances.get(instanceId); + const alert = alertInstances.get(`${instanceId}-*`); if (alert) { return alert.state.alertState; } else { @@ -73,11 +73,26 @@ const executor = (createLogThresholdExecutor('test', libsMock) as unknown) as (o // Wrapper to test type Comparison = [number, Comparator, number]; + async function callExecutor( [value, comparator, threshold]: Comparison, criteria: Criterion[] = [] ) { - services.callCluster.mockImplementationOnce(async (..._) => ({ count: value })); + services.callCluster.mockImplementationOnce(async (..._) => ({ + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + timed_out: false, + took: 123456789, + hits: { + total: { + value, + }, + }, + })); return await executor({ services, @@ -90,222 +105,427 @@ async function callExecutor( }); } -describe('Comparators trigger alerts correctly', () => { - it('does not alert when counts do not reach the threshold', async () => { - await callExecutor([0, Comparator.GT, 1]); - expect(getAlertState('test')).toBe(AlertStates.OK); +describe('Ungrouped alerts', () => { + describe('Comparators trigger alerts correctly', () => { + it('does not alert when counts do not reach the threshold', async () => { + await callExecutor([0, Comparator.GT, 1]); + expect(getAlertState('test')).toBe(AlertStates.OK); - await callExecutor([0, Comparator.GT_OR_EQ, 1]); - expect(getAlertState('test')).toBe(AlertStates.OK); + await callExecutor([0, Comparator.GT_OR_EQ, 1]); + expect(getAlertState('test')).toBe(AlertStates.OK); - await callExecutor([1, Comparator.LT, 0]); - expect(getAlertState('test')).toBe(AlertStates.OK); + await callExecutor([1, Comparator.LT, 0]); + expect(getAlertState('test')).toBe(AlertStates.OK); - await callExecutor([1, Comparator.LT_OR_EQ, 0]); - expect(getAlertState('test')).toBe(AlertStates.OK); - }); + await callExecutor([1, Comparator.LT_OR_EQ, 0]); + expect(getAlertState('test')).toBe(AlertStates.OK); + }); - it('alerts when counts reach the threshold', async () => { - await callExecutor([2, Comparator.GT, 1]); - expect(getAlertState('test')).toBe(AlertStates.ALERT); + it('alerts when counts reach the threshold', async () => { + await callExecutor([2, Comparator.GT, 1]); + expect(getAlertState('test')).toBe(AlertStates.ALERT); - await callExecutor([1, Comparator.GT_OR_EQ, 1]); - expect(getAlertState('test')).toBe(AlertStates.ALERT); + await callExecutor([1, Comparator.GT_OR_EQ, 1]); + expect(getAlertState('test')).toBe(AlertStates.ALERT); - await callExecutor([1, Comparator.LT, 2]); - expect(getAlertState('test')).toBe(AlertStates.ALERT); + await callExecutor([1, Comparator.LT, 2]); + expect(getAlertState('test')).toBe(AlertStates.ALERT); - await callExecutor([2, Comparator.LT_OR_EQ, 2]); - expect(getAlertState('test')).toBe(AlertStates.ALERT); + await callExecutor([2, Comparator.LT_OR_EQ, 2]); + expect(getAlertState('test')).toBe(AlertStates.ALERT); + }); }); -}); -describe('Comparators create the correct ES queries', () => { - beforeEach(() => { - services.callCluster.mockReset(); - }); + describe('Comparators create the correct ES queries', () => { + beforeEach(() => { + services.callCluster.mockReset(); + }); - it('Works with `Comparator.EQ`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.EQ, value: 'bar' }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - expect(query.body).toMatchObject({ - query: { - bool: { - must: [{ term: { foo: { value: 'bar' } } }], + it('Works with `Comparator.EQ`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.EQ, value: 'bar' }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + + expect(query.body).toMatchObject({ + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + format: 'epoch_millis', + }, + }, + }, + { + term: { + foo: { + value: 'bar', + }, + }, + }, + ], + }, }, - }, + size: 0, + }); }); - }); - it('works with `Comparator.NOT_EQ`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.NOT_EQ, value: 'bar' }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - expect(query.body).toMatchObject({ - query: { - bool: { - must_not: [{ term: { foo: { value: 'bar' } } }], + it('works with `Comparator.NOT_EQ`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.NOT_EQ, value: 'bar' }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + + expect(query.body).toMatchObject({ + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + format: 'epoch_millis', + }, + }, + }, + ], + must_not: [ + { + term: { + foo: { + value: 'bar', + }, + }, + }, + ], + }, }, - }, + size: 0, + }); }); - }); - it('works with `Comparator.MATCH`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.MATCH, value: 'bar' }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - expect(query.body).toMatchObject({ - query: { - bool: { - must: [{ match: { foo: 'bar' } }], + it('works with `Comparator.MATCH`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.MATCH, value: 'bar' }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + + expect(query.body).toMatchObject({ + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + format: 'epoch_millis', + }, + }, + }, + { + match: { + foo: 'bar', + }, + }, + ], + }, }, - }, + size: 0, + }); }); - }); - it('works with `Comparator.NOT_MATCH`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.NOT_MATCH, value: 'bar' }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - expect(query.body).toMatchObject({ - query: { - bool: { - must_not: [{ match: { foo: 'bar' } }], + it('works with `Comparator.NOT_MATCH`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.NOT_MATCH, value: 'bar' }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + + expect(query.body).toMatchObject({ + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + format: 'epoch_millis', + }, + }, + }, + ], + must_not: [ + { + match: { + foo: 'bar', + }, + }, + ], + }, }, - }, + size: 0, + }); }); - }); - it('works with `Comparator.MATCH_PHRASE`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.MATCH_PHRASE, value: 'bar' }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - expect(query.body).toMatchObject({ - query: { - bool: { - must: [{ match_phrase: { foo: 'bar' } }], + it('works with `Comparator.MATCH_PHRASE`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.MATCH_PHRASE, value: 'bar' }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + + expect(query.body).toMatchObject({ + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + format: 'epoch_millis', + }, + }, + }, + { + match_phrase: { + foo: 'bar', + }, + }, + ], + }, }, - }, + size: 0, + }); }); - }); - it('works with `Comparator.NOT_MATCH_PHRASE`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.NOT_MATCH_PHRASE, value: 'bar' }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - expect(query.body).toMatchObject({ - query: { - bool: { - must_not: [{ match_phrase: { foo: 'bar' } }], + it('works with `Comparator.NOT_MATCH_PHRASE`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.NOT_MATCH_PHRASE, value: 'bar' }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + + expect(query.body).toMatchObject({ + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + format: 'epoch_millis', + }, + }, + }, + ], + must_not: [ + { + match_phrase: { + foo: 'bar', + }, + }, + ], + }, }, - }, + size: 0, + }); }); - }); - it('works with `Comparator.GT`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.GT, value: 1 }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - expect(query.body).toMatchObject({ - query: { - bool: { - must: [{ range: { foo: { gt: 1 } } }], + it('works with `Comparator.GT`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.GT, value: 1 }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + + expect(query.body).toMatchObject({ + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + format: 'epoch_millis', + }, + }, + }, + { + range: { + foo: { + gt: 1, + }, + }, + }, + ], + }, }, - }, + size: 0, + }); }); - }); - it('works with `Comparator.GT_OR_EQ`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.GT_OR_EQ, value: 1 }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - expect(query.body).toMatchObject({ - query: { - bool: { - must: [{ range: { foo: { gte: 1 } } }], + it('works with `Comparator.GT_OR_EQ`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.GT_OR_EQ, value: 1 }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + + expect(query.body).toMatchObject({ + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + format: 'epoch_millis', + }, + }, + }, + { + range: { + foo: { + gte: 1, + }, + }, + }, + ], + }, }, - }, + size: 0, + }); }); - }); - it('works with `Comparator.LT`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.LT, value: 1 }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - expect(query.body).toMatchObject({ - query: { - bool: { - must: [{ range: { foo: { lt: 1 } } }], + it('works with `Comparator.LT`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.LT, value: 1 }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + + expect(query.body).toMatchObject({ + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + format: 'epoch_millis', + }, + }, + }, + { + range: { + foo: { + lt: 1, + }, + }, + }, + ], + }, }, - }, + size: 0, + }); }); - }); - it('works with `Comparator.LT_OR_EQ`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.LT_OR_EQ, value: 1 }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - expect(query.body).toMatchObject({ - query: { - bool: { - must: [{ range: { foo: { lte: 1 } } }], + it('works with `Comparator.LT_OR_EQ`', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [{ field: 'foo', comparator: Comparator.LT_OR_EQ, value: 1 }] + ); + + const query = services.callCluster.mock.calls[0][1]!; + + expect(query.body).toMatchObject({ + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + format: 'epoch_millis', + }, + }, + }, + { + range: { + foo: { + lte: 1, + }, + }, + }, + ], + }, }, - }, + size: 0, + }); }); }); -}); -describe('Multiple criteria create the right ES query', () => { - beforeEach(() => { - services.callCluster.mockReset(); - }); - it('works', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [ - { field: 'foo', comparator: Comparator.EQ, value: 'bar' }, - { field: 'http.status', comparator: Comparator.LT, value: 400 }, - ] - ); - - const query = services.callCluster.mock.calls[0][1]!; - expect(query.body).toMatchObject({ - query: { - bool: { - must: [{ term: { foo: { value: 'bar' } } }, { range: { 'http.status': { lt: 400 } } }], + describe('Multiple criteria create the right ES query', () => { + beforeEach(() => { + services.callCluster.mockReset(); + }); + it('works', async () => { + await callExecutor( + [2, Comparator.GT, 1], // Not relevant + [ + { field: 'foo', comparator: Comparator.EQ, value: 'bar' }, + { field: 'http.status', comparator: Comparator.LT, value: 400 }, + ] + ); + + const query = services.callCluster.mock.calls[0][1]!; + + expect(query.body).toMatchObject({ + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + format: 'epoch_millis', + }, + }, + }, + { + term: { + foo: { + value: 'bar', + }, + }, + }, + { + range: { + 'http.status': { + lt: 400, + }, + }, + }, + ], + }, }, - }, + size: 0, + }); }); }); }); diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts index ee4e1fcb3f6e2..a2fd01f859385 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -11,10 +11,19 @@ import { Comparator, LogDocumentCountAlertParams, Criterion, + GroupedSearchQueryResponseRT, + UngroupedSearchQueryResponseRT, + UngroupedSearchQueryResponse, + GroupedSearchQueryResponse, + LogDocumentCountAlertParamsRT, } from '../../../../common/alerting/logs/types'; import { InfraBackendLibs } from '../../infra_types'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { InfraSource } from '../../../../common/http_api/source_api'; +import { decodeOrThrow } from '../../../../common/runtime_types'; + +const UNGROUPED_FACTORY_KEY = '*'; +const COMPOSITE_GROUP_SIZE = 40; const checkValueAgainstComparatorMap: { [key: string]: (a: number, b: number) => boolean; @@ -25,37 +34,42 @@ const checkValueAgainstComparatorMap: { [Comparator.LT_OR_EQ]: (a: number, b: number) => a <= b, }; -export const createLogThresholdExecutor = (alertUUID: string, libs: InfraBackendLibs) => +export const createLogThresholdExecutor = (alertId: string, libs: InfraBackendLibs) => async function ({ services, params }: AlertExecutorOptions) { - const { count, criteria } = params as LogDocumentCountAlertParams; const { alertInstanceFactory, savedObjectsClient, callCluster } = services; const { sources } = libs; + const { groupBy } = params; const sourceConfiguration = await sources.getSourceConfiguration(savedObjectsClient, 'default'); const indexPattern = sourceConfiguration.configuration.logAlias; - - const alertInstance = alertInstanceFactory(alertUUID); + const alertInstance = alertInstanceFactory(alertId); try { - const query = getESQuery( - params as LogDocumentCountAlertParams, - sourceConfiguration.configuration - ); - const result = await getResults(query, indexPattern, callCluster); - - if (checkValueAgainstComparatorMap[count.comparator](result.count, count.value)) { - alertInstance.scheduleActions(FIRED_ACTIONS.id, { - matchingDocuments: result.count, - conditions: createConditionsMessage(criteria), - }); - - alertInstance.replaceState({ - alertState: AlertStates.ALERT, - }); + const validatedParams = decodeOrThrow(LogDocumentCountAlertParamsRT)(params); + + const query = + groupBy && groupBy.length > 0 + ? getGroupedESQuery(validatedParams, sourceConfiguration.configuration, indexPattern) + : getUngroupedESQuery(validatedParams, sourceConfiguration.configuration, indexPattern); + + if (!query) { + throw new Error('ES query could not be built from the provided alert params'); + } + + if (groupBy && groupBy.length > 0) { + processGroupByResults( + await getGroupedResults(query, callCluster), + validatedParams, + alertInstanceFactory, + alertId + ); } else { - alertInstance.replaceState({ - alertState: AlertStates.OK, - }); + processUngroupedResults( + await getUngroupedResults(query, callCluster), + validatedParams, + alertInstanceFactory, + alertId + ); } } catch (e) { alertInstance.replaceState({ @@ -66,27 +80,82 @@ export const createLogThresholdExecutor = (alertUUID: string, libs: InfraBackend } }; -const getESQuery = ( +const processUngroupedResults = ( + results: UngroupedSearchQueryResponse, params: LogDocumentCountAlertParams, - sourceConfiguration: InfraSource['configuration'] -): object => { + alertInstanceFactory: AlertExecutorOptions['services']['alertInstanceFactory'], + alertId: string +) => { + const { count, criteria } = params; + + const alertInstance = alertInstanceFactory(`${alertId}-${UNGROUPED_FACTORY_KEY}`); + const documentCount = results.hits.total.value; + + if (checkValueAgainstComparatorMap[count.comparator](documentCount, count.value)) { + alertInstance.scheduleActions(FIRED_ACTIONS.id, { + matchingDocuments: documentCount, + conditions: createConditionsMessage(criteria), + group: null, + }); + + alertInstance.replaceState({ + alertState: AlertStates.ALERT, + }); + } else { + alertInstance.replaceState({ + alertState: AlertStates.OK, + }); + } +}; + +interface ReducedGroupByResults { + name: string; + documentCount: number; +} + +const processGroupByResults = ( + results: GroupedSearchQueryResponse['aggregations']['groups']['buckets'], + params: LogDocumentCountAlertParams, + alertInstanceFactory: AlertExecutorOptions['services']['alertInstanceFactory'], + alertId: string +) => { + const { count, criteria } = params; + + const groupResults = results.reduce((acc, groupBucket) => { + const groupName = Object.values(groupBucket.key).join(', '); + const groupResult = { name: groupName, documentCount: groupBucket.filtered_results.doc_count }; + return [...acc, groupResult]; + }, []); + + groupResults.forEach((group) => { + const alertInstance = alertInstanceFactory(`${alertId}-${group.name}`); + const documentCount = group.documentCount; + + if (checkValueAgainstComparatorMap[count.comparator](documentCount, count.value)) { + alertInstance.scheduleActions(FIRED_ACTIONS.id, { + matchingDocuments: documentCount, + conditions: createConditionsMessage(criteria), + group: group.name, + }); + + alertInstance.replaceState({ + alertState: AlertStates.ALERT, + }); + } else { + alertInstance.replaceState({ + alertState: AlertStates.OK, + }); + } + }); +}; + +const buildFiltersFromCriteria = (params: LogDocumentCountAlertParams, timestampField: string) => { const { timeSize, timeUnit, criteria } = params; const interval = `${timeSize}${timeUnit}`; const intervalAsSeconds = getIntervalInSeconds(interval); + const intervalAsMs = intervalAsSeconds * 1000; const to = Date.now(); - const from = to - intervalAsSeconds * 1000; - - const rangeFilters = [ - { - range: { - [sourceConfiguration.fields.timestamp]: { - gte: from, - lte: to, - format: 'epoch_millis', - }, - }, - }, - ]; + const from = to - intervalAsMs; const positiveComparators = getPositiveComparators(); const negativeComparators = getNegativeComparators(); @@ -101,17 +170,121 @@ const getESQuery = ( // Negative assertions (things that "must not" match) const mustNotFilters = buildFiltersForCriteria(negativeCriteria); - const query = { + const rangeFilter = { + range: { + [timestampField]: { + gte: from, + lte: to, + format: 'epoch_millis', + }, + }, + }; + + // For group by scenarios we'll pad the time range by 1 x the interval size on the left (lte) and right (gte), this is so + // a wider net is cast to "capture" the groups. This is to account for scenarios where we want ascertain if + // there were "no documents" (less than 1 for example). In these cases we may be missing documents to build the groups + // and match / not match the criteria. + const groupedRangeFilter = { + range: { + [timestampField]: { + gte: from - intervalAsMs, + lte: to + intervalAsMs, + format: 'epoch_millis', + }, + }, + }; + + return { rangeFilter, groupedRangeFilter, mustFilters, mustNotFilters }; +}; + +const getGroupedESQuery = ( + params: LogDocumentCountAlertParams, + sourceConfiguration: InfraSource['configuration'], + index: string +): object | undefined => { + const { groupBy } = params; + + if (!groupBy || !groupBy.length) { + return; + } + + const timestampField = sourceConfiguration.fields.timestamp; + + const { rangeFilter, groupedRangeFilter, mustFilters, mustNotFilters } = buildFiltersFromCriteria( + params, + timestampField + ); + + const aggregations = { + groups: { + composite: { + size: COMPOSITE_GROUP_SIZE, + sources: groupBy.map((field, groupIndex) => ({ + [`group-${groupIndex}-${field}`]: { + terms: { field }, + }, + })), + }, + aggregations: { + filtered_results: { + filter: { + bool: { + // Scope the inner filtering back to the unpadded range + filter: [rangeFilter, ...mustFilters], + }, + }, + }, + }, + }, + }; + + const body = { query: { bool: { - filter: [...rangeFilters], - ...(mustFilters.length > 0 && { must: mustFilters }), + filter: [groupedRangeFilter], ...(mustNotFilters.length > 0 && { must_not: mustNotFilters }), }, }, + aggregations, + size: 0, }; - return query; + return { + index, + allowNoIndices: true, + ignoreUnavailable: true, + body, + }; +}; + +const getUngroupedESQuery = ( + params: LogDocumentCountAlertParams, + sourceConfiguration: InfraSource['configuration'], + index: string +): object => { + const { rangeFilter, mustFilters, mustNotFilters } = buildFiltersFromCriteria( + params, + sourceConfiguration.fields.timestamp + ); + + const body = { + // Ensure we accurately track the hit count for the ungrouped case, otherwise we can only ensure accuracy up to 10,000. + track_total_hits: true, + query: { + bool: { + filter: [rangeFilter, ...mustFilters], + ...(mustNotFilters.length > 0 && { must_not: mustNotFilters }), + }, + }, + size: 0, + }; + + return { + index, + allowNoIndices: true, + ignoreUnavailable: true, + body, + }; }; type SupportedESQueryTypes = 'term' | 'match' | 'match_phrase' | 'range'; @@ -145,7 +318,6 @@ const buildCriterionQuery = (criterion: Criterion): Filter | undefined => { }, }, }; - break; case 'match': { return { match: { @@ -221,15 +393,31 @@ const getQueryMappingForComparator = (comparator: Comparator) => { return queryMappings[comparator]; }; -const getResults = async ( - query: object, - index: string, - callCluster: AlertServices['callCluster'] -) => { - return await callCluster('count', { - body: query, - index, - }); +const getUngroupedResults = async (query: object, callCluster: AlertServices['callCluster']) => { + return decodeOrThrow(UngroupedSearchQueryResponseRT)(await callCluster('search', query)); +}; + +const getGroupedResults = async (query: object, callCluster: AlertServices['callCluster']) => { + let compositeGroupBuckets: GroupedSearchQueryResponse['aggregations']['groups']['buckets'] = []; + let lastAfterKey: GroupedSearchQueryResponse['aggregations']['groups']['after_key'] | undefined; + + while (true) { + const queryWithAfterKey: any = { ...query }; + queryWithAfterKey.body.aggregations.groups.composite.after = lastAfterKey; + const groupResponse: GroupedSearchQueryResponse = decodeOrThrow(GroupedSearchQueryResponseRT)( + await callCluster('search', queryWithAfterKey) + ); + compositeGroupBuckets = [ + ...compositeGroupBuckets, + ...groupResponse.aggregations.groups.buckets, + ]; + lastAfterKey = groupResponse.aggregations.groups.after_key; + if (groupResponse.aggregations.groups.buckets.length < COMPOSITE_GROUP_SIZE) { + break; + } + } + + return compositeGroupBuckets; }; const createConditionsMessage = (criteria: LogDocumentCountAlertParams['criteria']) => { diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts index ed7e82fe29e4c..43c298019b632 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts @@ -28,6 +28,13 @@ const conditionsActionVariableDescription = i18n.translate( } ); +const groupByActionVariableDescription = i18n.translate( + 'xpack.infra.logs.alerting.threshold.groupByActionVariableDescription', + { + defaultMessage: 'The name of the group responsible for triggering the alert', + } +); + const countSchema = schema.object({ value: schema.number(), comparator: schema.oneOf([ @@ -75,6 +82,7 @@ export async function registerLogThresholdAlertType( criteria: schema.arrayOf(criteriaSchema), timeUnit: schema.string(), timeSize: schema.number(), + groupBy: schema.maybe(schema.arrayOf(schema.string())), }), }, defaultActionGroupId: FIRED_ACTIONS.id, @@ -84,6 +92,7 @@ export async function registerLogThresholdAlertType( context: [ { name: 'matchingDocuments', description: documentCountActionVariableDescription }, { name: 'conditions', description: conditionsActionVariableDescription }, + { name: 'group', description: groupByActionVariableDescription }, ], }, producer: 'logs', From 06ee7bd2a3ac28872114db851fa7d6abd7f71e25 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 30 Jun 2020 12:14:21 +0200 Subject: [PATCH 09/23] [ML] Fix license subscription race condition. (#70074) Fixes a race condition where the ML plugin would be mounted before receiving its first license information update and thus redirecting to a fallback page (Kibana Home, Space-Chooser or Data Visualizer page depending on the setup). --- x-pack/plugins/ml/public/application/app.tsx | 6 +- .../application/license/check_license.tsx | 8 ++- .../license/ml_client_license.test.ts | 59 +++++++++++++++++++ 3 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/license/ml_client_license.test.ts diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index 3df176ff25cb4..9539d530bab04 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -80,11 +80,11 @@ export const renderApp = ( deps.kibanaLegacy.loadFontAwesome(); - const mlLicense = setLicenseCache(deps.licensing); - appMountParams.onAppLeave((actions) => actions.default()); - ReactDOM.render(, appMountParams.element); + const mlLicense = setLicenseCache(deps.licensing, [ + () => ReactDOM.render(, appMountParams.element), + ]); return () => { mlLicense.unsubscribe(); diff --git a/x-pack/plugins/ml/public/application/license/check_license.tsx b/x-pack/plugins/ml/public/application/license/check_license.tsx index 3584ee8fbee4b..583eec7d75414 100644 --- a/x-pack/plugins/ml/public/application/license/check_license.tsx +++ b/x-pack/plugins/ml/public/application/license/check_license.tsx @@ -5,6 +5,7 @@ */ import { LicensingPluginSetup } from '../../../../licensing/public'; +import { MlLicense } from '../../../common/license'; import { MlClientLicense } from './ml_client_license'; let mlLicense: MlClientLicense | null = null; @@ -16,9 +17,12 @@ let mlLicense: MlClientLicense | null = null; * @param {LicensingPluginSetup} licensingSetup * @returns {MlClientLicense} */ -export function setLicenseCache(licensingSetup: LicensingPluginSetup) { +export function setLicenseCache( + licensingSetup: LicensingPluginSetup, + postInitFunctions?: Array<(lic: MlLicense) => void> +) { mlLicense = new MlClientLicense(); - mlLicense.setup(licensingSetup.license$); + mlLicense.setup(licensingSetup.license$, postInitFunctions); return mlLicense; } diff --git a/x-pack/plugins/ml/public/application/license/ml_client_license.test.ts b/x-pack/plugins/ml/public/application/license/ml_client_license.test.ts new file mode 100644 index 0000000000000..b37d7cfaa00aa --- /dev/null +++ b/x-pack/plugins/ml/public/application/license/ml_client_license.test.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Observable, Subject } from 'rxjs'; +import { ILicense } from '../../../../licensing/common/types'; + +import { MlClientLicense } from './ml_client_license'; + +describe('MlClientLicense', () => { + test('should miss the license update when initialized without postInitFunction', () => { + const mlLicense = new MlClientLicense(); + + // upon instantiation the full license doesn't get set + expect(mlLicense.isFullLicense()).toBe(false); + + const license$ = new Subject(); + + mlLicense.setup(license$ as Observable); + + // if the observable wasn't triggered the full license is still not set + expect(mlLicense.isFullLicense()).toBe(false); + + license$.next({ + check: () => ({ state: 'valid' }), + getFeature: () => ({ isEnabled: true }), + status: 'valid', + }); + + // once the observable triggered the license should be set + expect(mlLicense.isFullLicense()).toBe(true); + }); + + test('should not miss the license update when initialized with postInitFunction', (done) => { + const mlLicense = new MlClientLicense(); + + // upon instantiation the full license doesn't get set + expect(mlLicense.isFullLicense()).toBe(false); + + const license$ = new Subject(); + + mlLicense.setup(license$ as Observable, [ + (license) => { + // when passed in via postInitFunction callback, the license should be valid + // even if the license$ observable gets triggered after this setup. + expect(license.isFullLicense()).toBe(true); + done(); + }, + ]); + + license$.next({ + check: () => ({ state: 'valid' }), + getFeature: () => ({ isEnabled: true }), + status: 'valid', + }); + }); +}); From 351629f8e9f009e4e134d76c6b730f40d42c0f86 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 30 Jun 2020 13:04:21 +0200 Subject: [PATCH 10/23] updates wording in Cases connectors (#70298) --- .../public/common/lib/connectors/jira/translations.ts | 2 +- .../public/common/lib/connectors/servicenow/translations.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/translations.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/translations.ts index bcb2c49a0de74..d7abf77a58d4c 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/translations.ts +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/translations.ts @@ -11,7 +11,7 @@ export * from '../translations'; export const JIRA_DESC = i18n.translate( 'xpack.securitySolution.case.connectors.jira.selectMessageText', { - defaultMessage: 'Push or update SIEM case data to a new issue in Jira', + defaultMessage: 'Push or update Security case data to a new issue in Jira', } ); diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/translations.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/translations.ts index 0f06a4259e070..b3e58dcd5b6be 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/translations.ts +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/servicenow/translations.ts @@ -11,7 +11,7 @@ export * from '../translations'; export const SERVICENOW_DESC = i18n.translate( 'xpack.securitySolution.case.connectors.servicenow.selectMessageText', { - defaultMessage: 'Push or update SIEM case data to a new incident in ServiceNow', + defaultMessage: 'Push or update Security case data to a new incident in ServiceNow', } ); From 7c352c0702d35f3e68451936dfc7c679dd67e8e1 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Tue, 30 Jun 2020 12:38:12 +0100 Subject: [PATCH 11/23] [Dashboard] Add visualization by value to dashboard (#69898) * Plugging in DashboardStart dependency * Create embeddable by reference and navigate back to dashboard * Trying to feature flag the new flow * Feature flagging new visualize flow * Removing unnecessary console statement * Fixing typescript errors * Adding a functional test for new functionality * Adding a functional test for new functionality * Fixing test name * Changing test name * Moving functional test to a separate folder * Trying to fix the config file * Adding an index file * Remove falsly included file * Adding aggs and params to vis input * Serializing vis before passing it as an input * Incorporating new state transfer logic * Remove dashboardStart as a dependency * Trying to get the test to run * Remove unused import * Readding spaces * Fixing type errors * Incorporating new changes --- scripts/functional_tests.js | 1 + .../application/dashboard_app_controller.tsx | 13 +- .../visualize_embeddable_factory.tsx | 21 +- src/plugins/visualize/config.ts | 26 + .../visualize/public/application/types.ts | 2 + .../application/utils/get_top_nav_config.tsx | 22 +- src/plugins/visualize/public/plugin.ts | 5 + src/plugins/visualize/server/index.ts | 11 +- tasks/function_test_groups.js | 2 + .../services/dashboard/visualizations.ts | 26 + test/new_visualize_flow/config.js | 157 ++++++ .../new_visualize_flow/dashboard_embedding.js | 83 +++ .../fixtures/es_archiver/kibana/data.json.gz | Bin 0 -> 20860 bytes .../fixtures/es_archiver/kibana/mappings.json | 490 ++++++++++++++++++ test/new_visualize_flow/index.ts | 27 + 15 files changed, 870 insertions(+), 16 deletions(-) create mode 100644 src/plugins/visualize/config.ts create mode 100644 test/new_visualize_flow/config.js create mode 100644 test/new_visualize_flow/dashboard_embedding.js create mode 100644 test/new_visualize_flow/fixtures/es_archiver/kibana/data.json.gz create mode 100644 test/new_visualize_flow/fixtures/es_archiver/kibana/mappings.json create mode 100644 test/new_visualize_flow/index.ts diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index fc88f2657018f..3fdab481dc750 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -22,6 +22,7 @@ const alwaysImportedTests = [ require.resolve('../test/functional/config.js'), require.resolve('../test/plugin_functional/config.js'), require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'), + require.resolve('../test/new_visualize_flow/config.js'), ]; // eslint-disable-next-line no-restricted-syntax const onlyNotInCoverageTests = [ diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index b52bf5bf02b7b..58477d28f9081 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -58,7 +58,6 @@ import { isErrorEmbeddable, openAddPanelFlyout, ViewMode, - SavedObjectEmbeddableInput, ContainerOutput, EmbeddableInput, } from '../../../embeddable/public'; @@ -432,14 +431,16 @@ export class DashboardAppController { .getIncomingEmbeddablePackage(); if (incomingState) { if ('id' in incomingState) { - container.addNewEmbeddable(incomingState.type, { + container.addNewEmbeddable(incomingState.type, { savedObjectId: incomingState.id, }); } else if ('input' in incomingState) { - container.addNewEmbeddable( - incomingState.type, - incomingState.input - ); + const input = incomingState.input; + delete input.id; + const explicitInput = { + savedVis: input, + }; + container.addNewEmbeddable(incomingState.type, explicitInput); } } } diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx index eb4b66401820f..b81ff5c166183 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx @@ -30,7 +30,7 @@ import { import { DisabledLabEmbeddable } from './disabled_lab_embeddable'; import { VisualizeEmbeddable, VisualizeInput, VisualizeOutput } from './visualize_embeddable'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; -import { Vis } from '../vis'; +import { SerializedVis, Vis } from '../vis'; import { getCapabilities, getTypes, @@ -124,13 +124,20 @@ export class VisualizeEmbeddableFactory } } - public async create() { + public async create(input: VisualizeInput & { savedVis?: SerializedVis }, parent?: IContainer) { // TODO: This is a bit of a hack to preserve the original functionality. Ideally we will clean this up // to allow for in place creation of visualizations without having to navigate away to a new URL. - showNewVisModal({ - originatingApp: await this.getCurrentAppId(), - outsideVisualizeApp: true, - }); - return undefined; + if (input.savedVis) { + const visState = input.savedVis; + const vis = new Vis(visState.type, visState); + await vis.setState(visState); + return createVisEmbeddableFromObject(this.deps)(vis, input, parent); + } else { + showNewVisModal({ + originatingApp: await this.getCurrentAppId(), + outsideVisualizeApp: true, + }); + return undefined; + } } } diff --git a/src/plugins/visualize/config.ts b/src/plugins/visualize/config.ts new file mode 100644 index 0000000000000..ee79a37717f26 --- /dev/null +++ b/src/plugins/visualize/config.ts @@ -0,0 +1,26 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + showNewVisualizeFlow: schema.boolean({ defaultValue: false }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts index 20d55d1110f62..a6adaf1f3c62b 100644 --- a/src/plugins/visualize/public/application/types.ts +++ b/src/plugins/visualize/public/application/types.ts @@ -44,6 +44,7 @@ import { SharePluginStart } from 'src/plugins/share/public'; import { SavedObjectsStart, SavedObject } from 'src/plugins/saved_objects/public'; import { EmbeddableStart } from 'src/plugins/embeddable/public'; import { KibanaLegacyStart } from 'src/plugins/kibana_legacy/public'; +import { ConfigSchema } from '../../config'; export type PureVisState = SavedVisState; @@ -110,6 +111,7 @@ export interface VisualizeServices extends CoreStart { createVisEmbeddableFromObject: VisualizationsStart['__LEGACY']['createVisEmbeddableFromObject']; restorePreviousUrl: () => void; scopedHistory: ScopedHistory; + featureFlagConfig: ConfigSchema; } export interface SavedVisInstance { diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index e04177fc619e2..96f64c6478fa9 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -21,6 +21,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { TopNavMenuData } from 'src/plugins/navigation/public'; +import uuid from 'uuid'; import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../visualizations/public'; import { showSaveModal, @@ -33,7 +34,6 @@ import { unhashUrl } from '../../../../kibana_utils/public'; import { SavedVisInstance, VisualizeServices, VisualizeAppStateContainer } from '../types'; import { VisualizeConstants } from '../visualize_constants'; import { getEditBreadcrumbs } from './breadcrumbs'; - interface TopNavConfigParams { hasUnsavedChanges: boolean; setHasUnsavedChanges: (value: boolean) => void; @@ -66,6 +66,7 @@ export const getTopNavConfig = ( toastNotifications, visualizeCapabilities, i18n: { Context: I18nContext }, + featureFlagConfig, }: VisualizeServices ) => { /** @@ -234,6 +235,19 @@ export const getTopNavConfig = ( return response; }; + const createVisReference = () => { + if (!originatingApp) { + return; + } + const input = { + ...vis.serialize(), + id: uuid.v4(), + }; + embeddable.getStateTransfer().navigateToWithEmbeddablePackage(originatingApp, { + state: { input, type: VISUALIZE_EMBEDDABLE_TYPE }, + }); + }; + const saveModal = ( ); - showSaveModal(saveModal, I18nContext); + if (originatingApp === 'dashboards' && featureFlagConfig.showNewVisualizeFlow) { + createVisReference(); + } else { + showSaveModal(saveModal, I18nContext); + } }, }, ] diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 5be560f7fb632..fd9a67599414f 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -60,6 +60,10 @@ export interface VisualizePluginSetupDependencies { data: DataPublicPluginSetup; } +export interface FeatureFlagConfig { + showNewVisualizeFlow: boolean; +} + export class VisualizePlugin implements Plugin { @@ -165,6 +169,7 @@ export class VisualizePlugin savedObjectsPublic: pluginsStart.savedObjects, scopedHistory: params.history, restorePreviousUrl, + featureFlagConfig: this.initializerContext.config.get(), }; params.element.classList.add('visAppWrapper'); diff --git a/src/plugins/visualize/server/index.ts b/src/plugins/visualize/server/index.ts index 5cebef71d8d22..6da0a513b1475 100644 --- a/src/plugins/visualize/server/index.ts +++ b/src/plugins/visualize/server/index.ts @@ -17,8 +17,17 @@ * under the License. */ -import { PluginInitializerContext } from 'kibana/server'; +import { PluginInitializerContext, PluginConfigDescriptor } from 'kibana/server'; import { VisualizeServerPlugin } from './plugin'; +import { ConfigSchema, configSchema } from '../config'; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + showNewVisualizeFlow: true, + }, + schema: configSchema, +}; + export const plugin = (initContext: PluginInitializerContext) => new VisualizeServerPlugin(initContext); diff --git a/tasks/function_test_groups.js b/tasks/function_test_groups.js index 799b9e9eb8194..d60f3ae53eecc 100644 --- a/tasks/function_test_groups.js +++ b/tasks/function_test_groups.js @@ -41,6 +41,8 @@ const getDefaultArgs = (tag) => { // '--config', 'test/functional/config.firefox.js', '--bail', '--debug', + '--config', + 'test/new_visualize_flow/config.js', ]; }; diff --git a/test/functional/services/dashboard/visualizations.ts b/test/functional/services/dashboard/visualizations.ts index 10747658d8c9b..a5c16010d3eba 100644 --- a/test/functional/services/dashboard/visualizations.ts +++ b/test/functional/services/dashboard/visualizations.ts @@ -139,5 +139,31 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }: F redirectToOrigin: true, }); } + + async createAndEmbedMetric(name: string) { + log.debug(`createAndEmbedMetric(${name})`); + const inViewMode = await PageObjects.dashboard.getIsInViewMode(); + if (inViewMode) { + await PageObjects.dashboard.switchToEditMode(); + } + await this.ensureNewVisualizationDialogIsShowing(); + await PageObjects.visualize.clickMetric(); + await find.clickByCssSelector('li.euiListGroupItem:nth-of-type(2)'); + await testSubjects.exists('visualizeSaveButton'); + await testSubjects.click('visualizeSaveButton'); + } + + async createAndEmbedMarkdown({ name, markdown }: { name: string; markdown: string }) { + log.debug(`createAndEmbedMarkdown(${markdown})`); + const inViewMode = await PageObjects.dashboard.getIsInViewMode(); + if (inViewMode) { + await PageObjects.dashboard.switchToEditMode(); + } + await this.ensureNewVisualizationDialogIsShowing(); + await PageObjects.visualize.clickMarkdownWidget(); + await PageObjects.visEditor.setMarkdownTxt(markdown); + await PageObjects.visEditor.clickGo(); + await testSubjects.click('visualizeSaveButton'); + } })(); } diff --git a/test/new_visualize_flow/config.js b/test/new_visualize_flow/config.js new file mode 100644 index 0000000000000..a6440d16481d5 --- /dev/null +++ b/test/new_visualize_flow/config.js @@ -0,0 +1,157 @@ +/* + * 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 { pageObjects } from '../functional/page_objects'; +import { services } from '../functional/services'; + +export default async function ({ readConfigFile }) { + const commonConfig = await readConfigFile(require.resolve('../functional/config.js')); + + return { + testFiles: [require.resolve('./dashboard_embedding')], + pageObjects, + services, + servers: commonConfig.get('servers'), + + esTestCluster: commonConfig.get('esTestCluster'), + + kbnTestServer: { + ...commonConfig.get('kbnTestServer'), + serverArgs: [ + ...commonConfig.get('kbnTestServer.serverArgs'), + '--oss', + '--telemetry.optIn=false', + '--visualize.showNewVisualizeFlow=true', + ], + }, + + uiSettings: { + defaults: { + 'accessibility:disableAnimations': true, + 'dateFormat:tz': 'UTC', + }, + }, + + apps: { + kibana: { + pathname: '/app/kibana', + }, + status_page: { + pathname: '/status', + }, + discover: { + pathname: '/app/discover', + hash: '/', + }, + context: { + pathname: '/app/discover', + hash: '/context', + }, + visualize: { + pathname: '/app/visualize', + hash: '/', + }, + dashboard: { + pathname: '/app/dashboards', + hash: '/list', + }, + management: { + pathname: '/app/management', + }, + console: { + pathname: '/app/dev_tools', + hash: '/console', + }, + home: { + pathname: '/app/home', + hash: '/', + }, + }, + junit: { + reportName: 'Chrome UI Functional Tests', + }, + browser: { + type: 'chrome', + }, + + security: { + roles: { + test_logstash_reader: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['logstash*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + //for sample data - can remove but not add sample data + kibana_sample_admin: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['kibana_sample*'], + privileges: ['read', 'view_index_metadata', 'manage', 'create_index', 'index'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + long_window_logstash: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['long-window-logstash-*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + + animals: { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['animals-*'], + privileges: ['read', 'view_index_metadata'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [], + }, + }, + defaultRoles: ['kibana_admin'], + }, + }; +} diff --git a/test/new_visualize_flow/dashboard_embedding.js b/test/new_visualize_flow/dashboard_embedding.js new file mode 100644 index 0000000000000..b1a6bd14547fb --- /dev/null +++ b/test/new_visualize_flow/dashboard_embedding.js @@ -0,0 +1,83 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; + +/** + * This tests both that one of each visualization can be added to a dashboard (as opposed to opening an existing + * dashboard with the visualizations already on it), as well as conducts a rough type of snapshot testing by checking + * for various ui components. The downside is these tests are a bit fragile to css changes (though not as fragile as + * actual screenshot snapshot regression testing), and can be difficult to diagnose failures (which visualization + * broke?). The upside is that this offers very good coverage with a minimal time investment. + */ + +export default function ({ getService, getPageObjects }) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const dashboardExpect = getService('dashboardExpect'); + const testSubjects = getService('testSubjects'); + const dashboardVisualizations = getService('dashboardVisualizations'); + const PageObjects = getPageObjects([ + 'common', + 'dashboard', + 'header', + 'visualize', + 'discover', + 'timePicker', + ]); + + describe('Dashboard Embedding', function describeIndexTests() { + before(async () => { + await esArchiver.load('kibana'); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.clickNewDashboard(); + }); + + it('adding a metric visualization', async function () { + const originalPanelCount = await PageObjects.dashboard.getPanelCount(); + expect(originalPanelCount).to.eql(0); + await testSubjects.exists('addVisualizationButton'); + await testSubjects.click('addVisualizationButton'); + await dashboardVisualizations.createAndEmbedMetric('Embedding Vis Test'); + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardExpect.metricValuesExist(['0']); + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(1); + }); + + it('adding a markdown', async function () { + const originalPanelCount = await PageObjects.dashboard.getPanelCount(); + expect(originalPanelCount).to.eql(1); + await testSubjects.exists('dashboardAddNewPanelButton'); + await testSubjects.click('dashboardAddNewPanelButton'); + await dashboardVisualizations.createAndEmbedMarkdown({ + name: 'Embedding Markdown Test', + markdown: 'Nice to meet you, markdown is my name', + }); + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardExpect.markdownWithValuesExists(['Nice to meet you, markdown is my name']); + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql(2); + }); + }); +} diff --git a/test/new_visualize_flow/fixtures/es_archiver/kibana/data.json.gz b/test/new_visualize_flow/fixtures/es_archiver/kibana/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..ae78761fef0d3415c8ec05ea4bfa9dcca070981a GIT binary patch literal 20860 zcmZs?18i^27cN}e?x}6twoh%hr?zd|w(a&;+qP|+r@iO>Ctq@J^6jiW$?Tb(%)*|m z{XDY>qM(5OyFfs%yDu|0M_LF!`~<|rB8+K73LSdqQeVd$v)zf;zlq6qB1#6SfW%Oc zq3l5o+BP=$>X?2`e>{R7xlegVHC_5s_FDSM%gy{O*&<|?D_bmet1N5cenRJxu3Uw3 zqvucsUUK2iLacD<&r{;=z+%jm>$LX6(VEm|G6?eF7CK8>ce2H(i z;2%~|nTenycoPRideInC<D#~P?Nbv-J%95$Q1Extgeph31%fglq&=4N^S4JA8N?2H z`U!p5@Y-ODUc<~0K3t~RN;2k{GHf>oU+cd2q2?9qq0lnR=TfA&f z7aQf1z5MAU*cEnK)RZ*zzz?;{i*7WLg0Rced8|; zLH4fZeQ3qqI4-}C!l0H zj6cDBH;JQ)k3AyEMkd$rJ>Fl0`gXH>^L@R`e?za(#hD4Vh|CK56L2NZYHVbi3eqW+;YL%AmVX^AtqW=`4Jh3OP2ra}w~a>i zGoj8kG|oeqTz#w8S#}8xwR_7oq6#(Q3?uA3D~P=fLxn<@XW+_Ya;G_5&A`#i6jA?r z4wntUy7LGeOol-gkZiy~>2-t-1D4G`?%@CFR+R4sfX8 zmcB2#Epv6`zebImH>Z;a{Lsymjlh2?xolv0E5f}2QR0r@y{dh|ulb|Lt~rX@_0s|~ zk^e!+@t&QZ%?pV#xk1@2Tf72Kf^Y8a{Mx5(Sr1`SR^SO7tzFtze)U)21LKeMcT=4T zG?10#{^Sqjg0h}7u{3B93%8?M@ynLIU>^km<=RJ}uzi-NzdTy0NCbC!&_z+!{5ev^ zEjWz=8%HQyK&M(6W0Yu&`F^^957*N4U2wmz3zpM&Pm|00nC4)zc{D$0Vn;VEV-G=~ z2O|SOH)0>?bW~CASc9sYGM1UwP4BS>H0P=Eu7ULuWdTkyJO2P6z5<6g3_B1O$RvOC zv9Vo4OcC?S(J@N!FborH>``GJ^V5Ivtl>_K`Bzj+R~EP!IhCjX;XX;8V_=sUE@-D5 zkSK$?oG2++lxEu>o)nWrDia}C4l2aKzweNALPWCAjZ`n3s4S&~94x$ol;QZThBv+; z=|Z}g$dk~RdOn$1nlsikm>^KABR$HJ9h){Mm*~m3ycmVn(~HJ08jH4sqwM5_Cnis* za${GtMzWj;H+UB8g}d5ldW(YTp|f=l$qGAnJAcdnIc@j-X*d7#`tz0dule3R`}u{C z_qXlmWwJBQP2cys_xtqcFYxXUtX! zW7NEkmURv@@nCKHF36U;)Jo@VU+H0R{V|Z68ErfvN8Vj%QHMi6z58w;142rhB=dRB zghd|kt8$CT(bFKlnJvEqMvqP&fWY4(@|NuS)=YP1JXam0aEZkK+(&p5yro0j?i0Od z8!YAqze}1FWT>QoA-Cc(ekO^R(P+Wh=&B?UH^KV(tX`bktyvleXTSbzyHYDBqDrUG zbJuTt2zq_>-e=&TTUOLwOJ}8#qw}`ca3AmueHTzDUAd9KZ;$SEaE8h6ZtZR04dX5N zEW{y$O|G;we$<$JfA-e*1@LW{ z(A8;X$I)gto&jV2@E}M4l2VYRk7j@}ngvZ^PA>b63Wk`qSL}FQA7J!_^W^j55s3jk z-l*-)n>jGa!NgY(U~9%HBJ|>cH4yX02NU#qglfRJ9dFV>A|~Rzyt)MMO`WG4X4 zZci%G#%&0y&4IVsQ{R7R6ogc^PSFP$O0sUo;l0<956278kH8`x3}m@)E;vEjW>VEv zwRt$d6UwN#q7muQPb6V-xJ^3@&tyUIE*Oy~S)6wZCVbnDzb^k6l48MMmS{kX_Zjmc z)!LAwzSpDo%3$!yaBJ;KU6e^JylFzyOg*?j4LPSXZ`$GhAh%AF^00fa<$ctT`d7P5Mny1=$~Zhbp&bC?WX2=Rlbw53eqth8XO2*$mXmrJR(`s|27Euz`vKF#^@>FJKBlPWd4$;5c~S<0$mfb|D#B0r`{4sZV|XGP=bdA=NufZo{5L zv_-9J+F>^#-CQSMxiLny!W(ECAz!&QM!g(uj)+UFVFiCVtyJY_jIu~@P$|Kdper6D zS;wuI@M%o%$)KaL+1T8eQebBeNYUdByx|KB3)J@j!=Lu)4yWhK{5&b*OOPm{BoRx3 zyX4;-v3mRi_EgW8RT@rQ%7C;FLJUPHs1A$+@KFqi|FEu+!ZJfH?ZAk0JQqOt0{CQ_ zNg+8r&PAmuktZwNe5lYu&Zw#kCc8r9%A;DUtkjW%eX98T;duF#jZtBOL%3+P>s`Te z(n=ZNW&eS;WLw9{CeGCfqv-8o5scn*39rOd073~XqCguo(A9x-rU33BH>613#G$>R zS1oYt$Ayf3f-4V>H+KBaeGCs!j>6)7^VS+x`t>FK5kw3lx67DJws&O20TpmA&)Kxw zl7<~#X027C@WMKhrG*CP_=q1h@O>-99_OSW1Kx7vtC^d!HN~prsU< z0U0`&P!oiF0C+&MqAp;E!ry2uL+}}V@>y1OUTs5Y5NdtWmSE%k^5Hwsb}By+YMzq( zFn-zqm1gc$`fZ}iMm>9%@p52EszCOs{^$|+HOqQQ&MFo%S2 zEXT#SZDo%ebmP36#?de!56)evzTRmsv zj1_XzY(>L%+O=^McW$Foz1d1_s+cXlyr?+e@js*t)SHLTF+<|w5wfGvrE-z`{lriC zOpPnC?{M&|$Mh+9^tFDIZSy>Mg&o#@^WpqGv)@5>SBI?e(CbvfYP~f&$C!B|*4a(G zl9=mb+?W}jGgjDbP-$xX{oFwW9MnXj5r-#9x0O^TNkQS|pBjXn!ng_?IghB<`NP*a z*Bn;nTz2V+InWc+vl%&3xR=XrUiUnsS1 z3s1uit{RHsyeHvIl|61qm8oi;#?8A!3q8%YeM_li>NY5_UeFY%CqxOG4>3)nn`^R~&1eFR08U6pPZJ}gH%_F3r{T;Vr`ehqXxNHki_?+4 z%j;j{PAdnwG?g;Q9(VXyVj8PS^<7%A9+pkr(4$I{ zHsh(5mgW+cn(Qevf1N|FCFW&oAcN}$9Fl=z=x}a^cz6bAVs-f1nlo-LVx2i80CZFo z>gRHtu_A7~^Ro&2=$n<;*RGgwb_-8um4{FMcdGx~?JmX=AwCm-SGm*C2?4b#JI>u5 zY1ij~MzXWYd*Er_&NFdiVh*P<)u>$2;v_uT4Kd2}$0qR4tM&U;x=u^Y235;+*_@lH zqZesaVnuHD-M{UUAwRF5O-0HH*WpU>InN0Y)mr=ccIp<_@%~BZfGr-!3_JHk-A~%lk78$ zI*a2(nOEiz?6m zwi1as9`2J(^O>ldNL|=v=SIU=7!8p|^d`u_9+*)kD7%VU`mED7kfO89#dnZszTa}= zOIM4VcmF>3gRXaWrg1L%4lQ|!n@srch80k^8kkX{;#%}RENszM_)UXhGRd%EGIomT zTFiX|six~s)rR#V$q_?U0|d&?Mpyc!uwLy%J6K-M@dp2g;YT^z8#TLk{%5dD^ugCk zdiUOiA&-zY3M)(g2-+JZT%w5)dijXBKf@eH)Aor4L^Xb4K;7+;k*0kYC+MJE+RAwr zidh|3cmYT&EVG8a-ItyVUV3r0N;fvo^mMPow*hyAi<})k&~24s>kC)xXl%gUoYAN+ z-*(W3E})RdUb6!AtAE7O{L?3e-HGO3d|a{Z;b<- zqq+9FJLHfK9wkkOu^m z3?u@4_tUgZ|CDO-ABlXyq?pRT=MrajrYpPMG_=v32p3AmpkE{xj4swXw`y@8UaX)^ zFs2+#m70-M6aP|XPGDFvqH0nX-VDNMM^p$0oKlN>{02^!Wm(+n2J*}2@;EI>H=B;l z=V6C7w1FB-8{L}ujp{({Dv!$Mo7@ncN8$EPfAy+BoFgL#^S33DJ>|?0vr928 z95!PafxWN|*&E~KWxlVQBK5z`y0b6m9j~BHTvrsS&nfFxRe1Lun`fMA#o{1t@!2JnClau6K6fwHzib6b%; zaiD1gVAc)b=2k+*T~UK7k+&(=w<{Z{EvWo$B7Zd={}~Lh%q*xYXQ;ewggj!q!98&( z^bQfE95zDRMdB6mUo^TG$fhyGO0AZO(~Pc_GAop*rdjL`>?i;km%{3KiV3xlM(h!K zw#Y{D63R_y7`mQj;1T+kf882N?dJ&$!pt2G&(`o z3d3YJYVq|_V>BC*hR!ACiC=6*wyG>y0VRw=%H(VmVIv0ZW5D85?0nUgmCZ@_wqZ9;sz()uc9=;rr&xm9uuKzH35qEW`Q zs8)&nLe2Ehn$$IILR`1Vdg*$?j3r)|8pkE3QK^jzM|EA&+Fbd}YnHOjk^m+26etoa zqDz8Qj|Wb&3_D6;jFjm7>XVXW+{L&KKgJ&7oOq~>weaQ8{h%WNe^CBtFn+A<`+@Em1&ST=Uim>Tq4IbbFMJ*7~0<7N1GjQReHpuj7tKFiK?lt0>dUYyIl|l>o_I8^Pt3)C9HQOY-Bv zrt8CD#-%dr|FW>^j@NV+T^RjUB_@ZZL8ir$Co+ItC%~1HQ8#ioqJ}clH;!wu zS90lVRSmFl9&T`Kq}-U}U|npWs4eAr!$n}5xDjO`CpY#pXLu3VDH!RO0zSck@k`j1 za`OCf4r5-QVw2S>6n9<9#hv&Xo8b+=a}X{4n$zfkTtHm-x)b$<1{!vjFlmMxyfBO$ zrtPQNU3kyEgU?Y$k+B$1P!11rdIH-=q#31|;LATak*<2>g4$pQc8^NAu5ilsVEJwU z@{*Waix}T6IpD?TD-$yw`Dz|By3f=skz z;ES0fCJYB14qUAE3nn3Okj1~`yt3`)2uHH!M9SK|I#|+-_r;NztbTa9n4iiPThmdf ziYr~$$i~!iGJ38jvC`{83mb05GxUrvvPk|@#+x{z*5=BUv@)Dfli7%MT`>H@OmIP6 zQThw(pts0eph?}GsNEvATN4Qx`b?RpZ@fvP8cBh53O@m|@d*|x-)+oPxMpI!x{h`C zTrWHE`_?J3R905COabOm0ZCj|u~m2iT~jX2o=dFEFodaWF}RY*fz6MVXsvA0eRpJM z*Xac%jBk^$GAEuF!Q@@&3ZpNQWRy$?JGYci7!|zCk8`4_Vu|P>Bi-BGeWX7^aZ$Lw zVQ$X{nBqA-LK(b_*yshgte)nUtzcBgd4;G_j~7@HH#S!h&ZJOrCo5lxKpKe2{)60K z8q{pDcm*EcRhJK zNdqo>N6D&b$B&S5fY5W7WPh;l9H-0bWXW*Q^tU$=bilBG(1A2`%d$rR-aC zvL0DfGCdWuL)L<9`r(8>>DHpv0o}{kb6_PgmEXcIQttDh*XV}+vR?5kW@h}1TVx#@ zYu8_ShaminQ)tkx>}@vz$_2)c>ty%xV+GOpJ9@wj{TVF^RCmfbWeWlTwmRA$x}_tp z63m6>pY3CW2t~Jon_iEN8gqC`<*9~_%cCd z4)lzua84Y%pm$FhR&JLrjk!j`r=@*z_XID~jiSt#EHgQEIUhL})+~Yg%l{m>MNN9e zTOw}ji!c_DDnX*vG#+w~W?R~{Pmp_+L0Rk3a~46C!9yUZ$A_eCXWItaup+5Mw%n!Q zqZRv;VEC39(gMjpNQXkgxOJX3xY;exr8`$8Eb>1JMV>&5&r_=o6FzMXJK&?lyUcg} zyt7}PqMe7nwMnKX+r>QoktrX!kyUD*3_X2eg&kj#J>Eo8z2CQ+(;xD0zr5)({Q12{DWuB5?++Kk7{MVApz4%1AQKbUS*{(QdT4(;Xw zVn77eO;bx*F^k5f6A*B>2hhclsi4%gObWVm)s#mSbtsXWF#r^)x`xcv9tlz@o7PYH-f{-cZ5ST51J3xL|r?ygt}!J^_bu2&4g!<-etB zGzUENKbA~xJ*H9_OR!_pJ^2mqzhFV67LJ46^0&|d=3x@Lx!`4+jcn6@##8J&lTNA~-isXZ++tuOJ@ z&ztYU&NWdr`Eb+eL+a>w8gDxwX-ec_{jL%=KHowFUNnk!s_sUmJ-=;#*JzVy;-9yg zjA@xyth`bxIZ3tnW~v;o;0$AppGYc`iM4npR(jijTraC$ct>0SJDUBZ^f@^fA$fv+ z6Td~eM|LsleT+&cb+-o{F=+)52m*FEsd2@@B{)#{ae8)u%t+KW>t_krW7Tz9E zdeJ%3E|wqT`jzW6YOIWDdM8nspWyV&2o$)m32jIG*1|zdp9j#Fyizu}CM=YtEo6Ep zEO<$tiwWXO^Pj3h(X+w+lMm9pS4wRU*%jb~?7!>js+ ztf2BfFE7`d6iu|+(Y3Xm->1!Gf2k+?G_W&bDPb`Jd$>h1V9^Vk3 zfwti9zTFQJpUWzmFSZYcC2Ik6JfzB85X6mc6oN4S9Ov}JBknu*LBAu8IhytWy+@qp z^}1RN{h#l)$S*P+a|gJiQ%-1~r$9@JyO4JIc4TZJwtwV_IHb19(l+Fd}gor!hE3?2!HZul-+8h>MBT|27)In_87qRX^qN{>B}5klo%R zIZ07%*D)FqMop8Y243ksr4|^a&0*v+D?z{5RdwI<8`=YbP1^6#Fjd3WKmW$2KyHF? ztnC`_0I)j2@C4t6p`Xa`qZTlEj{UVHvW}$fSAsbRSmzDFxj9H?WS7sQI5LI%Iytp^ zUFSV^yI|J=K}0_&<*GW529uvWJO6+o(5q7C&)73f*Y){&zPUJreCzH`0&2!PWfr?+ zsYP2+xb;AScA$IY41ODt7HEN@G;m&gN2y6rz7G_&fM7IX)S!|tRoClkKQ2s)j3?Ot zwsIv1hC-Fn!@cJ}AoW3dPo~!u$TjXscu#YS7I^S@7gwxPJ3klh+*!D>t)JhChh#3H z|0DG){j;OetGwxj7fyfnuF$dg0mYEV3Q|^yQAzjbz&|Gfv{V+>j#b0Y3^@c60jpq(h?C8=1kSd(@JYhsG?PsIK425~shB=%(yjCSUB=sVNMxtq|K z*sjchwXxS;VBrZ5Fyvjqy@`}YdkUM%TNjL z(?(85@cbh}(Qf_`yBtqsFag`yDlK^bMl&bn<&g2M6BG8qEK( zw3wI2velSHc`K8(QbhP`o`#Iqe>&aai$dtG92$N`Z>@E``XNl_Czpzmt9-wJuXz?% zhL9K8*yEKvafK}pa{Vh8TXQPxP|XxwIU->p(N44%I{5=o-`Vn%+Jx(S*gw)RH&L}N zimWNL@R%S67QT1yp3R)xZT6`Y0ul~~0_hq!|J)D}cofDa(R9q*@nsPwv=^x35bE^} z!oGm(pbNBDez9ML8iS&POSJ#xkQJ7qD;KB_j1bw$Mq@+MkH_SJ$Z~V{?0j1;S_5*x z)ZBSlp3Zr}6_UcUm>LOYHVxf!ypuKD9{^iL!uz75X&+In z|7A#MR5NSv5SK2%B-m#n^g^o<9c%_WglsoMDnp%91?LJDrS@u*hCk+$?WD&(YcW%{ zdShReuyS6_;AyQ*D5+lB9sgZ=U^tlBN6U=oDnI_~Afg&U&htWH1XTu;ib!j!Jo(Os zRMm!fL%Fq8UFod!?=&oOn7J%_?x`9qaPgK|(^duT$ihMq;1kL*ruCQDDGp*EK5%s0 zJ*1gXQ;63ar(W}D*_(KZYg?QH*tEeu;yTif;$s0#^H^Tp{T~sM! z;deP%{Fm{5)8k9j(D>z(8V#`s^Z10eRf1H^jYGPZ;yZsVliVzEkFsAxXeF^e9Ia-k zT*E$g<0Z~bs?qbP+^+T6aQcS zkASVNe%tBiX`BFz-XHH+`}s|~SiWObeTM)gg8O@`R%Awm(TDa@WX}XlzA!niU)ETp zxi$M)j_oOVp$z4`Us3NwmRv)!jRnPuk{}p#+;*a%jGL8+fMAW?^*RPM8J4ubhLbfW+|R#9JGu_sAtZyn{Zqi-yHlwH{%pVwk-ae>=|}?am$z2Ppw4m!4W4m&M%r;CO&-@90kHHBKhNwtb4# zPv^7OPG;zkc=DetMUWoO96912F$&T-j;FA0+I9%b+wJ|%ZC7g`nyx^iWw`Ve<9)`k zG6zTP^)9M>Kcy`ycI#TMKlPau!D3PB2<3L$`yL#BTD7mV3_5{>n;!3Lef9hYGr|Zw z%Abur4#}E(PVPiZ*F$W)42T^LzqNZN9+l<&^))gwbu^bbyIJG(_Ywc34E_b%Wt{6F zT8Af^S77bZlN$XE)8HJA~t|O-RV;ni5If_;j z5ohzjiP>u^wKywfpqE7DO9?FXX_-AfS+|4&%@96%|;~0I6O)!_Znd0~xrh$28(@ zsD+*YRhW~8+F`%6@;?9r*Nj{+=z)?7JR8AvHGg9sgL?YwLyBMgch=vMG7FhiGt{sE z;Q$(tu^*k~D-~F^EqV-Ic8W!;)zgzsQrJ00_4n>L- zqfrbcN)Qe252>SciD>|d^^=OiO0o|Et~WI!sRMZwdYI_l=WH-%ES|ImMo8rTNb3xO zQI!@6aaM0j`8XA6=AX01Pu4C$1jduOAGMt^>JLMR6C`sG>@7lo%169+Ivfq0(b!s0 zMdpLma2V4(zz`42ge~;@BZdEn4wZTLH=7ex}V_fDa-9RKl7EbVdIc!FraTq*+$@JH%o55o%bL zQ!vRwyk3CHP~WZpR^Gnf_Z{=gu37qc^6z7 z2hn?AJnJh$KLlu!JD_|in0z6e99#u$? zt!Pb9ttv7jkAeD)1`l{gyX}{{O+66kz^4WwPKL8l3c9-!4ckKg}gr)c1Mw`<-H%6 zJtI0qi1)BWHLeJ6kKY6m2&q3!TO0%e>fTcw4zZzg>3{r(}cUQ zW8|>iGWCm&2=>rIVDsyD&x(twLiiw|BQE5w2~xG}=_m22YZ~#sl5t`)> ze1RxkCV>}N(oUGt?9(PH$Z`kdF}sMpL#;)yGm}A=X^YqGf!jYq-k~(&KH}ce7##ft zrRFcv_I|t#RJhsL^9|WWrB(#enG5L>$=8X8(!)3rVgyE?8B#`TdT&_35J-eFtC-}? zV0T0VvrsR{3sRgY=Pf%3g7{G1g`Ag$9K}v{v&x|Q>+zQ5~;tMe^97PCW5E1`Q+&QfossK~jk*?XSlOw~J%yc)K zp5PvQGy>{!R=Hc52Yj#!YZ*+73AYX{g=tW<;2Et$0g-@7?_=hz)((sh3d>l-Dw;D! zjpRY$)%5>##oi3%`KcO5bYQeG-oF8DG7uCM z*;HAtYv_|%%;Kh0Z}uzAT~9x2L%ytIlAb$YGUa1mhz>T?(ijH^k(PiLpJW4V!dZ7= zoP7zTxvdYT>;2uh9#+|h@ewU7%#p0<-!6^nOvOg-&kC7~_`4oi=psrAyfScN{73K! zNvh&0gxFLrQf3Jr@*0epS>E0vcJ393aDV8|9XaFzyj}So*Cg?e_EdkQVxgbiYIMn7 z{RQr!Eq)6^NX&1X0fCJ$MT-h{q;!#d9+s1_I&8S>&&|v@O;r&$mmU5GUk}Y;>Ujd2 zC>+={m5Qc<8B&ctVI@{zbTOq=0ij| z*g{N-5Lnl^c$zeotV|C(c4*LJP97U|=+LI+YS53eiF((I!s+`|hlbx|>mHHn@m3=X z^YFTEk?KV=tc(tSv}+JxULIQ4#2sE(%r3S-yAs&+3G#IM_Xn-4R62WH83?V9cTivc z+caC*^(hquT)O|R9e*YqH_|IO>-#l-C%L$OD}U7QD0^D(Ml>>0O%&u+$zrgjeW=hXkrsBjqRP*QeRyZ2Au+K@S=gsOGN6;Q=}hUB4WK=k#L1?c2JZ7Bn{^o0O!-qOqZf|_>Aa|c&o__Zpqq)^zZVq) z(CV2(`T}$|e}tfEKP~?zVA<64!6SaLm`&)JZvta5NRpODnn?nvvPuuJB5$xVt>Hpj zi82}svRa2>DX4F`*}%Alrr(4lM{Hag_q7boyGt=b;@lTlhaS-$#-UaSr5sv|zdurc zB{%Q9MA>HyyoD61r+aND_QaJD(DHqu#8{%nFn*qUcTimvh{@qsk>wztT5CWQa7`6f zg(zmw5{M>TkTVQOCdGCSwJ~m-LVJuq$f9A-o>28Fb10#>m-kV~Vf3*FhabwJt)DZN znAtotKDT(1;RaX5_3r$`kAQmH+)(^qZVxJ$NKpCK3EWNo2cm)FJiR_{GTG0hPu!V% z15VS1bUe(YGf82+h*2byu~7uV?W|#8CC^Fi)4;)dw7ma*?oyXNl6}AwVN*_-KRF}f zg>k~l%a|8q_X%2+%Q!H1KO79xQDF1FKgZesuyy{xF!TTh^dFz_nh_ao=lyD$Hyy`x zym5sQ{n_~4SK2wNp;;Uf3Rk}%t`DndAX>l3+5%H+31%ITBF-S<=;n`vhx|y&)uVcJ z|Ey|nJWmn)m@DGPTAsyI%eE|&5lgWdObfMZe* zrTnhhEB}rdT9T1J$OkztT!?msJJkE%$!ZyuTi!y?QqPEsYs1y~!3pg+3N>M`7H4gT zMD&Qrw8t|q1Yz5qPhh_CX|5zaz$N>fx?ste)ZwcdOS|+@?Pda#NOqX$>MDe>+W&j%A+0FuZ-ou;OWqSggeOX2Vcv z<*~t;?#}V*UmN@w{OFdC(8>>Y~XOy}997umMcQzkP9_2!TWn#5urn7SCiu za7TB%M%7JTlsAh-{*T7bGJZ3P)x|iDwU+;tw^pbl;{mATrCL4K`>HP>%xU*n;C2RI z;|Y9`qT8A`@#Hn9m(%x%K5)E*e{b!7K`^33S0Wqy|ssZX;gX%78=;`Ik%}R{oJQAtichEMijl%O2cl{z|C~M3j%Ujhm zq%33eN8rcOp4t3zejGVrXN73}U`k!*U0)GbzzndqD`9RcpXzpSfX z9bvV}ivNm-rtr>VWn|Of(=faX;Hw5GdUNa$E_Sp|)6rW6kDZY2n`EY@B1Hm8v|vJS z)IJWL9UGuP6Z4(@y_tnp73L)p+Idq-Nk0#$JYV2+{XEiofiOe$i{d<7o#G{Xz4aJ8 zU*35l%%wsx1zr!&Px~0UcBCfkc>gvch2mCV0g+k&u#I0`h;5-%XBc$jM*v$iae+yZ zN6SJQu~rbTA|2>fz<=JHfHiNmjyO6Qb17xSCZ*xqFi1E~RdP?eKK^9Jh;^^Bwfg;F zhPL78RJpw1Z$H%kR~*t!s0CKesb_LIHU5J z5m|B-GKz_fr-w2i3gxT@rJic~-A$YYutjoA;rwoTgp#;v#+WX4OWC2aA?@c5mZk$$1OZXt*HjRfevwJTElFMc z4fF^_azrcOZaz~?s!f#~A&SJXVD4~g2;X+3DGur#r zw5~;&O~2Ero><#t67DF;>h-DrV~9g%c#Xe~XHtUPQw1NL;+ogASq5RBjf>oL(Uo<1 z%k^>Z$%ZgE?q`6Va;r@H+Vl%LzC381P5H=Ff7x$}$&&NWyip zAV}7hq?bL7109@;&Hb=ylNBPBD=|Qyo{OR~k(XTkBhhT?%w1c4Avg)u)aEvddJQl=~mUblcN(e~)Od4=+F_lN(5! zKy`hS$*+$k7eP19wR+=NU783Q|H>}z>e+EFlG?u9%Pwlo&3wozM-j1%5(KZKh@RQi z&Bqh48%f`b(r?rf>dFiL6TEA%r7CX}{w1n%>9DjUMI5zdA?~YYmE9AL?ECrhn-~Li zlGLk7Op-Uam-*i$3td-5+q2EUCsE$nZ(|gzz=LRo@^epsFqOx-2V^o@3%^&K45BkNk8BORb7OLEP5E7>T2Z;g${WnM>?d zIw=QvY{dPv;mk<3+)Q6OSfGRqfs9g!qhxoJ+tRr~?WW`-e~Vr6Zh!^)0bAP9ZpRAa zM^(xc51Hu$$JcA|7L#cpHeRb%qqno8RAxtss#XC#qFbzUdICMAp!Rtg&#li8GO%5- zkS279cI@(&x!drot$Kn_4eqEMd?3^7ZFL!PvxIzY)~G}-HCvjC>DwlD@ipaS`@HG{ z-17%#YQ$_T$TNfqqur{8QH_rL})So44 z7wU%+6?2IaZl)?fP?oD!h029YSrvs>dW3tAa%q{K@`=AH$8DU0;VHp}5?5vc8W9Tp zUdaNAU{o44>E4FilrC6o=_XwQYG{{ zjg|PH&4#55j;0xXEIZNql|=Qh82&QD0=ye}K{pL7w7`{Lo+hd%a@sdgY!;nLVEqQl z(;?^GN?=RYx(cn16H8!H@)+2$}0Pk-)@xN zD*y?a49-IHXz+^MaZX<_jkBEpw0f4NrTTyQQ9n$D7;MAedN02JH&5KfTUvRFecJq$ zm~J#HE0uJ#<%=en+;L9X(@>8_nfCva;GVsk+wDF$^fCU&m%9rUvVvlrsl*?ccpeW& zY0ERaKV1HgQvE^K^L7d8J>kOqL*S5D_@iaPfex>JN6e2<0NMA8fY<6P?h7KYRQV}$ zH{ZwGWwfv%m!YTZ;Up{zM)PQ>Cwm^H!k zK{}lYdV-?hwF2~gV6t+<8p5!tz`q372RV$WHd$A<^hklUw5nVRS0!IZC3C*>UaB2{ z-!ase%i*yn9ZzgS{V4n-QQxKdUqv+jeO(+PeGqM>jh4TR?DHxE`Z-f%)GY*0#sez= zarG#+`V$)}Jsu|G)Q~Kw3d!|2B$8>v(hT7upt4iQN$8rBol@80&tC5bb}nTax#b%s zr|<8Mr`i;hOV|*_G3^CN^{FxR(m$kuJJZn4;+Z7t%$xzQ=;-+D^>NQpusUHpEw@7jv%=kE7zvon%$qB zQ@T^ZD}EccRXE*-@cXfcwM^#_trgP$iW%Z;RHfZz?pRMKIZ)Orx2b4KuRu2wSk~0m zF5cFyI57O%i|elyOwo^lsjJ>DsjIxd#^`spkGqt1y3yJ>9SmX24=!>o__*cDEB2%$ z_YiW5$cM(g1bQOybtWTx<7)aVw{X=(vZJF!n=tV{{l$tQ zRL_TL3E0_2oXsSiXJeBQ*Lpj7TtyejPWUa@1> zrii^-qeexkQLAQ*dWxbp^{9EA^S;+}z2|y=`2G>!&wbsuiAicHZ-{==d^>UY9;w5I zGrbeHbai_4PNiGCYG<3934xrb%kP&V1%IlQp-yW9lU&(Jg3!$MM-=oDH7q6M2#Ely z4dIB1+h=kym{Hk!pok?QT5hOx*KDzWb)^dd!vzgfkOUVJG`!mlr5<3^G4B7`g!*K! z=9=P27!lj+!SZOVY~F{Sa4`%TR0dA-o629ixAo=4A%@M6e`=q}l;K#Qm4yxT>BJ*g-Fx+k znrLxyY-x0e_PoXYv_wip)+}3tZ8r7a6S%FN`-f~-mpPHP3h$s_CRU5-O*l2dk4{}& z0n49t*4Kaljc_;h9v;4Ke%*OFH$isPi9 z!onyc>QZ<=TahV~^`N!CaB@eq8Y!Z)kmVBe#Vv@|~5zQZ;WHl|N=VW%fUlqD5 zzF7Pu6ON-fCJ)vZ^4&37>chv zc;Dit9_aRz3lJUA_Kek0^htA>%~hY)OKyrtv@sSd+tNdzawvh)FY+B7YO85?Cg{4j z_6bSv{?tJ)$F79h2-mtDgPB6I;plTx$sA|GOaHV!9HYFnRIz#(9lyw=UspysgVk}I zdgUR6!C5B~>uW-V_bjtEvOx2V3(!;Hkc8KJ#cmjOBj_rowv`^(&jb6aNU$!vrvG52 zgxhIPGQqJUf%rP=NFmSaxBm9FIXl3iIfCswJ)#tK5=FFVs-z0bO)i|4vTG0H&kLv0 zRu!J7EPcsJDCU^*Z_p8MA;wK)A`wrnK0xb(_a@=0Azth3F%Xa)>^~FU05EC^d2$=U zfA0Lmi}6G#jp01&Wsy!!gmLE0F|rS6EFJpeF*SC}`_OaaS(agAmbe^#SCnWrU8El` zt-nrR2DeCq)f;fRgiHlG9?N7N96rsK9W+i4DuVs-SV>0Q<02b+_dsyv>g_m|rVvFb z@W;$eXDb%JKm$+OH@w??qxIcVoP%FHZcF3jR4TJgq#O#hDVGx%40nsrS=yQm$V@Ul_~%qRr+I!ZSi0ZghlqfK0*^&8Uv%z z%|UG|)ay9Ubw1!!otmNwx0eQ&_bA@i6%|B_X&3fCP$x)L&W-G^t{H0K!KRf-qJ1hGkLYj`rD3S!WMO!P7e0LyQ=0%PMVS=#|jIew_pIWr6OBgn-!hC-lsex^+v9m6s2qNTMhc zQX7|@9Q-pLX(kHvQDRA4!B4Z`?DJ?(7bf=HYQ`J8Ddz1^;T((p<$KP3VX@I|RZVt| z$gB#P0K+=hTV2Iwu}NVt+coXcq{j&`J`9QBj(qMHm#Xw)!^&g3d z$ep4Iy-AKr3^!D9HhiZoL?kHTqT|nL%{NE-jv{kbCjPp|lS1$P6b(-sn&0fmS}fZ# zuVX%q%x9<70(X$0zbVIdS8A#JHnQptBq zUI%{|57X`cTk@zr>6i0==|;pCC_?7!RyIU>qt@m;#(_aVo_&$RIiZ&ERj7h3u}H}N z;Pkla{t5aZV-<1XRO}-Q6T{P*@|XC0ChOU#v)M1@4&`#MAJt{!Jy;F%ul}0-Po?H< z+3zx$dLQu(xzbn4=vn&HwmdDBW<4EDA?^q3e^-e}Zhw9e@GVwzyGHLQ# zvpV&6-97WY`|L-W;mw+uW|R%XN#1{gHM)o48qwN!|LdD$Qbahvj0z0j(*xgN0dJ)f z?A%AFcbBTM6}*rtPZ}Q~F|N6l&H1+1Pefd@LDjnW)|z4Dg>!GB#}^ zA?OuIR=pE$d?vb!RcS zNomeiX3-J0=fpqbqJx&$I2yXl7R*_Ya(as z!zGWaCa$EGGJ+DsWS%=6+WN2+THp5*2qs&_!LRfD4iVA+{`x}Y#)hRy)Q z@rXPD7OpeP{*P^wXfrQ{`N5KhQ$ka&*nAy$0c zff@N1ICJm3Gx3K<8&#!vd6)YH2l@5T!@yqK&KSOMGtqS^#Tw1z#yC?e-%X6$OhB`@ zy;JRNWy02hQfe*nMqt7iwwkgt!RaOBX&iLyW73ATch;9x`UM~9jPj*dUz$5N#?EMa zFG?KzNE*9`TpuLFSIGFrZNGvHw0$eyZ@-wBBzKHO&SP82G+MQOHEnr`)LFSKeF*H> zAn(@3zm#6+yvW$@Dmr+J)b>ag}adcPP3VZ zC-6@4cQ-h4QrIS`0@-T4PqmaLpQ*Vr&)cfo(}kO+`{N$}Z~lVD^tYg&Pw^%QNO@J^Z&;6K8=bK z=v7v`Dc?#u1at-Id`ck}c|UqZupxNQJvn3_!gKL+8Rt4T#40iIv=mOuA*qH?h_Y_s zkJnrUdwdozxM$}9-2^E5HdD`H^aYtB35?sid~jVVoFTE%SMdUpr!lU(^&hAYl~`F{ zd?R3EYC^?y)MFquxZ?>)>wyN=8K1`d(xbYt8am3QcJyDrGe59j%)5T%5r{u$A9tbN z^D}v*oMfm>-!SJE$=A&YKHdYkP<$Wbm5&s(vF4Iect=2lch=zKRPVBp^w1R(|B~xG z+19XREcP+vyj7x5`6BhZ%yZ*8>*!EBn!Fz-bBs*g01h0CY{2Jr%dkC#(=}>0(WK1+ z(z>61WwZXy;dLwK%4EK@%7;J(7_l43B`6W7mAF8G8UBtO0kH@~nWCh3h9M25D$pBci(2&IHp6x!-o4amh;K&r~dddriv~<#QXxwL*YZuXX($!~)^yP(nvH=@lzy7T0HV1y{ODJ%kJB zVq}j?iO9|IPnmk#sRykGBo{PCD}x4;35>EW>%+uw!zo%zD^1n xVnt*8tj47Pg_Yt4t=BfjttKwd>rHR9;pt84k9z6J64u^^8vUdi&1ECN`xoXob{PNw literal 0 HcmV?d00001 diff --git a/test/new_visualize_flow/fixtures/es_archiver/kibana/mappings.json b/test/new_visualize_flow/fixtures/es_archiver/kibana/mappings.json new file mode 100644 index 0000000000000..9f5edaad0fe76 --- /dev/null +++ b/test/new_visualize_flow/fixtures/es_archiver/kibana/mappings.json @@ -0,0 +1,490 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", + "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", + "config": "ae24d22d5986d04124cc6568f771066f", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "181661168bbadd1eff5902361e2a0d5c", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "url": "b675c3be8d76ecf029294d51dc7ec65d", + "visualization": "52d7a13ad68a150c4525b292d23e12cc" + } + }, + "dynamic": "strict", + "properties": { + "application_usage_totals": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + } + } + }, + "application_usage_transactional": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + }, + "timestamp": { + "type": "date" + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "defaultIndex": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "notifications:lifetime:banner": { + "type": "long" + }, + "notifications:lifetime:error": { + "type": "long" + }, + "notifications:lifetime:info": { + "type": "long" + }, + "notifications:lifetime:warning": { + "type": "long" + }, + "xPackMonitoring:showBanner": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "index-pattern": { + "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" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "dashboard": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "search": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "visualization": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchRefName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/test/new_visualize_flow/index.ts b/test/new_visualize_flow/index.ts new file mode 100644 index 0000000000000..e915525155990 --- /dev/null +++ b/test/new_visualize_flow/index.ts @@ -0,0 +1,27 @@ +/* + * 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 '../functional/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ loadTestFile }: FtrProviderContext) { + describe('New Visualize Flow', function () { + this.tags('ciGroup2'); + loadTestFile(require.resolve('./dashboard_embedding')); + }); +} From 6027451687ed5697cab48bc3dff30ca391193fa4 Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Tue, 30 Jun 2020 15:27:39 +0200 Subject: [PATCH 12/23] [code coverage] ingest correct coveredFilePath for mocha (#70215) * [code coverage] ingest correct coveredFilePath for mocha * export variable globally so it is availble in node script --- src/dev/code_coverage/shell_scripts/ingest_coverage.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/dev/code_coverage/shell_scripts/ingest_coverage.sh b/src/dev/code_coverage/shell_scripts/ingest_coverage.sh index b7064a1e42671..2dae75484d68f 100644 --- a/src/dev/code_coverage/shell_scripts/ingest_coverage.sh +++ b/src/dev/code_coverage/shell_scripts/ingest_coverage.sh @@ -17,7 +17,7 @@ export ES_HOST STATIC_SITE_URL_BASE='https://kibana-coverage.elastic.dev' export STATIC_SITE_URL_BASE -for x in jest functional mocha; do +for x in jest functional; do echo "### Ingesting coverage for ${x}" COVERAGE_SUMMARY_FILE=target/kibana-coverage/${x}-combined/coverage-summary.json @@ -25,5 +25,11 @@ for x in jest functional mocha; do node scripts/ingest_coverage.js --verbose --path ${COVERAGE_SUMMARY_FILE} --vcsInfoPath ./VCS_INFO.txt done +# Need to override COVERAGE_INGESTION_KIBANA_ROOT since mocha json file has original intake worker path +COVERAGE_SUMMARY_FILE=target/kibana-coverage/mocha-combined/coverage-summary.json +export COVERAGE_INGESTION_KIBANA_ROOT=/dev/shm/workspace/kibana + +node scripts/ingest_coverage.js --verbose --path ${COVERAGE_SUMMARY_FILE} --vcsInfoPath ./VCS_INFO.txt + echo "### Ingesting Code Coverage - Complete" echo "" From 82fd107fc2df3e38c4c9d5f906826cba3f4de468 Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Tue, 30 Jun 2020 15:55:15 +0200 Subject: [PATCH 13/23] Fix typo in bootstrap command (#69976) --- packages/kbn-pm/dist/index.js | 2 +- packages/kbn-pm/src/commands/bootstrap.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 69611ed3f5c5e..b8794124ad197 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -8868,7 +8868,7 @@ const BootstrapCommand = { } if (cachedProjectCount > 0) { - _utils_log__WEBPACK_IMPORTED_MODULE_1__["log"].success(`${cachedProjectCount} bootsrap builds are cached`); + _utils_log__WEBPACK_IMPORTED_MODULE_1__["log"].success(`${cachedProjectCount} bootstrap builds are cached`); } await Object(_utils_parallelize__WEBPACK_IMPORTED_MODULE_2__["parallelizeBatches"])(batchedProjects, async project => { diff --git a/packages/kbn-pm/src/commands/bootstrap.ts b/packages/kbn-pm/src/commands/bootstrap.ts index f8e50a8247856..a559f9a20432a 100644 --- a/packages/kbn-pm/src/commands/bootstrap.ts +++ b/packages/kbn-pm/src/commands/bootstrap.ts @@ -82,7 +82,7 @@ export const BootstrapCommand: ICommand = { } if (cachedProjectCount > 0) { - log.success(`${cachedProjectCount} bootsrap builds are cached`); + log.success(`${cachedProjectCount} bootstrap builds are cached`); } await parallelizeBatches(batchedProjects, async (project) => { From 3caab366c7000a498d9e26c9c5dad2f248314ff8 Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Tue, 30 Jun 2020 08:04:48 -0600 Subject: [PATCH 14/23] [Maps] Add maps telemetry saved object in with mappings disabled (#69995) Co-authored-by: Rudolf Meijering Co-authored-by: Elastic Machine --- x-pack/plugins/maps/server/plugin.ts | 3 ++- .../maps/server/saved_objects/index.ts | 1 + .../server/saved_objects/maps_telemetry.ts | 23 +++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index 60f3a9b68202c..dbcce50ac2b9a 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -15,7 +15,7 @@ import { getFlightsSavedObjects } from './sample_data/flights_saved_objects.js'; import { getWebLogsSavedObjects } from './sample_data/web_logs_saved_objects.js'; import { registerMapsUsageCollector } from './maps_telemetry/collectors/register'; import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE, getExistingMapPath } from '../common/constants'; -import { mapSavedObjects } from './saved_objects'; +import { mapSavedObjects, mapsTelemetrySavedObjects } from './saved_objects'; import { MapsXPackConfig } from '../config'; // @ts-ignore import { setInternalRepository } from './kibana_server_services'; @@ -191,6 +191,7 @@ export class MapsPlugin implements Plugin { }, }); + core.savedObjects.registerType(mapsTelemetrySavedObjects); core.savedObjects.registerType(mapSavedObjects); registerMapsUsageCollector(usageCollection, currentConfig); diff --git a/x-pack/plugins/maps/server/saved_objects/index.ts b/x-pack/plugins/maps/server/saved_objects/index.ts index 804d720a13ab0..c4b779183a2de 100644 --- a/x-pack/plugins/maps/server/saved_objects/index.ts +++ b/x-pack/plugins/maps/server/saved_objects/index.ts @@ -3,4 +3,5 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +export { mapsTelemetrySavedObjects } from './maps_telemetry'; export { mapSavedObjects } from './map'; diff --git a/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts b/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts new file mode 100644 index 0000000000000..c0d36983f65cd --- /dev/null +++ b/x-pack/plugins/maps/server/saved_objects/maps_telemetry.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { SavedObjectsType } from 'src/core/server'; + +/* + * The maps-telemetry saved object type isn't used, but in order to remove these fields from + * the mappings we register this type with `type: 'object', enabled: true` to remove all + * previous fields from the mappings until https://github.com/elastic/kibana/issues/67086 is + * solved. + */ +export const mapsTelemetrySavedObjects: SavedObjectsType = { + name: 'maps-telemetry', + hidden: false, + namespaceType: 'agnostic', + mappings: { + // @ts-ignore Core types don't support this since it's only really valid when removing a previously registered type + type: 'object', + enabled: false, + }, +}; From 233d261674799668d0938ee033c8632e377cebc0 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Tue, 30 Jun 2020 10:07:50 -0400 Subject: [PATCH 15/23] [ML] Anomaly Detection: ensure 'Category examples' tab in the expanded table row can be seen (#70241) * remove space from tab id * update test --- .../application/components/anomalies_table/anomaly_details.js | 2 +- .../components/anomalies_table/anomaly_details.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.js index edc1790b3adac..7b979d74a329c 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.js @@ -279,7 +279,7 @@ export class AnomalyDetails extends Component { ), }, { - id: 'Category examples', + id: 'category-examples', name: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.categoryExamplesTitle', { defaultMessage: 'Category examples', }), diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.test.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.test.js index 9fd1ffc3b637f..78c036eac1903 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.test.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.test.js @@ -67,7 +67,7 @@ describe('AnomalyDetails', () => { tabIndex: 1, }; const wrapper = shallowWithIntl(); - expect(wrapper.prop('initialSelectedTab').id).toBe('Category examples'); + expect(wrapper.prop('initialSelectedTab').id).toBe('category-examples'); }); test('Renders with terms and regex when definition prop is not undefined', () => { From ad01223c5acba50ee71771a2330ac95a370c651e Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Tue, 30 Jun 2020 16:13:28 +0200 Subject: [PATCH 16/23] chore: improve support for mjs file extension (#70186) --- .eslintrc.js | 56 +++++++++---------- .../core/development-unit-tests.asciidoc | 2 +- packages/eslint-config-kibana/jest.js | 4 +- src/dev/jest/config.js | 4 +- src/dev/run_eslint.js | 2 +- x-pack/dev-tools/jest/create_jest_config.js | 12 ++-- x-pack/plugins/apm/jest.config.js | 6 +- x-pack/test_utils/jest/config.integration.js | 4 +- x-pack/test_utils/jest/config.js | 4 +- 9 files changed, 47 insertions(+), 47 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 32f59c4d6b3db..2c49bf78c67b1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -64,63 +64,63 @@ module.exports = { * Temporarily disable some react rules for specific plugins, remove in separate PRs */ { - files: ['packages/kbn-ui-framework/**/*.{js,ts,tsx}'], + files: ['packages/kbn-ui-framework/**/*.{js,mjs,ts,tsx}'], rules: { 'jsx-a11y/no-onchange': 'off', }, }, { - files: ['src/plugins/es_ui_shared/**/*.{js,ts,tsx}'], + files: ['src/plugins/es_ui_shared/**/*.{js,mjs,ts,tsx}'], rules: { 'react-hooks/exhaustive-deps': 'off', }, }, { - files: ['src/plugins/kibana_react/**/*.{js,ts,tsx}'], + files: ['src/plugins/kibana_react/**/*.{js,mjs,ts,tsx}'], rules: { 'react-hooks/rules-of-hooks': 'off', 'react-hooks/exhaustive-deps': 'off', }, }, { - files: ['src/plugins/kibana_utils/**/*.{js,ts,tsx}'], + files: ['src/plugins/kibana_utils/**/*.{js,mjs,ts,tsx}'], rules: { 'react-hooks/exhaustive-deps': 'off', }, }, { - files: ['x-pack/plugins/canvas/**/*.{js,ts,tsx}'], + files: ['x-pack/plugins/canvas/**/*.{js,mjs,ts,tsx}'], rules: { 'jsx-a11y/click-events-have-key-events': 'off', }, }, { - files: ['x-pack/plugins/cross_cluster_replication/**/*.{js,ts,tsx}'], + files: ['x-pack/plugins/cross_cluster_replication/**/*.{js,mjs,ts,tsx}'], rules: { 'jsx-a11y/click-events-have-key-events': 'off', }, }, { - files: ['x-pack/legacy/plugins/index_management/**/*.{js,ts,tsx}'], + files: ['x-pack/legacy/plugins/index_management/**/*.{js,mjs,ts,tsx}'], rules: { 'react-hooks/exhaustive-deps': 'off', 'react-hooks/rules-of-hooks': 'off', }, }, { - files: ['x-pack/plugins/lens/**/*.{js,ts,tsx}'], + files: ['x-pack/plugins/lens/**/*.{js,mjs,ts,tsx}'], rules: { 'react-hooks/exhaustive-deps': 'off', }, }, { - files: ['x-pack/plugins/ml/**/*.{js,ts,tsx}'], + files: ['x-pack/plugins/ml/**/*.{js,mjs,ts,tsx}'], rules: { 'react-hooks/exhaustive-deps': 'off', }, }, { - files: ['x-pack/legacy/plugins/snapshot_restore/**/*.{js,ts,tsx}'], + files: ['x-pack/legacy/plugins/snapshot_restore/**/*.{js,mjs,ts,tsx}'], rules: { 'react-hooks/exhaustive-deps': 'off', }, @@ -132,7 +132,7 @@ module.exports = { * Licence headers */ { - files: ['**/*.{js,ts,tsx}', '!plugins/**/*'], + files: ['**/*.{js,mjs,ts,tsx}', '!plugins/**/*'], rules: { '@kbn/eslint/require-license-header': [ 'error', @@ -153,7 +153,7 @@ module.exports = { * New Platform client-side */ { - files: ['{src,x-pack}/plugins/*/public/**/*.{js,ts,tsx}'], + files: ['{src,x-pack}/plugins/*/public/**/*.{js,mjs,ts,tsx}'], rules: { 'import/no-commonjs': 'error', }, @@ -163,7 +163,7 @@ module.exports = { * Files that require Elastic license headers instead of Apache 2.0 header */ { - files: ['x-pack/**/*.{js,ts,tsx}'], + files: ['x-pack/**/*.{js,mjs,ts,tsx}'], rules: { '@kbn/eslint/require-license-header': [ 'error', @@ -184,7 +184,7 @@ module.exports = { * Restricted paths */ { - files: ['**/*.{js,ts,tsx}'], + files: ['**/*.{js,mjs,ts,tsx}'], rules: { '@kbn/eslint/no-restricted-paths': [ 'error', @@ -251,8 +251,8 @@ module.exports = { ], from: [ '(src|x-pack)/plugins/**/(public|server)/**/*', - '!(src|x-pack)/plugins/**/(public|server)/mocks/index.{js,ts}', - '!(src|x-pack)/plugins/**/(public|server)/(index|mocks).{js,ts,tsx}', + '!(src|x-pack)/plugins/**/(public|server)/mocks/index.{js,mjs,ts}', + '!(src|x-pack)/plugins/**/(public|server)/(index|mocks).{js,mjs,ts,tsx}', ], allowSameFolder: true, errorMessage: 'Plugins may only import from top-level public and server modules.', @@ -264,11 +264,11 @@ module.exports = { 'src/legacy/core_plugins/**/*', '!src/legacy/core_plugins/**/server/**/*', - '!src/legacy/core_plugins/**/index.{js,ts,tsx}', + '!src/legacy/core_plugins/**/index.{js,mjs,ts,tsx}', 'x-pack/legacy/plugins/**/*', '!x-pack/legacy/plugins/**/server/**/*', - '!x-pack/legacy/plugins/**/index.{js,ts,tsx}', + '!x-pack/legacy/plugins/**/index.{js,mjs,ts,tsx}', 'examples/**/*', '!examples/**/server/**/*', @@ -530,7 +530,7 @@ module.exports = { * Jest specific rules */ { - files: ['**/*.test.{js,ts,tsx}'], + files: ['**/*.test.{js,mjs,ts,tsx}'], rules: { 'jest/valid-describe': 'error', }, @@ -595,8 +595,8 @@ module.exports = { { // front end and common typescript and javascript files only files: [ - 'x-pack/plugins/security_solution/public/**/*.{js,ts,tsx}', - 'x-pack/plugins/security_solution/common/**/*.{js,ts,tsx}', + 'x-pack/plugins/security_solution/public/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/security_solution/common/**/*.{js,mjs,ts,tsx}', ], rules: { 'import/no-nodejs-modules': 'error', @@ -646,7 +646,7 @@ module.exports = { // { // // will introduced after the other warns are fixed // // typescript and javascript for front end react performance - // files: ['x-pack/plugins/security_solution/public/**/!(*.test).{js,ts,tsx}'], + // files: ['x-pack/plugins/security_solution/public/**/!(*.test).{js,mjs,ts,tsx}'], // plugins: ['react-perf'], // rules: { // // 'react-perf/jsx-no-new-object-as-prop': 'error', @@ -657,7 +657,7 @@ module.exports = { // }, { // typescript and javascript for front and back end - files: ['x-pack/{,legacy/}plugins/security_solution/**/*.{js,ts,tsx}'], + files: ['x-pack/{,legacy/}plugins/security_solution/**/*.{js,mjs,ts,tsx}'], plugins: ['eslint-plugin-node', 'react'], env: { mocha: true, @@ -776,8 +776,8 @@ module.exports = { { // front end and common typescript and javascript files only files: [ - 'x-pack/plugins/lists/public/**/*.{js,ts,tsx}', - 'x-pack/plugins/lists/common/**/*.{js,ts,tsx}', + 'x-pack/plugins/lists/public/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/lists/common/**/*.{js,mjs,ts,tsx}', ], rules: { 'import/no-nodejs-modules': 'error', @@ -792,7 +792,7 @@ module.exports = { }, { // typescript and javascript for front and back end - files: ['x-pack/plugins/lists/**/*.{js,ts,tsx}'], + files: ['x-pack/plugins/lists/**/*.{js,mjs,ts,tsx}'], plugins: ['eslint-plugin-node'], env: { mocha: true, @@ -1020,8 +1020,8 @@ module.exports = { */ { files: [ - 'src/plugins/vis_type_timeseries/**/*.{js,ts,tsx}', - 'src/legacy/core_plugins/vis_type_timeseries/**/*.{js,ts,tsx}', + 'src/plugins/vis_type_timeseries/**/*.{js,mjs,ts,tsx}', + 'src/legacy/core_plugins/vis_type_timeseries/**/*.{js,mjs,ts,tsx}', ], rules: { 'import/no-default-export': 'error', diff --git a/docs/developer/core/development-unit-tests.asciidoc b/docs/developer/core/development-unit-tests.asciidoc index a738e2cf372d9..04cce0dfec901 100644 --- a/docs/developer/core/development-unit-tests.asciidoc +++ b/docs/developer/core/development-unit-tests.asciidoc @@ -22,7 +22,7 @@ yarn test:mocha [float] ==== Jest -Jest tests are stored in the same directory as source code files with the `.test.{js,ts,tsx}` suffix. +Jest tests are stored in the same directory as source code files with the `.test.{js,mjs,ts,tsx}` suffix. *Running Jest Unit Tests* diff --git a/packages/eslint-config-kibana/jest.js b/packages/eslint-config-kibana/jest.js index d682277ff905a..c374de7ae123c 100644 --- a/packages/eslint-config-kibana/jest.js +++ b/packages/eslint-config-kibana/jest.js @@ -2,8 +2,8 @@ module.exports = { overrides: [ { files: [ - '**/*.{test,test.mocks,mock}.{js,ts,tsx}', - '**/__mocks__/**/*.{js,ts,tsx}', + '**/*.{test,test.mocks,mock}.{js,mjs,ts,tsx}', + '**/__mocks__/**/*.{js,mjs,ts,tsx}', ], plugins: [ 'jest', diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index da343aa0f0672..391a52b7f0397 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -50,7 +50,7 @@ export default { 'packages/kbn-ui-framework/src/services/**/*.js', '!packages/kbn-ui-framework/src/services/index.js', '!packages/kbn-ui-framework/src/services/**/*/index.js', - 'src/legacy/core_plugins/**/*.{js,jsx,ts,tsx}', + 'src/legacy/core_plugins/**/*.{js,mjs,jsx,ts,tsx}', '!src/legacy/core_plugins/**/{__test__,__snapshots__}/**/*', '!src/legacy/core_plugins/tests_bundle/**', ], @@ -81,7 +81,7 @@ export default { ], coverageDirectory: '/target/kibana-coverage/jest', coverageReporters: !!process.env.CODE_COVERAGE ? ['json'] : ['html', 'text'], - moduleFileExtensions: ['js', 'json', 'ts', 'tsx', 'node'], + moduleFileExtensions: ['js', 'mjs', 'json', 'ts', 'tsx', 'node'], modulePathIgnorePatterns: ['__fixtures__/', 'target/'], testEnvironment: 'jest-environment-jsdom-thirteen', testMatch: ['**/*.test.{js,ts,tsx}'], diff --git a/src/dev/run_eslint.js b/src/dev/run_eslint.js index 3bfbb9cc876e0..3214a2fb45471 100644 --- a/src/dev/run_eslint.js +++ b/src/dev/run_eslint.js @@ -31,7 +31,7 @@ if (!process.argv.includes('--no-cache')) { } if (!process.argv.includes('--ext')) { - process.argv.push('--ext', '.js,.ts,.tsx'); + process.argv.push('--ext', '.js,.mjs,.ts,.tsx'); } // common-js is required so that logic before this executes before loading eslint diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index 9b6db8b74458b..a0574dbdf36da 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -9,7 +9,7 @@ export function createJestConfig({ kibanaDirectory, rootDir, xPackKibanaDirector return { rootDir, roots: ['/plugins', '/legacy/plugins', '/legacy/server'], - moduleFileExtensions: ['js', 'json', 'ts', 'tsx', 'node'], + moduleFileExtensions: ['js', 'mjs', 'json', 'ts', 'tsx', 'node'], moduleNameMapper: { '@elastic/eui$': `${kibanaDirectory}/node_modules/@elastic/eui/test-env`, '@elastic/eui/lib/(.*)?': `${kibanaDirectory}/node_modules/@elastic/eui/test-env/$1`, @@ -32,11 +32,11 @@ export function createJestConfig({ kibanaDirectory, rootDir, xPackKibanaDirector '^(!!)?file-loader!': fileMockPath, }, collectCoverageFrom: [ - 'legacy/plugins/**/*.{js,jsx,ts,tsx}', - 'legacy/server/**/*.{js,jsx,ts,tsx}', - 'plugins/**/*.{js,jsx,ts,tsx}', + 'legacy/plugins/**/*.{js,mjs,jsx,ts,tsx}', + 'legacy/server/**/*.{js,mjs,jsx,ts,tsx}', + 'plugins/**/*.{js,mjs,jsx,ts,tsx}', '!**/{__test__,__snapshots__,__examples__,integration_tests,tests}/**', - '!**/*.test.{js,ts,tsx}', + '!**/*.test.{js,mjs,ts,tsx}', '!**/flot-charts/**', '!**/test/**', '!**/build/**', @@ -60,7 +60,7 @@ export function createJestConfig({ kibanaDirectory, rootDir, xPackKibanaDirector `${kibanaDirectory}/src/dev/jest/setup/react_testing_library.js`, ], testEnvironment: 'jest-environment-jsdom-thirteen', - testMatch: ['**/*.test.{js,ts,tsx}'], + testMatch: ['**/*.test.{js,mjs,ts,tsx}'], testRunner: 'jest-circus/runner', transform: { '^.+\\.(js|tsx?)$': `${kibanaDirectory}/src/dev/jest/babel_transform.js`, diff --git a/x-pack/plugins/apm/jest.config.js b/x-pack/plugins/apm/jest.config.js index 43bdeb583c819..2f9d8a37376d9 100644 --- a/x-pack/plugins/apm/jest.config.js +++ b/x-pack/plugins/apm/jest.config.js @@ -29,10 +29,10 @@ module.exports = { roots: [`${rootDir}/common`, `${rootDir}/public`, `${rootDir}/server`], collectCoverage: true, collectCoverageFrom: [ - '**/*.{js,jsx,ts,tsx}', + '**/*.{js,mjs,jsx,ts,tsx}', '!**/{__test__,__snapshots__,__examples__,integration_tests,tests}/**', - '!**/*.stories.{js,ts,tsx}', - '!**/*.test.{js,ts,tsx}', + '!**/*.stories.{js,mjs,ts,tsx}', + '!**/*.test.{js,mjs,ts,tsx}', '!**/dev_docs/**', '!**/e2e/**', '!**/scripts/**', diff --git a/x-pack/test_utils/jest/config.integration.js b/x-pack/test_utils/jest/config.integration.js index 033c948c3c034..03917d34ab09c 100644 --- a/x-pack/test_utils/jest/config.integration.js +++ b/x-pack/test_utils/jest/config.integration.js @@ -10,9 +10,9 @@ import config from './config'; export default { ...config, testMatch: [ - `**/${RESERVED_DIR_JEST_INTEGRATION_TESTS}/**/*.test.{js,ts,tsx}`, + `**/${RESERVED_DIR_JEST_INTEGRATION_TESTS}/**/*.test.{js,mjs,ts,tsx}`, // Tests within `__jest__` directories should be treated as regular unit tests. - `!**/__jest__/${RESERVED_DIR_JEST_INTEGRATION_TESTS}/**/*.test.{js,ts,tsx}`, + `!**/__jest__/${RESERVED_DIR_JEST_INTEGRATION_TESTS}/**/*.test.{js,mjs,ts,tsx}`, ], testPathIgnorePatterns: config.testPathIgnorePatterns.filter( (pattern) => !pattern.includes(RESERVED_DIR_JEST_INTEGRATION_TESTS) diff --git a/x-pack/test_utils/jest/config.js b/x-pack/test_utils/jest/config.js index adee510ef2846..7bb073023b7f8 100644 --- a/x-pack/test_utils/jest/config.js +++ b/x-pack/test_utils/jest/config.js @@ -29,10 +29,10 @@ export default { ], coverageDirectory: '/../target/kibana-coverage/jest', coverageReporters: ['html'], - moduleFileExtensions: ['js', 'json', 'ts', 'tsx', 'node'], + moduleFileExtensions: ['js', 'mjs', 'json', 'ts', 'tsx', 'node'], modulePathIgnorePatterns: ['__fixtures__/', 'target/'], testEnvironment: 'jest-environment-jsdom-thirteen', - testMatch: ['**/*.test.{js,ts,tsx}'], + testMatch: ['**/*.test.{js,mjs,ts,tsx}'], testPathIgnorePatterns: [ '/packages/kbn-ui-framework/(dist|doc_site|generator-kui)/', '/packages/kbn-pm/dist/', From 93ef5c0c418374fdd145a3ed321fa6700a8132e3 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Tue, 30 Jun 2020 07:30:31 -0700 Subject: [PATCH 17/23] [Usage Collection] Report nodes feature usage (#70108) * Adds nodes feature usage stats merged into cluster_stats.nodes when usage collection is local --- .../__tests__/get_local_stats.js | 65 +++++++++++++-- .../telemetry_collection/get_local_stats.ts | 14 +++- .../get_nodes_usage.test.ts | 80 ++++++++++++++++++ .../telemetry_collection/get_nodes_usage.ts | 81 +++++++++++++++++++ .../apis/telemetry/telemetry_local.js | 1 + .../get_stats_with_xpack.test.ts.snap | 44 +++++++++- .../get_stats_with_xpack.test.ts | 25 ++++++ 7 files changed, 299 insertions(+), 11 deletions(-) create mode 100644 src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.test.ts create mode 100644 src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts diff --git a/src/plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js b/src/plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js index 29076537e9ae8..e78b92498e6e7 100644 --- a/src/plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js +++ b/src/plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js @@ -19,11 +19,12 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; +import { merge, omit } from 'lodash'; +import { TIMEOUT } from '../constants'; import { mockGetClusterInfo } from './get_cluster_info'; import { mockGetClusterStats } from './get_cluster_stats'; -import { omit } from 'lodash'; import { getLocalStats, handleLocalStats } from '../get_local_stats'; const mockUsageCollection = (kibanaUsage = {}) => ({ @@ -51,10 +52,26 @@ const getMockServer = (getCluster = sinon.stub()) => ({ elasticsearch: { getCluster }, }, }); +function mockGetNodesUsage(callCluster, nodesUsage, req) { + callCluster + .withArgs( + req, + { + method: 'GET', + path: '/_nodes/usage', + query: { + timeout: TIMEOUT, + }, + }, + 'transport.request' + ) + .returns(nodesUsage); +} -function mockGetLocalStats(callCluster, clusterInfo, clusterStats, req) { +function mockGetLocalStats(callCluster, clusterInfo, clusterStats, nodesUsage, req) { mockGetClusterInfo(callCluster, clusterInfo, req); mockGetClusterStats(callCluster, clusterStats, req); + mockGetNodesUsage(callCluster, nodesUsage, req); } describe('get_local_stats', () => { @@ -68,6 +85,28 @@ describe('get_local_stats', () => { number: version, }, }; + const nodesUsage = [ + { + node_id: 'some_node_id', + timestamp: 1588617023177, + since: 1588616945163, + rest_actions: { + nodes_usage_action: 1, + create_index_action: 1, + document_get_action: 1, + search_action: 19, + nodes_info_action: 36, + }, + aggregations: { + terms: { + bytes: 2, + }, + scripted_metric: { + other: 7, + }, + }, + }, + ]; const clusterStats = { _nodes: { failed: 123 }, cluster_name: 'real-cool', @@ -75,6 +114,7 @@ describe('get_local_stats', () => { nodes: { yup: 'abc' }, random: 123, }; + const kibana = { kibana: { great: 'googlymoogly', @@ -97,12 +137,16 @@ describe('get_local_stats', () => { snow: { chances: 0 }, }; + const clusterStatsWithNodesUsage = { + ...clusterStats, + nodes: merge(clusterStats.nodes, { usage: nodesUsage }), + }; const combinedStatsResult = { collection: 'local', cluster_uuid: clusterUuid, cluster_name: clusterName, version, - cluster_stats: omit(clusterStats, '_nodes', 'cluster_name'), + cluster_stats: omit(clusterStatsWithNodesUsage, '_nodes', 'cluster_name'), stack_stats: { kibana: { great: 'googlymoogly', @@ -135,7 +179,7 @@ describe('get_local_stats', () => { describe('handleLocalStats', () => { it('returns expected object without xpack and kibana data', () => { - const result = handleLocalStats(clusterInfo, clusterStats, void 0, context); + const result = handleLocalStats(clusterInfo, clusterStatsWithNodesUsage, void 0, context); expect(result.cluster_uuid).to.eql(combinedStatsResult.cluster_uuid); expect(result.cluster_name).to.eql(combinedStatsResult.cluster_name); expect(result.cluster_stats).to.eql(combinedStatsResult.cluster_stats); @@ -146,7 +190,7 @@ describe('get_local_stats', () => { }); it('returns expected object with xpack', () => { - const result = handleLocalStats(clusterInfo, clusterStats, void 0, context); + const result = handleLocalStats(clusterInfo, clusterStatsWithNodesUsage, void 0, context); const { stack_stats: stack, ...cluster } = result; expect(cluster.collection).to.be(combinedStatsResult.collection); expect(cluster.cluster_uuid).to.be(combinedStatsResult.cluster_uuid); @@ -167,7 +211,8 @@ describe('get_local_stats', () => { mockGetLocalStats( callClusterUsageFailed, Promise.resolve(clusterInfo), - Promise.resolve(clusterStats) + Promise.resolve(clusterStats), + Promise.resolve(nodesUsage) ); const result = await getLocalStats([], { server: getMockServer(), @@ -177,6 +222,7 @@ describe('get_local_stats', () => { expect(result.cluster_uuid).to.eql(combinedStatsResult.cluster_uuid); expect(result.cluster_name).to.eql(combinedStatsResult.cluster_name); expect(result.cluster_stats).to.eql(combinedStatsResult.cluster_stats); + expect(result.cluster_stats.nodes).to.eql(combinedStatsResult.cluster_stats.nodes); expect(result.version).to.be('2.3.4'); expect(result.collection).to.be('local'); @@ -188,7 +234,12 @@ describe('get_local_stats', () => { it('returns expected object with xpack and kibana data', async () => { const callCluster = sinon.stub(); const usageCollection = mockUsageCollection(kibana); - mockGetLocalStats(callCluster, Promise.resolve(clusterInfo), Promise.resolve(clusterStats)); + mockGetLocalStats( + callCluster, + Promise.resolve(clusterInfo), + Promise.resolve(clusterStats), + Promise.resolve(nodesUsage) + ); const result = await getLocalStats([], { server: getMockServer(callCluster), diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts index b77d01c5b431f..b42edde2f55ca 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts @@ -24,6 +24,7 @@ import { import { getClusterInfo, ESClusterInfo } from './get_cluster_info'; import { getClusterStats } from './get_cluster_stats'; import { getKibana, handleKibanaStats, KibanaUsageStats } from './get_kibana'; +import { getNodesUsage } from './get_nodes_usage'; /** * Handle the separate local calls by combining them into a single object response that looks like the @@ -67,12 +68,21 @@ export const getLocalStats: StatsGetter<{}, TelemetryLocalStats> = async ( return await Promise.all( clustersDetails.map(async (clustersDetail) => { - const [clusterInfo, clusterStats, kibana] = await Promise.all([ + const [clusterInfo, clusterStats, nodesUsage, kibana] = await Promise.all([ getClusterInfo(callCluster), // cluster info getClusterStats(callCluster), // cluster stats (not to be confused with cluster _state_) + getNodesUsage(callCluster), // nodes_usage info getKibana(usageCollection, callCluster), ]); - return handleLocalStats(clusterInfo, clusterStats, kibana, context); + return handleLocalStats( + clusterInfo, + { + ...clusterStats, + nodes: { ...clusterStats.nodes, usage: nodesUsage }, + }, + kibana, + context + ); }) ); }; diff --git a/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.test.ts new file mode 100644 index 0000000000000..4e4b0e11b7979 --- /dev/null +++ b/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.test.ts @@ -0,0 +1,80 @@ +/* + * 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 { getNodesUsage } from './get_nodes_usage'; +import { TIMEOUT } from './constants'; + +const mockedNodesFetchResponse = { + cluster_name: 'test cluster', + nodes: { + some_node_id: { + timestamp: 1588617023177, + since: 1588616945163, + rest_actions: { + nodes_usage_action: 1, + create_index_action: 1, + document_get_action: 1, + search_action: 19, + nodes_info_action: 36, + }, + aggregations: { + terms: { + bytes: 2, + }, + scripted_metric: { + other: 7, + }, + }, + }, + }, +}; +describe('get_nodes_usage', () => { + it('calls fetchNodesUsage', async () => { + const callCluster = jest.fn(); + callCluster.mockResolvedValueOnce(mockedNodesFetchResponse); + await getNodesUsage(callCluster); + expect(callCluster).toHaveBeenCalledWith('transport.request', { + path: '/_nodes/usage', + method: 'GET', + query: { + timeout: TIMEOUT, + }, + }); + }); + it('returns a modified array of node usage data', async () => { + const callCluster = jest.fn(); + callCluster.mockResolvedValueOnce(mockedNodesFetchResponse); + const result = await getNodesUsage(callCluster); + expect(result.nodes).toEqual([ + { + aggregations: { scripted_metric: { other: 7 }, terms: { bytes: 2 } }, + node_id: 'some_node_id', + rest_actions: { + create_index_action: 1, + document_get_action: 1, + nodes_info_action: 36, + nodes_usage_action: 1, + search_action: 19, + }, + since: 1588616945163, + timestamp: 1588617023177, + }, + ]); + }); +}); diff --git a/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts b/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts new file mode 100644 index 0000000000000..c5c110fbb4149 --- /dev/null +++ b/src/plugins/telemetry/server/telemetry_collection/get_nodes_usage.ts @@ -0,0 +1,81 @@ +/* + * 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 { LegacyAPICaller } from 'kibana/server'; +import { TIMEOUT } from './constants'; + +export interface NodeAggregation { + [key: string]: number; +} + +// we set aggregations as an optional type because it was only added in v7.8.0 +export interface NodeObj { + node_id?: string; + timestamp: number; + since: number; + rest_actions: { + [key: string]: number; + }; + aggregations?: { + [key: string]: NodeAggregation; + }; +} + +export interface NodesFeatureUsageResponse { + cluster_name: string; + nodes: { + [key: string]: NodeObj; + }; +} + +export type NodesUsageGetter = ( + callCluster: LegacyAPICaller +) => Promise<{ nodes: NodeObj[] | Array<{}> }>; +/** + * Get the nodes usage data from the connected cluster. + * + * This is the equivalent to GET /_nodes/usage?timeout=30s. + * + * The Nodes usage API was introduced in v6.0.0 + */ +export async function fetchNodesUsage( + callCluster: LegacyAPICaller +): Promise { + const response = await callCluster('transport.request', { + method: 'GET', + path: '/_nodes/usage', + query: { + timeout: TIMEOUT, + }, + }); + return response; +} + +/** + * Get the nodes usage from the connected cluster + * @param callCluster APICaller + * @returns Object containing array of modified usage information with the node_id nested within the data for that node. + */ +export const getNodesUsage: NodesUsageGetter = async (callCluster) => { + const result = await fetchNodesUsage(callCluster); + const transformedNodes = Object.entries(result?.nodes || {}).map(([key, value]) => ({ + ...(value as NodeObj), + node_id: key, + })); + return { nodes: transformedNodes }; +}; diff --git a/test/api_integration/apis/telemetry/telemetry_local.js b/test/api_integration/apis/telemetry/telemetry_local.js index 2875ff09a9a8d..e74cd180185ab 100644 --- a/test/api_integration/apis/telemetry/telemetry_local.js +++ b/test/api_integration/apis/telemetry/telemetry_local.js @@ -113,6 +113,7 @@ export default function ({ getService }) { 'cluster_stats.nodes.plugins', 'cluster_stats.nodes.process', 'cluster_stats.nodes.versions', + 'cluster_stats.nodes.usage', 'cluster_stats.status', 'cluster_stats.timestamp', 'cluster_uuid', diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap index 1a70504dc9391..ed82dc65eb410 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap @@ -4,7 +4,27 @@ exports[`Telemetry Collection: Get Aggregated Stats OSS-like telemetry (no licen Array [ Object { "cluster_name": "test", - "cluster_stats": Object {}, + "cluster_stats": Object { + "nodes": Object { + "usage": Object { + "nodes": Array [ + Object { + "aggregations": Object { + "terms": Object { + "bytes": 2, + }, + }, + "node_id": "some_node_id", + "rest_actions": Object { + "nodes_usage_action": 1, + }, + "since": 1588616945163, + "timestamp": 1588617023177, + }, + ], + }, + }, + }, "cluster_uuid": "test", "collection": "local", "stack_stats": Object { @@ -62,7 +82,27 @@ exports[`Telemetry Collection: Get Aggregated Stats X-Pack telemetry (license + Array [ Object { "cluster_name": "test", - "cluster_stats": Object {}, + "cluster_stats": Object { + "nodes": Object { + "usage": Object { + "nodes": Array [ + Object { + "aggregations": Object { + "terms": Object { + "bytes": 2, + }, + }, + "node_id": "some_node_id", + "rest_actions": Object { + "nodes_usage_action": 1, + }, + "since": 1588616945163, + "timestamp": 1588617023177, + }, + ], + }, + }, + }, "cluster_uuid": "test", "collection": "local", "stack_stats": Object { diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts index 5dfe3d3e99a7f..a8311933f0531 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts @@ -28,6 +28,20 @@ const kibana = { rain: { chances: 2 }, snow: { chances: 0 }, }; +const nodesUsage = { + some_node_id: { + timestamp: 1588617023177, + since: 1588616945163, + rest_actions: { + nodes_usage_action: 1, + }, + aggregations: { + terms: { + bytes: 2, + }, + }, + }, +}; const getContext = () => ({ version: '8675309-snapshot', @@ -47,6 +61,11 @@ describe('Telemetry Collection: Get Aggregated Stats', () => { if (options.path === '/_license' || options.path === '/_xpack/usage') { // eslint-disable-next-line no-throw-literal throw { statusCode: 404 }; + } else if (options.path === '/_nodes/usage') { + return { + cluster_name: 'test cluster', + nodes: nodesUsage, + }; } return {}; case 'info': @@ -81,6 +100,12 @@ describe('Telemetry Collection: Get Aggregated Stats', () => { if (options.path === '/_xpack/usage') { return {}; } + if (options.path === '/_nodes/usage') { + return { + cluster_name: 'test cluster', + nodes: nodesUsage, + }; + } case 'info': return { cluster_uuid: 'test', cluster_name: 'test', version: { number: '8.0.0' } }; default: From 8978ec8945df57b5df76a3b404b1bf35e0762c91 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Tue, 30 Jun 2020 07:31:47 -0700 Subject: [PATCH 18/23] [DOCS] Adds glossary to documentation (#69721) * [DOCS] Adds glossary to documentation * [DOCS] Fixes build errors * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * [DOCS] Adds more terms to glossary * [DOCS] Adds more terms to glossary * Update docs/glossary.asciidoc Co-authored-by: debadair * Update docs/glossary.asciidoc Co-authored-by: debadair * Update docs/glossary.asciidoc Co-authored-by: debadair * Update docs/glossary.asciidoc Co-authored-by: debadair * Update docs/glossary.asciidoc Co-authored-by: debadair * Update docs/glossary.asciidoc Co-authored-by: debadair * Update docs/glossary.asciidoc Co-authored-by: debadair * Update docs/glossary.asciidoc Co-authored-by: debadair * [DOCS] Incorporates review comments * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * Update docs/glossary.asciidoc Co-authored-by: Lisa Cawley * [DOCS] Incorporates review comments * [DOCS] Incorporates review comments Co-authored-by: Lisa Cawley Co-authored-by: debadair --- docs/glossary.asciidoc | 413 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 413 insertions(+) create mode 100644 docs/glossary.asciidoc diff --git a/docs/glossary.asciidoc b/docs/glossary.asciidoc new file mode 100644 index 0000000000000..d7a82068abbcb --- /dev/null +++ b/docs/glossary.asciidoc @@ -0,0 +1,413 @@ +[glossary] +[[glossary]] += Glossary + +<> | <> | <> | <> | <> | <> | <> | H | I | J | <> | <> | <> | N | O | <> | <> | R | <> | <> | <> | V | <> | X | Y | Z + +[float] +[[a_glos]] +== A + +[glossary] +[[glossary-action]] action :: ++ +-- +// tag::action-def[] +The alert-specific response that occurs when an alert fires. +An alert can have multiple actions. +See +{kibana-ref}/action-types.html[Action and connector types]. +// end::action-def[] +-- + +[[glossary-advanced-settings]] Advanced Settings :: +// tag::advanced-settings-def[] +Enables you to control the appearance and behavior of {kib} +by setting the date format, default index, and other attributes. +Part of {kib} Stack Management. +See {kibana-ref}/advanced-options.html[Advanced Settings]. +// end::advanced-settings-def[] + +[[glossary-alert]] alert :: +// tag::alert-def[] +A set of <>, schedules, and <> +that enable notifications. +See <>. +// end::alert-def[] + +[[glossary-alerts-and-actions]] Alerts and Actions :: +// tag::alerts-and-actions-def[] +A comprehensive view of all your alerts. Enables you to access and +manage alerts for all {kib} apps from one place. +See {kibana-ref}/alerting-getting-started.html[Alerts and Actions]. +// end::alerts-and-actions-def[] + +[[glossary-annotation]] annotation :: +// tag::annotation-def[] +A way to augment a data display with descriptive domain knowledge. +// end::alerts-annotation-def[] + + +[[glossary-app]] app :: +// tag::app-def[] +A top-level {kib} component that is accessed through the side navigation. +Apps include core {kib} components such as Discover and Dashboard, +solutions like Observability and Security, and special-purpose tools +like Maps and Stack Management. +// end::app-def[] + + +[float] +[[b_glos]] +== B + +[[glossary-basemap]] basemap :: +// tag::basemap-def[] +The background detail necessary to orient the location of a map. +// end::basemap-def[] + +[[glossary-bucket]] bucket :: +// tag::bucket-def[] +A set of documents in {kib} that have certain characteristics in common. +For example, matching documents might be bucketed by color, distance, or date range. +// end::bucket-def[] + +[[glossary-bucket-aggregation]] bucket aggregation:: +// tag::bucket-aggregation-def[] +An aggregation that creates buckets of documents. Each bucket is associated with a +criterion (depending on the aggregation type), which determines whether or not a document +in the current context falls into the bucket. +// end::bucket-aggregation-def[] + +[float] +[[c_glos]] +== C + +[[glossary-canvas]] Canvas :: +// tag::canvas-def[] +Enables you to create presentations and infographics that pull live data directly from {es}. +See {kibana-ref}/canvas.html[Canvas]. +// end::canvas-def[] + +[[glossary-canvas-language]] Canvas expression language:: +// tag::ccanvas-language-def[] +A pipeline-based expression language for manipulating and visualizing data. +Includes dozens of functions and other capabilities, such as table transforms, +type casting, and sub-expressions. Supports TinyMath functions for complex math calculations. +See {kibana-ref}/canvas-function-reference.html[Canvas function reference]. +// end::canvas-language-def[] + + +[[glossary-certainty]] certainty :: +// tag::certainty-def[] +Specifies how many documents must contain a pair of terms before it is considered +a useful connection in a graph. +// end::certainty-def[] + +[[glossary-condition]] condition :: +// tag::condition-def[] +Specifies the circumstances that must be met to trigger an alert. +// end::condition-def[] + +[[glossary-connector]] connector :: +// tag::connector-def[] +A configuration that enables integration with an external system (the destination for an action). +See {kibana-ref}/action-types.html[Action and connector types]. +// end::connector-def[] + +[[glossary-console]] Console :: +// tag::console-def[] +A tool for interacting with the {es} REST API. +You can send requests to {es}, view responses, +view API documentation, and get your request history. +See {kibana-ref}/console-kibana.html[Console]. +// end::console-def[] + +[float] +[[d_glos]] +== D + +[[glossary-dashboard]] dashboard :: +// tag::dashboard-def[] +A collection of +<>, <>, and +<> that +provide insights into your data from multiple perspectives. +// end::dashboard-def[] + +[[glossary-data-source]] data source :: +// tag::data-source-def[] +A file, database, or service that provides the underlying data for a map, Canvas element, or visualization. +// end::data-source-def[] + +[[glossary-discover]] Discover :: +// tag::discover-def[] +Enables you to search and filter your data to zoom in on the information +that you are interested in. +// end::discover-def[] + +[[glossary-drilldown]] drilldown :: +// tag::drilldown-def[] +A navigation path that retains context (time range and filters) +from the source to the destination, so you can view the data from a new perspective. +A dashboard that shows the overall status of multiple data centers +might have a drilldown to a dashboard for a single data center. See {kibana-ref}/drilldowns.html[Drilldowns]. +// end::drilldown-def[] + + + +[float] +[[e_glos]] +== E + +[[glossary-edge]] edge :: +// tag::edge-def[] +A connection between nodes in a graph that shows that they are related. +The line weight indicates the strength of the relationship. See +{kibana-ref}/xpack-graph.html[Graph]. +// end::edge-def[] + + +[[glossary-ems]] Elastic Maps Service (EMS) :: +// tag::ems-def[] +A service that provides basemap tiles, shape files, and other key features +that are essential for visualizing geospatial data. +// end::ems-def[] + +[[glossary-element]] element :: +// tag::element-def[] +A <> workpad object that displays an image, text, or visualization. +// end::element-def[] + + +[float] +[[f_glos]] +== F + +[[glossary-feature-controls]] Feature Controls :: +// tag::feature-controls-def[] +Enables administrators to customize which features are +available in each <>. See +{kibana-ref}//xpack-spaces.html#spaces-control-feature-visibility[Feature Controls]. +// end::feature-controls-def[] + +[float] +[[g_glos]] +== G + +[[glossary-graph]] graph :: +// tag::graph-def[] +A data structure and visualization that shows interconnections between +a set of entities. Each entity is represented by a node. Connections between +nodes are represented by <>. See {kibana-ref}/xpack-graph.html[Graph]. +// end::graph-def[] + +[[glossary-grok-debugger]] Grok Debugger :: +// tag::grok-debugger-def[] +A tool for building and debugging grok patterns. Grok is good for parsing +syslog, Apache, and other webserver logs. See +{kibana-ref}/xpack-grokdebugger.html[Debugging grok expressions]. +// end::grok-debugger-def[] + + +[float] +[[k_glos]] +== K + +[[glossary-kql]] {kib} Query Language (KQL) :: +// tag::kql-def[] +The default language for querying in {kib}. KQL provides +support for scripted fields. See +{kibana-ref}/kuery-query.html[Kibana Query Language]. +// end::kql-def[] + + +[float] +[[l_glos]] +== L + +[[glossary-lens]] Lens :: +// tag::lens-def[] +Enables you to build visualizations by dragging and dropping data fields. +Lens makes makes smart visualization suggestions for your data, +allowing you to switch between visualization types. +See {kibana-ref}/lens.html[Lens]. +// end::lens-def[] + + +[[glossary-lucene]] Lucene query syntax :: +// tag::lucene-def[] +The query syntax for {kib}’s legacy query language. The Lucene query +syntax is available under the options menu in the query bar and from +<>. +// end::lucene-def[] + +[float] +[[m_glos]] +== M + +[[glossary-map]] map :: +// tag::map-def[] +A representation of geographic data using symbols and labels. +See {kibana-ref}/maps.html[Maps]. +// end::map-def[] + +[[glossary-metric-aggregation]] metric aggregation :: +// tag::metric-aggregation-def[] +An aggregation that calculates and tracks metrics for a set of documents. +// end::metric-aggregation-def[] + + +[float] +[[p_glos]] +== P + +[[glossary-painless-lab]] Painless Lab :: +// tag::painless-lab-def[] +An interactive code editor that lets you test and debug Painless scripts in real-time. +See {kibana-ref}/painlesslab.html[Painless Lab]. +// end::painless-lab-def[] + + +[[glossary-panel]] panel :: +// tag::panel-def[] +A <> component that contains a +query element or visualization, such as a chart, table, or list. +// end::panel-def[] + + +[float] +[[q_glos]] +== Q + +[[glossary-query-profiler]] Query Profiler :: +// tag::query-profiler-def[] +A tool that enables you to inspect and analyze search queries to diagnose and debug poorly performing queries. +See {kibana-ref}/xpack-profiler.html[Query Profiler]. +// end::query-profiler-def[] + +[float] +[[s_glos]] +== S + +[[glossary-saved-object]] saved object :: +// tag::saved-object-def[] +A representation of a dashboard, visualization, map, index pattern, or Canvas workpad +that can be stored and reloaded. +// end::saved-object-def[] + +[[glossary-saved-search]] saved search :: +// tag::saved-search-def[] +The query text, filters, and time filter that make up a search, +saved for later retrieval and reuse. +// end::saved-search-def[] + +[[glossary-scripted-field]] scripted field :: +// tag::scripted-field-def[] +A field that computes data on the fly from the data in {es} indices. +Scripted field data is shown in Discover and used in visualizations. +// end::scripted-field-def[] + +[[glossary-shareable]] shareable :: +// tag::shareable-def[] +A Canvas workpad that can be embedded on any webpage. +Shareables enable you to display Canvas visualizations on internal wiki pages or public websites. +// end::shareable-def[] + +[[glossary-space]] space :: +// tag::space-def[] +A place for organizing <>, +<>, and other <> by category. +For example, you might have different spaces for each team, use case, or individual. +See +{kibana-ref}/xpack-spaces.html[Spaces]. +// end::space-def[] + + +[float] +[[t_glos]] +== T + +[[glossary-term-join]] term join :: +// tag::term-join-def[] +A shared key that combines vector features with the results of an +{es} terms aggregation. Term joins augment vector features with +properties for data-driven styling and rich tooltip content in maps. +// end::term-join-def[] + +[[glossary-time-filter]] time filter :: +// tag::time-filter-def[] +A {kib} control that constrains the search results to a particular time period. +// end::time-filter-def[] + +[[glossary-timelion]] Timelion :: +// tag::timelion-def[] +A tool for building a time series visualization that analyzes data in time order. +See {kibana-ref}/timelion.html[Timelion]. +// end::timelion-def[] + + +[[glossary-time-series-data]] time series data :: +// tag::time-series-data-def[] +Timestamped data such as logs, metrics, and events that is indexed on an ongoing basis. +// end::time-series-data-def[] + + +[[glossary-TSVB-data]] TSVB :: +// tag::TSVB-def[] +A time series data visualizer that allows you to combine an +infinite number of aggregations to display complex data. +See {kibana-ref}/TSVB.html[TSVB]. +// end::TSVB-def[] + + +[float] +[[u_glos]] +== U + +[[glossary-upgrade-assistant]] Upgrade Assistant :: +// tag::upgrade-assistant-def[] +A tool that helps you prepare for an upgrade to the next major version of +{es}. The assistant identifies the deprecated settings in your cluster and +indices and guides you through resolving issues, including reindexing. See +{kibana-ref}/upgrade-assistant.html[Upgrade Assistant]. +// end::upgrade-assistant-def[] + + +[float] +[[v_glos]] +== V + +[[glossary-vega]] Vega :: +// tag::vega-def[] +A declarative language used to create interactive visualizations. +See {kibana-ref}/vega-graph.html[Vega]. +// end::vega-def[] + +[[glossary-vector]] vector data:: +// tag::vector-def[] +Points, lines, and polygons used to represent a map. +// end::vector-def[] + +[[glossary-visualization]] visualization :: +// tag::visualization-def[] +A graphical representation of query results in {kib} (e.g., a histogram, line graph, pie chart, or heat map). +// end::visualization-def[] + +[float] +[[w_glos]] +== W + +[[glossary-watcher]] Watcher :: +// tag::watcher-def[] +The original suite of alerting features. +See +{kibana-ref}/watcher-ui.html[Watcher]. +// end::watcher-def[] + +[[glossary-workpad]] workpad :: +// tag::workpad-def[] +A workspace where you build presentations of your live data in <>. +See +{kibana-ref}/create-canvas-workpad.html[Create a workpad]. +// end::workpad-def[] From 606eb6b3d864dda97edae08d932252b6e9b198db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Tue, 30 Jun 2020 16:35:52 +0200 Subject: [PATCH 19/23] [APM] Add API test for service maps (#70185) * [APM] Add API test for service maps * Re-add custom links test * Improved test names * Disable eslint rule * Undo readme changes * Fix ts errors --- .eslintrc.js | 1 + .../test/apm_api_integration/basic/config.ts | 1 - .../basic/tests/agent_configuration.ts | 1 - .../basic/tests/annotations.ts | 1 - .../basic/tests/custom_link.ts | 1 - .../basic/tests/feature_controls.ts | 1 - .../apm_api_integration/basic/tests/index.ts | 2 +- .../basic/tests/service_maps.ts | 25 + .../test/apm_api_integration/trial/config.ts | 1 - .../fixtures/es_archiver/8.0.0/data.json.gz | Bin 0 -> 193103 bytes .../fixtures/es_archiver/8.0.0/mappings.json | 25698 ++++++++++++++++ .../trial/tests/annotations.ts | 1 - .../apm_api_integration/trial/tests/index.ts | 2 +- .../trial/tests/service_maps.ts | 261 + 14 files changed, 25987 insertions(+), 9 deletions(-) create mode 100644 x-pack/test/apm_api_integration/basic/tests/service_maps.ts create mode 100644 x-pack/test/apm_api_integration/trial/fixtures/es_archiver/8.0.0/data.json.gz create mode 100644 x-pack/test/apm_api_integration/trial/fixtures/es_archiver/8.0.0/mappings.json create mode 100644 x-pack/test/apm_api_integration/trial/tests/service_maps.ts diff --git a/.eslintrc.js b/.eslintrc.js index 2c49bf78c67b1..8d979dc0f8645 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -334,6 +334,7 @@ module.exports = { */ { files: [ + 'x-pack/test/apm_api_integration/**/*.ts', 'x-pack/test/functional/apps/**/*.js', 'x-pack/plugins/apm/**/*.js', 'test/*/config.ts', diff --git a/x-pack/test/apm_api_integration/basic/config.ts b/x-pack/test/apm_api_integration/basic/config.ts index 541fe9ec023bc..03b8b21bf3232 100644 --- a/x-pack/test/apm_api_integration/basic/config.ts +++ b/x-pack/test/apm_api_integration/basic/config.ts @@ -6,7 +6,6 @@ import { createTestConfig } from '../common/config'; -// eslint-disable-next-line import/no-default-export export default createTestConfig({ license: 'basic', name: 'X-Pack APM API integration tests (basic)', diff --git a/x-pack/test/apm_api_integration/basic/tests/agent_configuration.ts b/x-pack/test/apm_api_integration/basic/tests/agent_configuration.ts index 9f39da2037f8e..7b99622cc4657 100644 --- a/x-pack/test/apm_api_integration/basic/tests/agent_configuration.ts +++ b/x-pack/test/apm_api_integration/basic/tests/agent_configuration.ts @@ -8,7 +8,6 @@ import expect from '@kbn/expect'; import { AgentConfigurationIntake } from '../../../../plugins/apm/common/agent_configuration/configuration_types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -// eslint-disable-next-line import/no-default-export export default function agentConfigurationTests({ getService }: FtrProviderContext) { const supertestRead = getService('supertestAsApmReadUser'); const supertestWrite = getService('supertestAsApmWriteUser'); diff --git a/x-pack/test/apm_api_integration/basic/tests/annotations.ts b/x-pack/test/apm_api_integration/basic/tests/annotations.ts index c522ebcfb5c65..e0659fe195f93 100644 --- a/x-pack/test/apm_api_integration/basic/tests/annotations.ts +++ b/x-pack/test/apm_api_integration/basic/tests/annotations.ts @@ -8,7 +8,6 @@ import expect from '@kbn/expect'; import { JsonObject } from 'src/plugins/kibana_utils/common'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -// eslint-disable-next-line import/no-default-export export default function annotationApiTests({ getService }: FtrProviderContext) { const supertestWrite = getService('supertestAsApmAnnotationsWriteUser'); diff --git a/x-pack/test/apm_api_integration/basic/tests/custom_link.ts b/x-pack/test/apm_api_integration/basic/tests/custom_link.ts index 77fdc83523ca6..ec93d2b3a3b41 100644 --- a/x-pack/test/apm_api_integration/basic/tests/custom_link.ts +++ b/x-pack/test/apm_api_integration/basic/tests/custom_link.ts @@ -8,7 +8,6 @@ import expect from '@kbn/expect'; import { CustomLink } from '../../../../plugins/apm/common/custom_link/custom_link_types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -// eslint-disable-next-line import/no-default-export export default function customLinksTests({ getService }: FtrProviderContext) { const supertestRead = getService('supertestAsApmReadUser'); const supertestWrite = getService('supertestAsApmWriteUser'); diff --git a/x-pack/test/apm_api_integration/basic/tests/feature_controls.ts b/x-pack/test/apm_api_integration/basic/tests/feature_controls.ts index 42cbef69abbec..400d0d294bf02 100644 --- a/x-pack/test/apm_api_integration/basic/tests/feature_controls.ts +++ b/x-pack/test/apm_api_integration/basic/tests/feature_controls.ts @@ -7,7 +7,6 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../common/ftr_provider_context'; -// eslint-disable-next-line import/no-default-export export default function featureControlsTests({ getService }: FtrProviderContext) { const supertest = getService('supertestAsApmWriteUser'); const supertestWithoutAuth = getService('supertestWithoutAuth'); diff --git a/x-pack/test/apm_api_integration/basic/tests/index.ts b/x-pack/test/apm_api_integration/basic/tests/index.ts index 7c7e5a8dd93cc..02185b0761c5b 100644 --- a/x-pack/test/apm_api_integration/basic/tests/index.ts +++ b/x-pack/test/apm_api_integration/basic/tests/index.ts @@ -5,7 +5,6 @@ */ import { FtrProviderContext } from '../../common/ftr_provider_context'; -// eslint-disable-next-line import/no-default-export export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderContext) { describe('APM specs (basic)', function () { this.tags('ciGroup1'); @@ -14,5 +13,6 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./agent_configuration')); loadTestFile(require.resolve('./custom_link')); + loadTestFile(require.resolve('./service_maps')); }); } diff --git a/x-pack/test/apm_api_integration/basic/tests/service_maps.ts b/x-pack/test/apm_api_integration/basic/tests/service_maps.ts new file mode 100644 index 0000000000000..64910d2b45632 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/service_maps.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +export default function serviceMapsApiTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('Service Maps', () => { + it('should only be available to users with Platinum license (or higher)', async () => { + const response = await supertest.get( + '/api/apm/service-map?start=2020-06-28T10%3A24%3A46.055Z&end=2020-06-29T10%3A24%3A46.055Z' + ); + + expect(response.status).to.be(403); + expect(response.body.message).to.be( + "In order to access Service Maps, you must be subscribed to an Elastic Platinum license. With it, you'll have the ability to visualize your entire application stack along with your APM data." + ); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/trial/config.ts b/x-pack/test/apm_api_integration/trial/config.ts index ca5b11d469c47..94a6f808603c1 100644 --- a/x-pack/test/apm_api_integration/trial/config.ts +++ b/x-pack/test/apm_api_integration/trial/config.ts @@ -6,7 +6,6 @@ import { createTestConfig } from '../common/config'; -// eslint-disable-next-line import/no-default-export export default createTestConfig({ license: 'trial', name: 'X-Pack APM API integration tests (trial)', diff --git a/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/8.0.0/data.json.gz b/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/8.0.0/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..e9360878b7bb724924c89bbaea7b243a555f13d3 GIT binary patch literal 193103 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
#xkG+Z- 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 { + describe('when there is no data', () => { + it('returns empty list', async () => { + const response = await supertest.get( + '/api/apm/service-map?start=2020-06-28T10%3A24%3A46.055Z&end=2020-06-29T10%3A24%3A46.055Z' + ); + + expect(response.status).to.be(200); + expect(response.body).to.eql({ elements: [] }); + }); + }); + + describe('when there is data', () => { + before(() => esArchiver.load('8.0.0')); + after(() => esArchiver.unload('8.0.0')); + + it('returns service map elements', async () => { + const response = await supertest.get( + '/api/apm/service-map?start=2020-06-28T10%3A24%3A46.055Z&end=2020-06-29T10%3A24%3A46.055Z' + ); + + expect(response.status).to.be(200); + + expect(response.body).to.eql({ + elements: [ + { + data: { + source: 'client', + target: 'opbeans-node', + id: 'client~opbeans-node', + sourceData: { + id: 'client', + 'service.name': 'client', + 'agent.name': 'rum-js', + }, + targetData: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, + }, + }, + { + data: { + source: 'opbeans-java', + target: '>opbeans-java:3000', + id: 'opbeans-java~>opbeans-java:3000', + sourceData: { + id: 'opbeans-java', + 'service.environment': 'production', + 'service.name': 'opbeans-java', + 'agent.name': 'java', + }, + targetData: { + 'span.subtype': 'http', + 'span.destination.service.resource': 'opbeans-java:3000', + 'span.type': 'external', + id: '>opbeans-java:3000', + label: 'opbeans-java:3000', + }, + }, + }, + { + data: { + source: 'opbeans-java', + target: '>postgresql', + id: 'opbeans-java~>postgresql', + sourceData: { + id: 'opbeans-java', + 'service.environment': 'production', + 'service.name': 'opbeans-java', + 'agent.name': 'java', + }, + targetData: { + 'span.subtype': 'postgresql', + 'span.destination.service.resource': 'postgresql', + 'span.type': 'db', + id: '>postgresql', + label: 'postgresql', + }, + }, + }, + { + data: { + source: 'opbeans-java', + target: 'opbeans-node', + id: 'opbeans-java~opbeans-node', + sourceData: { + id: 'opbeans-java', + 'service.environment': 'production', + 'service.name': 'opbeans-java', + 'agent.name': 'java', + }, + targetData: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, + bidirectional: true, + }, + }, + { + data: { + source: 'opbeans-node', + target: '>93.184.216.34:80', + id: 'opbeans-node~>93.184.216.34:80', + sourceData: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, + targetData: { + 'span.subtype': 'http', + 'span.destination.service.resource': '93.184.216.34:80', + 'span.type': 'external', + id: '>93.184.216.34:80', + label: '93.184.216.34:80', + }, + }, + }, + { + data: { + source: 'opbeans-node', + target: '>postgresql', + id: 'opbeans-node~>postgresql', + sourceData: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, + targetData: { + 'span.subtype': 'postgresql', + 'span.destination.service.resource': 'postgresql', + 'span.type': 'db', + id: '>postgresql', + label: 'postgresql', + }, + }, + }, + { + data: { + source: 'opbeans-node', + target: '>redis', + id: 'opbeans-node~>redis', + sourceData: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, + targetData: { + 'span.subtype': 'redis', + 'span.destination.service.resource': 'redis', + 'span.type': 'cache', + id: '>redis', + label: 'redis', + }, + }, + }, + { + data: { + source: 'opbeans-node', + target: 'opbeans-java', + id: 'opbeans-node~opbeans-java', + sourceData: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, + targetData: { + id: 'opbeans-java', + 'service.environment': 'production', + 'service.name': 'opbeans-java', + 'agent.name': 'java', + }, + isInverseEdge: true, + }, + }, + { + data: { + id: 'opbeans-java', + 'service.environment': 'production', + 'service.name': 'opbeans-java', + 'agent.name': 'java', + }, + }, + { + data: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, + }, + { + data: { + 'span.subtype': 'http', + 'span.destination.service.resource': 'opbeans-java:3000', + 'span.type': 'external', + id: '>opbeans-java:3000', + label: 'opbeans-java:3000', + }, + }, + { + data: { + id: 'client', + 'service.name': 'client', + 'agent.name': 'rum-js', + }, + }, + { + data: { + 'span.subtype': 'redis', + 'span.destination.service.resource': 'redis', + 'span.type': 'cache', + id: '>redis', + label: 'redis', + }, + }, + { + data: { + 'span.subtype': 'postgresql', + 'span.destination.service.resource': 'postgresql', + 'span.type': 'db', + id: '>postgresql', + label: 'postgresql', + }, + }, + { + data: { + 'span.subtype': 'http', + 'span.destination.service.resource': '93.184.216.34:80', + 'span.type': 'external', + id: '>93.184.216.34:80', + label: '93.184.216.34:80', + }, + }, + ], + }); + }); + }); + }); +} From 43bfa4ab66aae5f9c2605b7b819b3fa6aefc5104 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Tue, 30 Jun 2020 16:56:40 +0200 Subject: [PATCH 20/23] [ML] Modifies page title to Create job (#70191) Changes Create data frame analytics job to Create job. --- .../data_frame_analytics/pages/analytics_creation/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx index ff718277a88a7..e821428890046 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/page.tsx @@ -149,13 +149,13 @@ export const Page: FC = ({ jobId }) => { {jobId === undefined && ( )} {jobId !== undefined && ( )} From 04b8d108d5b6ed93527f0dfd04d634a0854fc321 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Tue, 30 Jun 2020 11:14:04 -0400 Subject: [PATCH 21/23] remove logs link and alerts count (#70282) --- .../view/details/host_details.tsx | 25 +------------------ .../pages/endpoint_hosts/view/hooks.ts | 16 ------------ .../pages/endpoint_hosts/view/index.test.tsx | 25 ------------------- 3 files changed, 1 insertion(+), 65 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx index 80c4e2f379c7c..66abf993770a7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx @@ -19,7 +19,7 @@ import React, { memo, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { HostMetadata } from '../../../../../../common/endpoint/types'; -import { useHostSelector, useHostLogsUrl, useAgentDetailsIngestUrl } from '../hooks'; +import { useHostSelector, useAgentDetailsIngestUrl } from '../hooks'; import { useNavigateToAppEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; import { policyResponseStatus, uiQueryParams } from '../../store/selectors'; import { POLICY_STATUS_TO_HEALTH_COLOR } from '../host_constants'; @@ -51,7 +51,6 @@ const LinkToExternalApp = styled.div` const openReassignFlyoutSearch = '?openReassignFlyout=true'; export const HostDetails = memo(({ details }: { details: HostMetadata }) => { - const { url: logsUrl, appId: logsAppId, appPath: logsAppPath } = useHostLogsUrl(details.host.id); const agentId = details.elastic.agent.id; const { url: agentDetailsUrl, @@ -78,12 +77,6 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { }), description: , }, - { - title: i18n.translate('xpack.securitySolution.endpoint.host.details.alerts', { - defaultMessage: 'Alerts', - }), - description: '0', - }, ]; }, [details]); @@ -251,22 +244,6 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { listItems={detailsResultsLower} data-test-subj="hostDetailsLowerList" /> - - - - - - - - ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts index c072c812edbb5..68198b691da40 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks.ts @@ -21,22 +21,6 @@ export function useHostSelector(selector: (state: HostState) => TSele }); } -/** - * Returns an object that contains Kibana Logs app and URL information for a given host id - * @param hostId - */ -export const useHostLogsUrl = (hostId: string): { url: string; appId: string; appPath: string } => { - const { services } = useKibana(); - return useMemo(() => { - const appPath = `/stream?logFilter=(expression:'host.id:${hostId}',kind:kuery)`; - return { - url: `${services.application.getUrlForApp('logs')}${appPath}`, - appId: 'logs', - appPath, - }; - }, [hostId, services.application]); -}; - /** * Returns an object that contains Ingest app and URL information */ diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 68943797ea07e..073e2a07457ff 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -278,7 +278,6 @@ describe('when on the hosts page', () => { agentId = hostDetails.metadata.elastic.agent.id; coreStart.http.get.mockReturnValue(Promise.resolve(hostDetails)); - coreStart.application.getUrlForApp.mockReturnValue('/app/logs'); reactTestingLibrary.act(() => { history.push({ @@ -433,30 +432,6 @@ describe('when on the hosts page', () => { }); }); - it('should include the link to logs', async () => { - const renderResult = render(); - const linkToLogs = await renderResult.findByTestId('hostDetailsLinkToLogs'); - expect(linkToLogs).not.toBeNull(); - expect(linkToLogs.textContent).toEqual('Endpoint Logs'); - expect(linkToLogs.getAttribute('href')).toEqual( - "/app/logs/stream?logFilter=(expression:'host.id:1',kind:kuery)" - ); - }); - - describe('when link to logs is clicked', () => { - beforeEach(async () => { - const renderResult = render(); - const linkToLogs = await renderResult.findByTestId('hostDetailsLinkToLogs'); - reactTestingLibrary.act(() => { - reactTestingLibrary.fireEvent.click(linkToLogs); - }); - }); - - it('should navigate to logs without full page refresh', () => { - expect(coreStart.application.navigateToApp.mock.calls).toHaveLength(1); - }); - }); - describe('when showing host Policy Response panel', () => { let renderResult: ReturnType; beforeEach(async () => { From 2fe0051ec28fa565374584c4ecf0a4a4164674c1 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Tue, 30 Jun 2020 10:14:29 -0500 Subject: [PATCH 22/23] Index patterns - Server API (#69105) * index patterns on the server --- ...-data-public.indexpattern._constructor_.md | 4 +- ...plugin-plugins-data-public.indexpattern.md | 2 +- ...c.indexpatternattributes.fieldformatmap.md | 11 ++ ...lic.indexpatternattributes.intervalname.md | 11 ++ ...gins-data-public.indexpatternattributes.md | 3 + ...ic.indexpatternattributes.sourcefilters.md | 11 ++ ...r.indexpatternattributes.fieldformatmap.md | 11 ++ ...ver.indexpatternattributes.intervalname.md | 11 ++ ...gins-data-server.indexpatternattributes.md | 3 + ...er.indexpatternattributes.sourcefilters.md | 11 ++ ...plugin-plugins-data-server.plugin.start.md | 6 + ...s-data-server.pluginstart.indexpatterns.md | 11 ++ ...-plugin-plugins-data-server.pluginstart.md | 1 + ... => stubbed_saved_object_index_pattern.ts} | 8 +- .../data/common/index_patterns/index.ts | 1 + .../index_patterns/_fields_fetcher.ts | 3 +- .../ensure_default_index_pattern.ts | 10 +- .../index_patterns/index_patterns/index.ts | 1 - .../index_patterns/index_pattern.test.ts | 15 ++- .../index_patterns/index_pattern.ts | 102 ++++++++++------ .../index_patterns/index_patterns.test.ts | 39 +++--- .../index_patterns/index_patterns.ts | 73 +++++++----- .../data/common/index_patterns/lib/errors.ts | 2 +- .../data/common/index_patterns/types.ts | 52 ++++++++ .../data/common/index_patterns/utils.ts | 14 +-- .../data/public/index_patterns/index.ts | 9 +- .../index_patterns/index_patterns/index.ts | 1 + .../index_patterns_api_client.test.mock.ts | 0 .../index_patterns_api_client.test.ts | 0 .../index_patterns_api_client.ts | 17 +-- .../saved_objects_client_wrapper.ts | 67 +++++++++++ .../index_patterns/ui_settings_wrapper.ts | 45 +++++++ src/plugins/data/public/plugin.ts | 14 ++- src/plugins/data/public/public.api.md | 18 +-- .../data/server/index_patterns/index.ts | 2 +- .../index_patterns_api_client.ts | 29 +++++ .../index_patterns/index_patterns_service.ts | 45 ++++++- .../data/server/index_patterns/mocks.ts | 24 ++++ .../saved_objects_client_wrapper.ts | 53 +++++++++ .../index_patterns/ui_settings_wrapper.ts | 43 +++++++ src/plugins/data/server/mocks.ts | 2 + src/plugins/data/server/plugin.ts | 20 +++- src/plugins/data/server/server.api.md | 17 +++ test/plugin_functional/config.js | 1 + .../plugins/index_patterns/kibana.json | 9 ++ .../plugins/index_patterns/package.json | 17 +++ .../plugins/index_patterns/server/index.ts | 30 +++++ .../plugins/index_patterns/server/plugin.ts | 111 ++++++++++++++++++ .../plugins/index_patterns/tsconfig.json | 14 +++ .../test_suites/data_plugin/index.ts | 25 ++++ .../test_suites/data_plugin/index_patterns.ts | 64 ++++++++++ .../security/security_index_pattern_utils.ts | 7 +- .../use_create_analytics_form.ts | 3 +- 53 files changed, 945 insertions(+), 158 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.fieldformatmap.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.intervalname.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.sourcefilters.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.fieldformatmap.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.intervalname.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.sourcefilters.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.pluginstart.indexpatterns.md rename src/fixtures/{stubbed_saved_object_index_pattern.js => stubbed_saved_object_index_pattern.ts} (87%) rename src/plugins/data/{common => public}/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts (100%) rename src/plugins/data/{common => public}/index_patterns/index_patterns/index_patterns_api_client.test.ts (100%) rename src/plugins/data/{common => public}/index_patterns/index_patterns/index_patterns_api_client.ts (87%) create mode 100644 src/plugins/data/public/index_patterns/saved_objects_client_wrapper.ts create mode 100644 src/plugins/data/public/index_patterns/ui_settings_wrapper.ts create mode 100644 src/plugins/data/server/index_patterns/index_patterns_api_client.ts create mode 100644 src/plugins/data/server/index_patterns/mocks.ts create mode 100644 src/plugins/data/server/index_patterns/saved_objects_client_wrapper.ts create mode 100644 src/plugins/data/server/index_patterns/ui_settings_wrapper.ts create mode 100644 test/plugin_functional/plugins/index_patterns/kibana.json create mode 100644 test/plugin_functional/plugins/index_patterns/package.json create mode 100644 test/plugin_functional/plugins/index_patterns/server/index.ts create mode 100644 test/plugin_functional/plugins/index_patterns/server/plugin.ts create mode 100644 test/plugin_functional/plugins/index_patterns/tsconfig.json create mode 100644 test/plugin_functional/test_suites/data_plugin/index.ts create mode 100644 test/plugin_functional/test_suites/data_plugin/index_patterns.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md index 6574e7ee37926..0268846772f2c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md @@ -9,7 +9,7 @@ Constructs a new instance of the `IndexPattern` class Signature: ```typescript -constructor(id: string | undefined, { getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, }: IndexPatternDeps); +constructor(id: string | undefined, { getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, uiSettingsValues, }: IndexPatternDeps); ``` ## Parameters @@ -17,5 +17,5 @@ constructor(id: string | undefined, { getConfig, savedObjectsClient, apiClient, | Parameter | Type | Description | | --- | --- | --- | | id | string | undefined | | -| { getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, } | IndexPatternDeps | | +| { getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, uiSettingsValues, } | IndexPatternDeps | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index d39b384c538f1..bc999a3bb48e3 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -14,7 +14,7 @@ export declare class IndexPattern implements IIndexPattern | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(id, { getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, })](./kibana-plugin-plugins-data-public.indexpattern._constructor_.md) | | Constructs a new instance of the IndexPattern class | +| [(constructor)(id, { getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, uiSettingsValues, })](./kibana-plugin-plugins-data-public.indexpattern._constructor_.md) | | Constructs a new instance of the IndexPattern class | ## Properties diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.fieldformatmap.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.fieldformatmap.md new file mode 100644 index 0000000000000..9a454feab1e0e --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.fieldformatmap.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternAttributes](./kibana-plugin-plugins-data-public.indexpatternattributes.md) > [fieldFormatMap](./kibana-plugin-plugins-data-public.indexpatternattributes.fieldformatmap.md) + +## IndexPatternAttributes.fieldFormatMap property + +Signature: + +```typescript +fieldFormatMap?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.intervalname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.intervalname.md new file mode 100644 index 0000000000000..5902496fcd0e7 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.intervalname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternAttributes](./kibana-plugin-plugins-data-public.indexpatternattributes.md) > [intervalName](./kibana-plugin-plugins-data-public.indexpatternattributes.intervalname.md) + +## IndexPatternAttributes.intervalName property + +Signature: + +```typescript +intervalName?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md index 39ae328c14501..eff2349f053ff 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md @@ -20,7 +20,10 @@ export interface IndexPatternAttributes | Property | Type | Description | | --- | --- | --- | +| [fieldFormatMap](./kibana-plugin-plugins-data-public.indexpatternattributes.fieldformatmap.md) | string | | | [fields](./kibana-plugin-plugins-data-public.indexpatternattributes.fields.md) | string | | +| [intervalName](./kibana-plugin-plugins-data-public.indexpatternattributes.intervalname.md) | string | | +| [sourceFilters](./kibana-plugin-plugins-data-public.indexpatternattributes.sourcefilters.md) | string | | | [timeFieldName](./kibana-plugin-plugins-data-public.indexpatternattributes.timefieldname.md) | string | | | [title](./kibana-plugin-plugins-data-public.indexpatternattributes.title.md) | string | | | [type](./kibana-plugin-plugins-data-public.indexpatternattributes.type.md) | string | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.sourcefilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.sourcefilters.md new file mode 100644 index 0000000000000..43966112b97c3 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.sourcefilters.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternAttributes](./kibana-plugin-plugins-data-public.indexpatternattributes.md) > [sourceFilters](./kibana-plugin-plugins-data-public.indexpatternattributes.sourcefilters.md) + +## IndexPatternAttributes.sourceFilters property + +Signature: + +```typescript +sourceFilters?: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.fieldformatmap.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.fieldformatmap.md new file mode 100644 index 0000000000000..84cc8c705ff59 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.fieldformatmap.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternAttributes](./kibana-plugin-plugins-data-server.indexpatternattributes.md) > [fieldFormatMap](./kibana-plugin-plugins-data-server.indexpatternattributes.fieldformatmap.md) + +## IndexPatternAttributes.fieldFormatMap property + +Signature: + +```typescript +fieldFormatMap?: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.intervalname.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.intervalname.md new file mode 100644 index 0000000000000..77a0872546679 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.intervalname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternAttributes](./kibana-plugin-plugins-data-server.indexpatternattributes.md) > [intervalName](./kibana-plugin-plugins-data-server.indexpatternattributes.intervalname.md) + +## IndexPatternAttributes.intervalName property + +Signature: + +```typescript +intervalName?: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md index 1fcc49796f59e..4a5b61f5c179b 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md @@ -20,7 +20,10 @@ export interface IndexPatternAttributes | Property | Type | Description | | --- | --- | --- | +| [fieldFormatMap](./kibana-plugin-plugins-data-server.indexpatternattributes.fieldformatmap.md) | string | | | [fields](./kibana-plugin-plugins-data-server.indexpatternattributes.fields.md) | string | | +| [intervalName](./kibana-plugin-plugins-data-server.indexpatternattributes.intervalname.md) | string | | +| [sourceFilters](./kibana-plugin-plugins-data-server.indexpatternattributes.sourcefilters.md) | string | | | [timeFieldName](./kibana-plugin-plugins-data-server.indexpatternattributes.timefieldname.md) | string | | | [title](./kibana-plugin-plugins-data-server.indexpatternattributes.title.md) | string | | | [type](./kibana-plugin-plugins-data-server.indexpatternattributes.type.md) | string | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.sourcefilters.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.sourcefilters.md new file mode 100644 index 0000000000000..10223a6353f10 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.sourcefilters.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternAttributes](./kibana-plugin-plugins-data-server.indexpatternattributes.md) > [sourceFilters](./kibana-plugin-plugins-data-server.indexpatternattributes.sourcefilters.md) + +## IndexPatternAttributes.sourceFilters property + +Signature: + +```typescript +sourceFilters?: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md index 2c7a833ab641b..74bffc516725f 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -12,6 +12,9 @@ start(core: CoreStart): { fieldFormats: { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; + indexPatterns: { + indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; + }; }; ``` @@ -28,5 +31,8 @@ start(core: CoreStart): { fieldFormats: { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; + indexPatterns: { + indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; + }; }` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.pluginstart.indexpatterns.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.pluginstart.indexpatterns.md new file mode 100644 index 0000000000000..02ed24e05bc10 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.pluginstart.indexpatterns.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [PluginStart](./kibana-plugin-plugins-data-server.pluginstart.md) > [indexPatterns](./kibana-plugin-plugins-data-server.pluginstart.indexpatterns.md) + +## PluginStart.indexPatterns property + +Signature: + +```typescript +indexPatterns: IndexPatternsServiceStart; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.pluginstart.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.pluginstart.md index 1377d82123d41..b878a179657ed 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.pluginstart.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.pluginstart.md @@ -15,5 +15,6 @@ export interface DataPluginStart | Property | Type | Description | | --- | --- | --- | | [fieldFormats](./kibana-plugin-plugins-data-server.pluginstart.fieldformats.md) | FieldFormatsStart | | +| [indexPatterns](./kibana-plugin-plugins-data-server.pluginstart.indexpatterns.md) | IndexPatternsServiceStart | | | [search](./kibana-plugin-plugins-data-server.pluginstart.search.md) | ISearchStart | | diff --git a/src/fixtures/stubbed_saved_object_index_pattern.js b/src/fixtures/stubbed_saved_object_index_pattern.ts similarity index 87% rename from src/fixtures/stubbed_saved_object_index_pattern.js rename to src/fixtures/stubbed_saved_object_index_pattern.ts index 8e0e230ef33dd..02e6cb85e341f 100644 --- a/src/fixtures/stubbed_saved_object_index_pattern.js +++ b/src/fixtures/stubbed_saved_object_index_pattern.ts @@ -17,13 +17,13 @@ * under the License. */ +// @ts-expect-error import stubbedLogstashFields from './logstash_fields'; -import { SimpleSavedObject } from '../core/public'; const mockLogstashFields = stubbedLogstashFields(); -export function stubbedSavedObjectIndexPattern(id) { - return new SimpleSavedObject(undefined, { +export function stubbedSavedObjectIndexPattern(id: string | null = null) { + return { id, type: 'index-pattern', attributes: { @@ -32,5 +32,5 @@ export function stubbedSavedObjectIndexPattern(id) { fields: mockLogstashFields, }, version: 2, - }); + }; } diff --git a/src/plugins/data/common/index_patterns/index.ts b/src/plugins/data/common/index_patterns/index.ts index d26587efccc0f..51a642b775c29 100644 --- a/src/plugins/data/common/index_patterns/index.ts +++ b/src/plugins/data/common/index_patterns/index.ts @@ -19,3 +19,4 @@ export * from './fields'; export * from './types'; +export { IndexPatternsService } from './index_patterns'; diff --git a/src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts b/src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts index 727c4d445688d..baeb1587d57a9 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts @@ -17,7 +17,8 @@ * under the License. */ -import { GetFieldsOptions, IIndexPatternsApiClient, IndexPattern } from '.'; +import { IndexPattern } from '.'; +import { GetFieldsOptions, IIndexPatternsApiClient } from '../types'; /** @internal */ export const createFieldsFetcher = ( diff --git a/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts index 2737627bf1977..26f1a185ada3a 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts @@ -18,13 +18,13 @@ */ import { contains } from 'lodash'; -import { CoreStart } from 'kibana/public'; import { IndexPatternsContract } from './index_patterns'; +import { UiSettingsCommon } from '../types'; export type EnsureDefaultIndexPattern = () => Promise | undefined; export const createEnsureDefaultIndexPattern = ( - uiSettings: CoreStart['uiSettings'], + uiSettings: UiSettingsCommon, onRedirectNoIndexPattern: () => Promise | void ) => { /** @@ -33,12 +33,12 @@ export const createEnsureDefaultIndexPattern = ( */ return async function ensureDefaultIndexPattern(this: IndexPatternsContract) { const patterns = await this.getIds(); - let defaultId = uiSettings.get('defaultIndex'); + let defaultId = await uiSettings.get('defaultIndex'); let defined = !!defaultId; const exists = contains(patterns, defaultId); if (defined && !exists) { - uiSettings.remove('defaultIndex'); + await uiSettings.remove('defaultIndex'); defaultId = defined = false; } @@ -49,7 +49,7 @@ export const createEnsureDefaultIndexPattern = ( // If there is any index pattern created, set the first as default if (patterns.length >= 1) { defaultId = patterns[0]; - uiSettings.set('defaultIndex', defaultId); + await uiSettings.set('defaultIndex', defaultId); } else { return onRedirectNoIndexPattern(); } diff --git a/src/plugins/data/common/index_patterns/index_patterns/index.ts b/src/plugins/data/common/index_patterns/index_patterns/index.ts index 77527857ed0ca..31cd06b7dd0ea 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index.ts @@ -17,7 +17,6 @@ * under the License. */ -export * from './index_patterns_api_client'; export * from './_pattern_cache'; export * from './flatten_hit'; export * from './format_hit'; diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts index ba8e4f6fb3695..7fb1210fe1f32 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts @@ -66,7 +66,7 @@ const savedObjectsClient = { create: jest.fn(), get: jest.fn().mockImplementation(() => object), update: jest.fn().mockImplementation(async (type, id, body, { version }) => { - if (object._version !== version) { + if (object.version !== version) { throw new Object({ res: { status: 409, @@ -74,10 +74,10 @@ const savedObjectsClient = { }); } object.attributes.title = body.title; - object._version += 'a'; + object.version += 'a'; return { - id: object._id, - _version: object._version, + id: object.id, + version: object.version, }; }), }; @@ -109,6 +109,7 @@ function create(id: string, payload?: any): Promise { fieldFormats: fieldFormatsMock, onNotification: () => {}, onError: () => {}, + uiSettingsValues: { shortDotsEnable: false, metaFields: [] }, }); setDocsourcePayload(id, payload); @@ -382,8 +383,8 @@ describe('IndexPattern', () => { test('should handle version conflicts', async () => { setDocsourcePayload(null, { - _id: 'foo', - _version: 'foo', + id: 'foo', + version: 'foo', attributes: { title: 'something', }, @@ -397,6 +398,7 @@ describe('IndexPattern', () => { fieldFormats: fieldFormatsMock, onNotification: () => {}, onError: () => {}, + uiSettingsValues: { shortDotsEnable: false, metaFields: [] }, }); await pattern.init(); @@ -411,6 +413,7 @@ describe('IndexPattern', () => { fieldFormats: fieldFormatsMock, onNotification: () => {}, onError: () => {}, + uiSettingsValues: { shortDotsEnable: false, metaFields: [] }, }); await samePattern.init(); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index e9ac5a09b9db3..bde550c660a32 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -19,25 +19,23 @@ import _, { each, reject } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { SavedObjectsClientContract } from 'src/core/public'; -import { SavedObjectAttributes } from 'src/core/public'; +import { SavedObjectsClientCommon } from '../..'; import { DuplicateField, SavedObjectNotFound } from '../../../../kibana_utils/common'; -import { - ES_FIELD_TYPES, - KBN_FIELD_TYPES, - IIndexPattern, - IFieldType, - UI_SETTINGS, -} from '../../../common'; +import { ES_FIELD_TYPES, KBN_FIELD_TYPES, IIndexPattern, IFieldType } from '../../../common'; import { findByTitle } from '../utils'; import { IndexPatternMissingIndices } from '../lib'; import { Field, IIndexPatternFieldList, getIndexPatternFieldListCreator } from '../fields'; import { createFieldsFetcher } from './_fields_fetcher'; import { formatHitProvider } from './format_hit'; import { flattenHitWrapper } from './flatten_hit'; -import { IIndexPatternsApiClient } from '.'; -import { OnNotification, OnError } from '../types'; +import { + OnNotification, + OnError, + UiSettingsCommon, + IIndexPatternsApiClient, + IndexPatternAttributes, +} from '../types'; import { FieldFormatsStartCommon } from '../../field_formats'; import { PatternCache } from './_pattern_cache'; import { expandShorthand, FieldMappingSpec, MappingObject } from '../../field_mapping'; @@ -45,16 +43,22 @@ import { IndexPatternSpec, TypeMeta, FieldSpec, SourceFilter } from '../types'; import { SerializedFieldFormat } from '../../../../expressions/common'; const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3; -const type = 'index-pattern'; +const savedObjectType = 'index-pattern'; +interface IUiSettingsValues { + [key: string]: any; + shortDotsEnable: any; + metaFields: any; +} interface IndexPatternDeps { - getConfig: any; - savedObjectsClient: SavedObjectsClientContract; + getConfig: UiSettingsCommon['get']; + savedObjectsClient: SavedObjectsClientCommon; apiClient: IIndexPatternsApiClient; patternCache: PatternCache; fieldFormats: FieldFormatsStartCommon; onNotification: OnNotification; onError: OnError; + uiSettingsValues: IUiSettingsValues; } export class IndexPattern implements IIndexPattern { @@ -72,9 +76,9 @@ export class IndexPattern implements IIndexPattern { public metaFields: string[]; private version: string | undefined; - private savedObjectsClient: SavedObjectsClientContract; + private savedObjectsClient: SavedObjectsClientCommon; private patternCache: PatternCache; - private getConfig: any; + private getConfig: UiSettingsCommon['get']; private sourceFilters?: SourceFilter[]; private originalBody: { [key: string]: any } = {}; public fieldsFetcher: any; // probably want to factor out any direct usage and change to private @@ -83,6 +87,7 @@ export class IndexPattern implements IIndexPattern { private onNotification: OnNotification; private onError: OnError; private apiClient: IIndexPatternsApiClient; + private uiSettingsValues: IUiSettingsValues; private mapping: MappingObject = expandShorthand({ title: ES_FIELD_TYPES.TEXT, @@ -116,6 +121,7 @@ export class IndexPattern implements IIndexPattern { fieldFormats, onNotification, onError, + uiSettingsValues, }: IndexPatternDeps ) { this.id = id; @@ -127,9 +133,10 @@ export class IndexPattern implements IIndexPattern { this.fieldFormats = fieldFormats; this.onNotification = onNotification; this.onError = onError; + this.uiSettingsValues = uiSettingsValues; - this.shortDotsEnable = this.getConfig(UI_SETTINGS.SHORT_DOTS_ENABLE); - this.metaFields = this.getConfig(UI_SETTINGS.META_FIELDS); + this.shortDotsEnable = uiSettingsValues.shortDotsEnable; + this.metaFields = uiSettingsValues.metaFields; this.createFieldList = getIndexPatternFieldListCreator({ fieldFormats, @@ -138,12 +145,8 @@ export class IndexPattern implements IIndexPattern { this.fields = this.createFieldList(this, [], this.shortDotsEnable); this.apiClient = apiClient; - this.fieldsFetcher = createFieldsFetcher( - this, - apiClient, - this.getConfig(UI_SETTINGS.META_FIELDS) - ); - this.flattenHit = flattenHitWrapper(this, this.getConfig(UI_SETTINGS.META_FIELDS)); + this.fieldsFetcher = createFieldsFetcher(this, apiClient, uiSettingsValues.metaFields); + this.flattenHit = flattenHitWrapper(this, uiSettingsValues.metaFields); this.formatHit = formatHitProvider( this, fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.STRING) @@ -160,7 +163,13 @@ export class IndexPattern implements IIndexPattern { private deserializeFieldFormatMap(mapping: any) { const FieldFormat = this.fieldFormats.getType(mapping.id); - return FieldFormat && new FieldFormat(mapping.params, this.getConfig); + return ( + FieldFormat && + new FieldFormat( + mapping.params, + (key: string) => this.uiSettingsValues[key]?.userValue || this.uiSettingsValues[key]?.value + ) + ); } private initFields(input?: any) { @@ -228,7 +237,7 @@ export class IndexPattern implements IIndexPattern { private updateFromElasticSearch(response: any, forceFieldRefresh: boolean = false) { if (!response.found) { - throw new SavedObjectNotFound(type, this.id, 'management/kibana/indexPatterns'); + throw new SavedObjectNotFound(savedObjectType, this.id, 'management/kibana/indexPatterns'); } _.forOwn(this.mapping, (fieldMapping: FieldMappingSpec, name: string | undefined) => { @@ -296,12 +305,22 @@ export class IndexPattern implements IIndexPattern { return this; // no id === no elasticsearch document } - const savedObject = await this.savedObjectsClient.get(type, this.id); + const savedObject = await this.savedObjectsClient.get( + savedObjectType, + this.id + ); const response = { - version: savedObject._version, - found: savedObject._version ? true : false, - ...(_.cloneDeep(savedObject.attributes) as SavedObjectAttributes), + version: savedObject.version, + found: savedObject.version ? true : false, + title: savedObject.attributes.title, + timeFieldName: savedObject.attributes.timeFieldName, + intervalName: savedObject.attributes.intervalName, + fields: savedObject.attributes.fields, + sourceFilters: savedObject.attributes.sourceFilters, + fieldFormatMap: savedObject.attributes.fieldFormatMap, + typeMeta: savedObject.attributes.typeMeta, + type: savedObject.attributes.type, }; // Do this before we attempt to update from ES since that call can potentially perform a save this.originalBody = this.prepBody(); @@ -388,10 +407,10 @@ export class IndexPattern implements IIndexPattern { field.count = count; try { - const res = await this.savedObjectsClient.update(type, this.id, this.prepBody(), { + const res = await this.savedObjectsClient.update(savedObjectType, this.id, this.prepBody(), { version: this.version, }); - this.version = res._version; + this.version = res.version; } catch (e) { // no need for an error message here } @@ -462,13 +481,17 @@ export class IndexPattern implements IIndexPattern { fieldFormats: this.fieldFormats, onNotification: this.onNotification, onError: this.onError, + uiSettingsValues: { + shortDotsEnable: this.shortDotsEnable, + metaFields: this.metaFields, + }, }); await duplicatePattern.destroy(); } const body = this.prepBody(); - const response = await this.savedObjectsClient.create(type, body, { id: this.id }); + const response = await this.savedObjectsClient.create(savedObjectType, body, { id: this.id }); this.id = response.id; return response.id; @@ -496,10 +519,10 @@ export class IndexPattern implements IIndexPattern { (key) => body[key] !== this.originalBody[key] ); return this.savedObjectsClient - .update(type, this.id, body, { version: this.version }) - .then((resp: any) => { + .update(savedObjectType, this.id, body, { version: this.version }) + .then((resp) => { this.id = resp.id; - this.version = resp._version; + this.version = resp.version; }) .catch((err) => { if ( @@ -514,7 +537,12 @@ export class IndexPattern implements IIndexPattern { fieldFormats: this.fieldFormats, onNotification: this.onNotification, onError: this.onError, + uiSettingsValues: { + shortDotsEnable: this.shortDotsEnable, + metaFields: this.metaFields, + }, }); + return samePattern.init().then(() => { // What keys changed from now and what the server returned const updatedBody = samePattern.prepBody(); @@ -610,7 +638,7 @@ export class IndexPattern implements IIndexPattern { destroy() { if (this.id) { this.patternCache.clear(this.id); - return this.savedObjectsClient.delete(type, this.id); + return this.savedObjectsClient.delete(savedObjectType, this.id); } } } diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts index b0ecfc89d376b..2eb9744fc16b3 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts @@ -19,12 +19,14 @@ // eslint-disable-next-line max-classes-per-file import { IndexPatternsService } from './index_patterns'; -import { SavedObjectsClientContract, SavedObjectsFindResponsePublic } from 'kibana/public'; -import { coreMock, httpServiceMock } from '../../../../../core/public/mocks'; import { fieldFormatsMock } from '../../field_formats/mocks'; +import { + UiSettingsCommon, + IIndexPatternsApiClient, + SavedObjectsClientCommon, + SavedObject, +} from '../types'; -const core = coreMock.createStart(); -const http = httpServiceMock.createStartContract(); const fieldFormats = fieldFormatsMock; jest.mock('./index_pattern', () => { @@ -39,33 +41,26 @@ jest.mock('./index_pattern', () => { }; }); -jest.mock('./index_patterns_api_client', () => { - class IndexPatternsApiClient { - getFieldsForWildcard = async () => ({}); - } - - return { - IndexPatternsApiClient, - }; -}); - describe('IndexPatterns', () => { let indexPatterns: IndexPatternsService; - let savedObjectsClient: SavedObjectsClientContract; + let savedObjectsClient: SavedObjectsClientCommon; beforeEach(() => { - savedObjectsClient = {} as SavedObjectsClientContract; + savedObjectsClient = {} as SavedObjectsClientCommon; savedObjectsClient.find = jest.fn( () => - Promise.resolve({ - savedObjects: [{ id: 'id', attributes: { title: 'title' } }], - }) as Promise> + Promise.resolve([{ id: 'id', attributes: { title: 'title' } }]) as Promise< + Array> + > ); indexPatterns = new IndexPatternsService({ - uiSettings: core.uiSettings, - savedObjectsClient, - http, + uiSettings: ({ + get: () => Promise.resolve(false), + getAll: () => {}, + } as any) as UiSettingsCommon, + savedObjectsClient: (savedObjectsClient as unknown) as SavedObjectsClientCommon, + apiClient: {} as IIndexPatternsApiClient, fieldFormats, onNotification: () => {}, onError: () => {}, diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 5e51897d13372..ef03ca8fe2d14 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -17,25 +17,26 @@ * under the License. */ -import { - SavedObjectsClientContract, - SimpleSavedObject, - IUiSettingsClient, - HttpStart, - CoreStart, -} from 'src/core/public'; +import { SavedObjectsClientCommon } from '../..'; import { createIndexPatternCache } from '.'; import { IndexPattern } from './index_pattern'; -import { IndexPatternsApiClient, GetFieldsOptions } from '.'; import { createEnsureDefaultIndexPattern, EnsureDefaultIndexPattern, } from './ensure_default_index_pattern'; import { getIndexPatternFieldListCreator, CreateIndexPatternFieldList, Field } from '../fields'; -import { IndexPatternSpec, FieldSpec } from '../types'; -import { OnNotification, OnError } from '../types'; +import { + OnNotification, + OnError, + UiSettingsCommon, + IIndexPatternsApiClient, + GetFieldsOptions, + FieldSpec, + IndexPatternSpec, +} from '../types'; import { FieldFormatsStartCommon } from '../../field_formats'; +import { UI_SETTINGS, SavedObject } from '../../../common'; const indexPatternCache = createIndexPatternCache(); @@ -46,20 +47,20 @@ export interface IndexPatternSavedObjectAttrs { } interface IndexPatternsServiceDeps { - uiSettings: CoreStart['uiSettings']; - savedObjectsClient: SavedObjectsClientContract; - http: HttpStart; + uiSettings: UiSettingsCommon; + savedObjectsClient: SavedObjectsClientCommon; + apiClient: IIndexPatternsApiClient; fieldFormats: FieldFormatsStartCommon; onNotification: OnNotification; onError: OnError; - onRedirectNoIndexPattern: () => void; + onRedirectNoIndexPattern?: () => void; } export class IndexPatternsService { - private config: IUiSettingsClient; - private savedObjectsClient: SavedObjectsClientContract; - private savedObjectsCache?: Array> | null; - private apiClient: IndexPatternsApiClient; + private config: UiSettingsCommon; + private savedObjectsClient: SavedObjectsClientCommon; + private savedObjectsCache?: Array> | null; + private apiClient: IIndexPatternsApiClient; private fieldFormats: FieldFormatsStartCommon; private onNotification: OnNotification; private onError: OnError; @@ -74,13 +75,13 @@ export class IndexPatternsService { constructor({ uiSettings, savedObjectsClient, - http, + apiClient, fieldFormats, onNotification, onError, - onRedirectNoIndexPattern, + onRedirectNoIndexPattern = () => {}, }: IndexPatternsServiceDeps) { - this.apiClient = new IndexPatternsApiClient(http); + this.apiClient = apiClient; this.config = uiSettings; this.savedObjectsClient = savedObjectsClient; this.fieldFormats = fieldFormats; @@ -103,13 +104,11 @@ export class IndexPatternsService { } private async refreshSavedObjectsCache() { - this.savedObjectsCache = ( - await this.savedObjectsClient.find({ - type: 'index-pattern', - fields: ['title'], - perPage: 10000, - }) - ).savedObjects; + this.savedObjectsCache = await this.savedObjectsClient.find({ + type: 'index-pattern', + fields: ['title'], + perPage: 10000, + }); } getIds = async (refresh: boolean = false) => { @@ -172,7 +171,7 @@ export class IndexPatternsService { }; getDefault = async () => { - const defaultIndexPatternId = this.config.get('defaultIndex'); + const defaultIndexPatternId = await this.config.get('defaultIndex'); if (defaultIndexPatternId) { return await this.get(defaultIndexPatternId); } @@ -191,7 +190,11 @@ export class IndexPatternsService { return indexPatternCache.set(id, indexPattern); }; - specToIndexPattern(spec: IndexPatternSpec) { + async specToIndexPattern(spec: IndexPatternSpec) { + const shortDotsEnable = await this.config.get(UI_SETTINGS.SHORT_DOTS_ENABLE); + const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS); + const uiSettingsValues = await this.config.getAll(); + const indexPattern = new IndexPattern(spec.id, { getConfig: (cfg: any) => this.config.get(cfg), savedObjectsClient: this.savedObjectsClient, @@ -200,13 +203,18 @@ export class IndexPatternsService { fieldFormats: this.fieldFormats, onNotification: this.onNotification, onError: this.onError, + uiSettingsValues: { ...uiSettingsValues, shortDotsEnable, metaFields }, }); indexPattern.initFromSpec(spec); return indexPattern; } - make = (id?: string): Promise => { + async make(id?: string): Promise { + const shortDotsEnable = await this.config.get(UI_SETTINGS.SHORT_DOTS_ENABLE); + const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS); + const uiSettingsValues = await this.config.getAll(); + const indexPattern = new IndexPattern(id, { getConfig: (cfg: any) => this.config.get(cfg), savedObjectsClient: this.savedObjectsClient, @@ -215,10 +223,11 @@ export class IndexPatternsService { fieldFormats: this.fieldFormats, onNotification: this.onNotification, onError: this.onError, + uiSettingsValues: { ...uiSettingsValues, shortDotsEnable, metaFields }, }); return indexPattern.init(); - }; + } } export type IndexPatternsContract = PublicMethodsOf; diff --git a/src/plugins/data/common/index_patterns/lib/errors.ts b/src/plugins/data/common/index_patterns/lib/errors.ts index 12efab7a2ca40..59019000f1924 100644 --- a/src/plugins/data/common/index_patterns/lib/errors.ts +++ b/src/plugins/data/common/index_patterns/lib/errors.ts @@ -19,7 +19,7 @@ /* eslint-disable */ -import { KbnError } from '../../../../kibana_utils/public'; +import { KbnError } from '../../../../kibana_utils/common/'; /** * Tried to call a method that relies on SearchSource having an indexPattern assigned diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 94121a274d686..4241df5718243 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -18,6 +18,8 @@ */ import { ToastInputFields, ErrorToastOptions } from 'src/core/public/notifications'; +// eslint-disable-next-line +import type { SavedObject } from 'src/core/server'; import { IFieldType } from './fields'; import { SerializedFieldFormat } from '../../../expressions/common'; import { KBN_FIELD_TYPES } from '..'; @@ -49,11 +51,61 @@ export interface IndexPatternAttributes { title: string; typeMeta: string; timeFieldName?: string; + intervalName?: string; + sourceFilters?: string; + fieldFormatMap?: string; } export type OnNotification = (toastInputFields: ToastInputFields) => void; export type OnError = (error: Error, toastInputFields: ErrorToastOptions) => void; +export interface UiSettingsCommon { + get: (key: string) => Promise; + getAll: () => Promise>; + set: (key: string, value: any) => Promise; + remove: (key: string) => Promise; +} + +export interface SavedObjectsClientCommonFindArgs { + type: string | string[]; + fields?: string[]; + perPage?: number; + search?: string; + searchFields?: string[]; +} + +export interface SavedObjectsClientCommon { + find: (options: SavedObjectsClientCommonFindArgs) => Promise>>; + get: (type: string, id: string) => Promise>; + update: ( + type: string, + id: string, + attributes: Record, + options: Record + ) => Promise; + create: ( + type: string, + attributes: Record, + options: Record + ) => Promise; + delete: (type: string, id: string) => Promise<{}>; +} + +export interface GetFieldsOptions { + pattern?: string; + type?: string; + params?: any; + lookBack?: boolean; + metaFields?: string; +} + +export interface IIndexPatternsApiClient { + getFieldsForTimePattern: (options: GetFieldsOptions) => Promise; + getFieldsForWildcard: (options: GetFieldsOptions) => Promise; +} + +export type { SavedObject }; + export type AggregationRestrictions = Record< string, { diff --git a/src/plugins/data/common/index_patterns/utils.ts b/src/plugins/data/common/index_patterns/utils.ts index c3f9af62f8c0e..d9e1cfa0d952a 100644 --- a/src/plugins/data/common/index_patterns/utils.ts +++ b/src/plugins/data/common/index_patterns/utils.ts @@ -18,24 +18,24 @@ */ import { find } from 'lodash'; -import { SavedObjectsClientContract, SimpleSavedObject } from 'src/core/public'; +import { SavedObjectsClientCommon, SavedObject } from '..'; /** * Returns an object matching a given title * - * @param client {SavedObjectsClientContract} + * @param client {SavedObjectsClientCommon} * @param title {string} - * @returns {Promise} + * @returns {Promise} */ export async function findByTitle( - client: SavedObjectsClientContract, + client: SavedObjectsClientCommon, title: string -): Promise | void> { +): Promise | void> { if (!title) { return Promise.resolve(); } - const { savedObjects } = await client.find({ + const savedObjects = await client.find({ type: 'index-pattern', perPage: 10, search: `"${title}"`, @@ -45,6 +45,6 @@ export async function findByTitle( return find( savedObjects, - (obj: SimpleSavedObject) => obj.get('title').toLowerCase() === title.toLowerCase() + (obj: SavedObject) => obj.attributes.title.toLowerCase() === title.toLowerCase() ); } diff --git a/src/plugins/data/public/index_patterns/index.ts b/src/plugins/data/public/index_patterns/index.ts index 2c540527f468d..a6ee71c624f5a 100644 --- a/src/plugins/data/public/index_patterns/index.ts +++ b/src/plugins/data/public/index_patterns/index.ts @@ -34,4 +34,11 @@ export { IIndexPatternFieldList, } from '../../common/index_patterns'; -export { IndexPatternsService, IndexPatternsContract, IndexPattern } from './index_patterns'; +export { + IndexPatternsService, + IndexPatternsContract, + IndexPattern, + IndexPatternsApiClient, +} from './index_patterns'; +export { UiSettingsPublicToCommon } from './ui_settings_wrapper'; +export { SavedObjectsClientPublicToCommon } from './saved_objects_client_wrapper'; diff --git a/src/plugins/data/public/index_patterns/index_patterns/index.ts b/src/plugins/data/public/index_patterns/index_patterns/index.ts index 0db1c8c68b4ac..f63b48f877771 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index.ts @@ -19,3 +19,4 @@ export * from '../../../common/index_patterns/index_patterns'; export * from './redirect_no_index_pattern'; +export * from './index_patterns_api_client'; diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts similarity index 100% rename from src/plugins/data/common/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts rename to src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.mock.ts diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns_api_client.test.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.ts similarity index 100% rename from src/plugins/data/common/index_patterns/index_patterns/index_patterns_api_client.test.ts rename to src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.test.ts diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns_api_client.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts similarity index 87% rename from src/plugins/data/common/index_patterns/index_patterns/index_patterns_api_client.ts rename to src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts index cd189ccf0135b..377a3f7f91a50 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns_api_client.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts @@ -18,21 +18,12 @@ */ import { HttpSetup } from 'src/core/public'; -import { IndexPatternMissingIndices } from '../lib'; +import { IndexPatternMissingIndices } from '../../../common/index_patterns/lib'; +import { GetFieldsOptions, IIndexPatternsApiClient } from '../../../common/index_patterns/types'; const API_BASE_URL: string = `/api/index_patterns/`; -export interface GetFieldsOptions { - pattern?: string; - type?: string; - params?: any; - lookBack?: boolean; - metaFields?: string; -} - -export type IIndexPatternsApiClient = PublicMethodsOf; - -export class IndexPatternsApiClient { +export class IndexPatternsApiClient implements IIndexPatternsApiClient { private http: HttpSetup; constructor(http: HttpSetup) { @@ -53,7 +44,7 @@ export class IndexPatternsApiClient { }); } - _getUrl(path: string[]) { + private _getUrl(path: string[]) { return API_BASE_URL + path.filter(Boolean).map(encodeURIComponent).join('/'); } diff --git a/src/plugins/data/public/index_patterns/saved_objects_client_wrapper.ts b/src/plugins/data/public/index_patterns/saved_objects_client_wrapper.ts new file mode 100644 index 0000000000000..8f1d02c5ffd54 --- /dev/null +++ b/src/plugins/data/public/index_patterns/saved_objects_client_wrapper.ts @@ -0,0 +1,67 @@ +/* + * 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 { omit } from 'lodash'; +import { SavedObjectsClient, SimpleSavedObject } from 'src/core/public'; +import { + SavedObjectsClientCommon, + SavedObjectsClientCommonFindArgs, + SavedObject, +} from '../../common/index_patterns'; + +type SOClient = Pick; + +const simpleSavedObjectToSavedObject = ( + simpleSavedObject: SimpleSavedObject +): SavedObject => ({ + version: simpleSavedObject._version, + ...omit(simpleSavedObject, '_version'), +}); + +export class SavedObjectsClientPublicToCommon implements SavedObjectsClientCommon { + private savedObjectClient: SOClient; + constructor(savedObjectClient: SOClient) { + this.savedObjectClient = savedObjectClient; + } + async find(options: SavedObjectsClientCommonFindArgs) { + const response = (await this.savedObjectClient.find(options)).savedObjects; + return response.map>(simpleSavedObjectToSavedObject); + } + + async get(type: string, id: string) { + const response = await this.savedObjectClient.get(type, id); + return simpleSavedObjectToSavedObject(response); + } + async update( + type: string, + id: string, + attributes: Record, + options: Record + ) { + const response = await this.savedObjectClient.update(type, id, attributes, options); + return simpleSavedObjectToSavedObject(response); + } + async create(type: string, attributes: Record, options: Record) { + const response = await this.savedObjectClient.create(type, attributes, options); + return simpleSavedObjectToSavedObject(response); + } + delete(type: string, id: string) { + return this.savedObjectClient.delete(type, id); + } +} diff --git a/src/plugins/data/public/index_patterns/ui_settings_wrapper.ts b/src/plugins/data/public/index_patterns/ui_settings_wrapper.ts new file mode 100644 index 0000000000000..17fc88ddd674d --- /dev/null +++ b/src/plugins/data/public/index_patterns/ui_settings_wrapper.ts @@ -0,0 +1,45 @@ +/* + * 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 { IUiSettingsClient } from 'src/core/public'; +import { UiSettingsCommon } from '../../common/index_patterns'; + +export class UiSettingsPublicToCommon implements UiSettingsCommon { + private uiSettings: IUiSettingsClient; + constructor(uiSettings: IUiSettingsClient) { + this.uiSettings = uiSettings; + } + get(key: string) { + return Promise.resolve(this.uiSettings.get(key)); + } + + getAll() { + return Promise.resolve(this.uiSettings.getAll()); + } + + set(key: string, value: any) { + this.uiSettings.set(key, value); + return Promise.resolve(); + } + + remove(key: string) { + this.uiSettings.remove(key); + return Promise.resolve(); + } +} diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 51f96f10aa7c7..d5929cb9cd564 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -40,7 +40,12 @@ import { SearchService } from './search/search_service'; import { FieldFormatsService } from './field_formats'; import { QueryService } from './query'; import { createIndexPatternSelect } from './ui/index_pattern_select'; -import { IndexPatternsService, onRedirectNoIndexPattern } from './index_patterns'; +import { + IndexPatternsService, + onRedirectNoIndexPattern, + IndexPatternsApiClient, + UiSettingsPublicToCommon, +} from './index_patterns'; import { setFieldFormats, setHttp, @@ -76,6 +81,7 @@ import { ACTION_VALUE_CLICK, ValueClickActionContext, } from './actions/value_click_action'; +import { SavedObjectsClientPublicToCommon } from './index_patterns'; declare module '../../ui_actions/public' { export interface ActionContextMapping { @@ -164,9 +170,9 @@ export class DataPublicPlugin implements Plugin { notifications.toasts.add(toastInputFields); diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 0bb3fc3a3bf16..f2c7a907cda1d 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -66,8 +66,6 @@ import { GetSourceParams } from 'elasticsearch'; import { GetTemplateParams } from 'elasticsearch'; import { History } from 'history'; import { Href } from 'history'; -import { HttpSetup } from 'src/core/public'; -import { HttpStart } from 'src/core/public'; import { IconType } from '@elastic/eui'; import { IndexDocumentParams } from 'elasticsearch'; import { IndicesAnalyzeParams } from 'elasticsearch'; @@ -148,15 +146,15 @@ import { ReindexRethrottleParams } from 'elasticsearch'; import { RenderSearchTemplateParams } from 'elasticsearch'; import { Required } from '@kbn/utility-types'; import * as Rx from 'rxjs'; -import { SavedObject as SavedObject_2 } from 'src/core/public'; -import { SavedObjectsClientContract } from 'src/core/public'; +import { SavedObject } from 'src/core/server'; +import { SavedObject as SavedObject_3 } from 'src/core/public'; +import { SavedObjectsClientContract as SavedObjectsClientContract_3 } from 'src/core/public'; import { ScrollParams } from 'elasticsearch'; import { SearchParams } from 'elasticsearch'; import { SearchResponse as SearchResponse_2 } from 'elasticsearch'; import { SearchShardsParams } from 'elasticsearch'; import { SearchTemplateParams } from 'elasticsearch'; import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/public'; -import { SimpleSavedObject } from 'src/core/public'; import { SnapshotCreateParams } from 'elasticsearch'; import { SnapshotCreateRepositoryParams } from 'elasticsearch'; import { SnapshotDeleteParams } from 'elasticsearch'; @@ -300,7 +298,7 @@ export const connectToQueryState: ({ timefilter: { timefil // Warning: (ae-missing-release-tag) "createSavedQueryService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const createSavedQueryService: (savedObjectsClient: SavedObjectsClientContract) => SavedQueryService; +export const createSavedQueryService: (savedObjectsClient: SavedObjectsClientContract_3) => SavedQueryService; // Warning: (ae-missing-release-tag) "CustomFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -982,7 +980,7 @@ export type IMetricAggType = MetricAggType; // @public (undocumented) export class IndexPattern implements IIndexPattern { // Warning: (ae-forgotten-export) The symbol "IndexPatternDeps" needs to be exported by the entry point index.d.ts - constructor(id: string | undefined, { getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, }: IndexPatternDeps); + constructor(id: string | undefined, { getConfig, savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, uiSettingsValues, }: IndexPatternDeps); // (undocumented) [key: string]: any; // (undocumented) @@ -1097,9 +1095,15 @@ export type IndexPatternAggRestrictions = Record { +export interface IndexPatternsServiceStart { + indexPatternsServiceFactory: ( + kibanaRequest: KibanaRequest + ) => Promise; +} + +export interface IndexPatternsServiceStartDeps { + fieldFormats: FieldFormatsStart; + logger: Logger; +} + +export class IndexPatternsService implements Plugin { public setup(core: CoreSetup) { core.savedObjects.registerType(indexPatternSavedObjectType); core.capabilities.registerProvider(capabilitiesProvider); @@ -30,5 +46,28 @@ export class IndexPatternsService implements Plugin { registerRoutes(core.http); } - public start() {} + public start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps) { + const { uiSettings, savedObjects } = core; + + return { + indexPatternsServiceFactory: async (kibanaRequest: KibanaRequest) => { + const savedObjectsClient = savedObjects.getScopedClient(kibanaRequest); + const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); + const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient); + + return new IndexPatternsCommonService({ + uiSettings: new UiSettingsServerToCommon(uiSettingsClient), + savedObjectsClient: new SavedObjectsClientServerToCommon(savedObjectsClient), + apiClient: new IndexPatternsApiServer(), + fieldFormats: formats, + onError: (error) => { + logger.error(error); + }, + onNotification: ({ title, text }) => { + logger.warn(`${title} : ${text}`); + }, + }); + }, + }; + } } diff --git a/src/plugins/data/server/index_patterns/mocks.ts b/src/plugins/data/server/index_patterns/mocks.ts new file mode 100644 index 0000000000000..8f95afe3b3c9d --- /dev/null +++ b/src/plugins/data/server/index_patterns/mocks.ts @@ -0,0 +1,24 @@ +/* + * 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 function createIndexPatternsStartMock() { + return { + indexPatternsServiceFactory: jest.fn(), + }; +} diff --git a/src/plugins/data/server/index_patterns/saved_objects_client_wrapper.ts b/src/plugins/data/server/index_patterns/saved_objects_client_wrapper.ts new file mode 100644 index 0000000000000..c82695b7cb2ba --- /dev/null +++ b/src/plugins/data/server/index_patterns/saved_objects_client_wrapper.ts @@ -0,0 +1,53 @@ +/* + * 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, SavedObject } from 'src/core/server'; +import { + SavedObjectsClientCommon, + SavedObjectsClientCommonFindArgs, +} from '../../common/index_patterns'; + +export class SavedObjectsClientServerToCommon implements SavedObjectsClientCommon { + private savedObjectClient: SavedObjectsClientContract; + constructor(savedObjectClient: SavedObjectsClientContract) { + this.savedObjectClient = savedObjectClient; + } + async find(options: SavedObjectsClientCommonFindArgs) { + const result = await this.savedObjectClient.find(options); + return result.saved_objects; + } + + async get(type: string, id: string) { + return await this.savedObjectClient.get(type, id); + } + async update( + type: string, + id: string, + attributes: Record, + options: Record + ) { + return (await this.savedObjectClient.update(type, id, attributes, options)) as SavedObject; + } + async create(type: string, attributes: Record, options: Record) { + return await this.savedObjectClient.create(type, attributes, options); + } + delete(type: string, id: string) { + return this.savedObjectClient.delete(type, id); + } +} diff --git a/src/plugins/data/server/index_patterns/ui_settings_wrapper.ts b/src/plugins/data/server/index_patterns/ui_settings_wrapper.ts new file mode 100644 index 0000000000000..34cdfdff0b80f --- /dev/null +++ b/src/plugins/data/server/index_patterns/ui_settings_wrapper.ts @@ -0,0 +1,43 @@ +/* + * 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 { IUiSettingsClient } from 'src/core/server'; +import { UiSettingsCommon } from '../../common/index_patterns'; + +export class UiSettingsServerToCommon implements UiSettingsCommon { + private uiSettings: IUiSettingsClient; + constructor(uiSettings: IUiSettingsClient) { + this.uiSettings = uiSettings; + } + get(key: string) { + return this.uiSettings.get(key); + } + + getAll() { + return this.uiSettings.getAll(); + } + + set(key: string, value: any) { + return this.uiSettings.set(key, value); + } + + remove(key: string) { + return this.uiSettings.remove(key); + } +} diff --git a/src/plugins/data/server/mocks.ts b/src/plugins/data/server/mocks.ts index e2f2298234054..785e4a1ec41ab 100644 --- a/src/plugins/data/server/mocks.ts +++ b/src/plugins/data/server/mocks.ts @@ -19,6 +19,7 @@ import { createSearchSetupMock, createSearchStartMock } from './search/mocks'; import { createFieldFormatsSetupMock, createFieldFormatsStartMock } from './field_formats/mocks'; +import { createIndexPatternsStartMock } from './index_patterns/mocks'; function createSetupContract() { return { @@ -31,6 +32,7 @@ function createStartContract() { return { search: createSearchStartMock(), fieldFormats: createFieldFormatsStartMock(), + indexPatterns: createIndexPatternsStartMock(), }; } diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index 0edce458f1c6b..bcf1f4f8ab60b 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -17,9 +17,15 @@ * under the License. */ -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/server'; +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + Logger, +} from '../../../core/server'; import { ConfigSchema } from '../config'; -import { IndexPatternsService } from './index_patterns'; +import { IndexPatternsService, IndexPatternsServiceStart } from './index_patterns'; import { ISearchSetup, ISearchStart } from './search'; import { SearchService } from './search/search_service'; import { QueryService } from './query/query_service'; @@ -38,6 +44,7 @@ export interface DataPluginSetup { export interface DataPluginStart { search: ISearchStart; fieldFormats: FieldFormatsStart; + indexPatterns: IndexPatternsServiceStart; } export interface DataPluginSetupDependencies { @@ -52,12 +59,14 @@ export class DataServerPlugin implements Plugin) { this.searchService = new SearchService(initializerContext); this.scriptsService = new ScriptsService(); this.kqlTelemetryService = new KqlTelemetryService(initializerContext); this.autocompleteService = new AutocompleteService(initializerContext); + this.logger = initializerContext.logger.get('data'); } public setup( @@ -79,9 +88,14 @@ export class DataServerPlugin implements Plugin { fieldFormats: { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; + indexPatterns: { + indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; + }; }; // (undocumented) stop(): void; @@ -681,6 +694,10 @@ export interface PluginStart { // // (undocumented) fieldFormats: FieldFormatsStart; + // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceStart" needs to be exported by the entry point index.d.ts + // + // (undocumented) + indexPatterns: IndexPatternsServiceStart; // (undocumented) search: ISearchStart; } diff --git a/test/plugin_functional/config.js b/test/plugin_functional/config.js index 078eb9ee88a8e..f51fb5e1bade4 100644 --- a/test/plugin_functional/config.js +++ b/test/plugin_functional/config.js @@ -38,6 +38,7 @@ export default async function ({ readConfigFile }) { require.resolve('./test_suites/management'), require.resolve('./test_suites/doc_views'), require.resolve('./test_suites/application_links'), + require.resolve('./test_suites/data_plugin'), ], services: { ...functionalConfig.get('services'), diff --git a/test/plugin_functional/plugins/index_patterns/kibana.json b/test/plugin_functional/plugins/index_patterns/kibana.json new file mode 100644 index 0000000000000..e098950dc9677 --- /dev/null +++ b/test/plugin_functional/plugins/index_patterns/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "index_patterns_test_plugin", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["index_patterns_test_plugin"], + "server": true, + "ui": false, + "requiredPlugins": ["data"] +} diff --git a/test/plugin_functional/plugins/index_patterns/package.json b/test/plugin_functional/plugins/index_patterns/package.json new file mode 100644 index 0000000000000..eaba6ca624bd8 --- /dev/null +++ b/test/plugin_functional/plugins/index_patterns/package.json @@ -0,0 +1,17 @@ +{ + "name": "index_patterns_test_plugin", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/index_patterns_test_plugin", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.9.5" + } +} \ No newline at end of file diff --git a/test/plugin_functional/plugins/index_patterns/server/index.ts b/test/plugin_functional/plugins/index_patterns/server/index.ts new file mode 100644 index 0000000000000..0c99dd30c9cb6 --- /dev/null +++ b/test/plugin_functional/plugins/index_patterns/server/index.ts @@ -0,0 +1,30 @@ +/* + * 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 { PluginInitializer } from 'kibana/server'; +import { + IndexPatternsTestPlugin, + IndexPatternsTestPluginSetup, + IndexPatternsTestPluginStart, +} from './plugin'; + +export const plugin: PluginInitializer< + IndexPatternsTestPluginSetup, + IndexPatternsTestPluginStart +> = () => new IndexPatternsTestPlugin(); diff --git a/test/plugin_functional/plugins/index_patterns/server/plugin.ts b/test/plugin_functional/plugins/index_patterns/server/plugin.ts new file mode 100644 index 0000000000000..ffc70136ccffa --- /dev/null +++ b/test/plugin_functional/plugins/index_patterns/server/plugin.ts @@ -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 { CoreSetup, Plugin } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; +import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server'; + +export interface IndexPatternsTestStartDeps { + data: DataPluginStart; +} + +export class IndexPatternsTestPlugin + implements + Plugin< + IndexPatternsTestPluginSetup, + IndexPatternsTestPluginStart, + {}, + IndexPatternsTestStartDeps + > { + public setup(core: CoreSetup) { + const router = core.http.createRouter(); + + router.get( + { path: '/api/index-patterns-plugin/get-all', validate: false }, + async (context, req, res) => { + const [, { data }] = await core.getStartServices(); + const service = await data.indexPatterns.indexPatternsServiceFactory(req); + const ids = await service.getIds(); + return res.ok({ body: ids }); + } + ); + + router.get( + { + path: '/api/index-patterns-plugin/get/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, req, res) => { + const id = (req.params as Record).id; + const [, { data }] = await core.getStartServices(); + const service = await data.indexPatterns.indexPatternsServiceFactory(req); + const ip = await service.get(id); + return res.ok({ body: ip.toSpec() }); + } + ); + + router.get( + { + path: '/api/index-patterns-plugin/update/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, req, res) => { + const [, { data }] = await core.getStartServices(); + const id = (req.params as Record).id; + const service = await data.indexPatterns.indexPatternsServiceFactory(req); + const ip = await service.get(id); + await ip.save(); + return res.ok(); + } + ); + + router.get( + { + path: '/api/index-patterns-plugin/delete/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, req, res) => { + const [, { data }] = await core.getStartServices(); + const id = (req.params as Record).id; + const service = await data.indexPatterns.indexPatternsServiceFactory(req); + const ip = await service.get(id); + await ip.destroy(); + return res.ok(); + } + ); + } + + public start() {} + public stop() {} +} + +export type IndexPatternsTestPluginSetup = ReturnType; +export type IndexPatternsTestPluginStart = ReturnType; diff --git a/test/plugin_functional/plugins/index_patterns/tsconfig.json b/test/plugin_functional/plugins/index_patterns/tsconfig.json new file mode 100644 index 0000000000000..6f0c32ad30601 --- /dev/null +++ b/test/plugin_functional/plugins/index_patterns/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "server/**/*.ts", + "server/**/*.tsx", + "../../../../typings/**/*", + ], + "exclude": [] +} diff --git a/test/plugin_functional/test_suites/data_plugin/index.ts b/test/plugin_functional/test_suites/data_plugin/index.ts new file mode 100644 index 0000000000000..1c3f118135ffa --- /dev/null +++ b/test/plugin_functional/test_suites/data_plugin/index.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +// @ts-expect-error +export default function ({ loadTestFile }) { + describe('data plugin', () => { + loadTestFile(require.resolve('./index_patterns')); + }); +} diff --git a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts new file mode 100644 index 0000000000000..481e9d76e3acc --- /dev/null +++ b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts @@ -0,0 +1,64 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import expect from '@kbn/expect'; +import { PluginFunctionalProviderContext } from '../../services'; +import '../../plugins/core_provider_plugin/types'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const PageObjects = getPageObjects(['common', 'settings']); + + describe('index patterns', function () { + let indexPatternId = ''; + before(async () => { + await esArchiver.loadIfNeeded( + '../functional/fixtures/es_archiver/getting_started/shakespeare' + ); + await PageObjects.common.navigateToApp('settings'); + await PageObjects.settings.createIndexPattern('shakespeare', ''); + }); + + it('can get all ids', async () => { + const body = await (await supertest.get('/api/index-patterns-plugin/get-all').expect(200)) + .body; + indexPatternId = body[0]; + expect(body.length > 0).to.equal(true); + }); + + it('can get index pattern by id', async () => { + const body = await ( + await supertest.get(`/api/index-patterns-plugin/get/${indexPatternId}`).expect(200) + ).body; + expect(body.fields.length > 0).to.equal(true); + }); + + it('can update index pattern', async () => { + const body = await ( + await supertest.get(`/api/index-patterns-plugin/update/${indexPatternId}`).expect(200) + ).body; + expect(body).to.eql({}); + }); + + it('can delete index pattern', async () => { + await supertest.get(`/api/index-patterns-plugin/delete/${indexPatternId}`).expect(200); + }); + }); +} diff --git a/x-pack/plugins/maps/public/classes/layers/solution_layers/security/security_index_pattern_utils.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/security_index_pattern_utils.ts index 141b9133505b7..6ba27322757bf 100644 --- a/x-pack/plugins/maps/public/classes/layers/solution_layers/security/security_index_pattern_utils.ts +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/security/security_index_pattern_utils.ts @@ -6,9 +6,6 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ import minimatch from 'minimatch'; -import { SimpleSavedObject } from 'src/core/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { IndexPatternSavedObjectAttrs } from 'src/plugins/data/common/index_patterns/index_patterns/index_patterns'; import { getIndexPatternService, getUiSettings } from '../../../../kibana_services'; export type IndexPatternMeta = { @@ -29,13 +26,13 @@ export async function getSecurityIndexPatterns(): Promise { const indexPatternCache = await getIndexPatternService().getCache(); return indexPatternCache! - .filter((savedObject: SimpleSavedObject) => { + .filter((savedObject) => { return (securityIndexPatternTitles as string[]).some((indexPatternTitle) => { // glob matching index pattern title return minimatch(indexPatternTitle, savedObject?.attributes?.title); }); }) - .map((savedObject: SimpleSavedObject) => { + .map((savedObject) => { return { id: savedObject.id, title: savedObject.attributes.title, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index 2de9a1dcadd4b..f95d2f572a406 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -8,7 +8,6 @@ import { useReducer } from 'react'; import { i18n } from '@kbn/i18n'; -import { SimpleSavedObject } from 'kibana/public'; import { getErrorMessage } from '../../../../../../../common/util/errors'; import { DeepReadonly } from '../../../../../../../common/types/common'; import { ml } from '../../../../../services/ml_api_service'; @@ -235,7 +234,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { // Set the index pattern titles which the user can choose as the source. const indexPatternsMap: SourceIndexMap = {}; const savedObjects = (await mlContext.indexPatterns.getCache()) || []; - savedObjects.forEach((obj: SimpleSavedObject>) => { + savedObjects.forEach((obj) => { const title = obj?.attributes?.title; if (title !== undefined) { const id = obj?.id || ''; From 7c9db862abc1632bc758c4994e4a2133407d8133 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 30 Jun 2020 12:07:06 -0400 Subject: [PATCH 23/23] [Ingest Manager] Do not index every saved object field (#70162) --- .../server/saved_objects/index.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts index 470b808420136..1d412937e244f 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts @@ -65,9 +65,9 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { config_revision: { type: 'integer' }, config_newest_revision: { type: 'integer' }, default_api_key_id: { type: 'keyword' }, - default_api_key: { type: 'keyword' }, + default_api_key: { type: 'binary', index: false }, updated_at: { type: 'date' }, - current_error_events: { type: 'text' }, + current_error_events: { type: 'text', index: false }, packages: { type: 'keyword' }, }, }, @@ -83,7 +83,7 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { properties: { agent_id: { type: 'keyword' }, type: { type: 'keyword' }, - data: { type: 'binary' }, + data: { type: 'binary', index: false }, sent_at: { type: 'date' }, created_at: { type: 'date' }, }, @@ -130,7 +130,7 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { updated_at: { type: 'date' }, updated_by: { type: 'keyword' }, revision: { type: 'integer' }, - monitoring_enabled: { type: 'keyword' }, + monitoring_enabled: { type: 'keyword', index: false }, }, }, migrations: { @@ -148,7 +148,7 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { properties: { name: { type: 'keyword' }, type: { type: 'keyword' }, - api_key: { type: 'binary' }, + api_key: { type: 'binary', index: false }, api_key_id: { type: 'keyword' }, config_id: { type: 'keyword' }, created_at: { type: 'date' }, @@ -171,9 +171,9 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { type: { type: 'keyword' }, is_default: { type: 'boolean' }, hosts: { type: 'keyword' }, - ca_sha256: { type: 'keyword' }, - fleet_enroll_username: { type: 'binary' }, - fleet_enroll_password: { type: 'binary' }, + ca_sha256: { type: 'keyword', index: false }, + fleet_enroll_username: { type: 'binary', index: false }, + fleet_enroll_password: { type: 'binary', index: false }, config: { type: 'flattened' }, }, }, @@ -202,6 +202,7 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { output_id: { type: 'keyword' }, inputs: { type: 'nested', + enabled: false, properties: { type: { type: 'keyword' }, enabled: { type: 'boolean' },