From 4d346cdfc01f473dec471b2b6f18671005be6fb3 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Thu, 12 Nov 2020 09:04:08 -0500 Subject: [PATCH 01/17] Add maps_oss folder to code_owners (#83204) --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 62abf281e659f..b7fb3ff04db71 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -137,6 +137,7 @@ /x-pack/test/functional/es_archives/maps/ @elastic/kibana-gis /x-pack/test/visual_regression/tests/maps/index.js @elastic/kibana-gis #CC# /src/plugins/maps_legacy/ @elastic/kibana-gis +#CC# /src/plugins/maps_oss/ @elastic/kibana-gis #CC# /x-pack/plugins/file_upload @elastic/kibana-gis #CC# /x-pack/plugins/maps_legacy_licensing @elastic/kibana-gis #CC# /src/plugins/home/server/tutorials @elastic/kibana-gis From 3a849ff1040e7e9e522a3c8297539c1b013d762a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Thu, 12 Nov 2020 15:05:08 +0100 Subject: [PATCH 02/17] [Index Management] Add an index template link to data stream details (#82592) * Add index template link to data stream details * Fixed ILM policy link and added a check for index template name after navigation Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../helpers/test_subjects.ts | 1 + .../home/data_streams_tab.helpers.ts | 20 ++++++++++++++ .../home/data_streams_tab.test.ts | 27 +++++++++++++++++++ .../data_stream_detail_panel.tsx | 16 ++++++----- 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts index b5386dec34205..313ebefb85301 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts @@ -51,4 +51,5 @@ export type TestSubjects = | 'templateList' | 'templatesTab' | 'templateTable' + | 'title' | 'viewButton'; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts index 6bf6c11a37bb4..ab796767487b5 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts @@ -30,6 +30,7 @@ export interface DataStreamsTabTestBed extends TestBed { clickDeleteActionAt: (index: number) => void; clickConfirmDelete: () => void; clickDeleteDataStreamButton: () => void; + clickDetailPanelIndexTemplateLink: () => void; }; findDeleteActionAt: (index: number) => ReactWrapper; findDeleteConfirmationModal: () => ReactWrapper; @@ -38,6 +39,7 @@ export interface DataStreamsTabTestBed extends TestBed { findEmptyPromptIndexTemplateLink: () => ReactWrapper; findDetailPanelIlmPolicyLink: () => ReactWrapper; findDetailPanelIlmPolicyName: () => ReactWrapper; + findDetailPanelIndexTemplateLink: () => ReactWrapper; } export const setup = async (overridingDependencies: any = {}): Promise => { @@ -143,6 +145,17 @@ export const setup = async (overridingDependencies: any = {}): Promise { + const { component, router, find } = testBed; + const indexTemplateLink = find('indexTemplateLink'); + + await act(async () => { + router.navigateTo(indexTemplateLink.props().href!); + }); + + component.update(); + }; + const findDetailPanel = () => { const { find } = testBed; return find('dataStreamDetailPanel'); @@ -158,6 +171,11 @@ export const setup = async (overridingDependencies: any = {}): Promise { + const { find } = testBed; + return find('indexTemplateLink'); + }; + const findDetailPanelIlmPolicyName = () => { const descriptionList = testBed.component.find(EuiDescriptionListDescription); // ilm policy is the last in the details list @@ -176,6 +194,7 @@ export const setup = async (overridingDependencies: any = {}): Promise { setLoadIndicesResponse, setLoadDataStreamsResponse, setLoadDataStreamResponse, + setLoadTemplateResponse, + setLoadTemplatesResponse, } = httpRequestsMockHelpers; setLoadIndicesResponse([ @@ -103,6 +106,10 @@ describe('Data Streams tab', () => { ]); setLoadDataStreamResponse(dataStreamForDetailPanel); + const indexTemplate = fixtures.getTemplate({ name: 'indexTemplate' }); + setLoadTemplatesResponse({ templates: [indexTemplate], legacyTemplates: [] }); + setLoadTemplateResponse(indexTemplate); + testBed = await setup({ history: createMemoryHistory() }); await act(async () => { testBed.actions.goToDataStreamsList(); @@ -244,6 +251,26 @@ describe('Data Streams tab', () => { dataStreams: ['dataStream1'], }); }); + + test('clicking index template name navigates to the index template details', async () => { + const { + actions: { clickNameAt, clickDetailPanelIndexTemplateLink }, + findDetailPanelIndexTemplateLink, + component, + find, + } = testBed; + + await clickNameAt(0); + + const indexTemplateLink = findDetailPanelIndexTemplateLink(); + expect(indexTemplateLink.text()).toBe('indexTemplate'); + + await clickDetailPanelIndexTemplateLink(); + + component.update(); + expect(find('summaryTab').exists()).toBeTruthy(); + expect(find('title').text().trim()).toBe('indexTemplate'); + }); }); }); diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx index 9ec6993717435..05d7e97745b9e 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx @@ -29,9 +29,9 @@ import { useLoadDataStream } from '../../../../services/api'; import { DeleteDataStreamConfirmationModal } from '../delete_data_stream_confirmation_modal'; import { humanizeTimeStamp } from '../humanize_time_stamp'; import { useUrlGenerator } from '../../../../services/use_url_generator'; +import { getIndexListUri, getTemplateDetailsLink } from '../../../../services/routing'; import { ILM_PAGES_POLICY_EDIT, ILM_URL_GENERATOR_ID } from '../../../../constants'; import { useAppContext } from '../../../../app_context'; -import { getIndexListUri } from '../../../../..'; interface DetailsListProps { details: Array<{ @@ -207,7 +207,14 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ defaultMessage: 'The index template that configured the data stream and configures its backing indices', }), - content: indexTemplateName, + content: ( + + {indexTemplateName} + + ), }, { name: i18n.translate('xpack.idxMgmt.dataStreamDetailPanel.ilmPolicyTitle', { @@ -218,10 +225,7 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({ }), content: ilmPolicyName && ilmPolicyLink ? ( - + {ilmPolicyName} ) : ( From 0e7bcf6164c92b3a0a8bedebc3a81885624217bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?= Date: Thu, 12 Nov 2020 15:11:43 +0100 Subject: [PATCH 03/17] [Logs UI] Add pagination to the log stream shared component (#81193) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/http_api/log_entries/entries.ts | 16 +- .../public/components/log_stream/index.tsx | 47 ++++- .../containers/logs/log_entries/index.ts | 18 +- .../containers/logs/log_stream/index.ts | 171 ++++++++++++++++-- .../log_entries/kibana_log_entries_adapter.ts | 25 ++- .../log_entries_domain/log_entries_domain.ts | 52 +++--- .../server/routes/log_entries/entries.ts | 41 +++-- .../server/routes/log_entries/highlights.ts | 2 +- 8 files changed, 299 insertions(+), 73 deletions(-) diff --git a/x-pack/plugins/infra/common/http_api/log_entries/entries.ts b/x-pack/plugins/infra/common/http_api/log_entries/entries.ts index d38b4690fed71..48790c3faca52 100644 --- a/x-pack/plugins/infra/common/http_api/log_entries/entries.ts +++ b/x-pack/plugins/infra/common/http_api/log_entries/entries.ts @@ -99,11 +99,17 @@ export type LogEntryContext = rt.TypeOf; export type LogEntry = rt.TypeOf; export const logEntriesResponseRT = rt.type({ - data: rt.type({ - entries: rt.array(logEntryRT), - topCursor: rt.union([logEntriesCursorRT, rt.null]), - bottomCursor: rt.union([logEntriesCursorRT, rt.null]), - }), + data: rt.intersection([ + rt.type({ + entries: rt.array(logEntryRT), + topCursor: rt.union([logEntriesCursorRT, rt.null]), + bottomCursor: rt.union([logEntriesCursorRT, rt.null]), + }), + rt.partial({ + hasMoreBefore: rt.boolean, + hasMoreAfter: rt.boolean, + }), + ]), }); export type LogEntriesResponse = rt.TypeOf; diff --git a/x-pack/plugins/infra/public/components/log_stream/index.tsx b/x-pack/plugins/infra/public/components/log_stream/index.tsx index 6698018e8cc19..62a4d7ffc3d81 100644 --- a/x-pack/plugins/infra/public/components/log_stream/index.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; +import React, { useMemo, useCallback } from 'react'; import { noop } from 'lodash'; import useMount from 'react-use/lib/useMount'; import { euiStyled } from '../../../../observability/public'; @@ -17,6 +17,8 @@ import { useLogStream } from '../../containers/logs/log_stream'; import { ScrollableLogTextStreamView } from '../logging/log_text_stream'; +const PAGE_THRESHOLD = 2; + export interface LogStreamProps { sourceId?: string; startTimestamp: number; @@ -58,7 +60,16 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re }); // Internal state - const { loadingState, entries, fetchEntries } = useLogStream({ + const { + loadingState, + pageLoadingState, + entries, + hasMoreBefore, + hasMoreAfter, + fetchEntries, + fetchPreviousEntries, + fetchNextEntries, + } = useLogStream({ sourceId, startTimestamp, endTimestamp, @@ -70,6 +81,8 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re const isReloading = isLoadingSourceConfiguration || loadingState === 'uninitialized' || loadingState === 'loading'; + const isLoadingMore = pageLoadingState === 'loading'; + const columnConfigurations = useMemo(() => { return sourceConfiguration ? sourceConfiguration.configuration.logColumns : []; }, [sourceConfiguration]); @@ -84,13 +97,33 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re [entries] ); + const parsedHeight = typeof height === 'number' ? `${height}px` : height; + // Component lifetime useMount(() => { loadSourceConfiguration(); fetchEntries(); }); - const parsedHeight = typeof height === 'number' ? `${height}px` : height; + // Pagination handler + const handlePagination = useCallback( + ({ fromScroll, pagesBeforeStart, pagesAfterEnd }) => { + if (!fromScroll) { + return; + } + + if (isLoadingMore) { + return; + } + + if (pagesBeforeStart < PAGE_THRESHOLD) { + fetchPreviousEntries(); + } else if (pagesAfterEnd < PAGE_THRESHOLD) { + fetchNextEntries(); + } + }, + [isLoadingMore, fetchPreviousEntries, fetchNextEntries] + ); return ( @@ -101,13 +134,13 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re scale="medium" wrap={false} isReloading={isReloading} - isLoadingMore={false} - hasMoreBeforeStart={false} - hasMoreAfterEnd={false} + isLoadingMore={isLoadingMore} + hasMoreBeforeStart={hasMoreBefore} + hasMoreAfterEnd={hasMoreAfter} isStreaming={false} lastLoadedTime={null} jumpToTarget={noop} - reportVisibleInterval={noop} + reportVisibleInterval={handlePagination} loadNewerItems={noop} reloadItems={fetchEntries} highlightedItem={highlight ?? null} diff --git a/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts b/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts index 146746af980c9..bf4c5fbe0b13b 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_entries/index.ts @@ -367,16 +367,16 @@ const logEntriesStateReducer = (prevState: LogEntriesStateParams, action: Action case Action.ReceiveNewEntries: return { ...prevState, - ...action.payload, + entries: action.payload.entries, + topCursor: action.payload.topCursor, + bottomCursor: action.payload.bottomCursor, centerCursor: getCenterCursor(action.payload.entries), lastLoadedTime: new Date(), isReloading: false, - - // Be optimistic. If any of the before/after requests comes empty, set - // the corresponding flag to `false` - hasMoreBeforeStart: true, - hasMoreAfterEnd: true, + hasMoreBeforeStart: action.payload.hasMoreBefore ?? prevState.hasMoreBeforeStart, + hasMoreAfterEnd: action.payload.hasMoreAfter ?? prevState.hasMoreAfterEnd, }; + case Action.ReceiveEntriesBefore: { const newEntries = action.payload.entries; const prevEntries = cleanDuplicateItems(prevState.entries, newEntries); @@ -385,7 +385,7 @@ const logEntriesStateReducer = (prevState: LogEntriesStateParams, action: Action const update = { entries, isLoadingMore: false, - hasMoreBeforeStart: newEntries.length > 0, + hasMoreBeforeStart: action.payload.hasMoreBefore ?? prevState.hasMoreBeforeStart, // Keep the previous cursor if request comes empty, to easily extend the range. topCursor: newEntries.length > 0 ? action.payload.topCursor : prevState.topCursor, centerCursor: getCenterCursor(entries), @@ -402,7 +402,7 @@ const logEntriesStateReducer = (prevState: LogEntriesStateParams, action: Action const update = { entries, isLoadingMore: false, - hasMoreAfterEnd: newEntries.length > 0, + hasMoreAfterEnd: action.payload.hasMoreAfter ?? prevState.hasMoreAfterEnd, // Keep the previous cursor if request comes empty, to easily extend the range. bottomCursor: newEntries.length > 0 ? action.payload.bottomCursor : prevState.bottomCursor, centerCursor: getCenterCursor(entries), @@ -419,6 +419,8 @@ const logEntriesStateReducer = (prevState: LogEntriesStateParams, action: Action topCursor: null, bottomCursor: null, centerCursor: null, + // Assume there are more pages on both ends unless proven wrong by the + // API with an explicit `false` response. hasMoreBeforeStart: true, hasMoreAfterEnd: true, }; diff --git a/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts b/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts index 4a6da6063e960..566edcce91318 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_stream/index.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useState, useMemo } from 'react'; +import { useMemo, useEffect } from 'react'; +import useSetState from 'react-use/lib/useSetState'; +import usePrevious from 'react-use/lib/usePrevious'; import { esKuery } from '../../../../../../../src/plugins/data/public'; import { fetchLogEntries } from '../log_entries/api/fetch_log_entries'; import { useTrackedPromise } from '../../../utils/use_tracked_promise'; @@ -21,19 +23,62 @@ interface LogStreamProps { interface LogStreamState { entries: LogEntry[]; + topCursor: LogEntriesCursor | null; + bottomCursor: LogEntriesCursor | null; + hasMoreBefore: boolean; + hasMoreAfter: boolean; +} + +type LoadingState = 'uninitialized' | 'loading' | 'success' | 'error'; + +interface LogStreamReturn extends LogStreamState { fetchEntries: () => void; - loadingState: 'uninitialized' | 'loading' | 'success' | 'error'; + fetchPreviousEntries: () => void; + fetchNextEntries: () => void; + loadingState: LoadingState; + pageLoadingState: LoadingState; } +const INITIAL_STATE: LogStreamState = { + entries: [], + topCursor: null, + bottomCursor: null, + // Assume there are pages available until the API proves us wrong + hasMoreBefore: true, + hasMoreAfter: true, +}; + +const EMPTY_DATA = { + entries: [], + topCursor: null, + bottomCursor: null, +}; + export function useLogStream({ sourceId, startTimestamp, endTimestamp, query, center, -}: LogStreamProps): LogStreamState { +}: LogStreamProps): LogStreamReturn { const { services } = useKibanaContextForPlugin(); - const [entries, setEntries] = useState([]); + const [state, setState] = useSetState(INITIAL_STATE); + + // Ensure the pagination keeps working when the timerange gets extended + const prevStartTimestamp = usePrevious(startTimestamp); + const prevEndTimestamp = usePrevious(endTimestamp); + + useEffect(() => { + if (prevStartTimestamp && prevStartTimestamp > startTimestamp) { + setState({ hasMoreBefore: true }); + } + }, [prevStartTimestamp, startTimestamp, setState]); + + useEffect(() => { + if (prevEndTimestamp && prevEndTimestamp < endTimestamp) { + setState({ hasMoreAfter: true }); + } + }, [prevEndTimestamp, endTimestamp, setState]); const parsedQuery = useMemo(() => { return query @@ -46,7 +91,7 @@ export function useLogStream({ { cancelPreviousOn: 'creation', createPromise: () => { - setEntries([]); + setState(INITIAL_STATE); const fetchPosition = center ? { center } : { before: 'last' }; return fetchLogEntries( @@ -61,26 +106,130 @@ export function useLogStream({ ); }, onResolve: ({ data }) => { - setEntries(data.entries); + setState((prevState) => ({ + ...data, + hasMoreBefore: data.hasMoreBefore ?? prevState.hasMoreBefore, + hasMoreAfter: data.hasMoreAfter ?? prevState.hasMoreAfter, + })); }, }, [sourceId, startTimestamp, endTimestamp, query] ); - const loadingState = useMemo(() => convertPromiseStateToLoadingState(entriesPromise.state), [ - entriesPromise.state, - ]); + const [previousEntriesPromise, fetchPreviousEntries] = useTrackedPromise( + { + cancelPreviousOn: 'creation', + createPromise: () => { + if (state.topCursor === null) { + throw new Error( + 'useLogState: Cannot fetch previous entries. No cursor is set.\nEnsure you have called `fetchEntries` at least once.' + ); + } + + if (!state.hasMoreBefore) { + return Promise.resolve({ data: EMPTY_DATA }); + } + + return fetchLogEntries( + { + sourceId, + startTimestamp, + endTimestamp, + query: parsedQuery, + before: state.topCursor, + }, + services.http.fetch + ); + }, + onResolve: ({ data }) => { + if (!data.entries.length) { + return; + } + setState((prevState) => ({ + entries: [...data.entries, ...prevState.entries], + hasMoreBefore: data.hasMoreBefore ?? prevState.hasMoreBefore, + topCursor: data.topCursor ?? prevState.topCursor, + })); + }, + }, + [sourceId, startTimestamp, endTimestamp, query, state.topCursor] + ); + + const [nextEntriesPromise, fetchNextEntries] = useTrackedPromise( + { + cancelPreviousOn: 'creation', + createPromise: () => { + if (state.bottomCursor === null) { + throw new Error( + 'useLogState: Cannot fetch next entries. No cursor is set.\nEnsure you have called `fetchEntries` at least once.' + ); + } + + if (!state.hasMoreAfter) { + return Promise.resolve({ data: EMPTY_DATA }); + } + + return fetchLogEntries( + { + sourceId, + startTimestamp, + endTimestamp, + query: parsedQuery, + after: state.bottomCursor, + }, + services.http.fetch + ); + }, + onResolve: ({ data }) => { + if (!data.entries.length) { + return; + } + setState((prevState) => ({ + entries: [...prevState.entries, ...data.entries], + hasMoreAfter: data.hasMoreAfter ?? prevState.hasMoreAfter, + bottomCursor: data.bottomCursor ?? prevState.bottomCursor, + })); + }, + }, + [sourceId, startTimestamp, endTimestamp, query, state.bottomCursor] + ); + + const loadingState = useMemo( + () => convertPromiseStateToLoadingState(entriesPromise.state), + [entriesPromise.state] + ); + + const pageLoadingState = useMemo(() => { + const states = [previousEntriesPromise.state, nextEntriesPromise.state]; + + if (states.includes('pending')) { + return 'loading'; + } + + if (states.includes('rejected')) { + return 'error'; + } + + if (states.includes('resolved')) { + return 'success'; + } + + return 'uninitialized'; + }, [previousEntriesPromise.state, nextEntriesPromise.state]); return { - entries, + ...state, fetchEntries, + fetchPreviousEntries, + fetchNextEntries, loadingState, + pageLoadingState, }; } function convertPromiseStateToLoadingState( state: 'uninitialized' | 'pending' | 'resolved' | 'rejected' -): LogStreamState['loadingState'] { +): LoadingState { switch (state) { case 'uninitialized': return 'uninitialized'; diff --git a/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts index 9309ad85a3570..6ffa1ad4b0b82 100644 --- a/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts @@ -35,8 +35,9 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { sourceConfiguration: InfraSourceConfiguration, fields: string[], params: LogEntriesParams - ): Promise { - const { startTimestamp, endTimestamp, query, cursor, size, highlightTerm } = params; + ): Promise<{ documents: LogEntryDocument[]; hasMoreBefore?: boolean; hasMoreAfter?: boolean }> { + const { startTimestamp, endTimestamp, query, cursor, highlightTerm } = params; + const size = params.size ?? LOG_ENTRIES_PAGE_SIZE; const { sortDirection, searchAfterClause } = processCursor(cursor); @@ -72,7 +73,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { index: sourceConfiguration.logAlias, ignoreUnavailable: true, body: { - size: typeof size !== 'undefined' ? size : LOG_ENTRIES_PAGE_SIZE, + size: size + 1, // Extra one to test if it has more before or after track_total_hits: false, _source: false, fields, @@ -104,8 +105,22 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { esQuery ); - const hits = sortDirection === 'asc' ? esResult.hits.hits : esResult.hits.hits.reverse(); - return mapHitsToLogEntryDocuments(hits, fields); + const hits = esResult.hits.hits; + const hasMore = hits.length > size; + + if (hasMore) { + hits.pop(); + } + + if (sortDirection === 'desc') { + hits.reverse(); + } + + return { + documents: mapHitsToLogEntryDocuments(hits, fields), + hasMoreBefore: sortDirection === 'desc' ? hasMore : undefined, + hasMoreAfter: sortDirection === 'asc' ? hasMore : undefined, + }; } public async getContainedLogSummaryBuckets( diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts index cc9d4c749c77d..1cf0afd50b80c 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts @@ -74,7 +74,7 @@ export class InfraLogEntriesDomain { requestContext: RequestHandlerContext, sourceId: string, params: LogEntriesAroundParams - ) { + ): Promise<{ entries: LogEntry[]; hasMoreBefore?: boolean; hasMoreAfter?: boolean }> { const { startTimestamp, endTimestamp, center, query, size, highlightTerm } = params; /* @@ -87,14 +87,18 @@ export class InfraLogEntriesDomain { */ const halfSize = (size || LOG_ENTRIES_PAGE_SIZE) / 2; - const entriesBefore = await this.getLogEntries(requestContext, sourceId, { - startTimestamp, - endTimestamp, - query, - cursor: { before: center }, - size: Math.floor(halfSize), - highlightTerm, - }); + const { entries: entriesBefore, hasMoreBefore } = await this.getLogEntries( + requestContext, + sourceId, + { + startTimestamp, + endTimestamp, + query, + cursor: { before: center }, + size: Math.floor(halfSize), + highlightTerm, + } + ); /* * Elasticsearch's `search_after` returns documents after the specified cursor. @@ -108,23 +112,27 @@ export class InfraLogEntriesDomain { ? entriesBefore[entriesBefore.length - 1].cursor : { time: center.time - 1, tiebreaker: 0 }; - const entriesAfter = await this.getLogEntries(requestContext, sourceId, { - startTimestamp, - endTimestamp, - query, - cursor: { after: cursorAfter }, - size: Math.ceil(halfSize), - highlightTerm, - }); + const { entries: entriesAfter, hasMoreAfter } = await this.getLogEntries( + requestContext, + sourceId, + { + startTimestamp, + endTimestamp, + query, + cursor: { after: cursorAfter }, + size: Math.ceil(halfSize), + highlightTerm, + } + ); - return [...entriesBefore, ...entriesAfter]; + return { entries: [...entriesBefore, ...entriesAfter], hasMoreBefore, hasMoreAfter }; } public async getLogEntries( requestContext: RequestHandlerContext, sourceId: string, params: LogEntriesParams - ): Promise { + ): Promise<{ entries: LogEntry[]; hasMoreBefore?: boolean; hasMoreAfter?: boolean }> { const { configuration } = await this.libs.sources.getSourceConfiguration( requestContext.core.savedObjects.client, sourceId @@ -136,7 +144,7 @@ export class InfraLogEntriesDomain { const requiredFields = getRequiredFields(configuration, messageFormattingRules); - const documents = await this.adapter.getLogEntries( + const { documents, hasMoreBefore, hasMoreAfter } = await this.adapter.getLogEntries( requestContext, configuration, requiredFields, @@ -173,7 +181,7 @@ export class InfraLogEntriesDomain { }; }); - return entries; + return { entries, hasMoreBefore, hasMoreAfter }; } public async getLogSummaryBucketsBetween( @@ -323,7 +331,7 @@ export interface LogEntriesAdapter { sourceConfiguration: InfraSourceConfiguration, fields: string[], params: LogEntriesParams - ): Promise; + ): Promise<{ documents: LogEntryDocument[]; hasMoreBefore?: boolean; hasMoreAfter?: boolean }>; getContainedLogSummaryBuckets( requestContext: RequestHandlerContext, diff --git a/x-pack/plugins/infra/server/routes/log_entries/entries.ts b/x-pack/plugins/infra/server/routes/log_entries/entries.ts index c1f63d9c29577..2baf3fd7aa990 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/entries.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/entries.ts @@ -34,14 +34,21 @@ export const initLogEntriesRoute = ({ framework, logEntries }: InfraBackendLibs) } = payload; let entries; + let hasMoreBefore; + let hasMoreAfter; + if ('center' in payload) { - entries = await logEntries.getLogEntriesAround(requestContext, sourceId, { - startTimestamp, - endTimestamp, - query: parseFilterQuery(query), - center: payload.center, - size, - }); + ({ entries, hasMoreBefore, hasMoreAfter } = await logEntries.getLogEntriesAround( + requestContext, + sourceId, + { + startTimestamp, + endTimestamp, + query: parseFilterQuery(query), + center: payload.center, + size, + } + )); } else { let cursor: LogEntriesParams['cursor']; if ('before' in payload) { @@ -50,13 +57,17 @@ export const initLogEntriesRoute = ({ framework, logEntries }: InfraBackendLibs) cursor = { after: payload.after }; } - entries = await logEntries.getLogEntries(requestContext, sourceId, { - startTimestamp, - endTimestamp, - query: parseFilterQuery(query), - cursor, - size, - }); + ({ entries, hasMoreBefore, hasMoreAfter } = await logEntries.getLogEntries( + requestContext, + sourceId, + { + startTimestamp, + endTimestamp, + query: parseFilterQuery(query), + cursor, + size, + } + )); } const hasEntries = entries.length > 0; @@ -67,6 +78,8 @@ export const initLogEntriesRoute = ({ framework, logEntries }: InfraBackendLibs) entries, topCursor: hasEntries ? entries[0].cursor : null, bottomCursor: hasEntries ? entries[entries.length - 1].cursor : null, + hasMoreBefore, + hasMoreAfter, }, }), }); diff --git a/x-pack/plugins/infra/server/routes/log_entries/highlights.ts b/x-pack/plugins/infra/server/routes/log_entries/highlights.ts index cc8483fb5c658..b315d22c47165 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/highlights.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/highlights.ts @@ -79,7 +79,7 @@ export const initLogEntriesHighlightsRoute = ({ framework, logEntries }: InfraBa return response.ok({ body: logEntriesHighlightsResponseRT.encode({ - data: entriesPerHighlightTerm.map((entries) => { + data: entriesPerHighlightTerm.map(({ entries }) => { if (entries.length > 0) { return { entries, From 169dcef2bf478e0ed5014705c12f4cb102a76918 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 12 Nov 2020 15:16:23 +0100 Subject: [PATCH 04/17] [ML] Persisted URL state for the "Anomaly detection jobs" page (#83149) * [ML] table config in the URL state * [ML] fix job list on the management page * [ML] store query filter in the URL * [ML] fix context for the management page * [ML] update module_list_card.tsx in Logs UI * [ML] fix unit tests * [ML] fix unit tests * [ML] fix unit tests * [ML] move utils functions * [ML] url generator to support both job and group ids --- .../MachineLearningLinks/MLLink.test.tsx | 2 +- .../setup_flyout/module_list_card.tsx | 62 ++++-- x-pack/plugins/infra/public/types.ts | 2 + .../ml/common/util/string_utils.test.ts | 22 +- x-pack/plugins/ml/common/util/string_utils.ts | 8 + .../analytics_list/analytics_list.tsx | 6 +- .../job_filter_bar/{index.js => index.ts} | 0 .../job_filter_bar/job_filter_bar.js | 210 ------------------ .../job_filter_bar/job_filter_bar.tsx | 163 ++++++++++++++ .../components/jobs_list/jobs_list.js | 13 +- .../jobs_list_view/jobs_list_view.js | 22 +- .../jobs/jobs_list/components/utils.d.ts | 2 - .../jobs/jobs_list/components/utils.js | 19 -- .../jobs/jobs_list/components/utils.test.ts | 14 +- .../application/jobs/jobs_list/jobs.tsx | 45 +++- .../jobs_list_page/jobs_list_page.tsx | 99 ++++++--- .../anomaly_detection_urls_generator.ts | 19 +- .../ml_url_generator/ml_url_generator.test.ts | 4 +- .../ml_popover/jobs_table/jobs_table.test.tsx | 4 +- 19 files changed, 391 insertions(+), 325 deletions(-) rename x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/{index.js => index.ts} (100%) delete mode 100644 x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js create mode 100644 x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx index be00364cab92e..30d4bb34ea345 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx @@ -19,6 +19,6 @@ test('MLLink produces the correct URL', async () => { ); expect(href).toMatchInlineSnapshot( - `"/app/ml/jobs?mlManagement=(groupIds:!(apm),jobId:!(something))&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-5h,to:now-2h))"` + `"/app/ml/jobs?_a=(queryText:'id:(something)%20groups:(apm)')&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-5h,to:now-2h))"` ); }); diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_flyout/module_list_card.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_flyout/module_list_card.tsx index 67f2c8d58ec0d..39c21fdc228df 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_flyout/module_list_card.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/setup_flyout/module_list_card.tsx @@ -6,10 +6,11 @@ import { EuiCard, EuiIcon, EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { SetupStatus } from '../../../../../common/log_analysis'; import { CreateJobButton, RecreateJobButton } from '../../log_analysis_setup/create_job_button'; -import { useLinkProps } from '../../../../hooks/use_link_props'; +import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; +import { mountReactNode } from '../../../../../../../../src/core/public/utils'; export const LogAnalysisModuleListCard: React.FC<{ jobId: string; @@ -26,6 +27,39 @@ export const LogAnalysisModuleListCard: React.FC<{ moduleStatus, onViewSetup, }) => { + const { + services: { + ml, + application: { navigateToUrl }, + notifications: { toasts }, + }, + } = useKibanaContextForPlugin(); + + const [viewInMlLink, setViewInMlLink] = useState(''); + + const getMlUrl = async () => { + if (!ml.urlGenerator) { + toasts.addWarning({ + title: mountReactNode( + + ), + }); + return; + } + setViewInMlLink(await ml.urlGenerator.createUrl({ page: 'jobs', pageState: { jobId } })); + }; + + useEffect(() => { + getMlUrl(); + }); + + const navigateToMlApp = async () => { + await navigateToUrl(viewInMlLink); + }; + const moduleIcon = moduleStatus.type === 'required' ? ( @@ -33,12 +67,6 @@ export const LogAnalysisModuleListCard: React.FC<{ ); - const viewInMlLinkProps = useLinkProps({ - app: 'ml', - pathname: '/jobs', - search: { mlManagement: `(jobId:${jobId})` }, - }); - const moduleSetupButton = moduleStatus.type === 'required' ? ( @@ -50,13 +78,17 @@ export const LogAnalysisModuleListCard: React.FC<{ ) : ( <> - - - - + {viewInMlLink ? ( + <> + + + + + + ) : null} ); diff --git a/x-pack/plugins/infra/public/types.ts b/x-pack/plugins/infra/public/types.ts index 6ff699066eb15..116345b35fdce 100644 --- a/x-pack/plugins/infra/public/types.ts +++ b/x-pack/plugins/infra/public/types.ts @@ -18,6 +18,7 @@ import type { ObservabilityPluginStart, } from '../../observability/public'; import type { SpacesPluginStart } from '../../spaces/public'; +import { MlPluginStart } from '../../ml/public'; // Our own setup and start contract values export type InfraClientSetupExports = void; @@ -38,6 +39,7 @@ export interface InfraClientStartDeps { spaces: SpacesPluginStart; triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; usageCollection: UsageCollectionStart; + ml: MlPluginStart; } export type InfraClientCoreSetup = CoreSetup; diff --git a/x-pack/plugins/ml/common/util/string_utils.test.ts b/x-pack/plugins/ml/common/util/string_utils.test.ts index 8afc7e52c9fa5..3503e2be35e86 100644 --- a/x-pack/plugins/ml/common/util/string_utils.test.ts +++ b/x-pack/plugins/ml/common/util/string_utils.test.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { renderTemplate, getMedianStringLength, stringHash } from './string_utils'; +import { + renderTemplate, + getMedianStringLength, + stringHash, + getGroupQueryText, +} from './string_utils'; const strings: string[] = [ 'foo', @@ -54,4 +59,19 @@ describe('ML - string utils', () => { expect(hash1).not.toBe(hash2); }); }); + + describe('getGroupQueryText', () => { + const groupIdOne = 'test_group_id_1'; + const groupIdTwo = 'test_group_id_2'; + + it('should get query string for selected group ids', () => { + const actual = getGroupQueryText([groupIdOne, groupIdTwo]); + expect(actual).toBe(`groups:(${groupIdOne} or ${groupIdTwo})`); + }); + + it('should get query string for selected group id', () => { + const actual = getGroupQueryText([groupIdOne]); + expect(actual).toBe(`groups:(${groupIdOne})`); + }); + }); }); diff --git a/x-pack/plugins/ml/common/util/string_utils.ts b/x-pack/plugins/ml/common/util/string_utils.ts index b4591fd2943e6..4691bac0a065a 100644 --- a/x-pack/plugins/ml/common/util/string_utils.ts +++ b/x-pack/plugins/ml/common/util/string_utils.ts @@ -39,3 +39,11 @@ export function stringHash(str: string): number { } return hash < 0 ? hash * -2 : hash; } + +export function getGroupQueryText(groupIds: string[]): string { + return `groups:(${groupIds.join(' or ')})`; +} + +export function getJobQueryText(jobIds: string | string[]): string { + return Array.isArray(jobIds) ? `id:(${jobIds.join(' OR ')})` : jobIds; +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 8ed2436843e0e..17ef84179ce63 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -30,15 +30,13 @@ import { getTaskStateBadge, getJobTypeBadge, useColumns } from './use_columns'; import { ExpandedRow } from './expanded_row'; import { AnalyticStatsBarStats, StatsBar } from '../../../../../components/stats_bar'; import { CreateAnalyticsButton } from '../create_analytics_button'; -import { - getSelectedIdFromUrl, - getGroupQueryText, -} from '../../../../../jobs/jobs_list/components/utils'; +import { getSelectedIdFromUrl } from '../../../../../jobs/jobs_list/components/utils'; import { SourceSelection } from '../source_selection'; import { filterAnalytics } from '../../../../common/search_bar_filters'; import { AnalyticsEmptyPrompt } from './empty_prompt'; import { useTableSettings } from './use_table_settings'; import { RefreshAnalyticsListButton } from '../refresh_analytics_list_button'; +import { getGroupQueryText } from '../../../../../../../common/util/string_utils'; const filters: EuiSearchBarProps['filters'] = [ { diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/index.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/index.ts similarity index 100% rename from x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/index.js rename to x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/index.ts diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js deleted file mode 100644 index 08373542c1234..0000000000000 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.js +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import PropTypes from 'prop-types'; -import React, { Component, Fragment } from 'react'; - -import { ml } from '../../../../services/ml_api_service'; -import { JobGroup } from '../job_group'; -import { - getGroupQueryText, - getSelectedIdFromUrl, - clearSelectedJobIdFromUrl, - getJobQueryText, -} from '../utils'; - -import { EuiSearchBar, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -function loadGroups() { - return ml.jobs - .groups() - .then((groups) => { - return groups.map((g) => ({ - value: g.id, - view: ( -
- -   - - - -
- ), - })); - }) - .catch((error) => { - console.log(error); - return []; - }); -} - -export class JobFilterBar extends Component { - constructor(props) { - super(props); - - this.state = { error: null }; - this.setFilters = props.setFilters; - } - - urlFilterIdCleared = false; - - componentDidMount() { - // If job id is selected in url, filter table to that id - let defaultQueryText; - const { jobId, groupIds } = getSelectedIdFromUrl(window.location.href); - - if (groupIds !== undefined) { - defaultQueryText = getGroupQueryText(groupIds); - } else if (jobId !== undefined) { - defaultQueryText = getJobQueryText(jobId); - } - - if (defaultQueryText !== undefined) { - this.setState( - { - defaultQueryText, - }, - () => { - // trigger onChange with query for job id to trigger table filter - const query = EuiSearchBar.Query.parse(defaultQueryText); - this.onChange({ query }); - } - ); - } - } - - onChange = ({ query, error }) => { - if (error) { - this.setState({ error }); - } else { - if (query.text === '' && this.urlFilterIdCleared === false) { - this.urlFilterIdCleared = true; - clearSelectedJobIdFromUrl(window.location.href); - } - let clauses = []; - if (query && query.ast !== undefined && query.ast.clauses !== undefined) { - clauses = query.ast.clauses; - } - this.setFilters(clauses); - this.setState({ error: null }); - } - }; - - render() { - const { error, defaultQueryText } = this.state; - const filters = [ - { - type: 'field_value_toggle_group', - field: 'job_state', - items: [ - { - value: 'opened', - name: i18n.translate('xpack.ml.jobsList.jobFilterBar.openedLabel', { - defaultMessage: 'Opened', - }), - }, - { - value: 'closed', - name: i18n.translate('xpack.ml.jobsList.jobFilterBar.closedLabel', { - defaultMessage: 'Closed', - }), - }, - { - value: 'failed', - name: i18n.translate('xpack.ml.jobsList.jobFilterBar.failedLabel', { - defaultMessage: 'Failed', - }), - }, - ], - }, - { - type: 'field_value_toggle_group', - field: 'datafeed_state', - items: [ - { - value: 'started', - name: i18n.translate('xpack.ml.jobsList.jobFilterBar.startedLabel', { - defaultMessage: 'Started', - }), - }, - { - value: 'stopped', - name: i18n.translate('xpack.ml.jobsList.jobFilterBar.stoppedLabel', { - defaultMessage: 'Stopped', - }), - }, - ], - }, - { - type: 'field_value_selection', - field: 'groups', - name: i18n.translate('xpack.ml.jobsList.jobFilterBar.groupLabel', { - defaultMessage: 'Group', - }), - multiSelect: 'or', - cache: 10000, - options: () => loadGroups(), - }, - ]; - // if prop flag for default filter set to true - // set defaultQuery to job id and force trigger filter with onChange - pass it the query object for the job id - return ( - - - {defaultQueryText === undefined && ( - - )} - {defaultQueryText !== undefined && ( - - )} - - - - - - ); - } -} -JobFilterBar.propTypes = { - setFilters: PropTypes.func.isRequired, -}; - -function getError(error) { - if (error) { - return i18n.translate('xpack.ml.jobsList.jobFilterBar.invalidSearchErrorMessage', { - defaultMessage: 'Invalid search: {errorMessage}', - values: { errorMessage: error.message }, - }); - } - - return ''; -} diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx new file mode 100644 index 0000000000000..f0fa62b7a3d8a --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_filter_bar/job_filter_bar.tsx @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, Fragment, useCallback, useEffect, useMemo, useState } from 'react'; + +import { + EuiSearchBar, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + SearchFilterConfig, + EuiSearchBarProps, + Query, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +// @ts-ignore +import { JobGroup } from '../job_group'; +import { useMlKibana } from '../../../../contexts/kibana'; + +interface JobFilterBarProps { + jobId: string; + groupIds: string[]; + setFilters: (query: Query | null) => void; + queryText?: string; +} + +export const JobFilterBar: FC = ({ queryText, setFilters }) => { + const [error, setError] = useState(null); + const { + services: { + mlServices: { mlApiServices }, + }, + } = useMlKibana(); + + const loadGroups = useCallback(async () => { + try { + const response = await mlApiServices.jobs.groups(); + return response.map((g: any) => ({ + value: g.id, + view: ( +
+ +   + + + +
+ ), + })); + } catch (e) { + return []; + } + }, []); + + const queryInstance: Query = useMemo(() => { + return EuiSearchBar.Query.parse(queryText ?? ''); + }, [queryText]); + + const onChange: EuiSearchBarProps['onChange'] = ({ query, error: queryError }) => { + if (error) { + setError(queryError); + } else { + setFilters(query); + setError(null); + } + }; + + useEffect(() => { + setFilters(queryInstance); + }, []); + + const filters: SearchFilterConfig[] = useMemo( + () => [ + { + type: 'field_value_toggle_group', + field: 'job_state', + items: [ + { + value: 'opened', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.openedLabel', { + defaultMessage: 'Opened', + }), + }, + { + value: 'closed', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.closedLabel', { + defaultMessage: 'Closed', + }), + }, + { + value: 'failed', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.failedLabel', { + defaultMessage: 'Failed', + }), + }, + ], + }, + { + type: 'field_value_toggle_group', + field: 'datafeed_state', + items: [ + { + value: 'started', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.startedLabel', { + defaultMessage: 'Started', + }), + }, + { + value: 'stopped', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.stoppedLabel', { + defaultMessage: 'Stopped', + }), + }, + ], + }, + { + type: 'field_value_selection', + field: 'groups', + name: i18n.translate('xpack.ml.jobsList.jobFilterBar.groupLabel', { + defaultMessage: 'Group', + }), + multiSelect: 'or', + cache: 10000, + options: () => loadGroups(), + }, + ], + [] + ); + + const errorText = useMemo(() => { + if (error === null) return ''; + + return i18n.translate('xpack.ml.jobsList.jobFilterBar.invalidSearchErrorMessage', { + defaultMessage: 'Invalid search: {errorMessage}', + values: { errorMessage: error.message }, + }); + }, [error]); + + return ( + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js index c0abab6b52cf1..8a05cd51e4d65 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js @@ -22,7 +22,6 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { AnomalyDetectionJobIdLink } from './job_id_link'; -const PAGE_SIZE = 10; const PAGE_SIZE_OPTIONS = [10, 25, 50]; // 'isManagementTable' bool prop to determine when to configure table for use in Kibana management page @@ -32,11 +31,7 @@ export class JobsList extends Component { this.state = { jobsSummaryList: props.jobsSummaryList, - pageIndex: 0, - pageSize: PAGE_SIZE, itemIdToExpandedRowMap: {}, - sortField: 'id', - sortDirection: 'asc', }; } @@ -54,7 +49,7 @@ export class JobsList extends Component { const { field: sortField, direction: sortDirection } = sort; - this.setState({ + this.props.onJobsViewStateUpdate({ pageIndex, pageSize, sortField, @@ -88,7 +83,7 @@ export class JobsList extends Component { pageStart = Math.floor((listLength - 1) / size) * size; // set the state out of the render cycle setTimeout(() => { - this.setState({ + this.props.onJobsViewStateUpdate({ pageIndex: pageStart / size, }); }, 0); @@ -298,7 +293,7 @@ export class JobsList extends Component { }); } - const { pageIndex, pageSize, sortField, sortDirection } = this.state; + const { pageIndex, pageSize, sortField, sortDirection } = this.props.jobsViewState; const { pageOfItems, totalItemCount } = this.getPageOfJobs( pageIndex, @@ -368,6 +363,8 @@ JobsList.propTypes = { refreshJobs: PropTypes.func, selectedJobsCount: PropTypes.number.isRequired, loading: PropTypes.bool, + jobsViewState: PropTypes.object, + onJobsViewStateUpdate: PropTypes.func, }; JobsList.defaultProps = { isManagementTable: false, diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index 9eb7a03f0f5d7..570172abb28c1 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -222,8 +222,14 @@ export class JobsListView extends Component { this.setState({ selectedJobs }); } - setFilters = (filterClauses) => { + setFilters = (query) => { + const filterClauses = (query && query.ast && query.ast.clauses) || []; const filteredJobsSummaryList = filterJobs(this.state.jobsSummaryList, filterClauses); + + this.props.onJobsViewStateUpdate({ + queryText: query?.text, + }); + this.setState({ filteredJobsSummaryList, filterClauses }, () => { this.refreshSelectedJobs(); }); @@ -358,7 +364,10 @@ export class JobsListView extends Component {
- +
@@ -434,7 +445,10 @@ export class JobsListView extends Component { showDeleteJobModal={this.showDeleteJobModal} refreshJobs={() => this.refreshJobSummaryList(true)} /> - + this.refreshJobSummaryList(true)} + jobsViewState={this.props.jobsViewState} + onJobsViewStateUpdate={this.props.onJobsViewStateUpdate} selectedJobsCount={this.state.selectedJobs.length} loading={loading} /> diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts index 75d6b149fda08..b781199c85237 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.d.ts @@ -5,6 +5,4 @@ */ export function getSelectedIdFromUrl(str: string): { groupIds?: string[]; jobId?: string }; -export function getGroupQueryText(arr: string[]): string; -export function getJobQueryText(arr: string | string[]): string; export function clearSelectedJobIdFromUrl(str: string): void; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js index bc85153928a4b..397062248689d 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js @@ -395,22 +395,3 @@ export function getSelectedIdFromUrl(url) { } return result; } - -export function getGroupQueryText(groupIds) { - return `groups:(${groupIds.join(' or ')})`; -} - -export function getJobQueryText(jobIds) { - return Array.isArray(jobIds) ? `id:(${jobIds.join(' OR ')})` : jobIds; -} - -export function clearSelectedJobIdFromUrl(url) { - if (typeof url === 'string') { - url = decodeURIComponent(url); - if (url.includes('mlManagement') && (url.includes('jobId') || url.includes('groupIds'))) { - const urlParams = getUrlVars(url); - const clearedParams = `jobs?_g=${urlParams._g}`; - window.history.replaceState({}, document.title, clearedParams); - } - } -} diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.test.ts b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.test.ts index e4c3c21c5a54a..4414be0b4fdcb 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.test.ts +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getGroupQueryText, getSelectedIdFromUrl } from './utils'; +import { getSelectedIdFromUrl } from './utils'; describe('ML - Jobs List utils', () => { const jobId = 'test_job_id_1'; @@ -32,16 +32,4 @@ describe('ML - Jobs List utils', () => { expect(actual).toStrictEqual(expected); }); }); - - describe('getGroupQueryText', () => { - it('should get query string for selected group ids', () => { - const actual = getGroupQueryText([groupIdOne, groupIdTwo]); - expect(actual).toBe(`groups:(${groupIdOne} or ${groupIdTwo})`); - }); - - it('should get query string for selected group id', () => { - const actual = getGroupQueryText([groupIdOne]); - expect(actual).toBe(`groups:(${groupIdOne})`); - }); - }); }); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx index 1e45f28594572..4c6469f6800a7 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/jobs.tsx @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; - +import React, { FC, useCallback, useMemo } from 'react'; import { NavigationMenu } from '../../components/navigation_menu'; - // @ts-ignore import { JobsListView } from './components/jobs_list_view/index'; +import { useUrlState } from '../../util/url_state'; interface JobsPageProps { blockRefresh?: boolean; @@ -18,11 +17,49 @@ interface JobsPageProps { lastRefresh?: number; } +export interface AnomalyDetectionJobsListState { + pageSize: number; + pageIndex: number; + sortField: string; + sortDirection: string; + queryText?: string; +} + +export const getDefaultAnomalyDetectionJobsListState = (): AnomalyDetectionJobsListState => ({ + pageIndex: 0, + pageSize: 10, + sortField: 'id', + sortDirection: 'asc', +}); + export const JobsPage: FC = (props) => { + const [appState, setAppState] = useUrlState('_a'); + + const jobListState: AnomalyDetectionJobsListState = useMemo(() => { + return { + ...getDefaultAnomalyDetectionJobsListState(), + ...(appState ?? {}), + }; + }, [appState]); + + const onJobsViewStateUpdate = useCallback( + (update: Partial) => { + setAppState({ + ...jobListState, + ...update, + }); + }, + [appState, setAppState] + ); + return (
- +
); }; diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index 61dfea8897e82..ad4b9ad78902b 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useState, Fragment, FC } from 'react'; +import React, { useEffect, useState, Fragment, FC, useMemo, useCallback } from 'react'; import { Router } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { CoreStart } from 'kibana/public'; @@ -35,6 +35,11 @@ import { JobsListView } from '../../../../jobs/jobs_list/components/jobs_list_vi import { DataFrameAnalyticsList } from '../../../../data_frame_analytics/pages/analytics_management/components/analytics_list'; import { AccessDeniedPage } from '../access_denied_page'; import { SharePluginStart } from '../../../../../../../../../src/plugins/share/public'; +import { + AnomalyDetectionJobsListState, + getDefaultAnomalyDetectionJobsListState, +} from '../../../../jobs/jobs_list/jobs'; +import { getMlGlobalServices } from '../../../../app'; interface Tab { 'data-test-subj': string; @@ -43,38 +48,60 @@ interface Tab { content: any; } -function getTabs(isMlEnabledInSpace: boolean): Tab[] { - return [ - { - 'data-test-subj': 'mlStackManagementJobsListAnomalyDetectionTab', - id: 'anomaly_detection_jobs', - name: i18n.translate('xpack.ml.management.jobsList.anomalyDetectionTab', { - defaultMessage: 'Anomaly detection', - }), - content: ( - - - - - ), - }, - { - 'data-test-subj': 'mlStackManagementJobsListAnalyticsTab', - id: 'analytics_jobs', - name: i18n.translate('xpack.ml.management.jobsList.analyticsTab', { - defaultMessage: 'Analytics', - }), - content: ( - - - - - ), +function useTabs(isMlEnabledInSpace: boolean): Tab[] { + const [jobsViewState, setJobsViewState] = useState( + getDefaultAnomalyDetectionJobsListState() + ); + + const updateState = useCallback( + (update: Partial) => { + setJobsViewState({ + ...jobsViewState, + ...update, + }); }, - ]; + [jobsViewState] + ); + + return useMemo( + () => [ + { + 'data-test-subj': 'mlStackManagementJobsListAnomalyDetectionTab', + id: 'anomaly_detection_jobs', + name: i18n.translate('xpack.ml.management.jobsList.anomalyDetectionTab', { + defaultMessage: 'Anomaly detection', + }), + content: ( + + + + + ), + }, + { + 'data-test-subj': 'mlStackManagementJobsListAnalyticsTab', + id: 'analytics_jobs', + name: i18n.translate('xpack.ml.management.jobsList.analyticsTab', { + defaultMessage: 'Analytics', + }), + content: ( + + + + + ), + }, + ], + [isMlEnabledInSpace, jobsViewState, updateState] + ); } export const JobsListPage: FC<{ @@ -85,7 +112,7 @@ export const JobsListPage: FC<{ const [initialized, setInitialized] = useState(false); const [accessDenied, setAccessDenied] = useState(false); const [isMlEnabledInSpace, setIsMlEnabledInSpace] = useState(false); - const tabs = getTabs(isMlEnabledInSpace); + const tabs = useTabs(isMlEnabledInSpace); const [currentTabId, setCurrentTabId] = useState(tabs[0].id); const I18nContext = coreStart.i18n.Context; @@ -129,7 +156,7 @@ export const JobsListPage: FC<{ setCurrentTabId(id); }} size="s" - tabs={getTabs(isMlEnabledInSpace)} + tabs={tabs} initialSelectedTab={tabs[0]} /> ); @@ -142,7 +169,9 @@ export const JobsListPage: FC<{ return ( - + = { + ...(queryTextArr.length > 0 ? { queryText: queryTextArr.join(' ') } : {}), }; - url = setStateToKbnUrl( - 'mlManagement', + url = setStateToKbnUrl>( + '_a', queryState, { useHash: false, storeInHashQuery: false }, url diff --git a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts index 754f5bec57a07..e7f12ead3ffe9 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts @@ -30,7 +30,7 @@ describe('MlUrlGenerator', () => { jobId: 'fq_single_1', }, }); - expect(url).toBe('/app/ml/jobs?mlManagement=(jobId:fq_single_1)'); + expect(url).toBe('/app/ml/jobs?_a=(queryText:fq_single_1)'); }); it('should generate valid URL for the Anomaly Detection job management page for groupIds', async () => { @@ -40,7 +40,7 @@ describe('MlUrlGenerator', () => { groupIds: ['farequote', 'categorization'], }, }); - expect(url).toBe('/app/ml/jobs?mlManagement=(groupIds:!(farequote,categorization))'); + expect(url).toBe("/app/ml/jobs?_a=(queryText:'groups:(farequote%20or%20categorization)')"); }); it('should generate valid URL for the page for selecting the type of anomaly detection job to create', async () => { diff --git a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx index 156475f63aa65..b0965f8708558 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx @@ -55,7 +55,7 @@ describe('JobsTableComponent', () => { '[data-test-subj="jobs-table-link"]' ); await waitFor(() => - expect(href).toEqual('/app/ml/jobs?mlManagement=(jobId:linux_anomalous_network_activity_ecs)') + expect(href).toEqual('/app/ml/jobs?_a=(queryText:linux_anomalous_network_activity_ecs)') ); }); @@ -72,7 +72,7 @@ describe('JobsTableComponent', () => { '[data-test-subj="jobs-table-link"]' ); await waitFor(() => - expect(href).toEqual("/app/ml/jobs?mlManagement=(jobId:'job%20id%20with%20spaces')") + expect(href).toEqual("/app/ml/jobs?_a=(queryText:'job%20id%20with%20spaces')") ); }); From 35656b9921d1a0adcfeb1ad2ca54f9d96aa601f4 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 12 Nov 2020 08:28:35 -0600 Subject: [PATCH 05/17] Add additional sources routes (#83227) These were missed in #83125 --- .../routes/workplace_search/sources.test.ts | 188 ++++++++++++++++++ .../server/routes/workplace_search/sources.ts | 124 ++++++++++++ 2 files changed, 312 insertions(+) diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts index 9f2b4121351bc..6d22002222a66 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts @@ -7,6 +7,8 @@ import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; import { + registerAccountSourcesRoute, + registerAccountSourcesStatusRoute, registerAccountSourceRoute, registerAccountCreateSourceRoute, registerAccountSourceDocumentsRoute, @@ -15,6 +17,9 @@ import { registerAccountSourceSettingsRoute, registerAccountPreSourceRoute, registerAccountPrepareSourcesRoute, + registerAccountSourceSearchableRoute, + registerOrgSourcesRoute, + registerOrgSourcesStatusRoute, registerOrgSourceRoute, registerOrgCreateSourceRoute, registerOrgSourceDocumentsRoute, @@ -23,6 +28,7 @@ import { registerOrgSourceSettingsRoute, registerOrgPreSourceRoute, registerOrgPrepareSourcesRoute, + registerOrgSourceSearchableRoute, registerOrgSourceOauthConfigurationsRoute, registerOrgSourceOauthConfigurationRoute, } from './sources'; @@ -38,6 +44,60 @@ const mockConfig = { }; describe('sources routes', () => { + describe('GET /api/workplace_search/account/sources', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/account/sources', + payload: 'params', + }); + + registerAccountSourcesRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + mockRouter.callRoute({}); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/sources', + }); + }); + }); + + describe('GET /api/workplace_search/account/sources/status', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/account/sources/status', + payload: 'params', + }); + + registerAccountSourcesStatusRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + mockRouter.callRoute({}); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/sources/status', + }); + }); + }); + describe('GET /api/workplace_search/account/sources/{id}', () => { let mockRouter: MockRouter; @@ -351,6 +411,97 @@ describe('sources routes', () => { }); }); + describe('PUT /api/workplace_search/sources/{id}/searchable', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'put', + path: '/api/workplace_search/sources/{id}/searchable', + payload: 'body', + }); + + registerAccountSourceSearchableRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + const mockRequest = { + params: { + id: '123', + }, + body: { + searchable: true, + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/sources/123/searchable', + body: mockRequest.body, + }); + }); + }); + + describe('GET /api/workplace_search/org/sources', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/org/sources', + payload: 'params', + }); + + registerOrgSourcesRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + mockRouter.callRoute({}); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/sources', + }); + }); + }); + + describe('GET /api/workplace_search/org/sources/status', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/org/sources/status', + payload: 'params', + }); + + registerOrgSourcesStatusRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + mockRouter.callRoute({}); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/sources/status', + }); + }); + }); + describe('GET /api/workplace_search/org/sources/{id}', () => { let mockRouter: MockRouter; @@ -664,6 +815,43 @@ describe('sources routes', () => { }); }); + describe('PUT /api/workplace_search/org/sources/{id}/searchable', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a request handler', () => { + mockRouter = new MockRouter({ + method: 'put', + path: '/api/workplace_search/org/sources/{id}/searchable', + payload: 'body', + }); + + registerOrgSourceSearchableRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + + const mockRequest = { + params: { + id: '123', + }, + body: { + searchable: true, + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/sources/123/searchable', + body: mockRequest.body, + }); + }); + }); + describe('GET /api/workplace_search/org/settings/connectors', () => { let mockRouter: MockRouter; diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index f496628d02379..efef53440117e 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -25,6 +25,40 @@ const oAuthConfigSchema = schema.object({ consumer_key: schema.string(), }); +export function registerAccountSourcesRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/account/sources', + validate: false, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: '/ws/sources', + })(context, request, response); + } + ); +} + +export function registerAccountSourcesStatusRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/account/sources/status', + validate: false, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: '/ws/sources/status', + })(context, request, response); + } + ); +} + export function registerAccountSourceRoute({ router, enterpriseSearchRequestHandler, @@ -228,6 +262,65 @@ export function registerAccountPrepareSourcesRoute({ ); } +export function registerAccountSourceSearchableRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.put( + { + path: '/api/workplace_search/sources/{id}/searchable', + validate: { + body: schema.object({ + searchable: schema.boolean(), + }), + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/ws/sources/${request.params.id}/searchable`, + body: request.body, + })(context, request, response); + } + ); +} + +export function registerOrgSourcesRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/org/sources', + validate: false, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/sources', + })(context, request, response); + } + ); +} + +export function registerOrgSourcesStatusRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/org/sources/status', + validate: false, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/sources/status', + })(context, request, response); + } + ); +} + export function registerOrgSourceRoute({ router, enterpriseSearchRequestHandler, @@ -431,6 +524,31 @@ export function registerOrgPrepareSourcesRoute({ ); } +export function registerOrgSourceSearchableRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.put( + { + path: '/api/workplace_search/org/sources/{id}/searchable', + validate: { + body: schema.object({ + searchable: schema.boolean(), + }), + params: schema.object({ + id: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/ws/org/sources/${request.params.id}/searchable`, + body: request.body, + })(context, request, response); + } + ); +} + export function registerOrgSourceOauthConfigurationsRoute({ router, enterpriseSearchRequestHandler, @@ -522,6 +640,8 @@ export function registerOrgSourceOauthConfigurationRoute({ } export const registerSourcesRoutes = (dependencies: RouteDependencies) => { + registerAccountSourcesRoute(dependencies); + registerAccountSourcesStatusRoute(dependencies); registerAccountSourceRoute(dependencies); registerAccountCreateSourceRoute(dependencies); registerAccountSourceDocumentsRoute(dependencies); @@ -530,6 +650,9 @@ export const registerSourcesRoutes = (dependencies: RouteDependencies) => { registerAccountSourceSettingsRoute(dependencies); registerAccountPreSourceRoute(dependencies); registerAccountPrepareSourcesRoute(dependencies); + registerAccountSourceSearchableRoute(dependencies); + registerOrgSourcesRoute(dependencies); + registerOrgSourcesStatusRoute(dependencies); registerOrgSourceRoute(dependencies); registerOrgCreateSourceRoute(dependencies); registerOrgSourceDocumentsRoute(dependencies); @@ -538,6 +661,7 @@ export const registerSourcesRoutes = (dependencies: RouteDependencies) => { registerOrgSourceSettingsRoute(dependencies); registerOrgPreSourceRoute(dependencies); registerOrgPrepareSourcesRoute(dependencies); + registerOrgSourceSearchableRoute(dependencies); registerOrgSourceOauthConfigurationsRoute(dependencies); registerOrgSourceOauthConfigurationRoute(dependencies); }; From c3e57943ad377e29b9d6f4a2508a7d7ed1e0f06f Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Thu, 12 Nov 2020 09:32:22 -0500 Subject: [PATCH 06/17] [alerts] add executionStatus to event log doc for action execute (#82401) resolves https://github.com/elastic/kibana/issues/79785 Until now, the execution status was available in the the event log document for the execute action. In this PR we add it. The event log is extended to add the following fields: - `kibana.alerting.status` - from executionStatus.status - `event.reason` - from executionStatus.error.reason The date from the executionStatus and start date in the event log will be set to the same value. Previously, errors encountered while trying to execute an alert executor, eg decrypting the alert, would not end up with an event doc generated. Now they will. In addition, there were a few places where events that could have had the action group in them did not, and one where the instance id was undefined - those were fixed up. --- .../server/alert_instance/alert_instance.ts | 4 + .../create_execution_handler.test.ts | 1 + .../task_runner/create_execution_handler.ts | 1 + .../server/task_runner/task_runner.test.ts | 231 ++++++++++++++---- .../alerts/server/task_runner/task_runner.ts | 115 ++++++--- .../plugins/event_log/generated/mappings.json | 8 + x-pack/plugins/event_log/generated/schemas.ts | 4 +- x-pack/plugins/event_log/scripts/mappings.js | 6 + .../event_log/server/event_logger.test.ts | 6 +- .../plugins/event_log/server/event_logger.ts | 7 +- .../tests/alerting/event_log.ts | 84 +++++++ .../tests/alerting/index.ts | 1 + .../spaces_only/tests/alerting/event_log.ts | 106 +++++--- 13 files changed, 442 insertions(+), 132 deletions(-) create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts diff --git a/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts b/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts index 661fb75f81c00..01790b2a4a0c0 100644 --- a/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts +++ b/x-pack/plugins/alerts/server/alert_instance/alert_instance.ts @@ -51,6 +51,10 @@ export class AlertInstance< return false; } + getLastScheduledActions() { + return this.meta.lastScheduledActions; + } + getScheduledActionOptions() { return this.scheduledExecutionOptions; } diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts index 2f0754d34492f..ed73fec24db26 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts @@ -113,6 +113,7 @@ test('enqueues execution per selected action', async () => { }, "kibana": Object { "alerting": Object { + "action_group_id": "default", "instance_id": "2", }, "saved_objects": Array [ diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts index 21e642d228b4d..f49310c42c247 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts @@ -116,6 +116,7 @@ export function createExecutionHandler({ kibana: { alerting: { instance_id: alertInstanceId, + action_group_id: actionGroup, }, saved_objects: [ { rel: SAVED_OBJECT_REL_PRIMARY, type: 'alert', id: alertId, ...namespace }, diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index 4d0d69010914e..859b6ec4362ce 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -184,11 +184,15 @@ describe('Task Runner', () => { expect(eventLogger.logEvent).toHaveBeenCalledTimes(1); expect(eventLogger.logEvent.mock.calls[0][0]).toMatchInlineSnapshot(` Object { + "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { "action": "execute", "outcome": "success", }, "kibana": Object { + "alerting": Object { + "status": "ok", + }, "saved_objects": Array [ Object { "id": "1", @@ -249,29 +253,13 @@ describe('Task Runner', () => { const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; expect(eventLogger.logEvent).toHaveBeenCalledTimes(4); - expect(eventLogger.logEvent).toHaveBeenCalledWith({ - event: { - action: 'execute', - outcome: 'success', - }, - kibana: { - saved_objects: [ - { - id: '1', - namespace: undefined, - rel: 'primary', - type: 'alert', - }, - ], - }, - message: "alert executed: test:1: 'alert-name'", - }); - expect(eventLogger.logEvent).toHaveBeenCalledWith({ + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(1, { event: { action: 'new-instance', }, kibana: { alerting: { + action_group_id: 'default', instance_id: '1', }, saved_objects: [ @@ -285,7 +273,7 @@ describe('Task Runner', () => { }, message: "test:1: 'alert-name' created new instance: '1'", }); - expect(eventLogger.logEvent).toHaveBeenCalledWith({ + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(2, { event: { action: 'active-instance', }, @@ -305,13 +293,14 @@ describe('Task Runner', () => { }, message: "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", }); - expect(eventLogger.logEvent).toHaveBeenCalledWith({ + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(3, { event: { action: 'execute-action', }, kibana: { alerting: { instance_id: '1', + action_group_id: 'default', }, saved_objects: [ { @@ -330,6 +319,27 @@ describe('Task Runner', () => { message: "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: action:1", }); + expect(eventLogger.logEvent).toHaveBeenNthCalledWith(4, { + '@timestamp': '1970-01-01T00:00:00.000Z', + event: { + action: 'execute', + outcome: 'success', + }, + kibana: { + alerting: { + status: 'active', + }, + saved_objects: [ + { + id: '1', + namespace: undefined, + rel: 'primary', + type: 'alert', + }, + ], + }, + message: "alert executed: test:1: 'alert-name'", + }); }); test('includes the apiKey in the request used to initialize the actionsClient', async () => { @@ -402,10 +412,13 @@ describe('Task Runner', () => { Array [ Object { "event": Object { - "action": "execute", - "outcome": "success", + "action": "new-instance", }, "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "1", + }, "saved_objects": Array [ Object { "id": "1", @@ -415,17 +428,17 @@ describe('Task Runner', () => { }, ], }, - "message": "alert executed: test:1: 'alert-name'", + "message": "test:1: 'alert-name' created new instance: '1'", }, ], Array [ Object { "event": Object { - "action": "new-instance", + "action": "active-instance", }, "kibana": Object { "alerting": Object { - "action_group_id": undefined, + "action_group_id": "default", "instance_id": "1", }, "saved_objects": Array [ @@ -437,13 +450,13 @@ describe('Task Runner', () => { }, ], }, - "message": "test:1: 'alert-name' created new instance: '1'", + "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", }, ], Array [ Object { "event": Object { - "action": "active-instance", + "action": "execute-action", }, "kibana": Object { "alerting": Object { @@ -457,19 +470,26 @@ describe('Task Runner', () => { "rel": "primary", "type": "alert", }, + Object { + "id": "1", + "namespace": undefined, + "type": "action", + }, ], }, - "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + "message": "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: action:1", }, ], Array [ Object { + "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { - "action": "execute-action", + "action": "execute", + "outcome": "success", }, "kibana": Object { "alerting": Object { - "instance_id": "1", + "status": "active", }, "saved_objects": Array [ Object { @@ -478,14 +498,9 @@ describe('Task Runner', () => { "rel": "primary", "type": "alert", }, - Object { - "id": "1", - "namespace": undefined, - "type": "action", - }, ], }, - "message": "alert: test:1: 'alert-name' instanceId: '1' scheduled actionGroup: 'default' action: action:1", + "message": "alert executed: test:1: 'alert-name'", }, ], ] @@ -498,6 +513,7 @@ describe('Task Runner', () => { executorServices.alertInstanceFactory('1').scheduleActions('default'); } ); + const date = new Date().toISOString(); const taskRunner = new TaskRunner( alertType, { @@ -505,8 +521,14 @@ describe('Task Runner', () => { state: { ...mockedTaskInstance.state, alertInstances: { - '1': { meta: {}, state: { bar: false } }, - '2': { meta: {}, state: { bar: false } }, + '1': { + meta: { lastScheduledActions: { group: 'default', date } }, + state: { bar: false }, + }, + '2': { + meta: { lastScheduledActions: { group: 'default', date } }, + state: { bar: false }, + }, }, }, }, @@ -545,10 +567,13 @@ describe('Task Runner', () => { Array [ Object { "event": Object { - "action": "execute", - "outcome": "success", + "action": "resolved-instance", }, "kibana": Object { + "alerting": Object { + "action_group_id": "default", + "instance_id": "2", + }, "saved_objects": Array [ Object { "id": "1", @@ -558,18 +583,18 @@ describe('Task Runner', () => { }, ], }, - "message": "alert executed: test:1: 'alert-name'", + "message": "test:1: 'alert-name' resolved instance: '2'", }, ], Array [ Object { "event": Object { - "action": "resolved-instance", + "action": "active-instance", }, "kibana": Object { "alerting": Object { - "action_group_id": undefined, - "instance_id": "2", + "action_group_id": "default", + "instance_id": "1", }, "saved_objects": Array [ Object { @@ -580,18 +605,19 @@ describe('Task Runner', () => { }, ], }, - "message": "test:1: 'alert-name' resolved instance: '2'", + "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", }, ], Array [ Object { + "@timestamp": "1970-01-01T00:00:00.000Z", "event": Object { - "action": "active-instance", + "action": "execute", + "outcome": "success", }, "kibana": Object { "alerting": Object { - "action_group_id": "default", - "instance_id": "1", + "status": "active", }, "saved_objects": Array [ Object { @@ -602,7 +628,7 @@ describe('Task Runner', () => { }, ], }, - "message": "test:1: 'alert-name' active instance: '1' in actionGroup: 'default'", + "message": "alert executed: test:1: 'alert-name'", }, ], ] @@ -787,14 +813,19 @@ describe('Task Runner', () => { Array [ Array [ Object { + "@timestamp": "1970-01-01T00:00:00.000Z", "error": Object { "message": "OMG", }, "event": Object { "action": "execute", "outcome": "failure", + "reason": "execute", }, "kibana": Object { + "alerting": Object { + "status": "error", + }, "saved_objects": Array [ Object { "id": "1", @@ -834,6 +865,40 @@ describe('Task Runner', () => { "state": Object {}, } `); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(1); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "error": Object { + "message": "OMG", + }, + "event": Object { + "action": "execute", + "outcome": "failure", + "reason": "decrypt", + }, + "kibana": Object { + "alerting": Object { + "status": "error", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + }, + ], + }, + "message": "test:1: execution failed", + }, + ], + ] + `); }); test('recovers gracefully when the Alert Task Runner throws an exception when getting internal Services', async () => { @@ -867,6 +932,40 @@ describe('Task Runner', () => { "state": Object {}, } `); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(1); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "error": Object { + "message": "OMG", + }, + "event": Object { + "action": "execute", + "outcome": "failure", + "reason": "unknown", + }, + "kibana": Object { + "alerting": Object { + "status": "error", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + }, + ], + }, + "message": "test:1: execution failed", + }, + ], + ] + `); }); test('recovers gracefully when the Alert Task Runner throws an exception when fetching attributes', async () => { @@ -899,6 +998,40 @@ describe('Task Runner', () => { "state": Object {}, } `); + + const eventLogger = taskRunnerFactoryInitializerParams.eventLogger; + expect(eventLogger.logEvent).toHaveBeenCalledTimes(1); + expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "@timestamp": "1970-01-01T00:00:00.000Z", + "error": Object { + "message": "OMG", + }, + "event": Object { + "action": "execute", + "outcome": "failure", + "reason": "read", + }, + "kibana": Object { + "alerting": Object { + "status": "error", + }, + "saved_objects": Array [ + Object { + "id": "1", + "namespace": undefined, + "rel": "primary", + "type": "alert", + }, + ], + }, + "message": "test:1: execution failed", + }, + ], + ] + `); }); test('recovers gracefully when the Runner of a legacy Alert task which has no schedule throws an exception when fetching attributes', async () => { diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index 86bf7006e8d09..5bccf5c497a60 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { Dictionary, pickBy, mapValues, without } from 'lodash'; +import { Dictionary, pickBy, mapValues, without, cloneDeep } from 'lodash'; import { Logger, KibanaRequest } from '../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; import { ConcreteTaskInstance, throwUnrecoverableError } from '../../../task_manager/server'; @@ -40,6 +40,8 @@ import { partiallyUpdateAlert } from '../saved_objects'; const FALLBACK_RETRY_INTERVAL = '5m'; +type Event = Exclude; + interface AlertTaskRunResult { state: AlertTaskState; schedule: IntervalSchedule | undefined; @@ -153,7 +155,8 @@ export class TaskRunner { alert: SanitizedAlert, params: AlertExecutorOptions['params'], executionHandler: ReturnType, - spaceId: string + spaceId: string, + event: Event ): Promise { const { throttle, muteAll, mutedInstanceIds, name, tags, createdBy, updatedBy } = alert; const { @@ -166,24 +169,10 @@ export class TaskRunner { alertRawInstances, (rawAlertInstance) => new AlertInstance(rawAlertInstance) ); + const originalAlertInstances = cloneDeep(alertInstances); - const originalAlertInstanceIds = Object.keys(alertInstances); const eventLogger = this.context.eventLogger; const alertLabel = `${this.alertType.id}:${alertId}: '${name}'`; - const event: IEvent = { - event: { action: EVENT_LOG_ACTIONS.execute }, - kibana: { - saved_objects: [ - { - rel: SAVED_OBJECT_REL_PRIMARY, - type: 'alert', - id: alertId, - namespace, - }, - ], - }, - }; - eventLogger.startTiming(event); let updatedAlertTypeState: void | Record; try { @@ -205,21 +194,17 @@ export class TaskRunner { updatedBy, }); } catch (err) { - eventLogger.stopTiming(event); event.message = `alert execution failure: ${alertLabel}`; event.error = event.error || {}; event.error.message = err.message; event.event = event.event || {}; event.event.outcome = 'failure'; - eventLogger.logEvent(event); throw new ErrorWithReason(AlertExecutionStatusErrorReasons.Execute, err); } - eventLogger.stopTiming(event); event.message = `alert executed: ${alertLabel}`; event.event = event.event || {}; event.event.outcome = 'success'; - eventLogger.logEvent(event); // Cleanup alert instances that are no longer scheduling actions to avoid over populating the alertInstances object const instancesWithScheduledActions = pickBy(alertInstances, (alertInstance: AlertInstance) => @@ -227,7 +212,7 @@ export class TaskRunner { ); generateNewAndResolvedInstanceEvents({ eventLogger, - originalAlertInstanceIds, + originalAlertInstances, currentAlertInstances: instancesWithScheduledActions, alertId, alertLabel, @@ -261,7 +246,8 @@ export class TaskRunner { async validateAndExecuteAlert( services: Services, apiKey: RawAlert['apiKey'], - alert: SanitizedAlert + alert: SanitizedAlert, + event: Event ) { const { params: { alertId, spaceId }, @@ -278,10 +264,17 @@ export class TaskRunner { alert.actions, alert.params ); - return this.executeAlertInstances(services, alert, validatedParams, executionHandler, spaceId); + return this.executeAlertInstances( + services, + alert, + validatedParams, + executionHandler, + spaceId, + event + ); } - async loadAlertAttributesAndRun(): Promise> { + async loadAlertAttributesAndRun(event: Event): Promise> { const { params: { alertId, spaceId }, } = this.taskInstance; @@ -304,7 +297,7 @@ export class TaskRunner { return { state: await promiseResult( - this.validateAndExecuteAlert(services, apiKey, alert) + this.validateAndExecuteAlert(services, apiKey, alert, event) ), schedule: asOk( // fetch the alert again to ensure we return the correct schedule as it may have @@ -322,18 +315,65 @@ export class TaskRunner { schedule: taskSchedule, } = this.taskInstance; - const { state, schedule } = await errorAsAlertTaskRunResult(this.loadAlertAttributesAndRun()); - const namespace = spaceId === 'default' ? undefined : spaceId; + const namespace = this.context.spaceIdToNamespace(spaceId); + const eventLogger = this.context.eventLogger; + const event: IEvent = { + // explicitly set execute timestamp so it will be before other events + // generated here (new-instance, schedule-action, etc) + '@timestamp': new Date().toISOString(), + event: { action: EVENT_LOG_ACTIONS.execute }, + kibana: { + saved_objects: [ + { + rel: SAVED_OBJECT_REL_PRIMARY, + type: 'alert', + id: alertId, + namespace, + }, + ], + }, + }; + eventLogger.startTiming(event); + + const { state, schedule } = await errorAsAlertTaskRunResult( + this.loadAlertAttributesAndRun(event) + ); const executionStatus: AlertExecutionStatus = map( state, (alertTaskState: AlertTaskState) => executionStatusFromState(alertTaskState), (err: Error) => executionStatusFromError(err) ); + + // set the executionStatus date to same as event, if it's set + if (event.event?.start) { + executionStatus.lastExecutionDate = new Date(event.event.start); + } + this.logger.debug( `alertExecutionStatus for ${this.alertType.id}:${alertId}: ${JSON.stringify(executionStatus)}` ); + eventLogger.stopTiming(event); + event.kibana = event.kibana || {}; + event.kibana.alerting = event.kibana.alerting || {}; + event.kibana.alerting.status = executionStatus.status; + + // if executionStatus indicates an error, fill in fields in + // event from it + if (executionStatus.error) { + event.event = event.event || {}; + event.event.reason = executionStatus.error?.reason || 'unknown'; + event.event.outcome = 'failure'; + event.error = event.error || {}; + event.error.message = event.error.message || executionStatus.error.message; + if (!event.message) { + event.message = `${this.alertType.id}:${alertId}: execution failed`; + } + } + + eventLogger.logEvent(event); + const client = this.context.internalSavedObjectsRepository; const attributes = { executionStatus: alertExecutionStatusToRaw(executionStatus), @@ -381,7 +421,7 @@ export class TaskRunner { interface GenerateNewAndResolvedInstanceEventsParams { eventLogger: IEventLogger; - originalAlertInstanceIds: string[]; + originalAlertInstances: Dictionary; currentAlertInstances: Dictionary; alertId: string; alertLabel: string; @@ -389,26 +429,23 @@ interface GenerateNewAndResolvedInstanceEventsParams { } function generateNewAndResolvedInstanceEvents(params: GenerateNewAndResolvedInstanceEventsParams) { - const { - eventLogger, - alertId, - namespace, - currentAlertInstances, - originalAlertInstanceIds, - } = params; + const { eventLogger, alertId, namespace, currentAlertInstances, originalAlertInstances } = params; + const originalAlertInstanceIds = Object.keys(originalAlertInstances); const currentAlertInstanceIds = Object.keys(currentAlertInstances); const newIds = without(currentAlertInstanceIds, ...originalAlertInstanceIds); const resolvedIds = without(originalAlertInstanceIds, ...currentAlertInstanceIds); for (const id of resolvedIds) { + const actionGroup = originalAlertInstances[id].getLastScheduledActions()?.group; const message = `${params.alertLabel} resolved instance: '${id}'`; - logInstanceEvent(id, EVENT_LOG_ACTIONS.resolvedInstance, message); + logInstanceEvent(id, EVENT_LOG_ACTIONS.resolvedInstance, message, actionGroup); } for (const id of newIds) { + const actionGroup = currentAlertInstances[id].getScheduledActionOptions()?.actionGroup; const message = `${params.alertLabel} created new instance: '${id}'`; - logInstanceEvent(id, EVENT_LOG_ACTIONS.newInstance, message); + logInstanceEvent(id, EVENT_LOG_ACTIONS.newInstance, message, actionGroup); } for (const id of currentAlertInstanceIds) { @@ -425,7 +462,7 @@ function generateNewAndResolvedInstanceEvents(params: GenerateNewAndResolvedInst kibana: { alerting: { instance_id: instanceId, - action_group_id: group, + ...(group ? { action_group_id: group } : {}), }, saved_objects: [ { diff --git a/x-pack/plugins/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json index 5c7eb50117d9b..3131235ebcfbe 100644 --- a/x-pack/plugins/event_log/generated/mappings.json +++ b/x-pack/plugins/event_log/generated/mappings.json @@ -45,6 +45,10 @@ "outcome": { "ignore_above": 1024, "type": "keyword" + }, + "reason": { + "ignore_above": 1024, + "type": "keyword" } } }, @@ -85,6 +89,10 @@ "action_group_id": { "type": "keyword", "ignore_above": 1024 + }, + "status": { + "type": "keyword", + "ignore_above": 1024 } } }, diff --git a/x-pack/plugins/event_log/generated/schemas.ts b/x-pack/plugins/event_log/generated/schemas.ts index 3dbb43b15350f..d2024ea8ed14a 100644 --- a/x-pack/plugins/event_log/generated/schemas.ts +++ b/x-pack/plugins/event_log/generated/schemas.ts @@ -18,7 +18,7 @@ type DeepPartial = { [P in keyof T]?: T[P] extends Array ? Array> : DeepPartial; }; -export const ECS_VERSION = '1.5.0'; +export const ECS_VERSION = '1.6.0'; // types and config-schema describing the es structures export type IValidatedEvent = TypeOf; @@ -42,6 +42,7 @@ export const EventSchema = schema.maybe( duration: ecsNumber(), end: ecsDate(), outcome: ecsString(), + reason: ecsString(), }) ), error: schema.maybe( @@ -61,6 +62,7 @@ export const EventSchema = schema.maybe( schema.object({ instance_id: ecsString(), action_group_id: ecsString(), + status: ecsString(), }) ), saved_objects: schema.maybe( diff --git a/x-pack/plugins/event_log/scripts/mappings.js b/x-pack/plugins/event_log/scripts/mappings.js index c9af2b0aa57fb..bd05f84d4e2b8 100644 --- a/x-pack/plugins/event_log/scripts/mappings.js +++ b/x-pack/plugins/event_log/scripts/mappings.js @@ -22,6 +22,10 @@ exports.EcsKibanaExtensionsMappings = { type: 'keyword', ignore_above: 1024, }, + status: { + type: 'keyword', + ignore_above: 1024, + }, }, }, // array of saved object references, for "linking" via search @@ -63,11 +67,13 @@ exports.EcsEventLogProperties = [ 'event.duration', 'event.end', 'event.outcome', // optional, but one of failure, success, unknown + 'event.reason', 'error.message', 'user.name', 'kibana.server_uuid', 'kibana.alerting.instance_id', 'kibana.alerting.action_group_id', + 'kibana.alerting.status', 'kibana.saved_objects.rel', 'kibana.saved_objects.namespace', 'kibana.saved_objects.id', diff --git a/x-pack/plugins/event_log/server/event_logger.test.ts b/x-pack/plugins/event_log/server/event_logger.test.ts index 0ab3071f70efa..ea699af45ccd2 100644 --- a/x-pack/plugins/event_log/server/event_logger.test.ts +++ b/x-pack/plugins/event_log/server/event_logger.test.ts @@ -102,16 +102,16 @@ describe('EventLogger', () => { event: { provider: 'test-provider', action: 'a' }, }); - const ignoredTimestamp = '1999-01-01T00:00:00Z'; + const respectedTimestamp = '2999-01-01T00:00:00.000Z'; eventLogger.logEvent({ - '@timestamp': ignoredTimestamp, + '@timestamp': respectedTimestamp, event: { action: 'b', }, }); const event = await waitForLogEvent(systemLogger); - expect(event!['@timestamp']).not.toEqual(ignoredTimestamp); + expect(event!['@timestamp']).toEqual(respectedTimestamp); expect(event?.event?.action).toEqual('b'); }); diff --git a/x-pack/plugins/event_log/server/event_logger.ts b/x-pack/plugins/event_log/server/event_logger.ts index 8730870f9620b..658d90d809652 100644 --- a/x-pack/plugins/event_log/server/event_logger.ts +++ b/x-pack/plugins/event_log/server/event_logger.ts @@ -72,7 +72,6 @@ export class EventLogger implements IEventLogger { const event: IEvent = {}; const fixedProperties = { - '@timestamp': new Date().toISOString(), ecs: { version: ECS_VERSION, }, @@ -81,8 +80,12 @@ export class EventLogger implements IEventLogger { }, }; + const defaultProperties = { + '@timestamp': new Date().toISOString(), + }; + // merge the initial properties and event properties - merge(event, this.initialProperties, eventProperties, fixedProperties); + merge(event, defaultProperties, this.initialProperties, eventProperties, fixedProperties); let validatedEvent: IValidatedEvent; try { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts new file mode 100644 index 0000000000000..385d8bfca4a9a --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/event_log.ts @@ -0,0 +1,84 @@ +/* + * 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 { Spaces } from '../../scenarios'; +import { getUrlPrefix, getTestAlertData, ObjectRemover, getEventLog } from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { validateEvent } from '../../../spaces_only/tests/alerting/event_log'; + +// eslint-disable-next-line import/no-default-export +export default function eventLogTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const retry = getService('retry'); + + describe('eventLog', () => { + const objectRemover = new ObjectRemover(supertest); + + after(() => objectRemover.removeAll()); + + it('should generate events for alert decrypt errors', async () => { + const spaceId = Spaces[0].id; + const response = await supertest + .post(`${getUrlPrefix(spaceId)}/api/alerts/alert`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + alertTypeId: 'test.noop', + schedule: { interval: '1s' }, + throttle: null, + }) + ); + + expect(response.status).to.eql(200); + const alertId = response.body.id; + objectRemover.add(spaceId, alertId, 'alert', 'alerts'); + + // break AAD + await supertest + .put(`${getUrlPrefix(spaceId)}/api/alerts_fixture/saved_object/alert/${alertId}`) + .set('kbn-xsrf', 'foo') + .send({ + attributes: { + name: 'bar', + }, + }) + .expect(200); + + const events = await retry.try(async () => { + // there can be a successful execute before the error one + const someEvents = await getEventLog({ + getService, + spaceId, + type: 'alert', + id: alertId, + provider: 'alerting', + actions: ['execute'], + }); + const errorEvents = someEvents.filter( + (event) => event?.kibana?.alerting?.status === 'error' + ); + if (errorEvents.length === 0) { + throw new Error('no execute/error events yet'); + } + return errorEvents; + }); + + const event = events[0]; + expect(event).to.be.ok(); + + validateEvent(event, { + spaceId, + savedObjects: [{ type: 'alert', id: alertId, rel: 'primary' }], + outcome: 'failure', + message: `test.noop:${alertId}: execution failed`, + errorMessage: 'Unable to decrypt attribute "apiKey"', + status: 'error', + reason: 'decrypt', + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts index 1fbee9e18fdaa..4f8525cfcf683 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts @@ -26,6 +26,7 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./update')); loadTestFile(require.resolve('./update_api_key')); loadTestFile(require.resolve('./alerts')); + loadTestFile(require.resolve('./event_log')); // note that this test will destroy existing spaces loadTestFile(require.resolve('./rbac_legacy')); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts index dbf8eb162fca7..937045b6218c6 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/event_log.ts @@ -107,6 +107,8 @@ export default function eventLogTests({ getService }: FtrProviderContext) { expect(resolvedInstanceTimes[0] > newInstanceTimes[0]).to.be(true); // validate each event + let executeCount = 0; + const executeStatuses = ['ok', 'active', 'active']; for (const event of events) { switch (event?.event?.action) { case 'execute': @@ -115,6 +117,7 @@ export default function eventLogTests({ getService }: FtrProviderContext) { savedObjects: [{ type: 'alert', id: alertId, rel: 'primary' }], outcome: 'success', message: `alert executed: test.patternFiring:${alertId}: 'abc'`, + status: executeStatuses[executeCount++], }); break; case 'execute-action': @@ -125,6 +128,8 @@ export default function eventLogTests({ getService }: FtrProviderContext) { { type: 'action', id: createdAction.id }, ], message: `alert: test.patternFiring:${alertId}: 'abc' instanceId: 'instance' scheduled actionGroup: 'default' action: test.noop:${createdAction.id}`, + instanceId: 'instance', + actionGroupId: 'default', }); break; case 'new-instance': @@ -147,6 +152,8 @@ export default function eventLogTests({ getService }: FtrProviderContext) { spaceId: Spaces.space1.id, savedObjects: [{ type: 'alert', id: alertId, rel: 'primary' }], message: `test.patternFiring:${alertId}: 'abc' ${subMessage}`, + instanceId: 'instance', + actionGroupId: 'default', }); } }); @@ -187,60 +194,83 @@ export default function eventLogTests({ getService }: FtrProviderContext) { outcome: 'failure', message: `alert execution failure: test.throw:${alertId}: 'abc'`, errorMessage: 'this alert is intended to fail', + status: 'error', + reason: 'execute', }); }); }); +} + +interface SavedObject { + type: string; + id: string; + rel?: string; +} + +interface ValidateEventLogParams { + spaceId: string; + savedObjects: SavedObject[]; + outcome?: string; + message: string; + errorMessage?: string; + status?: string; + actionGroupId?: string; + instanceId?: string; + reason?: string; +} + +export function validateEvent(event: IValidatedEvent, params: ValidateEventLogParams): void { + const { spaceId, savedObjects, outcome, message, errorMessage } = params; + const { status, actionGroupId, instanceId, reason } = params; - interface SavedObject { - type: string; - id: string; - rel?: string; + if (status) { + expect(event?.kibana?.alerting?.status).to.be(status); } - interface ValidateEventLogParams { - spaceId: string; - savedObjects: SavedObject[]; - outcome?: string; - message: string; - errorMessage?: string; + if (actionGroupId) { + expect(event?.kibana?.alerting?.action_group_id).to.be(actionGroupId); } - function validateEvent(event: IValidatedEvent, params: ValidateEventLogParams): void { - const { spaceId, savedObjects, outcome, message, errorMessage } = params; + if (instanceId) { + expect(event?.kibana?.alerting?.instance_id).to.be(instanceId); + } - const duration = event?.event?.duration; - const eventStart = Date.parse(event?.event?.start || 'undefined'); - const eventEnd = Date.parse(event?.event?.end || 'undefined'); - const dateNow = Date.now(); + if (reason) { + expect(event?.event?.reason).to.be(reason); + } - if (duration !== undefined) { - expect(typeof duration).to.be('number'); - expect(eventStart).to.be.ok(); - expect(eventEnd).to.be.ok(); + const duration = event?.event?.duration; + const eventStart = Date.parse(event?.event?.start || 'undefined'); + const eventEnd = Date.parse(event?.event?.end || 'undefined'); + const dateNow = Date.now(); - const durationDiff = Math.abs( - Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart) - ); + if (duration !== undefined) { + expect(typeof duration).to.be('number'); + expect(eventStart).to.be.ok(); + expect(eventEnd).to.be.ok(); - // account for rounding errors - expect(durationDiff < 1).to.equal(true); - expect(eventStart <= eventEnd).to.equal(true); - expect(eventEnd <= dateNow).to.equal(true); - } + const durationDiff = Math.abs( + Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart) + ); - expect(event?.event?.outcome).to.equal(outcome); + // account for rounding errors + expect(durationDiff < 1).to.equal(true); + expect(eventStart <= eventEnd).to.equal(true); + expect(eventEnd <= dateNow).to.equal(true); + } - for (const savedObject of savedObjects) { - expect( - isSavedObjectInEvent(event, spaceId, savedObject.type, savedObject.id, savedObject.rel) - ).to.be(true); - } + expect(event?.event?.outcome).to.equal(outcome); - expect(event?.message).to.eql(message); + for (const savedObject of savedObjects) { + expect( + isSavedObjectInEvent(event, spaceId, savedObject.type, savedObject.id, savedObject.rel) + ).to.be(true); + } - if (errorMessage) { - expect(event?.error?.message).to.eql(errorMessage); - } + expect(event?.message).to.eql(message); + + if (errorMessage) { + expect(event?.error?.message).to.eql(errorMessage); } } From 55519665d64d58dd5bcc4773e609715c6b951cb7 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Thu, 12 Nov 2020 14:38:07 +0000 Subject: [PATCH 07/17] [Advanced Settings] Introducing telemetry (#82860) * [Advaned Settings] Introducing telemetry * Publishing doc changes * Move metric tracking to onSave method * Adding deprecated warning * Updating docs Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...ana-plugin-core-public.uisettingsparams.md | 1 + ...gin-core-public.uisettingsparams.metric.md | 21 +++++++++++++++++++ ...ana-plugin-core-server.uisettingsparams.md | 1 + ...gin-core-server.uisettingsparams.metric.md | 21 +++++++++++++++++++ src/core/public/public.api.md | 6 ++++++ src/core/server/server.api.md | 6 ++++++ src/core/types/ui_settings.ts | 10 +++++++++ src/plugins/advanced_settings/kibana.json | 2 +- .../management_app/advanced_settings.tsx | 4 ++++ .../management_app/components/form/form.tsx | 9 +++++++- .../management_app/lib/to_editable_config.ts | 1 + .../mount_management_section.tsx | 6 +++++- .../public/management_app/types.ts | 5 +++++ .../advanced_settings/public/plugin.ts | 12 +++++++++-- src/plugins/advanced_settings/public/types.ts | 2 ++ src/plugins/data/server/server.api.md | 1 + src/plugins/discover/server/ui_settings.ts | 7 ++++++- 17 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.uisettingsparams.metric.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.uisettingsparams.metric.md diff --git a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md index e7facb4a109cd..4a9fc940c596f 100644 --- a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md +++ b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.md @@ -19,6 +19,7 @@ export interface UiSettingsParams | [category](./kibana-plugin-core-public.uisettingsparams.category.md) | string[] | used to group the configured setting in the UI | | [deprecation](./kibana-plugin-core-public.uisettingsparams.deprecation.md) | DeprecationSettings | optional deprecation information. Used to generate a deprecation warning. | | [description](./kibana-plugin-core-public.uisettingsparams.description.md) | string | description provided to a user in UI | +| [metric](./kibana-plugin-core-public.uisettingsparams.metric.md) | {
type: UiStatsMetricType;
name: string;
} | Metric to track once this property changes | | [name](./kibana-plugin-core-public.uisettingsparams.name.md) | string | title in the UI | | [optionLabels](./kibana-plugin-core-public.uisettingsparams.optionlabels.md) | Record<string, string> | text labels for 'select' type UI element | | [options](./kibana-plugin-core-public.uisettingsparams.options.md) | string[] | array of permitted values for this setting | diff --git a/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.metric.md b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.metric.md new file mode 100644 index 0000000000000..0855cfd77a46b --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.uisettingsparams.metric.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) > [metric](./kibana-plugin-core-public.uisettingsparams.metric.md) + +## UiSettingsParams.metric property + +> Warning: This API is now obsolete. +> +> Temporary measure until https://github.com/elastic/kibana/issues/83084 is in place +> + +Metric to track once this property changes + +Signature: + +```typescript +metric?: { + type: UiStatsMetricType; + name: string; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md index f134decb5102b..7bcb996e98e16 100644 --- a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md +++ b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.md @@ -19,6 +19,7 @@ export interface UiSettingsParams | [category](./kibana-plugin-core-server.uisettingsparams.category.md) | string[] | used to group the configured setting in the UI | | [deprecation](./kibana-plugin-core-server.uisettingsparams.deprecation.md) | DeprecationSettings | optional deprecation information. Used to generate a deprecation warning. | | [description](./kibana-plugin-core-server.uisettingsparams.description.md) | string | description provided to a user in UI | +| [metric](./kibana-plugin-core-server.uisettingsparams.metric.md) | {
type: UiStatsMetricType;
name: string;
} | Metric to track once this property changes | | [name](./kibana-plugin-core-server.uisettingsparams.name.md) | string | title in the UI | | [optionLabels](./kibana-plugin-core-server.uisettingsparams.optionlabels.md) | Record<string, string> | text labels for 'select' type UI element | | [options](./kibana-plugin-core-server.uisettingsparams.options.md) | string[] | array of permitted values for this setting | diff --git a/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.metric.md b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.metric.md new file mode 100644 index 0000000000000..4d54bf9ae472b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.uisettingsparams.metric.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) > [metric](./kibana-plugin-core-server.uisettingsparams.metric.md) + +## UiSettingsParams.metric property + +> Warning: This API is now obsolete. +> +> Temporary measure until https://github.com/elastic/kibana/issues/83084 is in place +> + +Metric to track once this property changes + +Signature: + +```typescript +metric?: { + type: UiStatsMetricType; + name: string; + }; +``` diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 781a50f849e24..c8add5a8ddf58 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -38,6 +38,7 @@ import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; +import { UiStatsMetricType } from '@kbn/analytics'; import { UnregisterCallback } from 'history'; import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/types'; @@ -1362,6 +1363,11 @@ export interface UiSettingsParams { // Warning: (ae-forgotten-export) The symbol "DeprecationSettings" needs to be exported by the entry point index.d.ts deprecation?: DeprecationSettings; description?: string; + // @deprecated + metric?: { + type: UiStatsMetricType; + name: string; + }; name?: string; optionLabels?: Record; options?: string[]; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 88d7fecbcf502..a03e5ec9acd27 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -160,6 +160,7 @@ import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; +import { UiStatsMetricType } from '@kbn/analytics'; import { UpdateDocumentByQueryParams } from 'elasticsearch'; import { UpdateDocumentParams } from 'elasticsearch'; import { URL } from 'url'; @@ -2746,6 +2747,11 @@ export interface UiSettingsParams { category?: string[]; deprecation?: DeprecationSettings; description?: string; + // @deprecated + metric?: { + type: UiStatsMetricType; + name: string; + }; name?: string; optionLabels?: Record; options?: string[]; diff --git a/src/core/types/ui_settings.ts b/src/core/types/ui_settings.ts index ed1076b571960..0b7a8e1efd9df 100644 --- a/src/core/types/ui_settings.ts +++ b/src/core/types/ui_settings.ts @@ -17,6 +17,7 @@ * under the License. */ import { Type } from '@kbn/config-schema'; +import { UiStatsMetricType } from '@kbn/analytics'; /** * UI element type to represent the settings. @@ -80,6 +81,15 @@ export interface UiSettingsParams { * Used to validate value on write and read. */ schema: Type; + /** + * Metric to track once this property changes + * @deprecated + * Temporary measure until https://github.com/elastic/kibana/issues/83084 is in place + */ + metric?: { + type: UiStatsMetricType; + name: string; + }; } /** diff --git a/src/plugins/advanced_settings/kibana.json b/src/plugins/advanced_settings/kibana.json index 0e49fe17089f0..df0d31a904c59 100644 --- a/src/plugins/advanced_settings/kibana.json +++ b/src/plugins/advanced_settings/kibana.json @@ -4,6 +4,6 @@ "server": true, "ui": true, "requiredPlugins": ["management"], - "optionalPlugins": ["home"], + "optionalPlugins": ["home", "usageCollection"], "requiredBundles": ["kibanaReact", "home"] } diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx index afdd90959eabd..bbc27ca025ede 100644 --- a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx @@ -22,6 +22,7 @@ import { Subscription } from 'rxjs'; import { Comparators, EuiFlexGroup, EuiFlexItem, EuiSpacer, Query } from '@elastic/eui'; import { useParams } from 'react-router-dom'; +import { UiStatsMetricType } from '@kbn/analytics'; import { CallOuts } from './components/call_outs'; import { Search } from './components/search'; import { Form } from './components/form'; @@ -39,6 +40,7 @@ interface AdvancedSettingsProps { dockLinks: DocLinksStart['links']; toasts: ToastsStart; componentRegistry: ComponentRegistry['start']; + trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void; } interface AdvancedSettingsComponentProps extends AdvancedSettingsProps { @@ -241,6 +243,7 @@ export class AdvancedSettingsComponent extends Component< enableSaving={this.props.enableSaving} dockLinks={this.props.dockLinks} toasts={this.props.toasts} + trackUiMetric={this.props.trackUiMetric} /> { dockLinks={props.dockLinks} toasts={props.toasts} componentRegistry={props.componentRegistry} + trackUiMetric={props.trackUiMetric} /> ); }; diff --git a/src/plugins/advanced_settings/public/management_app/components/form/form.tsx b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx index d243d85e12a66..c30768a262056 100644 --- a/src/plugins/advanced_settings/public/management_app/components/form/form.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx @@ -36,6 +36,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { isEmpty } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { UiStatsMetricType } from '@kbn/analytics'; import { toMountPoint } from '../../../../../kibana_react/public'; import { DocLinksStart, ToastsStart } from '../../../../../../core/public'; @@ -56,6 +57,7 @@ interface FormProps { enableSaving: boolean; dockLinks: DocLinksStart['links']; toasts: ToastsStart; + trackUiMetric?: (metricType: UiStatsMetricType, eventName: string | string[]) => void; } interface FormState { @@ -149,7 +151,7 @@ export class Form extends PureComponent { if (!setting) { return; } - const { defVal, type, requiresPageReload } = setting; + const { defVal, type, requiresPageReload, metric } = setting; let valueToSave = value; let equalsToDefault = false; switch (type) { @@ -163,6 +165,11 @@ export class Form extends PureComponent { const isArray = Array.isArray(JSON.parse((defVal as string) || '{}')); valueToSave = valueToSave.trim(); valueToSave = valueToSave || (isArray ? '[]' : '{}'); + case 'boolean': + if (metric && this.props.trackUiMetric) { + const metricName = valueToSave ? `${metric.name}_on` : `${metric.name}_off`; + this.props.trackUiMetric(metric.type, metricName); + } default: equalsToDefault = valueToSave === defVal; } diff --git a/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts b/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts index 406bc35f826e8..e5a1ee1437a91 100644 --- a/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts +++ b/src/plugins/advanced_settings/public/management_app/lib/to_editable_config.ts @@ -75,6 +75,7 @@ export function toEditableConfig({ options: def.options, optionLabels: def.optionLabels, requiresPageReload: !!def.requiresPageReload, + metric: def.metric, }; return conf; diff --git a/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx b/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx index ab348451b1eef..0b3d73cb28806 100644 --- a/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx +++ b/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx @@ -30,6 +30,7 @@ import { ManagementAppMountParams } from '../../../management/public'; import { ComponentRegistry } from '../types'; import './index.scss'; +import { UsageCollectionSetup } from '../../../usage_collection/public'; const title = i18n.translate('advancedSettings.advancedSettingsLabel', { defaultMessage: 'Advanced Settings', @@ -49,12 +50,14 @@ const readOnlyBadge = { export async function mountManagementSection( getStartServices: StartServicesAccessor, params: ManagementAppMountParams, - componentRegistry: ComponentRegistry['start'] + componentRegistry: ComponentRegistry['start'], + usageCollection?: UsageCollectionSetup ) { params.setBreadcrumbs(crumb); const [{ uiSettings, notifications, docLinks, application, chrome }] = await getStartServices(); const canSave = application.capabilities.advancedSettings.save as boolean; + const trackUiMetric = usageCollection?.reportUiStats.bind(usageCollection, 'advanced_settings'); if (!canSave) { chrome.setBadge(readOnlyBadge); @@ -71,6 +74,7 @@ export async function mountManagementSection( dockLinks={docLinks.links} uiSettings={uiSettings} componentRegistry={componentRegistry} + trackUiMetric={trackUiMetric} /> diff --git a/src/plugins/advanced_settings/public/management_app/types.ts b/src/plugins/advanced_settings/public/management_app/types.ts index 6e243926f7d7d..05e695f998500 100644 --- a/src/plugins/advanced_settings/public/management_app/types.ts +++ b/src/plugins/advanced_settings/public/management_app/types.ts @@ -17,6 +17,7 @@ * under the License. */ +import { UiStatsMetricType } from '@kbn/analytics'; import { UiSettingsType, StringValidation, ImageValidation } from '../../../../core/public'; export interface FieldSetting { @@ -39,6 +40,10 @@ export interface FieldSetting { message: string; docLinksKey: string; }; + metric?: { + type: UiStatsMetricType; + name: string; + }; } // until eui searchbar and query are typed diff --git a/src/plugins/advanced_settings/public/plugin.ts b/src/plugins/advanced_settings/public/plugin.ts index 188b11177eaec..165af48b2023c 100644 --- a/src/plugins/advanced_settings/public/plugin.ts +++ b/src/plugins/advanced_settings/public/plugin.ts @@ -30,7 +30,10 @@ const title = i18n.translate('advancedSettings.advancedSettingsLabel', { export class AdvancedSettingsPlugin implements Plugin { - public setup(core: CoreSetup, { management, home }: AdvancedSettingsPluginSetup) { + public setup( + core: CoreSetup, + { management, home, usageCollection }: AdvancedSettingsPluginSetup + ) { const kibanaSection = management.sections.section.kibana; kibanaSection.registerApp({ @@ -41,7 +44,12 @@ export class AdvancedSettingsPlugin const { mountManagementSection } = await import( './management_app/mount_management_section' ); - return mountManagementSection(core.getStartServices, params, component.start); + return mountManagementSection( + core.getStartServices, + params, + component.start, + usageCollection + ); }, }); diff --git a/src/plugins/advanced_settings/public/types.ts b/src/plugins/advanced_settings/public/types.ts index cc59f52b1f30f..bd5cb0e61fb04 100644 --- a/src/plugins/advanced_settings/public/types.ts +++ b/src/plugins/advanced_settings/public/types.ts @@ -21,6 +21,7 @@ import { ComponentRegistry } from './component_registry'; import { HomePublicPluginSetup } from '../../home/public'; import { ManagementSetup } from '../../management/public'; +import { UsageCollectionSetup } from '../../usage_collection/public'; export interface AdvancedSettingsSetup { component: ComponentRegistry['setup']; @@ -32,6 +33,7 @@ export interface AdvancedSettingsStart { export interface AdvancedSettingsPluginSetup { management: ManagementSetup; home?: HomePublicPluginSetup; + usageCollection?: UsageCollectionSetup; } export { ComponentRegistry }; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 2984ca336819a..bb7a8f58c926c 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -60,6 +60,7 @@ import { ShardsResponse } from 'elasticsearch'; import { ToastInputFields } from 'src/core/public/notifications'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; +import { UiStatsMetricType } from '@kbn/analytics'; import { Unit } from '@elastic/datemath'; // Warning: (ae-forgotten-export) The symbol "AggConfigSerialized" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts index 5447b982eef14..f45281ee62202 100644 --- a/src/plugins/discover/server/ui_settings.ts +++ b/src/plugins/discover/server/ui_settings.ts @@ -21,6 +21,7 @@ import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; import { UiSettingsParams } from 'kibana/server'; +import { METRIC_TYPE } from '@kbn/analytics'; import { DEFAULT_COLUMNS_SETTING, SAMPLE_SIZE_SETTING, @@ -170,9 +171,13 @@ export const uiSettings: Record = { }), value: true, description: i18n.translate('discover.advancedSettings.discover.modifyColumnsOnSwitchText', { - defaultMessage: 'Remove columns that not available in the new index pattern.', + defaultMessage: 'Remove columns that are not available in the new index pattern.', }), category: ['discover'], schema: schema.boolean(), + metric: { + type: METRIC_TYPE.CLICK, + name: 'discover:modifyColumnsOnSwitchTitle', + }, }, }; From afbf1a983aadd60621ccc3689876211688f3fa1d Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 12 Nov 2020 15:49:22 +0100 Subject: [PATCH 08/17] [APM] Errors table for service overview (#83065) --- .../runtime_types/to_number_rt/index.ts | 16 ++ .../ServiceList/ServiceListMetric.tsx | 33 +-- .../components/app/service_overview/index.tsx | 34 +-- .../fetch_wrapper.tsx | 30 ++ .../service_overview_errors_table/index.tsx | 266 ++++++++++++++++++ .../service_overview/table_link_flex_item.tsx | 14 + .../{SparkPlot => spark_plot}/index.tsx | 0 .../spark_plot_with_value_label/index.tsx | 57 ++++ .../apm/server/lib/errors/get_error_groups.ts | 4 +- .../apm/server/lib/helpers/get_error_name.ts | 11 + .../get_service_error_groups/index.ts | 177 ++++++++++++ .../apm/server/routes/create_apm_api.ts | 2 + x-pack/plugins/apm/server/routes/services.ts | 44 +++ .../monitoring/workload_statistics.test.ts | 32 ++- .../apm_api_integration/basic/tests/index.ts | 4 + .../tests/service_overview/error_groups.ts | 220 +++++++++++++++ .../typings/elasticsearch/aggregations.d.ts | 2 + 17 files changed, 883 insertions(+), 63 deletions(-) create mode 100644 x-pack/plugins/apm/common/runtime_types/to_number_rt/index.ts create mode 100644 x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx create mode 100644 x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx create mode 100644 x-pack/plugins/apm/public/components/app/service_overview/table_link_flex_item.tsx rename x-pack/plugins/apm/public/components/shared/charts/{SparkPlot => spark_plot}/index.tsx (100%) create mode 100644 x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx create mode 100644 x-pack/plugins/apm/server/lib/helpers/get_error_name.ts create mode 100644 x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts create mode 100644 x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts diff --git a/x-pack/plugins/apm/common/runtime_types/to_number_rt/index.ts b/x-pack/plugins/apm/common/runtime_types/to_number_rt/index.ts new file mode 100644 index 0000000000000..0fe8181c11405 --- /dev/null +++ b/x-pack/plugins/apm/common/runtime_types/to_number_rt/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as t from 'io-ts'; + +export const toNumberRt = new t.Type( + 'ToNumber', + t.any.is, + (input, context) => { + const number = Number(input); + return !isNaN(number) ? t.success(number) : t.failure(input, context); + }, + t.identity +); diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx index c94c94d4a0b72..716fed7775f7b 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/ServiceListMetric.tsx @@ -3,15 +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 { EuiFlexItem } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; - import React from 'react'; -import { useTheme } from '../../../../hooks/useTheme'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { getEmptySeries } from '../../../shared/charts/CustomPlot/getEmptySeries'; -import { SparkPlot } from '../../../shared/charts/SparkPlot'; +import { SparkPlotWithValueLabel } from '../../../shared/charts/spark_plot/spark_plot_with_value_label'; export function ServiceListMetric({ color, @@ -22,28 +16,17 @@ export function ServiceListMetric({ series?: Array<{ x: number; y: number | null }>; valueLabel: React.ReactNode; }) { - const theme = useTheme(); - const { urlParams: { start, end }, } = useUrlParams(); - const colorValue = theme.eui[color]; - return ( - - - - - - {valueLabel} - - + ); } diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index 016ee3daf6b51..ee77157fe4eb3 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -12,9 +12,10 @@ import { useTrackPageview } from '../../../../../observability/public'; import { isRumAgentName } from '../../../../common/agent_name'; import { ChartsSyncContextProvider } from '../../../context/charts_sync_context'; import { TransactionErrorRateChart } from '../../shared/charts/transaction_error_rate_chart'; -import { ErrorOverviewLink } from '../../shared/Links/apm/ErrorOverviewLink'; import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink'; import { TransactionOverviewLink } from '../../shared/Links/apm/TransactionOverviewLink'; +import { ServiceOverviewErrorsTable } from './service_overview_errors_table'; +import { TableLinkFlexItem } from './table_link_flex_item'; const rowHeight = 310; const latencyChartRowHeight = 230; @@ -27,12 +28,6 @@ const LatencyChartRow = styled(EuiFlexItem)` height: ${latencyChartRowHeight}px; `; -const TableLinkFlexItem = styled(EuiFlexItem)` - & > a { - text-align: right; - } -`; - interface ServiceOverviewProps { agentName?: string; serviceName: string; @@ -130,30 +125,7 @@ export function ServiceOverview({ )} - - - -

- {i18n.translate( - 'xpack.apm.serviceOverview.errorsTableTitle', - { - defaultMessage: 'Errors', - } - )} -

-
-
- - - {i18n.translate( - 'xpack.apm.serviceOverview.errorsTableLinkText', - { - defaultMessage: 'View errors', - } - )} - - -
+
diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx new file mode 100644 index 0000000000000..4c8d368811a0c --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/fetch_wrapper.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FETCH_STATUS } from '../../../../hooks/useFetcher'; +import { ErrorStatePrompt } from '../../../shared/ErrorStatePrompt'; +import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; + +export function FetchWrapper({ + hasData, + status, + children, +}: { + hasData: boolean; + status: FETCH_STATUS; + children: React.ReactNode; +}) { + if (status === FETCH_STATUS.FAILURE) { + return ; + } + + if (!hasData && status !== FETCH_STATUS.SUCCESS) { + return ; + } + + return <>{children}; +} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx new file mode 100644 index 0000000000000..a5a002cf3aca4 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx @@ -0,0 +1,266 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useState } from 'react'; +import { EuiTitle } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiBasicTable } from '@elastic/eui'; +import { EuiBasicTableColumn } from '@elastic/eui'; +import styled from 'styled-components'; +import { EuiToolTip } from '@elastic/eui'; +import { asInteger } from '../../../../../common/utils/formatters'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/useFetcher'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink'; +import { TableLinkFlexItem } from '../table_link_flex_item'; +import { SparkPlotWithValueLabel } from '../../../shared/charts/spark_plot/spark_plot_with_value_label'; +import { callApmApi } from '../../../../services/rest/createCallApmApi'; +import { TimestampTooltip } from '../../../shared/TimestampTooltip'; +import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; +import { px, truncate, unit } from '../../../../style/variables'; +import { FetchWrapper } from './fetch_wrapper'; + +interface Props { + serviceName: string; +} + +interface ErrorGroupItem { + name: string; + last_seen: number; + group_id: string; + occurrences: { + value: number; + timeseries: Array<{ x: number; y: number }> | null; + }; +} + +type SortDirection = 'asc' | 'desc'; +type SortField = 'name' | 'last_seen' | 'occurrences'; + +const PAGE_SIZE = 5; +const DEFAULT_SORT = { + direction: 'desc' as const, + field: 'occurrences' as const, +}; + +const ErrorDetailLinkWrapper = styled.div` + width: 100%; + .euiToolTipAnchor { + width: 100% !important; + } +`; + +const StyledErrorDetailLink = styled(ErrorDetailLink)` + display: block; + ${truncate('100%')} +`; + +export function ServiceOverviewErrorsTable({ serviceName }: Props) { + const { + urlParams: { start, end }, + uiFilters, + } = useUrlParams(); + + const [tableOptions, setTableOptions] = useState<{ + pageIndex: number; + sort: { + direction: SortDirection; + field: SortField; + }; + }>({ + pageIndex: 0, + sort: DEFAULT_SORT, + }); + + const columns: Array> = [ + { + field: 'name', + name: i18n.translate('xpack.apm.serviceOverview.errorsTableColumnName', { + defaultMessage: 'Name', + }), + render: (_, { name, group_id: errorGroupId }) => { + return ( + + + + {name} + + + + ); + }, + }, + { + field: 'last_seen', + name: i18n.translate( + 'xpack.apm.serviceOverview.errorsTableColumnLastSeen', + { + defaultMessage: 'Last seen', + } + ), + render: (_, { last_seen: lastSeen }) => { + return ; + }, + width: px(unit * 8), + }, + { + field: 'occurrences', + name: i18n.translate( + 'xpack.apm.serviceOverview.errorsTableColumnOccurrences', + { + defaultMessage: 'Occurrences', + } + ), + width: px(unit * 12), + render: (_, { occurrences }) => { + return ( + + ); + }, + }, + ]; + + const { + data = { + totalItemCount: 0, + items: [], + tableOptions: { + pageIndex: 0, + sort: DEFAULT_SORT, + }, + }, + status, + } = useFetcher(() => { + if (!start || !end) { + return; + } + + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/error_groups', + params: { + path: { serviceName }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + size: PAGE_SIZE, + numBuckets: 20, + pageIndex: tableOptions.pageIndex, + sortField: tableOptions.sort.field, + sortDirection: tableOptions.sort.direction, + }, + }, + }).then((response) => { + return { + items: response.error_groups, + totalItemCount: response.total_error_groups, + tableOptions: { + pageIndex: tableOptions.pageIndex, + sort: { + field: tableOptions.sort.field, + direction: tableOptions.sort.direction, + }, + }, + }; + }); + }, [ + start, + end, + serviceName, + uiFilters, + tableOptions.pageIndex, + tableOptions.sort.field, + tableOptions.sort.direction, + ]); + + const { + items, + totalItemCount, + tableOptions: { pageIndex, sort }, + } = data; + + return ( + + + + + +

+ {i18n.translate('xpack.apm.serviceOverview.errorsTableTitle', { + defaultMessage: 'Errors', + })} +

+
+
+ + + {i18n.translate('xpack.apm.serviceOverview.errorsTableLinkText', { + defaultMessage: 'View errors', + })} + + +
+
+ + + { + setTableOptions({ + pageIndex: newTableOptions.page?.index ?? 0, + sort: newTableOptions.sort + ? { + field: newTableOptions.sort.field as SortField, + direction: newTableOptions.sort.direction, + } + : DEFAULT_SORT, + }); + }} + sorting={{ + enableAllColumns: true, + sort: { + direction: sort.direction, + field: sort.field, + }, + }} + /> + + +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/table_link_flex_item.tsx b/x-pack/plugins/apm/public/components/app/service_overview/table_link_flex_item.tsx new file mode 100644 index 0000000000000..35df003af380d --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/table_link_flex_item.tsx @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; + +export const TableLinkFlexItem = styled(EuiFlexItem)` + & > a { + text-align: right; + } +`; diff --git a/x-pack/plugins/apm/public/components/shared/charts/SparkPlot/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/charts/SparkPlot/index.tsx rename to x-pack/plugins/apm/public/components/shared/charts/spark_plot/index.tsx diff --git a/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx new file mode 100644 index 0000000000000..e2bb42fddb33b --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/spark_plot/spark_plot_with_value_label/index.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; + +import React from 'react'; +import { useTheme } from '../../../../../hooks/useTheme'; +import { getEmptySeries } from '../../CustomPlot/getEmptySeries'; +import { SparkPlot } from '../'; + +type Color = + | 'euiColorVis0' + | 'euiColorVis1' + | 'euiColorVis2' + | 'euiColorVis3' + | 'euiColorVis4' + | 'euiColorVis5' + | 'euiColorVis6' + | 'euiColorVis7' + | 'euiColorVis8' + | 'euiColorVis9'; + +export function SparkPlotWithValueLabel({ + start, + end, + color, + series, + valueLabel, +}: { + start: number; + end: number; + color: Color; + series?: Array<{ x: number; y: number | null }>; + valueLabel: React.ReactNode; +}) { + const theme = useTheme(); + + const colorValue = theme.eui[color]; + + return ( + + + + + + {valueLabel} + + + ); +} diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts index d734a1395fc5e..97c03924538c8 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts @@ -16,6 +16,7 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { getErrorGroupsProjection } from '../../projections/errors'; import { mergeProjection } from '../../projections/util/merge_projection'; +import { getErrorName } from '../helpers/get_error_name'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; export type ErrorGroupListAPIResponse = PromiseReturnType< @@ -93,8 +94,7 @@ export async function getErrorGroups({ // this is an exception rather than the rule so the ES type does not account for this. const hits = (resp.aggregations?.error_groups.buckets || []).map((bucket) => { const source = bucket.sample.hits.hits[0]._source; - const message = - source.error.log?.message || source.error.exception?.[0]?.message; + const message = getErrorName(source); return { message, diff --git a/x-pack/plugins/apm/server/lib/helpers/get_error_name.ts b/x-pack/plugins/apm/server/lib/helpers/get_error_name.ts new file mode 100644 index 0000000000000..dbc69592a4f8e --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/get_error_name.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { APMError } from '../../../typings/es_schemas/ui/apm_error'; + +export function getErrorName({ error }: APMError) { + return error.log?.message || error.exception?.[0]?.message; +} diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts new file mode 100644 index 0000000000000..99d978116180b --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts @@ -0,0 +1,177 @@ +/* + * 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 { ValuesType } from 'utility-types'; +import { orderBy } from 'lodash'; +import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; +import { PromiseReturnType } from '../../../../../observability/typings/common'; +import { rangeFilter } from '../../../../common/utils/range_filter'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { + ERROR_EXC_MESSAGE, + ERROR_GROUP_ID, + ERROR_LOG_MESSAGE, + SERVICE_NAME, +} from '../../../../common/elasticsearch_fieldnames'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { getErrorName } from '../../helpers/get_error_name'; + +export type ServiceErrorGroupItem = ValuesType< + PromiseReturnType +>; + +export async function getServiceErrorGroups({ + serviceName, + setup, + size, + numBuckets, + pageIndex, + sortDirection, + sortField, +}: { + serviceName: string; + setup: Setup & SetupTimeRange; + size: number; + pageIndex: number; + numBuckets: number; + sortDirection: 'asc' | 'desc'; + sortField: 'name' | 'last_seen' | 'occurrences'; +}) { + const { apmEventClient, start, end, esFilter } = setup; + + const { intervalString } = getBucketSize(start, end, numBuckets); + + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { range: rangeFilter(start, end) }, + ...esFilter, + ], + }, + }, + aggs: { + error_groups: { + terms: { + field: ERROR_GROUP_ID, + size: 500, + order: { + _count: 'desc', + }, + }, + aggs: { + sample: { + top_hits: { + size: 1, + _source: [ERROR_LOG_MESSAGE, ERROR_EXC_MESSAGE, '@timestamp'], + sort: { + '@timestamp': 'desc', + }, + }, + }, + }, + }, + }, + }, + }); + + const errorGroups = + response.aggregations?.error_groups.buckets.map((bucket) => ({ + group_id: bucket.key as string, + name: + getErrorName(bucket.sample.hits.hits[0]._source) ?? NOT_AVAILABLE_LABEL, + last_seen: new Date( + bucket.sample.hits.hits[0]?._source['@timestamp'] + ).getTime(), + occurrences: { + value: bucket.doc_count, + }, + })) ?? []; + + // Sort error groups first, and only get timeseries for data in view. + // This is to limit the possibility of creating too many buckets. + + const sortedAndSlicedErrorGroups = orderBy( + errorGroups, + (group) => { + if (sortField === 'occurrences') { + return group.occurrences.value; + } + return group[sortField]; + }, + [sortDirection] + ).slice(pageIndex * size, pageIndex * size + size); + + const sortedErrorGroupIds = sortedAndSlicedErrorGroups.map( + (group) => group.group_id + ); + + const timeseriesResponse = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.error], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { terms: { [ERROR_GROUP_ID]: sortedErrorGroupIds } }, + { term: { [SERVICE_NAME]: serviceName } }, + { range: rangeFilter(start, end) }, + ...esFilter, + ], + }, + }, + aggs: { + error_groups: { + terms: { + field: ERROR_GROUP_ID, + size, + }, + aggs: { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: intervalString, + min_doc_count: 0, + extended_bounds: { + min: start, + max: end, + }, + }, + }, + }, + }, + }, + }, + }); + + return { + total_error_groups: errorGroups.length, + is_aggregation_accurate: + (response.aggregations?.error_groups.sum_other_doc_count ?? 0) === 0, + error_groups: sortedAndSlicedErrorGroups.map((errorGroup) => ({ + ...errorGroup, + occurrences: { + ...errorGroup.occurrences, + timeseries: + timeseriesResponse.aggregations?.error_groups.buckets + .find((bucket) => bucket.key === errorGroup.group_id) + ?.timeseries.buckets.map((dateBucket) => ({ + x: dateBucket.key, + y: dateBucket.doc_count, + })) ?? null, + }, + })), + }; +} diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 2fbe404a70d82..34551c35ee234 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -21,6 +21,7 @@ import { serviceNodeMetadataRoute, serviceAnnotationsRoute, serviceAnnotationsCreateRoute, + serviceErrorGroupsRoute, } from './services'; import { agentConfigurationRoute, @@ -115,6 +116,7 @@ const createApmApi = () => { .add(serviceNodeMetadataRoute) .add(serviceAnnotationsRoute) .add(serviceAnnotationsCreateRoute) + .add(serviceErrorGroupsRoute) // Agent configuration .add(getSingleAgentConfigurationRoute) diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 590b6c49d71bf..ada1674d4555d 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -17,6 +17,8 @@ import { uiFiltersRt, rangeRt } from './default_api_types'; import { getServiceAnnotations } from '../lib/services/annotations'; import { dateAsStringRt } from '../../common/runtime_types/date_as_string_rt'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; +import { getServiceErrorGroups } from '../lib/services/get_service_error_groups'; +import { toNumberRt } from '../../common/runtime_types/to_number_rt'; export const servicesRoute = createRoute(() => ({ path: '/api/apm/services', @@ -195,3 +197,45 @@ export const serviceAnnotationsCreateRoute = createRoute(() => ({ }); }, })); + +export const serviceErrorGroupsRoute = createRoute(() => ({ + path: '/api/apm/services/{serviceName}/error_groups', + params: { + path: t.type({ + serviceName: t.string, + }), + query: t.intersection([ + rangeRt, + uiFiltersRt, + t.type({ + size: toNumberRt, + numBuckets: toNumberRt, + pageIndex: toNumberRt, + sortDirection: t.union([t.literal('asc'), t.literal('desc')]), + sortField: t.union([ + t.literal('last_seen'), + t.literal('occurrences'), + t.literal('name'), + ]), + }), + ]), + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + const { + path: { serviceName }, + query: { size, numBuckets, pageIndex, sortDirection, sortField }, + } = context.params; + + return getServiceErrorGroups({ + serviceName, + setup, + size, + numBuckets, + pageIndex, + sortDirection, + sortField, + }); + }, +})); diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts index d1c5256c81c63..c2e62b6e1898b 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts @@ -46,9 +46,13 @@ describe('Workload Statistics Aggregator', () => { aggregations: { taskType: { buckets: [], + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, }, schedule: { buckets: [], + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, }, idleTasks: { doc_count: 0, @@ -158,6 +162,8 @@ describe('Workload Statistics Aggregator', () => { }, aggregations: { schedule: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, buckets: [ { key: '3600s', @@ -174,11 +180,15 @@ describe('Workload Statistics Aggregator', () => { ], }, taskType: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, buckets: [ { key: 'actions_telemetry', doc_count: 2, status: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, buckets: [ { key: 'idle', @@ -191,6 +201,8 @@ describe('Workload Statistics Aggregator', () => { key: 'alerting_telemetry', doc_count: 1, status: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, buckets: [ { key: 'idle', @@ -203,6 +215,8 @@ describe('Workload Statistics Aggregator', () => { key: 'session_cleanup', doc_count: 1, status: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, buckets: [ { key: 'idle', @@ -608,6 +622,7 @@ describe('padBuckets', () => { key: 1601668047000, doc_count: 1, interval: { + doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [], }, @@ -617,6 +632,7 @@ describe('padBuckets', () => { key: 1601668050000, doc_count: 1, interval: { + doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [], }, @@ -626,6 +642,7 @@ describe('padBuckets', () => { key: 1601668053000, doc_count: 0, interval: { + doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [], }, @@ -635,6 +652,7 @@ describe('padBuckets', () => { key: 1601668056000, doc_count: 0, interval: { + doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [], }, @@ -644,6 +662,7 @@ describe('padBuckets', () => { key: 1601668059000, doc_count: 0, interval: { + doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [], }, @@ -653,6 +672,7 @@ describe('padBuckets', () => { key: 1601668062000, doc_count: 1, interval: { + doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [], }, @@ -678,13 +698,13 @@ describe('padBuckets', () => { key_as_string: '2020-10-02T20:40:09.000Z', key: 1601671209000, doc_count: 1, - interval: { buckets: [] }, + interval: { buckets: [], sum_other_doc_count: 0, doc_count_error_upper_bound: 0 }, }, { key_as_string: '2020-10-02T20:40:12.000Z', key: 1601671212000, doc_count: 1, - interval: { buckets: [] }, + interval: { buckets: [], sum_other_doc_count: 0, doc_count_error_upper_bound: 0 }, }, ], }, @@ -707,13 +727,13 @@ describe('padBuckets', () => { key_as_string: '2020-10-02T20:40:09.000Z', key: 1601671209000, doc_count: 1, - interval: { buckets: [] }, + interval: { buckets: [], sum_other_doc_count: 0, doc_count_error_upper_bound: 0 }, }, { key_as_string: '2020-10-02T20:40:12.000Z', key: 1601671212000, doc_count: 1, - interval: { buckets: [] }, + interval: { buckets: [], sum_other_doc_count: 0, doc_count_error_upper_bound: 0 }, }, ], }, @@ -796,7 +816,7 @@ function mockHistogram( key_as_string: key.toISOString(), key: key.getTime(), doc_count: count, - interval: { buckets: [] }, + interval: { buckets: [], doc_count_error_upper_bound: 0, sum_other_doc_count: 0 }, }); } return histogramBuckets; @@ -806,6 +826,8 @@ function mockHistogram( key: number; doc_count: number; interval: { + doc_count_error_upper_bound: number; + sum_other_doc_count: number; buckets: Array<{ key: string; doc_count: number; 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 df3e60d79aca5..39dd721c7067e 100644 --- a/x-pack/test/apm_api_integration/basic/tests/index.ts +++ b/x-pack/test/apm_api_integration/basic/tests/index.ts @@ -25,6 +25,10 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont loadTestFile(require.resolve('./services/transaction_types')); }); + describe('Service overview', function () { + loadTestFile(require.resolve('./service_overview/error_groups')); + }); + describe('Settings', function () { loadTestFile(require.resolve('./settings/custom_link')); loadTestFile(require.resolve('./settings/agent_configuration')); diff --git a/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts b/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts new file mode 100644 index 0000000000000..b699a30d40418 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/service_overview/error_groups.ts @@ -0,0 +1,220 @@ +/* + * 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 qs from 'querystring'; +import { pick, uniqBy } from 'lodash'; +import { expectSnapshot } from '../../../common/match_snapshot'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import archives from '../../../common/archives_metadata'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const archiveName = 'apm_8.0.0'; + const { start, end } = archives[archiveName]; + + describe('Service overview error groups', () => { + describe('when data is not loaded', () => { + it('handles the empty state', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'occurrences', + })}` + ); + + expect(response.status).to.be(200); + expect(response.body).to.eql({ + total_error_groups: 0, + error_groups: [], + is_aggregation_accurate: true, + }); + }); + }); + + describe('when data is loaded', () => { + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); + + it('returns the correct data', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'occurrences', + })}` + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body.total_error_groups).toMatchInline(`5`); + + expectSnapshot(response.body.error_groups.map((group: any) => group.name)).toMatchInline(` + Array [ + "Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getCost(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getCost() (through reference chain: co.elastic.apm.opbeans.repositories.Stats[\\"numbers\\"]->com.sun.proxy.$Proxy133[\\"cost\\"])", + "java.io.IOException: Connection reset by peer", + "Connection reset by peer", + "Could not write JSON: Unable to find co.elastic.apm.opbeans.model.Customer with id 6617; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Unable to find co.elastic.apm.opbeans.model.Customer with id 6617 (through reference chain: co.elastic.apm.opbeans.model.Customer_$$_jvst369_3[\\"email\\"])", + "Request method 'POST' not supported", + ] + `); + + expectSnapshot(response.body.error_groups.map((group: any) => group.occurrences.value)) + .toMatchInline(` + Array [ + 8, + 2, + 1, + 1, + 1, + ] + `); + + const firstItem = response.body.error_groups[0]; + + expectSnapshot(pick(firstItem, 'group_id', 'last_seen', 'name', 'occurrences.value')) + .toMatchInline(` + Object { + "group_id": "051f95eabf120ebe2f8b0399fe3e54c5", + "last_seen": 1601391561523, + "name": "Could not write JSON: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getCost(); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Null return value from advice does not match primitive return type for: public abstract double co.elastic.apm.opbeans.repositories.Numbers.getCost() (through reference chain: co.elastic.apm.opbeans.repositories.Stats[\\"numbers\\"]->com.sun.proxy.$Proxy133[\\"cost\\"])", + "occurrences": Object { + "value": 8, + }, + } + `); + + expectSnapshot( + firstItem.occurrences.timeseries.filter(({ y }: any) => y > 0).length + ).toMatchInline(`7`); + }); + + it('sorts items in the correct order', async () => { + const descendingResponse = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'occurrences', + })}` + ); + + expect(descendingResponse.status).to.be(200); + + const descendingOccurrences = descendingResponse.body.error_groups.map( + (item: any) => item.occurrences.value + ); + + expect(descendingOccurrences).to.eql(descendingOccurrences.concat().sort().reverse()); + + const ascendingResponse = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'occurrences', + })}` + ); + + const ascendingOccurrences = ascendingResponse.body.error_groups.map( + (item: any) => item.occurrences.value + ); + + expect(ascendingOccurrences).to.eql(ascendingOccurrences.concat().sort().reverse()); + }); + + it('sorts items by the correct field', async () => { + const response = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size: 5, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'last_seen', + })}` + ); + + expect(response.status).to.be(200); + + const dates = response.body.error_groups.map((group: any) => group.last_seen); + + expect(dates).to.eql(dates.concat().sort().reverse()); + }); + + it('paginates through the items', async () => { + const size = 1; + + const firstPage = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size, + numBuckets: 20, + pageIndex: 0, + sortDirection: 'desc', + sortField: 'occurrences', + })}` + ); + + expect(firstPage.status).to.eql(200); + + const totalItems = firstPage.body.total_error_groups; + + const pages = Math.floor(totalItems / size); + + const items = await new Array(pages) + .fill(undefined) + .reduce(async (prevItemsPromise, _, pageIndex) => { + const prevItems = await prevItemsPromise; + + const thisPage = await supertest.get( + `/api/apm/services/opbeans-java/error_groups?${qs.stringify({ + start, + end, + uiFilters: '{}', + size, + numBuckets: 20, + pageIndex, + sortDirection: 'desc', + sortField: 'occurrences', + })}` + ); + + return prevItems.concat(thisPage.body.error_groups); + }, Promise.resolve([])); + + expect(items.length).to.eql(totalItems); + + expect(uniqBy(items, 'group_id').length).to.eql(totalItems); + }); + }); + }); +} diff --git a/x-pack/typings/elasticsearch/aggregations.d.ts b/x-pack/typings/elasticsearch/aggregations.d.ts index 29c78e9383175..bc9ed447c8717 100644 --- a/x-pack/typings/elasticsearch/aggregations.d.ts +++ b/x-pack/typings/elasticsearch/aggregations.d.ts @@ -204,6 +204,8 @@ type SubAggregationResponseOf< interface AggregationResponsePart { terms: { + doc_count_error_upper_bound: number; + sum_other_doc_count: number; buckets: Array< { doc_count: number; From 58ad7ecd5adae6db8b91f648444ca045f3b5404e Mon Sep 17 00:00:00 2001 From: Bohdan Tsymbala Date: Thu, 12 Nov 2020 15:53:53 +0100 Subject: [PATCH 09/17] Btsymbala/registered av (#81910) * Moved out type for OperatingSystem and moved OS translations one level higher. * Changed the translation to be consistent between trusted apps and policy. * Unified translations of OS types between trusted apps and policy. * Removed unused types. * Added registered AV form section. * Changed the property structure to match the format expected by endpoint. * Fixed the visual alignment of titles in the form and added responsiveness. * Updated snapshots. * Moved out type for OperatingSystem and moved OS translations one level higher. * Added config form heading component. * Cleaned up translations. * Fixed type error with initialization. * Fixed error in trusted app creation form test. * Removed the guard for now in favour of better initialization. * Fixed the store test. * Fixing functional test data. * Added functional test config option to account for a custom header within security app. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/endpoint/models/policy_config.ts | 3 + .../common/endpoint/types/index.ts | 9 +- .../common/endpoint/types/os.ts | 10 +++ .../common/endpoint/types/trusted_apps.ts | 5 +- .../public/management/common/translations.ts | 14 +++ .../policy/store/policy_details/action.ts | 10 ++- .../policy/store/policy_details/index.test.ts | 3 + .../policy/store/policy_details/reducer.ts | 45 +++++++++- .../policy/store/policy_details/selectors.ts | 5 ++ .../public/management/pages/policy/types.ts | 62 ------------- .../components/config_form/index.stories.tsx | 60 +++++++++++++ .../view/components/config_form/index.tsx | 84 +++++++++++++++++ .../pages/policy/view/policy_details.tsx | 3 + .../antivirus_registration/index.tsx | 64 +++++++++++++ .../policy/view/policy_forms/config_form.tsx | 89 ------------------- .../policy/view/policy_forms/events/linux.tsx | 45 ++++------ .../policy/view/policy_forms/events/mac.tsx | 45 ++++------ .../view/policy_forms/events/translations.ts | 28 ++++++ .../view/policy_forms/events/windows.tsx | 45 ++++------ .../view/policy_forms/protections/malware.tsx | 39 ++++---- .../create_trusted_app_form.test.tsx | 2 +- .../__snapshots__/index.test.tsx.snap | 4 +- .../__snapshots__/index.test.tsx.snap | 18 ++-- .../__snapshots__/index.test.tsx.snap | 56 ++++++------ .../pages/trusted_apps/view/translations.ts | 14 +-- .../translations/translations/ja-JP.json | 7 -- .../translations/translations/zh-CN.json | 7 -- .../apps/endpoint/policy_details.ts | 3 + .../test/security_solution_endpoint/config.ts | 3 + 29 files changed, 447 insertions(+), 335 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/endpoint/types/os.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/config_form.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/translations.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts index 3250e048edad2..890def5b63d4a 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts @@ -33,6 +33,9 @@ export const factory = (): PolicyConfig => { logging: { file: 'info', }, + antivirus_registration: { + enabled: false, + }, }, mac: { events: { diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index 1d64578a6a7f1..673d04c856935 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -8,6 +8,7 @@ import { ApplicationStart } from 'kibana/public'; import { NewPackagePolicy, PackagePolicy } from '../../../../fleet/common'; import { ManifestSchema } from '../schema/manifest'; +export * from './os'; export * from './trusted_apps'; /** @@ -880,6 +881,9 @@ export interface PolicyConfig { enabled: boolean; }; }; + antivirus_registration: { + enabled: boolean; + }; }; mac: { advanced?: {}; @@ -919,7 +923,10 @@ export interface UIPolicyConfig { /** * Windows-specific policy configuration that is supported via the UI */ - windows: Pick; + windows: Pick< + PolicyConfig['windows'], + 'events' | 'malware' | 'popup' | 'antivirus_registration' | 'advanced' + >; /** * Mac-specific policy configuration that is supported via the UI */ diff --git a/x-pack/plugins/security_solution/common/endpoint/types/os.ts b/x-pack/plugins/security_solution/common/endpoint/types/os.ts new file mode 100644 index 0000000000000..b9afbd63ecd54 --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/types/os.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type Linux = 'linux'; +export type MacOS = 'macos'; +export type Windows = 'windows'; +export type OperatingSystem = Linux | MacOS | Windows; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts index 3568136dd0e7b..79d66443bc8f1 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts @@ -11,6 +11,7 @@ import { GetTrustedAppsRequestSchema, PostTrustedAppCreateRequestSchema, } from '../schema/trusted_apps'; +import { Linux, MacOS, Windows } from './os'; /** API request params for deleting Trusted App entry */ export type DeleteTrustedAppsRequestParams = TypeOf; @@ -51,11 +52,11 @@ export type NewTrustedApp = { description?: string; } & ( | { - os: 'linux' | 'macos'; + os: Linux | MacOS; entries: MacosLinuxConditionEntry[]; } | { - os: 'windows'; + os: Windows; entries: WindowsConditionEntry[]; } ); diff --git a/x-pack/plugins/security_solution/public/management/common/translations.ts b/x-pack/plugins/security_solution/public/management/common/translations.ts index d24eb1bd315fa..415658c1fd6af 100644 --- a/x-pack/plugins/security_solution/public/management/common/translations.ts +++ b/x-pack/plugins/security_solution/public/management/common/translations.ts @@ -6,6 +6,8 @@ import { i18n } from '@kbn/i18n'; +import { OperatingSystem } from '../../../common/endpoint/types'; + export const ENDPOINTS_TAB = i18n.translate('xpack.securitySolution.endpointsTab', { defaultMessage: 'Endpoints', }); @@ -21,3 +23,15 @@ export const TRUSTED_APPS_TAB = i18n.translate('xpack.securitySolution.trustedAp export const BETA_BADGE_LABEL = i18n.translate('xpack.securitySolution.administration.list.beta', { defaultMessage: 'Beta', }); + +export const OS_TITLES: Readonly<{ [K in OperatingSystem]: string }> = { + windows: i18n.translate('xpack.securitySolution.administration.os.windows', { + defaultMessage: 'Windows', + }), + macos: i18n.translate('xpack.securitySolution.administration.os.macos', { + defaultMessage: 'Mac', + }), + linux: i18n.translate('xpack.securitySolution.administration.os.linux', { + defaultMessage: 'Linux', + }), +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action.ts index f5a219bce4a6b..bda408cd00e75 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/action.ts @@ -31,6 +31,13 @@ interface UserChangedPolicyConfig { }; } +interface UserChangedAntivirusRegistration { + type: 'userChangedAntivirusRegistration'; + payload: { + enabled: boolean; + }; +} + interface ServerReturnedPolicyDetailsAgentSummaryData { type: 'serverReturnedPolicyDetailsAgentSummaryData'; payload: { @@ -62,4 +69,5 @@ export type PolicyDetailsAction = | ServerReturnedPolicyDetailsUpdateFailure | ServerReturnedUpdatedPolicyDetailsData | ServerFailedToReturnPolicyDetailsData - | UserChangedPolicyConfig; + | UserChangedPolicyConfig + | UserChangedAntivirusRegistration; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts index 89ba05547f447..69c2afbd01960 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts @@ -245,6 +245,9 @@ describe('policy details: ', () => { }, }, logging: { file: 'info' }, + antivirus_registration: { + enabled: false, + }, }, mac: { events: { process: true, file: true, network: true }, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts index 43a6ad2c585b4..bcdc7ba2089c6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/reducer.ts @@ -4,11 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ import { fullPolicy, isOnPolicyDetailsPage } from './selectors'; -import { Immutable, PolicyConfig, UIPolicyConfig } from '../../../../../../common/endpoint/types'; +import { + Immutable, + PolicyConfig, + UIPolicyConfig, + PolicyData, +} from '../../../../../../common/endpoint/types'; import { ImmutableReducer } from '../../../../../common/store'; import { AppAction } from '../../../../../common/store/actions'; import { PolicyDetailsState } from '../../types'; +const updatePolicyConfigInPolicyData = ( + policyData: Immutable, + policyConfig: Immutable +) => ({ + ...policyData, + inputs: policyData.inputs.map((input) => ({ + ...input, + config: input.config && { + ...input.config, + policy: { + ...input.config.policy, + value: policyConfig, + }, + }, + })), +}); + /** * Return a fresh copy of initial state, since we mutate state in the reducer. */ @@ -126,5 +148,26 @@ export const policyDetailsReducer: ImmutableReducer UIPolicyConfig = createSel events: windows.events, malware: windows.malware, popup: windows.popup, + antivirus_registration: windows.antivirus_registration, }, mac: { advanced: mac.advanced, @@ -122,6 +123,10 @@ export const policyConfig: (s: PolicyDetailsState) => UIPolicyConfig = createSel } ); +export const isAntivirusRegistrationEnabled = createSelector(policyConfig, (uiPolicyConfig) => { + return uiPolicyConfig.windows.antivirus_registration.enabled; +}); + /** Returns the total number of possible windows eventing configurations */ export const totalWindowsEvents = (state: PolicyDetailsState): number => { const config = policyConfig(state); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts index 152caff3714b0..3926ad2220e35 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts @@ -76,68 +76,6 @@ export interface PolicyListUrlSearchParams { page_size: number; } -/** - * Endpoint Policy configuration - */ -export interface PolicyConfig { - windows: { - events: { - dll_and_driver_load: boolean; - dns: boolean; - file: boolean; - network: boolean; - process: boolean; - registry: boolean; - security: boolean; - }; - malware: MalwareFields; - logging: { - stdout: string; - file: string; - }; - advanced: PolicyConfigAdvancedOptions; - }; - mac: { - events: { - file: boolean; - process: boolean; - network: boolean; - }; - malware: MalwareFields; - logging: { - stdout: string; - file: string; - }; - advanced: PolicyConfigAdvancedOptions; - }; - linux: { - events: { - file: boolean; - process: boolean; - network: boolean; - }; - logging: { - stdout: string; - file: string; - }; - advanced: PolicyConfigAdvancedOptions; - }; -} - -interface PolicyConfigAdvancedOptions { - elasticsearch: { - indices: { - control: string; - event: string; - logging: string; - }; - kernel: { - connect: boolean; - process: boolean; - }; - }; -} - export enum OS { windows = 'windows', mac = 'mac', diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx new file mode 100644 index 0000000000000..4f288af393b7c --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { storiesOf, addDecorator } from '@storybook/react'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { EuiCheckbox, EuiSpacer, EuiSwitch, EuiText } from '@elastic/eui'; + +import { ConfigForm } from '.'; + +addDecorator((storyFn) => ( + ({ eui: euiLightVars, darkMode: false })}>{storyFn()} +)); + +storiesOf('PolicyDetails/ConfigForm', module) + .add('One OS', () => { + return ( + + {'Some content'} + + ); + }) + .add('Multiple OSs', () => { + return ( + + {'Some content'} + + ); + }) + .add('Complex content', () => { + return ( + + + {'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore ' + + 'et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut ' + + 'aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum ' + + 'dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia ' + + 'deserunt mollit anim id est laborum.'} + + + {}} /> + + {}} /> + {}} /> + {}} /> + + ); + }) + .add('Right corner content', () => { + const toggle = {}} />; + + return ( + + {'Some content'} + + ); + }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx new file mode 100644 index 0000000000000..30c35de9b907f --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, ReactNode, memo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiHorizontalRule, + EuiText, + EuiShowFor, + EuiPanel, +} from '@elastic/eui'; + +import { OperatingSystem } from '../../../../../../../common/endpoint/types'; +import { OS_TITLES } from '../../../../../common/translations'; + +const TITLES = { + type: i18n.translate('xpack.securitySolution.endpoint.policyDetailType', { + defaultMessage: 'Type', + }), + os: i18n.translate('xpack.securitySolution.endpoint.policyDetailOS', { + defaultMessage: 'Operating System', + }), +}; + +interface ConfigFormProps { + /** + * A subtitle for this component. + **/ + type: string; + /** + * Types of supported operating systems. + */ + supportedOss: OperatingSystem[]; + dataTestSubj?: string; + /** React Node to be put on the right corner of the card */ + rightCorner?: ReactNode; +} + +export const ConfigFormHeading: FC = memo(({ children }) => ( + +
{children}
+
+)); + +ConfigFormHeading.displayName = 'ConfigFormHeading'; + +export const ConfigForm: FC = memo( + ({ type, supportedOss, dataTestSubj, rightCorner, children }) => ( + + + + {TITLES.type} + {type} + + + {TITLES.os} + {supportedOss.map((os) => OS_TITLES[os]).join(', ')} + + + + + {rightCorner} + + + + + {rightCorner} + + + + + + {children} + + ) +); + +ConfigForm.displayName = 'ConfigForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx index 8fc5de48f36db..9c11bc6f5a4d1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx @@ -36,6 +36,7 @@ import { AgentsSummary } from './agents_summary'; import { VerticalDivider } from './vertical_divider'; import { WindowsEvents, MacEvents, LinuxEvents } from './policy_forms/events'; import { MalwareProtections } from './policy_forms/protections/malware'; +import { AntivirusRegistrationForm } from './policy_forms/antivirus_registration'; import { useToasts } from '../../../../common/lib/kibana'; import { AppAction } from '../../../../common/store/actions'; import { SpyRoute } from '../../../../common/utils/route/spy_routes'; @@ -251,6 +252,8 @@ export const PolicyDetails = React.memo(() => { + + diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx new file mode 100644 index 0000000000000..8d1ac29c8ce1e --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/antivirus_registration/index.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { i18n } from '@kbn/i18n'; +import { EuiSpacer, EuiSwitch, EuiText } from '@elastic/eui'; + +import { isAntivirusRegistrationEnabled } from '../../../store/policy_details/selectors'; +import { usePolicyDetailsSelector } from '../../policy_hooks'; +import { ConfigForm } from '../../components/config_form'; + +export const AntivirusRegistrationForm = memo(() => { + const antivirusRegistrationEnabled = usePolicyDetailsSelector(isAntivirusRegistrationEnabled); + const dispatch = useDispatch(); + + const handleSwitchChange = useCallback( + (event) => + dispatch({ + type: 'userChangedAntivirusRegistration', + payload: { + enabled: event.target.checked, + }, + }), + [dispatch] + ); + + return ( + + + {i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.antivirusRegistration.explanation', + { + defaultMessage: 'Switch the toggle to on to register Elastic anti-virus', + } + )} + + + + + ); +}); + +AntivirusRegistrationForm.displayName = 'AntivirusRegistrationForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/config_form.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/config_form.tsx deleted file mode 100644 index 8e3c4138efb36..0000000000000 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/config_form.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useMemo } from 'react'; -import { - EuiCard, - EuiFlexGroup, - EuiFlexItem, - EuiTitle, - EuiHorizontalRule, - EuiText, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import styled from 'styled-components'; - -const PolicyDetailCard = styled.div` - .policyDetailTitleOS { - flex-grow: 2; - } - .policyDetailTitleFlexItem { - margin: 0; - } -`; -export const ConfigForm: React.FC<{ - /** - * A subtitle for this component. - **/ - type: string; - /** - * Types of supported operating systems. - */ - supportedOss: React.ReactNode; - children: React.ReactNode; - dataTestSubj: string; - /** React Node to be put on the right corner of the card */ - rightCorner: React.ReactNode; -}> = React.memo(({ type, supportedOss, children, dataTestSubj, rightCorner }) => { - const typeTitle = useMemo(() => { - return ( - - - - -
- -
-
-
- - {type} - -
- - - -
- -
-
-
- - {supportedOss} - -
- {rightCorner} -
- ); - }, [rightCorner, supportedOss, type]); - - return ( - - - - {children} - - - ); -}); - -ConfigForm.displayName = 'ConfigForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx index 66126adb7a4e1..b43f93f1a1e2b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/linux.tsx @@ -6,15 +6,19 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; +import { EuiText, EuiSpacer } from '@elastic/eui'; import { EventsCheckbox } from './checkbox'; import { OS } from '../../../types'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedLinuxEvents, totalLinuxEvents } from '../../../store/policy_details/selectors'; -import { ConfigForm } from '../config_form'; +import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; import { getIn, setIn } from '../../../models/policy_details_config'; import { UIPolicyConfig } from '../../../../../../../common/endpoint/types'; +import { + COLLECTIONS_ENABLED_MESSAGE, + EVENTS_FORM_TYPE_LABEL, + EVENTS_HEADING, +} from './translations'; export const LinuxEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedLinuxEvents); @@ -59,14 +63,7 @@ export const LinuxEvents = React.memo(() => { ]; return ( <> - -
- -
-
+ {EVENTS_HEADING} {items.map((item, index) => { return ( @@ -85,28 +82,16 @@ export const LinuxEvents = React.memo(() => { ); }, []); - const collectionsEnabled = useMemo(() => { - return ( - - - - ); - }, [selected, total]); - return ( + {COLLECTIONS_ENABLED_MESSAGE(selected, total)} + + } > {checkboxes} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx index dc70fc0ba0f4f..fbbe50fbec1b0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/mac.tsx @@ -6,15 +6,19 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; +import { EuiText, EuiSpacer } from '@elastic/eui'; import { EventsCheckbox } from './checkbox'; import { OS } from '../../../types'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedMacEvents, totalMacEvents } from '../../../store/policy_details/selectors'; -import { ConfigForm } from '../config_form'; +import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; import { getIn, setIn } from '../../../models/policy_details_config'; import { UIPolicyConfig } from '../../../../../../../common/endpoint/types'; +import { + COLLECTIONS_ENABLED_MESSAGE, + EVENTS_FORM_TYPE_LABEL, + EVENTS_HEADING, +} from './translations'; export const MacEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedMacEvents); @@ -59,14 +63,7 @@ export const MacEvents = React.memo(() => { ]; return ( <> - -
- -
-
+ {EVENTS_HEADING} {items.map((item, index) => { return ( @@ -85,28 +82,16 @@ export const MacEvents = React.memo(() => { ); }, []); - const collectionsEnabled = useMemo(() => { - return ( - - - - ); - }, [selected, total]); - return ( + {COLLECTIONS_ENABLED_MESSAGE(selected, total)} + + } > {checkboxes} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/translations.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/translations.ts new file mode 100644 index 0000000000000..3b48b7969a8ce --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/translations.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const EVENTS_HEADING = i18n.translate( + 'xpack.securitySolution.endpoint.policyDetailsConfig.eventingEvents', + { + defaultMessage: 'Events', + } +); + +export const EVENTS_FORM_TYPE_LABEL = i18n.translate( + 'xpack.securitySolution.endpoint.policy.details.eventCollection', + { + defaultMessage: 'Event Collection', + } +); + +export const COLLECTIONS_ENABLED_MESSAGE = (selected: number, total: number) => { + return i18n.translate('xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled', { + defaultMessage: '{selected} / {total} event collections enabled', + values: { selected, total }, + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx index 5acdf67922a3a..f7b1a8e901ed2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/events/windows.tsx @@ -6,15 +6,19 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; +import { EuiText, EuiSpacer } from '@elastic/eui'; import { EventsCheckbox } from './checkbox'; import { OS } from '../../../types'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { selectedWindowsEvents, totalWindowsEvents } from '../../../store/policy_details/selectors'; -import { ConfigForm } from '../config_form'; +import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; import { setIn, getIn } from '../../../models/policy_details_config'; import { UIPolicyConfig, Immutable } from '../../../../../../../common/endpoint/types'; +import { + COLLECTIONS_ENABLED_MESSAGE, + EVENTS_FORM_TYPE_LABEL, + EVENTS_HEADING, +} from './translations'; export const WindowsEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedWindowsEvents); @@ -99,14 +103,7 @@ export const WindowsEvents = React.memo(() => { ]; return ( <> - -
- -
-
+ {EVENTS_HEADING} {items.map((item, index) => { return ( @@ -125,28 +122,16 @@ export const WindowsEvents = React.memo(() => { ); }, []); - const collectionsEnabled = useMemo(() => { - return ( - - - - ); - }, [selected, total]); - return ( + {COLLECTIONS_ENABLED_MESSAGE(selected, total)} + + } > {checkboxes} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx index b61dee5269737..7259b2ec19ee2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -7,10 +7,11 @@ import React, { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiRadio, EuiSwitch, - EuiTitle, EuiText, EuiSpacer, EuiTextArea, @@ -18,15 +19,13 @@ import { EuiCallOut, EuiCheckbox, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import { cloneDeep } from 'lodash'; import { APP_ID } from '../../../../../../../common/constants'; import { SecurityPageName } from '../../../../../../app/types'; import { Immutable, ProtectionModes } from '../../../../../../../common/endpoint/types'; import { OS, MalwareProtectionOSes } from '../../../types'; -import { ConfigForm } from '../config_form'; +import { ConfigForm, ConfigFormHeading } from '../../components/config_form'; import { policyConfig } from '../../../store/policy_details/selectors'; import { usePolicyDetailsSelector } from '../../policy_hooks'; import { LinkToApp } from '../../../../../../common/components/endpoint/link_to_app'; @@ -200,14 +199,12 @@ export const MalwareProtections = React.memo(() => { const radioButtons = useMemo(() => { return ( <> - -
- -
-
+ + + {radios.map((radio) => { @@ -221,14 +218,12 @@ export const MalwareProtections = React.memo(() => { })} - -
- -
-
+ + + { type={i18n.translate('xpack.securitySolution.endpoint.policy.details.malware', { defaultMessage: 'Malware', })} - supportedOss={i18n.translate('xpack.securitySolution.endpoint.policy.details.windowsAndMac', { - defaultMessage: 'Windows, Mac', - })} + supportedOss={['windows', 'macos']} dataTestSubj="malwareProtectionsForm" rightCorner={protectionSwitch} > diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx index 211fc9ec3371e..4bac9164e1d62 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx @@ -118,7 +118,7 @@ describe('When showing the Trusted App Create Form', () => { '.euiSuperSelect__listbox button.euiSuperSelect__item' ) ).map((button) => button.textContent); - expect(options).toEqual(['Mac OS', 'Windows', 'Linux']); + expect(options).toEqual(['Mac', 'Windows', 'Linux']); }); it('should show Description as optional', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap index a94e6287a4f58..a47558257420c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_app_card/__snapshots__/index.test.tsx.snap @@ -17,7 +17,7 @@ exports[`trusted_app_card TrustedAppCard should render correctly 1`] = ` value={ } /> @@ -112,7 +112,7 @@ exports[`trusted_app_card TrustedAppCard should trim long texts 1`] = ` value={ } /> diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap index c82b9cac8ab1f..6d45059099f8d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap @@ -412,7 +412,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiToolTipAnchor" > - Mac OS + Mac @@ -1168,7 +1168,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiToolTipAnchor" > - Mac OS + Mac @@ -1924,7 +1924,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` class="euiToolTipAnchor" > - Mac OS + Mac @@ -3222,7 +3222,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiToolTipAnchor" > - Mac OS + Mac @@ -3978,7 +3978,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiToolTipAnchor" > - Mac OS + Mac @@ -4734,7 +4734,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="euiToolTipAnchor" > - Mac OS + Mac @@ -5990,7 +5990,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiToolTipAnchor" > - Mac OS + Mac @@ -6746,7 +6746,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiToolTipAnchor" > - Mac OS + Mac @@ -7502,7 +7502,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not class="euiToolTipAnchor" > - Mac OS + Mac diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap index 2797c433b8236..d0459871d4881 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/__snapshots__/index.test.tsx.snap @@ -1061,7 +1061,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -1448,7 +1448,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -1835,7 +1835,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -2222,7 +2222,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -2609,7 +2609,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -2996,7 +2996,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -3383,7 +3383,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -4001,7 +4001,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -4388,7 +4388,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -4775,7 +4775,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -5162,7 +5162,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -5549,7 +5549,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -5936,7 +5936,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -6323,7 +6323,7 @@ exports[`TrustedAppsList renders correctly when loaded data 1`] = ` class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -7099,7 +7099,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -7486,7 +7486,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -7873,7 +7873,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -8260,7 +8260,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -8647,7 +8647,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -9034,7 +9034,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -9421,7 +9421,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -10039,7 +10039,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -10426,7 +10426,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -10813,7 +10813,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -11200,7 +11200,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -11587,7 +11587,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -11974,7 +11974,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac @@ -12361,7 +12361,7 @@ exports[`TrustedAppsList renders correctly when new page and page size set (not class="euiToolTipAnchor eui-textTruncate" > - Mac OS + Mac diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts index b2f62c2f1da4e..4c2b3f0e59ccb 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts @@ -11,23 +11,13 @@ import { WindowsConditionEntry, } from '../../../../../common/endpoint/types'; +export { OS_TITLES } from '../../../common/translations'; + export const ABOUT_TRUSTED_APPS = i18n.translate('xpack.securitySolution.trustedapps.aboutInfo', { defaultMessage: 'Add a trusted application to improve performance or alleviate conflicts with other applications running on your hosts. Trusted applications will be applied to hosts running Endpoint Security.', }); -export const OS_TITLES: Readonly<{ [K in TrustedApp['os']]: string }> = { - windows: i18n.translate('xpack.securitySolution.trustedapps.os.windows', { - defaultMessage: 'Windows', - }), - macos: i18n.translate('xpack.securitySolution.trustedapps.os.macos', { - defaultMessage: 'Mac OS', - }), - linux: i18n.translate('xpack.securitySolution.trustedapps.os.linux', { - defaultMessage: 'Linux', - }), -}; - type Entry = MacosLinuxConditionEntry | WindowsConditionEntry; export const CONDITION_FIELD_TITLE: { [K in Entry['field']]: string } = { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 852c3ffd9bfd6..912cb01d458e2 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -17498,8 +17498,6 @@ "xpack.securitySolution.endpoint.policy.details.detectionRulesMessage": "{detectionRulesLink}を表示します。事前構築済みルールは、[検出ルール]ページで「Elastic」というタグが付けられています。", "xpack.securitySolution.endpoint.policy.details.eventCollection": "イベント収集", "xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled": "{selected} / {total}件のイベント収集が有効です", - "xpack.securitySolution.endpoint.policy.details.linux": "Linux", - "xpack.securitySolution.endpoint.policy.details.mac": "Mac", "xpack.securitySolution.endpoint.policy.details.malware": "マルウェア", "xpack.securitySolution.endpoint.policy.details.malwareProtectionsEnabled": "マルウェア保護{mode, select, true {有効} false {無効}}", "xpack.securitySolution.endpoint.policy.details.prevent": "防御", @@ -17514,8 +17512,6 @@ "xpack.securitySolution.endpoint.policy.details.updateErrorTitle": "失敗しました。", "xpack.securitySolution.endpoint.policy.details.updateSuccessMessage": "統合{name}が更新されました。", "xpack.securitySolution.endpoint.policy.details.updateSuccessTitle": "成功!", - "xpack.securitySolution.endpoint.policy.details.windows": "Windows", - "xpack.securitySolution.endpoint.policy.details.windowsAndMac": "Windows、Mac", "xpack.securitySolution.endpoint.policyDetailOS": "オペレーティングシステム", "xpack.securitySolution.endpoint.policyDetails.agentsSummary.errorTitle": "エラー", "xpack.securitySolution.endpoint.policyDetails.agentsSummary.offlineTitle": "オフライン", @@ -18466,9 +18462,6 @@ "xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND", "xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "条件が定義されていません", "xpack.securitySolution.trustedapps.noResults": "項目が見つかりません", - "xpack.securitySolution.trustedapps.os.linux": "Linux", - "xpack.securitySolution.trustedapps.os.macos": "Mac OS", - "xpack.securitySolution.trustedapps.os.windows": "Windows", "xpack.securitySolution.trustedapps.trustedapp.createdAt": "作成日", "xpack.securitySolution.trustedapps.trustedapp.createdBy": "作成者", "xpack.securitySolution.trustedapps.trustedapp.description": "説明", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 461b93e2e081d..8ae964d9ee7d0 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -17516,8 +17516,6 @@ "xpack.securitySolution.endpoint.policy.details.detectionRulesMessage": "请查看{detectionRulesLink}。在“检测规则”页面上,预置规则标记有“Elastic”。", "xpack.securitySolution.endpoint.policy.details.eventCollection": "事件收集", "xpack.securitySolution.endpoint.policy.details.eventCollectionsEnabled": "{selected} / {total} 事件收集已启用", - "xpack.securitySolution.endpoint.policy.details.linux": "Linux", - "xpack.securitySolution.endpoint.policy.details.mac": "Mac", "xpack.securitySolution.endpoint.policy.details.malware": "恶意软件", "xpack.securitySolution.endpoint.policy.details.malwareProtectionsEnabled": "恶意软件防护{mode, select, true {已启用} false {已禁用}}", "xpack.securitySolution.endpoint.policy.details.prevent": "防御", @@ -17533,8 +17531,6 @@ "xpack.securitySolution.endpoint.policy.details.updateErrorTitle": "失败!", "xpack.securitySolution.endpoint.policy.details.updateSuccessMessage": "集成 {name} 已更新。", "xpack.securitySolution.endpoint.policy.details.updateSuccessTitle": "成功!", - "xpack.securitySolution.endpoint.policy.details.windows": "Windows", - "xpack.securitySolution.endpoint.policy.details.windowsAndMac": "Windows、Mac", "xpack.securitySolution.endpoint.policyDetailOS": "操作系统", "xpack.securitySolution.endpoint.policyDetails.agentsSummary.errorTitle": "错误", "xpack.securitySolution.endpoint.policyDetails.agentsSummary.offlineTitle": "脱机", @@ -18485,9 +18481,6 @@ "xpack.securitySolution.trustedapps.logicalConditionBuilder.group.andOperator": "AND", "xpack.securitySolution.trustedapps.logicalConditionBuilder.noEntries": "未定义条件", "xpack.securitySolution.trustedapps.noResults": "找不到项目", - "xpack.securitySolution.trustedapps.os.linux": "Linux", - "xpack.securitySolution.trustedapps.os.macos": "Mac OS", - "xpack.securitySolution.trustedapps.os.windows": "Windows", "xpack.securitySolution.trustedapps.trustedapp.createdAt": "创建日期", "xpack.securitySolution.trustedapps.trustedapp.createdBy": "创建者", "xpack.securitySolution.trustedapps.trustedapp.description": "描述", diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index 15c26a1b9374d..f032416d2e7bb 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -221,6 +221,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { message: 'Elastic Security { action } { filename }', }, }, + antivirus_registration: { + enabled: false, + }, }, }, streams: [], diff --git a/x-pack/test/security_solution_endpoint/config.ts b/x-pack/test/security_solution_endpoint/config.ts index 9499c235a5f0d..f3cb4a5812a5c 100644 --- a/x-pack/test/security_solution_endpoint/config.ts +++ b/x-pack/test/security_solution_endpoint/config.ts @@ -43,5 +43,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...getRegistryUrlAsArray(), ], }, + layout: { + fixedHeaderHeight: 200, + }, }; } From eaa65535edf5ad7bb64d50373bc7587ca18d1d7f Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 12 Nov 2020 15:54:55 +0100 Subject: [PATCH 10/17] Use saved object references for dashboard drilldowns (#82602) --- ...ver.embeddablesetup.getattributeservice.md | 11 - ...ugins-embeddable-server.embeddablesetup.md | 3 +- .../embeddable/embeddable_references.test.ts | 87 ++++++ .../embeddable/embeddable_references.ts | 82 +++++ ...embeddable_saved_object_converters.test.ts | 26 +- .../embeddable_saved_object_converters.ts | 7 +- .../saved_dashboard_references.test.ts | 164 +++++----- .../saved_dashboard_references.ts | 75 ++++- src/plugins/dashboard/common/types.ts | 17 ++ .../application/dashboard_app_controller.tsx | 2 +- .../application/dashboard_state_manager.ts | 2 +- .../public/application/embeddable/types.ts | 12 +- src/plugins/dashboard/public/plugin.tsx | 1 + .../public/saved_dashboards/index.ts | 2 +- .../saved_dashboards/saved_dashboard.ts | 22 +- .../saved_dashboards/saved_dashboards.ts | 10 +- src/plugins/dashboard/public/types.ts | 10 +- src/plugins/dashboard/server/plugin.ts | 20 +- .../server/saved_objects/dashboard.ts | 15 +- .../dashboard_migrations.test.ts | 55 +++- .../saved_objects/dashboard_migrations.ts | 62 +++- .../dashboard/server/saved_objects/index.ts | 2 +- .../saved_objects/migrations_730.test.ts | 6 +- src/plugins/embeddable/common/index.ts | 21 ++ src/plugins/embeddable/common/lib/index.ts | 1 + .../lib}/saved_object_embeddable.ts | 2 +- src/plugins/embeddable/common/mocks.ts | 31 ++ src/plugins/embeddable/common/types.ts | 15 +- .../public/lib/containers/container.ts | 2 +- .../public/lib/containers/i_container.ts | 12 +- .../public/lib/embeddables/index.ts | 2 +- src/plugins/embeddable/server/mocks.ts | 30 ++ src/plugins/embeddable/server/plugin.ts | 17 +- src/plugins/embeddable/server/server.api.md | 5 +- .../dashboard_drilldown/constants.ts | 14 + ...hboard_drilldown_persistable_state.test.ts | 48 +++ .../dashboard_drilldown_persistable_state.ts | 75 +++++ .../drilldowns/dashboard_drilldown/index.ts | 9 + .../drilldowns/dashboard_drilldown/types.ts | 12 + .../common/drilldowns/index.ts | 7 + .../dashboard_enhanced/common/index.ts | 7 + x-pack/plugins/dashboard_enhanced/kibana.json | 2 +- .../abstract_dashboard_drilldown.tsx | 8 +- .../abstract_dashboard_drilldown/types.ts | 8 +- ...embeddable_to_dashboard_drilldown.test.tsx | 6 + .../embeddable_to_dashboard_drilldown.tsx | 5 + .../dashboard_enhanced/server/index.ts | 19 ++ .../dashboard_enhanced/server/plugin.ts | 44 +++ .../ui_actions_enhanced/common/index.ts | 7 + .../server/dynamic_action_enhancement.ts | 4 +- .../ui_actions_enhanced/server/index.ts | 6 +- .../ui_actions_enhanced/server/plugin.ts | 4 +- .../dashboard_to_dashboard_drilldown.ts | 279 +++++++++++------- .../reporting/hugedata/data.json.gz | Bin 33744 -> 33744 bytes .../spaces/copy_saved_objects/data.json | 4 +- .../kibana/dashboard/sample_dashboard.json | 8 +- .../kibana/dashboard/sample_dashboard2.json | 8 +- .../kibana/dashboard/sample_dashboard.json | 8 +- 58 files changed, 1122 insertions(+), 301 deletions(-) delete mode 100644 docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md create mode 100644 src/plugins/dashboard/common/embeddable/embeddable_references.test.ts create mode 100644 src/plugins/dashboard/common/embeddable/embeddable_references.ts rename src/plugins/dashboard/{public/application/lib => common/embeddable}/embeddable_saved_object_converters.test.ts (82%) rename src/plugins/dashboard/{public/application/lib => common/embeddable}/embeddable_saved_object_converters.ts (91%) rename src/plugins/dashboard/{public/saved_dashboards => common}/saved_dashboard_references.test.ts (52%) rename src/plugins/dashboard/{public/saved_dashboards => common}/saved_dashboard_references.ts (55%) create mode 100644 src/plugins/embeddable/common/index.ts rename src/plugins/embeddable/{public/lib/embeddables => common/lib}/saved_object_embeddable.ts (96%) create mode 100644 src/plugins/embeddable/common/mocks.ts create mode 100644 src/plugins/embeddable/server/mocks.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/constants.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.test.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/index.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/types.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/drilldowns/index.ts create mode 100644 x-pack/plugins/dashboard_enhanced/common/index.ts create mode 100644 x-pack/plugins/dashboard_enhanced/server/index.ts create mode 100644 x-pack/plugins/dashboard_enhanced/server/plugin.ts create mode 100644 x-pack/plugins/ui_actions_enhanced/common/index.ts diff --git a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md deleted file mode 100644 index 9cd77ca6e3a36..0000000000000 --- a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-embeddable-server](./kibana-plugin-plugins-embeddable-server.md) > [EmbeddableSetup](./kibana-plugin-plugins-embeddable-server.embeddablesetup.md) > [getAttributeService](./kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md) - -## EmbeddableSetup.getAttributeService property - -Signature: - -```typescript -getAttributeService: any; -``` diff --git a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md index bd024095e80be..5109a75ad57f0 100644 --- a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md +++ b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.embeddablesetup.md @@ -7,14 +7,13 @@ Signature: ```typescript -export interface EmbeddableSetup +export interface EmbeddableSetup extends PersistableStateService ``` ## Properties | Property | Type | Description | | --- | --- | --- | -| [getAttributeService](./kibana-plugin-plugins-embeddable-server.embeddablesetup.getattributeservice.md) | any | | | [registerEmbeddableFactory](./kibana-plugin-plugins-embeddable-server.embeddablesetup.registerembeddablefactory.md) | (factory: EmbeddableRegistryDefinition) => void | | | [registerEnhancement](./kibana-plugin-plugins-embeddable-server.embeddablesetup.registerenhancement.md) | (enhancement: EnhancementRegistryDefinition) => void | | diff --git a/src/plugins/dashboard/common/embeddable/embeddable_references.test.ts b/src/plugins/dashboard/common/embeddable/embeddable_references.test.ts new file mode 100644 index 0000000000000..fabc89f8c8233 --- /dev/null +++ b/src/plugins/dashboard/common/embeddable/embeddable_references.test.ts @@ -0,0 +1,87 @@ +/* + * 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 { + ExtractDeps, + extractPanelsReferences, + InjectDeps, + injectPanelsReferences, +} from './embeddable_references'; +import { createEmbeddablePersistableStateServiceMock } from '../../../embeddable/common/mocks'; +import { SavedDashboardPanel } from '../types'; +import { EmbeddableStateWithType } from '../../../embeddable/common'; + +const embeddablePersistableStateService = createEmbeddablePersistableStateServiceMock(); +const deps: InjectDeps & ExtractDeps = { + embeddablePersistableStateService, +}; + +test('inject/extract panel references', () => { + embeddablePersistableStateService.extract.mockImplementationOnce((state) => { + const { HARDCODED_ID, ...restOfState } = (state as unknown) as Record; + return { + state: restOfState as EmbeddableStateWithType, + references: [{ id: HARDCODED_ID as string, name: 'refName', type: 'type' }], + }; + }); + + embeddablePersistableStateService.inject.mockImplementationOnce((state, references) => { + const ref = references.find((r) => r.name === 'refName'); + return { + ...state, + HARDCODED_ID: ref!.id, + }; + }); + + const savedDashboardPanel: SavedDashboardPanel = { + type: 'search', + embeddableConfig: { + HARDCODED_ID: 'IMPORTANT_HARDCODED_ID', + }, + id: 'savedObjectId', + panelIndex: '123', + gridData: { + x: 0, + y: 0, + h: 15, + w: 15, + i: '123', + }, + version: '7.0.0', + }; + + const [{ panel: extractedPanel, references }] = extractPanelsReferences( + [savedDashboardPanel], + deps + ); + expect(extractedPanel.embeddableConfig).toEqual({}); + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "IMPORTANT_HARDCODED_ID", + "name": "refName", + "type": "type", + }, + ] + `); + + const [injectedPanel] = injectPanelsReferences([extractedPanel], references, deps); + + expect(injectedPanel).toEqual(savedDashboardPanel); +}); diff --git a/src/plugins/dashboard/common/embeddable/embeddable_references.ts b/src/plugins/dashboard/common/embeddable/embeddable_references.ts new file mode 100644 index 0000000000000..dd686203fa351 --- /dev/null +++ b/src/plugins/dashboard/common/embeddable/embeddable_references.ts @@ -0,0 +1,82 @@ +/* + * 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 { + convertSavedDashboardPanelToPanelState, + convertPanelStateToSavedDashboardPanel, +} from './embeddable_saved_object_converters'; +import { SavedDashboardPanel } from '../types'; +import { SavedObjectReference } from '../../../../core/types'; +import { EmbeddablePersistableStateService } from '../../../embeddable/common/types'; + +export interface InjectDeps { + embeddablePersistableStateService: EmbeddablePersistableStateService; +} + +export function injectPanelsReferences( + panels: SavedDashboardPanel[], + references: SavedObjectReference[], + deps: InjectDeps +): SavedDashboardPanel[] { + const result: SavedDashboardPanel[] = []; + for (const panel of panels) { + const embeddableState = convertSavedDashboardPanelToPanelState(panel); + embeddableState.explicitInput = omit( + deps.embeddablePersistableStateService.inject( + { ...embeddableState.explicitInput, type: panel.type }, + references + ), + 'type' + ); + result.push(convertPanelStateToSavedDashboardPanel(embeddableState, panel.version)); + } + return result; +} + +export interface ExtractDeps { + embeddablePersistableStateService: EmbeddablePersistableStateService; +} + +export function extractPanelsReferences( + panels: SavedDashboardPanel[], + deps: ExtractDeps +): Array<{ panel: SavedDashboardPanel; references: SavedObjectReference[] }> { + const result: Array<{ panel: SavedDashboardPanel; references: SavedObjectReference[] }> = []; + + for (const panel of panels) { + const embeddable = convertSavedDashboardPanelToPanelState(panel); + const { + state: embeddableInputWithExtractedReferences, + references, + } = deps.embeddablePersistableStateService.extract({ + ...embeddable.explicitInput, + type: embeddable.type, + }); + embeddable.explicitInput = omit(embeddableInputWithExtractedReferences, 'type'); + + const newPanel = convertPanelStateToSavedDashboardPanel(embeddable, panel.version); + result.push({ + panel: newPanel, + references, + }); + } + + return result; +} diff --git a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.test.ts b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.test.ts similarity index 82% rename from src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.test.ts rename to src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.test.ts index 926d5f405b384..bf044a1fa77d1 100644 --- a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.test.ts +++ b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.test.ts @@ -21,9 +21,8 @@ import { convertSavedDashboardPanelToPanelState, convertPanelStateToSavedDashboardPanel, } from './embeddable_saved_object_converters'; -import { SavedDashboardPanel } from '../../types'; -import { DashboardPanelState } from '../embeddable'; -import { EmbeddableInput } from '../../../../embeddable/public'; +import { SavedDashboardPanel, DashboardPanelState } from '../types'; +import { EmbeddableInput } from '../../../embeddable/common/types'; test('convertSavedDashboardPanelToPanelState', () => { const savedDashboardPanel: SavedDashboardPanel = { @@ -135,3 +134,24 @@ test('convertPanelStateToSavedDashboardPanel will not add an undefined id when n const converted = convertPanelStateToSavedDashboardPanel(dashboardPanel, '8.0.0'); expect(converted.hasOwnProperty('id')).toBe(false); }); + +test('convertPanelStateToSavedDashboardPanel will not leave title as part of embeddable config', () => { + const dashboardPanel: DashboardPanelState = { + gridData: { + x: 0, + y: 0, + h: 15, + w: 15, + i: '123', + }, + explicitInput: { + id: '123', + title: 'title', + } as EmbeddableInput, + type: 'search', + }; + + const converted = convertPanelStateToSavedDashboardPanel(dashboardPanel, '8.0.0'); + expect(converted.embeddableConfig.hasOwnProperty('title')).toBe(false); + expect(converted.title).toBe('title'); +}); diff --git a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.ts b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts similarity index 91% rename from src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.ts rename to src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts index b19ef31ccb9ac..b71b4f067ae33 100644 --- a/src/plugins/dashboard/public/application/lib/embeddable_saved_object_converters.ts +++ b/src/plugins/dashboard/common/embeddable/embeddable_saved_object_converters.ts @@ -17,9 +17,8 @@ * under the License. */ import { omit } from 'lodash'; -import { SavedDashboardPanel } from '../../types'; -import { DashboardPanelState } from '../embeddable'; -import { SavedObjectEmbeddableInput } from '../../embeddable_plugin'; +import { DashboardPanelState, SavedDashboardPanel } from '../types'; +import { SavedObjectEmbeddableInput } from '../../../embeddable/common/'; export function convertSavedDashboardPanelToPanelState( savedDashboardPanel: SavedDashboardPanel @@ -49,7 +48,7 @@ export function convertPanelStateToSavedDashboardPanel( type: panelState.type, gridData: panelState.gridData, panelIndex: panelState.explicitInput.id, - embeddableConfig: omit(panelState.explicitInput, ['id', 'savedObjectId']), + embeddableConfig: omit(panelState.explicitInput, ['id', 'savedObjectId', 'title']), ...(customTitle && { title: customTitle }), ...(savedObjectId !== undefined && { id: savedObjectId }), }; diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.test.ts b/src/plugins/dashboard/common/saved_dashboard_references.test.ts similarity index 52% rename from src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.test.ts rename to src/plugins/dashboard/common/saved_dashboard_references.test.ts index 48f15e84c9307..3632c4cca9e93 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.test.ts +++ b/src/plugins/dashboard/common/saved_dashboard_references.test.ts @@ -17,8 +17,18 @@ * under the License. */ -import { extractReferences, injectReferences } from './saved_dashboard_references'; -import { SavedObjectDashboard } from './saved_dashboard'; +import { + extractReferences, + injectReferences, + InjectDeps, + ExtractDeps, +} from './saved_dashboard_references'; +import { createEmbeddablePersistableStateServiceMock } from '../../embeddable/common/mocks'; + +const embeddablePersistableStateServiceMock = createEmbeddablePersistableStateServiceMock(); +const deps: InjectDeps & ExtractDeps = { + embeddablePersistableStateService: embeddablePersistableStateServiceMock, +}; describe('extractReferences', () => { test('extracts references from panelsJSON', () => { @@ -41,28 +51,28 @@ describe('extractReferences', () => { }, references: [], }; - const updatedDoc = extractReferences(doc); + const updatedDoc = extractReferences(doc, deps); expect(updatedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"panelRefName\\":\\"panel_0\\"},{\\"title\\":\\"Title 2\\",\\"panelRefName\\":\\"panel_1\\"}]", - }, - "references": Array [ - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], -} -`); + Object { + "attributes": Object { + "foo": true, + "panelsJSON": "[{\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"panelRefName\\":\\"panel_0\\"},{\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"panelRefName\\":\\"panel_1\\"}]", + }, + "references": Array [ + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + } + `); }); test('fails when "type" attribute is missing from a panel', () => { @@ -79,7 +89,7 @@ Object { }, references: [], }; - expect(() => extractReferences(doc)).toThrowErrorMatchingInlineSnapshot( + expect(() => extractReferences(doc, deps)).toThrowErrorMatchingInlineSnapshot( `"\\"type\\" attribute is missing from panel \\"0\\""` ); }); @@ -98,21 +108,21 @@ Object { }, references: [], }; - expect(extractReferences(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"title\\":\\"Title 1\\"}]", - }, - "references": Array [], -} -`); + expect(extractReferences(doc, deps)).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "foo": true, + "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\"}]", + }, + "references": Array [], + } + `); }); }); describe('injectReferences', () => { - test('injects references into context', () => { - const context = { + test('returns injected attributes', () => { + const attributes = { id: '1', title: 'test', panelsJSON: JSON.stringify([ @@ -125,7 +135,7 @@ describe('injectReferences', () => { title: 'Title 2', }, ]), - } as SavedObjectDashboard; + }; const references = [ { name: 'panel_0', @@ -138,49 +148,49 @@ describe('injectReferences', () => { id: '2', }, ]; - injectReferences(context, references); + const newAttributes = injectReferences({ attributes, references }, deps); - expect(context).toMatchInlineSnapshot(` -Object { - "id": "1", - "panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\",\\"type\\":\\"visualization\\"},{\\"title\\":\\"Title 2\\",\\"id\\":\\"2\\",\\"type\\":\\"visualization\\"}]", - "title": "test", -} -`); + expect(newAttributes).toMatchInlineSnapshot(` + Object { + "id": "1", + "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\",\\"id\\":\\"2\\"}]", + "title": "test", + } + `); }); test('skips when panelsJSON is missing', () => { - const context = { + const attributes = { id: '1', title: 'test', - } as SavedObjectDashboard; - injectReferences(context, []); - expect(context).toMatchInlineSnapshot(` -Object { - "id": "1", - "title": "test", -} -`); + }; + const newAttributes = injectReferences({ attributes, references: [] }, deps); + expect(newAttributes).toMatchInlineSnapshot(` + Object { + "id": "1", + "title": "test", + } + `); }); test('skips when panelsJSON is not an array', () => { - const context = { + const attributes = { id: '1', panelsJSON: '{}', title: 'test', - } as SavedObjectDashboard; - injectReferences(context, []); - expect(context).toMatchInlineSnapshot(` -Object { - "id": "1", - "panelsJSON": "{}", - "title": "test", -} -`); + }; + const newAttributes = injectReferences({ attributes, references: [] }, deps); + expect(newAttributes).toMatchInlineSnapshot(` + Object { + "id": "1", + "panelsJSON": "{}", + "title": "test", + } + `); }); test('skips a panel when panelRefName is missing', () => { - const context = { + const attributes = { id: '1', title: 'test', panelsJSON: JSON.stringify([ @@ -192,7 +202,7 @@ Object { title: 'Title 2', }, ]), - } as SavedObjectDashboard; + }; const references = [ { name: 'panel_0', @@ -200,18 +210,18 @@ Object { id: '1', }, ]; - injectReferences(context, references); - expect(context).toMatchInlineSnapshot(` -Object { - "id": "1", - "panelsJSON": "[{\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\",\\"type\\":\\"visualization\\"},{\\"title\\":\\"Title 2\\"}]", - "title": "test", -} -`); + const newAttributes = injectReferences({ attributes, references }, deps); + expect(newAttributes).toMatchInlineSnapshot(` + Object { + "id": "1", + "panelsJSON": "[{\\"type\\":\\"visualization\\",\\"embeddableConfig\\":{},\\"title\\":\\"Title 1\\",\\"id\\":\\"1\\"},{\\"embeddableConfig\\":{},\\"title\\":\\"Title 2\\"}]", + "title": "test", + } + `); }); test(`fails when it can't find the reference in the array`, () => { - const context = { + const attributes = { id: '1', title: 'test', panelsJSON: JSON.stringify([ @@ -220,9 +230,9 @@ Object { title: 'Title 1', }, ]), - } as SavedObjectDashboard; - expect(() => injectReferences(context, [])).toThrowErrorMatchingInlineSnapshot( - `"Could not find reference \\"panel_0\\""` - ); + }; + expect(() => + injectReferences({ attributes, references: [] }, deps) + ).toThrowErrorMatchingInlineSnapshot(`"Could not find reference \\"panel_0\\""`); }); }); diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.ts b/src/plugins/dashboard/common/saved_dashboard_references.ts similarity index 55% rename from src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.ts rename to src/plugins/dashboard/common/saved_dashboard_references.ts index 3df9e64887725..0726d301b34ac 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard_references.ts +++ b/src/plugins/dashboard/common/saved_dashboard_references.ts @@ -17,18 +17,47 @@ * under the License. */ -import { SavedObjectAttributes, SavedObjectReference } from 'kibana/public'; -import { SavedObjectDashboard } from './saved_dashboard'; +import { SavedObjectAttributes, SavedObjectReference } from '../../../core/types'; +import { + extractPanelsReferences, + injectPanelsReferences, +} from './embeddable/embeddable_references'; +import { SavedDashboardPanel730ToLatest } from './types'; +import { EmbeddablePersistableStateService } from '../../embeddable/common/types'; -export function extractReferences({ - attributes, - references = [], -}: { +export interface ExtractDeps { + embeddablePersistableStateService: EmbeddablePersistableStateService; +} + +export interface SavedObjectAttributesAndReferences { attributes: SavedObjectAttributes; references: SavedObjectReference[]; -}) { +} + +export function extractReferences( + { attributes, references = [] }: SavedObjectAttributesAndReferences, + deps: ExtractDeps +): SavedObjectAttributesAndReferences { + if (typeof attributes.panelsJSON !== 'string') { + return { attributes, references }; + } const panelReferences: SavedObjectReference[] = []; - const panels: Array> = JSON.parse(String(attributes.panelsJSON)); + let panels: Array> = JSON.parse(String(attributes.panelsJSON)); + + const extractedReferencesResult = extractPanelsReferences( + (panels as unknown) as SavedDashboardPanel730ToLatest[], + deps + ); + + panels = (extractedReferencesResult.map((res) => res.panel) as unknown) as Array< + Record + >; + extractedReferencesResult.forEach((res) => { + panelReferences.push(...res.references); + }); + + // TODO: This extraction should be done by EmbeddablePersistableStateService + // https://github.com/elastic/kibana/issues/82830 panels.forEach((panel, i) => { if (!panel.type) { throw new Error(`"type" attribute is missing from panel "${i}"`); @@ -46,6 +75,7 @@ export function extractReferences({ delete panel.type; delete panel.id; }); + return { references: [...references, ...panelReferences], attributes: { @@ -55,21 +85,28 @@ export function extractReferences({ }; } +export interface InjectDeps { + embeddablePersistableStateService: EmbeddablePersistableStateService; +} + export function injectReferences( - savedObject: SavedObjectDashboard, - references: SavedObjectReference[] -) { + { attributes, references = [] }: SavedObjectAttributesAndReferences, + deps: InjectDeps +): SavedObjectAttributes { // Skip if panelsJSON is missing otherwise this will cause saved object import to fail when // importing objects without panelsJSON. At development time of this, there is no guarantee each saved // object has panelsJSON in all previous versions of kibana. - if (typeof savedObject.panelsJSON !== 'string') { - return; + if (typeof attributes.panelsJSON !== 'string') { + return attributes; } - const panels = JSON.parse(savedObject.panelsJSON); + let panels = JSON.parse(attributes.panelsJSON); // Same here, prevent failing saved object import if ever panels aren't an array. if (!Array.isArray(panels)) { - return; + return attributes; } + + // TODO: This injection should be done by EmbeddablePersistableStateService + // https://github.com/elastic/kibana/issues/82830 panels.forEach((panel) => { if (!panel.panelRefName) { return; @@ -84,5 +121,11 @@ export function injectReferences( panel.type = reference.type; delete panel.panelRefName; }); - savedObject.panelsJSON = JSON.stringify(panels); + + panels = injectPanelsReferences(panels, references, deps); + + return { + ...attributes, + panelsJSON: JSON.stringify(panels), + }; } diff --git a/src/plugins/dashboard/common/types.ts b/src/plugins/dashboard/common/types.ts index 7cc82a9173976..ae214764052dc 100644 --- a/src/plugins/dashboard/common/types.ts +++ b/src/plugins/dashboard/common/types.ts @@ -17,6 +17,8 @@ * under the License. */ +import { EmbeddableInput, PanelState } from '../../../../src/plugins/embeddable/common/types'; +import { SavedObjectEmbeddableInput } from '../../../../src/plugins/embeddable/common/lib/saved_object_embeddable'; import { RawSavedDashboardPanelTo60, RawSavedDashboardPanel610, @@ -26,6 +28,21 @@ import { RawSavedDashboardPanel730ToLatest, } from './bwc/types'; +import { GridData } from './embeddable/types'; +export type PanelId = string; +export type SavedObjectId = string; + +export interface DashboardPanelState< + TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput +> extends PanelState { + readonly gridData: GridData; +} + +/** + * This should always represent the latest dashboard panel shape, after all possible migrations. + */ +export type SavedDashboardPanel = SavedDashboardPanel730ToLatest; + export type SavedDashboardPanel640To720 = Pick< RawSavedDashboardPanel640To720, Exclude diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index feae110c271fc..c99e4e4e06987 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -81,7 +81,6 @@ import { getTopNavConfig } from './top_nav/get_top_nav_config'; import { TopNavIds } from './top_nav/top_nav_ids'; import { getDashboardTitle } from './dashboard_strings'; import { DashboardAppScope } from './dashboard_app'; -import { convertSavedDashboardPanelToPanelState } from './lib/embeddable_saved_object_converters'; import { RenderDeps } from './application'; import { IKbnUrlStateStorage, @@ -97,6 +96,7 @@ import { subscribeWithScope, } from '../../../kibana_legacy/public'; import { migrateLegacyQuery } from './lib/migrate_legacy_query'; +import { convertSavedDashboardPanelToPanelState } from '../../common/embeddable/embeddable_saved_object_converters'; export interface DashboardAppControllerDependencies extends RenderDeps { $scope: DashboardAppScope; diff --git a/src/plugins/dashboard/public/application/dashboard_state_manager.ts b/src/plugins/dashboard/public/application/dashboard_state_manager.ts index 38479b1384477..6ef109ff60e42 100644 --- a/src/plugins/dashboard/public/application/dashboard_state_manager.ts +++ b/src/plugins/dashboard/public/application/dashboard_state_manager.ts @@ -30,7 +30,6 @@ import { migrateLegacyQuery } from './lib/migrate_legacy_query'; import { ViewMode } from '../embeddable_plugin'; import { getAppStateDefaults, migrateAppState, getDashboardIdFromUrl } from './lib'; -import { convertPanelStateToSavedDashboardPanel } from './lib/embeddable_saved_object_converters'; import { FilterUtils } from './lib/filter_utils'; import { DashboardAppState, @@ -48,6 +47,7 @@ import { } from '../../../kibana_utils/public'; import { SavedObjectDashboard } from '../saved_dashboards'; import { DashboardContainer } from './embeddable'; +import { convertPanelStateToSavedDashboardPanel } from '../../common/embeddable/embeddable_saved_object_converters'; /** * Dashboard state manager handles connecting angular and redux state between the angular and react portions of the diff --git a/src/plugins/dashboard/public/application/embeddable/types.ts b/src/plugins/dashboard/public/application/embeddable/types.ts index 66cdd22ed6bd4..efeb68c8a885a 100644 --- a/src/plugins/dashboard/public/application/embeddable/types.ts +++ b/src/plugins/dashboard/public/application/embeddable/types.ts @@ -16,14 +16,4 @@ * specific language governing permissions and limitations * under the License. */ -import { SavedObjectEmbeddableInput } from 'src/plugins/embeddable/public'; -import { GridData } from '../../../common'; -import { PanelState, EmbeddableInput } from '../../embeddable_plugin'; -export type PanelId = string; -export type SavedObjectId = string; - -export interface DashboardPanelState< - TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput -> extends PanelState { - readonly gridData: GridData; -} +export * from '../../../common/types'; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 53b892475704f..24bf736cfa274 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -450,6 +450,7 @@ export class DashboardPlugin const savedDashboardLoader = createSavedDashboardLoader({ savedObjectsClient: core.savedObjects.client, savedObjects: plugins.savedObjects, + embeddableStart: plugins.embeddable, }); const dashboardContainerFactory = plugins.embeddable.getEmbeddableFactory( DASHBOARD_CONTAINER_TYPE diff --git a/src/plugins/dashboard/public/saved_dashboards/index.ts b/src/plugins/dashboard/public/saved_dashboards/index.ts index 9b7745bd884f7..9adaf0dc3ba15 100644 --- a/src/plugins/dashboard/public/saved_dashboards/index.ts +++ b/src/plugins/dashboard/public/saved_dashboards/index.ts @@ -16,6 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -export * from './saved_dashboard_references'; +export * from '../../common/saved_dashboard_references'; export * from './saved_dashboard'; export * from './saved_dashboards'; diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts index bfc52ec33c35c..e3bfe346fbc07 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts +++ b/src/plugins/dashboard/public/saved_dashboards/saved_dashboard.ts @@ -17,10 +17,12 @@ * under the License. */ import { SavedObject, SavedObjectsStart } from '../../../../plugins/saved_objects/public'; -import { extractReferences, injectReferences } from './saved_dashboard_references'; import { Filter, ISearchSource, Query, RefreshInterval } from '../../../../plugins/data/public'; import { createDashboardEditUrl } from '../dashboard_constants'; +import { EmbeddableStart } from '../../../embeddable/public'; +import { SavedObjectAttributes, SavedObjectReference } from '../../../../core/types'; +import { extractReferences, injectReferences } from '../../common/saved_dashboard_references'; export interface SavedObjectDashboard extends SavedObject { id?: string; @@ -41,7 +43,8 @@ export interface SavedObjectDashboard extends SavedObject { // Used only by the savedDashboards service, usually no reason to change this export function createSavedDashboardClass( - savedObjectStart: SavedObjectsStart + savedObjectStart: SavedObjectsStart, + embeddableStart: EmbeddableStart ): new (id: string) => SavedObjectDashboard { class SavedDashboard extends savedObjectStart.SavedObjectClass { // save these objects with the 'dashboard' type @@ -77,8 +80,19 @@ export function createSavedDashboardClass( type: SavedDashboard.type, mapping: SavedDashboard.mapping, searchSource: SavedDashboard.searchSource, - extractReferences, - injectReferences, + extractReferences: (opts: { + attributes: SavedObjectAttributes; + references: SavedObjectReference[]; + }) => extractReferences(opts, { embeddablePersistableStateService: embeddableStart }), + injectReferences: (so: SavedObjectDashboard, references: SavedObjectReference[]) => { + const newAttributes = injectReferences( + { attributes: so._serialize().attributes, references }, + { + embeddablePersistableStateService: embeddableStart, + } + ); + Object.assign(so, newAttributes); + }, // if this is null/undefined then the SavedObject will be assigned the defaults id, diff --git a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts index 750fec4d4d1f9..7193a77fd0ec9 100644 --- a/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts +++ b/src/plugins/dashboard/public/saved_dashboards/saved_dashboards.ts @@ -20,16 +20,22 @@ import { SavedObjectsClientContract } from 'kibana/public'; import { SavedObjectLoader, SavedObjectsStart } from '../../../../plugins/saved_objects/public'; import { createSavedDashboardClass } from './saved_dashboard'; +import { EmbeddableStart } from '../../../embeddable/public'; interface Services { savedObjectsClient: SavedObjectsClientContract; savedObjects: SavedObjectsStart; + embeddableStart: EmbeddableStart; } /** * @param services */ -export function createSavedDashboardLoader({ savedObjects, savedObjectsClient }: Services) { - const SavedDashboard = createSavedDashboardClass(savedObjects); +export function createSavedDashboardLoader({ + savedObjects, + savedObjectsClient, + embeddableStart, +}: Services) { + const SavedDashboard = createSavedDashboardClass(savedObjects, embeddableStart); return new SavedObjectLoader(SavedDashboard, savedObjectsClient); } diff --git a/src/plugins/dashboard/public/types.ts b/src/plugins/dashboard/public/types.ts index 1af739c34b76a..8f6fe7fce5cfe 100644 --- a/src/plugins/dashboard/public/types.ts +++ b/src/plugins/dashboard/public/types.ts @@ -19,9 +19,12 @@ import { Query, Filter } from 'src/plugins/data/public'; import { SavedObject as SavedObjectType, SavedObjectAttributes } from 'src/core/public'; -import { SavedDashboardPanel730ToLatest } from '../common'; + import { ViewMode } from './embeddable_plugin'; +import { SavedDashboardPanel } from '../common/types'; +export { SavedDashboardPanel }; + export interface DashboardCapabilities { showWriteControls: boolean; createNew: boolean; @@ -71,11 +74,6 @@ export interface Field { export type NavAction = (anchorElement?: any) => void; -/** - * This should always represent the latest dashboard panel shape, after all possible migrations. - */ -export type SavedDashboardPanel = SavedDashboardPanel730ToLatest; - export interface DashboardAppState { panels: SavedDashboardPanel[]; fullScreenMode: boolean; diff --git a/src/plugins/dashboard/server/plugin.ts b/src/plugins/dashboard/server/plugin.ts index ba7bdeeda0133..6a4c297f25881 100644 --- a/src/plugins/dashboard/server/plugin.ts +++ b/src/plugins/dashboard/server/plugin.ts @@ -25,22 +25,34 @@ import { Logger, } from '../../../core/server'; -import { dashboardSavedObjectType } from './saved_objects'; +import { createDashboardSavedObjectType } from './saved_objects'; import { capabilitiesProvider } from './capabilities_provider'; import { DashboardPluginSetup, DashboardPluginStart } from './types'; +import { EmbeddableSetup } from '../../embeddable/server'; -export class DashboardPlugin implements Plugin { +interface SetupDeps { + embeddable: EmbeddableSetup; +} + +export class DashboardPlugin + implements Plugin { private readonly logger: Logger; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); } - public setup(core: CoreSetup) { + public setup(core: CoreSetup, plugins: SetupDeps) { this.logger.debug('dashboard: Setup'); - core.savedObjects.registerType(dashboardSavedObjectType); + core.savedObjects.registerType( + createDashboardSavedObjectType({ + migrationDeps: { + embeddable: plugins.embeddable, + }, + }) + ); core.capabilities.registerProvider(capabilitiesProvider); return {}; diff --git a/src/plugins/dashboard/server/saved_objects/dashboard.ts b/src/plugins/dashboard/server/saved_objects/dashboard.ts index a85f67f5ba56a..7d3e48ce1ae8b 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard.ts @@ -18,9 +18,16 @@ */ import { SavedObjectsType } from 'kibana/server'; -import { dashboardSavedObjectTypeMigrations } from './dashboard_migrations'; +import { + createDashboardSavedObjectTypeMigrations, + DashboardSavedObjectTypeMigrationsDeps, +} from './dashboard_migrations'; -export const dashboardSavedObjectType: SavedObjectsType = { +export const createDashboardSavedObjectType = ({ + migrationDeps, +}: { + migrationDeps: DashboardSavedObjectTypeMigrationsDeps; +}): SavedObjectsType => ({ name: 'dashboard', hidden: false, namespaceType: 'single', @@ -65,5 +72,5 @@ export const dashboardSavedObjectType: SavedObjectsType = { version: { type: 'integer' }, }, }, - migrations: dashboardSavedObjectTypeMigrations, -}; + migrations: createDashboardSavedObjectTypeMigrations(migrationDeps), +}); diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts index 22ed18f75c652..50f12d21d4db9 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts @@ -19,7 +19,14 @@ import { SavedObjectUnsanitizedDoc } from 'kibana/server'; import { savedObjectsServiceMock } from '../../../../core/server/mocks'; -import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations'; +import { createEmbeddableSetupMock } from '../../../embeddable/server/mocks'; +import { createDashboardSavedObjectTypeMigrations } from './dashboard_migrations'; +import { DashboardDoc730ToLatest } from '../../common'; + +const embeddableSetupMock = createEmbeddableSetupMock(); +const migrations = createDashboardSavedObjectTypeMigrations({ + embeddable: embeddableSetupMock, +}); const contextMock = savedObjectsServiceMock.createMigrationContext(); @@ -448,4 +455,50 @@ Object { `); }); }); + + describe('7.11.0 - embeddable persistable state extraction', () => { + const migration = migrations['7.11.0']; + const doc: DashboardDoc730ToLatest = { + attributes: { + description: '', + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"query":{"language":"kuery","query":""},"filter":[{"query":{"match_phrase":{"machine.os.keyword":"osx"}},"$state":{"store":"appState"},"meta":{"type":"phrase","key":"machine.os.keyword","params":{"query":"osx"},"disabled":false,"negate":false,"alias":null,"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index"}}]}', + }, + optionsJSON: '{"useMargins":true,"hidePanelTitles":false}', + panelsJSON: + '[{"version":"7.9.3","gridData":{"x":0,"y":0,"w":24,"h":15,"i":"82fa0882-9f9e-476a-bbb9-03555e5ced91"},"panelIndex":"82fa0882-9f9e-476a-bbb9-03555e5ced91","embeddableConfig":{"enhancements":{"dynamicActions":{"events":[]}}},"panelRefName":"panel_0"}]', + timeRestore: false, + title: 'Dashboard A', + version: 1, + }, + id: '376e6260-1f5e-11eb-91aa-7b6d5f8a61d6', + references: [ + { + id: '90943e30-9a47-11e8-b64d-95841ca0b247', + name: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', + type: 'index-pattern', + }, + { id: '14e2e710-4258-11e8-b3aa-73fdaf54bfc9', name: 'panel_0', type: 'visualization' }, + ], + type: 'dashboard', + }; + + test('should migrate 7.3.0 doc without embeddable state to extract', () => { + const newDoc = migration(doc, contextMock); + expect(newDoc).toEqual(doc); + }); + + test('should migrate 7.3.0 doc and extract embeddable state', () => { + embeddableSetupMock.extract.mockImplementationOnce((state) => ({ + state: { ...state, __extracted: true }, + references: [{ id: '__new', name: '__newRefName', type: '__newType' }], + })); + + const newDoc = migration(doc, contextMock); + expect(newDoc).not.toEqual(doc); + expect(newDoc.references).toHaveLength(doc.references.length + 1); + expect(JSON.parse(newDoc.attributes.panelsJSON)[0].embeddableConfig.__extracted).toBe(true); + }); + }); }); diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts index ac91c5a92048a..177440c5ea5d1 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts @@ -18,11 +18,12 @@ */ import { get, flow } from 'lodash'; - -import { SavedObjectMigrationFn } from 'kibana/server'; +import { SavedObjectAttributes, SavedObjectMigrationFn } from 'kibana/server'; import { migrations730 } from './migrations_730'; import { migrateMatchAllQuery } from './migrate_match_all_query'; -import { DashboardDoc700To720 } from '../../common'; +import { DashboardDoc700To720, DashboardDoc730ToLatest } from '../../common'; +import { EmbeddableSetup } from '../../../embeddable/server'; +import { injectReferences, extractReferences } from '../../common/saved_dashboard_references'; function migrateIndexPattern(doc: DashboardDoc700To720) { const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); @@ -100,7 +101,57 @@ const migrations700: SavedObjectMigrationFn = (doc): DashboardDoc700To return doc as DashboardDoc700To720; }; -export const dashboardSavedObjectTypeMigrations = { +/** + * In 7.8.0 we introduced dashboard drilldowns which are stored inside dashboard saved object as part of embeddable state + * In 7.11.0 we created an embeddable references/migrations system that allows to properly extract embeddable persistable state + * https://github.com/elastic/kibana/issues/71409 + * The idea of this migration is to inject all the embeddable panel references and then run the extraction again. + * As the result of the extraction: + * 1. In addition to regular `panel_` we will get new references which are extracted by `embeddablePersistableStateService` (dashboard drilldown references) + * 2. `panel_` references will be regenerated + * All other references like index-patterns are forwarded non touched + * @param deps + */ +function createExtractPanelReferencesMigration( + deps: DashboardSavedObjectTypeMigrationsDeps +): SavedObjectMigrationFn { + return (doc) => { + const references = doc.references ?? []; + + /** + * Remembering this because dashboard's extractReferences won't return those + * All other references like `panel_` will be overwritten + */ + const oldNonPanelReferences = references.filter((ref) => !ref.name.startsWith('panel_')); + + const injectedAttributes = injectReferences( + { + attributes: (doc.attributes as unknown) as SavedObjectAttributes, + references, + }, + { embeddablePersistableStateService: deps.embeddable } + ); + + const { attributes, references: newPanelReferences } = extractReferences( + { attributes: injectedAttributes, references: [] }, + { embeddablePersistableStateService: deps.embeddable } + ); + + return { + ...doc, + references: [...oldNonPanelReferences, ...newPanelReferences], + attributes, + }; + }; +} + +export interface DashboardSavedObjectTypeMigrationsDeps { + embeddable: EmbeddableSetup; +} + +export const createDashboardSavedObjectTypeMigrations = ( + deps: DashboardSavedObjectTypeMigrationsDeps +) => ({ /** * We need to have this migration twice, once with a version prior to 7.0.0 once with a version * after it. The reason for that is, that this migration has been introduced once 7.0.0 was already @@ -115,4 +166,5 @@ export const dashboardSavedObjectTypeMigrations = { '7.0.0': flow(migrations700), '7.3.0': flow(migrations730), '7.9.3': flow(migrateMatchAllQuery), -}; + '7.11.0': flow(createExtractPanelReferencesMigration(deps)), +}); diff --git a/src/plugins/dashboard/server/saved_objects/index.ts b/src/plugins/dashboard/server/saved_objects/index.ts index ca97b9d2a6b70..ea4808de96848 100644 --- a/src/plugins/dashboard/server/saved_objects/index.ts +++ b/src/plugins/dashboard/server/saved_objects/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { dashboardSavedObjectType } from './dashboard'; +export { createDashboardSavedObjectType } from './dashboard'; diff --git a/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts b/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts index a58df547fa522..37a8881ab520b 100644 --- a/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts +++ b/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts @@ -18,12 +18,16 @@ */ import { savedObjectsServiceMock } from '../../../../core/server/mocks'; -import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations'; +import { createDashboardSavedObjectTypeMigrations } from './dashboard_migrations'; import { migrations730 } from './migrations_730'; import { DashboardDoc700To720, DashboardDoc730ToLatest, DashboardDocPre700 } from '../../common'; import { RawSavedDashboardPanel730ToLatest } from '../../common'; +import { createEmbeddableSetupMock } from '../../../embeddable/server/mocks'; const mockContext = savedObjectsServiceMock.createMigrationContext(); +const migrations = createDashboardSavedObjectTypeMigrations({ + embeddable: createEmbeddableSetupMock(), +}); test('dashboard migration 7.3.0 migrates filters to query on search source', () => { const doc: DashboardDoc700To720 = { diff --git a/src/plugins/embeddable/common/index.ts b/src/plugins/embeddable/common/index.ts new file mode 100644 index 0000000000000..a4cbfb11b36f8 --- /dev/null +++ b/src/plugins/embeddable/common/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 * from './types'; +export * from './lib'; diff --git a/src/plugins/embeddable/common/lib/index.ts b/src/plugins/embeddable/common/lib/index.ts index e180ca9489df0..1ac6834365cd1 100644 --- a/src/plugins/embeddable/common/lib/index.ts +++ b/src/plugins/embeddable/common/lib/index.ts @@ -22,3 +22,4 @@ export * from './inject'; export * from './migrate'; export * from './migrate_base_input'; export * from './telemetry'; +export * from './saved_object_embeddable'; diff --git a/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts b/src/plugins/embeddable/common/lib/saved_object_embeddable.ts similarity index 96% rename from src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts rename to src/plugins/embeddable/common/lib/saved_object_embeddable.ts index 5f093c55e94e4..f2dc9ed1ae395 100644 --- a/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts +++ b/src/plugins/embeddable/common/lib/saved_object_embeddable.ts @@ -17,7 +17,7 @@ * under the License. */ -import { EmbeddableInput } from '..'; +import { EmbeddableInput } from '../types'; export interface SavedObjectEmbeddableInput extends EmbeddableInput { savedObjectId: string; diff --git a/src/plugins/embeddable/common/mocks.ts b/src/plugins/embeddable/common/mocks.ts new file mode 100644 index 0000000000000..a9ac144d1f276 --- /dev/null +++ b/src/plugins/embeddable/common/mocks.ts @@ -0,0 +1,31 @@ +/* + * 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 { EmbeddablePersistableStateService } from './types'; + +export const createEmbeddablePersistableStateServiceMock = (): jest.Mocked< + EmbeddablePersistableStateService +> => { + return { + inject: jest.fn((state, references) => state), + extract: jest.fn((state) => ({ state, references: [] })), + migrate: jest.fn((state, version) => state), + telemetry: jest.fn((state, collector) => ({})), + }; +}; diff --git a/src/plugins/embeddable/common/types.ts b/src/plugins/embeddable/common/types.ts index 7e024eda9b793..8965446cc85fa 100644 --- a/src/plugins/embeddable/common/types.ts +++ b/src/plugins/embeddable/common/types.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SerializableState } from '../../kibana_utils/common'; +import { PersistableStateService, SerializableState } from '../../kibana_utils/common'; import { Query, TimeRange } from '../../data/common/query'; import { Filter } from '../../data/common/es_query/filters'; @@ -74,8 +74,21 @@ export type EmbeddableInput = { searchSessionId?: string; }; +export interface PanelState { + // The type of embeddable in this panel. Will be used to find the factory in which to + // load the embeddable. + type: string; + + // Stores input for this embeddable that is specific to this embeddable. Other parts of embeddable input + // will be derived from the container's input. **Any state in here will override any state derived from + // the container.** + explicitInput: Partial & { id: string }; +} + export type EmbeddableStateWithType = EmbeddableInput & { type: string }; +export type EmbeddablePersistableStateService = PersistableStateService; + export interface CommonEmbeddableStartContract { getEmbeddableFactory: (embeddableFactoryId: string) => any; getEnhancement: (enhancementId: string) => any; diff --git a/src/plugins/embeddable/public/lib/containers/container.ts b/src/plugins/embeddable/public/lib/containers/container.ts index 4dede8bf5d752..a5c5133dbc702 100644 --- a/src/plugins/embeddable/public/lib/containers/container.ts +++ b/src/plugins/embeddable/public/lib/containers/container.ts @@ -31,7 +31,7 @@ import { import { IContainer, ContainerInput, ContainerOutput, PanelState } from './i_container'; import { PanelNotFoundError, EmbeddableFactoryNotFoundError } from '../errors'; import { EmbeddableStart } from '../../plugin'; -import { isSavedObjectEmbeddableInput } from '../embeddables/saved_object_embeddable'; +import { isSavedObjectEmbeddableInput } from '../../../common/lib/saved_object_embeddable'; const getKeys = (o: T): Array => Object.keys(o) as Array; diff --git a/src/plugins/embeddable/public/lib/containers/i_container.ts b/src/plugins/embeddable/public/lib/containers/i_container.ts index db219fa8b7314..270caec2f3f84 100644 --- a/src/plugins/embeddable/public/lib/containers/i_container.ts +++ b/src/plugins/embeddable/public/lib/containers/i_container.ts @@ -24,17 +24,9 @@ import { ErrorEmbeddable, IEmbeddable, } from '../embeddables'; +import { PanelState } from '../../../common/types'; -export interface PanelState { - // The type of embeddable in this panel. Will be used to find the factory in which to - // load the embeddable. - type: string; - - // Stores input for this embeddable that is specific to this embeddable. Other parts of embeddable input - // will be derived from the container's input. **Any state in here will override any state derived from - // the container.** - explicitInput: Partial & { id: string }; -} +export { PanelState }; export interface ContainerOutput extends EmbeddableOutput { embeddableLoaded: { [key: string]: boolean }; diff --git a/src/plugins/embeddable/public/lib/embeddables/index.ts b/src/plugins/embeddable/public/lib/embeddables/index.ts index 5bab5ac27f3cc..2f6de1be60c9c 100644 --- a/src/plugins/embeddable/public/lib/embeddables/index.ts +++ b/src/plugins/embeddable/public/lib/embeddables/index.ts @@ -24,5 +24,5 @@ export * from './default_embeddable_factory_provider'; export { ErrorEmbeddable, isErrorEmbeddable } from './error_embeddable'; export { withEmbeddableSubscription } from './with_subscription'; export { EmbeddableRoot } from './embeddable_root'; -export * from './saved_object_embeddable'; +export * from '../../../common/lib/saved_object_embeddable'; export { EmbeddableRenderer, EmbeddableRendererProps } from './embeddable_renderer'; diff --git a/src/plugins/embeddable/server/mocks.ts b/src/plugins/embeddable/server/mocks.ts new file mode 100644 index 0000000000000..28bb9542ab7cb --- /dev/null +++ b/src/plugins/embeddable/server/mocks.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 { createEmbeddablePersistableStateServiceMock } from '../common/mocks'; +import { EmbeddableSetup, EmbeddableStart } from './plugin'; + +export const createEmbeddableSetupMock = (): jest.Mocked => ({ + ...createEmbeddablePersistableStateServiceMock(), + registerEmbeddableFactory: jest.fn(), + registerEnhancement: jest.fn(), +}); + +export const createEmbeddableStartMock = (): jest.Mocked => + createEmbeddablePersistableStateServiceMock(); diff --git a/src/plugins/embeddable/server/plugin.ts b/src/plugins/embeddable/server/plugin.ts index 6e9186e286491..d99675f950ad0 100644 --- a/src/plugins/embeddable/server/plugin.ts +++ b/src/plugins/embeddable/server/plugin.ts @@ -32,23 +32,32 @@ import { getMigrateFunction, getTelemetryFunction, } from '../common/lib'; -import { SerializableState } from '../../kibana_utils/common'; +import { PersistableStateService, SerializableState } from '../../kibana_utils/common'; import { EmbeddableStateWithType } from '../common/types'; -export interface EmbeddableSetup { - getAttributeService: any; +export interface EmbeddableSetup extends PersistableStateService { registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => void; registerEnhancement: (enhancement: EnhancementRegistryDefinition) => void; } -export class EmbeddableServerPlugin implements Plugin { +export type EmbeddableStart = PersistableStateService; + +export class EmbeddableServerPlugin implements Plugin { private readonly embeddableFactories: EmbeddableFactoryRegistry = new Map(); private readonly enhancements: EnhancementsRegistry = new Map(); public setup(core: CoreSetup) { + const commonContract = { + getEmbeddableFactory: this.getEmbeddableFactory, + getEnhancement: this.getEnhancement, + }; return { registerEmbeddableFactory: this.registerEmbeddableFactory, registerEnhancement: this.registerEnhancement, + telemetry: getTelemetryFunction(commonContract), + extract: getExtractFunction(commonContract), + inject: getInjectFunction(commonContract), + migrate: getMigrateFunction(commonContract), }; } diff --git a/src/plugins/embeddable/server/server.api.md b/src/plugins/embeddable/server/server.api.md index 87f7d76cffaa8..d3921ab11457c 100644 --- a/src/plugins/embeddable/server/server.api.md +++ b/src/plugins/embeddable/server/server.api.md @@ -18,12 +18,11 @@ export interface EmbeddableRegistryDefinition

{ // (undocumented) registerEmbeddableFactory: (factory: EmbeddableRegistryDefinition) => void; // (undocumented) diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/constants.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/constants.ts new file mode 100644 index 0000000000000..922ec36619a4b --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/constants.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * NOTE: DO NOT CHANGE THIS STRING WITHOUT CAREFUL CONSIDERATOIN, BECAUSE IT IS + * STORED IN SAVED OBJECTS. + * + * Also temporary dashboard drilldown migration code inside embeddable plugin relies on it + * x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts + */ +export const EMBEDDABLE_TO_DASHBOARD_DRILLDOWN = 'DASHBOARD_TO_DASHBOARD_DRILLDOWN'; diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.test.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.test.ts new file mode 100644 index 0000000000000..dd890b2463226 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.test.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 { createExtract, createInject } from './dashboard_drilldown_persistable_state'; +import { SerializedEvent } from '../../../../ui_actions_enhanced/common'; + +const drilldownId = 'test_id'; +const extract = createExtract({ drilldownId }); +const inject = createInject({ drilldownId }); + +const state: SerializedEvent = { + eventId: 'event_id', + triggers: [], + action: { + factoryId: drilldownId, + name: 'name', + config: { + dashboardId: 'dashboardId_1', + }, + }, +}; + +test('should extract and injected dashboard reference', () => { + const { state: extractedState, references } = extract(state); + expect(extractedState).not.toEqual(state); + expect(extractedState.action.config.dashboardId).toBeUndefined(); + expect(references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "dashboardId_1", + "name": "drilldown:test_id:event_id:dashboardId", + "type": "dashboard", + }, + ] + `); + + let injectedState = inject(extractedState, references); + expect(injectedState).toEqual(state); + + references[0].id = 'dashboardId_2'; + + injectedState = inject(extractedState, references); + expect(injectedState).not.toEqual(extractedState); + expect(injectedState.action.config.dashboardId).toBe('dashboardId_2'); +}); diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.ts new file mode 100644 index 0000000000000..bd972723c649b --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/dashboard_drilldown_persistable_state.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectReference } from '../../../../../../src/core/types'; +import { PersistableStateService } from '../../../../../../src/plugins/kibana_utils/common'; +import { SerializedAction, SerializedEvent } from '../../../../ui_actions_enhanced/common'; +import { DrilldownConfig } from './types'; + +type DashboardDrilldownPersistableState = PersistableStateService; + +const generateRefName = (state: SerializedEvent, id: string) => + `drilldown:${id}:${state.eventId}:dashboardId`; + +const injectDashboardId = (state: SerializedEvent, dashboardId: string): SerializedEvent => { + return { + ...state, + action: { + ...state.action, + config: { + ...state.action.config, + dashboardId, + }, + }, + }; +}; + +export const createInject = ({ + drilldownId, +}: { + drilldownId: string; +}): DashboardDrilldownPersistableState['inject'] => { + return (state: SerializedEvent, references: SavedObjectReference[]) => { + const action = state.action as SerializedAction; + const refName = generateRefName(state, drilldownId); + const ref = references.find((r) => r.name === refName); + if (!ref) return state; + if (ref.id && ref.id === action.config.dashboardId) return state; + return injectDashboardId(state, ref.id); + }; +}; + +export const createExtract = ({ + drilldownId, +}: { + drilldownId: string; +}): DashboardDrilldownPersistableState['extract'] => { + return (state: SerializedEvent) => { + const action = state.action as SerializedAction; + const references: SavedObjectReference[] = action.config.dashboardId + ? [ + { + name: generateRefName(state, drilldownId), + type: 'dashboard', + id: action.config.dashboardId, + }, + ] + : []; + + const { dashboardId, ...restOfConfig } = action.config; + + return { + state: { + ...state, + action: ({ + ...state.action, + config: restOfConfig, + } as unknown) as SerializedAction, + }, + references, + }; + }; +}; diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/index.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/index.ts new file mode 100644 index 0000000000000..f6a757ad7a180 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createExtract, createInject } from './dashboard_drilldown_persistable_state'; +export { EMBEDDABLE_TO_DASHBOARD_DRILLDOWN } from './constants'; +export { DrilldownConfig } from './types'; diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/types.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/types.ts new file mode 100644 index 0000000000000..3be2a9739837e --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/dashboard_drilldown/types.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type DrilldownConfig = { + dashboardId?: string; + useCurrentFilters: boolean; + useCurrentDateRange: boolean; +}; diff --git a/x-pack/plugins/dashboard_enhanced/common/drilldowns/index.ts b/x-pack/plugins/dashboard_enhanced/common/drilldowns/index.ts new file mode 100644 index 0000000000000..76c9abbd4bfbe --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/drilldowns/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './dashboard_drilldown'; diff --git a/x-pack/plugins/dashboard_enhanced/common/index.ts b/x-pack/plugins/dashboard_enhanced/common/index.ts new file mode 100644 index 0000000000000..8cc3e12906531 --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/common/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './drilldowns'; diff --git a/x-pack/plugins/dashboard_enhanced/kibana.json b/x-pack/plugins/dashboard_enhanced/kibana.json index f79a69c9f4aba..b24c0b6983f40 100644 --- a/x-pack/plugins/dashboard_enhanced/kibana.json +++ b/x-pack/plugins/dashboard_enhanced/kibana.json @@ -1,7 +1,7 @@ { "id": "dashboardEnhanced", "version": "kibana", - "server": false, + "server": true, "ui": true, "requiredPlugins": ["data", "uiActionsEnhanced", "embeddable", "dashboard", "share"], "configPath": ["xpack", "dashboardEnhanced"], diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx index b098d66619814..451254efd9648 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/abstract_dashboard_drilldown.tsx @@ -9,19 +9,19 @@ import { DataPublicPluginStart } from 'src/plugins/data/public'; import { DashboardStart } from 'src/plugins/dashboard/public'; import { reactToUiComponent } from '../../../../../../../src/plugins/kibana_react/public'; import { - TriggerId, TriggerContextMapping, + TriggerId, } from '../../../../../../../src/plugins/ui_actions/public'; import { CollectConfigContainer } from './components'; import { - UiActionsEnhancedDrilldownDefinition as Drilldown, - UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext, AdvancedUiActionsStart, + UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext, + UiActionsEnhancedDrilldownDefinition as Drilldown, } from '../../../../../ui_actions_enhanced/public'; import { txtGoToDashboard } from './i18n'; import { - StartServicesGetter, CollectConfigProps, + StartServicesGetter, } from '../../../../../../../src/plugins/kibana_utils/public'; import { KibanaURL } from '../../../../../../../src/plugins/share/public'; import { Config } from './types'; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts index 330a501a78d39..7f5137812ee32 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/abstract_dashboard_drilldown/types.ts @@ -6,12 +6,8 @@ import { UiActionsEnhancedBaseActionFactoryContext } from '../../../../../ui_actions_enhanced/public'; import { APPLY_FILTER_TRIGGER } from '../../../../../../../src/plugins/ui_actions/public'; +import { DrilldownConfig } from '../../../../common'; -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -export type Config = { - dashboardId?: string; - useCurrentFilters: boolean; - useCurrentDateRange: boolean; -}; +export type Config = DrilldownConfig; export type FactoryContext = UiActionsEnhancedBaseActionFactoryContext; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx index f6de2ba931c58..5bfb175ea0d00 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx @@ -65,6 +65,12 @@ test('getHref is defined', () => { expect(drilldown.getHref).toBeDefined(); }); +test('inject/extract are defined', () => { + const drilldown = new EmbeddableToDashboardDrilldown({} as any); + expect(drilldown.extract).toBeDefined(); + expect(drilldown.inject).toBeDefined(); +}); + describe('.execute() & getHref', () => { /** * A convenience test setup helper diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx index 25bc93ad38b36..921c2aed00624 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.tsx @@ -22,6 +22,7 @@ import { } from '../abstract_dashboard_drilldown'; import { KibanaURL } from '../../../../../../../src/plugins/share/public'; import { EMBEDDABLE_TO_DASHBOARD_DRILLDOWN } from './constants'; +import { createExtract, createInject } from '../../../../common'; type Trigger = typeof APPLY_FILTER_TRIGGER; type Context = TriggerContextMapping[Trigger]; @@ -80,4 +81,8 @@ export class EmbeddableToDashboardDrilldown extends AbstractDashboardDrilldown { + constructor(protected readonly context: PluginInitializerContext) {} + + public setup(core: CoreSetup, plugins: SetupDependencies): SetupContract { + plugins.uiActionsEnhanced.registerActionFactory({ + id: EMBEDDABLE_TO_DASHBOARD_DRILLDOWN, + inject: createInject({ drilldownId: EMBEDDABLE_TO_DASHBOARD_DRILLDOWN }), + extract: createExtract({ drilldownId: EMBEDDABLE_TO_DASHBOARD_DRILLDOWN }), + }); + + return {}; + } + + public start(core: CoreStart, plugins: StartDependencies): StartContract { + return {}; + } + + public stop() {} +} diff --git a/x-pack/plugins/ui_actions_enhanced/common/index.ts b/x-pack/plugins/ui_actions_enhanced/common/index.ts new file mode 100644 index 0000000000000..9f4141dbcae7d --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/common/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './types'; diff --git a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts index b366436200914..ade78c31211ab 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts @@ -7,11 +7,11 @@ import { EnhancementRegistryDefinition } from '../../../../src/plugins/embeddable/server'; import { SavedObjectReference } from '../../../../src/core/types'; import { DynamicActionsState, SerializedEvent } from './types'; -import { AdvancedUiActionsPublicPlugin } from './plugin'; +import { AdvancedUiActionsServerPlugin } from './plugin'; import { SerializableState } from '../../../../src/plugins/kibana_utils/common'; export const dynamicActionEnhancement = ( - uiActionsEnhanced: AdvancedUiActionsPublicPlugin + uiActionsEnhanced: AdvancedUiActionsServerPlugin ): EnhancementRegistryDefinition => { return { id: 'dynamicActions', diff --git a/x-pack/plugins/ui_actions_enhanced/server/index.ts b/x-pack/plugins/ui_actions_enhanced/server/index.ts index 5419c4135796d..e1363be35e2e9 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/index.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/index.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AdvancedUiActionsPublicPlugin } from './plugin'; +import { AdvancedUiActionsServerPlugin } from './plugin'; export function plugin() { - return new AdvancedUiActionsPublicPlugin(); + return new AdvancedUiActionsServerPlugin(); } -export { AdvancedUiActionsPublicPlugin as Plugin }; +export { AdvancedUiActionsServerPlugin as Plugin }; export { SetupContract as AdvancedUiActionsSetup, StartContract as AdvancedUiActionsStart, diff --git a/x-pack/plugins/ui_actions_enhanced/server/plugin.ts b/x-pack/plugins/ui_actions_enhanced/server/plugin.ts index d6d18848be4de..718304018730d 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/plugin.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/plugin.ts @@ -16,7 +16,7 @@ import { } from './types'; export interface SetupContract { - registerActionFactory: any; + registerActionFactory: (definition: ActionFactoryDefinition) => void; } export type StartContract = void; @@ -25,7 +25,7 @@ interface SetupDependencies { embeddable: EmbeddableSetup; // Embeddable are needed because they register basic triggers/actions. } -export class AdvancedUiActionsPublicPlugin +export class AdvancedUiActionsServerPlugin implements Plugin { protected readonly actionFactories: ActionFactoryRegistry = new Map(); diff --git a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts index 43b88915b69d9..9326f7e240e3e 100644 --- a/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts +++ b/x-pack/test/functional/apps/dashboard/drilldowns/dashboard_to_dashboard_drilldown.ts @@ -14,7 +14,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardPanelActions = getService('dashboardPanelActions'); const dashboardDrilldownPanelActions = getService('dashboardDrilldownPanelActions'); const dashboardDrilldownsManage = getService('dashboardDrilldownsManage'); - const PageObjects = getPageObjects(['dashboard', 'common', 'header', 'timePicker']); + const PageObjects = getPageObjects([ + 'dashboard', + 'common', + 'header', + 'timePicker', + 'settings', + 'copySavedObjectsToSpace', + ]); const pieChart = getService('pieChart'); const log = getService('log'); const browser = getService('browser'); @@ -22,120 +29,188 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const filterBar = getService('filterBar'); const security = getService('security'); + const spaces = getService('spaces'); describe('Dashboard to dashboard drilldown', function () { - before(async () => { - log.debug('Dashboard Drilldowns:initTests'); - await security.testUser.setRoles(['test_logstash_reader', 'global_dashboard_all']); - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.preserveCrossAppState(); - }); - - after(async () => { - await security.testUser.restoreDefaults(); - }); - - it('should create dashboard to dashboard drilldown, use it, and then delete it', async () => { - await PageObjects.dashboard.gotoDashboardEditMode( - dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME - ); - - // create drilldown - await dashboardPanelActions.openContextMenu(); - await dashboardDrilldownPanelActions.expectExistsCreateDrilldownAction(); - await dashboardDrilldownPanelActions.clickCreateDrilldown(); - await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutOpen(); - await dashboardDrilldownsManage.fillInDashboardToDashboardDrilldownWizard({ - drilldownName: DRILLDOWN_TO_AREA_CHART_NAME, - destinationDashboardTitle: dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME, + describe('Create & use drilldowns', () => { + before(async () => { + log.debug('Dashboard Drilldowns:initTests'); + await security.testUser.setRoles(['test_logstash_reader', 'global_dashboard_all']); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); }); - await dashboardDrilldownsManage.saveChanges(); - await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutClose(); - - // check that drilldown notification badge is shown - expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(1); - - // save dashboard, navigate to view mode - await PageObjects.dashboard.saveDashboard( - dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME, - { - saveAsNew: false, - waitDialogIsClosed: true, - } - ); - - // trigger drilldown action by clicking on a pie and picking drilldown action by it's name - await pieChart.clickOnPieSlice('40,000'); - await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); - - const href = await dashboardDrilldownPanelActions.getActionHrefByText( - DRILLDOWN_TO_AREA_CHART_NAME - ); - expect(typeof href).to.be('string'); // checking that action has a href - const dashboardIdFromHref = PageObjects.dashboard.getDashboardIdFromUrl(href); - - await navigateWithinDashboard(async () => { - await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_AREA_CHART_NAME); - }); - // checking that href is at least pointing to the same dashboard that we are navigated to by regular click - expect(dashboardIdFromHref).to.be(await PageObjects.dashboard.getDashboardIdFromCurrentUrl()); - - // check that we drilled-down with filter from pie chart - expect(await filterBar.getFilterCount()).to.be(1); - - const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - // brush area chart and drilldown back to pie chat dashboard - await brushAreaChart(); - await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); - - await navigateWithinDashboard(async () => { - await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + after(async () => { + await security.testUser.restoreDefaults(); }); - // because filters are preserved during navigation, we expect that only one slice is displayed (filter is still applied) - expect(await filterBar.getFilterCount()).to.be(1); - await pieChart.expectPieSliceCount(1); - - // check that new time range duration was applied - const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); - - // delete drilldown - await PageObjects.dashboard.switchToEditMode(); - await dashboardPanelActions.openContextMenu(); - await dashboardDrilldownPanelActions.expectExistsManageDrilldownsAction(); - await dashboardDrilldownPanelActions.clickManageDrilldowns(); - await dashboardDrilldownsManage.expectsManageDrilldownsFlyoutOpen(); - - await dashboardDrilldownsManage.deleteDrilldownsByTitles([DRILLDOWN_TO_AREA_CHART_NAME]); - await dashboardDrilldownsManage.closeFlyout(); + it('should create dashboard to dashboard drilldown, use it, and then delete it', async () => { + await PageObjects.dashboard.gotoDashboardEditMode( + dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME + ); + + // create drilldown + await dashboardPanelActions.openContextMenu(); + await dashboardDrilldownPanelActions.expectExistsCreateDrilldownAction(); + await dashboardDrilldownPanelActions.clickCreateDrilldown(); + await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutOpen(); + await dashboardDrilldownsManage.fillInDashboardToDashboardDrilldownWizard({ + drilldownName: DRILLDOWN_TO_AREA_CHART_NAME, + destinationDashboardTitle: dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME, + }); + await dashboardDrilldownsManage.saveChanges(); + await dashboardDrilldownsManage.expectsCreateDrilldownFlyoutClose(); + + // check that drilldown notification badge is shown + expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(1); + + // save dashboard, navigate to view mode + await PageObjects.dashboard.saveDashboard( + dashboardDrilldownsManage.DASHBOARD_WITH_PIE_CHART_NAME, + { + saveAsNew: false, + waitDialogIsClosed: true, + } + ); + + // trigger drilldown action by clicking on a pie and picking drilldown action by it's name + await pieChart.clickOnPieSlice('40,000'); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + + const href = await dashboardDrilldownPanelActions.getActionHrefByText( + DRILLDOWN_TO_AREA_CHART_NAME + ); + expect(typeof href).to.be('string'); // checking that action has a href + const dashboardIdFromHref = PageObjects.dashboard.getDashboardIdFromUrl(href); + + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_AREA_CHART_NAME); + }); + // checking that href is at least pointing to the same dashboard that we are navigated to by regular click + expect(dashboardIdFromHref).to.be( + await PageObjects.dashboard.getDashboardIdFromCurrentUrl() + ); + + // check that we drilled-down with filter from pie chart + expect(await filterBar.getFilterCount()).to.be(1); + + const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + + // brush area chart and drilldown back to pie chat dashboard + await brushAreaChart(); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + }); + + // because filters are preserved during navigation, we expect that only one slice is displayed (filter is still applied) + expect(await filterBar.getFilterCount()).to.be(1); + await pieChart.expectPieSliceCount(1); + + // check that new time range duration was applied + const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); + + // delete drilldown + await PageObjects.dashboard.switchToEditMode(); + await dashboardPanelActions.openContextMenu(); + await dashboardDrilldownPanelActions.expectExistsManageDrilldownsAction(); + await dashboardDrilldownPanelActions.clickManageDrilldowns(); + await dashboardDrilldownsManage.expectsManageDrilldownsFlyoutOpen(); + + await dashboardDrilldownsManage.deleteDrilldownsByTitles([DRILLDOWN_TO_AREA_CHART_NAME]); + await dashboardDrilldownsManage.closeFlyout(); + + // check that drilldown notification badge is shown + expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(0); + }); - // check that drilldown notification badge is shown - expect(await PageObjects.dashboard.getPanelDrilldownCount()).to.be(0); + it('browser back/forward navigation works after drilldown navigation', async () => { + await PageObjects.dashboard.loadSavedDashboard( + dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME + ); + const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + await brushAreaChart(); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + }); + // check that new time range duration was applied + const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); + expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); + + await navigateWithinDashboard(async () => { + await browser.goBack(); + }); + + expect(await PageObjects.timePicker.getTimeDurationInHours()).to.be( + originalTimeRangeDurationHours + ); + }); }); - it('browser back/forward navigation works after drilldown navigation', async () => { - await PageObjects.dashboard.loadSavedDashboard( - dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME - ); - const originalTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - await brushAreaChart(); - await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); - await navigateWithinDashboard(async () => { - await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + describe('Copy to space', () => { + const destinationSpaceId = 'custom_space'; + before(async () => { + await spaces.create({ + id: destinationSpaceId, + name: 'custom_space', + disabledFeatures: [], + }); + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSavedObjects(); }); - // check that new time range duration was applied - const newTimeRangeDurationHours = await PageObjects.timePicker.getTimeDurationInHours(); - expect(newTimeRangeDurationHours).to.be.lessThan(originalTimeRangeDurationHours); - await navigateWithinDashboard(async () => { - await browser.goBack(); + after(async () => { + await spaces.delete(destinationSpaceId); }); - expect(await PageObjects.timePicker.getTimeDurationInHours()).to.be( - originalTimeRangeDurationHours - ); + it('Dashboards linked by a drilldown are both copied to a space', async () => { + await PageObjects.copySavedObjectsToSpace.openCopyToSpaceFlyoutForObject( + dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME + ); + await PageObjects.copySavedObjectsToSpace.setupForm({ + destinationSpaceId, + }); + await PageObjects.copySavedObjectsToSpace.startCopy(); + + // Wait for successful copy + await testSubjects.waitForDeleted(`cts-summary-indicator-loading-${destinationSpaceId}`); + await testSubjects.existOrFail(`cts-summary-indicator-success-${destinationSpaceId}`); + + const summaryCounts = await PageObjects.copySavedObjectsToSpace.getSummaryCounts(); + + expect(summaryCounts).to.eql({ + success: 5, // 2 dashboards (linked by a drilldown) + 2 visualizations + 1 index pattern + pending: 0, + skipped: 0, + errors: 0, + }); + + await PageObjects.copySavedObjectsToSpace.finishCopy(); + + // Actually use copied dashboards in a new space: + + await PageObjects.common.navigateToApp('dashboard', { + basePath: `/s/${destinationSpaceId}`, + }); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.loadSavedDashboard( + dashboardDrilldownsManage.DASHBOARD_WITH_AREA_CHART_NAME + ); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); + + // brush area chart and drilldown back to pie chat dashboard + await brushAreaChart(); + await dashboardDrilldownPanelActions.expectMultipleActionsMenuOpened(); + + await navigateWithinDashboard(async () => { + await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME); + }); + await pieChart.expectPieSliceCount(10); + }); }); }); diff --git a/x-pack/test/functional/es_archives/reporting/hugedata/data.json.gz b/x-pack/test/functional/es_archives/reporting/hugedata/data.json.gz index c524379640df7e2c11cd0f86603cf4048a826bd3..c616730ff35b64074b036f0bf30b77d0e45b6c75 100644 GIT binary patch delta 32038 zcmV+0KqSA=hXT-t0tg?A2neVLq+hWJ;syjynVid$?grC;Q+oH~2R~L1CmBBe#)bX! z4`?(qfg(N|L~D1HA_yDWZD3(!|u)Dt}j}X2pU)sLQVm)&|(=hLv(1fa|r0I57sQTOEN^*3a(!Vfa_NUE+kil zDRAT%bI6F#a`KzN_43detCt4_DCdJmpR%W4MsJpOm|Eb3b#TJ%aIz7>fVC!AXMM8% zS@wrp`1T4wm3ej*s7OF1Ym*7dLdr|R04M;piC=$zNpDd3D_ClLA0xNX?XLz}r6#DO zHINnVk3a8^hnwE~{YeZ3G-B^AN22j5`k3PqGcYYIl0kd@x-(p_Uu(9Qy(i=HnaF9d z%eYW7#p*D*IxxBIU?S@aCS%ABz!A$@Qc;Opxh?eR$KA2BLXiY2R5`koTqYixC^B5y zUXxsZ1MAf7?zXd1kpv1D5?RF-@Y!4!zXM`G{G z2q#KNRj2My38_+&r6FZQNaWMo zcsGE8kr0|aj#2Vr=XJb!=}PCOuF6X{oJnV^yKqt!jvj8t#tKoWd^9ZvrE zrn3be-)Qb?oT^s^pvs)P4%9e){rGtI{?NOaItwwrW-(x=?xp9GT6!L)nV|+yWj3e- zMKm;Jil0fTCTjXDI=V^S`E*kulU$aNVG$IpVsK~^g?>I#R2Y`j9h!8wLoKk%axyDm z`DlWVHjtb0nNfQlVb~-qQw6PmGIF(o)*$R^Qmr?Kb8pom5mc~ZEW2s98E#-qSP48Taf7tmuV$PAlGC1*}5WumTpim{LaLymy3HAKvg}h3DSw+p`cF zBhA?b1EgY@!3!^<6h3d#-f-W0;Okz+DFu@ub%%;&(9jxV z?9x?(tBk;}!Zl7u^8MSppSmm8-A`)5&jyz)`FOC#C!qw^PPHmvRjOGkR%675q*p)o z)u|u927M~Ws7RMIjH2}^AVthRMQ2kI3KG&(Ty;%ilC>Od0)*!jiNweN{SFCp$R2kDWJ?Op9n6uER-D+~M3RH#e zN{1?VM53WSd7D!ZqMKG_`N5&W5Cd8=!^~_{h;5um7q;Q+6_6^V>l#v?9zlv~f;B!E zyR<$h&ZBM<2EN;W_wLo6Wze!PBeZ)=e7(lzxkAFOZJwiGIay}vm&}_3525v*y zyRYe60n0mx(O`Dmc4e3OPn(-~_KRY@?cDJ(%W&NaEr-m}hZK1CoRE$QO@cwJhE^dL z)zQM339OvCK|6*dOgxK2yjv9JE5TK4i8@?F9~>mhLlY2xP#_OYxWf$B`L1*O<1B*~ z7Y@@adb=y%E?S6{vRV|4BQA>01k47k;I(X%iBwkys6tAr0c9=ym(YqHLrOwKXeE}l z4JCc-UzvDRlI@0%jzSKkPG|rH-wv%BK5lovc8BU$Jtq^ri<$XoLJ;QXo6N&8Y1r4; zdsW66_UhVyE1-wZa?cfH!zII#aC@6D<=c2;jIcSP+!tF$QE+Wwjb`xsa09Fog`3A5Rk0U zA<=d4!mKvx5!qI9u>5k{X~;=FpKw=a94TGM1Ei9yMF=f_g2@nx5t!heu^yL*iG5aEP^emf6_Kb_ zz)VQQ3Ve*1Fb83ZL|nbFMZ`yfB}593WAHh}Amr4=2XKo_LKUD&wMqfX zWu6V3_x~p=%#cKE)d*7ssxscW0#(c*K_)Lx!LiWO5pt7i>Bamyr(7lxu7braRh-Cw z%S0Hm@ku%JKgdoM2hAjbGZmaO9CjJKp>(IuZk6m}Vg zCROK)L5^ULUxPW$X^75OURZ+bP$h%T8$MuSBKRz>p5lKx|$q% zdIT{%v*}}t?wC4Y6}u}HtU-E^FJ@SQix%6VdU0@&1u2syRw?iln&L`9j8!EW$9wvs zi$a?Ww%V;-39dq#wNeht)}Xh47K5kAP}-8xE^Ar^twPteh86`p$pql82kspeqMc@8 zbob(+ats^Ld*{es(Q9bQ<1h0K;tH&7tgw6WPyvcBIwM~UPvAnb;h7q>jRAHq9cmh7 zJy_%&MzLrTBJk&L(W^qJ%>{Ug-w+l-jFN>G)5g@h{?aR%A! z%WBnIslI}XjIIhyWeTP81NO}^8S?2AIHzaQsZF{@-|vp){=5t+7D3A}U`CHQMu>ql zY=M=OH{K)}Qvs;55KxC{h(wI7gHxG@QgIsN^bO&(KlQ#6#1d#4<++`~a!D|G6lRo~ zto)yldbhvbcTTTggwXJR8sbC(PI%RTt0iWtKrf~H!-9NZ;Id!O5#M9{z*V}Tl6!0Ns1<06C>HuDPBK(8WLn~jiX z7s>k;Rh(LYmFt%Z*dTkO4Fh&8N2`d#vjiGBb)HNya50C^C_M9jJn9x>IxOf-jV97U z7BNflmGB4__S~J+&r%E@0>MYbIu{E+h;bD&hlnXSkBOK?b^@b_uGN+2dZ1M>O${`g zDS+fWp!FevbmJ@7CVNwCP0*?srvh5aJSR#O*W4QRuhQoV4Dn;J*b zih0J3b|;OuAx7_iGQ9)Q6P*=q^@?q7TVxEYK~}D2DrA=UD@2_*2 z{U3nSjbf9LNSVP900R%{3xSeUGuz7hSAeTny%e}8ICALOd)XLJO5clv)LXH#(;&3$ zwCearxy)V>wF-K+g4!4nfO$ggFhW>9iJ@uQ7!8)6fHnYs$2jRPrg1hz!i>*n25`#~ zlB8!_r8DY4R^iRmA+wRx3#>K4I_s0r_$=w!cF*SF*m-L4gBV&;Hbf*s83E)yV&UAZ4ZbwcOX?Wu>p>evOum(Zh4W?C@#;#7GDQuR*ru&Gxvzz3pz1B!dFi zINg~nYK4Hj)ZOpZ{KX(`1Q&BW_YaXshVhLNE^xi=-bgSBp#e6=vh*kY8bZM|4TEGT zgyf@8k@t5AbXA&H*eIH;f>&uQxq=rD3`OwH2TzWF?3u$=8wHY8@G5>Db-WNUpbaUQ z1QDd}i&k3)XIcB_U)R!z(eT!@KzxEUED7^_BBR)-Dn3L_{Hs_iYwO`v!6eneD-)CD z?XrPP(@QgIt;YFC*0&=Rk>2V>w=w#JXM?N}L6+L17C=HIx}GQ^a?v9FvwR@4T5~c8FV);wBLX#xx*8$gH;dTwUO2cg(t|8|9E{g%0QeRjLN^k4oRjyqsUY6oT z!YdP4;In{N!_LDI8Yxz~C@27ejNDJ10qrt>Vr;brv=Umy#;Bt;#+^=E4X$Ea)W>IH z2BN3dd*Ide%Vw}9`J0O=4v?I`?Nq89r$UWVa7vzl^4U{dNFk53C$80H zJQB0JOIP1N>moU-m>qfbC>&05P0ES35>ln6ry&I?CXbnjyY*4HTqd3r-X^JBqpn|= zo6)NJ5V;&?d10!LV1$zpnA+uLYJgRLs#gkFXe>GOfCN>}Mrhi#&?I)Ao%&t*tb3_j zztaZYU8eZPB@0~E$-)Uwsh55m$NpH}=~n32AGJQWLAEGBC8!Eh3@TKEtfa?EP!)Q1 z4JzP8&WU`wb-@{sE?Bo4X0HHMp6xZrl-|VQ{>u~ z4IV9`&{G7V389*ms(0p-B+&f%Fx9~ObLWeIEP)o!j4&F^K%L)q#_9a_8cb**5cR#1sQ&8zMgQaoi+R zS_!c7$590=aWNGTIj?nprM02u>o`I*be-GVWD!ix;Ia>7mCJ-gG)>w?p&DG}?n{9y z+L#QtBJ%;y9E7~7xLmio|C-_2_paKr2udWBIdcOs^Dn&-;%u>TW+P``0jyG2uYwiO zK;R|^n>ji7%yy9Kj^13ry50BZ^^+9iCvq;34dCkcitic*E~o~7SF!U_;TmMBX@={5 zcj$g|>LP`jC=4EXOo!u0ZdS*en(K{m$ZFIGgX zlvk`G<`a0x*=G;lMxiNCf^4H0vJPaWLFBrU4O3I=KP`P$=UKsHa*?PjUp8>rF8e6O zh(*#A0-dpEW^a#!fYxuF*9@r{i zm8zBo7LpwXbSWY_f@=X)y910>peiJ%x?Z`&MGkPmnk+?sKcZS~!%XkXTX9NFP;0~) zhiKL4oG;{AYUlh&iJJcvq`1`xU`o82L(0#>QNQaeyB z@R~7jXt;4C3T<4?u795^43L##rIisgW^}>xN(>vawC177==24u8vUomXsHdT#v(*A z!^mmn5K!2n)oj+BWbK|!CJ{8|K#m>2^C=K^^Fr=_yz;iPB=H^`myul|r2uG8%dojrGEkC~>R7R$i+DT*Yong^N`s8Aq>TiY^M-88JNE z%5PPFfUDSXX>gh7h?imrfFVT)g6C?&Zg;)M_E`jl$$wD-TY}}AnPL^N$}CX_ ziyVBU_%LP)r+t(TiPO2^6FTxl}YM)c$n4?;RS>LTIdi6jH*Fr=6TgF6()zhfREYCBVwO`zl}q z-MXD*?Oh6+g;3Z7kkfJ$_HY+7k*y_r5XU(;Y5ikaN5JbUTtM-3$j(Iq%LNpM#}LJf z1J&Vj)!-^yC9J~54nJ~A-e!!(^F@oL|B~XKO_mA&eCRz6MG|PPes%X!xk*T_^%-;)$ZYDe|sp;I~3{FMF@?`N4`EvG}WSaG5Mv#YE>26 z{pGwrb%u*YQ1i?5h;5eYGggn?4<=6{Xo@w)`ek-nkB^6A?=;Ot2o10? zW;bm;zzX$i4X{CWp>wFXt8iwfEKb~iX=i4*itne3blm^og;x=!^;?_F1@A@};o0d^ zDLA#^h8(yTJ_&V1C*}b+!>fGVcR5_zX7qt>UDScAtk|;#*C1tTbAjtGoukA_2JK-S zXWqOHT;;~7!8OLLxvhp)Szx`YT*GXYXGwn@z!ZPIiWcx@HxDUCV)rR7+vBl+U2?t- zTIH6gpoP4tEVyiNQZr$zn|Rk~vuCj!uJUJ5Q!ZXm=765WKuRXG1wxzqew=CEJ#y~`=uJ>LClHfX2-XKw`WIbt^4}xQFH-TRRsnR!4LCQG~fYI^t_#gy-H?aw?!b(7u z`YH{m)d#T2?&aB2b)WrZ5w2r}JjiM@`F78y7?*10nw_6{^{#ivA4zZxD&YPizU^*p z&RYJmz=hI|wdW%tOJXM}uJY?H9Em7prM!0S*M(xyx#!9|GCOR2pe zRtT#bpsNH`S#nQ-YHFN+3BeGr%H-djMPcMu;_I8F_ikR_^=>noWzYh_O#Cx}&p`3B zu*kK^(px50@46GKcN$+9sK01`+dE>u)i*{CpixW}`rorqZ&@HvKx305&X7zr!d|f^ zW0+V8sWPUZBgH33V@M&v^a?IZf#Rv@O_)Lrs0xiwgNigNL$nEh6R*CI9=~SQn%hI~ zY7I%Cq+fWSrjH?zUPUY2)D#ozn+#CYfT~cbRH(djHiQ_!^2IRW*t^*OxCu?Tt+8Xe zj3?acE2ak_3bP3wyB|cEWVi|!AHT8`G*f`X{k76eK)b~tb>J#iuIq43F|*Gx^CKwq zJti??-Xy(O2dhGV(x9qV_KZp#YZ`cQ!-(R!tzmCx_mb3y|r4Qo0 z3fCZI!CAMuk58}&T1Ys=uIb5bz3blI&LU`vHO4^?TMMj0Gt|NIF#|hz?}11cEL&=& zj;n2=op+DrSQRoY5v?@w+c>Q9IVK=Ea)A1WuDaBCG^ep<7p>0tl85dO$WDLn3G|;24`_CSUb)_e|fI1%b_YU$$@e!{lHX6<~vOZ2+4 zB9;!;nqWz%lAu=2W>3puD(g+s5t# zV&&#osA01dDq_Y*zH7y6G_!EULqe>&j$IHtygK&>jD)z37oRpx(Si4viJPLZ39E@w zegtfP02N~?G!-MgBDi9N7|5$VD@5Gpg{rd7ok_i_^tu-+*GjLu4KZ5) zw3zLb0(=%++bGwY?lWI3g2pE(ns$)_>KLqzLeFiR^^mR>ScR&!0@fI7%O}k`yg7E~ z^(=!DRXcRa@mVVe&RW3^+g!AV39jzVGe@m|d4?A{MVo_-K6)=q381)DW|Q3KF3R07+cSP~t(Z zN85zKVF=Q7Ip3swIAPVl*ZZjUdLw(P43Qw?ei32EyKNT|)&Q%}2z9UmamxTi7$|-Y zW|>eHcad&mgb(}jA-Y1vA`vuy$}q+iQCADBLd9AGYlzK}6IhQUq~0I(QpG5MO{Vzk zp}&)785FpZ@zzCNVQ%RkY2$4>rd0*5LiJjKYmlBA4tGxbV~wM$Wf9LTgqj2d%mIj+ zI$~c!>Y>T`AzKfxGILzTYnbIBr^Anj-jh(X4C9B?IK$O>7-|whHN_EsCJQF4Dy&N^ zgi76(I0jOdk?Kt#PU?H;tPL*Nl#$c%!kP#1V8s^fp$bw(+(Admx=cjN2MVOKvDOJe zPm>{n$^6PkR0^$68BZCd3})*9RcL$~P{S-P7#*k2os@8Ys2QcIP|hhNRBACN1|n0u zHp|C^E-cBXxU*mzI@P;>UcUlW&OCsR;0^LTurS%$#7kYEs!*dS21~7d4dv8pz*Xw6 zR^S?@s!F0BE8HwB^;$>u!lQRS5m3CKjrWcTS(#Q>D=NWNYK$sez{z*Rw`jeMJnFQ7 zOWd5lkulbQtJD}(xDe2Ko&XLha$$O@UA9$HSr=ViMmtHG0^7<}^^Xj-c<0MR`!e3~tuCU}0jtVQ?suQaf1%-o8FiGspRA>wjOz&9y*T^!b~cZb782rb(2 zka7szKr@vK7l1XHKY6mmW9MPHix3K4gj7rv@}M^nFbG6T^7C7fhZ=wt^9LQUp^`OK z04sN3I$%R=Jo<=N=PrjQDKsAjY?DO3ocGS>rG_y98Y&v909UawYH*Ema>iDJtC&mZ;{=vo zM#w}XoB;%3k4TeR8N+qAyLsq*cx{$JbM<44LQQbJ>W$Sa)oTs0_|;56z3rW=XA#u+ zXOug2Bp>47QX;!Zxv`bemfEf979mW)hKnVB6 z+U2?KZhB`0NdiT;$o|klc7Y-w%tG8F78y4(#Lhc1R-oc7)j%p`jSohsFuEp%{z<8N z6L^(>y^pbY+10_RP@%q#DPWSb`;g|Hdacel8zPa>gaHD$WdPlz$!)g1?)zzG8Lor% z<$cX1ac@FLaYE~t-q-zR85Fi)yyMk0#=40u2ulE^(Vk6`Gj-r9RW2Pa@Co@AFc>7| z5-!(?xvWhJJtXR5?@Z|=gTfl3C06BaoHj0h8{r^`YtnQ&dHK6?=3eOJ@3ffEWCzxH zJ}nk_u3#Aywpwk+g7z*?vj~chcOe6LEg;QuE(rZ@t!^l@pcNwMLLbjEX!K#2S$vZ8 zwt~|t)~#F3YYj3Ld~sUk`S)V4rKR6VQG7<^0gPZF3jCo7Ic2aOdav?f2^6SOc;Y$HQ&+KzbHJA@IR+<542NMWGKa`)F*lG1p3X724q%UO-U}nv{rhA@Nfo;E@tq z+t^|qzzWTA4KQ+E8H4jahnT#yRlnJPPLYQJvNaESwq}&_JsafXuKP|c7C{4RjHv=M zgH-{oGG{-@pb;)e2}7Q)sIW;s?{VCut*s7RWyzW~xI6`k8Tfz^dwXF7qsc*1tcF&x zDe4REh|EJm$cAaXFniu4Z+bDs-c$N4!szu2s0CHA73v$wh|yEf+$qY@>eH^|pO2r-{fxOM1Evwou*NEPao zhLj_CfU`5!8w`(FhhFf1Q{qmoZQj1#y)$ZkB6-5dXHPL8imQBDT}mM_d*@>w zHImL%FN-`^4%z!8K!weE-XHsS89nMoY-WWso7q#kqy>=2o0c(&G?pO<~!fcfe);NiOwfj8OzHgBSp>-0!VG#p%ufr>t2w!TXXM2 z%oIW;sEWM%3RHtQ_gSl|9KJ7AthK}UBk5M}^)r(Q8dwf(=H(#htq)eXP~PhBeI2k0 z9sCMdW02C>6wBAxmD-_yvcnE}4O9TfOH@J-ctevi7ghqQh>z+(5hp-rv&q>3N7Bpi zxz%#SDoB-@pSBh;8}7XdU}BCus+9TKJBe@&EbtRtDPwhIa8{7Kl*8J}2s_tLb+AnGJ^*m2!?OW@Qs;JR)zmjSSZstD zh=y87T%3qP_jZ%voSBN&h@VTjsD1@&kY!@F8eGM?rNiZ{OM#rd57`on5Ssd$%=gaf z)j!bDc1B{%JV_CJ2-yp>s^Uc978%kiNR_^Z3Q{)slyh(pv*8iUWr)+N$-N3vr530m zWgU_CNZ))8#z_NzC%(z1TSrjUm=-Up(%-4G;)5I+V5A;zIs^51a~-FQKKo3bDsaj@ zN(;oAtpeHgUre{t7t;mq3*|bF&k?c}*1n62Q#P4ks)ANo-D?G{LG}RK39R0m!*<%* zFvBNU>Gk)SAPWLnXtLMBRRXH4`AP?B47Wact8%wq=&KZew{D^#MwK&1Ho!BgOGnkL zNu~l&WxWh4Pyu5^@>HH;W^aXBm*~+q>13z?RG~lDfZ`TD&zq66C!nIRtf<*6Y6YMQ zo%vOu073+7LzwQI7-5qPq|~h8e|OWHrB5O#Q8KR!OM%`zQY?ySgkoTm1*5Y=?_A(w z2^0hT@`T}k^DfhFvZJU5RHYWE-m@B`Y&tvj-nEEHgs+6A-`wtd4|7d2T!)L8f&~}2 zFql&EUMSu)sYbYJaFzOZ6)uxmS0aIh*0FX835IqK{Pgj)}V< zh2p%#9X9guHP9-zMHQ_vCQL4H4S<#cRwCwrfv|Fa+4@gQrXM51hP<=_SjB!z1B;)+ zY(fYLY!cSUxMt(cyPNK7WLN|(gof;LWFjAZ%)%0xHbuc1tO1S$>{gBhjC2fUS7Gyg z>5Hf}Kn`*o4J$!aO5`=DQXq~MoF(N7$x63iB>%n8p)k>f9N_5{wRil$n9*-Wa2AuNY&yHq^qZu->d<1u0nw znf{xy^I4cwlE~*a1?L)A6^1lARuuhBTfvA|ao$SEhqkLtkfgW0nF7l&KL32uts2)R z%GFChH!;q(dzQ6(fWtCeM+*bFuWYRK#7C_YoR@fsa2qyS1+HTLpuq*n=D^cS+zmp1 zu73G0iVN?ziE`*0>91VcSs{~%MUZA;Mt>gG!Veq)sIU zSp#A#LRLY^zJg<47rxs4T<9c&reI@#EE2QT;3`+I@s-O}L#vppDrk{j0Uu=|c2ABA zrIv@#E+bE(b}t~Z48tqe<$3JhxO`OFaV-zoljDkLB2m_*&uVj96>C9N?BlOO1$^B& zM8rTef_|~Y3XOPNb(6~`ENS z?UR0upk4}GffOr;Y?ATbfYdYDrXLro!Bt$@(BR6E>(w49%=RWrwdyG4yEa)kT?ejW z4xzwB4BjEcawQUX_P8h6;sr!SOM~`QV0-vG!cbz2AU*QvA5!<0}zO-U?H*6OtDX(D9AY>-)v)> zZPZ@@R=lMxn(34%nIchoAv@oM$v-))0rEDF>I#T4ZZcpsxXPWDz5>D_1v~`XG?O9J z%9@-Xx7FY(H$@GuWFQ8Aa_|%=IxtCS;%(B|^<<2_PxehRd_BfEK$1&uA+e4)JOVwA z^sDr|LhMGi`0lQ|DZaZKKLIg=bsV69P~8M zd%#tLt26{QNh!oo`W~l;Kp_^ZUYozhAhwi5MptsKRrr`kDqIWR~y-Dr3X$7cC z^-2w>KPRTk3PzKEjm#5Lf2rZ&3$gl(>fs^AWQmamp@z}qA^~S>dZ$fQ+AzlG z$^=vIodF|T4ju34K3oK$+{SPKyvS4*>2h<=kNr7SMM^A(&}peq4O9)Q0aYP$(4iWGW0Ajlc=@ukb7u(@s8S#c48+|X1_0rZ zX0u+Jhu-U|A2gk^ynopSvO!CT3DJ#sn{^|cx_5ZbGH8s!S(_u;6nOqgXwnk5Rc{1W zf3Vc~=@Bk}9x_m!Z;3r98z>>8p{Fl};DP%|mv5hK#Xui!y5pp+>Kr7`?k1or%pDfx ztR@k@h6Eo8+IjmqOK=q_Fd;Ho_tATZ(qSwRM=G|M5qNn0y7Q6;mOyb;oMBJ&08-$` z;{r>lB%-iIL{kN+qKIY%DRw-`IN}N^a;<9tm3Xv&Xp1aN6{yO*`ifFzOa93bQwTA6 zAxv&_Ywcoxoon-x2-m^#2Fdg&>Z9n!2ceN6x4M*42dkn;c?Bz%a<<$p8bTtPzGR=V z$yoI4tvZi0u>{wjz?can2h6G{3>3zhtQaY8k$b)S5CD61gL>?ALiYm$m>goVhs8K3V!*tP-oZU-YC%wYu zeI$RLF;TR`%j=d|hIUF-0jpBATEPkunIBgrjHf{QSy*kOSI6!b2WA;GF6u3~K+z3< z{gb@(1h!hfRtc^`-O}MQF%xxn6zaG@K1xA8|uJSs$4o; zgT&Dn?dqPSArZb7zf}ROV%5^X8iGK7Oi*>FS_;py8J#Dcay$ZHg}rN`Np@z&srT&Q zB*Jy9JVs6`J1!cA;GEEh)hwlaf8L)uJ1Z7J2@fGg<9xuFrk!g-FIKb6Llv;fG;0Mc z>-c5`xpWE;t#QIos?-azi5Z?>z3q$@NuZ>YokyLVl*zDU>Q$75Vm6tTNc>t@nU7!?L8) zYQ|)6qX_Z;Hvc(MgEZiXJ(P?b3`(6emVorcOTWJ#+Uq~p> zDsjYQll__9k4S4kd9F#2dy2e&&o&9&eYwdf&NSUzVRF83x>?^cFvcBZtOQk|@u^Ua zvAlLxs{YG9lwBXryPREOALZ%w%fhzIPp3|{9-6NKR^imV29`+_EE8Gq6#W@1XcYu0 z0NiHN2MJnZuVJBUpQTW}))4C}p46+xb-8j4)33`7vOE(jLRRYR*B~2zq_FBPkoDfK zHpy@mE}O{VNAl|?x)g0*<{N@-LgO!ftoy?9Nd_hR&KDMJ!r(9jX{r^22AOYydVrNuuNA<5oDZIySN44B$_q2(P3Cs69$uyPxYo}$hDFW_R%6_-RKN6j z!;zM*Re`Hiy;k5FNxS+lAyv0(#%Op*+Qo#1Q_f4)C8lvUN=+OqL_npq>!?IP90LWD zA!M>iX7@q})Pi??C1^Q}&J$3G=s5GW1glk(NF}sN10WSG?|ck@&gJYq67P~4kXlWs zu&4t>*ZMgyXKirFri@^@0b&7Hlj`dQt=^UDS%&Lq0RZnnOg?5(GGY-+N8e<{-y8&$ z2iK($kP=%1r%oLrpRA=ITe#gW)?haZe|q0*(xCze7F;Bp08$h-m}8T+8mt6WDKFKa zB4=eIcn%e_6+&u%u?KLQ!hQv$N}bhJq|lHuAuo3b23=kfQj6jfS0;) zdL(14fL5_Fs%Q%4_b~n>!q=f%-FfMC zoUHXR#EF_3u`}1ZR>uTL%>EXrA6gyaoPetZRiXKXw@&Cr85)@VzN0& zizLBG58)*YuO@-oDAtPlOuA-GQSR~qz z=?&zSe@l0j#Ac&y5;RrdD%C9&u9)~uA#un+|^gxD?tUk9#2+MvM&;63jX;j@{} zL@Qms5?7UOL*T2xRrnsNaK)H>_7Uk~)MUv5F#Cb+JISOvGQ7huPThc1r)P<{ot*zX^d zGr%UA99$*<&;z1$fmF(LRd@VPa1HQG9u!)EkjNO51Iy)WBe*ariMrJ@d8oiDie3Ie zZ8aTeL)k3r051QaJ23APc*vRfKDqvXD6GzHa>aa(iz?ijE)9#+Thks~Ox*Y8ymwyO zO4%YPn$)YiW~s$RBC164Idn`Bgwi!M8%FG}QVLMuz=x2%C00)_q7?$|CQaWHGe3AW*-10?EKz~Yi3#LU0s{-h;X?GiUtGK1x4sl5=NH=hdBp`G8V0#~8m zT7fGhK7be_Cd}di#AfFl>Y!D~wbqo&22v~vcA_(YAULijjW!=_u{()a%Q_Q(pn!+d zH6{|77pk&N7RzHjz$%!d2G~e{;dC9qDp;fn*bvvCK3HV;s>VvfIdQ`-@<^voY?(`g z6vtS$X)1X$LZ$J|+6&1o61;%l=Lp#fNgMHv+@_Dd4q~Myxq{dj19&rDZ+Gvz!$mTv z!BFO@VV=C6wi;Z;%C!R5 zFvp@MQTxY@hZR;2WQPzeq(m%2pb(NZZWK0-P=;Np!e(AN!q+)-;OJ3GdbR6LtpivE zgVX>^&Rd&+96!+WD3PvzCYc=f-S2lVcb!KzSOPUAF~a^Wn2whn5mO(*lkaJO3 z4Xz?Gu?p81M~0u!>P)?68Lp#+Xfg0IGn)+0t1sIm#I+GyFt18~a22|H9j;LtmL|A5 zZy=_xq*!5$+`Y?MMQ%Q2fuXkQ?5Y7&;e)6E1;|sMkvHi&VqVhW0*P^L!cPykz3C}Q zpgn|R^mN%uP?cGq4%HZCMYg`%n_mu{J1b`yG|E6E)MJhjVlZau)FsBSi3Q#gJiTpj zl0k7;KF8!tplAnwLcl0EtX9`JE5TJpuvXw2q(h0(djIm!87q?DI#SLV?oFC{oGmWZ zs?8=rTM4K#=dJ>^K84X_w-o!U{>NQ+E5}Ml$!K9Mw8|`S1+8&n=Tn_*X&G9lwS4Iy zZS!^ltn-d3mOy<6M>*fag7#+bEP`r>BP`@H6IK3kz9G+fwehg19 zRSO)YnYji~g`QmlYLF)8$^2ecSV&oDelOLHlr94WHb5b?v*3j`ttK69c)_YWW6%f# z;RA*jmk~}Wpj$F`Z&I@;rw&|&&)^Eb8iX{sYH*e1vDV;<){|awJ8p{3rX=)wOD?{8 zD(iza?v9;*&lh|ULn9E8_rqH5k#_{3G+N!%CToCJ!7O#qoSzal#@n1c#^*(fo8%xS zVSn8BzA5ri4i&ZF(WmTrD1&I&0<~7dE0eVQ+X_{Ta&p09sIgFpMDG(ZR-ze8MQd`l z=E>|YQR*CiSD%G$xyoShs63$+%JX2rlz?4iq@AwSfCPV6C zJT9xSw$%c&DoB-@p@tMpEl7@OYjjbtL6n&OMo(jfrAftFr5Ipv4hSa;iQ-5j*dIW{ zN3r)Rt`)GnLCfoj1FwxuHVWmkh9P(FbzCc8ISR9Z8*SXQkba3(Y|^zgTVemi^GhY# z29qIwhKwe7ii0QwT6!$g#JgiHuu82^OHD++m}`>Fykj>y;oO$EdSn|be096)-8(tS zpi-<63P7$JT7`M96}0Fb^u}jT;SL&j=6T^QZhY@fH+Q|Uy1CP0!(kTnQxx0Vy(@fl zkk!Ms4o-!>O5^R3yg6W?SCI56`sHLWHt|${6Hv$gJ@Jzanx_g8bBHlH3;;n$(6_oP zQVFg?XQji%2myFR3Zt!aPFVNT>V_MOdg{CcVwOSCE{dLQj%ZWxCVL?%-)87yhO7Ig zpIHVqQ98ur<^`_)u1h=S<~+Hqgqa{qCJS?7v5DiFusZcVA2o@fC>F)eka_z9vr*}P zfPAxw;3`-Z`uG*B5`fFRRfL><@&b8p!bqPKYXr^G;2LAF=PN;Ly0a2eG%f=Di?r{w zsU?nJiYi>3)v{!fDOzDFA8eCBs41aUA?hj2XK7*2C@q4v5>%yDs6jPGr^sxA-Omfp zGJGWoni|JL3R5)t`H&38XNsf2IUitunSr-W7GppytP1O^E2ma41ZN#WN?CdevDIGr zI#?AucMU7cBl+ODFC>d2`K|5%*1@XSW2x(p#&B4dm+<)JeAm5u@*>9gnTJ`!Dgaih zVH#Kz>Vyy??^}r&aLFHhvu#nQQ+bl6^g&dYt^|iXsXm69|YJ&5IhP?f%i zDpU>h~nd?az9u-3y=>Iw6 zhr-Sy6jyt1L$JW z+l1Q}P1yVl+iiuKSSe(;T2TxlL~dO+oMJY@&KhwQ(nh99KilqRNit}ez|6<(z-KTf z*kv)l-P>3NtWwp|!5Sp4p5f~MK&o|o6N1SYvIB4y)AJVOZexZIuio~5wn7p?0V^4A zUF4ogOaBC+rKZ{D_$pu(DwYmbh>p9vGe+aV<1^>aw=u%*Z=n_<4&trY1n&>Goh^_g z(9&6i6WJYK>OmtU;(fDv7|D6KyYH-4vk>EZch3Kv569j!DJ+5p);PK@)P zRPVZHvt}6-xJFqYH5p-@-QIjRiG-O4=Yd&(-Y5fzU;i=VU|I$L9{7nZ*wFn zFKv$(@6BwHf2f33;hU(VWnzwy9Pnz498u`=7SpBMJc-BMO^q3X!Z&f2LJfTeSl9*? z0V{U%D}dRY9as2&6JQ1}v~4vxbdDbp>&_!CVtftpbv?|A-ToS8BgNRWw4LV*K8SG@ zG`_nTj1R;vC?X2NX{gv_#^6cOYUC}QRa9GDw1sgA?i4Ffptw85-8}>d?rw$R!JXpn zZo%D(yBCK7#l1KbDDBOE?|sVa85zmmYtA*ldC;dG9O+T>U=U%`?iwQo(N~7vnQQpQ z+0b|DT44FO6J+G%6>s$y;&5`!y_MP-svxqcv{wibm$FiN$C8=oS{!7M3TxF~_TRTz z^C)xia`p%`@b1s>hWr!9HiUB$jCB}E1n?%xDUDgBj+Xw<{Vk{;Yz<6;nkneR*X|{1 zNluk~{2R_RikA46eDSdn-Za)DrMVQbbU@8M*)vgvR1AIvB6}w<6p@f9R=JM*>7sNo zMp78TQFWTx`!CP?I`wX?^OtgEpBaJ?MfgMT`J(gkRr`O0U* zk47_;U6>{YUsD*r(9E;g^ycKFXU)X+;a!W$KkDVH^^{9g>Nh`8IWmigx4p4nSFd*< zVsn2gKX|CzXcs9bpGmyf@U&lg6Ay0qk>4s3$6c7-Kg)-hJ_ycsPQL}<-Dz2vjc~)6 zV*dJWA0-Vgq&xZOU3qJ+rD)Gty(!(0Zbzav328qaT@9g{(dJrpdk?-r*)L^j3@z&K z_E@xnVmOij8Qz1CJ;%OMh`OJ;ud#uGeq4^Fo^@WJV$*O_bU>=uuY$w$KOU#3R(B0OpY~sSE9}0{N7!9 zlYA)d0;ouOjQ@19!13m*RO_!%LG&WQ`r#?JkQ-vwXWw9jD2Ktr!ko6B%RK|Pyj6w7t`GTDm|84rEoEe+=oN~)bz*FF- z!4;%Ky6A=KowSVJ?845DoUpkvFSO!@yCtacF0=Jswb6)}waSCOzLz-;7aiU6jCV2s z&bf3atR$l;Rj<#JlPVN=! zaLrG3nKe{OcEL}Lw?YA@h{rfnD*fliB=&1P0w|c}mRDD0ST@hodY#8Pr^EYyqX*Tt z;U^TRi(Eb=a2bKhJ&HZ5rvHh=4%ZoCf`m3hL{n{7Dd?j0hklq(e$EC1-NmRN+|R!g z2|DXC!!esP6L!#`i@yTCv?!p^AE2Y)>*0%zTB3Ix%SLVzn!sczD8MLmD#G%4)VXb< zc|%B%-5`Bkp+Mjq+wAiSuY|A<7c(3WK=rE-Jz$28xl* z8pSpL0ROZAKvwrUs4O2wL?nEPC!&5Q?26ShVV9#{cl73D>x8I;h4388_{+_J$6Q zR1wEmu?MB?AaN+esc|#9nhj4+rJc~Eg5=v0SH;~h9%R5cdPM>c`ku+W-WZY$!w;HuLF*_?m()~&o2AiN zR%~-w>HB3yuCol^o5y`>P0@IE?Hsiz{_{PaV5_@f&0y`RpBlCm(OM~Xo~sX~`XA=& z&Y`gpJmd= zhX#L4JxoJrX5;fmg%i`N*T^hGI~th8*ZRa=%R;A)Gbv=(PM8Kqe@}lN=3g%Odl@VY z`VjAiJx@^?YBgY(g6kN^g!*CwpALd16S$+ z(=dRScMo|xiIjND zL+AT`SXzD5k*c1rs8JBY9MG39GM!Ys)?TX{s(rn6p=5HH_a*t{Vk~);p9S77Cp!Jn ziw8M{_6C+v$Tg)kFn>N{2HyXsKR3UIM|`1f@ewe5@BT~NHV}9moFvbRKX(gK_NBjA zxm860bC`M-po*RQMEs->oD$s@qyO-u8orV3OUW14Apaczm@6lYR0{!sg3Jt-%1AXy4x@Gxb#KeOEK*u_}!~+gD$_~9(Uegib$GDB>94|O>l8$5`ClOJ< z_{n&lnLOL64Yo9^YS${fHSN?QO~Qzf%*1KlqfK%W-hh!)YjfoiBQ+RkB?r$CclL7X zOXbui?#<85dG8%sc8UGGT5(p_!emMl|E6#kCc3(uZaVzbz1a5b@#ifC-eV_kPV%F7 zkIP_fPLANrlk`QbkqQWN5I^q+3Ir)O5oW7?ZFKo}%lmijb>jd=?dtu1pG{O7Mv{56 zMar?s7Y>$|L*kbPMKsku$fzK%opnEot@EpAS&+EGroa*+6f~sM4@UuVr4UL!PcU1* zp|0ayJ4%8gf_6W2qL5zkHE`kAD#(l>VG+P@GLTgxf{Nx$5FHbDL)PiKKhg&-n-?dM z$^J)(f%e-2nm2trwjQ>*25A|INyqKh9o@b9W7RTH*61KZD8C^dHg~W3u_Tcl=3`Yk z1uQaRRyD=|u5fukbAJzyWH${t&kXO8`N%n@>tQ zO+p9%7m?VD8HkD=JWsy*die0f5*=ganp|g=>{rvoRpmxgr%6I`sW6#osqwXgCCg;E zngLzsud8V+>YT`^6AQ{rX%@YpgPmzIc}}@9I&$yEs#broH4-qVsY`FEE?Yq-{7+I=}y4)vFCp@h_=I*qVzw@a_f`F<-&&W_lC1>%XtZwS!tl(E-ru6uE zCT=(N4~eUC=g7kPH|=fH95TEdG4Wu@Qvib`lAX9|O2{4oxDOj{wSF~^ge<>`A>s_N zk;Cpqm(|1(aaGy{N>zu5oPR$l?+#{ISj8LV(n>iLZCFh-Z5Fh zT9FhA5-afY68t%2x_Z__Ql;1$01rwKR2I{?@wf8eFZ}?vRC8V5l+oXMSH4%WwCHux zBa$uZJ=v^Cb{zWl-+g&=OrL5MSO+~Tp;N=F16(GBaBf+v04%nKSIt8rC|VU^N^Jf~ zTH~O-#E#>bI#B}vJrqD}mlC_q4P5zXcHZPLSZh6UcD0_iEspr9AOV;A2@U{>ZENyA zclj{3-7TOTY#-z3{+X%s!m$)in0>KS)fv}S@b~_&fN-kFCJ@ZFE5Wq5ATgWq{o=i& z->eXF!xtOy+6}m+# zBtK}6#*b1`OsC+zm$J4B_I1E$VvyIMb|vVRc? zT@Fwl|4%izH)QF=%7Z5AF|g#=1su4#M1MRv-&)}DE|XcDt|3G5AoE(gWL8ywmLsqufwYQoNVPt}?d9B7@5`Khdaxg~2S%IM z7x?6BIqfgAQ0v-4n6(xpZ)e9;WK#WPk!$VUGtCCi8s&b1qgG0Y1ye$m_{quvT;4X# z4SlWaFTujDMBhSLTyKbRXMZORxxExOwKA!_ysqo_=B zY|VlTz76soq47-*A*&Wmzv$H<+>tJs4}J4>ZB{nsChsqD(@>2EWLP2UJSa)`r4v3` zFkD~?w4sOyPrk<&6w^qUtt&U7KS2&PlOXQm6Yg{L9ZJG#nH zLcBY~{Ajq)_C{`!7P#5{QQtd2+6s^u6r z2%x9KA7g25DsX7&PoWb0xx;vlTHhO-asyxq@2xj7({-}rlsjD2=|M^qH%{Li@bGVM z{evZHcxyzoODC$$C1Ua7<2w4)$}`Rx&nFpReU3z0q7FvQSi}sX+ikLU z>LNn5+^d5}w9PVkdZT_c5ryBjdIvbCjF8aT#s91TwF5)mUo*T8(>MKuX7G1F_?PPN zibsh0X{(5ElhjL%P{M0uutGGlmKPbCWSn(y7 zt2JjiDXy-%P(|oMlB>}9>2f+rCKzHnRMNT_2pzPbcTVB0N8#Bc+>DUwG@g-#f#f-E ziRsVJm~B%Ix{@^-?VnrZs%Zy)|D81yWfixqQL$6F!cI+ib1;s|BMvtdPmNrOze6_W zNT-Cpfl=H;OQ1D)sNV)6hU;ByF#nNjr$N@mpQsc@rpnsqhDNa3UIDZ9QD(GZOGH!a z4rZk9vOazKD9Ck^)Zgu1*f(h4H4%0q6i@WKZ3KbkBj2#gL2ZQTjRS8W=8-9T z5LW-bC+$^#S=9JgU~d|6LHovnK1Q8ZdD)Pxiy2_RfHx`97`dF@Sm;NTJuLY1BY-Ig zzU!xxml(>uX&B@inD;w>C|fp_Jv=ZQ1sn~P{=EGzhYP>rNs3Do{I5YZ;@SUKuc2G| zZjIY-BC2t7Nd32j&;uuS*JZTY*qiSb=aTH{o3Kib%@ROnEWGz6@%8sN`aIiK<(NYX z>Mr2WIR|?pAirj!rKTnVcr#U5j7%C`d;8lO8$QgVRu!@>D|hbvqn0bc{HxNKETMGj4xaa?|)y&HkgL=9~y~No^%V|M>?cSDSE9_uo}4H zQYakowvt`Y==4SM0- z07e;{K0v-~xkRYY+FtGAYKx{gKe*WDTWKite3S#=;p>~rRlQf-yD^0JeNco{C5AF$ z;D3BaxHWlD|2v}eN$ifHhd$iaQ@jZuT$WxrI6}4E86=~I=;LD+PpH(|#Kv#|S{j3I zIoFK!qbN9Oo7TZHYrDVQ`*TG6!7FW^Sy`~Y5E+srkIywC;ieunm$$f6Y7T^*)RV2n zU>ObcrZ9BIZ|Tw@K-&r!RlZP3IVzjaX%>%`;@dGpVqA3Ee;gXPD)V38cSz-S;0oz zbqMzw406Nw#$xu;5<>}+p`+3?1$9o`w`3Ka^$eNf?=pkFW-K#Rd zYC!kd9aDf;3HeiQ<&z@uN0wI@I!2ndE% zcz%*GH$;*C=`n>D!Wl|45@~o->)sIamJT4!GTU=YOJpr&J+RH5slmhnHbs19mUdnD z{a#BCbV}xh!M-CGI*3yrUHl&pe)#mx;n(mVd>%Z@_+___RXpfN9;2-M;1b)tZK|4;h=aLm^u({atw72S;KRaUH=f{gd{VlpOvP zFa0YlYwB+QqwF)MAHb`-QemyHB+GOFG$)4av@o$B^Jjvmr;USK5lt66kP_b(8dRK# zg%pJw?43=p1pU!Z0mZ+|LDkcSRT?yjh7~S(dhW$IbB^D(=$17QtD00ozY?~S2u+E+ zr$z>!rKX^*F!)4}Df~CnG8soEY16&(AP}|Av_)dZ6I@}?_#-4g6s@hoT{8M%l}>Jp z5iyZ3Sev^ghiB_LiqDE&m;_HO-|88~s6?h@e_Ef1h72ovRoEEFM zexVVm+E{Wjc%R&+XQ_>$YMGe)UVZEQ?(8D$W0%A*0COj7+DjioNZ8KeVgcDg?NMF$4r?S63q#a;WH5fwx2o@Si#?cyY@D zwzR6fb98i(D*;StfMtnSXV`##w!l}3kMk4BhM*(SG4bq%4g7+s8}6&h1bm76%_(rD z`2E-YU1ihU+!qLmtQn$pbCYC%6v_s=A{|gliNQohlt0h z(loftUy^Si2~9dYoH#s8a?3(Y0ecjtOy`k^Dj6H#&jT9n(S$!@vITT)`kzCkp4ga} zwZL?q6znO+!2}zTjyc?aN_F`7CkgG-0%=oF2Pm%r`H+4KWht%(qqgdQFL^`_T2lS6 zQmiO7dqLt16_*9+*LgqmmC!aghs4E49|=C2s_C zJb#JZcL+dQa%Sj!wHF7XN^6T9kf?724F_tHS!2~YGW9ePhpLMyD!&-eKb@KCT8FWK zu(+uNS%Al(?ySKGx)Ra@s!BcObH~4@d8dgHXIO2IjWZR)&P|2ndjKW-m;M8Qbl(fUV;^|wk~<)5U+3FAp`i}3e0V_^8|ux<6?>ogK$N6pkeMw z<^Khik&&q8X4c(sYBgFUckC}-cOSN@a+^uaWod19v656HnKU?0np%`}z!n|oB%;8M zYJ;P-gfq=$N&E}zh-!~rYuIvg%Bt`fha&l1vM76KF2n1QBO7$m(G|JI*1Tz$1mfQx zB!-ri3B;12v%9jnuB7~m=!_IUEaTJLJLQF**758eE4u(&0PbHf>mb&~{1>D~uCCH3 z;PlPcpq0u-s>RpCb<*shf!qG&;v#Yk>%1DBd*E^q?DY`@DmvvB(y4MTldaG7aNXY~! z_YdfokZR}Y3g9;c!~PB%Dt6VZ8+Y4@$9!Ff*@wQ3L>&m0x5hoAv@eW@G{d>4a)FL$ z9Bt-bi8~pI5i`I8zdP>#s6K+GUzSC!Kb>-w9EaIWj|=GH?Ikzy+tKDhNYCxy27SLj zR}cN)oR*{(&}v$fV($555HsZ)F=oHlWL@a{^F*7Oem^a6$q*CwAC~ZL6uh=*Gj}LqS=n#@;TbkcuV~P7{q$1I9uu4}(W$YEUiFX`owwyPUs@E_r{9v)0ecx5Ann*TED-Fnk)1095R({0^I8rvRqzrSWn5G*19ud^ zzkll0dkw^uR;KfFLGW(LQlAA_gqzbqVtZ^w=VLf5-|>2xtFdQ3ebM@^`siT6bwgQ_BCGIExCC z2Hebz2&ahM$B2V8 zsfH+Z0v-PR3fy`!eiex>q~D3OqZGX~?!63H2$P*p=Hj|rmpd1AO>K_L%3)jy)3w2% zgXhg1*Do8U#-lo%E_T8jBYA$+j6Q5atf)G7~|nk?x0YG_ikL@klXMtfy6Psk#)4MHeCkB}u(I z@6=JKFLb46=S5GmfxcYPb)dnL0uCX&&7m^2U{~PAe0C0HYz-2qis>vU{2;FxJ6Tqp ztZj+o1JfFVeEt`Kyc}{60-NpQK#BVLB8Sh>lRnfer5OMVvTmN%ec)4o>_9GBedqRJ z1F&hlTa|rVKbeDpvXh3LQomQ@leBa?S>JuUxo zDxJ|L`~~5>N8WP?7+AlRMzpHdeOZ#IUK%iw2WZcp2)x7?HScFq{USkMwMd!yGReE3b#4Y^Te$Xn}Q6BRsN zHF2M#J1)#QBF)%H#{AKuib1H5iJa`0L`$+Wcb+?gC-~d`u#bhwMy)W;z(4<6p*_1% z9j(0^vb`_)`MQ5TNUCTzV(I=P#~q`YOd`$G<+g7L__4=+E#kNJ>4jTF4qmzk+sz}9 zvv-~d#ffJiYCX{ZDklj3vh41(y86W$#%!aXBDQiJg%|h!IRucF<+0Xk z*&~xN<5i^#M}nCzLeiU8IzxeJ_qy7I3p1Hdn?h%)mt2>9RHj1R5C#yJZe2-q!#J_e z7D^1U=~$S{VVNiE@-AxY`wc%Hrg6wb7gzi~>bG zsPth0H7zYP2*_&~tnCiH7{p7L)o=u;4EV#R4w1T0#qJm%oq-N+P^@a}1KiZJtyyU6 z_Y`EqjUuAmIf#KZOEF#LbgR-nAV;~NoZ@8B{H9^JRqAe34vK8YXMOR-TBl+6K^-2A zLe}=cH2wA>9CGtZ+QKZ+=02h+}}yA&dxKAyWk%Dp&ZwbnGkfQs!$DkoP6+ z{(AmRPF*wYZ%vHa`!n_F(Tu9RRQJ(T`=IH#ul@etzW%w(pnrS!oi9DQI6EDV|)VR0yR?v->Evo3Hm%mU-cabld=w(#d}BNfWio{h()l|`V}t0Gav zL8uO;gOV_7<9p%N5!IQ_5YP8+m=5YjK^7+GNm(@8)(6UZ`>EWDDhR;eo*MmJqe-cj z#_x-wShvS!#VOsg@9(c4czvkMc?OiOkXjE}lV5Wlud!=r;(c(T`TsKgb3gM8cn?X~ z!EuMnKSEKIK4>`QQr)O*p|a``ZX>d!Hf`0h(4<#arnl0Bc6*Q5-y*GAhrA18fmF&~ zuU_Aun9REd0M>&Z`a<;zeG^$Zfc#TRSb`>!8^q<6%=L92<_Dc^pUtnS3>*lB2g&mD z|2^FMXNxL6Z(<@g%RZJ3kIdp6PhgJ{XAX3X_U1482&)mRfSO%vh%-C2wMP#kZQ;wr z1fTcj`k8>%n17CmshCggBef=_AxOW6BD5phDR&~T{g0U(<44|DK+WjHhSy>`jPBqp zT&0?&rY2W;>B5-2!dqQIejQBp%HTy^+dt$?TaVD*BZ{JYnuu8(NsD-%$V{fYIe0N!*iZB(`aH0n;HX4dD)3exU)~2GyxIprky48a4a6{5X@o*T?h7< zJ)AHuT7K98!qLv%&rA`aJNj1(>8}eGl>ZF2wY`5`bR1aRu?ES`(w-0Wald_mn4Qc$ z?8G-|0DY7urZt-{idZ~oq*DS=d;Xkm51tp0zfKOlRlZdi2R& zuY%4rmmI>qTjl|V?~OMgJZ$bqkvM7xR743ZVI}CRQrY~D=Lc1=Aq;IeSu(Xh_)Q6q zjht_G1)0I6`RJ{LcbZyih=O{`Ml;KNC}}HueN#OL9oS|duhNcVuoYwN6crBaFi>GLtEy$T!IYcvaT%TcNA{PuFT&wurPKss`#iLecf5`kD++D? z_c>dgYPv+;0jU^{@6Bx^Fw1z4p+_fj&ZW8H>l(sTS$a~nkWsk_+m_02x`5iL)fC9T zd3og#Bo;IRx4KF0P{Kl6;Vv?eyvm|Kq*LWrb`0{BG;+V#a^++^<0F?A3cWm*NN&Xr zm55>c*ujZrH4_o!RETc4l;R6z+13g&Gf8_>5n!5`hHsr1?g*%)_b!5P!8P38UB4sC zR4XE}%G4Uzn)gN57bI;kxrV{^1-GtmpHKkG?tz{TGCg4Ns?srM6XMs5(o*|^^X^;? z_KP`UL1Z4S3|Hu%P4m#+W|ZjFf|>r~5#MGL`t?2_9AZQGj+u(5qeYoBDs|h1Pk7eg zS?W!)KxGINK2g)Le|~iedUHw>W@=MtBx1+r zxvRyodX)Z6^pD2+j)iNW`ZM)-h%7VTaw%(V^}`+BooqnEEzvwbu!iD%)Y$a-TgaDw z1DPm;;_G`0FWe^Nf{^I^qvM*COJMA{u_;G`KbJQuREDMu);OUXGpXw_)A%*>mq>x} z#~ny10qoM=b1_Nb;ZDkIvRE4{JOW1E%lSzho^!8hl%>N*-wDGLXvr4Zq-HKWA3i)B zjvb)=aO9k?-URqK=-?Bu#kVGO%?12*;MRd;B@jf_DmX~S*wAZNEuAG6uj8cDw=|j> z6gR9(WE;W#wc1XLpr6$lkGwiIj9kgTnu2^}ASI&at9z3gWlXqh^mmQhO{^}*0DYsg z3Js@w8J}KdA(=fJnAWt|;7ITY_Vw{6p+_ml*F>7uA6PAWxy${l& zxwv6ecq^RFRIN|VV-hrKGE}P6`ZXDRj*u2>z7=eb&g_fPH<3`0gMKBI#=`?8&a%<1 z%T;7?+Dn8}>Fsy#PD@(LGUXzYRY)YTEs^mYPx;9)mR)P(#xq!Wu+tfPIglJo&$sV z$${Cy3{t2)p6(G=t>y>niCdALmff0PX5NSn!KGF;yi186c$MNEWQ2OI8x0^Yh0tY0 z_Ak`gNVF@gc~5ZEp#d@6-zL=(s!F>*Q{^HJI)55JUDk20E#^$grZui>Q%=}5Tztg_ z27{z5g0kbLT*DVr;c?k86G+*7Pc`oaMS{v-`nd;XjS}6#eaz9I6g3VIeq5XDBai}RNVqm$vv=OSD_&E zSrL@sca*k&oUa6#metXJ#SW(C%W5%!N(*a9aPx;vi|I=GFb!)8A37fY;b~F-Qg_A1 z&ExAM9Pn^eCcK}Z+JN91@AQHNJyqPouQ%F@`H@~J99F=iy}t02kOO!xbr$s~MN*ne zW%ZuKWN*Ly@SbXlel{24%$iYnG0>=ttaidicNnMpRrVE*DrKp{WpB;2jCF5}O*dp1 z^K-#` zibiM2h_NErPky%`W^qnBAsPIFT1+3-7nQoR+@8NJ3@*JE&LC|J^f;fS6XyNU!#j8; z_h(ba^@ydKv%MUEcC~P zm+x(jDZ^-}wTX3N^B6sg&_f06j!qTh(qzamxo$91#Fs|JR1xkK))yB@dP^@*<8mYK zn5)UKk(^@xDk9^XXNbrzV8ChJisSTmz_W`aH^m6s%OQ$^xV!bVCVJEb#em^UH7$b7 zPZ5f2lb$`(Asp&=l;=E_zvD;Mzom_$ug@wI22?(3&#Upx628lX`L! zu|Iou)USy+$`ZkfOwHtOZb3!w9LVQm_~0&I=A70H*%N3v5*O>sx~$efG-LkbBK6l$ z`+?jOp^E^#jW8C5!O0f^)ltIju-^8|@U6H`eB8EsOj(`i`O&CBkzbBukr$`Qiq@Fq zXoiw>0)I%u_fK5wPYq{#OqPK(8okvaB|=r4U-xNnDu?bnh2UImWE2+Z$H<<>NRC-FI}@fJQ2r!Q}(x_x`ic^!jj5R5mxlx_p932(|;F>{qx*%$hfMSUJIC4i=D=y-3(& z%CPx;?=TV~OC6e`+C#`Vsh$0mbkxd2sG$CXB5OmBeO7DGVGH@ceQxcd0f=%?~v>{fSk#t{$F)MLb|2%Y18pdNB}DgLkz@e{Rf0S6&+b&R^#d zz0uz1Z|3&~9MkOsE5y-gG5NoAqnV5mZ4iVZw=U*9xO3b@6w}1VNCxB7i?}l%h58YI>3+ohI96 zaoy5PiaOw|MD<0o#y=4iDrVPbrTQ{rcaD&C4;&`QV$=;jXzYRuEQY|TN#F+v^`zNA z2b>Y#u0409E`cO^8IwU!wd1v>Ouw?WbS_60EXX`I=TedVwN*#tp+ze6+EM_G;0Pbk4T|`)GnG#a z5v<%?MrMc5WxGO>#vLRame9~JE_qft4;R&fV0sJ%%4?dNm|0j{ZdtzT3H~&DC7y$* zgo_(9EsInM=3#c9(HNUhW8*^lVpCj0bX0~uykO3783TORwB8|m@U(a@0O237$Efr8 zeZ^}6H>-teaCnF*EO{6wMNrMxR^C9MH4Dd^WKah0h6U+=o>{bMHfq*V+(Kk^!89;s z)75%{n^HV*Y6xoXDrK`axaHI7n%;jY7wx}2V%=;;1I%Q zwTj4i{Bho$k=;c}xr(qZH)loTtMu1w3=I=&lObFTC=PXaY9<7~O{vk}5u;i}3*WEe z&hi^OF$gPyuYBD!v^w;v!D-?2X8E2zGVCcfi2bTeO~yo(fn&t5T+PtTBfh^0xu7KrQq&^Q2<{!@*iBi7ec z6hbQWsKiJSqN8!zQj_pF_FEUV2vlLXl@x6xg#@lXy4%11m|KHI40cc;jk3w>`}CGs z3*m(D?0NX(h`=9Bfxo~z+#(rip>ML#Fq4c&K(WE17l&huR<|mr94v8#;Nu=kf$<(S zFC1=c`AY7?M9Uu4Q?bfKO1I9--B16vuI)pYmko}Doj-Qu()i%ib&oN~5AR2;Y!2<* zi6`gap%}lD+E#Pqvci%*l^doBDhmt}g50f&_7l_mPW~47=_QuNKCn~bsbE}Xu8#N4~g)Y9$9fT~d;fQJF z6nA$kLXW;rYUNlN@+HG~&jEJ%KvUuD5Ly~;d!wMs3k|%NsP0Tt?`2x`rA89axAjO7 z)7r+r0x_AbU%7PI^m)ZPwTrLlYv1{08IjXz#MP4wxhD9;-x? zB35WhXanZm@li2q*=*e0T@o$uqx&hrOuLEeZ97H3uP-XDDT|Y4K;Q1*M7hp!Q~0fg z+`BUNZ{7v>s=aG@*R=RT-onS(yu~W(=)0M2qb`ga@b^2 zj#g97Y>qYJL7gK(9RoM^qo0vgc->Dd=<_*qh6p)naB}5srMDJ~S~e*c*)pSW8!giWnnijYGOJ)x}zz+T~6+OeMb~xGE$CAUgyrjZgl< z5>kYRM|x~1cFI1c*<6I#T@V*S1JfIuhW}Tp&$VW>C^#A7vp9dNe2&{S92>ffrWm8| zGm*YT6!D?5|6rzGJ_nV#r+TE`JK*Hz#tjTswPaUgo)BB>#mMPUw8eV+1bX{l_jL+4 zFI$I=$X01Ev#kKL?3rjgOL)iEmY(IMo#mhf4d00x)|EBQ2O+Pf^fZ`!#MlbcG%QaP zF)KfXk1kt>QoivJ){+DutWV(NZg@D;>NeLGMPda$1x=m;iFMGqkC`7oI>u|wCSh>%ucRfYJ_CNf|JUTUZFhgCQvWL;qhf^Yc+#3WxLv4fiSB8Km)N^>1oXJ-D>-@ z$xzNaU``9&yL;zG&XmGR!LG5A4t zScUPs7`+=YV%d{GSemnjDsa^XaT^(6pM<tNdfGEpO_Afs3vnb}e7-55P!E8#SIR;c%xJ@BR8dnK>(y5Q8 zmX)c*1kyM2E4c~zyj|f2uzV43$29=wfVYs%2B*G=cx{PPXuDfJOlqZ4o7gmw5^?`_ z4Oq5o%N(~39e5|x(oz-?ZqB~(kRTYB{1`lSX-EuaLGS4On=5+IPYn7Ikk&1WU*+y7XbIV*Dtw zSiw?>KTn3J@JjEE%|p^!hxL`P_{{O2UO+3tVbasTZ1s(-SZ~8%7gecFH8{T^y_zBGNY*7dQ)$4E;ZU22z)X)MHY= z_}KjScPP(~mYP+@t?cVQNI~tzzvG3V!`ET<8sE|vIPxUI&yer^I?fW8IU;eu(J=jR z_s-1B9*b})({Kk`#2(ytNyNcA4R2-On$`E4fQl2L%{H}egb#+XBgMB`ba1Joi#0mT z6B}utFc*5XOOC*U2#68T@`tisCeB5Qv>4-jBV}4YXT%6SixSuTnNCR77J0UUzWrzz zK`?G;17MUMWfa+MF8s^o-TTUa^z=Xxed1wzuIl_GXRe@vsghdChN;k7w$NQ!$hr=H z+2j)flKs3x-Penu>riZg4J6kcU$+M}!LHMV&;SY^yDL_Zc?N@jfNc67U&lQBpWqST z8vIjdB75l5-pte9PNgISLav0KyxKjP<{XwFf7mfhs6~)yKWq8rf^g+*C11eta=asq z%a{IKL@-mUQAVqtSj$s3ckD58oVE$md$=jvz+e~bN$}y(C+HlZ%Mk%EOcCVFiI3$< zAk1l7;aGw6%LH_H|3nyYL|_;?zG2ExS1Z-gv5BA*n*&aZZIHj})hEgzbfxACbz^(D zQ+QjJs=3S38@=R5L}7lAU>KKGX(Yjlh8-TqiwQ8vTgBiu!>tmrs@S$C8>qBLU|#l* zaC+RYMssdsEq?rw7uSThccYuze_CY4f?uz6fKdifq0t+2#&71VNuLi}!elhVuhGL? zgkdBUpt~an)<|WxBOZ5nagpI;U5k%4jEamGKvbKhp2#{k%luFA) zNYq?}x}q((jn2kN9dgbjIs-DtTO%cxemGXz?y7~QjSCo4gClDft$1~%KZ$fP7Wu7pIG6}& zaJaEM84?@Rz`FpHDpAYt?aC%|b$MpYFd)cH_CKqynZp)$2f}ecyQ&B7Vbo<#!XtfI zrng5IKx27FkG@?v*|5v-WGqQxIB;&pu9O5wt3Tqlwi9` zeZ56gV$Bilm}-~Vvl_zW#VqqAn66-VnN2~0x2F7PXv*bW{?|ERMx1@AKpKW+1&Pgg z-FM<1@l`Uvo4)GQ5WQODs2)>1#p<>ER`2WIf|b*tqy`hh)tdJw59Qg#u`Z6)=mvR2 zrwU|5+IhQ9dbRQS@`)*)dO`A=24fsKoc6|8*l=3Lb>-4esgzVPoB^?1Oc%NYh^E3R zfU$i3_GhB~$}q>p;y1Js;}Zsoa7dDQU5PkecfP^dx19~MRy8at1Z`*GX%w~>^dit% z&v3-ln#whsIQ#xLq3@{NdkntN(&B}Lb|Z8+OdJu8Em)a3Cw0lhx*RL{%4DZA=;@9> zdQ{CXZ~(1_*70gEkq0_c&?32U&-|y$$VX$q9xvonh;6Kx>ckziarxsy2eMgeODEPc zO55$fv<+mxf_o<~q=o%=cTsgQ5pbW8f7Lv|sVUEl_@&Mh(W+kuFwSz#PFSLCWIb6* z{Sa{awL$UIgfMWPljyVa2Y?z`^V_+#%;?ByF)AaA4BCor{i85KO>ZVXq+o&l@tmJR z%BDc+AeV01+~x)G*XfFSbx1Va8r|o&yC^U9aI~edgW?N!`ws#AKs^K%lF=m_yFjaK zHD=?1oS)C3p-t^Jt-Pu)Pd-~gc+lGJ3X5t4IJzfN+o&hvnE1+ysyxBApQ@Z3=UQKW z?xm>k9H(A-`#KvMBGRCySuah56qCjGFD}>r9-tH2fL43_L5qwBQdVlLssT-SyG<=u z#_x2LKg35haL*)nj?Lp}b4VwVF>x?(b`-oVQ8t&>Sc z1?y(wZ>qmXM_k@Pdf&G?(Y5eT;r(Q$uxe-wsfQum3XXV;b+YqhxIhX(vG4rQi0`_0 z7jBQ>{%c}ev0SEhi#3OT3x{0A&t=A5vO!@}BIfO&Nmjq#tk_#}+~szl3~N$&so|JP zK#Pzg2A`AXYziwr#mp}0B{lRx!<)*ijF@H)7kB)677GhPIi^H$Av6@wcee3QYO-Vm zI`lhX>tdokk=nKbL9MbEmslo>-i*!{Eb!f)ZD|R;7MAX+V50dSXG_^vSUM@0X}p7n zlKP7$BQEAF`|h33pJA9a6!Z!M?Mm>N)bE=Oo28G2GvmsawvO`oq|px#TsPiY^mpQS zmRUYAy6!Ol($HBGeY{HtTutlweR zpe8FjRUEJ!ZR-zAK*r{mVZr#sIGS5997M2~^P8v%TG;$E_Q}`PFx!J0-+%&(gU?^h zpQ#6Vt`v$Tsou7tP&Oy;$K|8zDU(I8Iu#TQ{%8_bdKKVImS-mWlr)~=iJXq

atCNF<#X=hu(7lI7dXy{2hMuoaY>BY+H)2(cDXw#DKKc0zpRw6*=TL`_*sZLPxLN+m== z-iYynooh-lmyOKmr90WLwCWE883aVG2@>7G2PwIQ~|MrjGC^roi{xZ#*qmDZEKAKeA*ANl1 z827*P7!l{qrTBOE=K$mVX{EyhrT(w9P$UUUo7mvAbS>4liH#`7I2^Rlli{lPx-Qw6 zuM?zI*J`WZ`0nDkwVAx!XqXjkOe}ft z>}Jwm2~L34ipCdoCu2PZr{#HMorX|(*I4a_K7H%?aM{cW50>5&s4?~*uOfw7#FVNp zJ=nt7SNB$|Q~c=5Zn9{yw`B~*OKxIFa&z(I3E+el8Pb~7ScWy0yt*o7mFLZIYFC97 zy7ZK?t&+aiMLDev3L_y78SZW9?*5>a*j&S{u`fZ)%;pgjKJ9Js)IEtjFvx?%P$If$auk-cO_hZEeDP*=RL}u2U z2g#D^qXuZT{8)=N^lW5yXWIscr3QtgQXiD0e!lH(z-q+GR}JxWLYk9nF+=GpxcL6# zF5mj;bzih_M#C!B1BsDJ3#YIY-v$Dl9O+uZs7P=2E$P@!|KbR0lGo`0vbKRs^$m)h zjGs|Ti)~aWzNBVZ2scQ5e(NZZn@kKjYFvUtgA9H7)PYwD{nisHJ`2ZNc4XgQQ@QR_ zp{k;`K6tFakQm!?9tvlgoCEG&@kglY6mjblnosSB0%l%8QoBZSi3vjWKgYMgRHpR8 zIA29V<$}0xn<Rz9 zZ>sNk#Laa>v@R8XD?35WdTk57Q_~H(!m{9bfIQBBfH^KLYsH?|-=oJYx55{+|HIw( z|Ga9?q30-s)!(o52rA)NnrgfLN3{(Ezr+>nj^E+OiN2H%rbDXS! zPm*L{hQXo%W9}Tc7;f6g=|>JRdCkza&E^oPz+%(CEHT-cL}c-^qx)(L3KPN-CmsIc zihPJPL?q7m;kKMfCBgVn2ZA^L9@6ftlvlxoA%cx`es%pbQ!Bi*a&C0?<;d5*&cc{VRF!b8_Xl5S?lkn*c(@SMSXC(laB38{) z+0ycW$@0I4l~yrZv7TFg(oZZznuC9f$9N=a6G+}s{0Cf~SPu<06jBekni#eZ>2_yX z_&uLw3ipvuzx|1)Qr;dq=aD48v&Q1446C@S#)gr`*K;StnYXC@$NyrCzq28Esu9N& zQh}2EBN3k)m3e#ejQ^3>&vJ}E2K0*?cUWN7&wHE2y2VLRhLCclR4I7`5tJalqk>Ey~Y%%O9yx z`Vas167t47S~Vd{5C8~gVZYaRQs_>C(3;hu*F}RHg-@N6eX16~QSepj+h2LP5yv&R z%)tTAO>_wLe}}dSnEU!9rdD(n@ca$in#{M(jEr2d{`21pi z>3*h5Ed}WK#!?Otn_-^eTUHlsP;=s$J!Ple;lQd4?98LL&Vrm~VNla+63ekDW;K)L2MUPA7dcgZG?G8FJYBwQQ4GrA z?eiJO$O&lj9@Iar_%Yh}IyHU5F!y4YW@)m^Jv(BHWOx^3% z@<7ehTNkP1=9(`!Iu6-YrxaF^$s6^A64=}vmcxn140M)IdRNpB6knw5Y;%ZulgRU% zQqdVy?O1VI^uU2H29ehizxucFz{a9kG9t>Jgd?hZ)L!Hx^Q9Mu!ZZ$xG{isW6_(9q zkJ5_$`NlGO?bPpjOl-)2L)lwOc80gn8nn~g%o#WVk@X5piP40Bp6#UvjCua3NU%smdsvrN538!nxKf5&!xfg% zCAiN7k8`xNN=TZ5pFC(AzBT#gov(F43yC2HVuM+{ABWdpP8#y}I#RjwixH6A%AJq& z{2k+`RgM`;+*vkPQ9;rj4m^W_@ix;&bz~DjkkN{L*jG1 znm&=A?ieceiuI88l1N-9_hU^CM|sS*C*N)Zxhd$)`wwY_N5a=);%34aCC4+t7>K_< z2I!jRTG-0dZWa(159bG&6jCCKqdN%`tPIhPb4-&15mqwojKFu*I7XdMdfN#STtjtZ zVf~`GtX@FDmAZ>BjvaTs@H_rUa(svB4TY({nqJXTX6}V+V{1le@Wmy#HMSUO$M}Gw z4zYYQQ+d~CNgAZ*{d34O$H<@fLIQitj>q2J?wAOI+5w2+d5=K{PM&N2PS+2nt{;V6 zYNcF&s5i_DJ5uunoCVJ5V|hP+tpMY)O!pi3;)t5+>3|?`t-Tv8S@ z#q4{x7S;aF8IZ~HohJr?Y2rfKS#LkIxcK;1^V3oY_$t?&<8&g2$h>V*UPE|yD<-Ds z@iMB&eip(!P9~#KLT?Pc_=t@VCU&wGZvj|5)0^6v)AbnJLcRpdvy=|XhoG*dSdb7Ss&!nc<-{9;=9lH%MX-tk)nh2AKlw<+W^KDwG|Oi z`&e{~gt(M#Q-!4qG&`zlz<5o)k)Tf+ra6*Ja}JVP4gM~)@N^9>)T3CW_Zek16_+_8 z2+3ZTeyz7#2|5a^(X)_zy{{rx++!d@$%tb^JaG_s+XT?fa?R4?Gjq789A7>dhX+}) zA}&ZAbsW-gMdI2;tu?E*?8$w00Y?|lSx%cZvfpKuJqs|`0iZCE5WAn0)sUb5g>^zLOeaBWxQgB2btpn-77)M5T2mg%4{JPS3_4%UV`l zo~w)eD|ESIl46Qg?0S(L8XK<2td&|cqsH%gJGLkxItR}!O9Ta~5|XrEn^8fwfq~}q z=3Fwn6|@-EYGjeyo%uo`5~H!-EiM~BP%GM8TV?iVTR%8f8e=0wG#3Zha9{AsC*=*g zyr@vJZBxH*ufqnf6?9;LTjtX6(=*!NZI~x$kH)MKLN45+Uc2o~lUCsvSA5*MO(9ap zz^+jgq|yeozTUie9~;w5O3w#@W(w`QaC4_mhFYH5Q2emOM~Ynb0ag{-J-JQZ=L|Zs zQr@BMw=hEGHb%7U-Nu=ReWtZd|1(}DyXpea=)PiV@=NRYMThPPEC1S62_qNMF7!+OZ{D z{3`;Ldo?nIs}K#aEvR@*TC%&38qdJXw@q&Rm{+vCx3{w@tL;Yy!gNns(>5bPf}Tw= z-C-8TjKB+EVIFTHMG#~?kFI|A>4c5x{&nzKGMC>#H<@a1;G+RR$D@N4_X7GB z#55Nl(*NxWXmxKvoYO}4p~Q?PU>nGHhZqo7%Ud_!e$+hT@kv*FPy01bvH{bmQ`prz zS=h}JMIoq-8-Oc9W(OwoR=7eVRt;s?J!{{TCel_vBuw4~yF+pc8=}CSoIq#PY$Fh4 zC|uN9J>}XGUq3hDkA?p=S8PeR%(MxZ`rJq#+=mRd*Abwy9_5Sb48cp7&!PV#=0iu$)Lb2 z=t)0~e*d{3tM*9`ms%0s4W>lfMr~Y?P*EQswB~p*cP%}Q{$fcZ{iTKo6BlE)i>XuA)>k>t7+eHxgqx9ILo??dhk zHk}|Q(Pzf2Bu(pDeVfg^qZOu|jz>-??78{t4Y_#~pyNv%N|Q2l5(;q9i*^D_w{J{ogKf_eKCAm zG)jYW`f~lpT}rOkZE_l>Go?QC-#!1L88sf|?CO4;Czm*x{8M z&1p!Aj|@L@VN!~h01bdtz{^4+RvcuQu$I)F;N5Fa5QV4Pd9L6de*0s>iDKNKd*s&( z+;?1b^@Af{8~CxmQEq!f>cl8!J%!bnIuf$ZWX$!a&i$tT;sHLSswd@2s$VV*)WRTQ z)wNl2_MfU$N&5VG`VU4ni3;3PdgM0lR}fDPcE1xj>%ze&QNay^;%X+|%XJrf6#S9Y zf9> zu7&y`dx0w|Lyj_qFCq{OwJq^A&#{!#4%xobUdx|s(WB_2}y{OzB4Dc8H>21j0lpY%IzjZfjmozm2kwX$R zKW!r6?{B8wxj~9|pgMDh)}K?i2=p;{JhRp~dbJ|&R``=&ag-gAVMrrj54H>Qgci+{ z@UJg|F1xK(@}RnmIox%=!tAU$q|v0+>X_dsEH|C_P*)Y`ZC6%d=@o9|Ol}VSVZOx& zdz0=@)wxhz^Jf-LqiS5^(q=2nq+sQ8QrV~Nemo0P4+xmMJ2aSz{3X@PBQ4^skbY9R z-T4n`>*6++FMgd)8#wWb-C>WWaW{umZa5i(ZB*_InVQx(-jh71V@=#hspzU7=- z@F;wL4~f<7^%hsQ{8;eNRA|QLfqJoAu!D zDK3T)aNS<;HhpJJYbU(DR%9Pa{Sg5n_$Fb!h_!OhjFM7w2 zv6HL4(tD0Y{-Hmt;?v#%%rW`Oy4pU`knu=-IN*$&pj5A&GbJL^p*)l#F5{6|#w|)y z!eQUP2YIOY%D55xeRZS(Sz@E%N1>EDLm|KdP_F8?`2* zKt=xvXHh7TK=td1A2;X53s=<|V5T1cKg)C~K4Ofr;oG%vDS_rS?*;#AjLvO6WTinN zOO#}rJvizW3BOjG#4kOXYTzPI!hoKem6G%=y?53loT?+HrZ&C$D?pM!vv>Jj@6~b9 zTP(_#&U?LfAaLcHc(uZN)W51m^)l4 z!93T}HOC{1jq}~p8)-g~Hfmd}db?*m@9|>q`m>gD3I?^aJm}>yd4R<92yt3w@O8vZWA6-O^fujCIyE z9}z!;XuGXVS&5K-Tn8h?m9~Z@O>FDg=8iWC(+9pPXT*@4)jAl&w$?s;Tg?A!HN^OW zEjp5hm5KuT>R20#5MwU=?_cnKDUS8XUqxK^YI|*}{t2dB;#E?HRQcVd&dD8hz_(cR z?;L3_k9u!HYHYN#c|IyY5D)-J8;{&+#@PZdFX%;SFm= z>R=jGJn9hHWTYpjg!Jf5^a`GL&B!NVPa2DBaw_uak$C)G&XxBNiVhk{c14`Q>p$Sh zFyMgUd9Qy*OhOE>-_SjY-u!c6VdK&^@R0mbDxxBXOk@tv2~Ba8eZCw#>b?2XlUe44 zWr4%2`7`ZjT?+GtP`g`=TT><7urZ`N)WG{*gm{hM`3el_G!m@OLT-xzl1_pQo4XPXL0J;0; z^5r2_mT=MO7&Ut_n{YHwIt57=-vW<@r;qt2F=y5vLahAMHwtFo70y0Z+y{Iy!GQ|U zUIvRNH+tF<43aCjv^Zt#YcX|bWNF3{gn|07331drqbCDw1>G`0B=L`{vidUPCCC`( zc*78es^m$CsubRCzF3it<-hvEP8yj9SphDwwTOV?w~5%re8xD?I`H?#lHSP${>a@u zi!lXD0=4Es0?-#TIKu?LGZe3D_G>dC{Rdh{>Q5ZKb=xfo0cYSF(k=Xza#u7!YTW4jm$P3m#^Sfn7`4+5QcJlMsj}#QJWXqarB_2J|zL(OaYc zy=;3`ZUmYj$v3$4;4Gi$Spw*lYpdP8QAleBfDZcBGS7y7d4HFK6+?CL!jK!mnPXn> zj-w{!>A$;AwUS#x13O}9bTgy4ICss}{uD?fL4M|#NrgtE+dVU^TrWMA zyxJi8`!?b3-=d1^=qkcya2`6&`t%c3X8Y;wYWt_G4He*1Yi~uIqY3l|VO4xU_wUul376fsK(KQ%2co(;4yEXC*6)ldgH7fg5fi7Q#TWB zQctHAq-4?g>@x{Iy}s3Jy6B+oeNy9(@R}>7`3uSH z(040nZwR_fGX11hEOfo5PZ+lg`~=R?Q=8g)$bFMs?zc3_Hj|}vCy%dd(_DQ2^`C`m zYG_Vcg)@#wZ+Naq335Tl_qf0SMd{2P1fZvIpOfbgUF#&$%6c|;hL}=(a=ntHv(GAZ zGFA;m%DVmZ`_Zx}u`Fa(5hbp(3;9z&p}5R;^s#Eo4ywkn$eDm8cgPq${xVU!veZKW znGm)NFdn@Y8COdajYiXB2yz}lUUboLTr6)qk%~f5_>=9j4`YdC`onT9F!oZD%I)XX zvs%~E`{yZCMJijVAVeAuRc4|R%xsqlv%*DyaKB(I)0X>7j9iNcRs>c<&XG!%m{k40 z$g@bXwr0zxpjqL--On<*!}zmn?!JNjX}?9nJX;wX0eTgORdp&;G4C5Na&y^JPx&7V zGv>dIJj&d!=&+e%68yz7(#XU8R5bfD;$5M{+60DdY_yE=euee=8z$)uvO+43mj}o; zk-_O(eJ1{l9uhRh(<}FI$YN)asrYR&F*Sn?PFX26ISqH&Z11MMlv8Dy`0kOOV6D{v zy?tJ89gb0>=ZUxUnI4PNrBd54TXba~*G+G8=q-tVOHmrF=vzIk7ofXAM6J}j4waY| z)Z;dkMrIPpA22cwy9ZM92&uejSE)bw^d z$-vXdoH`rSE0Ij4Ok|@krCqfjI6j}1rRgxn7(x5;%H|KGXR@W>oTnDuum-#K9x=*O zFs;)YBw$Ov!&iDD+5WUOWS&8|vcpzsJ%BApSee;FlOU;GOIA;BUh&TYI%&V0rDk0k zfUp2XmKH^Qshhogce?Afv~Xvi)X&gvw=1YtD)rJQHaRSesAaba?SQO)%A(R>+4ZWtU#5B`Y%bHXixd#(>*i+=>8AVA*NYEcb)gL%wTi@m zPL%X>&BUN=#7f+dEFiZ9=vk`TmYVs86$WlrU-~P8=|lH93V!1+oAZ z+BGa}C%-QJ=}N(I;^9@!Mpp5|as&CYM<8H-gxijW+k2@HN+;tM~Y$9b?(e zB?m&iQIQ-D4I0dIt&o-BB9(?3Vksv5=BeC@y*dZOh-GAgwZOhdlI2#b?c^)e)tkyl zLaf7gKeY7?pt*NkuTDnepX9e{qD)D%eMFrsrKT!V3ZA0RmE}TUE%pi}w5S8PP zqPI#FT~aC|ezz%g{OeXtset1gjqLM$rr3lblMzwfZrnR1f+Qsx_1>^U$g&rytCO}2 zhv0;Gf6K(X7e;jaRi-wGlH4#CKWyh?K4n|D^R>Ur`-p50Uy2b_JZRioKjaV*wz8jWJKMAU!!Ky95Wx(neFNM8W}k5JwM!Qnk{N(^OYZROI!2rhNQ z^SVDLXjr>IV>N`B{y7B?z*lEX^82bI!>v&`m6uDRqYS4;rCO%t*tDe0^;=@M@Sbm< zyD)5%3J8sf>V2y!8p6HR&`Aa)zS-loHgO$&^+3IG?RQapba4?7##h)0#9HL}!3F+- za&&f=86PqTtx|=FVf*uDY6y1XRE1RbXP{gIX?L1HU-y}3!sW&2TZJiWCR}G0>5oKv zg)|*FY6^_4VacNLsWF__X5-edu8jN*W^a^k)>b(7_NhwN>X_axJzVU@%_<&##t>Avu)O=hQRubuNhSd(BBkjfN417s$e+A=wCg$ZM@&W zY}t2yzG3n|^*V-u=@S%-t{_#uhR8WAz3)23)vn^dv}%iRpCz{i`Ll01ZsJv4Fm0g( zvYq$xVVneZF@!w7W!PhhVPNS6f56Ak=tM{?p}~=vEy)Cq#+dW*i6(3C_Q&~y?@ngI zv#|viQZ8^A-0|ja{Az$-tco;TJ-UH);7w%U*2igba}|JAoYwM(-^^wHv;@Ox2?CB- z%&1;;&wd1_t~r&{DurA)<)2?2{p7%S9(S@i1I1dNG84rFrne#~9iRmBGgAdlW&y}L z8-YG{tw=*(nZa5*#CMnyq-BYh@qNRzd)4Y_{V&I00J29>L@wcxr|XxZv;A0|V8!A< zEsvh%z(hr%Ku}@~EUxn9nv=T-wcLC7^qv;~z%4hbbvTaR34}9jovAgY?~Z zdpjGQ)8g?$ANz8XuY%7nzN5Nz)Qo$@0$SF$ffpPRqYW_T*NFq8)}V>OO&nfI#Kp#% zT~X*uHG6!qIzU?N$eeAZ8aF|&OPwG{zHsaO@>2HZP93L}T3F+YA!i350OE#S?~_6! zSGaYnW0i&l@L_37IN=y}>)=#rP*DEy|x)*If?TrA2y%{j~;lU}U87;a5ZaJzm&HYVh~t zrN*a`2os#-#83i3U4JFWe;hC6BI>dm3?t#X?fxBFQ_7o6P#HnkOnM-xn2D8t#F@GL zfg$0FNx=>MZuSfLh^0=<2exR9AF(c+Y$n}b;@^Cz`=QXNLxfBac=sbhYS{f0VIoc) zm{XhGw-r`08YAPK1pRgcuG z7*o-VZ7xln(~I(aPUU2!{^xyD(=sMf5@T4UxPRM-Yx4g1bYC?+ouHySs1!FR^+Kr9 z>d7w4ffu4;ELZQYN%z^0k`@k_?yYW`Ab0Nk;vDyJT_I`Hsh|+{$t3^Uueuo@uRHM?G>8vBCUD=@`dHMP5B)y#jpCmfPJm^} zJc*CtKzI>d1opk{aBUHD=p>i#pR~VS@q#J^IS(<`1+#w=U!3(J!_pPU`);7v1wvAU zil)Z<1p?|@B;M%m4L@=cw@TlRlbTiFjp3ga4SRex<24F7Z7^!H7TvIgAV*Q$M_F>8x~>NaLozy<5V1xza`m^( z$u?=tkxR6a{7WxCYXK&7-NQOv=89 zg7`_lPaW0QpbR=2|}zPnj^7H}SF?dxjhLFO@%rD#C**LbG>dOwsyfNi$OO zGLwvl=jS9%eo5jg9t;GyV3iLq_t)shj&Zy3j!}EICyK8o4}RLZ&-btHkHDtE=MJZY z5*@CV*WsXE>V&3v^_a;TUa``MI}~OLIY-~9L|UQO=s5*Y@rspejZHE|D(2)wFP8lw z^c<#_2|_znb9nP_+ztQ}d7dN(;?|jFo6Mrq4n6Q$6V}t4j?`9s7FP~BQEBgbyK5D# zdquy`bx2{&zFpy2$UIKVSAybLK+_@qm)7dIc!4eNn0<}&isdPK;)O}$txRnp?#3ZM z<&s{p6nND7c5Kag zcRXNXd&RoO!oQW-zs@DCkBNZj{ix3|isgy0Kp=b4I#&Tpf``Ju`@(?N2Gx)(hfNaG z+eD`m!jZ^acb$1Qb? zA^e&176j8770avYR#rj-! zxif!k-4hP|_dgMx{LW(P5TO|n97(cPmO&gXZt~{C06b{xvgFc4eYn-!e%ImTMWcEX zwsEsS_N5H4RoP()GRG4*c7nJ#BYS)5I~*r%YMF*7eM7pdS2IjyTEMYI(%Zn?jAfeQ zpKEGERIZVwAtxh0D!;;;&{w0|{F3mq*YAUNl-TI!?kBX|HcmJ7$SCX#(J%dDIHqP2 z19v_kq>h)c55){Q_%Uz6*|_FhfTc~TrzatmRB@GB1xvbJgVqck;4!r2== zc;-p@k9(!)oJLN=uwlh_(mz0Ns=ipDxiHtK56X{OL@bMh!U^6UHykJ{bM?+3*(cY} z=7sq)Y2QB?Yg#;nwOxWRi(r&ePCjG9^Y&>sjoC{*XLkdAu_t(@B6ThC#a=S=6S2d z3`8c_=0J4Kv~kyUB_?IUCtn{EPd%X;LtK4t>t90vaK8d`;ZJ*Ifc8qwA-3D)7F-&N z`(EkGqMSCpHoOF`+tYdzSD&;vU|JY4lRj3dKhd`svL`soYD!(P+Iz(>EhIWa6}r_I z?uR?NNRpUZe1riIH0`N=MZ2VVt`W!0&R8`k|(IxaCQWm&K0Cf$9#ISfZ3H4fUZDYE(&IU5^)%*#C z+2K6Ph;X(?zQ$IPu1i?+9mqEk>*e;nG^hD%o zYnEJDBA+x4itp5#-$Jr(YMhl{nqW8;hK{MvCh1kKTw85yX0n6UP-e-)u_ov@1bFq9S`Ta_x91c-!zD2*svL0dfu2_fJr*C~=);fTynraerN zj1T=-a#_*9DX(5Cmy%{XqHHi&#$9~+Y2~RWjdwW$vB!eGapV>A5tBQf7Wcb0zN zoDyXef$!rnOg#lBhnPbzEMwg=V=D0L zry1f-_;|sz>_L}J;Ix9bCf3z;GTp9dhJRRq3h391PW#$jDY-fR9SXMQ1y>709$uju zT>(ewmL(G0f&_c14^=+ z6OY!(-F8Qi8|)Xuvv|t;&&cfe`%LIO$zaLWarzQNAx$Q+qpIlSUlQD@ze3V>xuoON zS6|%sdL@KqDyawr|K@$qJi$WWrck$ZHNo-oUmJ+(bD=qs)|H=qrH zfPEZ<^)r6)E(;(cn|V^~*M%_wLXU4zk0R~mVZOcS%?O%?l!y3>qqpcP(rUEJX@4AZw zgGl(S$%(fx2#q;y(D|&8V)?nHYiG2qSmtF!u7hq%VwqC``#NRMxTV@eJ6%`q&S~F? zQ?{aZCb4j5NCf=L*?dZ;^jkqb8?LeR@xel4_uURIgii0w8*Mh81eHcn`>lWd?glat zmLKkH9RM&?{v$RVYg@#Niq+{;kHS`g&^-4Dy<)Ri$0+UjgT`$B1tOX2!j zXr&%mIr*HD==5{&nf{1oB<4q3*OiTrsennT==g_E#~^(TONk$#k?7r#d(BG|0zY@ z;EgP8eJ4fSeDD6=eg9+?3SrpEUG2WN27)dO8COxqIjMFrV>@LtSbA4u%n zsZdC~9tt!ocEsycxh%iL{+)T$Yxb@XiJ*Q{7hT7Njjk^m(Y!m>=j)SexS*e z*S!Q79mSXYkvQ>ir;f+hFjuA@(bgg{=V*l^zk9Gdzvu1@!P|Qwbud&Wq%mqn)9Exn z|60+x#-&ZV4`2*UsEDE}+q<|(nhU=AGxc!ek%W9QtA@r!1@lP^JPmm`5pdwQ_*3sF z2Y$^7oyCg}{()dt^<}=ekcBVt5C7RJx`7-GHQs9h5=$b)r$x$v)>##3M*Z|o7wrJB z18=`L!WV7w%nkCYB=J|s{)dcj;YUyHPoKI!i{qE>hz}>V)WsUDWppI0uyBu#ObIlTCA1QMCrGlA_}>;{{ikLDprtPu|b5WvfLWt zf6a||%Wt$*jh)(qN}1{f?sd*LcoQmTVTfOOy_05IXpogb__3k+2!XlAbnxJWS+OA* zd8PKnU+q4)q^tuzqb0_tfSo_WxKcV@3Z{)qG z+5QV)so@{4Q|G)~b`IPgdiFPzk3A#)ClYUWeS-AuVoI*GQJZN>pU|n>6jk~YHva;w zC7ixe{exiT4iI>lX%iRiy&3MhiA$ulQVO#mX|`?u60@cze8=x`^bm$T8_4EoBMjVo4Ji$uFiNlbvCpVhrY!xRuV)}#Md(>Xs>{zqFs z+jgDY$;MQZZQGM=pKMz*xyiOB+s0IrU6b8B=X>vc{)5ji=e_q@d#%?=#aYFq@)Zp( z;oeT|mE4s|Yxl5gf-itGJNz`3oJvWdVe!yG=P3r&OAof_&Ht)vA}Cr*tcV3bY4dpl zs_6uBYx-V?#U8b)tD%gBX)E@)W%lO&D~B9cIoB9d+YzW~iB$2GXw2Q7M1xFY*r6LAT|BLH}~vY?hP&p^x2!^aF(2 zgb6x&`)ab%f%r-}5Nr|Zg1%D^!%ewBIfd$f3il=)|rm^_kY`s=yj?d?-ABb*?A)Ci&~xq$^v zVOqNwo&D}d=>>{J;mbC6|D@pV@>kuSy=zeD6YbwDF)3e$-E6dK-xMp)XD4Zdp`F?i zp9mXfI$iX-S4M(yXIz$pC0IB%#BpG_`YKQB8FO8jR>j-GTnJ87vtB5|%uV?y7K^=@ zE~_(Y(h5v;=A& zrpBDy@Vl_hF!gU|sqOXGXj392EB8Mfy1cG{Qd zjbm%N)I$}~3aMJd(QK!`y_qirxPs)W<>}GZX$O(P@^84c+{(#&Jd=)5f=laUu0gT$ zmQKIBSf!?mT}h07hVjNV&!ihxzSoEXYcHqAp}GOhB>boyPN$55Jw?|yv|CpOqI&$h z-G4#nPy@&~J5jt2ZfuV0I()5hSw)l0`+twyC7r9<-5(R;uh+V6kDEq zIJgBmqo7lmGTe>n_SL@1B?#^My*&@-jL-cH8kE~tW)$a@s%?BKT1>7KSO(LvgPr(M zii0+Zl$K<(hgptullXa5P$c-~<#E|*mK`KfWso4TRqVdjG#+psi~DG#&OKS6qI&-W zdzxWE{3H&)EQzMQwQ=yX3PS5ojG!Vewu`FOciqRhOk42~rRiK9HydhX)6lj`n$> z&YS&qNSRYa5q&~}hteey*>$0=eCBs~hF8HdRLgkvPa{CLw;|V|k-nvyd!6T|#BlUV zs5})T=6WxiQyPuQo8l6Sr#_Z--||%xvfdR|9df^)v*;dAke2FREQ+B|F=F;=GB)~K z$+-~&O-Mfi$RebFr0>Zfj6I&Ve-=gKNhDda9ZhRW@XZFGQg1D)bD@m$3-@aclop}T za{q+P4O)_sq_i=EJa4Do_kr&C~&e&d9XqlfN!-cBu?E%SJXi(k`p0gy zpUzas+;)Ww{7J6UooL!o5W)&}aO0~W`L4M5EaPozLp}IfuqhX6SN`(nZZqHoDqan* zVfE1pWkJu}E?CnxnnJkTViu{Kvs5{^?OT2fm7p;_Z zCoT!Wskc$;7g0{FEo93lWBCqHHIzmvPq`@fNJ~bvz;7>foa?&W{AoNjuH-kV5N{-U zE^0LJB}fwp3yeVlY=j{)Md_;Vl-SuJVRqGBYIu37C1GPFtJoP(oKf7f^6IW5f>TWQ zr$99{apx6P`YczhcbwL9=kwloed!6J=55&7T%UEL0SWU@U? zHPZLa7+j&i@lFE&z93-)hwbT=$hXdC>x7hqVy6vCdx{nikrosp8XN<~r3(=YHp&9fG~0`KXZX%h zA$(X&$p|(CqyBqwL5@b@jAamrbYLA1v<7lwDUB)6ep%qC5_pymV<>m}(fZnGUlo!h&^M+MgJ;k$Se>_U$ z&_4Xb@N$rw2Y|^?@~;Gip8B``>m!jjZkHD+D>-TfZcpezYop=-3y11ptLqK){cs(C z2ORm0mHceqK{ZKX3b#x(zu5dG82|OLmxmwR+Wr4Lo1#mxlC_JqgO}B4bwJegK;av52a`Q<47A=-S@{I4)LwgkfS1T;^X>^ZgTfa#bZw2F!Bd>f;xo!*+E^y36A zYWBq)t9t@mIJgm!Xj@esEt9DT4j$B)baFX{BQOA%O&Ao&tpDY?}$0#s^0osDf}xZis{Z z(~aDz6{WB{Y`3~hYL+YEbp=3inkK-z z%d@L=7cJduV4gek65VA9qw{%NA^---&z>v9%85fb0xYkV7*H!!S*SO_`I?(>+?rcN zA0Gq)cU3v7=tY%W6qAdYpMzD~O`c5{+K}SNGn@wOuzS}7pHz_xZ*9qhT9DOCSeku7 zRka4UqOOMPh{wD&Hd5P+kRUDF#LkFdmn1hBGu>`RbKRhpz1#F@g~oSJtqw`xaT6Ti zZ!3}B;YKMbvG|ZkPI`zehUf040LsvJz>DHmW#ZlS z*!Z=sDNbaZlA8jhd94E(|Q=0^LD{#4*(l zax#lkD@7?7iX_&v0P05~pmP0X^M|xv47pS%eY=GHbGd8VFIgt!T*U?B61$ETS#(1_ zBd3{cnszOK5i8c26nM^YgyY+wkh=S^6{!F?&oEHYF4^04l0s+J4;}Yk0bvRx8M% z%q20DWBEeN5R@>HTp93Ffmb%#hP$CV{^D1XH`6QZhfm9nMq40w`NQ{cCa|Ws-c#| zo_2W;Q|tCzEHrs?S})EK{i!GST?a<7Wkg576VeU_)U44irCa%Rf4L_#O;Tn3gkGQ4 zX-D%UjiChXda1QYl;?|raPL$2slG@27Qrzm9db3s0S-|C-r-;8gwQF?7ZkP0NC($) zNFV@wodRQ<0MQ0rO$?p%7VE>7PYzjru;obW44=nDEC>XUGd*&tt z4910`$&CZ0^9In*@p7nPP*y;hMOe8#~gPZ%W<0B^D z3v|{T&((43k+!;!@Uk!vS9y!lnv znICyMmjuzF8UFe4J1r%11-rS*=%0>hz8t&OhA?hz+D$N<*T+S~XNVG@^S&UQx$NsI=IoaiWaA zbzSwNMHg7uhMfNT#HT-@sqnouw}4TVE5 zZ}l|ifJ@AzHL8_M?^r(`M?bxmEdgAtU#D6Z7I(dC|8&JvyJYOzIp3$x0UJj&n|m*U zy^Tgs@oGQa+rRF37zJ@n7W8vV*x0Mp&e-In&a|=>kCIRgnS?Dyq@X$`7x6D!BQj0Q z<}E2Y{9*f>4MKDp;ifyR&;B~p0ijlm&S z?z;4$hqTbgO>JjFEIiQ#B^ibs0AD3@*j|T*z6n{>8)Ba-fX+KnJb5U87P1NSU+?c@ zUz`~zZySuAN{5eYJl$79s?uyuo#*wl#Om=B4+^z|ZKAc&c#HHj!SE|%B$FT(@7(W+ zkU|87AXfbN=4q4IvPiw4^fE|o%5#kGUL%%qL#I6|3OK7@?Nd#K^Zl;1^1eOWk_3js zH?HkeUGG!)>;5#UB2LSW>VoZd2g%auU8b1v!$;LF|9y=` zE(tld<+wbnEYy7GsI-QZGOH5o%@&8)HLyzsc_q(xZf*&t8J{wU+kqS8_@ne1%FaD! z@yk^=FDocZT~{(aT{DM>DsMcDA;8`799(f=UMu_DC5x{gj33zNvmQazJS@qWiUzm{1r1|CpMgbc~TNw#sk zF!lmYduF>4on!%eTZz>$+7&dF&JB@XEELI$B1T3!RN>&~GZyg+-h&IorSH-n*(hBn zGpD9_7IRNG->;70C_|d&39K1LYf;cavV;P=%5UsFx8r->imYYuOYK@ikB_Ml!ej~+ zQ&&p(VHwZk<#p&^HRrU&V8Qqo*p?^c<5P62rN_w1utJ-jPo#pYR|toz$|Ou}_OgdL zuMQ|`RkRfbpCrTa%EfRosQBOy>Qa`-(OY##+?~s#I50+wRx!#93BjvvD+r|(Nh2J_ zC^Y7KS|TI`X0$X8ftDkOkb`Bpz%^TzL_1Q({vp@JJT~k?Ka#S^y;|`*&qWWM&TwJ@ zoYo&}_~34VoijWIl>yr->H0hRJtVCm?7YD|j-XgE6@uXp2mPLqEjJKQD|AIUpd~~8<9~2OT);)@XBh8wtI9%3O)9rVS%iV;ccf_i?rl5ozZHmc= zOX4s7WCU4b_OG_+5PU5&P>gI=ubdFbfvya{a0~@%3lKyNZD$yqu@%>?G`5kYjjGdZjAgu;R8&dS-wTym@ z=ijbj6jLTLH@jN?RV3ABze+D7Sm6Bq`qKiuu2HFK%s z!DV0eFUx(m^@O2sDCMilMf`Uu z-j)kSqVOp~u>kdhXmGp~EE`OHu&p$qD>fxfUJ|KfS}dR%zLcD~oq?Bu%oI4J4r!77 zQ3$67-f5*{R!t9Z#y{gkBpsSA0pj~ZZ(<1fK^6h@kK7_(CXApf8rzg5C#2lh1zyLp%`d@a4qjnz7eF?J)NVCL#+8zj=K~ zzOAQ^pM60e(O_x?0PY8;i`ZwAtnp4-Wo6$AvbrWv`)TIqkkAzYs)UVkTN-eiR#OwQ zQcSK?jC-zk6TG1Nx}#POLG$&m!>~{jm-zZ4X9|mJ78WnTjwK!iWS1!qKvgXZ8d+ysXxgVHK7dt6t!x8H80&ZKqc#=+8ATqbz^v=RoQZfJCh1# zfFH^mS1fG72tT~iI-A&rE4$)j5Q$$nb)qW}a7wa7tQ8yn_+a<_5??4+JZ+uR3>r{T zx%>O-aXn}Z=9Y+)TWI0i6>6qM=!!GyBRN$M|4CI|Na@uY?-9^RC)`ES3XOxGiCKOr zAdxQ+!JSiaPQ|J_z2g_=L~R-t;Y8l(z_-NO70{W%tQ$D-{$gA|z~4bDG}4NvQA%4b z7S3|Z^{sVY`Z}y7vF2(%rQmy=JE$IJl_9ik@pT`jJClC2RLe9aZyAqH;Qs>4kinyF zZl1rz8F3uu+KAo)y#u^}#~0x`$q0Jh;uedhNH{2+Of^`zs|1b%l8Lyrl^Ohx#l9LI zNibd6g=Yrs+2G@*E8)otbfiUuMdBpI&+%SOcNSY zcH@mu&^B^Eh**@0UDe3xy3tUK#!-0NOw}E?e1fkFX<4Mo4E$ZT!M4C9y0{;7X;DeE zqE$n<68bof2tjRco0~H}y}D61ZUIw7N={@6xC;T_hD`Cqpr-&gUfTqhHRxJ0RJ^B0 z=H`MAMycai29ZdiphAREo%5F<9+w4JHHX@`J3DWIE62xUH>b)6>WC4=Q9d6;Fk6pn z+$x9%t?=uOR?3xa>#E~MVv#x6J`6XqhpmOk?#8cch!U3bL14G)OeQBDh^@&gmSl+; z?xWFW6J`+tr3&FS1$rbBD*H~I@S75G-YRN_wU<;Y=E%^2Mtq5a>VLTRp8emRR-$gq zymnTiv(=Na*Z5<%A*a2XuzY(f5F8NiznvKjOM%3mcB?aT3|W3z-kebZN4ATtW|9CQ zv)xOEYncoz&1+DVqw3c*3{3scr{3D&eqdcM6z*^YZFW{Sv^Zpo%CXHtmdAYMOke<( zsNyKdPq{F5_Fy11>5uJR6^giKe+~YPN`}b3ra4NU2~Bwn))b=(YUAOyW=Q3nHiOE` z0rZB9Fc1v-3!vlO?DzE&yx;BNoI+(OXmaM@a>69lFR zSVT@#8-6Ely;@o!%B0_mzL&B5V_rD9rBk6vPO+0K=<%q}6p}(mT5{J9mIiTh)j{7l z$>B@qMpc{XJD4^t{5=+`xBcRDpyEZSogIl9(KCw3`@7oYfZD;8Dm5DAMGyY9VB`Dz zTHTj+eM6!~DRPExUnk^TM;E+*Qw;oJgQ>7v=UuEMl1aBoPe^M@xP}pNrzJF9*lyE1 z3H0(kF=Wh>m}_4qkY6RJHU?I|GY_8OdmLV4NRq-;j?7$g6fAw;gh)=k4z|{GmXfn> z(d{?_BOHz@-OCNa#`GJQ_^WWj$(Y!SyQAxTF9Zdop$y=GRCQJli%`WN`_uxp+GV0& zUB*v6tDj1g*5gm6deX{W+D?APT&9@K77A1Tt@FOQbf9b0 z)HUYz!a2uN7jFf{=r2X6+jc4w^bl*+ZVURp0rafOrgLZ!SLXzWXnV#vb$T}2=`fe# zzA)fQM~=`T;bww5mrDX1M$X(%dfMK>JOk~hnJ>bnMEtry4E2WFZcNt8f3%gvVjSdX z5Joqnlkpk4Un{^6kaJS*B%sIN)#QORP64G~(z!*$G>jg5iZP04Cl|i?v+EUkTmXnW z#sQQ$BXE)!y*(cmSymPQOVfw)182o8w^^n-pjSzL^d{kF<$;p*76+r%&I6ogW#O) z5nrBsx(pTTb^!Os_@tm50j9-RKv&x~eWp+yQ7fQVSHLfC*D~zTK)yR5oY?3X4c_5DC%$WXfcnvf@Y~UoF0%w zrb@qSxkguDS$HGs|5U=NLn|2~sxilip_izR!VWE+UN6j!xY++(?&fb?BOppnjn|N|YgP$YWbZ!J(h5G5)sV za55O7(}iGdsq9f$$lJQ|3+4h{!5lT~1x5|Dzktd2W-j-podDo;T0>?21zNYR`8(ty ze*R5ZhZRnX8^cK1X5u2(Q`BCDKg~Dz(DE5iCzgb@bG|NoQaa1+nZGB?$>Cfv=i1ik1yHY%#imfl&WTgz08A=&3I|^(D^0Y2&Kcg0s&|=;v=xgGHbvi1-g40Q2 z!;)O1aPJRoAJ*R3p?8I9DLVT(X!G3Y3oWTt#V5*1V91JAJ^gInJNo#>m8+!3(1!V| z9EjoB?VPNrjES`W=s#935HT;$LahpbVuHT^xus)S>ZW zhKInJnYxJYA4g(9RwRQ>y3iU<$HmJv^T$8$6+YZtG!-Ax^MG=Zgpje@M0!Nd_?_AQ zeu74in)00)WQBofOeqo;*ITGJ$fE=a;`&lw9w%h`8z(7$O7z^`c5iq#8Scyx&{CB> z9p<9&PStIUbm<$c=i)Ww$^o%1->wsMX1P5aD&c4cWGlo2M`8*~V?pom{;!|L&OI9@ zeE5xSG$B)(D*IEve&V=fv8-M=?Y~Z163aC`5#+})ODP?vxlx75B#Vq|o=Gl*2AxqL zo`+AQg&Bs~g){0j!MfM+WI5xSftoqEM&!~QJe_S=>!l4&PGK^y11`u6C?nvxIRfY@ zW1kxCl%?}%g@pWt6n{jnDbz1~5q02?J@_mW>K?R}F(-qmI{r#aPPe7Nw#}wO6r`}5 zEL0V|fS^JAQ(92{333uQ{HYJA)V}RpE~*q*`ORVM4~tPlj$nUFwh7!W637UyR4(g;I$urnzpB%v8(r`EE(-_(SYspsYQg`hBBOw(QdxhzDC9LJuQX2aUKu@FsU8(n( z(>W%c5c*$7c%)qCMq%151_<)Aes^itVe|0H-|@j0FSDS+j~yBh@BNHbj# zgedemLU4(&$;&GWk519>z~cg*x6Z}sfx+>ncWR2Fj>&!!uxpetz-w z`%xNUTZyew=Bc(kFSf9(@%in;!OqwF zc{TQaqydS%s>Ue_So)lVS~&2xaP1hgq;{2=^}ne=)+=*Yx>Cgi#HERjs}7PSDsc*Py+kc=k=4 zYtw2~Ku8f)p^_clX)jkOQf2nn~#X5atQr0D0%%b7*jGG!zF8ZwhH$~GjUstkh(g?)OF>#~7cHC5T$%+J+v zUeM_f)MU-`=ITM50(IF1!6SQl$1Xwv_caxo=QlM7ARt9b6%S1L>A=(<~$&Wh#S=15-Bov#o6$)0=bP z{k`UfC-DQr&3DysNUS1Du8hQu`f~0XWB9fnl1Fz`hRGYhawr-&-Vb5ct)f1_3UXL# z_QWsmPWTpMi#IlfUvRU^R-sl?OugDEa#*C}RiGCVvpwK+yMJmq(4}%Xg;FJXq?^uY zwIG+vM-8FQTL0Ve8?`5`j)Ej6)&a~0y(;M~_48zd<+<+fIJT265@I=3JePWviMPOV zU;Lw}tA@2sD5EkG%~hm-FLQy0a-sIH~IgeTrPe;_lup1 zGy0)q{NYzI^K6b?4PjuO8whRZtJXGTm)TC)MpKEldpxD#dH=CrHV~O4P}DOA%kWcr zeRMeU`PqTLXVf*C^CyUSl7`;T`9dui+ZS1u-#p!XUN~r!`r$N}k4J|g_Sr=~oq`>D zi_X2exD`TcR8C7x+&Y@VO>^;Cz1@jp2v8hQfv;1`@s~^|);>IvSZDFt__J|9;g+#l ze2Xob+VEiJtS;`W?#q+yU>C@lebZJKgR!&}2ZupX z?YJ>v;zc=i8C1v9eGUGnx@-~IkKj~-%iL0%v5>{-F4|dlCt4=uGbf!MCX8s-2s=Y% zGumYI*r1%o`>f#@ed^cnzgxl9h`X$mhh7ggKikmo8Mtuvq&|BfZ2JjVe-JE0Q!7Ly;LUobROS#0q0ug$p4}p}#E) zTXj;hGt{qK3bi@9@c8GrxC1X0%8s7I#xC3%RN=E_coE2e#EJPJkFkJK8^UIZpg^Zn zgkH}g@cZG;b6)5i$LJ+SGSsTatQ zBCEyB=}Ph*;-WSB$EX4LN?PnoA7v!*0)kyUYAW*MHAzu~E?YmBI0!Tl)6L0fe7kqs zn>zWOmx69$0P>JBF?nlQ($TrbirZQ}zf~*r5351Xs5LK%vrd*>z$K9^q<^_d|5h|U zq`n7T!DP1evF}1D!~+=9v0!SmpSDZ483apuymCGcJUCdrm~bM0o(}5Jnd2af*;DIh ziBkKD_{dEEOT_sHoT@P^Tfq>M)%A%K0q$=*`cfRqYr?5f9BTykPb6_L*eYkKk(Hj& zMa+OKb<9E}f4j}xx*b3Xh-n=yNBrIhH8ObtS`jnN|0z^?&NooyXWHf}N7HpUgG&FR zPpIOeHmmY2t^78z9F-j<_u`AYXX6c{$m4@TNh<&gnW;|ClG((kM9e(1vVw-GMf0%!nN2ce zk-X_kpndCc{ zT#aI-qujsg#%ibiV6;|g-sR6vX*YB881k0?m^nTTq~WPXUwxFkpeR3l zC(?7(qh9sKV2j+SXMmR5-~P}(Sq3F2ec(k1ky(9Lwf;4ok!BXfFFpk-3a=e=LBoc; zfXd)RQmn`8eCyH`=)pAYqg|r$rQSN2qAoZl z*09ovQd?hhA}V@3p{DZBnfriI^j5L3#H!0fmm>=E=3o2XU$!Cazu=}IeLEn+Y~jH_ zaJd#Sexo?5uKu-s(;xy0TH*^At>J*Kx2h66MWf-_K=5@yRA0<~go(SwpIySU z&Z|&OhOJhIz}0xUXl|Z|&SWWygo&HG&5AN}=4BSxMpObiYTEL0hHM*eQvAfbx7nj0 zESNA#H%v(2YFvx~=aNgKx;#q%R`!W)xQ`8#OLLZNSCM9fJ&z;i%LmuTbiQK!M#wqI z=a*^xpr)l@Eg$|+B^CbSxiH)K8itFoG)@S`5-l4M189(Q;NAVY{c5#_+7rYiaVVaS zeEJgDCByO|X8KdjViTL4;g6cma@XE#gI?dwKl#W66<72V=5`3(Pf0+kA*J8f+yB{( zMa!(;$~vp@d*=9h0B65{r%7iLfPGr*2<=U4x?Ht3a`Yus_J0&h3?82dkUvvx^M~1d zx%Q9}$hxC9+-u&pgSy;69y>x0=LPQ}x$|w*Idt-u$)BN#HC7#qP^QQJaY;gjJiN2F z2Z)Im#AcG1$=n4sU^wG(8F$)#Qt=VgQs(+9+Yr<_v82JQrr`?8_;n z5_+(ZA}Me+1TN2tKiC78Md?>gwY)6MGBF@CD_`FEJ`j&>Lt$5mu;BJ|k6D$!S~7yB zcH*8%b_SJ#UN}|_`*H+DGc^Vt<)S8F8v$U-C%WIujeEAB`s*YT75yqd>pCh&UFue7 z+#qJ90zWmOlX@pQ&_de4(T{Hy#M`}L;?a`Pk+6x#VkNU3dVbX~j(O8w`0)7m={@nl zGAvj-BB>2qUqM$~wG-0?!K8xRWCrx^fnG0(&|6YRp7?1tmf5`)@>DoLARY|I@iz(T zU(4}R@zFo96ghWaS6L>?k{Y|hw(^mbkn6GB=$Z9!t6aa1Gz-y0SVowm{8x@zL^=D_ zB1Nj+S9qEX>>$<)@G`~?$-9C+KmQgFN>s3R7{{~eW62xTu0y=tdGP3gdO?9$<$=8l zPfR?!Zb2TWY@P>$XJ!ec%xYwNxRvXN30WqOFqv7w%HVnRB?D0l4;)oQX#m_Ul;jx@ z4*zCNOtl$2oTqn@2sF<5iJO&BRv;4p`@E!{-flV3)Zc;bzH}OH11f?^)wj7^D>!laHu;3emf26eqXU4Uli1L)cP)WRQLf+M( zx4)E0ojROD#6|NboCE5VJ+C}wt^sjE7)tH}A8E=&py*2R_+jLQ5-2~G<0i8)Std<& zjWgEtV{vkS5o%~}+(=uukkK33?z3~^a0X7QfZDcRm)02y=kT?y^Tao-5n=NTH2Rmr z-%iaLH6-zS(ybaBa)pNyeN16fW&z7lD!RjFjcO;4PEs=jiJFCRzg_`WW22wAl9~ua zuu!#i?vc;fBNy220uhJf>AVwG|F2SY2(-(J>K{BzybA@{Zoe?&5LY{{a>!8%(e{5g zwVn<}ZbOoJ5YVb1IV7R^*np;^Gp~0B)|3IEJHyx0i!gB`FDZ?Fkdu+WJJrtEw-B1T zr~=dEVp}xJ^kQVL=utU%JU-`>axu^8s@-AjAJx*b{(^*cWD&&U!=bU<#wLU!i@4hp zeEcr|mRzGqo=fJ7c-}()e9X(0S-!A4F%8`jDsb&ght&+3>WKM3)GyX(T9{sVMcwML z-ABnJvntGM-ZlFXu2+%194x@M*wvAt6Z|WK2)_Nfla5`!~287=jva} zh-F%4S-2j3rYUNAAR3ST;#DKWBF77KnF=LdT5!~qgqSWi8OMPqtX(S{jJ`<#COdXa zv$Aa7u*m5Ls%a^)M(XmzdXQIeh@M41x5}6pStY3PlqkY|r>z={6BCvD&Tq##Fm$CtwKS`cizkS5mV9%oi1H}jrXjS9uVf1PoF>q`z;{Mu`yECA$;8ifO-lNJ) z9M^%_)FLIShe0Y+F#sOzSa45R$S z$tuXHsjI}k|CL#-Ta4S!Tu*_mPfw!R61i{j9zu{1l3uT!fnADFow3CJwgeFHwOh<1 zhnco<-NYFVli8udv@k2y^MeYlxFq0@>stowsvMMK-B&Ag{(4wEFXKbU`N7S$RVU@d zrmvB_j7(Sc13}cDFjL1yPmp%mly}TWH$@`C7l-tdOn|Ad{5k4Mu4sl(Qu24{u)i=l3fl{G(w5S zfbs8RMKow3GBeBM8?#G>ns|KHP>*m%3)(J5(-%E0ZNgZ=T`Ow>hV;s*ozESvqEAHp NJ4CM=j|g~({|Afl)R_PP diff --git a/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json b/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json index 3434e1f80a7ce..552142d3b190a 100644 --- a/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json +++ b/x-pack/test/functional/es_archives/spaces/copy_saved_objects/data.json @@ -126,7 +126,7 @@ "title": "Dashboard Foo", "hits": 0, "description": "", - "panelsJSON": "[{}]", + "panelsJSON": "[]", "optionsJSON": "{}", "version": 1, "timeRestore": false, @@ -156,7 +156,7 @@ "title": "Dashboard Bar", "hits": 0, "description": "", - "panelsJSON": "[{}]", + "panelsJSON": "[]", "optionsJSON": "{}", "version": 1, "timeRestore": false, diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json index ef08d69324210..7f416c26cc9aa 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json @@ -11,6 +11,12 @@ "title": "[Logs Sample] Overview ECS", "version": 1 }, + "references": [ + { "id": "sample_visualization", "name": "panel_0", "type": "visualization" }, + { "id": "sample_search", "name": "panel_1", "type": "search" }, + { "id": "sample_search", "name": "panel_2", "type": "search" }, + { "id": "sample_visualization", "name": "panel_3", "type": "visualization" } + ], "id": "sample_dashboard", "type": "dashboard" -} \ No newline at end of file +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard2.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard2.json index 7ea63c5d444ba..c99506fec3cf5 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard2.json +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard2.json @@ -11,6 +11,12 @@ "title": "[Logs Sample2] Overview ECS", "version": 1 }, + "references": [ + { "id": "sample_visualization", "name": "panel_0", "type": "visualization" }, + { "id": "sample_search", "name": "panel_1", "type": "search" }, + { "id": "sample_search", "name": "panel_2", "type": "search" }, + { "id": "sample_visualization", "name": "panel_3", "type": "visualization" } + ], "id": "sample_dashboard2", "type": "dashboard" -} \ No newline at end of file +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json index ef08d69324210..4513c07f27786 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json @@ -11,6 +11,12 @@ "title": "[Logs Sample] Overview ECS", "version": 1 }, + "references": [ + { "id": "sample_visualization", "name": "panel_0", "type": "visualization" }, + { "id": "sample_search2", "name": "panel_1", "type": "search" }, + { "id": "sample_search2", "name": "panel_2", "type": "search" }, + { "id": "sample_visualization", "name": "panel_3", "type": "visualization" } + ], "id": "sample_dashboard", "type": "dashboard" -} \ No newline at end of file +} From bd99b19bd81a635da4a81de2cfd7f36ccca24014 Mon Sep 17 00:00:00 2001 From: Constance Date: Thu, 12 Nov 2020 07:42:41 -0800 Subject: [PATCH 11/17] [App Search] Version documentation links (#83245) * Fix CURRENT_MAJOR_VERSION for use in Elastic docs links - Was previously just sending (e.g.) "7". instead of "7.9" * Add App Search DOCS_PREFIX constant - follow WS's example * Update all App Search doc links to use prefixed URLs - except for Enterprise Search setup guide, which should be updated to use a shared URL at some point in any case --- x-pack/plugins/enterprise_search/common/version.ts | 2 +- .../app_search/components/credentials/constants.ts | 3 ++- .../settings/log_retention/log_retention_panel.tsx | 7 +++---- .../app_search/components/setup_guide/setup_guide.tsx | 5 +++-- .../public/applications/app_search/routes.ts | 4 ++++ 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/enterprise_search/common/version.ts b/x-pack/plugins/enterprise_search/common/version.ts index e29ad8a9f866b..c23b05f7cdb3d 100644 --- a/x-pack/plugins/enterprise_search/common/version.ts +++ b/x-pack/plugins/enterprise_search/common/version.ts @@ -8,4 +8,4 @@ import { SemVer } from 'semver'; import pkg from '../../../../package.json'; export const CURRENT_VERSION = new SemVer(pkg.version as string); -export const CURRENT_MAJOR_VERSION = CURRENT_VERSION.major; +export const CURRENT_MAJOR_VERSION = `${CURRENT_VERSION.major}.${CURRENT_VERSION.minor}`; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts index ea4906ec08946..2b96e3322cd55 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { DOCS_PREFIX } from '../../routes'; export const CREDENTIALS_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.credentials.title', @@ -100,4 +101,4 @@ export const TOKEN_TYPE_INFO = [ export const FLYOUT_ARIA_LABEL_ID = 'credentialsFlyoutTitle'; -export const DOCS_HREF = 'https://www.elastic.co/guide/en/app-search/current/authentication.html'; +export const DOCS_HREF = `${DOCS_PREFIX}/authentication.html`; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx index 23572074b3c69..3297f0df4d7bd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx @@ -10,6 +10,8 @@ import { i18n } from '@kbn/i18n'; import { EuiLink, EuiSpacer, EuiSwitch, EuiText, EuiTextColor, EuiTitle } from '@elastic/eui'; import { useActions, useValues } from 'kea'; +import { DOCS_PREFIX } from '../../../routes'; + import { LogRetentionLogic } from './log_retention_logic'; import { AnalyticsLogRetentionMessage, ApiLogRetentionMessage } from './messaging'; import { LogRetentionOptions } from './types'; @@ -41,10 +43,7 @@ export const LogRetentionPanel: React.FC = () => { {i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.description', { defaultMessage: 'Manage the default write settings for API Logs and Analytics.', })}{' '} - + {i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.learnMore', { defaultMessage: 'Learn more about retention settings.', })} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx index 60d7f6951a478..b3faa73dfaed6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx @@ -13,14 +13,15 @@ import { APP_SEARCH_PLUGIN } from '../../../../../common/constants'; import { SetupGuide as SetupGuideLayout } from '../../../shared/setup_guide'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; +import { DOCS_PREFIX } from '../../routes'; import GettingStarted from './assets/getting_started.png'; export const SetupGuide: React.FC = () => ( Date: Thu, 12 Nov 2020 16:49:47 +0100 Subject: [PATCH 12/17] [Lens] Add suffix formatter (#82852) --- .../indexpattern_datasource/format_column.ts | 16 +++++- .../public/indexpattern_datasource/index.ts | 2 + .../indexpattern_datasource/indexpattern.tsx | 1 + .../suffix_formatter.test.ts | 28 ++++++++++ .../suffix_formatter.ts | 51 +++++++++++++++++++ .../indexpattern_datasource/time_scale.ts | 2 +- 6 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.test.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.ts diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/format_column.ts b/x-pack/plugins/lens/public/indexpattern_datasource/format_column.ts index 3666528f43166..1f337298a03ad 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/format_column.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/format_column.ts @@ -116,9 +116,18 @@ export const formatColumn: ExpressionFunctionDefinition< }); } if (parentFormatParams) { - const innerParams = (col.meta.params?.params as Record) ?? {}; + // if original format is already a nested one, we are just replacing the wrapper params + // otherwise wrapping it inside parentFormatId/parentFormatParams + const isNested = isNestedFormat(col.meta.params); + const innerParams = isNested + ? col.meta.params?.params + : { id: col.meta.params?.id, params: col.meta.params?.params }; + + const formatId = isNested ? col.meta.params?.id : parentFormatId; + return withParams(col, { ...col.meta.params, + id: formatId, params: { ...innerParams, ...parentFormatParams, @@ -132,6 +141,11 @@ export const formatColumn: ExpressionFunctionDefinition< }, }; +function isNestedFormat(params: DatatableColumn['meta']['params']) { + // if there is a nested params object with an id, it's a nested format + return !!params?.params?.id; +} + function withParams(col: DatatableColumn, params: Record) { return { ...col, meta: { ...col.meta, params } }; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts index 92280b0fb6ce6..793f3387e707d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts @@ -38,8 +38,10 @@ export class IndexPatternDatasource { renameColumns, formatColumn, getTimeScaleFunction, + getSuffixFormatter, } = await import('../async_services'); return core.getStartServices().then(([coreStart, { data }]) => { + data.fieldFormats.register([getSuffixFormatter(data.fieldFormats.deserialize)]); expressions.registerFunction(getTimeScaleFunction(data)); expressions.registerFunction(renameColumns); expressions.registerFunction(formatColumn); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index e37c31559cd0c..94f240058d618 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -78,6 +78,7 @@ export function columnToOperation(column: IndexPatternColumn, uniqueLabel?: stri export * from './rename_columns'; export * from './format_column'; export * from './time_scale'; +export * from './suffix_formatter'; export function getIndexPatternDatasource({ core, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.test.ts new file mode 100644 index 0000000000000..ef1739e4424fa --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.test.ts @@ -0,0 +1,28 @@ +/* + * 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 { FormatFactory } from '../types'; +import { getSuffixFormatter } from './suffix_formatter'; + +describe('suffix formatter', () => { + it('should call nested formatter and apply suffix', () => { + const convertMock = jest.fn((x) => x); + const formatFactory = jest.fn(() => ({ convert: convertMock })); + const SuffixFormatter = getSuffixFormatter((formatFactory as unknown) as FormatFactory); + const nestedParams = { abc: 123 }; + const formatterInstance = new SuffixFormatter({ + unit: 'h', + id: 'nestedFormatter', + params: nestedParams, + }); + + const result = formatterInstance.convert(12345); + + expect(result).toEqual('12345/h'); + expect(convertMock).toHaveBeenCalledWith(12345); + expect(formatFactory).toHaveBeenCalledWith({ id: 'nestedFormatter', params: nestedParams }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.ts b/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.ts new file mode 100644 index 0000000000000..5594976738efe --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { FieldFormat, KBN_FIELD_TYPES } from '../../../../../src/plugins/data/public'; +import { FormatFactory } from '../types'; +import { TimeScaleUnit } from './time_scale'; + +const unitSuffixes: Record = { + s: i18n.translate('xpack.lens.fieldFormats.suffix.s', { defaultMessage: '/h' }), + m: i18n.translate('xpack.lens.fieldFormats.suffix.m', { defaultMessage: '/m' }), + h: i18n.translate('xpack.lens.fieldFormats.suffix.h', { defaultMessage: '/h' }), + d: i18n.translate('xpack.lens.fieldFormats.suffix.d', { defaultMessage: '/d' }), +}; + +export function getSuffixFormatter(formatFactory: FormatFactory) { + return class SuffixFormatter extends FieldFormat { + static id = 'suffix'; + static title = i18n.translate('xpack.lens.fieldFormats.suffix.title', { + defaultMessage: 'Suffix', + }); + static fieldType = KBN_FIELD_TYPES.NUMBER; + allowsNumericalAggregations = true; + + getParamDefaults() { + return { + unit: undefined, + nestedParams: {}, + }; + } + + textConvert = (val: unknown) => { + const unit = this.param('unit') as TimeScaleUnit | undefined; + const suffix = unit ? unitSuffixes[unit] : undefined; + const nestedFormatter = this.param('id'); + const nestedParams = this.param('params'); + + const formattedValue = formatFactory({ id: nestedFormatter, params: nestedParams }).convert( + val + ); + + if (suffix) { + return `${formattedValue}${suffix}`; + } + return formattedValue; + }; + }; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts b/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts index 7a4e8f6bc0638..06ff8058b1d09 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/time_scale.ts @@ -11,7 +11,7 @@ import { DataPublicPluginStart } from 'src/plugins/data/public'; import { search } from '../../../../../src/plugins/data/public'; import { buildResultColumns } from '../../../../../src/plugins/expressions/common'; -type TimeScaleUnit = 's' | 'm' | 'h' | 'd'; +export type TimeScaleUnit = 's' | 'm' | 'h' | 'd'; export interface TimeScaleArgs { dateColumnId: string; From 4932dc55a6f4e97690a5b2d659eb0a854d65cf17 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Thu, 12 Nov 2020 08:58:05 -0700 Subject: [PATCH 13/17] [Reporting] Move "common" types and constants to allow cross-plugin integration (#83198) --- x-pack/plugins/reporting/common/constants.ts | 41 +++++- x-pack/plugins/reporting/common/index.ts | 9 ++ x-pack/plugins/reporting/common/poller.ts | 11 +- x-pack/plugins/reporting/common/types.ts | 127 +++++++++++++++--- x-pack/plugins/reporting/constants.ts | 39 ------ .../buttons/report_download_button.tsx | 4 +- .../buttons/report_error_button.tsx | 4 +- .../components/buttons/report_info_button.tsx | 2 +- .../reporting/public/components/index.ts | 1 + .../public/components/job_download_button.tsx | 3 +- .../public/components/job_failure.tsx | 2 +- .../public/components/job_success.tsx | 3 +- .../components/job_warning_formulas.tsx | 3 +- .../components/job_warning_max_size.tsx | 3 +- .../public/components/report_listing.tsx | 2 +- .../components/reporting_panel_content.tsx | 7 +- x-pack/plugins/reporting/public/index.ts | 34 ++--- .../lib/job_completion_notifications.ts | 2 +- .../public/lib/reporting_api_client.ts | 12 +- .../public/lib/stream_handler.test.ts | 3 +- .../reporting/public/lib/stream_handler.ts | 14 +- .../panel_actions/get_csv_panel_action.tsx | 16 +-- x-pack/plugins/reporting/public/plugin.ts | 32 ++++- .../server/export_types/csv/create_job.ts | 2 +- .../server/export_types/csv/index.ts | 2 +- .../csv_from_savedobject/index.ts | 2 +- .../csv_from_savedobject/metadata.ts | 2 +- .../export_types/png/create_job/index.ts | 2 +- .../printable_pdf/create_job/index.ts | 2 +- .../server/lib/layouts/create_layout.ts | 5 +- .../reporting/server/lib/layouts/index.ts | 54 ++------ .../server/lib/layouts/preserve_layout.ts | 15 +-- .../server/lib/layouts/print_layout.ts | 14 +- .../reporting/server/lib/store/index.ts | 3 +- .../reporting/server/lib/store/report.ts | 51 +------ .../reporting/server/lib/tasks/index.ts | 11 +- .../generate_from_savedobject_immediate.ts | 4 +- .../create_mock_layoutinstance.ts | 5 +- x-pack/plugins/reporting/server/types.ts | 13 +- 39 files changed, 282 insertions(+), 279 deletions(-) delete mode 100644 x-pack/plugins/reporting/constants.ts diff --git a/x-pack/plugins/reporting/common/constants.ts b/x-pack/plugins/reporting/common/constants.ts index 07a239494da23..16e40bab65a46 100644 --- a/x-pack/plugins/reporting/common/constants.ts +++ b/x-pack/plugins/reporting/common/constants.ts @@ -11,13 +11,6 @@ export const BROWSER_TYPE = 'chromium'; export const JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY = 'xpack.reporting.jobCompletionNotifications'; -export const API_BASE_URL = '/api/reporting'; // "Generation URL" from share menu -export const API_BASE_URL_V1 = '/api/reporting/v1'; // -export const API_BASE_GENERATE_V1 = `${API_BASE_URL_V1}/generate`; -export const API_LIST_URL = '/api/reporting/jobs'; -export const API_GENERATE_IMMEDIATE = `${API_BASE_URL_V1}/generate/immediate/csv/saved-object`; -export const API_DIAGNOSE_URL = `${API_BASE_URL}/diagnose`; - export const CONTENT_TYPE_CSV = 'text/csv'; export const CSV_REPORTING_ACTION = 'downloadCsvReport'; export const CSV_BOM_CHARS = '\ufeff'; @@ -57,15 +50,49 @@ export const UI_SETTINGS_CUSTOM_PDF_LOGO = 'xpackReporting:customPdfLogo'; export const UI_SETTINGS_CSV_SEPARATOR = 'csv:separator'; export const UI_SETTINGS_CSV_QUOTE_VALUES = 'csv:quoteValues'; +export const LAYOUT_TYPES = { + PRESERVE_LAYOUT: 'preserve_layout', + PRINT: 'print', +}; + +// Export Type Definitions +export const CSV_REPORT_TYPE = 'CSV'; +export const PDF_REPORT_TYPE = 'printablePdf'; +export const PNG_REPORT_TYPE = 'PNG'; + export const PDF_JOB_TYPE = 'printable_pdf'; export const PNG_JOB_TYPE = 'PNG'; export const CSV_JOB_TYPE = 'csv'; export const CSV_FROM_SAVEDOBJECT_JOB_TYPE = 'csv_from_savedobject'; export const USES_HEADLESS_JOB_TYPES = [PDF_JOB_TYPE, PNG_JOB_TYPE]; +// Licenses export const LICENSE_TYPE_TRIAL = 'trial'; export const LICENSE_TYPE_BASIC = 'basic'; export const LICENSE_TYPE_STANDARD = 'standard'; export const LICENSE_TYPE_GOLD = 'gold'; export const LICENSE_TYPE_PLATINUM = 'platinum'; export const LICENSE_TYPE_ENTERPRISE = 'enterprise'; + +// Routes +export const API_BASE_URL = '/api/reporting'; // "Generation URL" from share menu +export const API_BASE_GENERATE = `${API_BASE_URL}/generate`; +export const API_LIST_URL = `${API_BASE_URL}/jobs`; +export const API_DIAGNOSE_URL = `${API_BASE_URL}/diagnose`; + +// hacky endpoint +export const API_BASE_URL_V1 = '/api/reporting/v1'; // +export const API_GENERATE_IMMEDIATE = `${API_BASE_URL_V1}/generate/immediate/csv/saved-object`; + +// Management UI route +export const REPORTING_MANAGEMENT_HOME = '/app/management/insightsAndAlerting/reporting'; + +// Statuses +export enum JOB_STATUSES { + PENDING = 'pending', + PROCESSING = 'processing', + COMPLETED = 'completed', + FAILED = 'failed', + CANCELLED = 'cancelled', + WARNINGS = 'completed_with_warnings', +} diff --git a/x-pack/plugins/reporting/common/index.ts b/x-pack/plugins/reporting/common/index.ts index cda8934fc8bf6..0be6ab6682774 100644 --- a/x-pack/plugins/reporting/common/index.ts +++ b/x-pack/plugins/reporting/common/index.ts @@ -4,5 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { LayoutSelectorDictionary } from './types'; + export { CancellationToken } from './cancellation_token'; export { Poller } from './poller'; + +export const getDefaultLayoutSelectors = (): LayoutSelectorDictionary => ({ + screenshot: '[data-shared-items-container]', + renderComplete: '[data-shared-item]', + itemsCountAttribute: 'data-shared-items-count', + timefilterDurationAttribute: 'data-shared-timefilter-duration', +}); diff --git a/x-pack/plugins/reporting/common/poller.ts b/x-pack/plugins/reporting/common/poller.ts index 2127a876f4a27..017dbac13e29b 100644 --- a/x-pack/plugins/reporting/common/poller.ts +++ b/x-pack/plugins/reporting/common/poller.ts @@ -5,7 +5,16 @@ */ import _ from 'lodash'; -import { PollerOptions } from './types'; + +interface PollerOptions { + functionToPoll: () => Promise; + pollFrequencyInMillis: number; + trailing?: boolean; + continuePollingOnError?: boolean; + pollFrequencyErrorMultiplier?: number; + successFunction?: (...args: any) => any; + errorFunction?: (error: Error) => any; +} // @TODO Maybe move to observables someday export class Poller { diff --git a/x-pack/plugins/reporting/common/types.ts b/x-pack/plugins/reporting/common/types.ts index 24c126bfe0571..abd0bee7fb6ea 100644 --- a/x-pack/plugins/reporting/common/types.ts +++ b/x-pack/plugins/reporting/common/types.ts @@ -4,15 +4,94 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { ReportingConfigType } from '../server/config'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutParams } from '../server/lib/layouts'; -export { LayoutParams }; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { ReportDocument, ReportSource } from '../server/lib/store/report'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { BaseParams } from '../server/types'; +export interface PageSizeParams { + pageMarginTop: number; + pageMarginBottom: number; + pageMarginWidth: number; + tableBorderWidth: number; + headingHeight: number; + subheadingHeight: number; +} + +export interface LayoutSelectorDictionary { + screenshot: string; + renderComplete: string; + itemsCountAttribute: string; + timefilterDurationAttribute: string; +} + +export interface PdfImageSize { + width: number; + height?: number; +} + +export interface Size { + width: number; + height: number; +} + +export interface LayoutParams { + id: string; + dimensions?: Size; + selectors?: LayoutSelectorDictionary; +} + +export interface ReportDocumentHead { + _id: string; + _index: string; + _seq_no: unknown; + _primary_term: unknown; +} + +export interface TaskRunResult { + content_type: string | null; + content: string | null; + csv_contains_formulas?: boolean; + size: number; + max_size_reached?: boolean; + warnings?: string[]; +} + +export interface ReportSource { + jobtype: string; + kibana_name: string; + kibana_id: string; + created_by: string | false; + payload: { + headers: string; // encrypted headers + browserTimezone?: string; // may use timezone from advanced settings + objectType: string; + title: string; + layout?: LayoutParams; + }; + meta: { objectType: string; layout?: string }; + browser_type: string; + max_attempts: number; + timeout: number; + + status: JobStatus; + attempts: number; + output: TaskRunResult | null; + started_at?: string; + completed_at?: string; + created_at: string; + priority?: number; + process_expiration?: string; +} + +/* + * The document created by Reporting to store in the .reporting index + */ +export interface ReportDocument extends ReportDocumentHead { + _source: ReportSource; +} + +export interface BaseParams { + browserTimezone?: string; // browserTimezone is optional: it is not in old POST URLs that were generated prior to being added to this interface + layout?: LayoutParams; + objectType: string; + title: string; +} export type JobId = string; export type JobStatus = @@ -59,18 +138,28 @@ export interface ReportApiJSON { status: string; } -export interface PollerOptions { - functionToPoll: () => Promise; - pollFrequencyInMillis: number; - trailing?: boolean; - continuePollingOnError?: boolean; - pollFrequencyErrorMultiplier?: number; - successFunction?: (...args: any) => any; - errorFunction?: (error: Error) => any; -} - export interface LicenseCheckResults { enableLinks: boolean; showLinks: boolean; message: string; } + +export interface JobSummary { + id: JobId; + status: JobStatus; + title: string; + jobtype: string; + maxSizeReached?: boolean; + csvContainsFormulas?: boolean; +} + +export interface JobSummarySet { + completed: JobSummary[]; + failed: JobSummary[]; +} + +type DownloadLink = string; +export type DownloadReportFn = (jobId: JobId) => DownloadLink; + +type ManagementLink = string; +export type ManagementLinkFn = () => ManagementLink; diff --git a/x-pack/plugins/reporting/constants.ts b/x-pack/plugins/reporting/constants.ts deleted file mode 100644 index 772c52dde4a15..0000000000000 --- a/x-pack/plugins/reporting/constants.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY = - 'xpack.reporting.jobCompletionNotifications'; - -// Routes -export const API_BASE_URL = '/api/reporting'; -export const API_LIST_URL = `${API_BASE_URL}/jobs`; -export const API_BASE_GENERATE = `${API_BASE_URL}/generate`; -export const API_GENERATE_IMMEDIATE = `${API_BASE_URL}/v1/generate/immediate/csv/saved-object`; -export const REPORTING_MANAGEMENT_HOME = '/app/management/insightsAndAlerting/reporting'; - -// Statuses -export const JOB_STATUS_FAILED = 'failed'; -export const JOB_STATUS_COMPLETED = 'completed'; -export const JOB_STATUS_WARNINGS = 'completed_with_warnings'; - -export enum JobStatuses { - PENDING = 'pending', - PROCESSING = 'processing', - COMPLETED = 'completed', - FAILED = 'failed', - CANCELLED = 'cancelled', - WARNINGS = 'completed_with_warnings', -} - -// Types -export const PDF_JOB_TYPE = 'printable_pdf'; -export const PNG_JOB_TYPE = 'PNG'; -export const CSV_JOB_TYPE = 'csv'; -export const CSV_FROM_SAVEDOBJECT_JOB_TYPE = 'csv_from_savedobject'; -export const USES_HEADLESS_JOB_TYPES = [PDF_JOB_TYPE, PNG_JOB_TYPE]; - -// Actions -export const CSV_REPORTING_ACTION = 'downloadCsvReport'; diff --git a/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx index 6c13264ebcb1f..4bd86d15949e8 100644 --- a/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/report_download_button.tsx @@ -6,7 +6,7 @@ import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import React, { FunctionComponent } from 'react'; -import { JobStatuses } from '../../../constants'; +import { JOB_STATUSES } from '../../../common/constants'; import { Job as ListingJob, Props as ListingProps } from '../report_listing'; type Props = { record: ListingJob } & ListingProps; @@ -14,7 +14,7 @@ type Props = { record: ListingJob } & ListingProps; export const ReportDownloadButton: FunctionComponent = (props: Props) => { const { record, apiClient, intl } = props; - if (record.status !== JobStatuses.COMPLETED && record.status !== JobStatuses.WARNINGS) { + if (record.status !== JOB_STATUSES.COMPLETED && record.status !== JOB_STATUSES.WARNINGS) { return null; } diff --git a/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx index 4eee86cd79ce7..2864802f843f4 100644 --- a/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/report_error_button.tsx @@ -7,7 +7,7 @@ import { EuiButtonIcon, EuiCallOut, EuiPopover } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { JobStatuses } from '../../../constants'; +import { JOB_STATUSES } from '../../../common/constants'; import { JobContent, ReportingAPIClient } from '../../lib/reporting_api_client'; import { Job as ListingJob } from '../report_listing'; @@ -43,7 +43,7 @@ class ReportErrorButtonUi extends Component { public render() { const { record, intl } = this.props; - if (record.status !== JobStatuses.FAILED) { + if (record.status !== JOB_STATUSES.FAILED) { return null; } diff --git a/x-pack/plugins/reporting/public/components/buttons/report_info_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_info_button.tsx index 068cb7d44b0a1..0e249f156f587 100644 --- a/x-pack/plugins/reporting/public/components/buttons/report_info_button.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/report_info_button.tsx @@ -17,8 +17,8 @@ import { } from '@elastic/eui'; import { get } from 'lodash'; import React, { Component, Fragment } from 'react'; +import { USES_HEADLESS_JOB_TYPES } from '../../../common/constants'; import { ReportApiJSON } from '../../../common/types'; -import { USES_HEADLESS_JOB_TYPES } from '../../../constants'; import { ReportingAPIClient } from '../../lib/reporting_api_client'; interface Props { diff --git a/x-pack/plugins/reporting/public/components/index.ts b/x-pack/plugins/reporting/public/components/index.ts index 354ef189704ad..370e90c8d2d08 100644 --- a/x-pack/plugins/reporting/public/components/index.ts +++ b/x-pack/plugins/reporting/public/components/index.ts @@ -9,3 +9,4 @@ export { getFailureToast } from './job_failure'; export { getWarningFormulasToast } from './job_warning_formulas'; export { getWarningMaxSizeToast } from './job_warning_max_size'; export { getGeneralErrorToast } from './general_error'; +export { ScreenCapturePanelContent } from './screen_capture_panel_content'; diff --git a/x-pack/plugins/reporting/public/components/job_download_button.tsx b/x-pack/plugins/reporting/public/components/job_download_button.tsx index 8cf3ce8644add..7dff2cafa047b 100644 --- a/x-pack/plugins/reporting/public/components/job_download_button.tsx +++ b/x-pack/plugins/reporting/public/components/job_download_button.tsx @@ -7,8 +7,7 @@ import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { JobSummary } from '../'; -import { JobId } from '../../common/types'; +import { JobId, JobSummary } from '../../common/types'; interface Props { getUrl: (jobId: JobId) => string; diff --git a/x-pack/plugins/reporting/public/components/job_failure.tsx b/x-pack/plugins/reporting/public/components/job_failure.tsx index 8d8f32f692343..e9c3a448cfe41 100644 --- a/x-pack/plugins/reporting/public/components/job_failure.tsx +++ b/x-pack/plugins/reporting/public/components/job_failure.tsx @@ -9,8 +9,8 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { ToastInput } from 'src/core/public'; -import { JobSummary, ManagementLinkFn } from '../'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; +import { JobSummary, ManagementLinkFn } from '../../common/types'; export const getFailureToast = ( errorText: string, diff --git a/x-pack/plugins/reporting/public/components/job_success.tsx b/x-pack/plugins/reporting/public/components/job_success.tsx index 05cf2c4c5784a..f03914b2be2f2 100644 --- a/x-pack/plugins/reporting/public/components/job_success.tsx +++ b/x-pack/plugins/reporting/public/components/job_success.tsx @@ -7,9 +7,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { ToastInput } from 'src/core/public'; -import { JobSummary } from '../'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { JobId } from '../../common/types'; +import { JobId, JobSummary } from '../../common/types'; import { DownloadButton } from './job_download_button'; import { ReportLink } from './report_link'; diff --git a/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx b/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx index 8cccc94e98dcd..338c718a060c1 100644 --- a/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx +++ b/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx @@ -7,9 +7,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { ToastInput } from 'src/core/public'; -import { JobSummary } from '../'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { JobId } from '../../common/types'; +import { JobId, JobSummary } from '../../common/types'; import { DownloadButton } from './job_download_button'; import { ReportLink } from './report_link'; diff --git a/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx b/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx index c350eef0e5a54..cab743e2006df 100644 --- a/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx +++ b/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx @@ -7,9 +7,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { ToastInput } from 'src/core/public'; -import { JobSummary } from '../'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { JobId } from '../../common/types'; +import { JobId, JobSummary } from '../../common/types'; import { DownloadButton } from './job_download_button'; import { ReportLink } from './report_link'; diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index a512b1305b8e0..ac6d03a407c28 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -22,9 +22,9 @@ import { Component, default as React, Fragment } from 'react'; import { Subscription } from 'rxjs'; import { ApplicationStart, ToastsSetup } from 'src/core/public'; import { ILicense, LicensingPluginSetup } from '../../../licensing/public'; +import { JOB_STATUSES as JobStatuses } from '../../common/constants'; import { Poller } from '../../common/poller'; import { durationToNumber } from '../../common/schema_utils'; -import { JobStatuses } from '../../constants'; import { checkLicense } from '../lib/license_check'; import { JobQueueEntry, ReportingAPIClient } from '../lib/reporting_api_client'; import { ClientConfigType } from '../plugin'; diff --git a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx index 18895f9e623eb..7f48b5d9101ba 100644 --- a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx @@ -10,6 +10,7 @@ import React, { Component, ReactElement } from 'react'; import { ToastsSetup } from 'src/core/public'; import url from 'url'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; +import { CSV_REPORT_TYPE, PDF_REPORT_TYPE, PNG_REPORT_TYPE } from '../../common/constants'; import { BaseParams } from '../../common/types'; import { ReportingAPIClient } from '../lib/reporting_api_client'; @@ -165,12 +166,12 @@ class ReportingPanelContentUi extends Component { private prettyPrintReportingType = () => { switch (this.props.reportType) { - case 'printablePdf': + case PDF_REPORT_TYPE: return 'PDF'; case 'csv': - return 'CSV'; + return CSV_REPORT_TYPE; case 'png': - return 'PNG'; + return PNG_REPORT_TYPE; default: return this.props.reportType; } diff --git a/x-pack/plugins/reporting/public/index.ts b/x-pack/plugins/reporting/public/index.ts index 251fd14ee4d57..f15a5ca481757 100644 --- a/x-pack/plugins/reporting/public/index.ts +++ b/x-pack/plugins/reporting/public/index.ts @@ -5,33 +5,21 @@ */ import { PluginInitializerContext } from 'src/core/public'; -import { ReportingPublicPlugin } from './plugin'; +import { ScreenCapturePanelContent } from './components/screen_capture_panel_content'; import * as jobCompletionNotifications from './lib/job_completion_notifications'; -import { JobId, JobStatus } from '../common/types'; +import { ReportingAPIClient } from './lib/reporting_api_client'; +import { ReportingPublicPlugin } from './plugin'; -export function plugin(initializerContext: PluginInitializerContext) { - return new ReportingPublicPlugin(initializerContext); +export interface ReportingSetup { + components: { + ScreenCapturePanel: typeof ScreenCapturePanelContent; + }; } -export { ReportingPublicPlugin as Plugin }; -export { jobCompletionNotifications }; +export type ReportingStart = ReportingSetup; -export interface JobSummary { - id: JobId; - status: JobStatus; - title: string; - jobtype: string; - maxSizeReached?: boolean; - csvContainsFormulas?: boolean; -} +export { ReportingAPIClient, ReportingPublicPlugin as Plugin, jobCompletionNotifications }; -export interface JobSummarySet { - completed: JobSummary[]; - failed: JobSummary[]; +export function plugin(initializerContext: PluginInitializerContext) { + return new ReportingPublicPlugin(initializerContext); } - -type DownloadLink = string; -export type DownloadReportFn = (jobId: JobId) => DownloadLink; - -type ManagementLink = string; -export type ManagementLinkFn = () => ManagementLink; diff --git a/x-pack/plugins/reporting/public/lib/job_completion_notifications.ts b/x-pack/plugins/reporting/public/lib/job_completion_notifications.ts index 06694361b757d..39a7c9f84b8e5 100644 --- a/x-pack/plugins/reporting/public/lib/job_completion_notifications.ts +++ b/x-pack/plugins/reporting/public/lib/job_completion_notifications.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../../constants'; +import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../../common/constants'; type JobId = string; diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client.ts index 2853caaaaa1b5..71b57d0c0124e 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client.ts @@ -7,14 +7,20 @@ import { stringify } from 'query-string'; import rison from 'rison-node'; import { HttpSetup } from 'src/core/public'; -import { DownloadReportFn, ManagementLinkFn } from '../'; -import { JobId, ReportApiJSON, ReportDocument, ReportSource } from '../../common/types'; import { API_BASE_GENERATE, API_BASE_URL, API_LIST_URL, REPORTING_MANAGEMENT_HOME, -} from '../../constants'; +} from '../../common/constants'; +import { + DownloadReportFn, + JobId, + ManagementLinkFn, + ReportApiJSON, + ReportDocument, + ReportSource, +} from '../../common/types'; import { add } from './job_completion_notifications'; export interface JobQueueEntry { diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts index f91517e4397f9..31d324bd77159 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts @@ -6,8 +6,7 @@ import sinon, { stub } from 'sinon'; import { NotificationsStart } from 'src/core/public'; -import { JobSummary } from '../'; -import { ReportDocument } from '../../common/types'; +import { JobSummary, ReportDocument } from '../../common/types'; import { ReportingAPIClient } from './reporting_api_client'; import { ReportingNotifierStreamHandler } from './stream_handler'; diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.ts b/x-pack/plugins/reporting/public/lib/stream_handler.ts index d97c0a7a2d11e..4b2305b60c413 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.ts @@ -8,14 +8,8 @@ import { i18n } from '@kbn/i18n'; import * as Rx from 'rxjs'; import { catchError, map } from 'rxjs/operators'; import { NotificationsSetup } from 'src/core/public'; -import { JobSummarySet, JobSummary } from '../'; -import { JobId, ReportDocument } from '../../common/types'; -import { - JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, - JOB_STATUS_COMPLETED, - JOB_STATUS_FAILED, - JOB_STATUS_WARNINGS, -} from '../../constants'; +import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JOB_STATUSES } from '../../common/constants'; +import { JobId, JobSummary, JobSummarySet, ReportDocument } from '../../common/types'; import { getFailureToast, getGeneralErrorToast, @@ -107,9 +101,9 @@ export class ReportingNotifierStreamHandler { _source: { status: jobStatus }, } = job; if (storedJobs.includes(jobId)) { - if (jobStatus === JOB_STATUS_COMPLETED || jobStatus === JOB_STATUS_WARNINGS) { + if (jobStatus === JOB_STATUSES.COMPLETED || jobStatus === JOB_STATUSES.WARNINGS) { completedJobs.push(getReportStatus(job)); - } else if (jobStatus === JOB_STATUS_FAILED) { + } else if (jobStatus === JOB_STATUSES.FAILED) { failedJobs.push(getReportStatus(job)); } else { pending.push(jobId); diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index 1e3f7e34bebdb..9a4832b114e40 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -9,20 +9,18 @@ import _ from 'lodash'; import moment from 'moment-timezone'; import { CoreSetup } from 'src/core/public'; import { - UiActionsActionDefinition as ActionDefinition, + ISearchEmbeddable, + SEARCH_EMBEDDABLE_TYPE, +} from '../../../../../src/plugins/discover/public'; +import { IEmbeddable, ViewMode } from '../../../../../src/plugins/embeddable/public'; +import { IncompatibleActionError, + UiActionsActionDefinition as ActionDefinition, } from '../../../../../src/plugins/ui_actions/public'; import { LicensingPluginSetup } from '../../../licensing/public'; +import { API_GENERATE_IMMEDIATE, CSV_REPORTING_ACTION } from '../../common/constants'; import { checkLicense } from '../lib/license_check'; -import { ViewMode, IEmbeddable } from '../../../../../src/plugins/embeddable/public'; -import { - ISearchEmbeddable, - SEARCH_EMBEDDABLE_TYPE, -} from '../../../../../src/plugins/discover/public'; - -import { API_GENERATE_IMMEDIATE, CSV_REPORTING_ACTION } from '../../constants'; - function isSavedSearchEmbeddable( embeddable: IEmbeddable | ISearchEmbeddable ): embeddable is ISearchEmbeddable { diff --git a/x-pack/plugins/reporting/public/plugin.ts b/x-pack/plugins/reporting/public/plugin.ts index 33f4fd4abf72c..52362b4c68734 100644 --- a/x-pack/plugins/reporting/public/plugin.ts +++ b/x-pack/plugins/reporting/public/plugin.ts @@ -24,11 +24,14 @@ import { import { ManagementSetup, ManagementStart } from '../../../../src/plugins/management/public'; import { SharePluginSetup, SharePluginStart } from '../../../../src/plugins/share/public'; import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/public'; +import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../common/constants'; import { durationToNumber } from '../common/schema_utils'; -import { JobId, ReportingConfigType } from '../common/types'; -import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../constants'; -import { JobSummarySet } from './'; -import { getGeneralErrorToast } from './components'; +import { JobId, JobSummarySet } from '../common/types'; +import { ReportingSetup, ReportingStart } from './'; +import { + getGeneralErrorToast, + ScreenCapturePanelContent as ScreenCapturePanel, +} from './components'; import { ReportingAPIClient } from './lib/reporting_api_client'; import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_handler'; import { GetCsvReportPanelAction } from './panel_actions/get_csv_panel_action'; @@ -36,7 +39,12 @@ import { csvReportingProvider } from './share_context_menu/register_csv_reportin import { reportingPDFPNGProvider } from './share_context_menu/register_pdf_png_reporting'; export interface ClientConfigType { - poll: ReportingConfigType['poll']; + poll: { + jobsRefresh: { + interval: number; + intervalErrorMultiplier: number; + }; + }; } function getStored(): JobId[] { @@ -75,8 +83,13 @@ export interface ReportingPublicPluginStartDendencies { export class ReportingPublicPlugin implements - Plugin { - private config: ClientConfigType; + Plugin< + ReportingSetup, + ReportingStart, + ReportingPublicPluginSetupDendencies, + ReportingPublicPluginStartDendencies + > { + private readonly contract: ReportingStart = { components: { ScreenCapturePanel } }; private readonly stop$ = new Rx.ReplaySubject(1); private readonly title = i18n.translate('xpack.reporting.management.reportingTitle', { defaultMessage: 'Reporting', @@ -84,6 +97,7 @@ export class ReportingPublicPlugin private readonly breadcrumbText = i18n.translate('xpack.reporting.breadcrumb', { defaultMessage: 'Reporting', }); + private config: ClientConfigType; constructor(initializerContext: PluginInitializerContext) { this.config = initializerContext.config.get(); @@ -149,6 +163,8 @@ export class ReportingPublicPlugin uiSettings, }) ); + + return this.contract; } public start(core: CoreStart) { @@ -166,6 +182,8 @@ export class ReportingPublicPlugin catchError((err) => handleError(notifications, err)) ) .subscribe(); + + return this.contract; } public stop() { diff --git a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts index 5b98a198b7d1a..43243d265e926 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CSV_JOB_TYPE } from '../../../constants'; +import { CSV_JOB_TYPE } from '../../../common/constants'; import { cryptoFactory } from '../../lib'; import { CreateJobFn, CreateJobFnFactory } from '../../types'; import { IndexPatternSavedObject, JobParamsCSV, TaskPayloadCSV } from './types'; diff --git a/x-pack/plugins/reporting/server/export_types/csv/index.ts b/x-pack/plugins/reporting/server/export_types/csv/index.ts index e66cfef18c6e2..f7b7ff5709fe6 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/index.ts @@ -5,6 +5,7 @@ */ import { + CSV_JOB_TYPE as jobType, LICENSE_TYPE_BASIC, LICENSE_TYPE_ENTERPRISE, LICENSE_TYPE_GOLD, @@ -12,7 +13,6 @@ import { LICENSE_TYPE_STANDARD, LICENSE_TYPE_TRIAL, } from '../../../common/constants'; -import { CSV_JOB_TYPE as jobType } from '../../../constants'; import { CreateJobFn, ExportTypeDefinition, RunTaskFn } from '../../types'; import { createJobFnFactory } from './create_job'; import { runTaskFnFactory } from './execute_job'; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts index abe9fbf3e3950..2c163aeb57a64 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts @@ -5,6 +5,7 @@ */ import { + CSV_FROM_SAVEDOBJECT_JOB_TYPE, LICENSE_TYPE_BASIC, LICENSE_TYPE_ENTERPRISE, LICENSE_TYPE_GOLD, @@ -12,7 +13,6 @@ import { LICENSE_TYPE_STANDARD, LICENSE_TYPE_TRIAL, } from '../../../common/constants'; -import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../constants'; import { ExportTypeDefinition } from '../../types'; import { createJobFnFactory, ImmediateCreateJobFn } from './create_job'; import { ImmediateExecuteFn, runTaskFnFactory } from './execute_job'; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/metadata.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/metadata.ts index a0fd8a29fdcc4..fda360103a115 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/metadata.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/metadata.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../constants'; +import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; export const metadata = { id: CSV_FROM_SAVEDOBJECT_JOB_TYPE, diff --git a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts index b1fcdbe05fd67..010b6f431db7e 100644 --- a/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/create_job/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PNG_JOB_TYPE } from '../../../../constants'; +import { PNG_JOB_TYPE } from '../../../../common/constants'; import { cryptoFactory } from '../../../lib'; import { CreateJobFn, CreateJobFnFactory } from '../../../types'; import { validateUrls } from '../../common'; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts index dcd33a0fc8d53..a529cb864b6f7 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PDF_JOB_TYPE } from '../../../../constants'; +import { PDF_JOB_TYPE } from '../../../../common/constants'; import { cryptoFactory } from '../../../lib'; import { CreateJobFn, CreateJobFnFactory } from '../../../types'; import { validateUrls } from '../../common'; diff --git a/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts index e69b8d61dec0d..c90f67b81317e 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { LAYOUT_TYPES } from '../../../common/constants'; import { CaptureConfig } from '../../types'; -import { LayoutInstance, LayoutParams, LayoutTypes } from './'; +import { LayoutInstance, LayoutParams } from './'; import { PreserveLayout } from './preserve_layout'; import { PrintLayout } from './print_layout'; @@ -13,7 +14,7 @@ export function createLayout( captureConfig: CaptureConfig, layoutParams?: LayoutParams ): LayoutInstance { - if (layoutParams && layoutParams.dimensions && layoutParams.id === LayoutTypes.PRESERVE_LAYOUT) { + if (layoutParams && layoutParams.dimensions && layoutParams.id === LAYOUT_TYPES.PRESERVE_LAYOUT) { return new PreserveLayout(layoutParams.dimensions); } diff --git a/x-pack/plugins/reporting/server/lib/layouts/index.ts b/x-pack/plugins/reporting/server/lib/layouts/index.ts index c091339a60582..8bfe79aeb8a21 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/index.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/index.ts @@ -4,59 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HeadlessChromiumDriver } from '../../browsers'; import { LevelLogger } from '../'; +import { LayoutSelectorDictionary, Size } from '../../../common/types'; +import { HeadlessChromiumDriver } from '../../browsers'; import { Layout } from './layout'; +export { + LayoutParams, + LayoutSelectorDictionary, + PageSizeParams, + PdfImageSize, + Size, +} from '../../../common/types'; export { createLayout } from './create_layout'; export { Layout } from './layout'; export { PreserveLayout } from './preserve_layout'; export { PrintLayout } from './print_layout'; -export const LayoutTypes = { - PRESERVE_LAYOUT: 'preserve_layout', - PRINT: 'print', -}; - -export const getDefaultLayoutSelectors = (): LayoutSelectorDictionary => ({ - screenshot: '[data-shared-items-container]', - renderComplete: '[data-shared-item]', - itemsCountAttribute: 'data-shared-items-count', - timefilterDurationAttribute: 'data-shared-timefilter-duration', -}); - -export interface PageSizeParams { - pageMarginTop: number; - pageMarginBottom: number; - pageMarginWidth: number; - tableBorderWidth: number; - headingHeight: number; - subheadingHeight: number; -} - -export interface LayoutSelectorDictionary { - screenshot: string; - renderComplete: string; - itemsCountAttribute: string; - timefilterDurationAttribute: string; -} - -export interface PdfImageSize { - width: number; - height?: number; -} - -export interface Size { - width: number; - height: number; -} - -export interface LayoutParams { - id: string; - dimensions?: Size; - selectors?: LayoutSelectorDictionary; -} - interface LayoutSelectors { // Fields that are not part of Layout: the instances // independently implement these fields on their own diff --git a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts index faddaae64ce5d..549e898d8a13e 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts @@ -6,15 +6,10 @@ import path from 'path'; import { CustomPageSize } from 'pdfmake/interfaces'; -import { - getDefaultLayoutSelectors, - Layout, - LayoutInstance, - LayoutSelectorDictionary, - LayoutTypes, - PageSizeParams, - Size, -} from './'; +import { getDefaultLayoutSelectors } from '../../../common'; +import { LAYOUT_TYPES } from '../../../common/constants'; +import { LayoutSelectorDictionary, PageSizeParams, Size } from '../../../common/types'; +import { Layout, LayoutInstance } from './'; // We use a zoom of two to bump up the resolution of the screenshot a bit. const ZOOM: number = 2; @@ -28,7 +23,7 @@ export class PreserveLayout extends Layout implements LayoutInstance { private readonly scaledWidth: number; constructor(size: Size, layoutSelectors?: LayoutSelectorDictionary) { - super(LayoutTypes.PRESERVE_LAYOUT); + super(LAYOUT_TYPES.PRESERVE_LAYOUT); this.height = size.height; this.width = size.width; this.scaledHeight = size.height * ZOOM; diff --git a/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts index e979cdeeb71fe..8db1fa7ff6347 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts @@ -8,16 +8,12 @@ import path from 'path'; import { PageOrientation, PredefinedPageSize } from 'pdfmake/interfaces'; import { EvaluateFn, SerializableOrJSHandle } from 'puppeteer'; import { LevelLogger } from '../'; +import { getDefaultLayoutSelectors } from '../../../common'; +import { LAYOUT_TYPES } from '../../../common/constants'; +import { LayoutSelectorDictionary, Size } from '../../../common/types'; import { HeadlessChromiumDriver } from '../../browsers'; import { CaptureConfig } from '../../types'; -import { - getDefaultLayoutSelectors, - LayoutInstance, - LayoutSelectorDictionary, - LayoutTypes, - Size, -} from './'; -import { Layout } from './layout'; +import { Layout, LayoutInstance } from './'; export class PrintLayout extends Layout implements LayoutInstance { public readonly selectors: LayoutSelectorDictionary = { @@ -28,7 +24,7 @@ export class PrintLayout extends Layout implements LayoutInstance { private captureConfig: CaptureConfig; constructor(captureConfig: CaptureConfig) { - super(LayoutTypes.PRINT); + super(LAYOUT_TYPES.PRINT); this.captureConfig = captureConfig; } diff --git a/x-pack/plugins/reporting/server/lib/store/index.ts b/x-pack/plugins/reporting/server/lib/store/index.ts index a48f266120323..17f0fb5bf0389 100644 --- a/x-pack/plugins/reporting/server/lib/store/index.ts +++ b/x-pack/plugins/reporting/server/lib/store/index.ts @@ -4,5 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export { Report, ReportDocument } from './report'; +export { ReportDocument } from '../../../common/types'; +export { Report } from './report'; export { ReportingStore } from './store'; diff --git a/x-pack/plugins/reporting/server/lib/store/report.ts b/x-pack/plugins/reporting/server/lib/store/report.ts index d82b90f4025ed..2e4473ef8f2ea 100644 --- a/x-pack/plugins/reporting/server/lib/store/report.ts +++ b/x-pack/plugins/reporting/server/lib/store/report.ts @@ -7,51 +7,8 @@ import moment from 'moment'; // @ts-ignore no module definition import Puid from 'puid'; -import { JobStatus, ReportApiJSON } from '../../../common/types'; -import { JobStatuses } from '../../../constants'; -import { LayoutParams } from '../layouts'; -import { TaskRunResult } from '../tasks'; - -interface ReportDocumentHead { - _id: string; - _index: string; - _seq_no: unknown; - _primary_term: unknown; -} - -/* - * The document created by Reporting to store in the .reporting index - */ -export interface ReportDocument extends ReportDocumentHead { - _source: ReportSource; -} - -export interface ReportSource { - jobtype: string; - kibana_name: string; - kibana_id: string; - created_by: string | false; - payload: { - headers: string; // encrypted headers - browserTimezone?: string; // may use timezone from advanced settings - objectType: string; - title: string; - layout?: LayoutParams; - }; - meta: { objectType: string; layout?: string }; - browser_type: string; - max_attempts: number; - timeout: number; - - status: JobStatus; - attempts: number; - output: TaskRunResult | null; - started_at?: string; - completed_at?: string; - created_at: string; - priority?: number; - process_expiration?: string; -} +import { JOB_STATUSES } from '../../../common/constants'; +import { ReportApiJSON, ReportDocumentHead, ReportSource } from '../../../common/types'; const puid = new Puid(); @@ -107,7 +64,7 @@ export class Report implements Partial { this.browser_type = opts.browser_type; this.priority = opts.priority; - this.status = opts.status || JobStatuses.PENDING; + this.status = opts.status || JOB_STATUSES.PENDING; this.output = opts.output || null; } @@ -175,3 +132,5 @@ export class Report implements Partial { }; } } + +export { ReportApiJSON, ReportSource }; diff --git a/x-pack/plugins/reporting/server/lib/tasks/index.ts b/x-pack/plugins/reporting/server/lib/tasks/index.ts index 0dd9945985bfb..c866c81c9793c 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/index.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/index.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ReportSource, TaskRunResult } from '../../../common/types'; import { BasePayload } from '../../types'; -import { ReportSource } from '../store/report'; /* * The document created by Reporting to store as task parameters for Task @@ -22,11 +22,4 @@ export interface ReportTaskParams { meta: ReportSource['meta']; } -export interface TaskRunResult { - content_type: string | null; - content: string | null; - csv_contains_formulas?: boolean; - size: number; - max_size_reached?: boolean; - warnings?: string[]; -} +export { TaskRunResult }; diff --git a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts index 400fbb16f54dc..6ac5875acd34c 100644 --- a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts @@ -7,7 +7,6 @@ import { schema } from '@kbn/config-schema'; import { KibanaRequest } from 'src/core/server'; import { ReportingCore } from '../'; -import { API_BASE_GENERATE_V1 } from '../../common/constants'; import { createJobFnFactory } from '../export_types/csv_from_savedobject/create_job'; import { runTaskFnFactory } from '../export_types/csv_from_savedobject/execute_job'; import { @@ -20,6 +19,9 @@ import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routi import { getJobParamsFromRequest } from './lib/get_job_params_from_request'; import { HandlerErrorFunction } from './types'; +const API_BASE_URL_V1 = '/api/reporting/v1'; +const API_BASE_GENERATE_V1 = `${API_BASE_URL_V1}/generate`; + export type CsvFromSavedObjectRequest = KibanaRequest< JobParamsPanelCsv, unknown, diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts index c9dbbda9fd68d..12a3ac5c762c7 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createLayout, LayoutInstance, LayoutTypes } from '../lib/layouts'; +import { LAYOUT_TYPES } from '../../common/constants'; +import { createLayout, LayoutInstance } from '../lib/layouts'; import { CaptureConfig } from '../types'; export const createMockLayoutInstance = (captureConfig: CaptureConfig) => { const mockLayout = createLayout(captureConfig, { - id: LayoutTypes.PRESERVE_LAYOUT, + id: LAYOUT_TYPES.PRESERVE_LAYOUT, dimensions: { height: 100, width: 100 }, }) as LayoutInstance; mockLayout.selectors = { diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index eb046a3eab075..8cd26df032f64 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -8,15 +8,15 @@ import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { DataPluginStart } from 'src/plugins/data/server/plugin'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { SpacesPluginSetup } from '../../spaces/server'; -import { CancellationToken } from '../../../plugins/reporting/common'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { AuthenticatedUser, SecurityPluginSetup } from '../../security/server'; +import { SpacesPluginSetup } from '../../spaces/server'; +import { CancellationToken } from '../common'; +import { BaseParams } from '../common/types'; import { ReportingConfigType } from './config'; import { ReportingCore } from './core'; import { LevelLogger } from './lib'; -import { LayoutParams } from './lib/layouts'; import { ReportTaskParams, TaskRunResult } from './lib/tasks'; /* @@ -47,12 +47,7 @@ export type ReportingUser = { username: AuthenticatedUser['username'] } | false; export type CaptureConfig = ReportingConfigType['capture']; export type ScrollConfig = ReportingConfigType['csv']['scroll']; -export interface BaseParams { - browserTimezone?: string; // browserTimezone is optional: it is not in old POST URLs that were generated prior to being added to this interface - layout?: LayoutParams; - objectType: string; - title: string; -} +export { BaseParams }; // base params decorated with encrypted headers that come into runJob functions export interface BasePayload extends BaseParams { From 208e86e66a7001a1f93f0dd0d937af5cc4deb9db Mon Sep 17 00:00:00 2001 From: John Schulz Date: Thu, 12 Nov 2020 11:05:17 -0500 Subject: [PATCH 14/17] [Ingest Manager] Lift up registry/{stream,extract} functions (#83239) ## Summary * Move stream utility functions from `server/services/epm/registry/streams.ts` to `server/services/epm/streams.ts` * They're only used in registry at the moment but aren't specific to registry * Move archive extraction functions from `server/services/epm/registry/extract.ts` to `server/services/epm/archive.ts` * The Registry isn't the only service/code which needs to extract packages. Continue consolidating archive-related code under archive vs registry --- .../server/services/epm/{registry => archive}/extract.ts | 8 ++------ x-pack/plugins/fleet/server/services/epm/archive/index.ts | 8 +++++++- .../fleet/server/services/epm/archive/validation.ts | 3 ++- .../services/epm/elasticsearch/ingest_pipeline/install.ts | 3 +-- .../plugins/fleet/server/services/epm/packages/assets.ts | 6 +++--- .../fleet/server/services/epm/registry/index.test.ts | 5 ++--- .../plugins/fleet/server/services/epm/registry/index.ts | 4 +--- .../fleet/server/services/epm/registry/requests.ts | 2 +- .../fleet/server/services/epm/{registry => }/streams.ts | 0 9 files changed, 19 insertions(+), 20 deletions(-) rename x-pack/plugins/fleet/server/services/epm/{registry => archive}/extract.ts (95%) rename x-pack/plugins/fleet/server/services/epm/{registry => }/streams.ts (100%) diff --git a/x-pack/plugins/fleet/server/services/epm/registry/extract.ts b/x-pack/plugins/fleet/server/services/epm/archive/extract.ts similarity index 95% rename from x-pack/plugins/fleet/server/services/epm/registry/extract.ts rename to x-pack/plugins/fleet/server/services/epm/archive/extract.ts index b79218638ce24..6ac81a25dfc21 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/extract.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/extract.ts @@ -6,12 +6,8 @@ import tar from 'tar'; import yauzl from 'yauzl'; -import { bufferToStream, streamToBuffer } from './streams'; - -export interface ArchiveEntry { - path: string; - buffer?: Buffer; -} +import { bufferToStream, streamToBuffer } from '../streams'; +import { ArchiveEntry } from './index'; export async function untarBuffer( buffer: Buffer, diff --git a/x-pack/plugins/fleet/server/services/epm/archive/index.ts b/x-pack/plugins/fleet/server/services/epm/archive/index.ts index 810740d697fcb..6d1150b3ac8bd 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/index.ts @@ -14,10 +14,16 @@ import { setArchiveFilelist, deleteArchiveFilelist, } from './cache'; -import { ArchiveEntry, getBufferExtractor } from '../registry/extract'; +import { getBufferExtractor } from './extract'; import { parseAndVerifyArchiveEntries } from './validation'; export * from './cache'; +export { untarBuffer, unzipBuffer, getBufferExtractor } from './extract'; + +export interface ArchiveEntry { + path: string; + buffer?: Buffer; +} export async function getArchivePackage({ archiveBuffer, diff --git a/x-pack/plugins/fleet/server/services/epm/archive/validation.ts b/x-pack/plugins/fleet/server/services/epm/archive/validation.ts index d9d451544a953..992020cb073ad 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/validation.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/validation.ts @@ -15,7 +15,8 @@ import { RegistryVarsEntry, } from '../../../../common/types'; import { PackageInvalidArchiveError } from '../../../errors'; -import { ArchiveEntry, pkgToPkgKey } from '../registry'; +import { ArchiveEntry } from './index'; +import { pkgToPkgKey } from '../registry'; const MANIFESTS: Record = {}; const MANIFEST_NAME = 'manifest.yml'; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts index c5c9e8ac2c01b..b6988f64843d0 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts @@ -11,8 +11,7 @@ import { ElasticsearchAssetType, InstallablePackage, } from '../../../../types'; -import { ArchiveEntry } from '../../registry'; -import { getAsset, getPathParts } from '../../archive'; +import { ArchiveEntry, getAsset, getPathParts } from '../../archive'; import { CallESAsCurrentUser } from '../../../../types'; import { saveInstalledEsRefs } from '../../packages/install'; import { getInstallationObject } from '../../packages'; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/assets.ts b/x-pack/plugins/fleet/server/services/epm/packages/assets.ts index 50d8f2f4d2fb2..80e1cbba6484b 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/assets.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/assets.ts @@ -6,7 +6,7 @@ import { InstallablePackage } from '../../../types'; import * as Registry from '../registry'; -import { getArchiveFilelist, getAsset } from '../archive'; +import { ArchiveEntry, getArchiveFilelist, getAsset } from '../archive'; // paths from RegistryPackage are routes to the assets on EPR // e.g. `/package/nginx/1.2.0/data_stream/access/fields/fields.yml` @@ -51,14 +51,14 @@ export async function getAssetsData( packageInfo: InstallablePackage, filter = (path: string): boolean => true, datasetName?: string -): Promise { +): Promise { // TODO: Needs to be called to fill the cache but should not be required await Registry.ensureCachedArchiveInfo(packageInfo.name, packageInfo.version, 'registry'); // Gather all asset data const assets = getAssets(packageInfo, filter, datasetName); - const entries: Registry.ArchiveEntry[] = assets.map((path) => { + const entries: ArchiveEntry[] = assets.map((path) => { const buffer = getAsset(path); return { path, buffer }; diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts index 1208ffdaefe4a..aea28b5d56ab9 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.test.ts @@ -5,9 +5,8 @@ */ import { AssetParts } from '../../../types'; -import { getPathParts } from '../archive'; -import { getBufferExtractor, splitPkgKey } from './index'; -import { untarBuffer, unzipBuffer } from './extract'; +import { getBufferExtractor, getPathParts, untarBuffer, unzipBuffer } from '../archive'; +import { splitPkgKey } from './index'; const testPaths = [ { diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.ts index c35e91bdf580b..aef1bb75619cd 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.ts @@ -24,13 +24,11 @@ import { unpackArchiveToCache, } from '../archive'; import { fetchUrl, getResponse, getResponseStream } from './requests'; -import { streamToBuffer } from './streams'; +import { streamToBuffer } from '../streams'; import { getRegistryUrl } from './registry_url'; import { appContextService } from '../..'; import { PackageNotFoundError, PackageCacheError } from '../../../errors'; -export { ArchiveEntry, getBufferExtractor } from './extract'; - export interface SearchParams { category?: CategoryId; experimental?: boolean; diff --git a/x-pack/plugins/fleet/server/services/epm/registry/requests.ts b/x-pack/plugins/fleet/server/services/epm/registry/requests.ts index 2b9c349565790..c8d158c8afaaa 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/requests.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/requests.ts @@ -6,7 +6,7 @@ import fetch, { FetchError, Response, RequestInit } from 'node-fetch'; import pRetry from 'p-retry'; -import { streamToString } from './streams'; +import { streamToString } from '../streams'; import { appContextService } from '../../app_context'; import { RegistryError, RegistryConnectionError, RegistryResponseError } from '../../../errors'; import { getProxyAgent, getRegistryProxyUrl } from './proxy'; diff --git a/x-pack/plugins/fleet/server/services/epm/registry/streams.ts b/x-pack/plugins/fleet/server/services/epm/streams.ts similarity index 100% rename from x-pack/plugins/fleet/server/services/epm/registry/streams.ts rename to x-pack/plugins/fleet/server/services/epm/streams.ts From ab72206da338e81e549da128e6ca5fd7a30e2b30 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 12 Nov 2020 16:39:40 +0000 Subject: [PATCH 15/17] [Alerting] Moves the Index & Geo Threshold UIs into the Stack Alerts Public Plugin (#82951) This PR includes the following refactors: 1. Moves the Index Pattern Api from _Stack Alerts_ to the _Server_ plugin of _Trigger Actions UI_. This fixes a potential bug where a user could disable the _Stack Alerts_ plugin and inadvertently break the UI of the _ES Index _ action type. 2. Extracts the UI components for _Index Threshold_ and _Geo Threshold_ from the _Trigger Actions UI_ plugin and moves them into _Stack Alerts_. --- .eslintrc.js | 9 +- packages/kbn-optimizer/limits.yml | 1 + .../stack_alerts/{server => common}/config.ts | 1 + x-pack/plugins/stack_alerts/common/index.ts | 2 +- x-pack/plugins/stack_alerts/kibana.json | 4 +- .../alert_types}/geo_threshold/index.ts | 7 +- .../expressions/boundary_index_expression.tsx | 17 +- .../expressions/entity_by_expression.tsx | 8 +- .../expressions/entity_index_expression.tsx | 24 +- .../geo_threshold/query_builder/index.tsx | 37 ++- .../expression_with_popover.tsx | 12 +- .../geo_index_pattern_select.tsx | 26 +- .../util_components/single_field_select.tsx | 2 +- .../alert_types}/geo_threshold/types.ts | 0 .../geo_threshold/validation.test.ts | 0 .../alert_types}/geo_threshold/validation.ts | 18 +- .../public/alert_types}/index.ts | 15 +- .../alert_types}/threshold/expression.scss | 0 .../alert_types}/threshold/expression.tsx | 65 +++-- .../public/alert_types}/threshold/index.ts | 9 +- .../threshold/index_threshold_api.ts | 45 ++++ .../public/alert_types}/threshold/types.ts | 0 .../alert_types}/threshold/validation.test.ts | 0 .../alert_types}/threshold/validation.ts | 22 +- .../alert_types}/threshold/visualization.tsx | 40 +-- x-pack/plugins/stack_alerts/public/index.ts | 10 + x-pack/plugins/stack_alerts/public/plugin.tsx | 35 +++ .../alert_types/geo_threshold/alert_type.ts | 6 +- .../geo_threshold/es_query_builder.ts | 2 +- .../geo_threshold/geo_threshold.ts | 4 +- .../server/alert_types/geo_threshold/index.ts | 9 +- .../geo_threshold/tests/alert_type.test.ts | 6 +- .../stack_alerts/server/alert_types/index.ts | 8 +- .../alert_types/index_threshold/README.md | 230 +----------------- .../index_threshold/alert_type.test.ts | 10 +- .../alert_types/index_threshold/alert_type.ts | 19 +- .../index_threshold/alert_type_params.test.ts | 186 +++++++++++++- .../index_threshold/alert_type_params.ts | 5 +- .../alert_types/index_threshold/index.ts | 24 +- x-pack/plugins/stack_alerts/server/index.ts | 19 +- .../stack_alerts/server/plugin.test.ts | 29 --- x-pack/plugins/stack_alerts/server/plugin.ts | 31 +-- x-pack/plugins/stack_alerts/server/types.ts | 17 +- .../translations/translations/ja-JP.json | 150 ++++++------ .../translations/translations/zh-CN.json | 150 ++++++------ x-pack/plugins/triggers_actions_ui/README.md | 2 +- .../common/data}/index.ts | 0 .../triggers_actions_ui/common/index.ts | 7 + .../plugins/triggers_actions_ui/kibana.json | 4 +- .../public/application/boot.tsx | 2 +- .../public/common/index.ts | 4 + .../public/common/index_controls/index.ts | 19 +- .../public/common/lib/data_apis.ts | 67 +++++ .../public/common/lib/index.ts | 6 + .../public/common/lib/index_threshold_api.ts | 91 ------- .../triggers_actions_ui/public/index.ts | 7 +- .../triggers_actions_ui/public/plugin.ts | 23 +- .../triggers_actions_ui/server/data/README.md | 228 +++++++++++++++++ .../triggers_actions_ui/server/data/index.ts | 40 +++ .../server/data}/lib/core_query_types.test.ts | 0 .../server/data}/lib/core_query_types.ts | 67 +++-- .../server/data}/lib/date_range_info.test.ts | 0 .../server/data}/lib/date_range_info.ts | 15 +- .../server/data/lib/index.ts | 12 + .../data}/lib/time_series_query.test.ts | 21 +- .../server/data}/lib/time_series_query.ts | 5 +- .../data}/lib/time_series_types.test.ts | 0 .../server/data}/lib/time_series_types.ts | 34 +-- .../server/data}/routes/fields.ts | 12 +- .../server/data}/routes/index.ts | 15 +- .../server/data}/routes/indices.ts | 18 +- .../server/data}/routes/time_series_query.ts | 22 +- .../triggers_actions_ui/server/index.ts | 19 +- .../triggers_actions_ui/server/plugin.ts | 39 +++ .../index_threshold/alert.ts | 7 +- .../index_threshold/fields_endpoint.ts | 2 +- .../index_threshold/indices_endpoint.ts | 2 +- .../time_series_query_endpoint.ts | 4 +- 78 files changed, 1212 insertions(+), 896 deletions(-) rename x-pack/plugins/stack_alerts/{server => common}/config.ts (85%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/geo_threshold/index.ts (75%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/geo_threshold/query_builder/expressions/boundary_index_expression.tsx (85%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/geo_threshold/query_builder/expressions/entity_by_expression.tsx (87%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/geo_threshold/query_builder/expressions/entity_index_expression.tsx (83%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/geo_threshold/query_builder/index.tsx (89%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/geo_threshold/query_builder/util_components/expression_with_popover.tsx (88%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/geo_threshold/query_builder/util_components/geo_index_pattern_select.tsx (80%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/geo_threshold/query_builder/util_components/single_field_select.tsx (96%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/geo_threshold/types.ts (100%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/geo_threshold/validation.test.ts (100%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/geo_threshold/validation.ts (72%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/index.ts (57%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/threshold/expression.scss (100%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/threshold/expression.tsx (86%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/threshold/index.ts (77%) create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/threshold/index_threshold_api.ts rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/threshold/types.ts (100%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/threshold/validation.test.ts (100%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/threshold/validation.ts (81%) rename x-pack/plugins/{triggers_actions_ui/public/application/components/builtin_alert_types => stack_alerts/public/alert_types}/threshold/visualization.tsx (86%) create mode 100644 x-pack/plugins/stack_alerts/public/index.ts create mode 100644 x-pack/plugins/stack_alerts/public/plugin.tsx rename x-pack/plugins/{stack_alerts/common/alert_types/index_threshold => triggers_actions_ui/common/data}/index.ts (100%) create mode 100644 x-pack/plugins/triggers_actions_ui/common/index.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/common/lib/index.ts delete mode 100644 x-pack/plugins/triggers_actions_ui/public/common/lib/index_threshold_api.ts create mode 100644 x-pack/plugins/triggers_actions_ui/server/data/README.md create mode 100644 x-pack/plugins/triggers_actions_ui/server/data/index.ts rename x-pack/plugins/{stack_alerts/server/alert_types/index_threshold => triggers_actions_ui/server/data}/lib/core_query_types.test.ts (100%) rename x-pack/plugins/{stack_alerts/server/alert_types/index_threshold => triggers_actions_ui/server/data}/lib/core_query_types.ts (68%) rename x-pack/plugins/{stack_alerts/server/alert_types/index_threshold => triggers_actions_ui/server/data}/lib/date_range_info.test.ts (100%) rename x-pack/plugins/{stack_alerts/server/alert_types/index_threshold => triggers_actions_ui/server/data}/lib/date_range_info.ts (89%) create mode 100644 x-pack/plugins/triggers_actions_ui/server/data/lib/index.ts rename x-pack/plugins/{stack_alerts/server/alert_types/index_threshold => triggers_actions_ui/server/data}/lib/time_series_query.test.ts (58%) rename x-pack/plugins/{stack_alerts/server/alert_types/index_threshold => triggers_actions_ui/server/data}/lib/time_series_query.ts (96%) rename x-pack/plugins/{stack_alerts/server/alert_types/index_threshold => triggers_actions_ui/server/data}/lib/time_series_types.test.ts (100%) rename x-pack/plugins/{stack_alerts/server/alert_types/index_threshold => triggers_actions_ui/server/data}/lib/time_series_types.ts (81%) rename x-pack/plugins/{stack_alerts/server/alert_types/index_threshold => triggers_actions_ui/server/data}/routes/fields.ts (90%) rename x-pack/plugins/{stack_alerts/server/alert_types/index_threshold => triggers_actions_ui/server/data}/routes/index.ts (55%) rename x-pack/plugins/{stack_alerts/server/alert_types/index_threshold => triggers_actions_ui/server/data}/routes/indices.ts (85%) rename x-pack/plugins/{stack_alerts/server/alert_types/index_threshold => triggers_actions_ui/server/data}/routes/time_series_query.ts (58%) create mode 100644 x-pack/plugins/triggers_actions_ui/server/plugin.ts diff --git a/.eslintrc.js b/.eslintrc.js index 561e9bc55bf9d..ad9de04251e4c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1035,12 +1035,19 @@ module.exports = { * Alerting Services overrides */ { - // typescript only for front and back end + // typescript for front and back end files: ['x-pack/plugins/{alerts,stack_alerts,actions,task_manager,event_log}/**/*.{ts,tsx}'], rules: { '@typescript-eslint/no-explicit-any': 'error', }, }, + { + // typescript only for back end + files: ['x-pack/plugins/triggers_actions_ui/server/**/*.ts'], + rules: { + '@typescript-eslint/no-explicit-any': 'error', + }, + }, /** * Lens overrides diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 701b7cab21600..e326c8e2cac39 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -102,3 +102,4 @@ pageLoadAssetSize: visualizations: 295025 visualize: 57431 watcher: 43598 + stackAlerts: 29684 diff --git a/x-pack/plugins/stack_alerts/server/config.ts b/x-pack/plugins/stack_alerts/common/config.ts similarity index 85% rename from x-pack/plugins/stack_alerts/server/config.ts rename to x-pack/plugins/stack_alerts/common/config.ts index 8a13aedd5fdd8..2e997ce0ebad6 100644 --- a/x-pack/plugins/stack_alerts/server/config.ts +++ b/x-pack/plugins/stack_alerts/common/config.ts @@ -8,6 +8,7 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), + enableGeoTrackingThresholdAlert: schema.boolean({ defaultValue: false }), }); export type Config = TypeOf; diff --git a/x-pack/plugins/stack_alerts/common/index.ts b/x-pack/plugins/stack_alerts/common/index.ts index 79dd18d321f07..a75625d0641aa 100644 --- a/x-pack/plugins/stack_alerts/common/index.ts +++ b/x-pack/plugins/stack_alerts/common/index.ts @@ -3,5 +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 * from './config'; export const STACK_ALERTS_FEATURE_ID = 'stackAlerts'; diff --git a/x-pack/plugins/stack_alerts/kibana.json b/x-pack/plugins/stack_alerts/kibana.json index b26114577c430..b7405c38d1611 100644 --- a/x-pack/plugins/stack_alerts/kibana.json +++ b/x-pack/plugins/stack_alerts/kibana.json @@ -3,7 +3,7 @@ "server": true, "version": "8.0.0", "kibanaVersion": "kibana", - "requiredPlugins": ["alerts", "features"], + "requiredPlugins": ["alerts", "features", "triggersActionsUi", "kibanaReact"], "configPath": ["xpack", "stack_alerts"], - "ui": false + "ui": true } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/index.ts b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/index.ts similarity index 75% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/index.ts rename to x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/index.ts index 00d9ebbbbc066..35f5648de40f3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/index.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/index.ts @@ -5,18 +5,17 @@ */ import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; -import { AlertTypeModel } from '../../../../types'; import { validateExpression } from './validation'; import { GeoThresholdAlertParams } from './types'; -import { AlertsContextValue } from '../../../context/alerts_context'; +import { AlertTypeModel, AlertsContextValue } from '../../../../triggers_actions_ui/public'; export function getAlertType(): AlertTypeModel { return { id: '.geo-threshold', - name: i18n.translate('xpack.triggersActionsUI.geoThreshold.name.trackingThreshold', { + name: i18n.translate('xpack.stackAlerts.geoThreshold.name.trackingThreshold', { defaultMessage: 'Tracking threshold', }), - description: i18n.translate('xpack.triggersActionsUI.geoThreshold.descriptionText', { + description: i18n.translate('xpack.stackAlerts.geoThreshold.descriptionText', { defaultMessage: 'Alert when an entity enters or leaves a geo boundary.', }), iconClass: 'globe', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/expressions/boundary_index_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/boundary_index_expression.tsx similarity index 85% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/expressions/boundary_index_expression.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/boundary_index_expression.tsx index 497e053a4ed60..55dfc82bdbdb8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/expressions/boundary_index_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/boundary_index_expression.tsx @@ -7,14 +7,13 @@ import React, { Fragment, FunctionComponent, useEffect, useRef } from 'react'; import { EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { IErrorObject } from '../../../../../../types'; +import { IErrorObject, AlertsContextValue } from '../../../../../../triggers_actions_ui/public'; import { ES_GEO_SHAPE_TYPES, GeoThresholdAlertParams } from '../../types'; -import { AlertsContextValue } from '../../../../../context/alerts_context'; import { GeoIndexPatternSelect } from '../util_components/geo_index_pattern_select'; import { SingleFieldSelect } from '../util_components/single_field_select'; import { ExpressionWithPopover } from '../util_components/expression_with_popover'; -import { IFieldType } from '../../../../../../../../../../src/plugins/data/common/index_patterns/fields'; -import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/common/index_patterns'; +import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; interface Props { alertParams: GeoThresholdAlertParams; @@ -117,12 +116,12 @@ export const BoundaryIndexExpression: FunctionComponent = ({ = ({ = ({ defaultValue={'Select an index pattern and geo shape field'} value={boundaryIndexPattern.title} popoverContent={indexPopover} - expressionDescription={i18n.translate('xpack.triggersActionsUI.geoThreshold.indexLabel', { + expressionDescription={i18n.translate('xpack.stackAlerts.geoThreshold.indexLabel', { defaultMessage: 'index', })} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/expressions/entity_by_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_by_expression.tsx similarity index 87% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/expressions/entity_by_expression.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_by_expression.tsx index f355d25796b7c..f519ad882802c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/expressions/entity_by_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_by_expression.tsx @@ -8,10 +8,10 @@ import React, { FunctionComponent, useEffect, useRef } from 'react'; import { EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import _ from 'lodash'; -import { IErrorObject } from '../../../../../../types'; +import { IErrorObject } from '../../../../../../triggers_actions_ui/public'; import { SingleFieldSelect } from '../util_components/single_field_select'; import { ExpressionWithPopover } from '../util_components/expression_with_popover'; -import { IFieldType } from '../../../../../../../../../../src/plugins/data/common/index_patterns/fields'; +import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; interface Props { errors: IErrorObject; @@ -59,7 +59,7 @@ export const EntityByExpression: FunctionComponent = ({ = ({ value={entity} defaultValue={'Select entity field'} popoverContent={indexPopover} - expressionDescription={i18n.translate('xpack.triggersActionsUI.geoThreshold.entityByLabel', { + expressionDescription={i18n.translate('xpack.stackAlerts.geoThreshold.entityByLabel', { defaultMessage: 'by', })} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx similarity index 83% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx index 506530c171cd4..e5e43210d1e6b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/expressions/entity_index_expression.tsx @@ -8,14 +8,13 @@ import React, { Fragment, FunctionComponent, useEffect, useRef } from 'react'; import { EuiFormRow } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { IErrorObject } from '../../../../../../types'; +import { IErrorObject, AlertsContextValue } from '../../../../../../triggers_actions_ui/public'; import { ES_GEO_FIELD_TYPES } from '../../types'; -import { AlertsContextValue } from '../../../../../context/alerts_context'; import { GeoIndexPatternSelect } from '../util_components/geo_index_pattern_select'; import { SingleFieldSelect } from '../util_components/single_field_select'; import { ExpressionWithPopover } from '../util_components/expression_with_popover'; -import { IFieldType } from '../../../../../../../../../../src/plugins/data/common/index_patterns/fields'; -import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/common/index_patterns'; +import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; interface Props { dateField: string; @@ -105,13 +104,13 @@ export const EntityIndexExpression: FunctionComponent = ({ fullWidth label={ } > = ({ = ({ value={indexPattern.title} defaultValue={'Select an index pattern and geo shape/point field'} popoverContent={indexPopover} - expressionDescription={i18n.translate( - 'xpack.triggersActionsUI.geoThreshold.entityIndexLabel', - { - defaultMessage: 'index', - } - )} + expressionDescription={i18n.translate('xpack.stackAlerts.geoThreshold.entityIndexLabel', { + defaultMessage: 'index', + })} /> ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/index.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx similarity index 89% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/index.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx index ccc2ddd9c01ca..f138c08c0f993 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/index.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/index.tsx @@ -19,15 +19,17 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { AlertTypeParamsExpressionProps } from '../../../../../types'; +import { + AlertTypeParamsExpressionProps, + getTimeOptions, + AlertsContextValue, +} from '../../../../../triggers_actions_ui/public'; import { GeoThresholdAlertParams, TrackingEvent } from '../types'; -import { AlertsContextValue } from '../../../../context/alerts_context'; import { ExpressionWithPopover } from './util_components/expression_with_popover'; import { EntityIndexExpression } from './expressions/entity_index_expression'; import { EntityByExpression } from './expressions/entity_by_expression'; import { BoundaryIndexExpression } from './expressions/boundary_index_expression'; -import { IIndexPattern } from '../../../../../../../../../src/plugins/data/common/index_patterns'; -import { getTimeOptions } from '../../../../../common/lib/get_time_options'; +import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns'; const DEFAULT_VALUES = { TRACKING_EVENT: '', @@ -45,20 +47,20 @@ const DEFAULT_VALUES = { }; const conditionOptions = Object.keys(TrackingEvent).map((key) => ({ - text: (TrackingEvent as any)[key], - value: (TrackingEvent as any)[key], + text: TrackingEvent[key as TrackingEvent], + value: TrackingEvent[key as TrackingEvent], })); const labelForDelayOffset = ( <> {' '} @@ -125,7 +127,7 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent

@@ -221,7 +223,7 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent
@@ -251,7 +253,7 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent
@@ -280,19 +282,16 @@ export const GeoThresholdAlertTypeExpression: React.FunctionComponent } - expressionDescription={i18n.translate( - 'xpack.triggersActionsUI.geoThreshold.whenEntityLabel', - { - defaultMessage: 'when entity', - } - )} + expressionDescription={i18n.translate('xpack.stackAlerts.geoThreshold.whenEntityLabel', { + defaultMessage: 'when entity', + })} />
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/util_components/expression_with_popover.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/expression_with_popover.tsx similarity index 88% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/util_components/expression_with_popover.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/expression_with_popover.tsx index 7e1cae51f1411..a83667cfd92c6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/util_components/expression_with_popover.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/expression_with_popover.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; +import React, { ReactNode, useState } from 'react'; import { EuiButtonIcon, EuiExpression, @@ -22,10 +22,10 @@ export const ExpressionWithPopover: ({ value, isInvalid, }: { - popoverContent: any; - expressionDescription: any; - defaultValue?: any; - value?: any; + popoverContent: ReactNode; + expressionDescription: ReactNode; + defaultValue?: ReactNode; + value?: ReactNode; isInvalid?: boolean; }) => JSX.Element = ({ popoverContent, expressionDescription, defaultValue, value, isInvalid }) => { const [popoverOpen, setPopoverOpen] = useState(false); @@ -61,7 +61,7 @@ export const ExpressionWithPopover: ({ iconType="cross" color="danger" aria-label={i18n.translate( - 'xpack.triggersActionsUI.sections.alertAdd.geoThreshold.closePopoverLabel', + 'xpack.stackAlerts.geoThreshold.ui.expressionPopover.closePopoverLabel', { defaultMessage: 'Close', } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/util_components/geo_index_pattern_select.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/geo_index_pattern_select.tsx similarity index 80% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/util_components/geo_index_pattern_select.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/geo_index_pattern_select.tsx index 42995dfb1b9d6..a552d6d998c7e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/util_components/geo_index_pattern_select.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/geo_index_pattern_select.tsx @@ -14,6 +14,7 @@ import { HttpSetup } from 'kibana/public'; interface Props { onChange: (indexPattern: IndexPattern) => void; value: string | undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any IndexPatternSelectComponent: any; indexPatternService: IndexPatternsContract | undefined; http: HttpSetup; @@ -39,7 +40,7 @@ export class GeoIndexPatternSelect extends Component { this._isMounted = true; } - _onIndexPatternSelect = async (indexPatternId: any) => { + _onIndexPatternSelect = async (indexPatternId: string) => { if (!indexPatternId || indexPatternId.length === 0 || !this.props.indexPatternService) { return; } @@ -70,42 +71,39 @@ export class GeoIndexPatternSelect extends Component { return ( <>

@@ -123,7 +121,7 @@ export class GeoIndexPatternSelect extends Component { {this._renderNoIndexPatternWarning()} @@ -133,7 +131,7 @@ export class GeoIndexPatternSelect extends Component { indexPatternId={this.props.value} onChange={this._onIndexPatternSelect} placeholder={i18n.translate( - 'xpack.triggersActionsUI.geoThreshold.indexPatternSelectPlaceholder', + 'xpack.stackAlerts.geoThreshold.indexPatternSelectPlaceholder', { defaultMessage: 'Select index pattern', } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/util_components/single_field_select.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/single_field_select.tsx similarity index 96% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/util_components/single_field_select.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/single_field_select.tsx index 30389b31ce8c8..ef6e6f6f5e18f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/query_builder/util_components/single_field_select.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/query_builder/util_components/single_field_select.tsx @@ -14,7 +14,7 @@ import { EuiFlexItem, } from '@elastic/eui'; import { IFieldType } from 'src/plugins/data/public'; -import { FieldIcon } from '../../../../../../../../../../src/plugins/kibana_react/public'; +import { FieldIcon } from '../../../../../../../../src/plugins/kibana_react/public'; function fieldsToOptions(fields?: IFieldType[]): Array> { if (!fields) { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/types.ts b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/types.ts similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/types.ts rename to x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/types.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/validation.test.ts b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/validation.test.ts similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/validation.test.ts rename to x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/validation.test.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/validation.ts b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/validation.ts similarity index 72% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/validation.ts rename to x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/validation.ts index 078a88d9e8415..7a511f681ecaa 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/geo_threshold/validation.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_threshold/validation.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { ValidationResult } from '../../../../types'; +import { ValidationResult } from '../../../../triggers_actions_ui/public'; import { GeoThresholdAlertParams } from './types'; export const validateExpression = (alertParams: GeoThresholdAlertParams): ValidationResult => { @@ -35,7 +35,7 @@ export const validateExpression = (alertParams: GeoThresholdAlertParams): Valida if (!index) { errors.index.push( - i18n.translate('xpack.triggersActionsUI.geoThreshold.error.requiredIndexTitleText', { + i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredIndexTitleText', { defaultMessage: 'Index pattern is required.', }) ); @@ -43,7 +43,7 @@ export const validateExpression = (alertParams: GeoThresholdAlertParams): Valida if (!geoField) { errors.geoField.push( - i18n.translate('xpack.triggersActionsUI.geoThreshold.error.requiredGeoFieldText', { + i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredGeoFieldText', { defaultMessage: 'Geo field is required.', }) ); @@ -51,7 +51,7 @@ export const validateExpression = (alertParams: GeoThresholdAlertParams): Valida if (!entity) { errors.entity.push( - i18n.translate('xpack.triggersActionsUI.geoThreshold.error.requiredEntityText', { + i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredEntityText', { defaultMessage: 'Entity is required.', }) ); @@ -59,7 +59,7 @@ export const validateExpression = (alertParams: GeoThresholdAlertParams): Valida if (!dateField) { errors.dateField.push( - i18n.translate('xpack.triggersActionsUI.geoThreshold.error.requiredDateFieldText', { + i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredDateFieldText', { defaultMessage: 'Date field is required.', }) ); @@ -67,7 +67,7 @@ export const validateExpression = (alertParams: GeoThresholdAlertParams): Valida if (!trackingEvent) { errors.trackingEvent.push( - i18n.translate('xpack.triggersActionsUI.geoThreshold.error.requiredTrackingEventText', { + i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredTrackingEventText', { defaultMessage: 'Tracking event is required.', }) ); @@ -75,7 +75,7 @@ export const validateExpression = (alertParams: GeoThresholdAlertParams): Valida if (!boundaryType) { errors.boundaryType.push( - i18n.translate('xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryTypeText', { + i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredBoundaryTypeText', { defaultMessage: 'Boundary type is required.', }) ); @@ -83,7 +83,7 @@ export const validateExpression = (alertParams: GeoThresholdAlertParams): Valida if (!boundaryIndexTitle) { errors.boundaryIndexTitle.push( - i18n.translate('xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryIndexTitleText', { + i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredBoundaryIndexTitleText', { defaultMessage: 'Boundary index pattern title is required.', }) ); @@ -91,7 +91,7 @@ export const validateExpression = (alertParams: GeoThresholdAlertParams): Valida if (!boundaryGeoField) { errors.boundaryGeoField.push( - i18n.translate('xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryGeoFieldText', { + i18n.translate('xpack.stackAlerts.geoThreshold.error.requiredBoundaryGeoFieldText', { defaultMessage: 'Boundary geo field is required.', }) ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/index.ts b/x-pack/plugins/stack_alerts/public/alert_types/index.ts similarity index 57% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/index.ts rename to x-pack/plugins/stack_alerts/public/alert_types/index.ts index 4b2860dcf9b72..61cf7193fedb7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/index.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/index.ts @@ -6,18 +6,17 @@ import { getAlertType as getGeoThresholdAlertType } from './geo_threshold'; import { getAlertType as getThresholdAlertType } from './threshold'; -import { TypeRegistry } from '../../type_registry'; -import { AlertTypeModel } from '../../../types'; -import { TriggersActionsUiConfigType } from '../../../plugin'; +import { Config } from '../../common'; +import { TriggersAndActionsUIPublicPluginSetup } from '../../../triggers_actions_ui/public'; -export function registerBuiltInAlertTypes({ +export function registerAlertTypes({ alertTypeRegistry, - triggerActionsUiConfig, + config, }: { - alertTypeRegistry: TypeRegistry; - triggerActionsUiConfig: TriggersActionsUiConfigType; + alertTypeRegistry: TriggersAndActionsUIPublicPluginSetup['alertTypeRegistry']; + config: Config; }) { - if (triggerActionsUiConfig.enableGeoTrackingThresholdAlert) { + if (config.enableGeoTrackingThresholdAlert) { alertTypeRegistry.register(getGeoThresholdAlertType()); } alertTypeRegistry.register(getThresholdAlertType()); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.scss b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.scss similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.scss rename to x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.scss diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx similarity index 86% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx index e309d97b57f34..92cb8c9055bde 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx @@ -29,21 +29,20 @@ import { getIndexPatterns, getIndexOptions, getFields, -} from '../../../../common/index_controls'; -import { COMPARATORS, builtInComparators } from '../../../../common/constants'; -import { getTimeFieldOptions } from '../../../../common/lib/get_time_options'; -import { ThresholdVisualization } from './visualization'; -import { WhenExpression } from '../../../../common'; -import { + COMPARATORS, + builtInComparators, + getTimeFieldOptions, OfExpression, ThresholdExpression, ForLastExpression, GroupByExpression, -} from '../../../../common'; -import { builtInAggregationTypes } from '../../../../common/constants'; + WhenExpression, + builtInAggregationTypes, + AlertTypeParamsExpressionProps, + AlertsContextValue, +} from '../../../../triggers_actions_ui/public'; +import { ThresholdVisualization } from './visualization'; import { IndexThresholdAlertParams } from './types'; -import { AlertTypeParamsExpressionProps } from '../../../../types'; -import { AlertsContextValue } from '../../../context/alerts_context'; import './expression.scss'; const DEFAULT_VALUES = { @@ -89,7 +88,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent>([]); + const [esFields, setEsFields] = useState([]); const [indexOptions, setIndexOptions] = useState([]); const [timeFieldOptions, setTimeFieldOptions] = useState([firstFieldOption]); const [isIndiciesLoading, setIsIndiciesLoading] = useState(false); @@ -98,7 +97,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent expressionFieldsWithValidation.includes(errorKey) && errors[errorKey].length >= 1 && - (alertParams as { [key: string]: any })[errorKey] !== undefined + alertParams[errorKey as keyof IndexThresholdAlertParams] !== undefined ); const canShowVizualization = !!Object.keys(errors).find( @@ -106,7 +105,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent 0) { const currentEsFields = await getFields(http, index); - const timeFields = getTimeFieldOptions(currentEsFields as any); + const timeFields = getTimeFieldOptions(currentEsFields); setEsFields(currentEsFields); setTimeFieldOptions([firstFieldOption, ...timeFields]); @@ -159,7 +158,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent } @@ -167,7 +166,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent } @@ -211,7 +210,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent } @@ -284,7 +283,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent

@@ -296,12 +295,9 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent 0 ? renderIndices(index) : firstFieldOption.text} isActive={indexPopoverOpen} onClick={() => { @@ -321,12 +317,9 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent - {i18n.translate( - 'xpack.triggersActionsUI.sections.alertAdd.threshold.indexButtonLabel', - { - defaultMessage: 'index', - } - )} + {i18n.translate('xpack.stackAlerts.threshold.ui.alertParams.indexButtonLabel', { + defaultMessage: 'index', + })}
@@ -411,10 +404,10 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent + onChangeWindowSize={(selectedWindowSize: number | undefined) => setAlertParams('timeWindowSize', selectedWindowSize) } - onChangeWindowUnit={(selectedWindowUnit: any) => + onChangeWindowUnit={(selectedWindowUnit: string) => setAlertParams('timeWindowUnit', selectedWindowUnit) } /> @@ -427,7 +420,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/index.ts b/x-pack/plugins/stack_alerts/public/alert_types/threshold/index.ts similarity index 77% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/index.ts rename to x-pack/plugins/stack_alerts/public/alert_types/threshold/index.ts index a5b2fbb37e838..b7923a3013613 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/index.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/index.ts @@ -5,18 +5,17 @@ */ import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; -import { AlertTypeModel } from '../../../../types'; import { validateExpression } from './validation'; import { IndexThresholdAlertParams } from './types'; -import { AlertsContextValue } from '../../../context/alerts_context'; +import { AlertTypeModel, AlertsContextValue } from '../../../../triggers_actions_ui/public'; export function getAlertType(): AlertTypeModel { return { id: '.index-threshold', - name: i18n.translate('xpack.triggersActionsUI.indexThresholdAlert.nameText', { + name: i18n.translate('xpack.stackAlerts.threshold.ui.alertType.nameText', { defaultMessage: 'Index threshold', }), - description: i18n.translate('xpack.triggersActionsUI.indexThresholdAlert.descriptionText', { + description: i18n.translate('xpack.stackAlerts.threshold.ui.alertType.descriptionText', { defaultMessage: 'Alert when an aggregated query meets the threshold.', }), iconClass: 'alert', @@ -26,7 +25,7 @@ export function getAlertType(): AlertTypeModel import('./expression')), validate: validateExpression, defaultActionMessage: i18n.translate( - 'xpack.triggersActionsUI.components.builtinAlertTypes.threshold.alertDefaultActionMessage', + 'xpack.stackAlerts.threshold.ui.alertType.defaultActionMessage', { defaultMessage: `alert \\{\\{alertName\\}\\} group \\{\\{context.group\\}\\} value \\{\\{context.value\\}\\} exceeded threshold \\{\\{context.function\\}\\} over \\{\\{params.timeWindowSize\\}\\}\\{\\{params.timeWindowUnit\\}\\} on \\{\\{context.date\\}\\}`, } diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/index_threshold_api.ts b/x-pack/plugins/stack_alerts/public/alert_types/threshold/index_threshold_api.ts new file mode 100644 index 0000000000000..ec531b26fc8c6 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/index_threshold_api.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { HttpSetup } from 'kibana/public'; +import { TimeSeriesResult } from '../../../../triggers_actions_ui/common'; +import { IndexThresholdAlertParams } from './types'; + +const INDEX_THRESHOLD_DATA_API_ROOT = '/api/triggers_actions_ui/data'; + +export interface GetThresholdAlertVisualizationDataParams { + model: IndexThresholdAlertParams; + visualizeOptions: { + rangeFrom: string; + rangeTo: string; + interval: string; + }; + http: HttpSetup; +} + +export async function getThresholdAlertVisualizationData({ + model, + visualizeOptions, + http, +}: GetThresholdAlertVisualizationDataParams): Promise { + const timeSeriesQueryParams = { + index: model.index, + timeField: model.timeField, + aggType: model.aggType, + aggField: model.aggField, + groupBy: model.groupBy, + termField: model.termField, + termSize: model.termSize, + timeWindowSize: model.timeWindowSize, + timeWindowUnit: model.timeWindowUnit, + dateStart: new Date(visualizeOptions.rangeFrom).toISOString(), + dateEnd: new Date(visualizeOptions.rangeTo).toISOString(), + interval: visualizeOptions.interval, + }; + + return await http.post(`${INDEX_THRESHOLD_DATA_API_ROOT}/_time_series_query`, { + body: JSON.stringify(timeSeriesQueryParams), + }); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/types.ts b/x-pack/plugins/stack_alerts/public/alert_types/threshold/types.ts similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/types.ts rename to x-pack/plugins/stack_alerts/public/alert_types/threshold/types.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/validation.test.ts b/x-pack/plugins/stack_alerts/public/alert_types/threshold/validation.test.ts similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/validation.test.ts rename to x-pack/plugins/stack_alerts/public/alert_types/threshold/validation.test.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/validation.ts b/x-pack/plugins/stack_alerts/public/alert_types/threshold/validation.ts similarity index 81% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/validation.ts rename to x-pack/plugins/stack_alerts/public/alert_types/threshold/validation.ts index 3912b2fffae1e..4bbf80906377b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/validation.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/validation.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { ValidationResult } from '../../../../types'; import { IndexThresholdAlertParams } from './types'; import { + ValidationResult, builtInGroupByTypes, builtInAggregationTypes, builtInComparators, -} from '../../../../common/constants'; +} from '../../../../triggers_actions_ui/public'; export const validateExpression = (alertParams: IndexThresholdAlertParams): ValidationResult => { const { @@ -39,21 +39,21 @@ export const validateExpression = (alertParams: IndexThresholdAlertParams): Vali validationResult.errors = errors; if (!index || index.length === 0) { errors.index.push( - i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredIndexText', { + i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.requiredIndexText', { defaultMessage: 'Index is required.', }) ); } if (!timeField) { errors.timeField.push( - i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredTimeFieldText', { + i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.requiredTimeFieldText', { defaultMessage: 'Time field is required.', }) ); } if (aggType && builtInAggregationTypes[aggType].fieldRequired && !aggField) { errors.aggField.push( - i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredAggFieldText', { + i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.requiredAggFieldText', { defaultMessage: 'Aggregation field is required.', }) ); @@ -65,7 +65,7 @@ export const validateExpression = (alertParams: IndexThresholdAlertParams): Vali !termSize ) { errors.termSize.push( - i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredTermSizedText', { + i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.requiredTermSizedText', { defaultMessage: 'Term size is required.', }) ); @@ -77,21 +77,21 @@ export const validateExpression = (alertParams: IndexThresholdAlertParams): Vali !termField ) { errors.termField.push( - i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredtTermFieldText', { + i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.requiredTermFieldText', { defaultMessage: 'Term field is required.', }) ); } if (!timeWindowSize) { errors.timeWindowSize.push( - i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredTimeWindowSizeText', { + i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.requiredTimeWindowSizeText', { defaultMessage: 'Time window size is required.', }) ); } if (!threshold || threshold.length === 0 || threshold[0] === undefined) { errors.threshold0.push( - i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredThreshold0Text', { + i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold0Text', { defaultMessage: 'Threshold0 is required.', }) ); @@ -104,14 +104,14 @@ export const validateExpression = (alertParams: IndexThresholdAlertParams): Vali (threshold && threshold.length < builtInComparators[thresholdComparator!].requiredValues)) ) { errors.threshold1.push( - i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.requiredThreshold1Text', { + i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold1Text', { defaultMessage: 'Threshold1 is required.', }) ); } if (threshold && threshold.length === 2 && threshold[0] > threshold[1]) { errors.threshold1.push( - i18n.translate('xpack.triggersActionsUI.sections.addAlert.error.greaterThenThreshold0Text', { + i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.greaterThenThreshold0Text', { defaultMessage: 'Threshold1 should be > Threshold0.', }) ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx b/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx similarity index 86% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx rename to x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx index a282fa08e8f38..6145aa3671a7f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/visualization.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx @@ -29,11 +29,17 @@ import { EuiLoadingSpinner, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { getThresholdAlertVisualizationData } from '../../../../common/lib/index_threshold_api'; -import { AggregationType, Comparator } from '../../../../common/types'; -import { AlertsContextValue } from '../../../context/alerts_context'; +import { + getThresholdAlertVisualizationData, + GetThresholdAlertVisualizationDataParams, +} from './index_threshold_api'; +import { + AlertsContextValue, + AggregationType, + Comparator, +} from '../../../../triggers_actions_ui/public'; import { IndexThresholdAlertParams } from './types'; -import { parseDuration } from '../../../../../../alerts/common/parse_duration'; +import { parseDuration } from '../../../../alerts/common/parse_duration'; const customTheme = () => { return { @@ -125,7 +131,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ const { http, toastNotifications, charts, uiSettings, dataFieldsFormats } = alertsContext; const [loadingState, setLoadingState] = useState(null); - const [error, setError] = useState(undefined); + const [error, setError] = useState(undefined); const [visualizationData, setVisualizationData] = useState>(); const [startVisualizationAt, setStartVisualizationAt] = useState(new Date()); @@ -150,7 +156,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ if (toastNotifications) { toastNotifications.addDanger({ title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertAdd.unableToLoadVisualizationMessage', + 'xpack.stackAlerts.threshold.ui.visualization.unableToLoadVisualizationMessage', { defaultMessage: 'Unable to load visualization' } ), }); @@ -199,7 +205,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ body={ @@ -215,7 +221,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ @@ -239,7 +245,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ const alertVisualizationDataKeys = Object.keys(visualizationData); const timezone = getTimezone(uiSettings); const actualThreshold = getThreshold(); - let maxY = actualThreshold[actualThreshold.length - 1] as any; + let maxY = actualThreshold[actualThreshold.length - 1]; (Object.values(visualizationData) as number[][][]).forEach((data) => { data.forEach(([, y]) => { @@ -288,14 +294,14 @@ export const ThresholdVisualization: React.FunctionComponent = ({ /> ); })} - {actualThreshold.map((_value: any, i: number) => { - const specId = i === 0 ? 'threshold' : `threshold${i}`; + {actualThreshold.map((_value: number, thresholdIndex: number) => { + const specId = thresholdIndex === 0 ? 'threshold' : `threshold${thresholdIndex}`; return ( ); })} @@ -305,14 +311,14 @@ export const ThresholdVisualization: React.FunctionComponent = ({ size="s" title={ } color="warning" > @@ -325,7 +331,11 @@ export const ThresholdVisualization: React.FunctionComponent = ({ }; // convert the data from the visualization API into something easier to digest with charts -async function getVisualizationData(model: any, visualizeOptions: any, http: HttpSetup) { +async function getVisualizationData( + model: IndexThresholdAlertParams, + visualizeOptions: GetThresholdAlertVisualizationDataParams['visualizeOptions'], + http: HttpSetup +) { const vizData = await getThresholdAlertVisualizationData({ model, visualizeOptions, diff --git a/x-pack/plugins/stack_alerts/public/index.ts b/x-pack/plugins/stack_alerts/public/index.ts new file mode 100644 index 0000000000000..2f84a5949f111 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext } from 'src/core/public'; +import { StackAlertsPublicPlugin } from './plugin'; + +export const plugin = (ctx: PluginInitializerContext) => new StackAlertsPublicPlugin(ctx); diff --git a/x-pack/plugins/stack_alerts/public/plugin.tsx b/x-pack/plugins/stack_alerts/public/plugin.tsx new file mode 100644 index 0000000000000..63176e7b30277 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/plugin.tsx @@ -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 { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/public'; +import { TriggersAndActionsUIPublicPluginSetup } from '../../../plugins/triggers_actions_ui/public'; +import { registerAlertTypes } from './alert_types'; +import { Config } from '../common'; + +export type Setup = void; +export type Start = void; + +export interface StackAlertsPublicSetupDeps { + triggersActionsUi: TriggersAndActionsUIPublicPluginSetup; +} + +export class StackAlertsPublicPlugin implements Plugin { + private initializerContext: PluginInitializerContext; + + constructor(initializerContext: PluginInitializerContext) { + this.initializerContext = initializerContext; + } + + public setup(core: CoreSetup, { triggersActionsUi }: StackAlertsPublicSetupDeps) { + registerAlertTypes({ + alertTypeRegistry: triggersActionsUi.alertTypeRegistry, + config: this.initializerContext.config.get(), + }); + } + + public start() {} + public stop() {} +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts index 360a6eb169573..9fc46fe2f2586 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/alert_type.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; -import { Service } from '../../types'; +import { Logger } from 'src/core/server'; import { STACK_ALERTS_FEATURE_ID } from '../../../common'; import { getGeoThresholdExecutor } from './geo_threshold'; import { @@ -173,7 +173,7 @@ export interface GeoThresholdParams { } export function getAlertType( - service: Omit + logger: Logger ): { defaultActionGroupId: string; actionGroups: ActionGroup[]; @@ -222,7 +222,7 @@ export function getAlertType( name: alertTypeName, actionGroups: [{ id: ActionGroupId, name: actionGroupName }], defaultActionGroupId: ActionGroupId, - executor: getGeoThresholdExecutor(service), + executor: getGeoThresholdExecutor(logger), producer: STACK_ALERTS_FEATURE_ID, validate: { params: ParamsSchema, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/es_query_builder.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/es_query_builder.ts index c4238e62ff261..97be51b2a6256 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/es_query_builder.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/es_query_builder.ts @@ -6,7 +6,7 @@ import { ILegacyScopedClusterClient } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; -import { Logger } from '../../types'; +import { Logger } from 'src/core/server'; export const OTHER_CATEGORY = 'other'; // Consider dynamically obtaining from config? diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts index f30dea151ece8..394ee8d606abe 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/geo_threshold.ts @@ -6,10 +6,10 @@ import _ from 'lodash'; import { SearchResponse } from 'elasticsearch'; +import { Logger } from 'src/core/server'; import { executeEsQueryFactory, getShapesFilters, OTHER_CATEGORY } from './es_query_builder'; import { AlertServices, AlertTypeState } from '../../../../alerts/server'; import { ActionGroupId, GEO_THRESHOLD_ID, GeoThresholdParams } from './alert_type'; -import { Logger } from '../../types'; interface LatestEntityLocation { location: number[]; @@ -169,7 +169,7 @@ function getOffsetTime(delayOffsetWithUnits: string, oldTime: Date): Date { return adjustedDate; } -export const getGeoThresholdExecutor = ({ logger: log }: { logger: Logger }) => +export const getGeoThresholdExecutor = (log: Logger) => async function ({ previousStartedAt, startedAt, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/index.ts index d57f219bb8f9a..2fa2bed9d8419 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/index.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/index.ts @@ -4,15 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Service, AlertingSetup } from '../../types'; +import { Logger } from 'src/core/server'; +import { AlertingSetup } from '../../types'; import { getAlertType } from './alert_type'; interface RegisterParams { - service: Omit; + logger: Logger; alerts: AlertingSetup; } export function register(params: RegisterParams) { - const { service, alerts } = params; - alerts.registerType(getAlertType(service)); + const { logger, alerts } = params; + alerts.registerType(getAlertType(logger)); } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/alert_type.test.ts index 5cf113f519a5a..49b56b5571b44 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_threshold/tests/alert_type.test.ts @@ -8,11 +8,9 @@ import { loggingSystemMock } from '../../../../../../../src/core/server/mocks'; import { getAlertType, GeoThresholdParams } from '../alert_type'; describe('alertType', () => { - const service = { - logger: loggingSystemMock.create().get(), - }; + const logger = loggingSystemMock.create().get(); - const alertType = getAlertType(service); + const alertType = getAlertType(logger); it('alert type creation structure is the expected value', async () => { expect(alertType.id).toBe('.geo-threshold'); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/index.ts index dd9f1488092f4..461358d1296e2 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index.ts @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Service, IRouter, AlertingSetup } from '../types'; +import { Logger } from 'src/core/server'; +import { AlertingSetup, StackAlertsStartDeps } from '../types'; import { register as registerIndexThreshold } from './index_threshold'; import { register as registerGeoThreshold } from './geo_threshold'; interface RegisterAlertTypesParams { - service: Service; - router: IRouter; + logger: Logger; + data: Promise; alerts: AlertingSetup; - baseRoute: string; } export function registerBuiltInAlertTypes(params: RegisterAlertTypesParams) { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/README.md b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/README.md index 0ff01ddfb49c7..9b0eb23950cc3 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/README.md +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/README.md @@ -13,10 +13,7 @@ is exceeded. ## alertType `.index-threshold` -The alertType parameters are specified in -[`lib/core_query_types.ts`][it-core-query] -and -[`alert_type_params.ts`][it-alert-params]. +The alertType parameters are specified in [`alert_type_params.ts`][it-alert-params]. The alertType has a single actionGroup, `'threshold met'`. The `context` object provided to actions is specified in @@ -123,227 +120,6 @@ server log [17:32:10.060] [warning][actions][actions][plugins] \ [now-iso]: https://github.com/pmuellr/now-iso -## http endpoints +## Data Apis via the TriggersActionsUi plugin and its http endpoints -The following endpoints are provided for this alert type: - -- `POST /api/stack_alerts/index_threshold/_indices` -- `POST /api/stack_alerts/index_threshold/_fields` -- `POST /api/stack_alerts/index_threshold/_time_series_query` - -### `POST .../_indices` - -This HTTP endpoint is provided for the alerting ui to list the available -"index names" for the user to select to use with the alert. This API also -returns aliases which match the supplied pattern. - -The request body is expected to be a JSON object in the following form, where the -`pattern` value may include comma-separated names and wildcards. - -```js -{ - pattern: "index-name-pattern" -} -``` - -The response body is a JSON object in the following form, where each element -of the `indices` array is the name of an index or alias. The number of elements -returned is limited, as this API is intended to be used to help narrow down -index names to use with the alert, and not support pagination, etc. - -```js -{ - indices: ["index-name-1", "alias-name-1", ...] -} -``` - -### `POST .../_fields` - -This HTTP endpoint is provided for the alerting ui to list the available -fields for the user to select to use with the alert. - -The request body is expected to be a JSON object in the following form, where the -`indexPatterns` array elements may include comma-separated names and wildcards. - -```js -{ - indexPatterns: ["index-pattern-1", "index-pattern-2"] -} -``` - -The response body is a JSON object in the following form, where each element -fields array is a field object. - -```js -{ - fields: [fieldObject1, fieldObject2, ...] -} -``` - -A field object is the following shape: - -```typescript -{ - name: string, // field name - type: string, // field type - eg 'keyword', 'date', 'long', etc - normalizedType: string, // for numeric types, this will be 'number' - aggregatable: true, // value from elasticsearch field capabilities - searchable: true, // value from elasticsearch field capabilities -} -``` - -### `POST .../_time_series_query` - -This HTTP endpoint is provided to return the values the alertType would calculate, -over a series of time. It is intended to be used in the alerting UI to -provide a "preview" of the alert during creation/editing based on recent data, -and could be used to show a "simulation" of the the alert over an arbitrary -range of time. - -The endpoint is `POST /api/stack_alerts/index_threshold/_time_series_query`. -The request and response bodies are specifed in -[`lib/core_query_types.ts`][it-core-query] -and -[`lib/time_series_types.ts`][it-timeSeries-types]. -The request body is very similar to the alertType's parameters. - -### example - -Continuing with the example above, here's a query to get the values calculated -for the last 10 seconds. -This example uses [now-iso][] to generate iso date strings. - -```console -curl -k "https://elastic:changeme@localhost:5601/api/stack_alerts/index_threshold/_time_series_query" \ - -H "kbn-xsrf: foo" -H "content-type: application/json" -d "{ - \"index\": \"es-hb-sim\", - \"timeField\": \"@timestamp\", - \"aggType\": \"avg\", - \"aggField\": \"summary.up\", - \"groupBy\": \"top\", - \"termSize\": 100, - \"termField\": \"monitor.name.keyword\", - \"interval\": \"1s\", - \"dateStart\": \"`now-iso -10s`\", - \"dateEnd\": \"`now-iso`\", - \"timeWindowSize\": 5, - \"timeWindowUnit\": \"s\" -}" -``` - -``` -{ - "results": [ - { - "group": "host-A", - "metrics": [ - [ "2020-02-26T15:10:40.000Z", 0 ], - [ "2020-02-26T15:10:41.000Z", 0 ], - [ "2020-02-26T15:10:42.000Z", 0 ], - [ "2020-02-26T15:10:43.000Z", 0 ], - [ "2020-02-26T15:10:44.000Z", 0 ], - [ "2020-02-26T15:10:45.000Z", 0 ], - [ "2020-02-26T15:10:46.000Z", 0 ], - [ "2020-02-26T15:10:47.000Z", 0 ], - [ "2020-02-26T15:10:48.000Z", 0 ], - [ "2020-02-26T15:10:49.000Z", 0 ], - [ "2020-02-26T15:10:50.000Z", 0 ] - ] - } - ] -} -``` - -To get the current value of the calculated metric, you can leave off the date: - -``` -curl -k "https://elastic:changeme@localhost:5601/api/stack_alerts/index_threshold/_time_series_query" \ - -H "kbn-xsrf: foo" -H "content-type: application/json" -d '{ - "index": "es-hb-sim", - "timeField": "@timestamp", - "aggType": "avg", - "aggField": "summary.up", - "groupBy": "top", - "termField": "monitor.name.keyword", - "termSize": 100, - "interval": "1s", - "timeWindowSize": 5, - "timeWindowUnit": "s" -}' -``` - -``` -{ - "results": [ - { - "group": "host-A", - "metrics": [ - [ "2020-02-26T15:23:36.635Z", 0 ] - ] - } - ] -} -``` - -[it-timeSeries-types]: lib/time_series_types.ts - -## service functions - -A single service function is available that provides the functionality -of the http endpoint `POST /api/stack_alerts/index_threshold/_time_series_query`, -but as an API for Kibana plugins. The function is available as -`alertingService.indexThreshold.timeSeriesQuery()` - -The parameters and return value for the function are the same as for the HTTP -request, though some additional parameters are required (logger, callCluster, -etc). - -## notes on the timeSeriesQuery API / http endpoint - -This API provides additional parameters beyond what the alertType itself uses: - -- `dateStart` -- `dateEnd` -- `interval` - -The `dateStart` and `dateEnd` parameters are ISO date strings. - -The `interval` parameter is intended to model the `interval` the alert is -currently using, and uses the same `1s`, `2m`, `3h`, etc format. Over the -supplied date range, a time-series data point will be calculated every -`interval` duration. - -So the number of time-series points in the output of the API should be: - -``` -( dateStart - dateEnd ) / interval -``` - -Example: - -``` -dateStart: '2020-01-01T00:00:00' -dateEnd: '2020-01-02T00:00:00' -interval: '1h' -``` - -The date range is 1 day === 24 hours. The interval is 1 hour. So there should -be ~24 time series points in the output. - -For preview purposes: - -- The `termSize` parameter should be used to help cut -down on the amount of work ES does, and keep the generated graphs a little -simpler. Probably something like `10`. - -- For queries with long date ranges, you probably don't want to use the -`interval` the alert is set to, as the `interval` used in the query, as this -could result in a lot of time-series points being generated, which is both -costly in ES, and may result in noisy graphs. - -- The `timeWindow*` parameters should be the same as what the alert is using, -especially for the `count` and `sum` aggregation types. Those aggregations -don't scale the same way the others do, when the window changes. Even for -the other aggregations, changing the window could result in dramatically -different values being generated - `avg` will be more "average-y", `min` -and `max` will be a little stickier. \ No newline at end of file +The Index Threshold Alert Type is backed by Apis exposed by the [TriggersActionsUi plugin](../../../../triggers_actions_ui/README.md). diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts index d75f3af22ab06..0febe805af4e0 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts @@ -9,14 +9,12 @@ import { getAlertType } from './alert_type'; import { Params } from './alert_type_params'; describe('alertType', () => { - const service = { - indexThreshold: { - timeSeriesQuery: jest.fn(), - }, - logger: loggingSystemMock.create().get(), + const logger = loggingSystemMock.create().get(); + const data = { + timeSeriesQuery: jest.fn(), }; - const alertType = getAlertType(service); + const alertType = getAlertType(logger, Promise.resolve(data)); it('alert type creation structure is the expected value', async () => { expect(alertType.id).toBe('.index-threshold'); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts index e0a9cd981dac0..2d9e1b3adc1b8 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts @@ -5,23 +5,26 @@ */ import { i18n } from '@kbn/i18n'; -import { AlertType, AlertExecutorOptions } from '../../types'; +import { Logger } from 'src/core/server'; +import { AlertType, AlertExecutorOptions, StackAlertsStartDeps } from '../../types'; import { Params, ParamsSchema } from './alert_type_params'; import { ActionContext, BaseActionContext, addMessages } from './action_context'; -import { TimeSeriesQuery } from './lib/time_series_query'; -import { Service } from '../../types'; import { STACK_ALERTS_FEATURE_ID } from '../../../common'; +import { + CoreQueryParamsSchemaProperties, + TimeSeriesQuery, +} from '../../../../triggers_actions_ui/server'; export const ID = '.index-threshold'; -import { CoreQueryParamsSchemaProperties } from './lib/core_query_types'; const ActionGroupId = 'threshold met'; const ComparatorFns = getComparatorFns(); export const ComparatorFnNames = new Set(ComparatorFns.keys()); -export function getAlertType(service: Service): AlertType { - const { logger } = service; - +export function getAlertType( + logger: Logger, + data: Promise +): AlertType { const alertTypeName = i18n.translate('xpack.stackAlerts.indexThreshold.alertTypeTitle', { defaultMessage: 'Index threshold', }); @@ -152,7 +155,7 @@ export function getAlertType(service: Service): AlertType> = { index: 'index-name', @@ -71,3 +71,185 @@ describe('alertType Params validate()', () => { return ParamsSchema.validate(params); } }); + +export function runTests(schema: ObjectType, defaultTypeParams: Record): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let params: any; + + const CoreDefaultParams: Writable> = { + index: 'index-name', + timeField: 'time-field', + aggType: 'count', + groupBy: 'all', + timeWindowSize: 5, + timeWindowUnit: 'm', + }; + + describe('coreQueryTypes', () => { + beforeEach(() => { + params = { ...CoreDefaultParams, ...defaultTypeParams }; + }); + + it('succeeds with minimal properties', async () => { + expect(validate()).toBeTruthy(); + }); + + it('succeeds with maximal properties', async () => { + params.aggType = 'avg'; + params.aggField = 'agg-field'; + params.groupBy = 'top'; + params.termField = 'group-field'; + params.termSize = 200; + expect(validate()).toBeTruthy(); + + params.index = ['index-name-1', 'index-name-2']; + params.aggType = 'avg'; + params.aggField = 'agg-field'; + params.groupBy = 'top'; + params.termField = 'group-field'; + params.termSize = 200; + expect(validate()).toBeTruthy(); + }); + + it('fails for invalid index', async () => { + delete params.index; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[index]: expected at least one defined value but got [undefined]"` + ); + + params.index = 42; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot(` +"[index]: types that failed validation: +- [index.0]: expected value of type [string] but got [number] +- [index.1]: expected value of type [array] but got [number]" +`); + + params.index = ''; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot(` +"[index]: types that failed validation: +- [index.0]: value has length [0] but it must have a minimum length of [1]. +- [index.1]: could not parse array value from json input" +`); + + params.index = ['', 'a']; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot(` +"[index]: types that failed validation: +- [index.0]: expected value of type [string] but got [Array] +- [index.1.0]: value has length [0] but it must have a minimum length of [1]." +`); + }); + + it('fails for invalid timeField', async () => { + delete params.timeField; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[timeField]: expected value of type [string] but got [undefined]"` + ); + + params.timeField = 42; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[timeField]: expected value of type [string] but got [number]"` + ); + + params.timeField = ''; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[timeField]: value has length [0] but it must have a minimum length of [1]."` + ); + }); + + it('fails for invalid aggType', async () => { + params.aggType = 42; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[aggType]: expected value of type [string] but got [number]"` + ); + + params.aggType = '-not-a-valid-aggType-'; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[aggType]: invalid aggType: \\"-not-a-valid-aggType-\\""` + ); + }); + + it('fails for invalid aggField', async () => { + params.aggField = 42; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[aggField]: expected value of type [string] but got [number]"` + ); + + params.aggField = ''; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[aggField]: value has length [0] but it must have a minimum length of [1]."` + ); + }); + + it('fails for invalid termField', async () => { + params.groupBy = 'top'; + params.termField = 42; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[termField]: expected value of type [string] but got [number]"` + ); + + params.termField = ''; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[termField]: value has length [0] but it must have a minimum length of [1]."` + ); + }); + + it('fails for invalid termSize', async () => { + params.groupBy = 'top'; + params.termField = 'fee'; + params.termSize = 'foo'; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[termSize]: expected value of type [number] but got [string]"` + ); + + params.termSize = MAX_GROUPS + 1; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[termSize]: must be less than or equal to 1000"` + ); + + params.termSize = 0; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[termSize]: Value must be equal to or greater than [1]."` + ); + }); + + it('fails for invalid timeWindowSize', async () => { + params.timeWindowSize = 'foo'; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[timeWindowSize]: expected value of type [number] but got [string]"` + ); + + params.timeWindowSize = 0; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[timeWindowSize]: Value must be equal to or greater than [1]."` + ); + }); + + it('fails for invalid timeWindowUnit', async () => { + params.timeWindowUnit = 42; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[timeWindowUnit]: expected value of type [string] but got [number]"` + ); + + params.timeWindowUnit = 'x'; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[timeWindowUnit]: invalid timeWindowUnit: \\"x\\""` + ); + }); + + it('fails for invalid aggType/aggField', async () => { + params.aggType = 'avg'; + delete params.aggField; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[aggField]: must have a value when [aggType] is \\"avg\\""` + ); + }); + }); + + function onValidate(): () => void { + return () => validate(); + } + + function validate(): unknown { + return schema.validate(params); + } +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts index 34d74fa98f959..b51545770dd7b 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type_params.ts @@ -7,7 +7,10 @@ import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import { ComparatorFnNames, getInvalidComparatorMessage } from './alert_type'; -import { CoreQueryParamsSchemaProperties, validateCoreQueryBody } from './lib/core_query_types'; +import { + CoreQueryParamsSchemaProperties, + validateCoreQueryBody, +} from '../../../../triggers_actions_ui/server'; // alert type parameters diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/index.ts index 9787ece323c59..a075b0d614cbb 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/index.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/index.ts @@ -4,34 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Service, AlertingSetup, IRouter } from '../../types'; -import { timeSeriesQuery } from './lib/time_series_query'; +import { Logger } from 'src/core/server'; +import { AlertingSetup, StackAlertsStartDeps } from '../../types'; import { getAlertType } from './alert_type'; -import { registerRoutes } from './routes'; // future enhancement: make these configurable? export const MAX_INTERVALS = 1000; export const MAX_GROUPS = 1000; export const DEFAULT_GROUPS = 100; -export function getService() { - return { - timeSeriesQuery, - }; -} - interface RegisterParams { - service: Service; - router: IRouter; + logger: Logger; + data: Promise; alerts: AlertingSetup; - baseRoute: string; } export function register(params: RegisterParams) { - const { service, router, alerts, baseRoute } = params; - - alerts.registerType(getAlertType(service)); - - const baseBuiltInRoute = `${baseRoute}/index_threshold`; - registerRoutes({ service, router, baseRoute: baseBuiltInRoute }); + const { logger, data, alerts } = params; + alerts.registerType(getAlertType(logger, data)); } diff --git a/x-pack/plugins/stack_alerts/server/index.ts b/x-pack/plugins/stack_alerts/server/index.ts index 108393c0d1469..adb617558e6f4 100644 --- a/x-pack/plugins/stack_alerts/server/index.ts +++ b/x-pack/plugins/stack_alerts/server/index.ts @@ -4,15 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'src/core/server'; +import { PluginConfigDescriptor, PluginInitializerContext } from 'src/core/server'; import { AlertingBuiltinsPlugin } from './plugin'; -import { configSchema } from './config'; +import { configSchema, Config } from '../common/config'; export { ID as INDEX_THRESHOLD_ID } from './alert_types/index_threshold/alert_type'; -export const plugin = (ctx: PluginInitializerContext) => new AlertingBuiltinsPlugin(ctx); - -export const config = { +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + enableGeoTrackingThresholdAlert: true, + }, schema: configSchema, + deprecations: ({ renameFromRoot }) => [ + renameFromRoot( + 'xpack.triggers_actions_ui.enableGeoTrackingThresholdAlert', + 'xpack.stack_alerts.enableGeoTrackingThresholdAlert' + ), + ], }; -export { IService } from './types'; +export const plugin = (ctx: PluginInitializerContext) => new AlertingBuiltinsPlugin(ctx); diff --git a/x-pack/plugins/stack_alerts/server/plugin.test.ts b/x-pack/plugins/stack_alerts/server/plugin.test.ts index 3e2a919be0f13..71972707852fe 100644 --- a/x-pack/plugins/stack_alerts/server/plugin.test.ts +++ b/x-pack/plugins/stack_alerts/server/plugin.test.ts @@ -69,34 +69,5 @@ describe('AlertingBuiltins Plugin', () => { expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledWith(BUILT_IN_ALERTS_FEATURE); }); - - it('should return a service in the expected shape', async () => { - const alertingSetup = alertsMock.createSetup(); - const featuresSetup = featuresPluginMock.createSetup(); - const service = await plugin.setup(coreSetup, { - alerts: alertingSetup, - features: featuresSetup, - }); - - expect(typeof service.indexThreshold.timeSeriesQuery).toBe('function'); - }); - }); - - describe('start()', () => { - let context: ReturnType; - let plugin: AlertingBuiltinsPlugin; - let coreStart: ReturnType; - - beforeEach(() => { - context = coreMock.createPluginInitializerContext(); - plugin = new AlertingBuiltinsPlugin(context); - coreStart = coreMock.createStart(); - }); - - it('should return a service in the expected shape', async () => { - const service = await plugin.start(coreStart); - - expect(typeof service.indexThreshold.timeSeriesQuery).toBe('function'); - }); }); }); diff --git a/x-pack/plugins/stack_alerts/server/plugin.ts b/x-pack/plugins/stack_alerts/server/plugin.ts index f250bbc70fb80..66ac9e455e8b6 100644 --- a/x-pack/plugins/stack_alerts/server/plugin.ts +++ b/x-pack/plugins/stack_alerts/server/plugin.ts @@ -4,40 +4,35 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Plugin, Logger, CoreSetup, CoreStart, PluginInitializerContext } from 'src/core/server'; +import { Plugin, Logger, CoreSetup, PluginInitializerContext } from 'src/core/server'; -import { Service, IService, StackAlertsDeps } from './types'; -import { getService as getServiceIndexThreshold } from './alert_types/index_threshold'; +import { StackAlertsDeps, StackAlertsStartDeps } from './types'; import { registerBuiltInAlertTypes } from './alert_types'; import { BUILT_IN_ALERTS_FEATURE } from './feature'; -export class AlertingBuiltinsPlugin implements Plugin { +export class AlertingBuiltinsPlugin + implements Plugin { private readonly logger: Logger; - private readonly service: Service; constructor(ctx: PluginInitializerContext) { this.logger = ctx.logger.get(); - this.service = { - indexThreshold: getServiceIndexThreshold(), - logger: this.logger, - }; } - public async setup(core: CoreSetup, { alerts, features }: StackAlertsDeps): Promise { + public async setup( + core: CoreSetup, + { alerts, features }: StackAlertsDeps + ): Promise { features.registerKibanaFeature(BUILT_IN_ALERTS_FEATURE); registerBuiltInAlertTypes({ - service: this.service, - router: core.http.createRouter(), + logger: this.logger, + data: core + .getStartServices() + .then(async ([, { triggersActionsUi }]) => triggersActionsUi.data), alerts, - baseRoute: '/api/stack_alerts', }); - return this.service; - } - - public async start(core: CoreStart): Promise { - return this.service; } + public async start(): Promise {} public async stop(): Promise {} } diff --git a/x-pack/plugins/stack_alerts/server/types.ts b/x-pack/plugins/stack_alerts/server/types.ts index d0eb8aa768915..e37596e8ff970 100644 --- a/x-pack/plugins/stack_alerts/server/types.ts +++ b/x-pack/plugins/stack_alerts/server/types.ts @@ -4,11 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Logger, LegacyScopedClusterClient } from '../../../../src/core/server'; +import { PluginStartContract as TriggersActionsUiStartContract } from '../../triggers_actions_ui/server'; import { PluginSetupContract as AlertingSetup } from '../../alerts/server'; -import { getService as getServiceIndexThreshold } from './alert_types/index_threshold'; - -export { Logger, IRouter } from '../../../../src/core/server'; export { PluginSetupContract as AlertingSetup, @@ -23,14 +20,6 @@ export interface StackAlertsDeps { features: FeaturesPluginSetup; } -// external service exposed through plugin setup/start -export interface IService { - indexThreshold: ReturnType; -} - -// version of service for internal use -export interface Service extends IService { - logger: Logger; +export interface StackAlertsStartDeps { + triggersActionsUi: TriggersActionsUiStartContract; } - -export type CallCluster = LegacyScopedClusterClient['callAsCurrentUser']; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 912cb01d458e2..238b3dccc698a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -19525,24 +19525,24 @@ "xpack.stackAlerts.indexThreshold.actionVariableContextThresholdLabel": "しきい値として使用する値の配列。「between」と「notBetween」には2つの値が必要です。その他は1つの値が必要です。", "xpack.stackAlerts.indexThreshold.actionVariableContextTitleLabel": "アラートの事前構成タイトル。", "xpack.stackAlerts.indexThreshold.actionVariableContextValueLabel": "しきい値を超えた値。", - "xpack.stackAlerts.indexThreshold.aggTypeRequiredErrorMessage": "[aggType]が「{aggType}」のときには[aggField]に値が必要です", + "xpack.triggersActionsUI.data.coreQueryParams.aggTypeRequiredErrorMessage": "[aggType]が「{aggType}」のときには[aggField]に値が必要です", "xpack.stackAlerts.indexThreshold.alertTypeContextMessageDescription": "アラート{name}グループ{group}値{value}が{date}に{window}にわたってしきい値{function}を超えました", "xpack.stackAlerts.indexThreshold.alertTypeContextSubjectTitle": "アラート{name}グループ{group}がしきい値を超えました", "xpack.stackAlerts.indexThreshold.alertTypeTitle": "インデックスしきい値", - "xpack.stackAlerts.indexThreshold.dateStartGTdateEndErrorMessage": "[dateStart]が[dateEnd]よりも大です", - "xpack.stackAlerts.indexThreshold.formattedFieldErrorMessage": "{fieldName}の無効な{formatName}形式:「{fieldValue}」", - "xpack.stackAlerts.indexThreshold.intervalRequiredErrorMessage": "[interval]: [dateStart]が[dateEnd]と等しくない場合に指定する必要があります", - "xpack.stackAlerts.indexThreshold.invalidAggTypeErrorMessage": "無効な aggType:「{aggType}」", + "xpack.triggersActionsUI.data.coreQueryParams.dateStartGTdateEndErrorMessage": "[dateStart]が[dateEnd]よりも大です", + "xpack.triggersActionsUI.data.coreQueryParams.formattedFieldErrorMessage": "{fieldName}の無効な{formatName}形式:「{fieldValue}」", + "xpack.triggersActionsUI.data.coreQueryParams.intervalRequiredErrorMessage": "[interval]: [dateStart]が[dateEnd]と等しくない場合に指定する必要があります", + "xpack.triggersActionsUI.data.coreQueryParams.invalidAggTypeErrorMessage": "無効な aggType:「{aggType}」", "xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage": "無効なthresholdComparatorが指定されました: {comparator}", - "xpack.stackAlerts.indexThreshold.invalidDateErrorMessage": "無効な日付{date}", - "xpack.stackAlerts.indexThreshold.invalidDurationErrorMessage": "無効な期間:「{duration}」", - "xpack.stackAlerts.indexThreshold.invalidGroupByErrorMessage": "無効なgroupBy:「{groupBy}」", - "xpack.stackAlerts.indexThreshold.invalidTermSizeMaximumErrorMessage": "[termSize]: {maxGroups}以下でなければなりません。", + "xpack.triggersActionsUI.data.coreQueryParams.invalidDateErrorMessage": "無効な日付{date}", + "xpack.triggersActionsUI.data.coreQueryParams.invalidDurationErrorMessage": "無効な期間:「{duration}」", + "xpack.triggersActionsUI.data.coreQueryParams.invalidGroupByErrorMessage": "無効なgroupBy:「{groupBy}」", + "xpack.triggersActionsUI.data.coreQueryParams.invalidTermSizeMaximumErrorMessage": "[termSize]: {maxGroups}以下でなければなりません。", "xpack.stackAlerts.indexThreshold.invalidThreshold2ErrorMessage": "[threshold]: 「{thresholdComparator}」比較子の場合には2つの要素が必要です", - "xpack.stackAlerts.indexThreshold.invalidTimeWindowUnitsErrorMessage": "無効なtimeWindowUnit:「{timeWindowUnit}」", - "xpack.stackAlerts.indexThreshold.maxIntervalsErrorMessage": "間隔{intervals}の計算値が{maxIntervals}よりも大です", - "xpack.stackAlerts.indexThreshold.termFieldRequiredErrorMessage": "[termField]: [groupBy]がトップのときにはtermFieldが必要です", - "xpack.stackAlerts.indexThreshold.termSizeRequiredErrorMessage": "[termSize]: [groupBy]がトップのときにはtermSizeが必要です", + "xpack.triggersActionsUI.data.coreQueryParams.invalidTimeWindowUnitsErrorMessage": "無効なtimeWindowUnit:「{timeWindowUnit}」", + "xpack.triggersActionsUI.data.coreQueryParams.maxIntervalsErrorMessage": "間隔{intervals}の計算値が{maxIntervals}よりも大です", + "xpack.triggersActionsUI.data.coreQueryParams.termFieldRequiredErrorMessage": "[termField]: [groupBy]がトップのときにはtermFieldが必要です", + "xpack.triggersActionsUI.data.coreQueryParams.termSizeRequiredErrorMessage": "[termSize]: [groupBy]がトップのときにはtermSizeが必要です", "xpack.transform.actionDeleteTransform.bulkDeleteDestinationIndexTitle": "ディスティネーションインデックスの削除", "xpack.transform.actionDeleteTransform.bulkDeleteDestIndexPatternTitle": "ディスティネーションインデックスパターンの削除", "xpack.transform.actionDeleteTransform.deleteDestinationIndexTitle": "ディスティネーションインデックス{destinationIndex}の削除", @@ -20078,42 +20078,42 @@ "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.cancelButtonLabel": "キャンセル", "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.deleteButtonLabel": "{numIdsToDelete, plural, one {{singleTitle}} other {# {multipleTitle}}}を削除 ", "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.descriptionText": "{numIdsToDelete, plural, one {a deleted {singleTitle}} other {deleted {multipleTitle}}}を回復できません。", - "xpack.triggersActionsUI.geoThreshold.boundaryNameSelect": "境界名を選択", - "xpack.triggersActionsUI.geoThreshold.boundaryNameSelectLabel": "人間が読み取れる境界名(任意)", - "xpack.triggersActionsUI.geoThreshold.delayOffset": "遅延評価オフセット", - "xpack.triggersActionsUI.geoThreshold.delayOffsetTooltip": "遅延サイクルでアラートを評価し、データレイテンシに合わせて調整します", - "xpack.triggersActionsUI.geoThreshold.entityByLabel": "グループ基準", - "xpack.triggersActionsUI.geoThreshold.entityIndexLabel": "インデックス", - "xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryGeoFieldText": "境界地理フィールドは必須です。", - "xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryIndexTitleText": "境界インデックスパターンタイトルは必須です。", - "xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryTypeText": "境界タイプは必須です。", - "xpack.triggersActionsUI.geoThreshold.error.requiredDateFieldText": "日付フィールドが必要です。", - "xpack.triggersActionsUI.geoThreshold.error.requiredEntityText": "エンティティは必須です。", - "xpack.triggersActionsUI.geoThreshold.error.requiredGeoFieldText": "地理フィールドは必須です。", - "xpack.triggersActionsUI.geoThreshold.error.requiredIndexTitleText": "インデックスパターンが必要です。", - "xpack.triggersActionsUI.geoThreshold.error.requiredTrackingEventText": "追跡イベントは必須です。", - "xpack.triggersActionsUI.geoThreshold.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。", - "xpack.triggersActionsUI.geoThreshold.geofieldLabel": "地理空間フィールド", - "xpack.triggersActionsUI.geoThreshold.indexLabel": "インデックス", - "xpack.triggersActionsUI.geoThreshold.indexPatternSelectLabel": "インデックスパターン", - "xpack.triggersActionsUI.geoThreshold.indexPatternSelectPlaceholder": "インデックスパターンを選択", - "xpack.triggersActionsUI.geoThreshold.name.trackingThreshold": "追跡しきい値", - "xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisLinkTextDescription": "インデックスパターンを作成します", - "xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisPrefixDescription": "次のことが必要です ", - "xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisSuffixDescription": " 地理空間フィールドを含む", - "xpack.triggersActionsUI.geoThreshold.noIndexPattern.getStartedLinkText": "サンプルデータセットで始めましょう。", - "xpack.triggersActionsUI.geoThreshold.noIndexPattern.hintDescription": "地理空間データセットがありませんか? ", - "xpack.triggersActionsUI.geoThreshold.noIndexPattern.messageTitle": "地理空間フィールドを含むインデックスパターンが見つかりませんでした", - "xpack.triggersActionsUI.geoThreshold.selectBoundaryIndex": "境界を選択:", - "xpack.triggersActionsUI.geoThreshold.selectEntity": "エンティティを選択", - "xpack.triggersActionsUI.geoThreshold.selectGeoLabel": "ジオフィールドを選択", - "xpack.triggersActionsUI.geoThreshold.selectIndex": "条件を定義してください", - "xpack.triggersActionsUI.geoThreshold.selectLabel": "ジオフィールドを選択", - "xpack.triggersActionsUI.geoThreshold.selectOffset": "オフセットを選択(任意)", - "xpack.triggersActionsUI.geoThreshold.selectTimeLabel": "時刻フィールドを選択", - "xpack.triggersActionsUI.geoThreshold.timeFieldLabel": "時間フィールド", - "xpack.triggersActionsUI.geoThreshold.topHitsSplitFieldSelectPlaceholder": "エンティティフィールドを選択", - "xpack.triggersActionsUI.geoThreshold.whenEntityLabel": "エンティティ", + "xpack.stackAlerts.geoThreshold.boundaryNameSelect": "境界名を選択", + "xpack.stackAlerts.geoThreshold.boundaryNameSelectLabel": "人間が読み取れる境界名(任意)", + "xpack.stackAlerts.geoThreshold.delayOffset": "遅延評価オフセット", + "xpack.stackAlerts.geoThreshold.delayOffsetTooltip": "遅延サイクルでアラートを評価し、データレイテンシに合わせて調整します", + "xpack.stackAlerts.geoThreshold.entityByLabel": "グループ基準", + "xpack.stackAlerts.geoThreshold.entityIndexLabel": "インデックス", + "xpack.stackAlerts.geoThreshold.error.requiredBoundaryGeoFieldText": "境界地理フィールドは必須です。", + "xpack.stackAlerts.geoThreshold.error.requiredBoundaryIndexTitleText": "境界インデックスパターンタイトルは必須です。", + "xpack.stackAlerts.geoThreshold.error.requiredBoundaryTypeText": "境界タイプは必須です。", + "xpack.stackAlerts.geoThreshold.error.requiredDateFieldText": "日付フィールドが必要です。", + "xpack.stackAlerts.geoThreshold.error.requiredEntityText": "エンティティは必須です。", + "xpack.stackAlerts.geoThreshold.error.requiredGeoFieldText": "地理フィールドは必須です。", + "xpack.stackAlerts.geoThreshold.error.requiredIndexTitleText": "インデックスパターンが必要です。", + "xpack.stackAlerts.geoThreshold.error.requiredTrackingEventText": "追跡イベントは必須です。", + "xpack.stackAlerts.geoThreshold.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。", + "xpack.stackAlerts.geoThreshold.geofieldLabel": "地理空間フィールド", + "xpack.stackAlerts.geoThreshold.indexLabel": "インデックス", + "xpack.stackAlerts.geoThreshold.indexPatternSelectLabel": "インデックスパターン", + "xpack.stackAlerts.geoThreshold.indexPatternSelectPlaceholder": "インデックスパターンを選択", + "xpack.stackAlerts.geoThreshold.name.trackingThreshold": "追跡しきい値", + "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisLinkTextDescription": "インデックスパターンを作成します", + "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisPrefixDescription": "次のことが必要です ", + "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisSuffixDescription": " 地理空間フィールドを含む", + "xpack.stackAlerts.geoThreshold.noIndexPattern.getStartedLinkText": "サンプルデータセットで始めましょう。", + "xpack.stackAlerts.geoThreshold.noIndexPattern.hintDescription": "地理空間データセットがありませんか? ", + "xpack.stackAlerts.geoThreshold.noIndexPattern.messageTitle": "地理空間フィールドを含むインデックスパターンが見つかりませんでした", + "xpack.stackAlerts.geoThreshold.selectBoundaryIndex": "境界を選択:", + "xpack.stackAlerts.geoThreshold.selectEntity": "エンティティを選択", + "xpack.stackAlerts.geoThreshold.selectGeoLabel": "ジオフィールドを選択", + "xpack.stackAlerts.geoThreshold.selectIndex": "条件を定義してください", + "xpack.stackAlerts.geoThreshold.selectLabel": "ジオフィールドを選択", + "xpack.stackAlerts.geoThreshold.selectOffset": "オフセットを選択(任意)", + "xpack.stackAlerts.geoThreshold.selectTimeLabel": "時刻フィールドを選択", + "xpack.stackAlerts.geoThreshold.timeFieldLabel": "時間フィールド", + "xpack.stackAlerts.geoThreshold.topHitsSplitFieldSelectPlaceholder": "エンティティフィールドを選択", + "xpack.stackAlerts.geoThreshold.whenEntityLabel": "エンティティ", "xpack.triggersActionsUI.home.alertsTabTitle": "アラート", "xpack.triggersActionsUI.home.appTitle": "アラートとアクション", "xpack.triggersActionsUI.home.breadcrumbTitle": "アラートとアクション", @@ -20156,15 +20156,15 @@ "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHeaderValueText": "値が必要です。", "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText": "メソッドが必要です", "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText": "パスワードが必要です。", - "xpack.triggersActionsUI.sections.addAlert.error.greaterThenThreshold0Text": "しきい値 1 はしきい値 0 よりも大きい値にしてください。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredAggFieldText": "集約フィールドが必要です。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredIndexText": "インデックスが必要です。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredTermSizedText": "用語サイズが必要です。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredThreshold0Text": "しきい値 0 が必要です。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredThreshold1Text": "しきい値 1 が必要です。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredTimeFieldText": "時間フィールドが必要です。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredTimeWindowSizeText": "時間ウィンドウサイズが必要です。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredtTermFieldText": "用語フィールドが必要です。", + "xpack.stackAlerts.threshold.ui.validation.error.greaterThenThreshold0Text": "しきい値 1 はしきい値 0 よりも大きい値にしてください。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredAggFieldText": "集約フィールドが必要です。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredIndexText": "インデックスが必要です。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredTermSizedText": "用語サイズが必要です。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold0Text": "しきい値 0 が必要です。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold1Text": "しきい値 1 が必要です。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredTimeFieldText": "時間フィールドが必要です。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredTimeWindowSizeText": "時間ウィンドウサイズが必要です。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredTermFieldText": "用語フィールドが必要です。", "xpack.triggersActionsUI.sections.addConnectorForm.flyoutTitle": "{actionTypeName} コネクタ", "xpack.triggersActionsUI.sections.addConnectorForm.selectConnectorFlyoutTitle": "コネクターを選択", "xpack.triggersActionsUI.sections.addConnectorForm.updateErrorNotificationText": "コネクターを作成できません。", @@ -20173,27 +20173,27 @@ "xpack.triggersActionsUI.sections.addModalConnectorForm.flyoutTitle": "{actionTypeName} コネクター", "xpack.triggersActionsUI.sections.addModalConnectorForm.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.addModalConnectorForm.updateSuccessNotificationText": "「{connectorName}」を作成しました", - "xpack.triggersActionsUI.sections.alertAdd.conditionPrompt": "条件を定義してください", - "xpack.triggersActionsUI.sections.alertAdd.errorLoadingAlertVisualizationTitle": "アラートビジュアライゼーションを読み込めません", + "xpack.stackAlerts.threshold.ui.conditionPrompt": "条件を定義してください", + "xpack.stackAlerts.threshold.ui.visualization.errorLoadingAlertVisualizationTitle": "アラートビジュアライゼーションを読み込めません", "xpack.triggersActionsUI.sections.alertAdd.flyoutTitle": "アラートの作成", - "xpack.triggersActionsUI.sections.alertAdd.geoThreshold.closePopoverLabel": "閉じる", - "xpack.triggersActionsUI.sections.alertAdd.loadingAlertVisualizationDescription": "アラートビジュアライゼーションを読み込み中...", + "xpack.stackAlerts.geoThreshold.ui.expressionPopover.closePopoverLabel": "閉じる", + "xpack.stackAlerts.threshold.ui.visualization.loadingAlertVisualizationDescription": "アラートビジュアライゼーションを読み込み中...", "xpack.triggersActionsUI.sections.alertAdd.operationName": "作成", - "xpack.triggersActionsUI.sections.alertAdd.previewAlertVisualizationDescription": "プレビューを生成するための式を完成します。", + "xpack.stackAlerts.threshold.ui.previewAlertVisualizationDescription": "プレビューを生成するための式を完成します。", "xpack.triggersActionsUI.sections.alertAdd.saveErrorNotificationText": "アラートを作成できません。", "xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText": "「{alertName}」 を保存しました", - "xpack.triggersActionsUI.sections.alertAdd.selectIndex": "インデックスを選択してください", - "xpack.triggersActionsUI.sections.alertAdd.threshold.closeIndexPopoverLabel": "閉じる", - "xpack.triggersActionsUI.sections.alertAdd.threshold.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。", - "xpack.triggersActionsUI.sections.alertAdd.threshold.howToBroadenSearchQueryDescription": "* で検索クエリの範囲を広げます。", - "xpack.triggersActionsUI.sections.alertAdd.threshold.indexButtonLabel": "インデックス", - "xpack.triggersActionsUI.sections.alertAdd.threshold.indexLabel": "インデックス", - "xpack.triggersActionsUI.sections.alertAdd.threshold.indicesToQueryLabel": "クエリを実行するインデックス", - "xpack.triggersActionsUI.sections.alertAdd.threshold.timeFieldLabel": "時間フィールド", - "xpack.triggersActionsUI.sections.alertAdd.threshold.timeFieldOptionLabel": "フィールドを選択", - "xpack.triggersActionsUI.sections.alertAdd.thresholdPreviewChart.dataDoesNotExistTextMessage": "時間範囲とフィルターが正しいことを確認してください。", - "xpack.triggersActionsUI.sections.alertAdd.thresholdPreviewChart.noDataTitle": "このクエリに一致するデータはありません", - "xpack.triggersActionsUI.sections.alertAdd.unableToLoadVisualizationMessage": "ビジュアライゼーションを読み込めません", + "xpack.stackAlerts.threshold.ui.selectIndex": "インデックスを選択してください", + "xpack.stackAlerts.threshold.ui.alertParams.closeIndexPopoverLabel": "閉じる", + "xpack.stackAlerts.threshold.ui.alertParams.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。", + "xpack.stackAlerts.threshold.ui.alertParams.howToBroadenSearchQueryDescription": "* で検索クエリの範囲を広げます。", + "xpack.stackAlerts.threshold.ui.alertParams.indexButtonLabel": "インデックス", + "xpack.stackAlerts.threshold.ui.alertParams.indexLabel": "インデックス", + "xpack.stackAlerts.threshold.ui.alertParams.indicesToQueryLabel": "クエリを実行するインデックス", + "xpack.stackAlerts.threshold.ui.alertParams.timeFieldLabel": "時間フィールド", + "xpack.triggersActionsUI.sections.alertAdd.indexControls.timeFieldOptionLabel": "フィールドを選択", + "xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.dataDoesNotExistTextMessage": "時間範囲とフィルターが正しいことを確認してください。", + "xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.noDataTitle": "このクエリに一致するデータはありません", + "xpack.stackAlerts.threshold.ui.visualization.unableToLoadVisualizationMessage": "ビジュアライゼーションを読み込めません", "xpack.triggersActionsUI.sections.alertDetails.alertInstances.disabledAlert": "このアラートは無効になっていて再表示できません。[↑ を無効にする]を切り替えてアクティブにします。", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.duration": "期間", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.instance": "インスタンス", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8ae964d9ee7d0..48654a5ec5ff4 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -19544,24 +19544,24 @@ "xpack.stackAlerts.indexThreshold.actionVariableContextThresholdLabel": "用作阈值的值数组;“between”和“notBetween”需要两个值,其他则需要一个值。", "xpack.stackAlerts.indexThreshold.actionVariableContextTitleLabel": "告警的预构造标题。", "xpack.stackAlerts.indexThreshold.actionVariableContextValueLabel": "超过阈值的值。", - "xpack.stackAlerts.indexThreshold.aggTypeRequiredErrorMessage": "[aggField]:当 [aggType] 为“{aggType}”时必须有值", + "xpack.triggersActionsUI.data.coreQueryParams.aggTypeRequiredErrorMessage": "[aggField]:当 [aggType] 为“{aggType}”时必须有值", "xpack.stackAlerts.indexThreshold.alertTypeContextMessageDescription": "告警 {name} 组 {group} 值 {value} 在 {window} 于 {date}超过了阈值 {function}", "xpack.stackAlerts.indexThreshold.alertTypeContextSubjectTitle": "告警 {name} 组 {group} 超过了阈值", "xpack.stackAlerts.indexThreshold.alertTypeTitle": "索引阈值", - "xpack.stackAlerts.indexThreshold.dateStartGTdateEndErrorMessage": "[dateStart]:晚于 [dateEnd]", - "xpack.stackAlerts.indexThreshold.formattedFieldErrorMessage": "{fieldName} 的 {formatName} 格式无效:“{fieldValue}”", - "xpack.stackAlerts.indexThreshold.intervalRequiredErrorMessage": "[interval]:如果 [dateStart] 不等于 [dateEnd],则必须指定", - "xpack.stackAlerts.indexThreshold.invalidAggTypeErrorMessage": "aggType 无效:“{aggType}”", + "xpack.triggersActionsUI.data.coreQueryParams.dateStartGTdateEndErrorMessage": "[dateStart]:晚于 [dateEnd]", + "xpack.triggersActionsUI.data.coreQueryParams.formattedFieldErrorMessage": "{fieldName} 的 {formatName} 格式无效:“{fieldValue}”", + "xpack.triggersActionsUI.data.coreQueryParams.intervalRequiredErrorMessage": "[interval]:如果 [dateStart] 不等于 [dateEnd],则必须指定", + "xpack.triggersActionsUI.data.coreQueryParams.invalidAggTypeErrorMessage": "aggType 无效:“{aggType}”", "xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage": "指定的 thresholdComparator 无效:{comparator}", - "xpack.stackAlerts.indexThreshold.invalidDateErrorMessage": "日期 {date} 无效", - "xpack.stackAlerts.indexThreshold.invalidDurationErrorMessage": "持续时间无效:“{duration}”", - "xpack.stackAlerts.indexThreshold.invalidGroupByErrorMessage": "groupBy 无效:“{groupBy}”", - "xpack.stackAlerts.indexThreshold.invalidTermSizeMaximumErrorMessage": "[termSize]:必须小于或等于 {maxGroups}", + "xpack.triggersActionsUI.data.coreQueryParams.invalidDateErrorMessage": "日期 {date} 无效", + "xpack.triggersActionsUI.data.coreQueryParams.invalidDurationErrorMessage": "持续时间无效:“{duration}”", + "xpack.triggersActionsUI.data.coreQueryParams.invalidGroupByErrorMessage": "groupBy 无效:“{groupBy}”", + "xpack.triggersActionsUI.data.coreQueryParams.invalidTermSizeMaximumErrorMessage": "[termSize]:必须小于或等于 {maxGroups}", "xpack.stackAlerts.indexThreshold.invalidThreshold2ErrorMessage": "[threshold]:对于“{thresholdComparator}”比较运算符,必须包含两个元素", - "xpack.stackAlerts.indexThreshold.invalidTimeWindowUnitsErrorMessage": "timeWindowUnit 无效:“{timeWindowUnit}”", - "xpack.stackAlerts.indexThreshold.maxIntervalsErrorMessage": "时间间隔 {intervals} 的计算数目大于最大值 {maxIntervals}", - "xpack.stackAlerts.indexThreshold.termFieldRequiredErrorMessage": "[termField]:[groupBy] 为 top 时,termField 为必需", - "xpack.stackAlerts.indexThreshold.termSizeRequiredErrorMessage": "[termSize]:[groupBy] 为 top 时,termSize 为必需", + "xpack.triggersActionsUI.data.coreQueryParams.invalidTimeWindowUnitsErrorMessage": "timeWindowUnit 无效:“{timeWindowUnit}”", + "xpack.triggersActionsUI.data.coreQueryParams.maxIntervalsErrorMessage": "时间间隔 {intervals} 的计算数目大于最大值 {maxIntervals}", + "xpack.triggersActionsUI.data.coreQueryParams.termFieldRequiredErrorMessage": "[termField]:[groupBy] 为 top 时,termField 为必需", + "xpack.triggersActionsUI.data.coreQueryParams.termSizeRequiredErrorMessage": "[termSize]:[groupBy] 为 top 时,termSize 为必需", "xpack.transform.actionDeleteTransform.bulkDeleteDestinationIndexTitle": "删除目标索引", "xpack.transform.actionDeleteTransform.bulkDeleteDestIndexPatternTitle": "删除目标索引模式", "xpack.transform.actionDeleteTransform.deleteDestinationIndexTitle": "删除目标索引 {destinationIndex}", @@ -20097,42 +20097,42 @@ "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.cancelButtonLabel": "取消", "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.deleteButtonLabel": "删除{numIdsToDelete, plural, one {{singleTitle}} other { # 个{multipleTitle}}} ", "xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.descriptionText": "无法恢复{numIdsToDelete, plural, one {删除的{singleTitle}} other {删除的{multipleTitle}}}。", - "xpack.triggersActionsUI.geoThreshold.boundaryNameSelect": "选择边界名称", - "xpack.triggersActionsUI.geoThreshold.boundaryNameSelectLabel": "可人工读取的边界名称(可选)", - "xpack.triggersActionsUI.geoThreshold.delayOffset": "已延迟的评估偏移", - "xpack.triggersActionsUI.geoThreshold.delayOffsetTooltip": "评估延迟周期内的告警,以针对数据延迟进行调整", - "xpack.triggersActionsUI.geoThreshold.entityByLabel": "方式", - "xpack.triggersActionsUI.geoThreshold.entityIndexLabel": "索引", - "xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryGeoFieldText": "“边界地理”字段必填。", - "xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryIndexTitleText": "“边界索引模式标题”必填。", - "xpack.triggersActionsUI.geoThreshold.error.requiredBoundaryTypeText": "“边界类型”必填。", - "xpack.triggersActionsUI.geoThreshold.error.requiredDateFieldText": "“日期”字段必填。", - "xpack.triggersActionsUI.geoThreshold.error.requiredEntityText": "“实体”必填。", - "xpack.triggersActionsUI.geoThreshold.error.requiredGeoFieldText": "“地理”字段必填。", - "xpack.triggersActionsUI.geoThreshold.error.requiredIndexTitleText": "“索引模式”必填。", - "xpack.triggersActionsUI.geoThreshold.error.requiredTrackingEventText": "“跟踪事件”必填。", - "xpack.triggersActionsUI.geoThreshold.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。", - "xpack.triggersActionsUI.geoThreshold.geofieldLabel": "地理空间字段", - "xpack.triggersActionsUI.geoThreshold.indexLabel": "索引", - "xpack.triggersActionsUI.geoThreshold.indexPatternSelectLabel": "索引模式", - "xpack.triggersActionsUI.geoThreshold.indexPatternSelectPlaceholder": "选择索引模式", - "xpack.triggersActionsUI.geoThreshold.name.trackingThreshold": "跟踪阈值", - "xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisLinkTextDescription": "创建索引模式", - "xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisPrefixDescription": "您将需要 ", - "xpack.triggersActionsUI.geoThreshold.noIndexPattern.doThisSuffixDescription": " (包含地理空间字段)。", - "xpack.triggersActionsUI.geoThreshold.noIndexPattern.getStartedLinkText": "开始使用一些样例数据集。", - "xpack.triggersActionsUI.geoThreshold.noIndexPattern.hintDescription": "没有任何地理空间数据集? ", - "xpack.triggersActionsUI.geoThreshold.noIndexPattern.messageTitle": "找不到任何具有地理空间字段的索引模式", - "xpack.triggersActionsUI.geoThreshold.selectBoundaryIndex": "选择边界:", - "xpack.triggersActionsUI.geoThreshold.selectEntity": "选择实体", - "xpack.triggersActionsUI.geoThreshold.selectGeoLabel": "选择地理字段", - "xpack.triggersActionsUI.geoThreshold.selectIndex": "定义条件", - "xpack.triggersActionsUI.geoThreshold.selectLabel": "选择地理字段", - "xpack.triggersActionsUI.geoThreshold.selectOffset": "选择偏移(可选)", - "xpack.triggersActionsUI.geoThreshold.selectTimeLabel": "选择时间字段", - "xpack.triggersActionsUI.geoThreshold.timeFieldLabel": "时间字段", - "xpack.triggersActionsUI.geoThreshold.topHitsSplitFieldSelectPlaceholder": "选择实体字段", - "xpack.triggersActionsUI.geoThreshold.whenEntityLabel": "当实体", + "xpack.stackAlerts.geoThreshold.boundaryNameSelect": "选择边界名称", + "xpack.stackAlerts.geoThreshold.boundaryNameSelectLabel": "可人工读取的边界名称(可选)", + "xpack.stackAlerts.geoThreshold.delayOffset": "已延迟的评估偏移", + "xpack.stackAlerts.geoThreshold.delayOffsetTooltip": "评估延迟周期内的告警,以针对数据延迟进行调整", + "xpack.stackAlerts.geoThreshold.entityByLabel": "方式", + "xpack.stackAlerts.geoThreshold.entityIndexLabel": "索引", + "xpack.stackAlerts.geoThreshold.error.requiredBoundaryGeoFieldText": "“边界地理”字段必填。", + "xpack.stackAlerts.geoThreshold.error.requiredBoundaryIndexTitleText": "“边界索引模式标题”必填。", + "xpack.stackAlerts.geoThreshold.error.requiredBoundaryTypeText": "“边界类型”必填。", + "xpack.stackAlerts.geoThreshold.error.requiredDateFieldText": "“日期”字段必填。", + "xpack.stackAlerts.geoThreshold.error.requiredEntityText": "“实体”必填。", + "xpack.stackAlerts.geoThreshold.error.requiredGeoFieldText": "“地理”字段必填。", + "xpack.stackAlerts.geoThreshold.error.requiredIndexTitleText": "“索引模式”必填。", + "xpack.stackAlerts.geoThreshold.error.requiredTrackingEventText": "“跟踪事件”必填。", + "xpack.stackAlerts.geoThreshold.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。", + "xpack.stackAlerts.geoThreshold.geofieldLabel": "地理空间字段", + "xpack.stackAlerts.geoThreshold.indexLabel": "索引", + "xpack.stackAlerts.geoThreshold.indexPatternSelectLabel": "索引模式", + "xpack.stackAlerts.geoThreshold.indexPatternSelectPlaceholder": "选择索引模式", + "xpack.stackAlerts.geoThreshold.name.trackingThreshold": "跟踪阈值", + "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisLinkTextDescription": "创建索引模式", + "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisPrefixDescription": "您将需要 ", + "xpack.stackAlerts.geoThreshold.noIndexPattern.doThisSuffixDescription": " (包含地理空间字段)。", + "xpack.stackAlerts.geoThreshold.noIndexPattern.getStartedLinkText": "开始使用一些样例数据集。", + "xpack.stackAlerts.geoThreshold.noIndexPattern.hintDescription": "没有任何地理空间数据集? ", + "xpack.stackAlerts.geoThreshold.noIndexPattern.messageTitle": "找不到任何具有地理空间字段的索引模式", + "xpack.stackAlerts.geoThreshold.selectBoundaryIndex": "选择边界:", + "xpack.stackAlerts.geoThreshold.selectEntity": "选择实体", + "xpack.stackAlerts.geoThreshold.selectGeoLabel": "选择地理字段", + "xpack.stackAlerts.geoThreshold.selectIndex": "定义条件", + "xpack.stackAlerts.geoThreshold.selectLabel": "选择地理字段", + "xpack.stackAlerts.geoThreshold.selectOffset": "选择偏移(可选)", + "xpack.stackAlerts.geoThreshold.selectTimeLabel": "选择时间字段", + "xpack.stackAlerts.geoThreshold.timeFieldLabel": "时间字段", + "xpack.stackAlerts.geoThreshold.topHitsSplitFieldSelectPlaceholder": "选择实体字段", + "xpack.stackAlerts.geoThreshold.whenEntityLabel": "当实体", "xpack.triggersActionsUI.home.alertsTabTitle": "告警", "xpack.triggersActionsUI.home.appTitle": "告警和操作", "xpack.triggersActionsUI.home.breadcrumbTitle": "告警和操作", @@ -20176,15 +20176,15 @@ "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredHeaderValueText": "“值”必填。", "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredMethodText": "“方法”必填", "xpack.triggersActionsUI.sections.addAction.webhookAction.error.requiredPasswordText": "“密码”必填。", - "xpack.triggersActionsUI.sections.addAlert.error.greaterThenThreshold0Text": "阈值 1 应 > 阈值 0。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredAggFieldText": "聚合字段必填。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredIndexText": "“索引”必填。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredTermSizedText": "“词大小”必填。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredThreshold0Text": "阈值 0 必填。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredThreshold1Text": "阈值 1 必填。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredTimeFieldText": "时间字段必填。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredTimeWindowSizeText": "“时间窗大小”必填。", - "xpack.triggersActionsUI.sections.addAlert.error.requiredtTermFieldText": "词字段必填。", + "xpack.stackAlerts.threshold.ui.validation.error.greaterThenThreshold0Text": "阈值 1 应 > 阈值 0。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredAggFieldText": "聚合字段必填。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredIndexText": "“索引”必填。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredTermSizedText": "“词大小”必填。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold0Text": "阈值 0 必填。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredThreshold1Text": "阈值 1 必填。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredTimeFieldText": "时间字段必填。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredTimeWindowSizeText": "“时间窗大小”必填。", + "xpack.stackAlerts.threshold.ui.validation.error.requiredTermFieldText": "词字段必填。", "xpack.triggersActionsUI.sections.addConnectorForm.flyoutTitle": "{actionTypeName} 连接器", "xpack.triggersActionsUI.sections.addConnectorForm.selectConnectorFlyoutTitle": "选择连接器", "xpack.triggersActionsUI.sections.addConnectorForm.updateErrorNotificationText": "无法创建连接器。", @@ -20193,27 +20193,27 @@ "xpack.triggersActionsUI.sections.addModalConnectorForm.flyoutTitle": "{actionTypeName} 连接器", "xpack.triggersActionsUI.sections.addModalConnectorForm.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.addModalConnectorForm.updateSuccessNotificationText": "已创建“{connectorName}”", - "xpack.triggersActionsUI.sections.alertAdd.conditionPrompt": "定义条件", - "xpack.triggersActionsUI.sections.alertAdd.errorLoadingAlertVisualizationTitle": "无法加载告警可视化", + "xpack.stackAlerts.threshold.ui.conditionPrompt": "定义条件", + "xpack.stackAlerts.threshold.ui.visualization.errorLoadingAlertVisualizationTitle": "无法加载告警可视化", "xpack.triggersActionsUI.sections.alertAdd.flyoutTitle": "创建告警", - "xpack.triggersActionsUI.sections.alertAdd.geoThreshold.closePopoverLabel": "关闭", - "xpack.triggersActionsUI.sections.alertAdd.loadingAlertVisualizationDescription": "正在加载告警可视化……", + "xpack.stackAlerts.geoThreshold.ui.expressionPopover.closePopoverLabel": "关闭", + "xpack.stackAlerts.threshold.ui.visualization.loadingAlertVisualizationDescription": "正在加载告警可视化……", "xpack.triggersActionsUI.sections.alertAdd.operationName": "创建", - "xpack.triggersActionsUI.sections.alertAdd.previewAlertVisualizationDescription": "完成表达式以生成预览。", + "xpack.stackAlerts.threshold.ui.previewAlertVisualizationDescription": "完成表达式以生成预览。", "xpack.triggersActionsUI.sections.alertAdd.saveErrorNotificationText": "无法创建告警。", "xpack.triggersActionsUI.sections.alertAdd.saveSuccessNotificationText": "已保存“{alertName}”", - "xpack.triggersActionsUI.sections.alertAdd.selectIndex": "选择索引", - "xpack.triggersActionsUI.sections.alertAdd.threshold.closeIndexPopoverLabel": "关闭", - "xpack.triggersActionsUI.sections.alertAdd.threshold.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。", - "xpack.triggersActionsUI.sections.alertAdd.threshold.howToBroadenSearchQueryDescription": "使用 * 可扩大您的查询范围。", - "xpack.triggersActionsUI.sections.alertAdd.threshold.indexButtonLabel": "索引", - "xpack.triggersActionsUI.sections.alertAdd.threshold.indexLabel": "索引", - "xpack.triggersActionsUI.sections.alertAdd.threshold.indicesToQueryLabel": "要查询的索引", - "xpack.triggersActionsUI.sections.alertAdd.threshold.timeFieldLabel": "时间字段", - "xpack.triggersActionsUI.sections.alertAdd.threshold.timeFieldOptionLabel": "选择字段", - "xpack.triggersActionsUI.sections.alertAdd.thresholdPreviewChart.dataDoesNotExistTextMessage": "确认您的时间范围和筛选正确。", - "xpack.triggersActionsUI.sections.alertAdd.thresholdPreviewChart.noDataTitle": "没有数据匹配此查询", - "xpack.triggersActionsUI.sections.alertAdd.unableToLoadVisualizationMessage": "无法加载可视化", + "xpack.stackAlerts.threshold.ui.selectIndex": "选择索引", + "xpack.stackAlerts.threshold.ui.alertParams.closeIndexPopoverLabel": "关闭", + "xpack.stackAlerts.threshold.ui.alertParams.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。", + "xpack.stackAlerts.threshold.ui.alertParams.howToBroadenSearchQueryDescription": "使用 * 可扩大您的查询范围。", + "xpack.stackAlerts.threshold.ui.alertParams.indexButtonLabel": "索引", + "xpack.stackAlerts.threshold.ui.alertParams.indexLabel": "索引", + "xpack.stackAlerts.threshold.ui.alertParams.indicesToQueryLabel": "要查询的索引", + "xpack.stackAlerts.threshold.ui.alertParams.timeFieldLabel": "时间字段", + "xpack.triggersActionsUI.sections.alertAdd.indexControls.timeFieldOptionLabel": "选择字段", + "xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.dataDoesNotExistTextMessage": "确认您的时间范围和筛选正确。", + "xpack.stackAlerts.threshold.ui.visualization.thresholdPreviewChart.noDataTitle": "没有数据匹配此查询", + "xpack.stackAlerts.threshold.ui.visualization.unableToLoadVisualizationMessage": "无法加载可视化", "xpack.triggersActionsUI.sections.alertDetails.alertInstances.disabledAlert": "此告警已禁用,无法显示。切换禁用 ↑ 以激活。", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.duration": "持续时间", "xpack.triggersActionsUI.sections.alertDetails.alertInstancesList.columns.instance": "实例", diff --git a/x-pack/plugins/triggers_actions_ui/README.md b/x-pack/plugins/triggers_actions_ui/README.md index 32e157255c0cc..ef81065608ad4 100644 --- a/x-pack/plugins/triggers_actions_ui/README.md +++ b/x-pack/plugins/triggers_actions_ui/README.md @@ -220,7 +220,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent .... diff --git a/x-pack/plugins/stack_alerts/common/alert_types/index_threshold/index.ts b/x-pack/plugins/triggers_actions_ui/common/data/index.ts similarity index 100% rename from x-pack/plugins/stack_alerts/common/alert_types/index_threshold/index.ts rename to x-pack/plugins/triggers_actions_ui/common/data/index.ts diff --git a/x-pack/plugins/triggers_actions_ui/common/index.ts b/x-pack/plugins/triggers_actions_ui/common/index.ts new file mode 100644 index 0000000000000..5775cc2454a7e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/common/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './data'; diff --git a/x-pack/plugins/triggers_actions_ui/kibana.json b/x-pack/plugins/triggers_actions_ui/kibana.json index 72e1f0be5f7f4..9d79ab9232bf3 100644 --- a/x-pack/plugins/triggers_actions_ui/kibana.json +++ b/x-pack/plugins/triggers_actions_ui/kibana.json @@ -3,8 +3,8 @@ "version": "kibana", "server": true, "ui": true, - "optionalPlugins": ["alerts", "stackAlerts", "features", "home"], - "requiredPlugins": ["management", "charts", "data", "kibanaReact"], + "optionalPlugins": ["alerts", "features", "home"], + "requiredPlugins": ["management", "charts", "data"], "configPath": ["xpack", "trigger_actions_ui"], "extraPublicDirs": ["public/common", "public/common/constants"], "requiredBundles": ["home", "alerts", "esUiShared"] diff --git a/x-pack/plugins/triggers_actions_ui/public/application/boot.tsx b/x-pack/plugins/triggers_actions_ui/public/application/boot.tsx index 80f9ac532d1c9..bb46fd02a98a9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/boot.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/boot.tsx @@ -9,7 +9,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { SavedObjectsClientContract } from 'src/core/public'; import { App, AppDeps } from './app'; -import { setSavedObjectsClient } from '../common/lib/index_threshold_api'; +import { setSavedObjectsClient } from '../common/lib/data_apis'; interface BootDeps extends AppDeps { element: HTMLElement; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/index.ts index 32b323334654e..86c33a373753f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/index.ts @@ -5,6 +5,10 @@ */ export * from './expression_items'; +export * from './constants'; +export * from './index_controls'; +export * from './lib'; +export * from './types'; export { connectorConfiguration as ServiceNowConnectorConfiguration } from '../application/components/builtin_action_types/servicenow/config'; export { connectorConfiguration as JiraConnectorConfiguration } from '../application/components/builtin_action_types/jira/config'; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/index_controls/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/index_controls/index.ts index da332aa326ccf..8d10e531930cc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/index_controls/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/index_controls/index.ts @@ -9,10 +9,10 @@ import { HttpSetup } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { loadIndexPatterns, - getMatchingIndicesForThresholdAlertType, - getThresholdAlertTypeFields, + getMatchingIndices, + getESIndexFields, getSavedObjectsClient, -} from '../lib/index_threshold_api'; +} from '../lib/data_apis'; export interface IOption { label: string; @@ -39,7 +39,7 @@ export const getIndexOptions = async ( return options; } - const matchingIndices = (await getMatchingIndicesForThresholdAlertType({ + const matchingIndices = (await getMatchingIndices({ pattern, http, })) as string[]; @@ -85,12 +85,15 @@ export const getIndexOptions = async ( }; export const getFields = async (http: HttpSetup, indexes: string[]) => { - return await getThresholdAlertTypeFields({ indexes, http }); + return await getESIndexFields({ indexes, http }); }; export const firstFieldOption = { - text: i18n.translate('xpack.triggersActionsUI.sections.alertAdd.threshold.timeFieldOptionLabel', { - defaultMessage: 'Select a field', - }), + text: i18n.translate( + 'xpack.triggersActionsUI.sections.alertAdd.indexControls.timeFieldOptionLabel', + { + defaultMessage: 'Select a field', + } + ), value: '', }; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.ts new file mode 100644 index 0000000000000..573d306ae5550 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/data_apis.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { HttpSetup } from 'kibana/public'; + +const DATA_API_ROOT = '/api/triggers_actions_ui/data'; + +export async function getMatchingIndices({ + pattern, + http, +}: { + pattern: string; + http: HttpSetup; +}): Promise> { + if (!pattern.startsWith('*')) { + pattern = `*${pattern}`; + } + if (!pattern.endsWith('*')) { + pattern = `${pattern}*`; + } + const { indices } = await http.post(`${DATA_API_ROOT}/_indices`, { + body: JSON.stringify({ pattern }), + }); + return indices; +} + +export async function getESIndexFields({ + indexes, + http, +}: { + indexes: string[]; + http: HttpSetup; +}): Promise< + Array<{ + name: string; + type: string; + normalizedType: string; + searchable: boolean; + aggregatable: boolean; + }> +> { + const { fields } = await http.post(`${DATA_API_ROOT}/_fields`, { + body: JSON.stringify({ indexPatterns: indexes }), + }); + return fields; +} + +let savedObjectsClient: any; + +export const setSavedObjectsClient = (aSavedObjectsClient: any) => { + savedObjectsClient = aSavedObjectsClient; +}; + +export const getSavedObjectsClient = () => { + return savedObjectsClient; +}; + +export const loadIndexPatterns = async () => { + const { savedObjects } = await getSavedObjectsClient().find({ + type: 'index-pattern', + fields: ['title'], + perPage: 10000, + }); + return savedObjects; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/index.ts new file mode 100644 index 0000000000000..7671e239f8fff --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export { getTimeFieldOptions, getTimeOptions } from './get_time_options'; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/index_threshold_api.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/index_threshold_api.ts deleted file mode 100644 index 11d273fcb7a42..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/common/lib/index_threshold_api.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { HttpSetup } from 'kibana/public'; -import { TimeSeriesResult } from '../../../../stack_alerts/common/alert_types/index_threshold'; - -const INDEX_THRESHOLD_API_ROOT = '/api/stack_alerts/index_threshold'; - -export async function getMatchingIndicesForThresholdAlertType({ - pattern, - http, -}: { - pattern: string; - http: HttpSetup; -}): Promise> { - if (!pattern.startsWith('*')) { - pattern = `*${pattern}`; - } - if (!pattern.endsWith('*')) { - pattern = `${pattern}*`; - } - const { indices } = await http.post(`${INDEX_THRESHOLD_API_ROOT}/_indices`, { - body: JSON.stringify({ pattern }), - }); - return indices; -} - -export async function getThresholdAlertTypeFields({ - indexes, - http, -}: { - indexes: string[]; - http: HttpSetup; -}): Promise> { - const { fields } = await http.post(`${INDEX_THRESHOLD_API_ROOT}/_fields`, { - body: JSON.stringify({ indexPatterns: indexes }), - }); - return fields; -} - -let savedObjectsClient: any; - -export const setSavedObjectsClient = (aSavedObjectsClient: any) => { - savedObjectsClient = aSavedObjectsClient; -}; - -export const getSavedObjectsClient = () => { - return savedObjectsClient; -}; - -export const loadIndexPatterns = async () => { - const { savedObjects } = await getSavedObjectsClient().find({ - type: 'index-pattern', - fields: ['title'], - perPage: 10000, - }); - return savedObjects; -}; - -interface GetThresholdAlertVisualizationDataParams { - model: any; - visualizeOptions: any; - http: HttpSetup; -} - -export async function getThresholdAlertVisualizationData({ - model, - visualizeOptions, - http, -}: GetThresholdAlertVisualizationDataParams): Promise { - const timeSeriesQueryParams = { - index: model.index, - timeField: model.timeField, - aggType: model.aggType, - aggField: model.aggField, - groupBy: model.groupBy, - termField: model.termField, - termSize: model.termSize, - timeWindowSize: model.timeWindowSize, - timeWindowUnit: model.timeWindowUnit, - dateStart: new Date(visualizeOptions.rangeFrom).toISOString(), - dateEnd: new Date(visualizeOptions.rangeTo).toISOString(), - interval: visualizeOptions.interval, - }; - - return await http.post(`${INDEX_THRESHOLD_API_ROOT}/_time_series_query`, { - body: JSON.stringify(timeSeriesQueryParams), - }); -} diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index a28b10683c28f..3794112e1d502 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'src/core/public'; import { Plugin } from './plugin'; export { AlertsContextProvider, AlertsContextValue } from './application/context/alerts_context'; @@ -22,15 +21,17 @@ export { ValidationResult, ActionVariable, ActionConnector, + IErrorObject, } from './types'; export { ConnectorAddFlyout, ConnectorEditFlyout, } from './application/sections/action_connector_form'; export { loadActionTypes } from './application/lib/action_connector_api'; +export * from './common'; -export function plugin(ctx: PluginInitializerContext) { - return new Plugin(ctx); +export function plugin() { + return new Plugin(); } export { Plugin }; diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 61cd7699c50c5..2d93d368ad8e5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -4,17 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - CoreSetup, - CoreStart, - Plugin as CorePlugin, - PluginInitializerContext, -} from 'src/core/public'; +import { CoreSetup, CoreStart, Plugin as CorePlugin } from 'src/core/public'; import { i18n } from '@kbn/i18n'; import { FeaturesPluginStart } from '../../features/public'; import { registerBuiltInActionTypes } from './application/components/builtin_action_types'; -import { registerBuiltInAlertTypes } from './application/components/builtin_alert_types'; import { ActionTypeModel, AlertTypeModel } from './types'; import { TypeRegistry } from './application/type_registry'; import { @@ -29,10 +23,6 @@ import { ChartsPluginStart } from '../../../../src/plugins/charts/public'; import { PluginStartContract as AlertingStart } from '../../alerts/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; -export interface TriggersActionsUiConfigType { - enableGeoTrackingThresholdAlert: boolean; -} - export interface TriggersAndActionsUIPublicPluginSetup { actionTypeRegistry: TypeRegistry; alertTypeRegistry: TypeRegistry; @@ -66,14 +56,10 @@ export class Plugin > { private actionTypeRegistry: TypeRegistry; private alertTypeRegistry: TypeRegistry; - private initializerContext: PluginInitializerContext; - constructor(initializerContext: PluginInitializerContext) { + constructor() { this.actionTypeRegistry = new TypeRegistry(); - this.alertTypeRegistry = new TypeRegistry(); - - this.initializerContext = initializerContext; } public setup(core: CoreSetup, plugins: PluginsSetup): TriggersAndActionsUIPublicPluginSetup { @@ -142,11 +128,6 @@ export class Plugin actionTypeRegistry: this.actionTypeRegistry, }); - registerBuiltInAlertTypes({ - alertTypeRegistry: this.alertTypeRegistry, - triggerActionsUiConfig: this.initializerContext.config.get(), - }); - return { actionTypeRegistry: this.actionTypeRegistry, alertTypeRegistry: this.alertTypeRegistry, diff --git a/x-pack/plugins/triggers_actions_ui/server/data/README.md b/x-pack/plugins/triggers_actions_ui/server/data/README.md new file mode 100644 index 0000000000000..78577f0783008 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/server/data/README.md @@ -0,0 +1,228 @@ +# Data Apis + +The TriggersActionsUi plugin's Data Apis back the functionality needed by the Index Threshold Stack Alert. + +## http endpoints + +The following endpoints are provided for this alert type: + +- `POST /api/triggers_actions_ui/data/_indices` +- `POST /api/triggers_actions_ui/data/_fields` +- `POST /api/triggers_actions_ui/data/_time_series_query` + +### `POST .../_indices` + +This HTTP endpoint is provided for the alerting ui to list the available +"index names" for the user to select to use with the alert. This API also +returns aliases which match the supplied pattern. + +The request body is expected to be a JSON object in the following form, where the +`pattern` value may include comma-separated names and wildcards. + +```js +{ + pattern: "index-name-pattern" +} +``` + +The response body is a JSON object in the following form, where each element +of the `indices` array is the name of an index or alias. The number of elements +returned is limited, as this API is intended to be used to help narrow down +index names to use with the alert, and not support pagination, etc. + +```js +{ + indices: ["index-name-1", "alias-name-1", ...] +} +``` + +### `POST .../_fields` + +This HTTP endpoint is provided for the alerting ui to list the available +fields for the user to select to use with the alert. + +The request body is expected to be a JSON object in the following form, where the +`indexPatterns` array elements may include comma-separated names and wildcards. + +```js +{ + indexPatterns: ["index-pattern-1", "index-pattern-2"] +} +``` + +The response body is a JSON object in the following form, where each element +fields array is a field object. + +```js +{ + fields: [fieldObject1, fieldObject2, ...] +} +``` + +A field object is the following shape: + +```typescript +{ + name: string, // field name + type: string, // field type - eg 'keyword', 'date', 'long', etc + normalizedType: string, // for numeric types, this will be 'number' + aggregatable: true, // value from elasticsearch field capabilities + searchable: true, // value from elasticsearch field capabilities +} +``` + +### `POST .../_time_series_query` + +This HTTP endpoint is provided to return the values the alertType would calculate, +over a series of time. It is intended to be used in the alerting UI to +provide a "preview" of the alert during creation/editing based on recent data, +and could be used to show a "simulation" of the the alert over an arbitrary +range of time. + +The endpoint is `POST /api/triggers_actions_ui/data/_time_series_query`. +The request and response bodies are specifed in +[`lib/core_query_types.ts`][it-core-query] +and +[`lib/time_series_types.ts`][it-timeSeries-types]. +The request body is very similar to the alertType's parameters. + +### example + +Continuing with the example above, here's a query to get the values calculated +for the last 10 seconds. +This example uses [now-iso][] to generate iso date strings. + +```console +curl -k "https://elastic:changeme@localhost:5601/api/triggers_actions_ui/data/_time_series_query" \ + -H "kbn-xsrf: foo" -H "content-type: application/json" -d "{ + \"index\": \"es-hb-sim\", + \"timeField\": \"@timestamp\", + \"aggType\": \"avg\", + \"aggField\": \"summary.up\", + \"groupBy\": \"top\", + \"termSize\": 100, + \"termField\": \"monitor.name.keyword\", + \"interval\": \"1s\", + \"dateStart\": \"`now-iso -10s`\", + \"dateEnd\": \"`now-iso`\", + \"timeWindowSize\": 5, + \"timeWindowUnit\": \"s\" +}" +``` + +``` +{ + "results": [ + { + "group": "host-A", + "metrics": [ + [ "2020-02-26T15:10:40.000Z", 0 ], + [ "2020-02-26T15:10:41.000Z", 0 ], + [ "2020-02-26T15:10:42.000Z", 0 ], + [ "2020-02-26T15:10:43.000Z", 0 ], + [ "2020-02-26T15:10:44.000Z", 0 ], + [ "2020-02-26T15:10:45.000Z", 0 ], + [ "2020-02-26T15:10:46.000Z", 0 ], + [ "2020-02-26T15:10:47.000Z", 0 ], + [ "2020-02-26T15:10:48.000Z", 0 ], + [ "2020-02-26T15:10:49.000Z", 0 ], + [ "2020-02-26T15:10:50.000Z", 0 ] + ] + } + ] +} +``` + +To get the current value of the calculated metric, you can leave off the date: + +``` +curl -k "https://elastic:changeme@localhost:5601/api/triggers_actions_ui/data/_time_series_query" \ + -H "kbn-xsrf: foo" -H "content-type: application/json" -d '{ + "index": "es-hb-sim", + "timeField": "@timestamp", + "aggType": "avg", + "aggField": "summary.up", + "groupBy": "top", + "termField": "monitor.name.keyword", + "termSize": 100, + "interval": "1s", + "timeWindowSize": 5, + "timeWindowUnit": "s" +}' +``` + +``` +{ + "results": [ + { + "group": "host-A", + "metrics": [ + [ "2020-02-26T15:23:36.635Z", 0 ] + ] + } + ] +} +``` + +[it-timeSeries-types]: lib/time_series_types.ts + +## service functions + +A single service function is available that provides the functionality +of the http endpoint `POST /api/triggers_actions_ui/data/_time_series_query`, +but as an API for Kibana plugins. The function is available as +`triggersActionsUi.data.timeSeriesQuery()` on the plugin's _Start_ contract + +The parameters and return value for the function are the same as for the HTTP +request, though some additional parameters are required (logger, callCluster, +etc). + +## notes on the timeSeriesQuery API / http endpoint + +This API provides additional parameters beyond what the alertType itself uses: + +- `dateStart` +- `dateEnd` +- `interval` + +The `dateStart` and `dateEnd` parameters are ISO date strings. + +The `interval` parameter is intended to model the `interval` the alert is +currently using, and uses the same `1s`, `2m`, `3h`, etc format. Over the +supplied date range, a time-series data point will be calculated every +`interval` duration. + +So the number of time-series points in the output of the API should be: + +``` +( dateStart - dateEnd ) / interval +``` + +Example: + +``` +dateStart: '2020-01-01T00:00:00' +dateEnd: '2020-01-02T00:00:00' +interval: '1h' +``` + +The date range is 1 day === 24 hours. The interval is 1 hour. So there should +be ~24 time series points in the output. + +For preview purposes: + +- The `termSize` parameter should be used to help cut +down on the amount of work ES does, and keep the generated graphs a little +simpler. Probably something like `10`. + +- For queries with long date ranges, you probably don't want to use the +`interval` the alert is set to, as the `interval` used in the query, as this +could result in a lot of time-series points being generated, which is both +costly in ES, and may result in noisy graphs. + +- The `timeWindow*` parameters should be the same as what the alert is using, +especially for the `count` and `sum` aggregation types. Those aggregations +don't scale the same way the others do, when the window changes. Even for +the other aggregations, changing the window could result in dramatically +different values being generated - `avg` will be more "average-y", `min` +and `max` will be a little stickier. \ No newline at end of file diff --git a/x-pack/plugins/triggers_actions_ui/server/data/index.ts b/x-pack/plugins/triggers_actions_ui/server/data/index.ts new file mode 100644 index 0000000000000..6ee2b4bb8a5fe --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/server/data/index.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Logger, IRouter } from '../../../../../src/core/server'; +import { timeSeriesQuery } from './lib/time_series_query'; +import { registerRoutes } from './routes'; + +export { + TimeSeriesQuery, + CoreQueryParams, + CoreQueryParamsSchemaProperties, + validateCoreQueryBody, +} from './lib'; + +// future enhancement: make these configurable? +export const MAX_INTERVALS = 1000; +export const MAX_GROUPS = 1000; +export const DEFAULT_GROUPS = 100; + +export function getService() { + return { + timeSeriesQuery, + }; +} + +interface RegisterParams { + logger: Logger; + router: IRouter; + data: ReturnType; + baseRoute: string; +} + +export function register(params: RegisterParams) { + const { logger, router, data, baseRoute } = params; + const baseBuiltInRoute = `${baseRoute}/data`; + registerRoutes({ logger, router, data, baseRoute: baseBuiltInRoute }); +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/core_query_types.test.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/core_query_types.test.ts similarity index 100% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/core_query_types.test.ts rename to x-pack/plugins/triggers_actions_ui/server/data/lib/core_query_types.test.ts diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/core_query_types.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/core_query_types.ts similarity index 68% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/core_query_types.ts rename to x-pack/plugins/triggers_actions_ui/server/data/lib/core_query_types.ts index d96f555eceff4..bc7d0c352756e 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/core_query_types.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/core_query_types.ts @@ -51,33 +51,45 @@ export function validateCoreQueryBody(anyParams: unknown): string | undefined { termSize, }: CoreQueryParams = anyParams as CoreQueryParams; if (aggType !== 'count' && !aggField) { - return i18n.translate('xpack.stackAlerts.indexThreshold.aggTypeRequiredErrorMessage', { - defaultMessage: '[aggField]: must have a value when [aggType] is "{aggType}"', - values: { - aggType, - }, - }); + return i18n.translate( + 'xpack.triggersActionsUI.data.coreQueryParams.aggTypeRequiredErrorMessage', + { + defaultMessage: '[aggField]: must have a value when [aggType] is "{aggType}"', + values: { + aggType, + }, + } + ); } // check grouping if (groupBy === 'top') { if (termField == null) { - return i18n.translate('xpack.stackAlerts.indexThreshold.termFieldRequiredErrorMessage', { - defaultMessage: '[termField]: termField required when [groupBy] is top', - }); + return i18n.translate( + 'xpack.triggersActionsUI.data.coreQueryParams.termFieldRequiredErrorMessage', + { + defaultMessage: '[termField]: termField required when [groupBy] is top', + } + ); } if (termSize == null) { - return i18n.translate('xpack.stackAlerts.indexThreshold.termSizeRequiredErrorMessage', { - defaultMessage: '[termSize]: termSize required when [groupBy] is top', - }); + return i18n.translate( + 'xpack.triggersActionsUI.data.coreQueryParams.termSizeRequiredErrorMessage', + { + defaultMessage: '[termSize]: termSize required when [groupBy] is top', + } + ); } if (termSize > MAX_GROUPS) { - return i18n.translate('xpack.stackAlerts.indexThreshold.invalidTermSizeMaximumErrorMessage', { - defaultMessage: '[termSize]: must be less than or equal to {maxGroups}', - values: { - maxGroups: MAX_GROUPS, - }, - }); + return i18n.translate( + 'xpack.triggersActionsUI.data.coreQueryParams.invalidTermSizeMaximumErrorMessage', + { + defaultMessage: '[termSize]: must be less than or equal to {maxGroups}', + values: { + maxGroups: MAX_GROUPS, + }, + } + ); } } } @@ -89,7 +101,7 @@ function validateAggType(aggType: string): string | undefined { return; } - return i18n.translate('xpack.stackAlerts.indexThreshold.invalidAggTypeErrorMessage', { + return i18n.translate('xpack.triggersActionsUI.data.coreQueryParams.invalidAggTypeErrorMessage', { defaultMessage: 'invalid aggType: "{aggType}"', values: { aggType, @@ -102,7 +114,7 @@ export function validateGroupBy(groupBy: string): string | undefined { return; } - return i18n.translate('xpack.stackAlerts.indexThreshold.invalidGroupByErrorMessage', { + return i18n.translate('xpack.triggersActionsUI.data.coreQueryParams.invalidGroupByErrorMessage', { defaultMessage: 'invalid groupBy: "{groupBy}"', values: { groupBy, @@ -117,10 +129,13 @@ export function validateTimeWindowUnits(timeWindowUnit: string): string | undefi return; } - return i18n.translate('xpack.stackAlerts.indexThreshold.invalidTimeWindowUnitsErrorMessage', { - defaultMessage: 'invalid timeWindowUnit: "{timeWindowUnit}"', - values: { - timeWindowUnit, - }, - }); + return i18n.translate( + 'xpack.triggersActionsUI.data.coreQueryParams.invalidTimeWindowUnitsErrorMessage', + { + defaultMessage: 'invalid timeWindowUnit: "{timeWindowUnit}"', + values: { + timeWindowUnit, + }, + } + ); } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/date_range_info.test.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/date_range_info.test.ts similarity index 100% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/date_range_info.test.ts rename to x-pack/plugins/triggers_actions_ui/server/data/lib/date_range_info.test.ts diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/date_range_info.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/date_range_info.ts similarity index 89% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/date_range_info.ts rename to x-pack/plugins/triggers_actions_ui/server/data/lib/date_range_info.ts index 34f276d08706b..da125ba7ea29d 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/date_range_info.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/date_range_info.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { times } from 'lodash'; -import { parseDuration } from '../../../../../alerts/server'; +import { parseDuration } from '../../../../alerts/server'; import { MAX_INTERVALS } from '../index'; // dates as numbers are epoch millis @@ -100,7 +100,7 @@ function getDuration(durationS: string, field: string): number { } function getParseErrorMessage(formatName: string, fieldName: string, fieldValue: string) { - return i18n.translate('xpack.stackAlerts.indexThreshold.formattedFieldErrorMessage', { + return i18n.translate('xpack.triggersActionsUI.data.coreQueryParams.formattedFieldErrorMessage', { defaultMessage: 'invalid {formatName} format for {fieldName}: "{fieldValue}"', values: { formatName, @@ -111,7 +111,7 @@ function getParseErrorMessage(formatName: string, fieldName: string, fieldValue: } export function getTooManyIntervalsErrorMessage(intervals: number, maxIntervals: number) { - return i18n.translate('xpack.stackAlerts.indexThreshold.maxIntervalsErrorMessage', { + return i18n.translate('xpack.triggersActionsUI.data.coreQueryParams.maxIntervalsErrorMessage', { defaultMessage: 'calculated number of intervals {intervals} is greater than maximum {maxIntervals}', values: { @@ -122,7 +122,10 @@ export function getTooManyIntervalsErrorMessage(intervals: number, maxIntervals: } export function getDateStartAfterDateEndErrorMessage(): string { - return i18n.translate('xpack.stackAlerts.indexThreshold.dateStartGTdateEndErrorMessage', { - defaultMessage: '[dateStart]: is greater than [dateEnd]', - }); + return i18n.translate( + 'xpack.triggersActionsUI.data.coreQueryParams.dateStartGTdateEndErrorMessage', + { + defaultMessage: '[dateStart]: is greater than [dateEnd]', + } + ); } diff --git a/x-pack/plugins/triggers_actions_ui/server/data/lib/index.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/index.ts new file mode 100644 index 0000000000000..096a928249fd5 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { TimeSeriesQuery } from './time_series_query'; +export { + CoreQueryParams, + CoreQueryParamsSchemaProperties, + validateCoreQueryBody, +} from './core_query_types'; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_query.test.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.test.ts similarity index 58% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_query.test.ts rename to x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.test.ts index 0565a8634fc71..f1234249a257f 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_query.test.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.test.ts @@ -6,12 +6,8 @@ // test error conditions of calling timeSeriesQuery - postive results tested in FT -import { loggingSystemMock } from '../../../../../../../src/core/server/mocks'; -import { coreMock } from '../../../../../../../src/core/server/mocks'; -import { AlertingBuiltinsPlugin } from '../../../plugin'; -import { TimeSeriesQueryParameters, TimeSeriesResult, TimeSeriesQuery } from './time_series_query'; - -type TimeSeriesQueryFn = (query: TimeSeriesQueryParameters) => Promise; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { TimeSeriesQueryParameters, TimeSeriesQuery, timeSeriesQuery } from './time_series_query'; const DefaultQueryParams: TimeSeriesQuery = { index: 'index-name', @@ -32,16 +28,7 @@ describe('timeSeriesQuery', () => { let params: TimeSeriesQueryParameters; const mockCallCluster = jest.fn(); - let timeSeriesQueryFn: TimeSeriesQueryFn; - beforeEach(async () => { - // rather than use the function from an import, retrieve it from the plugin - const context = coreMock.createPluginInitializerContext(); - const plugin = new AlertingBuiltinsPlugin(context); - const coreStart = coreMock.createStart(); - const service = await plugin.start(coreStart); - timeSeriesQueryFn = service.indexThreshold.timeSeriesQuery; - mockCallCluster.mockReset(); params = { logger: loggingSystemMock.create().get(), @@ -52,14 +39,14 @@ describe('timeSeriesQuery', () => { it('fails as expected when the callCluster call fails', async () => { mockCallCluster.mockRejectedValue(new Error('woopsie')); - expect(timeSeriesQueryFn(params)).rejects.toThrowErrorMatchingInlineSnapshot( + expect(timeSeriesQuery(params)).rejects.toThrowErrorMatchingInlineSnapshot( `"error running search"` ); }); it('fails as expected when the query params are invalid', async () => { params.query = { ...params.query, dateStart: 'x' }; - expect(timeSeriesQueryFn(params)).rejects.toThrowErrorMatchingInlineSnapshot( + expect(timeSeriesQuery(params)).rejects.toThrowErrorMatchingInlineSnapshot( `"invalid date format for dateStart: \\"x\\""` ); }); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_query.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts similarity index 96% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_query.ts rename to x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts index 9c4133be6f483..29a996bbb5ef6 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_query.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_query.ts @@ -5,16 +5,17 @@ */ import { SearchResponse } from 'elasticsearch'; +import { Logger } from 'kibana/server'; +import { LegacyScopedClusterClient } from '../../../../../../src/core/server'; import { DEFAULT_GROUPS } from '../index'; import { getDateRangeInfo } from './date_range_info'; -import { Logger, CallCluster } from '../../../types'; import { TimeSeriesQuery, TimeSeriesResult, TimeSeriesResultRow } from './time_series_types'; export { TimeSeriesQuery, TimeSeriesResult } from './time_series_types'; export interface TimeSeriesQueryParameters { logger: Logger; - callCluster: CallCluster; + callCluster: LegacyScopedClusterClient['callAsCurrentUser']; query: TimeSeriesQuery; } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_types.test.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_types.test.ts similarity index 100% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_types.test.ts rename to x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_types.test.ts diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_types.ts b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_types.ts similarity index 81% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_types.ts rename to x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_types.ts index a8b35c34c596f..ef0fa15cf31e9 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_types.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/lib/time_series_types.ts @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; -import { parseDuration } from '../../../../../alerts/server'; +import { parseDuration } from '../../../../alerts/server'; import { MAX_INTERVALS } from '../index'; import { CoreQueryParamsSchemaProperties, validateCoreQueryBody } from './core_query_types'; import { @@ -18,11 +18,7 @@ import { getDateStartAfterDateEndErrorMessage, } from './date_range_info'; -export { - TimeSeriesResult, - TimeSeriesResultRow, - MetricResult, -} from '../../../../common/alert_types/index_threshold'; +export { TimeSeriesResult, TimeSeriesResultRow, MetricResult } from '../../../common/data'; // The parameters here are very similar to the alert parameters. // Missing are `comparator` and `threshold`, which aren't needed to generate @@ -66,9 +62,12 @@ function validateBody(anyParams: unknown): string | undefined { } if (epochStart !== epochEnd && !interval) { - return i18n.translate('xpack.stackAlerts.indexThreshold.intervalRequiredErrorMessage', { - defaultMessage: '[interval]: must be specified if [dateStart] does not equal [dateEnd]', - }); + return i18n.translate( + 'xpack.triggersActionsUI.data.coreQueryParams.intervalRequiredErrorMessage', + { + defaultMessage: '[interval]: must be specified if [dateStart] does not equal [dateEnd]', + } + ); } if (interval) { @@ -84,7 +83,7 @@ function validateBody(anyParams: unknown): string | undefined { function validateDate(dateString: string): string | undefined { const parsed = Date.parse(dateString); if (isNaN(parsed)) { - return i18n.translate('xpack.stackAlerts.indexThreshold.invalidDateErrorMessage', { + return i18n.translate('xpack.triggersActionsUI.data.coreQueryParams.invalidDateErrorMessage', { defaultMessage: 'invalid date {date}', values: { date: dateString, @@ -97,11 +96,14 @@ export function validateDuration(duration: string): string | undefined { try { parseDuration(duration); } catch (err) { - return i18n.translate('xpack.stackAlerts.indexThreshold.invalidDurationErrorMessage', { - defaultMessage: 'invalid duration: "{duration}"', - values: { - duration, - }, - }); + return i18n.translate( + 'xpack.triggersActionsUI.data.coreQueryParams.invalidDurationErrorMessage', + { + defaultMessage: 'invalid duration: "{duration}"', + values: { + duration, + }, + } + ); } } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/routes/fields.ts b/x-pack/plugins/triggers_actions_ui/server/data/routes/fields.ts similarity index 90% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/routes/fields.ts rename to x-pack/plugins/triggers_actions_ui/server/data/routes/fields.ts index ea1e17002c4a5..17a2b2929f0cf 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/routes/fields.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/routes/fields.ts @@ -16,7 +16,7 @@ import { KibanaResponseFactory, ILegacyScopedClusterClient, } from 'kibana/server'; -import { Service } from '../../../types'; +import { Logger } from '../../../../../../src/core/server'; const bodySchema = schema.object({ indexPatterns: schema.arrayOf(schema.string()), @@ -24,9 +24,9 @@ const bodySchema = schema.object({ type RequestBody = TypeOf; -export function createFieldsRoute(service: Service, router: IRouter, baseRoute: string) { +export function createFieldsRoute(logger: Logger, router: IRouter, baseRoute: string) { const path = `${baseRoute}/_fields`; - service.logger.debug(`registering indexThreshold route POST ${path}`); + logger.debug(`registering indexThreshold route POST ${path}`); router.post( { path, @@ -41,7 +41,7 @@ export function createFieldsRoute(service: Service, router: IRouter, baseRoute: req: KibanaRequest, res: KibanaResponseFactory ): Promise { - service.logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`); + logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`); let rawFields: RawFields; @@ -54,7 +54,7 @@ export function createFieldsRoute(service: Service, router: IRouter, baseRoute: rawFields = await getRawFields(ctx.core.elasticsearch.legacy.client, req.body.indexPatterns); } catch (err) { const indexPatterns = req.body.indexPatterns.join(','); - service.logger.warn( + logger.warn( `route ${path} error getting fields from pattern "${indexPatterns}": ${err.message}` ); return res.ok({ body: { fields: [] } }); @@ -62,7 +62,7 @@ export function createFieldsRoute(service: Service, router: IRouter, baseRoute: const result = { fields: getFieldsFromRawFields(rawFields) }; - service.logger.debug(`route ${path} response: ${JSON.stringify(result)}`); + logger.debug(`route ${path} response: ${JSON.stringify(result)}`); return res.ok({ body: result }); } } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/routes/index.ts b/x-pack/plugins/triggers_actions_ui/server/data/routes/index.ts similarity index 55% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/routes/index.ts rename to x-pack/plugins/triggers_actions_ui/server/data/routes/index.ts index 8410e48dd46d9..664b78cabb560 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/routes/index.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/routes/index.ts @@ -4,19 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Service, IRouter } from '../../../types'; +import { Logger } from '../../../../../../src/core/server'; import { createTimeSeriesQueryRoute } from './time_series_query'; import { createFieldsRoute } from './fields'; import { createIndicesRoute } from './indices'; +import { IRouter } from '../../../../../../src/core/server'; +import { getService } from '..'; interface RegisterRoutesParams { - service: Service; + logger: Logger; router: IRouter; + data: ReturnType; baseRoute: string; } export function registerRoutes(params: RegisterRoutesParams) { - const { service, router, baseRoute } = params; - createTimeSeriesQueryRoute(service, router, baseRoute); - createFieldsRoute(service, router, baseRoute); - createIndicesRoute(service, router, baseRoute); + const { logger, router, baseRoute, data } = params; + createTimeSeriesQueryRoute(logger, data.timeSeriesQuery, router, baseRoute); + createFieldsRoute(logger, router, baseRoute); + createIndicesRoute(logger, router, baseRoute); } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/routes/indices.ts b/x-pack/plugins/triggers_actions_ui/server/data/routes/indices.ts similarity index 85% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/routes/indices.ts rename to x-pack/plugins/triggers_actions_ui/server/data/routes/indices.ts index c94705829ec60..9b84ce5ac0bcc 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/routes/indices.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/routes/indices.ts @@ -19,7 +19,7 @@ import { ILegacyScopedClusterClient, } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; -import { Service } from '../../../types'; +import { Logger } from '../../../../../../src/core/server'; const bodySchema = schema.object({ pattern: schema.string(), @@ -27,9 +27,9 @@ const bodySchema = schema.object({ type RequestBody = TypeOf; -export function createIndicesRoute(service: Service, router: IRouter, baseRoute: string) { +export function createIndicesRoute(logger: Logger, router: IRouter, baseRoute: string) { const path = `${baseRoute}/_indices`; - service.logger.debug(`registering indexThreshold route POST ${path}`); + logger.debug(`registering indexThreshold route POST ${path}`); router.post( { path, @@ -45,7 +45,7 @@ export function createIndicesRoute(service: Service, router: IRouter, baseRoute: res: KibanaResponseFactory ): Promise { const pattern = req.body.pattern; - service.logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`); + logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`); if (pattern.trim() === '') { return res.ok({ body: { indices: [] } }); @@ -55,23 +55,19 @@ export function createIndicesRoute(service: Service, router: IRouter, baseRoute: try { aliases = await getAliasesFromPattern(ctx.core.elasticsearch.legacy.client, pattern); } catch (err) { - service.logger.warn( - `route ${path} error getting aliases from pattern "${pattern}": ${err.message}` - ); + logger.warn(`route ${path} error getting aliases from pattern "${pattern}": ${err.message}`); } let indices: string[] = []; try { indices = await getIndicesFromPattern(ctx.core.elasticsearch.legacy.client, pattern); } catch (err) { - service.logger.warn( - `route ${path} error getting indices from pattern "${pattern}": ${err.message}` - ); + logger.warn(`route ${path} error getting indices from pattern "${pattern}": ${err.message}`); } const result = { indices: uniqueCombined(aliases, indices, MAX_INDICES) }; - service.logger.debug(`route ${path} response: ${JSON.stringify(result)}`); + logger.debug(`route ${path} response: ${JSON.stringify(result)}`); return res.ok({ body: result }); } } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/routes/time_series_query.ts b/x-pack/plugins/triggers_actions_ui/server/data/routes/time_series_query.ts similarity index 58% rename from x-pack/plugins/stack_alerts/server/alert_types/index_threshold/routes/time_series_query.ts rename to x-pack/plugins/triggers_actions_ui/server/data/routes/time_series_query.ts index 9af01dc766a99..805bd7d4004c2 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/routes/time_series_query.ts +++ b/x-pack/plugins/triggers_actions_ui/server/data/routes/time_series_query.ts @@ -11,14 +11,20 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'kibana/server'; +import { Logger } from '../../../../../../src/core/server'; +import { TimeSeriesQueryParameters } from '../lib/time_series_query'; -import { Service } from '../../../types'; -import { TimeSeriesQuery, TimeSeriesQuerySchema } from '../lib/time_series_types'; +import { TimeSeriesQuery, TimeSeriesQuerySchema, TimeSeriesResult } from '../lib/time_series_types'; export { TimeSeriesQuery, TimeSeriesResult } from '../lib/time_series_types'; -export function createTimeSeriesQueryRoute(service: Service, router: IRouter, baseRoute: string) { +export function createTimeSeriesQueryRoute( + logger: Logger, + timeSeriesQuery: (params: TimeSeriesQueryParameters) => Promise, + router: IRouter, + baseRoute: string +) { const path = `${baseRoute}/_time_series_query`; - service.logger.debug(`registering indexThreshold route POST ${path}`); + logger.debug(`registering indexThreshold route POST ${path}`); router.post( { path, @@ -33,15 +39,15 @@ export function createTimeSeriesQueryRoute(service: Service, router: IRouter, ba req: KibanaRequest, res: KibanaResponseFactory ): Promise { - service.logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`); + logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`); - const result = await service.indexThreshold.timeSeriesQuery({ - logger: service.logger, + const result = await timeSeriesQuery({ + logger, callCluster: ctx.core.elasticsearch.legacy.client.callAsCurrentUser, query: req.body, }); - service.logger.debug(`route ${path} response: ${JSON.stringify(result)}`); + logger.debug(`route ${path} response: ${JSON.stringify(result)}`); return res.ok({ body: result }); } } diff --git a/x-pack/plugins/triggers_actions_ui/server/index.ts b/x-pack/plugins/triggers_actions_ui/server/index.ts index c12572f4ea7e9..abd61f2bd3541 100644 --- a/x-pack/plugins/triggers_actions_ui/server/index.ts +++ b/x-pack/plugins/triggers_actions_ui/server/index.ts @@ -4,8 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginConfigDescriptor } from 'kibana/server'; +import { PluginConfigDescriptor, PluginInitializerContext } from 'kibana/server'; import { configSchema, ConfigSchema } from '../config'; +import { TriggersActionsPlugin } from './plugin'; + +export { PluginStartContract } from './plugin'; +export { + TimeSeriesQuery, + CoreQueryParams, + CoreQueryParamsSchemaProperties, + validateCoreQueryBody, + MAX_INTERVALS, + MAX_GROUPS, + DEFAULT_GROUPS, +} from './data'; export const config: PluginConfigDescriptor = { exposeToBrowser: { @@ -14,7 +26,4 @@ export const config: PluginConfigDescriptor = { schema: configSchema, }; -export const plugin = () => ({ - setup() {}, - start() {}, -}); +export const plugin = (ctx: PluginInitializerContext) => new TriggersActionsPlugin(ctx); diff --git a/x-pack/plugins/triggers_actions_ui/server/plugin.ts b/x-pack/plugins/triggers_actions_ui/server/plugin.ts new file mode 100644 index 0000000000000..c0d29341e217b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/server/plugin.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Logger, Plugin, CoreSetup, PluginInitializerContext } from 'src/core/server'; +import { getService, register as registerDataService } from './data'; + +export interface PluginStartContract { + data: ReturnType; +} + +export class TriggersActionsPlugin implements Plugin { + private readonly logger: Logger; + private readonly data: PluginStartContract['data']; + + constructor(ctx: PluginInitializerContext) { + this.logger = ctx.logger.get(); + this.data = getService(); + } + + public async setup(core: CoreSetup): Promise { + registerDataService({ + logger: this.logger, + data: this.data, + router: core.http.createRouter(), + baseRoute: '/api/triggers_actions_ui', + }); + } + + public async start(): Promise { + return { + data: this.data, + }; + } + + public async stop(): Promise {} +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts index c05fa6cf051ff..e918ce174a031 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts @@ -15,7 +15,6 @@ import { ObjectRemover, } from '../../../../../common/lib'; import { createEsDocuments } from './create_test_data'; -import { getAlertType } from '../../../../../../../plugins/triggers_actions_ui/public/application/components/builtin_alert_types/threshold/'; const ALERT_TYPE_ID = '.index-threshold'; const ACTION_TYPE_ID = '.index'; @@ -27,7 +26,7 @@ const ALERT_INTERVALS_TO_WRITE = 5; const ALERT_INTERVAL_SECONDS = 3; const ALERT_INTERVAL_MILLIS = ALERT_INTERVAL_SECONDS * 1000; -const DefaultActionMessage = getAlertType().defaultActionMessage; +const DefaultActionMessage = `alert {{alertName}} group {{context.group}} value {{context.value}} exceeded threshold {{context.function}} over {{params.timeWindowSize}}{{params.timeWindowUnit}} on {{context.date}}`; // eslint-disable-next-line import/no-default-export export default function alertTests({ getService }: FtrProviderContext) { @@ -65,10 +64,6 @@ export default function alertTests({ getService }: FtrProviderContext) { await esTestIndexToolOutput.destroy(); }); - it('has a default action message', () => { - expect(DefaultActionMessage).to.be.ok(); - }); - // The tests below create two alerts, one that will fire, one that will // never fire; the tests ensure the ones that should fire, do fire, and // those that shouldn't fire, do not fire. diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/fields_endpoint.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/fields_endpoint.ts index 76ff41aac5397..881be83236be5 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/fields_endpoint.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/fields_endpoint.ts @@ -10,7 +10,7 @@ import { Spaces } from '../../../../scenarios'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { ESTestIndexTool, ES_TEST_INDEX_NAME, getUrlPrefix } from '../../../../../common/lib'; -const API_URI = 'api/stack_alerts/index_threshold/_fields'; +const API_URI = 'api/triggers_actions_ui/data/_fields'; // eslint-disable-next-line import/no-default-export export default function fieldsEndpointTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/indices_endpoint.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/indices_endpoint.ts index ba2b71e7134b6..7d89e2701d628 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/indices_endpoint.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/indices_endpoint.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { ESTestIndexTool, ES_TEST_INDEX_NAME, getUrlPrefix } from '../../../../../common/lib'; import { createEsDocuments } from './create_test_data'; -const API_URI = 'api/stack_alerts/index_threshold/_indices'; +const API_URI = 'api/triggers_actions_ui/data/_indices'; // eslint-disable-next-line import/no-default-export export default function indicesEndpointTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts index 70dbb860e08d6..334f898232bbc 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts @@ -9,11 +9,11 @@ import expect from '@kbn/expect'; import { Spaces } from '../../../../scenarios'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { ESTestIndexTool, ES_TEST_INDEX_NAME, getUrlPrefix } from '../../../../../common/lib'; -import { TimeSeriesQuery } from '../../../../../../../plugins/stack_alerts/server/alert_types/index_threshold/lib/time_series_query'; +import { TimeSeriesQuery } from '../../../../../../../plugins/triggers_actions_ui/server'; import { createEsDocuments } from './create_test_data'; -const INDEX_THRESHOLD_TIME_SERIES_QUERY_URL = 'api/stack_alerts/index_threshold/_time_series_query'; +const INDEX_THRESHOLD_TIME_SERIES_QUERY_URL = 'api/triggers_actions_ui/data/_time_series_query'; const START_DATE_MM_DD_HH_MM_SS_MS = '01-01T00:00:00.000Z'; const START_DATE = `2020-${START_DATE_MM_DD_HH_MM_SS_MS}`; From 1babb5f6bfb08791506c5472377c2d76c3ac0dc8 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Thu, 12 Nov 2020 11:44:15 -0500 Subject: [PATCH 16/17] [Fleet] IngestManager Plugin interface for registering UI extensions (#82783) * Expose `registerExtension()` interface on `Plugin#start` * Refactor use of `CustomConfigurePackagePolicy` to the new registerExtension approach * Refactor to always show registered ui extension (even if Integration has configuration options) --- .../fleet/components/extension_wrapper.tsx | 17 ++++ .../fleet/hooks/use_ui_extension.ts | 35 +++++++ .../fleet/public/applications/fleet/index.tsx | 12 ++- .../components/custom_package_policy.tsx | 61 ------------ .../components/index.ts | 1 - .../create_package_policy_page/index.tsx | 34 ++++++- .../step_configure_package.tsx | 38 ++++++-- .../edit_package_policy_page/index.tsx | 48 +++++++++- .../fleet/services/ui_extensions.test.ts | 76 +++++++++++++++ .../fleet/services/ui_extensions.ts | 26 +++++ .../public/applications/fleet/types/index.ts | 2 + .../applications/fleet/types/ui_extensions.ts | 96 +++++++++++++++++++ x-pack/plugins/fleet/public/index.ts | 7 +- x-pack/plugins/fleet/public/plugin.ts | 20 +++- .../mock/endpoint/dependencies_start_mock.ts | 4 +- .../endpoint_policy_create_extension.tsx | 35 +++++++ ...tsx => endpoint_policy_edit_extension.tsx} | 34 ++----- .../lazy_endpoint_policy_create_extension.tsx | 17 ++++ .../lazy_endpoint_policy_edit_extension.tsx | 18 ++++ .../security_solution/public/plugin.tsx | 22 +++-- 20 files changed, 478 insertions(+), 125 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/components/extension_wrapper.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/hooks/use_ui_extension.ts delete mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/custom_package_policy.tsx create mode 100644 x-pack/plugins/fleet/public/applications/fleet/services/ui_extensions.test.ts create mode 100644 x-pack/plugins/fleet/public/applications/fleet/services/ui_extensions.ts create mode 100644 x-pack/plugins/fleet/public/applications/fleet/types/ui_extensions.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension.tsx rename x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/{configure_package_policy.tsx => endpoint_policy_edit_extension.tsx} (81%) create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension.tsx diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/extension_wrapper.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/extension_wrapper.tsx new file mode 100644 index 0000000000000..874c91e8e546b --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/components/extension_wrapper.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, ReactNode, Suspense } from 'react'; +import { EuiErrorBoundary } from '@elastic/eui'; +import { Loading } from './loading'; + +export const ExtensionWrapper = memo<{ children: ReactNode }>(({ children }) => { + return ( + + }>{children} + + ); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_ui_extension.ts b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_ui_extension.ts new file mode 100644 index 0000000000000..93bc1eae28cf6 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_ui_extension.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 React, { useContext } from 'react'; +import { UIExtensionPoint, UIExtensionsStorage } from '../types'; + +export const UIExtensionsContext = React.createContext({}); + +type NarrowExtensionPoint = A extends { + view: V; +} + ? A + : never; + +export const useUIExtension = ( + packageName: UIExtensionPoint['package'], + view: V +): NarrowExtensionPoint['component'] | undefined => { + const registeredExtensions = useContext(UIExtensionsContext); + + if (!registeredExtensions) { + throw new Error('useUIExtension called outside of UIExtensionsContext'); + } + + const extension = registeredExtensions?.[packageName]?.[view]; + + if (extension) { + // FIXME:PT Revisit ignore below and see if TS error can be addressed + // @ts-ignore + return extension.component; + } +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/index.tsx index a49306f8e8d55..d4e652ad95831 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/index.tsx @@ -36,6 +36,8 @@ import { import { PackageInstallProvider } from './sections/epm/hooks'; import { FleetStatusProvider, useBreadcrumbs } from './hooks'; import { IntraAppStateProvider } from './hooks/use_intra_app_state'; +import { UIExtensionsStorage } from './types'; +import { UIExtensionsContext } from './hooks/use_ui_extension'; export interface ProtectedRouteProps extends RouteProps { isAllowed?: boolean; @@ -235,6 +237,7 @@ const IngestManagerApp = ({ config, history, kibanaVersion, + extensions, }: { basepath: string; coreStart: CoreStart; @@ -243,6 +246,7 @@ const IngestManagerApp = ({ config: IngestManagerConfigType; history: AppMountParameters['history']; kibanaVersion: string; + extensions: UIExtensionsStorage; }) => { const isDarkMode = useObservable(coreStart.uiSettings.get$('theme:darkMode')); return ( @@ -252,7 +256,9 @@ const IngestManagerApp = ({ - + + + @@ -268,7 +274,8 @@ export function renderApp( setupDeps: IngestManagerSetupDeps, startDeps: IngestManagerStartDeps, config: IngestManagerConfigType, - kibanaVersion: string + kibanaVersion: string, + extensions: UIExtensionsStorage ) { ReactDOM.render( , element ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/custom_package_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/custom_package_policy.tsx deleted file mode 100644 index d5163e1b9abbe..0000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/custom_package_policy.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiEmptyPrompt, EuiText } from '@elastic/eui'; -import { NewPackagePolicy } from '../../../../types'; -import { CreatePackagePolicyFrom } from '../types'; - -export interface CustomConfigurePackagePolicyProps { - packageName: string; - from: CreatePackagePolicyFrom; - packagePolicy: NewPackagePolicy; - packagePolicyId?: string; -} - -/** - * Custom content type that external plugins can provide to Ingest's - * package policy UI. - */ -export type CustomConfigurePackagePolicyContent = React.FC; - -type AllowedPackageKey = 'endpoint'; -const PackagePolicyMapping: { - [key: string]: CustomConfigurePackagePolicyContent; -} = {}; - -/** - * Plugins can call this function from the start lifecycle to - * register a custom component in the Ingest package policy. - */ -export function registerPackagePolicyComponent( - key: AllowedPackageKey, - value: CustomConfigurePackagePolicyContent -) { - PackagePolicyMapping[key] = value; -} - -const EmptyPackagePolicy: CustomConfigurePackagePolicyContent = () => ( - -

- -

- - } - /> -); - -export const CustomPackagePolicy = (props: CustomConfigurePackagePolicyProps) => { - const CustomPackagePolicyContent = PackagePolicyMapping[props.packageName] || EmptyPackagePolicy; - return ; -}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/index.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/index.ts index 893ed00cca9ac..58b5b1cd3126e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/index.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/index.ts @@ -6,4 +6,3 @@ export { CreatePackagePolicyPageLayout } from './layout'; export { PackagePolicyInputPanel } from './package_policy_input_panel'; export { PackagePolicyInputVarField } from './package_policy_input_var_field'; -export { CustomPackagePolicy } from './custom_package_policy'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx index b45794b9f87db..a837ed33e4110 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx @@ -46,6 +46,9 @@ import { StepSelectAgentPolicy } from './step_select_agent_policy'; import { StepConfigurePackagePolicy } from './step_configure_package'; import { StepDefinePackagePolicy } from './step_define_package_policy'; import { useIntraAppState } from '../../../hooks/use_intra_app_state'; +import { useUIExtension } from '../../../hooks/use_ui_extension'; +import { ExtensionWrapper } from '../../../components/extension_wrapper'; +import { PackagePolicyEditExtensionComponentProps } from '../../../types'; const StepsWithLessPadding = styled(EuiSteps)` .euiStep__content { @@ -191,6 +194,21 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { [packagePolicy, updatePackagePolicyValidation] ); + const handleExtensionViewOnChange = useCallback< + PackagePolicyEditExtensionComponentProps['onChange'] + >( + ({ isValid, updatedPolicy }) => { + updatePackagePolicy(updatedPolicy); + setFormState((prevState) => { + if (prevState === 'VALID' && !isValid) { + return 'INVALID'; + } + return prevState; + }); + }, + [updatePackagePolicy] + ); + // Cancel path const cancelUrl = useMemo(() => { if (routeState && routeState.onCancelUrl) { @@ -287,6 +305,8 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { [pkgkey, updatePackageInfo, agentPolicy, updateAgentPolicy] ); + const ExtensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-create'); + const stepSelectPackage = useMemo( () => ( { validationResults={validationResults!} submitAttempted={formState === 'INVALID'} /> + {/* If an Agent Policy and a package has been selected, then show UI extension (if any) */} + {packagePolicy.policy_id && packagePolicy.package?.name && ExtensionView && ( + + + + )} ) : (
), [ - agentPolicy, - formState, isLoadingSecondStep, - packagePolicy, + agentPolicy, packageInfo, + packagePolicy, updatePackagePolicy, validationResults, + formState, + ExtensionView, + handleExtensionViewOnChange, ] ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.tsx index b335ff439684b..671bc829af82a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.tsx @@ -4,7 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + EuiHorizontalRule, + EuiFlexGroup, + EuiFlexItem, + EuiEmptyPrompt, + EuiText, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { PackageInfo, RegistryStream, @@ -13,8 +20,9 @@ import { } from '../../../types'; import { Loading } from '../../../components'; import { PackagePolicyValidationResults } from './services'; -import { PackagePolicyInputPanel, CustomPackagePolicy } from './components'; +import { PackagePolicyInputPanel } from './components'; import { CreatePackagePolicyFrom } from './types'; +import { useUIExtension } from '../../../hooks/use_ui_extension'; const findStreamsForInputType = ( inputType: string, @@ -55,6 +63,12 @@ export const StepConfigurePackagePolicy: React.FunctionComponent<{ validationResults, submitAttempted, }) => { + const hasUiExtension = + useUIExtension( + packageInfo.name, + from === 'edit' ? 'package-policy-edit' : 'package-policy-create' + ) !== undefined; + // Configure inputs (and their streams) // Assume packages only export one config template for now const renderConfigureInputs = () => @@ -98,12 +112,20 @@ export const StepConfigurePackagePolicy: React.FunctionComponent<{ })} - ) : ( - +

+ +

+ + } /> ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index d642619515a57..bfc10848d378f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -41,6 +41,10 @@ import { } from '../create_package_policy_page/types'; import { StepConfigurePackagePolicy } from '../create_package_policy_page/step_configure_package'; import { StepDefinePackagePolicy } from '../create_package_policy_page/step_define_package_policy'; +import { useUIExtension } from '../../../hooks/use_ui_extension'; +import { ExtensionWrapper } from '../../../components/extension_wrapper'; +import { GetOnePackagePolicyResponse } from '../../../../../../common/types/rest_spec'; +import { PackagePolicyEditExtensionComponentProps } from '../../../types'; export const EditPackagePolicyPage: React.FunctionComponent = () => { const { notifications } = useCore(); @@ -68,6 +72,9 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { inputs: [], version: '', }); + const [originalPackagePolicy, setOriginalPackagePolicy] = useState< + GetOnePackagePolicyResponse['item'] + >(); // Retrieve agent policy, package, and package policy info useEffect(() => { @@ -83,6 +90,8 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { setAgentPolicy(agentPolicyData.item); } if (packagePolicyData?.item) { + setOriginalPackagePolicy(packagePolicyData.item); + const { id, revision, @@ -189,6 +198,21 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { [packagePolicy, updatePackagePolicyValidation] ); + const handleExtensionViewOnChange = useCallback< + PackagePolicyEditExtensionComponentProps['onChange'] + >( + ({ isValid, updatedPolicy }) => { + updatePackagePolicy(updatedPolicy); + setFormState((prevState) => { + if (prevState === 'VALID' && !isValid) { + return 'INVALID'; + } + return prevState; + }); + }, + [updatePackagePolicy] + ); + // Cancel url const cancelUrl = getHref('policy_details', { policyId }); @@ -267,6 +291,8 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { packageInfo, }; + const ExtensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-edit'); + const configurePackage = useMemo( () => agentPolicy && packageInfo ? ( @@ -288,16 +314,32 @@ export const EditPackagePolicyPage: React.FunctionComponent = () => { validationResults={validationResults!} submitAttempted={formState === 'INVALID'} /> + + {packagePolicy.policy_id && + packagePolicy.package?.name && + originalPackagePolicy && + ExtensionView && ( + + + + )} ) : null, [ agentPolicy, - formState, - packagePolicy, - packagePolicyId, packageInfo, + packagePolicy, updatePackagePolicy, validationResults, + packagePolicyId, + formState, + originalPackagePolicy, + ExtensionView, + handleExtensionViewOnChange, ] ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/services/ui_extensions.test.ts b/x-pack/plugins/fleet/public/applications/fleet/services/ui_extensions.test.ts new file mode 100644 index 0000000000000..97c0203fab056 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/services/ui_extensions.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { lazy } from 'react'; + +import { + PackagePolicyEditExtensionComponent, + UIExtensionRegistrationCallback, + UIExtensionsStorage, +} from '../types'; +import { createExtensionRegistrationCallback } from './ui_extensions'; + +describe('UI Extension services', () => { + describe('When using createExtensionRegistrationCallback factory', () => { + let storage: UIExtensionsStorage; + let register: UIExtensionRegistrationCallback; + + beforeEach(() => { + storage = {}; + register = createExtensionRegistrationCallback(storage); + }); + + it('should return a function', () => { + expect(register).toBeInstanceOf(Function); + }); + + it('should store an extension points', () => { + const LazyCustomView = lazy(async () => { + return { default: ((() => {}) as unknown) as PackagePolicyEditExtensionComponent }; + }); + register({ + view: 'package-policy-edit', + package: 'endpoint', + component: LazyCustomView, + }); + + expect(storage.endpoint['package-policy-edit']).toEqual({ + view: 'package-policy-edit', + package: 'endpoint', + component: LazyCustomView, + }); + }); + + it('should throw if extension point has already registered', () => { + const LazyCustomView = lazy(async () => { + return { default: ((() => {}) as unknown) as PackagePolicyEditExtensionComponent }; + }); + const LazyCustomView2 = lazy(async () => { + return { default: ((() => {}) as unknown) as PackagePolicyEditExtensionComponent }; + }); + + register({ + view: 'package-policy-edit', + package: 'endpoint', + component: LazyCustomView, + }); + + expect(() => { + register({ + view: 'package-policy-edit', + package: 'endpoint', + component: LazyCustomView2, + }); + }).toThrow(); + + expect(storage.endpoint['package-policy-edit']).toEqual({ + view: 'package-policy-edit', + package: 'endpoint', + component: LazyCustomView, + }); + }); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/services/ui_extensions.ts b/x-pack/plugins/fleet/public/applications/fleet/services/ui_extensions.ts new file mode 100644 index 0000000000000..5af9122d4f12a --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/services/ui_extensions.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UIExtensionRegistrationCallback, UIExtensionsStorage } from '../types'; + +/** Factory that returns a callback that can be used to register UI extensions */ +export const createExtensionRegistrationCallback = ( + storage: UIExtensionsStorage +): UIExtensionRegistrationCallback => { + return (extensionPoint) => { + const { package: packageName, view } = extensionPoint; + + if (!storage[packageName]) { + storage[packageName] = {}; + } + + if (storage[packageName]?.[view]) { + throw new Error(`Extension point has already been registered: [${packageName}][${view}]`); + } + + storage[packageName][view] = extensionPoint; + }; +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/types/index.ts b/x-pack/plugins/fleet/public/applications/fleet/types/index.ts index 1cf8077aeda40..78cb355318d40 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/types/index.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/types/index.ts @@ -119,3 +119,5 @@ export { } from '../../../../common'; export * from './intra_app_route_state'; + +export * from './ui_extensions'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/types/ui_extensions.ts b/x-pack/plugins/fleet/public/applications/fleet/types/ui_extensions.ts new file mode 100644 index 0000000000000..fbede8af95b66 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/types/ui_extensions.ts @@ -0,0 +1,96 @@ +/* + * 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 { ComponentType, LazyExoticComponent } from 'react'; +import { NewPackagePolicy, PackagePolicy } from './index'; + +/** Register a Fleet UI extension */ +export type UIExtensionRegistrationCallback = (extensionPoint: UIExtensionPoint) => void; + +/** Internal storage for registered UI Extension Points */ +export interface UIExtensionsStorage { + [key: string]: Partial>; +} + +/** + * UI Component Extension is used on the pages displaying the ability to edit an + * Integration Policy + */ +export type PackagePolicyEditExtensionComponent = ComponentType< + PackagePolicyEditExtensionComponentProps +>; + +export interface PackagePolicyEditExtensionComponentProps { + /** The current integration policy being edited */ + policy: PackagePolicy; + /** The new (updated) integration policy that will be saved */ + newPolicy: NewPackagePolicy; + /** + * A callback that should be executed anytime a change to the Integration Policy needs to + * be reported back to the Fleet Policy Edit page + */ + onChange: (opts: { + /** is current form state is valid */ + isValid: boolean; + /** The updated Integration Policy to be merged back and included in the API call */ + updatedPolicy: NewPackagePolicy; + }) => void; +} + +/** Extension point registration contract for Integration Policy Edit views */ +export interface PackagePolicyEditExtension { + package: string; + view: 'package-policy-edit'; + component: LazyExoticComponent; +} + +/** + * UI Component Extension is used on the pages displaying the ability to Create an + * Integration Policy + */ +export type PackagePolicyCreateExtensionComponent = ComponentType< + PackagePolicyCreateExtensionComponentProps +>; + +export interface PackagePolicyCreateExtensionComponentProps { + /** The integration policy being created */ + newPolicy: NewPackagePolicy; + /** + * A callback that should be executed anytime a change to the Integration Policy needs to + * be reported back to the Fleet Policy Edit page + */ + onChange: (opts: { + /** is current form state is valid */ + isValid: boolean; + /** The updated Integration Policy to be merged back and included in the API call */ + updatedPolicy: NewPackagePolicy; + }) => void; +} + +/** Extension point registration contract for Integration Policy Create views */ +export interface PackagePolicyCreateExtension { + package: string; + view: 'package-policy-create'; + component: LazyExoticComponent; +} + +/** + * UI Component Extension is used to display a Custom tab (and view) under a given Integration + */ +export type PackageCustomExtensionComponent = ComponentType; + +/** Extension point registration contract for Integration details Custom view */ +export interface PackageCustomExtension { + package: string; + view: 'package-detail-custom'; + component: LazyExoticComponent; +} + +/** Fleet UI Extension Point */ +export type UIExtensionPoint = + | PackagePolicyEditExtension + | PackageCustomExtension + | PackagePolicyCreateExtension; diff --git a/x-pack/plugins/fleet/public/index.ts b/x-pack/plugins/fleet/public/index.ts index f974a8c3d3cc8..1de001a6fc69e 100644 --- a/x-pack/plugins/fleet/public/index.ts +++ b/x-pack/plugins/fleet/public/index.ts @@ -12,13 +12,8 @@ export const plugin = (initializerContext: PluginInitializerContext) => { return new IngestManagerPlugin(initializerContext); }; -export { - CustomConfigurePackagePolicyContent, - CustomConfigurePackagePolicyProps, - registerPackagePolicyComponent, -} from './applications/fleet/sections/agent_policy/create_package_policy_page/components/custom_package_policy'; - export type { NewPackagePolicy } from './applications/fleet/types'; export * from './applications/fleet/types/intra_app_route_state'; +export * from './applications/fleet/types/ui_extensions'; export { pagePathGetters } from './applications/fleet/constants'; diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts index 2e7cbb9cb86ab..377ba770b5ca2 100644 --- a/x-pack/plugins/fleet/public/plugin.ts +++ b/x-pack/plugins/fleet/public/plugin.ts @@ -30,7 +30,8 @@ import { TutorialDirectoryHeaderLink, TutorialModuleNotice, } from './applications/fleet/components/home_integration'; -import { registerPackagePolicyComponent } from './applications/fleet/sections/agent_policy/create_package_policy_page/components/custom_package_policy'; +import { createExtensionRegistrationCallback } from './applications/fleet/services/ui_extensions'; +import { UIExtensionRegistrationCallback, UIExtensionsStorage } from './applications/fleet/types'; export { IngestManagerConfigType } from '../common/types'; @@ -43,7 +44,7 @@ export interface IngestManagerSetup {} * Describes public IngestManager plugin contract returned at the `start` stage. */ export interface IngestManagerStart { - registerPackagePolicyComponent: typeof registerPackagePolicyComponent; + registerExtension: UIExtensionRegistrationCallback; isInitialized: () => Promise; } @@ -62,6 +63,7 @@ export class IngestManagerPlugin Plugin { private config: IngestManagerConfigType; private kibanaVersion: string; + private extensions: UIExtensionsStorage = {}; constructor(private readonly initializerContext: PluginInitializerContext) { this.config = this.initializerContext.config.get(); @@ -71,6 +73,7 @@ export class IngestManagerPlugin public setup(core: CoreSetup, deps: IngestManagerSetupDeps) { const config = this.config; const kibanaVersion = this.kibanaVersion; + const extensions = this.extensions; // Set up http client setHttpClient(core.http); @@ -92,7 +95,15 @@ export class IngestManagerPlugin IngestManagerStart ]; const { renderApp, teardownIngestManager } = await import('./applications/fleet/'); - const unmount = renderApp(coreStart, params, deps, startDeps, config, kibanaVersion); + const unmount = renderApp( + coreStart, + params, + deps, + startDeps, + config, + kibanaVersion, + extensions + ); return () => { unmount(); @@ -153,7 +164,8 @@ export class IngestManagerPlugin return successPromise; }, - registerPackagePolicyComponent, + + registerExtension: createExtensionRegistrationCallback(this.extensions), }; } diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts b/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts index ff3fe7517e64a..3388fb5355845 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IngestManagerStart, registerPackagePolicyComponent } from '../../../../../fleet/public'; +import { IngestManagerStart } from '../../../../../fleet/public'; import { dataPluginMock, Start as DataPublicStartMock, @@ -58,7 +58,7 @@ export const depsStartMock: () => DepsStartMock = () => { data: dataMock, ingestManager: { isInitialized: () => Promise.resolve(true), - registerPackagePolicyComponent, + registerExtension: jest.fn(), }, }; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension.tsx new file mode 100644 index 0000000000000..69406a41fe055 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension.tsx @@ -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 React, { memo } from 'react'; +import { EuiCallOut, EuiSpacer, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { PackagePolicyCreateExtensionComponentProps } from '../../../../../../../fleet/public'; + +/** + * Exports Endpoint-specific package policy instructions + * for use in the Ingest app create / edit package policy + */ +export const EndpointPolicyCreateExtension = memo( + () => { + return ( + <> + + + +

+ +

+
+
+ + ); + } +); +EndpointPolicyCreateExtension.displayName = 'EndpointPolicyCreateExtension'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_package_policy.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx similarity index 81% rename from x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_package_policy.tsx rename to x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx index 0be5f119e5eff..b667ea965af68 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_package_policy.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_edit_extension.tsx @@ -20,9 +20,8 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { - CustomConfigurePackagePolicyContent, - CustomConfigurePackagePolicyProps, pagePathGetters, + PackagePolicyEditExtensionComponentProps, } from '../../../../../../../fleet/public'; import { getPolicyDetailPath, getTrustedAppsListPath } from '../../../../common/routing'; import { MANAGEMENT_APP_ID } from '../../../../common/constants'; @@ -37,42 +36,21 @@ import { useNavigateToAppEventHandler } from '../../../../../common/hooks/endpoi * Exports Endpoint-specific package policy instructions * for use in the Ingest app create / edit package policy */ -export const ConfigureEndpointPackagePolicy = memo( - ({ - from, - packagePolicyId, - packagePolicy: { policy_id: agentPolicyId }, - }: CustomConfigurePackagePolicyProps) => { +export const EndpointPolicyEditExtension = memo( + ({ policy }) => { return ( <> - + - {from === 'edit' ? ( - <> - - - ) : ( -

- -

- )} +
); } ); -ConfigureEndpointPackagePolicy.displayName = 'ConfigureEndpointPackagePolicy'; +EndpointPolicyEditExtension.displayName = 'EndpointPolicyEditExtension'; const EditFlowMessage = memo<{ agentPolicyId: string; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension.tsx new file mode 100644 index 0000000000000..b7a6fa36e4eb7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { lazy } from 'react'; +import { PackagePolicyCreateExtensionComponent } from '../../../../../../../fleet/public'; + +export const LazyEndpointPolicyCreateExtension = lazy( + async () => { + const { EndpointPolicyCreateExtension } = await import('./endpoint_policy_create_extension'); + return { + default: EndpointPolicyCreateExtension, + }; + } +); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension.tsx new file mode 100644 index 0000000000000..b417bc9ad5d9c --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { lazy } from 'react'; +import { PackagePolicyEditExtensionComponent } from '../../../../../../../fleet/public'; + +export const LazyEndpointPolicyEditExtension = lazy( + async () => { + const { EndpointPolicyEditExtension } = await import('./endpoint_policy_edit_extension'); + return { + // FIXME: remove casting once old UI component registration is removed + default: (EndpointPolicyEditExtension as unknown) as PackagePolicyEditExtensionComponent, + }; + } +); diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 08c780d4a7203..5895880adb26a 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import { BehaviorSubject } from 'rxjs'; import { pluck } from 'rxjs/operators'; - import { PluginSetup, PluginStart, @@ -44,8 +43,6 @@ import { DEFAULT_INDEX_KEY, } from '../common/constants'; -import { ConfigureEndpointPackagePolicy } from './management/pages/policy/view/ingest_manager_integration/configure_package_policy'; - import { SecurityPageName } from './app/types'; import { manageOldSiemRoutes } from './helpers'; import { @@ -63,6 +60,8 @@ import { } from '../common/search_strategy/index_fields'; import { SecurityAppStore } from './common/store/store'; import { getCaseConnectorUI } from './common/lib/connectors'; +import { LazyEndpointPolicyEditExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension'; +import { LazyEndpointPolicyCreateExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension'; export class Plugin implements IPlugin { private kibanaVersion: string; @@ -332,10 +331,19 @@ export class Plugin implements IPlugin Date: Thu, 12 Nov 2020 17:51:42 +0100 Subject: [PATCH 17/17] [ML] Update apidoc config with the Trained models endpoints (#83274) * [ML] fix apidoc annotations * [ML] add trained models * [ML] use full path to the apidoc-markdown package --- x-pack/plugins/ml/package.json | 2 +- x-pack/plugins/ml/server/routes/apidoc.json | 8 ++++- .../ml/server/routes/trained_models.ts | 30 +++++++++---------- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/x-pack/plugins/ml/package.json b/x-pack/plugins/ml/package.json index a41e9e845063f..1ec697568a849 100644 --- a/x-pack/plugins/ml/package.json +++ b/x-pack/plugins/ml/package.json @@ -6,6 +6,6 @@ "license": "Elastic-License", "scripts": { "build:apiDocScripts": "cd server/routes/apidoc_scripts && ../../../../../../node_modules/.bin/tsc", - "apiDocs": "yarn build:apiDocScripts && cd ./server/routes/ && ../../../../../node_modules/.bin/apidoc --parse-workers apischema=./apidoc_scripts/target/schema_worker.js --parse-parsers apischema=./apidoc_scripts/target/schema_parser.js --parse-filters apiversion=./apidoc_scripts/target/version_filter.js -i . -o ../routes_doc && apidoc-markdown -p ../routes_doc -o ../routes_doc/ML_API.md -t ./apidoc_scripts/template.md" + "apiDocs": "yarn build:apiDocScripts && cd ./server/routes/ && ../../../../../node_modules/.bin/apidoc --parse-workers apischema=./apidoc_scripts/target/schema_worker.js --parse-parsers apischema=./apidoc_scripts/target/schema_parser.js --parse-filters apiversion=./apidoc_scripts/target/version_filter.js -i . -o ../routes_doc && ../../../../../node_modules/.bin/apidoc-markdown -p ../routes_doc -o ../routes_doc/ML_API.md -t ./apidoc_scripts/template.md" } } \ No newline at end of file diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index 780835e2a300b..8d6dd692cc130 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -148,6 +148,12 @@ "InitializeJobSavedObjects", "AssignJobsToSpaces", "RemoveJobsFromSpaces", - "JobsSpaces" + "JobsSpaces", + + "TrainedModels", + "GetTrainedModel", + "GetTrainedModelStats", + "GetTrainedModelPipelines", + "DeleteTrainedModel" ] } diff --git a/x-pack/plugins/ml/server/routes/trained_models.ts b/x-pack/plugins/ml/server/routes/trained_models.ts index 579f63e13328d..e9bd854864c2d 100644 --- a/x-pack/plugins/ml/server/routes/trained_models.ts +++ b/x-pack/plugins/ml/server/routes/trained_models.ts @@ -16,11 +16,11 @@ import { InferenceConfigResponse } from '../../common/types/trained_models'; export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization) { /** - * @apiGroup Inference + * @apiGroup TrainedModels * * @api {get} /api/ml/trained_models/:modelId Get info of a trained inference model - * @apiName GetInferenceModel - * @apiDescription Retrieves configuration information for a trained inference model. + * @apiName GetTrainedModel + * @apiDescription Retrieves configuration information for a trained model. */ router.get( { @@ -68,11 +68,11 @@ export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization) ); /** - * @apiGroup Inference + * @apiGroup TrainedModels * - * @api {get} /api/ml/trained_models/:modelId/_stats Get stats of a trained inference model - * @apiName GetInferenceModelStats - * @apiDescription Retrieves usage information for trained inference models. + * @api {get} /api/ml/trained_models/:modelId/_stats Get stats of a trained model + * @apiName GetTrainedModelStats + * @apiDescription Retrieves usage information for trained models. */ router.get( { @@ -100,11 +100,11 @@ export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization) ); /** - * @apiGroup Inference + * @apiGroup TrainedModels * - * @api {get} /api/ml/trained_models/:modelId/pipelines Get model pipelines - * @apiName GetModelPipelines - * @apiDescription Retrieves pipelines associated with a model + * @api {get} /api/ml/trained_models/:modelId/pipelines Get trained model pipelines + * @apiName GetTrainedModelPipelines + * @apiDescription Retrieves pipelines associated with a trained model */ router.get( { @@ -130,11 +130,11 @@ export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization) ); /** - * @apiGroup Inference + * @apiGroup TrainedModels * - * @api {delete} /api/ml/trained_models/:modelId Get stats of a trained inference model - * @apiName DeleteInferenceModel - * @apiDescription Deletes an existing trained inference model that is currently not referenced by an ingest pipeline. + * @api {delete} /api/ml/trained_models/:modelId Delete a trained model + * @apiName DeleteTrainedModel + * @apiDescription Deletes an existing trained model that is currently not referenced by an ingest pipeline. */ router.delete( {