From 2c02458ee872d845c54fc901978d632c77ee9416 Mon Sep 17 00:00:00 2001 From: Chris Davies Date: Tue, 15 Oct 2019 19:40:30 -0400 Subject: [PATCH 01/48] [Lens] Add save modal to Lens (#48013) --- .../lens/public/app_plugin/app.test.tsx | 132 +++++++++++++++--- .../plugins/lens/public/app_plugin/app.tsx | 94 ++++++++----- .../_workspace_panel_wrapper.scss | 11 +- .../editor_frame/editor_frame.tsx | 2 +- .../editor_frame/workspace_panel_wrapper.tsx | 19 +-- .../test/functional/apps/lens/smokescreen.ts | 4 +- .../test/functional/page_objects/lens_page.ts | 18 ++- 7 files changed, 183 insertions(+), 97 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx index 83d6e273578d5..77d0d5a5305aa 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx @@ -288,6 +288,11 @@ describe('Lens App', () => { }); describe('save button', () => { + interface SaveProps { + newCopyOnSave: boolean; + newTitle: string; + } + function getButton(instance: ReactWrapper): TopNavMenuData { return (instance .find('[data-test-subj="lnsApp_topNav"]') @@ -296,6 +301,68 @@ describe('Lens App', () => { )!; } + function testSave(instance: ReactWrapper, saveProps: SaveProps) { + act(() => { + getButton(instance).run(instance.getDOMNode()); + }); + + instance.update(); + + const handler = instance.findWhere(el => el.prop('onSave')).prop('onSave') as (( + p: unknown + ) => void); + handler(saveProps); + } + + async function save({ + initialDocId, + ...saveProps + }: SaveProps & { + initialDocId?: string; + }) { + const args = { + ...makeDefaultArgs(), + docId: initialDocId, + }; + args.editorFrame = frame; + (args.docStorage.load as jest.Mock).mockResolvedValue({ + id: '1234', + state: { + query: 'fake query', + datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, + }, + }); + (args.docStorage.save as jest.Mock).mockImplementation(async ({ id }) => ({ + id: id || 'aaa', + })); + + const instance = mount(); + + await waitForPromises(); + + if (initialDocId) { + expect(args.docStorage.load).toHaveBeenCalledTimes(1); + } else { + expect(args.docStorage.load).not.toHaveBeenCalled(); + } + + const onChange = frame.mount.mock.calls[0][1].onChange; + onChange({ + filterableIndexPatterns: [], + doc: ({ id: initialDocId } as unknown) as Document, + }); + + instance.update(); + + expect(getButton(instance).disableButton).toEqual(false); + + testSave(instance, saveProps); + + await waitForPromises(); + + return { args, instance }; + } + it('shows a disabled save button when the user does not have permissions', async () => { const args = makeDefaultArgs(); args.core.application = { @@ -335,37 +402,61 @@ describe('Lens App', () => { expect(getButton(instance).disableButton).toEqual(false); }); - it('saves the latest doc and then prevents more saving', async () => { - const args = makeDefaultArgs(); - args.editorFrame = frame; - (args.docStorage.save as jest.Mock).mockResolvedValue({ id: '1234' }); + it('saves new docs', async () => { + const { args, instance } = await save({ + initialDocId: undefined, + newCopyOnSave: false, + newTitle: 'hello there', + }); - const instance = mount(); + expect(args.docStorage.save).toHaveBeenCalledWith({ + id: undefined, + title: 'hello there', + }); - expect(frame.mount).toHaveBeenCalledTimes(1); + expect(args.redirectTo).toHaveBeenCalledWith('aaa'); - const onChange = frame.mount.mock.calls[0][1].onChange; - onChange({ filterableIndexPatterns: [], doc: ({ id: undefined } as unknown) as Document }); + instance.setProps({ docId: 'aaa' }); - instance.update(); + expect(args.docStorage.load).not.toHaveBeenCalled(); + }); - expect(getButton(instance).disableButton).toEqual(false); + it('saves the latest doc as a copy', async () => { + const { args, instance } = await save({ + initialDocId: '1234', + newCopyOnSave: true, + newTitle: 'hello there', + }); - act(() => { - getButton(instance).run(instance.getDOMNode()); + expect(args.docStorage.save).toHaveBeenCalledWith({ + id: undefined, + title: 'hello there', }); - expect(args.docStorage.save).toHaveBeenCalledWith({ id: undefined }); + expect(args.redirectTo).toHaveBeenCalledWith('aaa'); - await waitForPromises(); + instance.setProps({ docId: 'aaa' }); - expect(args.redirectTo).toHaveBeenCalledWith('1234'); + expect(args.docStorage.load).toHaveBeenCalledTimes(1); + }); - instance.setProps({ docId: '1234' }); + it('saves existing docs', async () => { + const { args, instance } = await save({ + initialDocId: '1234', + newCopyOnSave: false, + newTitle: 'hello there', + }); - expect(args.docStorage.load).not.toHaveBeenCalled(); + expect(args.docStorage.save).toHaveBeenCalledWith({ + id: '1234', + title: 'hello there', + }); - expect(getButton(instance).disableButton).toEqual(true); + expect(args.redirectTo).not.toHaveBeenCalled(); + + instance.setProps({ docId: '1234' }); + + expect(args.docStorage.load).toHaveBeenCalledTimes(1); }); it('handles save failure by showing a warning, but still allows another save', async () => { @@ -380,11 +471,8 @@ describe('Lens App', () => { instance.update(); - act(() => { - getButton(instance).run(instance.getDOMNode()); - }); + testSave(instance, { newCopyOnSave: false, newTitle: 'hello there' }); - await waitForPromises(); await waitForPromises(); expect(args.core.notifications.toasts.addDanger).toHaveBeenCalled(); diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx index ef53f6046f266..640f1fdb99a21 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -5,12 +5,12 @@ */ import _ from 'lodash'; -import React, { useState, useEffect, useCallback, useRef } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { Storage } from 'ui/storage'; import { DataPublicPluginStart } from 'src/plugins/data/public'; - +import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; import { CoreStart, NotificationsStart } from 'src/core/public'; import { DataStart, @@ -28,9 +28,10 @@ import { NativeRenderer } from '../native_renderer'; interface State { isLoading: boolean; - isDirty: boolean; + isSaveModalVisible: boolean; indexPatternsForTopNav: IndexPatternInstance[]; persistedDoc?: Document; + lastKnownDoc?: Document; // Properties needed to interface with TopNav dateRange: { @@ -67,9 +68,8 @@ export function App({ const [state, setState] = useState({ isLoading: !!docId, - isDirty: false, + isSaveModalVisible: false, indexPatternsForTopNav: [], - query: { query: '', language }, dateRange: { fromDate: timeDefaults.from, @@ -78,7 +78,7 @@ export function App({ filters: [], }); - const lastKnownDocRef = useRef(undefined); + const { lastKnownDoc } = state; useEffect(() => { const subscription = dataShim.filter.filterManager.getUpdates$().subscribe({ @@ -124,6 +124,7 @@ export function App({ ...s, isLoading: false, persistedDoc: doc, + lastKnownDoc: doc, query: doc.state.query, filters: doc.state.filters, indexPatternsForTopNav: indexPatterns, @@ -139,7 +140,7 @@ export function App({ setState(s => ({ ...s, isLoading: false })); core.notifications.toasts.addDanger( - i18n.translate('xpack.lens.editorFrame.docLoadingError', { + i18n.translate('xpack.lens.app.docLoadingError', { defaultMessage: 'Error loading saved document', }) ); @@ -149,10 +150,7 @@ export function App({ } }, [docId]); - // Can save if the frame has told us what it has, and there is either: - // a) No saved doc - // b) A saved doc that differs from the frame state - const isSaveable = state.isDirty && (core.application.capabilities.visualize.save as boolean); + const isSaveable = lastKnownDoc && core.application.capabilities.visualize.save; const onError = useCallback( (e: { message: string }) => @@ -177,32 +175,12 @@ export function App({ { - if (isSaveable && lastKnownDocRef.current) { - docStorage - .save(lastKnownDocRef.current) - .then(({ id }) => { - // Prevents unnecessary network request and disables save button - const newDoc = { ...lastKnownDocRef.current!, id }; - setState(s => ({ - ...s, - isDirty: false, - persistedDoc: newDoc, - })); - if (docId !== id) { - redirectTo(id); - } - }) - .catch(() => { - core.notifications.toasts.addDanger( - i18n.translate('xpack.lens.editorFrame.docSavingError', { - defaultMessage: 'Error saving document', - }) - ); - }); + if (isSaveable && lastKnownDoc) { + setState(s => ({ ...s, isSaveModalVisible: true })); } }, testId: 'lnsApp_saveButton', @@ -278,10 +256,8 @@ export function App({ doc: state.persistedDoc, onError, onChange: ({ filterableIndexPatterns, doc }) => { - lastKnownDocRef.current = doc; - if (!_.isEqual(state.persistedDoc, doc)) { - setState(s => ({ ...s, isDirty: true })); + setState(s => ({ ...s, lastKnownDoc: doc })); } // Update the cached index patterns if the user made a change to any of them @@ -307,6 +283,48 @@ export function App({ /> )} + {lastKnownDoc && state.isSaveModalVisible && ( + { + const doc = { + ...lastKnownDoc, + id: props.newCopyOnSave ? undefined : lastKnownDoc.id, + title: props.newTitle, + }; + + docStorage + .save(doc) + .then(({ id }) => { + // Prevents unnecessary network request and disables save button + const newDoc = { ...doc, id }; + setState(s => ({ + ...s, + isSaveModalVisible: false, + persistedDoc: newDoc, + lastKnownDoc: newDoc, + })); + + if (docId !== id) { + redirectTo(id); + } + }) + .catch(() => { + core.notifications.toasts.addDanger( + i18n.translate('xpack.lens.app.docSavingError', { + defaultMessage: 'Error saving document', + }) + ); + setState(s => ({ ...s, isSaveModalVisible: false })); + }); + }} + onClose={() => setState(s => ({ ...s, isSaveModalVisible: false }))} + title={lastKnownDoc.title || ''} + showCopyOnSave={true} + objectType={i18n.translate('xpack.lens.app.saveModalType', { + defaultMessage: 'Lens visualization', + })} + /> + )} ); @@ -321,7 +339,7 @@ export async function getAllIndexPatterns( return await Promise.all(ids.map(({ id }) => indexPatternsService.get(id))); } catch (e) { notifications.toasts.addDanger( - i18n.translate('xpack.lens.editorFrame.indexPatternLoadingError', { + i18n.translate('xpack.lens.app.indexPatternLoadingError', { defaultMessage: 'Error loading index patterns', }) ); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_workspace_panel_wrapper.scss b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_workspace_panel_wrapper.scss index b8f5f95df8f08..7811df93ba8ce 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_workspace_panel_wrapper.scss +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/_workspace_panel_wrapper.scss @@ -8,7 +8,8 @@ flex-direction: column; .lnsWorkspacePanelWrapper__pageContentHeader { - padding: $euiSizeS; + @include euiTitle('xs'); + padding: $euiSizeM; border-bottom: $euiBorderThin; // override EuiPage margin-bottom: 0 !important; // sass-lint:disable-line no-important @@ -31,11 +32,3 @@ } } } - -.lnsWorkspacePanelWrapper__titleInput { - @include euiTitle('xs'); - width: 100%; - max-width: none; - background: transparent; - box-shadow: none; -} diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx index 5d623fa86cd86..04c0b22c378d7 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx @@ -259,7 +259,7 @@ export function EditorFrame(props: EditorFrameProps) { } workspacePanel={ allLoaded && ( - + ; children: React.ReactNode | React.ReactNode[]; } -export function WorkspacePanelWrapper({ children, title, dispatch }: Props) { +export function WorkspacePanelWrapper({ children, title }: Props) { return ( - dispatch({ type: 'UPDATE_TITLE', title: e.target.value })} - aria-label={i18n.translate('xpack.lens.chartTitleAriaLabel', { - defaultMessage: 'Title', - })} - /> + {title} {children} diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index a262d5b14cbf7..c511fcd997217 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -99,9 +99,7 @@ export default function({ getService, getPageObjects, ...rest }: FtrProviderCont field: 'ip', }); - await PageObjects.lens.setTitle('Afancilenstest'); - - await PageObjects.lens.save(); + await PageObjects.lens.save('Afancilenstest'); // Ensure the visualization shows up in the visualize list, and takes // us back to the visualization as we configured it. diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 1825876782d15..8153a8713ca2f 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -124,16 +124,20 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont /** * Save the current Lens visualization. */ - save() { - return testSubjects.click('lnsApp_saveButton'); - }, - - setTitle(title: string) { - return testSubjects.setValue('lns_ChartTitle', title); + async save(title: string) { + await testSubjects.click('lnsApp_saveButton'); + await testSubjects.setValue('savedObjectTitle', title); + await testSubjects.click('confirmSaveSavedObjectButton'); + retry.waitForWithTimeout('Save modal to disappear', 1000, () => + testSubjects + .missingOrFail('confirmSaveSavedObjectButton') + .then(() => true) + .catch(() => false) + ); }, getTitle() { - return testSubjects.getAttribute('lns_ChartTitle', 'value'); + return testSubjects.getVisibleText('lns_ChartTitle'); }, }); } From 125f43e376a75784744d1871a31cd051c075acf0 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 15 Oct 2019 17:07:41 -0700 Subject: [PATCH 02/48] [SIEM] update ip/hosts details tables to zero when detailName changes (#48253) --- .../network_top_n_flow_table/index.tsx | 12 +- .../siem/public/containers/source/index.tsx | 2 + .../siem/public/pages/hosts/details/index.tsx | 12 +- .../siem/public/pages/hosts/details/types.ts | 8 +- .../__snapshots__/ip_details.test.tsx.snap | 64 +-- .../public/pages/network/ip_details.test.tsx | 9 +- .../siem/public/pages/network/ip_details.tsx | 543 +++++++++--------- .../siem/public/pages/network/types.ts | 5 + .../siem/public/store/hosts/actions.ts | 4 + .../siem/public/store/hosts/reducer.ts | 8 + 10 files changed, 320 insertions(+), 347 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.tsx index b38b261e85743..507f8b75b842a 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexItem } from '@elastic/eui'; import { isEqual, last } from 'lodash/fp'; -import React, { useEffect } from 'react'; +import React from 'react'; import { connect } from 'react-redux'; import styled from 'styled-components'; import { ActionCreator } from 'typescript-fsa'; @@ -31,7 +31,6 @@ interface OwnProps { flowTargeted: FlowTargetSourceDest; id: string; indexPattern: StaticIndexPattern; - ip?: string; isInspect: boolean; loading: boolean; loadPage: (newActivePage: number) => void; @@ -47,7 +46,6 @@ interface NetworkTopNFlowTableReduxProps { } interface NetworkTopNFlowTableDispatchProps { - setIpDetailsTablesActivePageToZero: ActionCreator; updateIpDetailsTableActivePage: ActionCreator<{ activePage: number; tableType: networkModel.IpDetailsTableType; @@ -93,12 +91,10 @@ const NetworkTopNFlowTableComponent = React.memo( flowTargeted, id, indexPattern, - ip, isInspect, limit, loading, loadPage, - setIpDetailsTablesActivePageToZero, showMorePagesIndicator, topNFlowSort, totalCount, @@ -108,11 +104,6 @@ const NetworkTopNFlowTableComponent = React.memo( updateTopNFlowLimit, updateTopNFlowSort, }) => { - useEffect(() => { - if (ip && activePage !== 0) { - setIpDetailsTablesActivePageToZero(null); - } - }, [ip]); const onChange = (criteria: Criteria, tableType: networkModel.TopNTableType) => { if (criteria.sort != null) { const splitField = criteria.sort.field.split('.'); @@ -200,7 +191,6 @@ const mapStateToProps = (state: State, ownProps: OwnProps) => export const NetworkTopNFlowTable = connect( mapStateToProps, { - setIpDetailsTablesActivePageToZero: networkActions.setIpDetailsTablesActivePageToZero, updateTopNFlowLimit: networkActions.updateTopNFlowLimit, updateTopNFlowSort: networkActions.updateTopNFlowSort, updateNetworkPageTableActivePage: networkActions.updateNetworkPageTableActivePage, diff --git a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx b/x-pack/legacy/plugins/siem/public/containers/source/index.tsx index ead483baab43e..edd5740f62879 100644 --- a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/source/index.tsx @@ -108,5 +108,7 @@ export const WithSource = React.memo(({ children, sourceId }) = ); }); +WithSource.displayName = 'WithSource'; + export const indicesExistOrDataTemporarilyUnavailable = (indicesExist: boolean | undefined) => indicesExist || isUndefined(indicesExist); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx index fec7109ef71d2..349ede9158113 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx @@ -5,7 +5,7 @@ */ import { EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; -import React, { useContext } from 'react'; +import React, { useContext, useEffect } from 'react'; import { compose } from 'redux'; import { connect } from 'react-redux'; import { StickyContainer } from 'react-sticky'; @@ -19,6 +19,7 @@ import { MlCapabilitiesContext } from '../../../components/ml/permissions/ml_cap import { hasMlUserPermissions } from '../../../components/ml/permissions/has_ml_user_permissions'; import { AnomalyTableProvider } from '../../../components/ml/anomaly/anomaly_table_provider'; import { scoreIntervalToDateTime } from '../../../components/ml/score/score_interval_to_datetime'; +import { setHostDetailsTablesActivePageToZero as dispatchHostDetailsTablesActivePageToZero } from '../../../store/hosts/actions'; import { SiemNavigation } from '../../../components/navigation'; import { manageQuery } from '../../../components/page/manage_query'; import { HostOverview } from '../../../components/page/hosts/host_overview'; @@ -45,14 +46,18 @@ const KpiHostDetailsManage = manageQuery(KpiHostsComponent); const HostDetailsComponent = React.memo( ({ detailName, - isInitializing, filters, from, + isInitializing, query, - setQuery, setAbsoluteRangeDatePicker, + setHostDetailsTablesActivePageToZero, + setQuery, to, }) => { + useEffect(() => { + setHostDetailsTablesActivePageToZero(null); + }, [detailName]); const capabilities = useContext(MlCapabilitiesContext); return ( <> @@ -193,6 +198,7 @@ export const HostDetails = compose; +} + export interface HostDetailsBodyProps extends HostsQueryProps { children: CommonChildren | AnomaliesChildren; } @@ -43,7 +47,7 @@ export type HostDetailsComponentProps = HostDetailsComponentReduxProps & HostsQueryProps; export type HostDetailsBodyComponentProps = HostDetailsComponentReduxProps & - HostDetailsComponentDispatchProps & + HostBodyComponentDispatchProps & HostDetailsBodyProps; type KeyHostDetailsNavTabWithoutMlPermission = HostsTableType.authentications & diff --git a/x-pack/legacy/plugins/siem/public/pages/network/__snapshots__/ip_details.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/network/__snapshots__/ip_details.test.tsx.snap index baba4fc36ef8f..e7598ef03d786 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/__snapshots__/ip_details.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/pages/network/__snapshots__/ip_details.test.tsx.snap @@ -1,59 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Ip Details it matches the snapshot 1`] = ` - + + + + + + `; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details.test.tsx b/x-pack/legacy/plugins/siem/public/pages/network/ip_details.test.tsx index b9576dc680898..e903a0b4b2761 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/ip_details.test.tsx @@ -75,7 +75,6 @@ const getMockHistory = (ip: string) => ({ const to = new Date('2018-03-23T18:49:23.132Z').valueOf(); const from = new Date('2018-03-24T03:33:52.253Z').valueOf(); - const getMockProps = (ip: string) => ({ to, from, @@ -98,6 +97,7 @@ const getMockProps = (ip: string) => ({ from: number; to: number; }>, + setIpDetailsTablesActivePageToZero: (jest.fn() as unknown) as ActionCreator, }); jest.mock('ui/documentation_links', () => ({ @@ -137,12 +137,7 @@ describe('Ip Details', () => { }); test('it renders', () => { const wrapper = shallow(); - expect( - wrapper - .dive() - .find('[data-test-subj="ip-details-page"]') - .exists() - ).toBe(true); + expect(wrapper.find('[data-test-subj="ip-details-page"]').exists()).toBe(true); }); test('it matches the snapshot', () => { diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details.tsx b/x-pack/legacy/plugins/siem/public/pages/network/ip_details.tsx index 6fd27e0ab178c..77364fa3a17b3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/ip_details.tsx @@ -6,27 +6,28 @@ import { EuiHorizontalRule, EuiSpacer, EuiFlexItem } from '@elastic/eui'; import { getOr } from 'lodash/fp'; -import React from 'react'; +import React, { useEffect } from 'react'; import { connect } from 'react-redux'; import { StickyContainer } from 'react-sticky'; -import { pure } from 'recompose'; import { Breadcrumb } from 'ui/chrome'; import { FiltersGlobal } from '../../components/filters_global'; import { HeaderPage } from '../../components/header_page'; import { LastEventTime } from '../../components/last_event_time'; import { getNetworkUrl } from '../../components/link_to/redirect_to_network'; -import { manageQuery } from '../../components/page/manage_query'; import { AnomalyTableProvider } from '../../components/ml/anomaly/anomaly_table_provider'; +import { networkToCriteria } from '../../components/ml/criteria/network_to_criteria'; import { scoreIntervalToDateTime } from '../../components/ml/score/score_interval_to_datetime'; import { AnomaliesNetworkTable } from '../../components/ml/tables/anomalies_network_table'; -import { networkToCriteria } from '../../components/ml/criteria/network_to_criteria'; +import { manageQuery } from '../../components/page/manage_query'; import { FlowTargetSelectConnected } from '../../components/page/network/flow_target_select_connected'; import { IpOverview } from '../../components/page/network/ip_overview'; -import { UsersTable } from '../../components/page/network/users_table'; +import { NetworkTopNFlowTable } from '../../components/page/network/network_top_n_flow_table'; import { TlsTable } from '../../components/page/network/tls_table'; +import { UsersTable } from '../../components/page/network/users_table'; import { SiemSearchBar } from '../../components/search_bar'; import { IpOverviewQuery } from '../../containers/ip_overview'; +import { NetworkTopNFlowQuery } from '../../containers/network_top_n_flow'; import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; import { TlsQuery } from '../../containers/tls'; import { UsersQuery } from '../../containers/users'; @@ -36,11 +37,9 @@ import { convertToBuildEsQuery } from '../../lib/keury'; import { ConditionalFlexGroup } from '../../pages/network/navigation/conditional_flex_group'; import { networkModel, networkSelectors, State, inputsSelectors } from '../../store'; import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../store/inputs/actions'; +import { setIpDetailsTablesActivePageToZero as dispatchIpDetailsTablesActivePageToZero } from '../../store/network/actions'; import { SpyRoute } from '../../utils/route/spy_routes'; - import { NetworkEmptyPage } from './network_empty_page'; -import { NetworkTopNFlowQuery } from '../../containers/network_top_n_flow'; -import { NetworkTopNFlowTable } from '../../components/page/network/network_top_n_flow_table'; import * as i18n from './translations'; import { IPDetailsComponentProps } from './types'; @@ -49,286 +48,291 @@ const UsersTableManage = manageQuery(UsersTable); const IpOverviewManage = manageQuery(IpOverview); const NetworkTopNFlowTableManage = manageQuery(NetworkTopNFlowTable); -export const IPDetailsComponent = pure( +export const IPDetailsComponent = React.memo( ({ detailName, filters, flowTarget, - setAbsoluteRangeDatePicker, - to, from, + isInitializing, query, + setAbsoluteRangeDatePicker, + setIpDetailsTablesActivePageToZero, setQuery, - isInitializing, - }) => ( - <> - - {({ indicesExist, indexPattern }) => { - const ip = decodeIpv6(detailName); - const filterQuery = convertToBuildEsQuery({ - indexPattern, - queries: [query], - filters, - }); - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - + to, + }) => { + useEffect(() => { + setIpDetailsTablesActivePageToZero(null); + }, [detailName]); + return ( + <> + + {({ indicesExist, indexPattern }) => { + const ip = decodeIpv6(detailName); + const filterQuery = convertToBuildEsQuery({ + indexPattern, + queries: [query], + filters, + }); + return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + + + + - } - title={ip} - draggableArguments={{ field: `${flowTarget}.ip`, value: ip }} - > - - + } + title={ip} + draggableArguments={{ field: `${flowTarget}.ip`, value: ip }} + > + + - - {({ id, inspect, ipOverviewData, loading, refetch }) => ( - - {({ isLoadingAnomaliesData, anomaliesData }) => ( - { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }} - /> - )} - - )} - + + {({ id, inspect, ipOverviewData, loading, refetch }) => ( + + {({ isLoadingAnomaliesData, anomaliesData }) => ( + { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }} + /> + )} + + )} + - + - - - - {({ - id, - inspect, - isInspected, - loading, - loadPage, - networkTopNFlow, - pageInfo, - refetch, - totalCount, - }) => ( - - )} - - + + + + {({ + id, + inspect, + isInspected, + loading, + loadPage, + networkTopNFlow, + pageInfo, + refetch, + totalCount, + }) => ( + + )} + + - - - {({ - id, - inspect, - isInspected, - loading, - loadPage, - networkTopNFlow, - pageInfo, - refetch, - totalCount, - }) => ( - - )} - - - + + + {({ + id, + inspect, + isInspected, + loading, + loadPage, + networkTopNFlow, + pageInfo, + refetch, + totalCount, + }) => ( + + )} + + + - + - - {({ - id, - inspect, - isInspected, - users, - totalCount, - pageInfo, - loading, - loadPage, - refetch, - }) => ( - - )} - + + {({ + id, + inspect, + isInspected, + users, + totalCount, + pageInfo, + loading, + loadPage, + refetch, + }) => ( + + )} + - + - - {({ - id, - inspect, - isInspected, - tls, - totalCount, - pageInfo, - loading, - loadPage, - refetch, - }) => ( - - )} - + + {({ + id, + inspect, + isInspected, + tls, + totalCount, + pageInfo, + loading, + loadPage, + refetch, + }) => ( + + )} + - + - { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }} - /> - - ) : ( - <> - + { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }} + /> + + ) : ( + <> + - - - ); - }} - - - - ) + + + ); + }} + + + + ); + } ); IPDetailsComponent.displayName = 'IPDetailsComponent'; @@ -348,6 +352,7 @@ export const IPDetails = connect( makeMapStateToProps, { setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker, + setIpDetailsTablesActivePageToZero: dispatchIpDetailsTablesActivePageToZero, } )(IPDetailsComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/network/types.ts b/x-pack/legacy/plugins/siem/public/pages/network/types.ts index 4dedb27d93fcb..be58572cb6621 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/network/types.ts @@ -37,8 +37,13 @@ interface IPDetailsComponentReduxProps { filters: Filter[]; flowTarget: FlowTarget; query: Query; +} + +interface IPDetailsComponentDispatchProps { setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; + setIpDetailsTablesActivePageToZero: ActionCreator; } export type IPDetailsComponentProps = IPDetailsComponentReduxProps & + IPDetailsComponentDispatchProps & GlobalTimeArgs & { detailName: string }; diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/actions.ts b/x-pack/legacy/plugins/siem/public/store/hosts/actions.ts index 4dcc5b0db9b0b..6dec6170fb72d 100644 --- a/x-pack/legacy/plugins/siem/public/store/hosts/actions.ts +++ b/x-pack/legacy/plugins/siem/public/store/hosts/actions.ts @@ -19,6 +19,10 @@ export const updateTableActivePage = actionCreator<{ export const setHostTablesActivePageToZero = actionCreator('SET_HOST_TABLES_ACTIVE_PAGE_TO_ZERO'); +export const setHostDetailsTablesActivePageToZero = actionCreator( + 'SET_HOST_DETAILS_TABLES_ACTIVE_PAGE_TO_ZERO' +); + export const updateTableLimit = actionCreator<{ hostsType: HostsType; limit: number; diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/reducer.ts b/x-pack/legacy/plugins/siem/public/store/hosts/reducer.ts index ef08f0c2c3656..11b0a985c5762 100644 --- a/x-pack/legacy/plugins/siem/public/store/hosts/reducer.ts +++ b/x-pack/legacy/plugins/siem/public/store/hosts/reducer.ts @@ -10,6 +10,7 @@ import { Direction, HostsFields } from '../../graphql/types'; import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../constants'; import { + setHostDetailsTablesActivePageToZero, setHostTablesActivePageToZero, updateHostsSort, updateTableActivePage, @@ -84,6 +85,13 @@ export const hostsReducer = reducerWithInitialState(initialHostsState) queries: setHostDetailsQueriesActivePageToZero(state), }, })) + .case(setHostDetailsTablesActivePageToZero, state => ({ + ...state, + details: { + ...state.details, + queries: setHostDetailsQueriesActivePageToZero(state), + }, + })) .case(updateTableActivePage, (state, { activePage, hostsType, tableType }) => ({ ...state, [hostsType]: { From 3a171b21ed2f5a8137442bbf2245b4a0639003c6 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Tue, 15 Oct 2019 20:19:45 -0400 Subject: [PATCH 03/48] update link siem to new url state structure for query (#48123) --- .../linux_anomalous_network_activity_ecs.json | 67 +++++++++++-------- ...x_anomalous_network_port_activity_ecs.json | 10 +-- .../ml/linux_anomalous_network_service.json | 10 +-- ...linux_anomalous_process_all_hosts_ecs.json | 10 +-- .../ml/linux_anomalous_user_name_ecs.json | 30 ++++++--- .../ml/rare_process_by_host_linux_ecs.json | 10 +-- .../ml/suspicious_login_activity_ecs.json | 10 +-- .../ml/suspicious_login_activity_ecs.json | 6 +- .../ml/rare_process_by_host_windows_ecs.json | 13 ++-- ...indows_anomalous_network_activity_ecs.json | 67 +++++++++++-------- .../windows_anomalous_path_activity_ecs.json | 16 ++--- ...ndows_anomalous_process_all_hosts_ecs.json | 31 ++++++--- .../windows_anomalous_process_creation.json | 10 +-- .../ml/windows_anomalous_script.json | 6 +- .../ml/windows_anomalous_user_name_ecs.json | 10 +-- .../ml/windows_rare_user_runas_event.json | 30 ++++++--- ...windows_rare_user_type10_remote_login.json | 10 +-- 17 files changed, 204 insertions(+), 142 deletions(-) diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_activity_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_activity_ecs.json index 97c793c51f753..ef0d51e274dac 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_activity_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_activity_ecs.json @@ -1,10 +1,19 @@ { - "job_type": "anomaly_detector", - "description": "SIEM Auditbeat: Looks for unusual processes using the network which could indicate command-and-control, lateral movement, persistence, or data exfiltration activity (beta)", - "groups": [ - "siem", - "auditbeat", - "process" + "job_type": "anomaly_detector", + "description": "SIEM Auditbeat: Looks for unusual processes using the network which could indicate command-and-control, lateral movement, persistence, or data exfiltration activity (beta)", + "groups": [ + "siem", + "auditbeat", + "process" + ], + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "rare by \"process.name\"", + "function": "rare", + "by_field_name": "process.name" + } ], "analysis_config": { "bucket_span": "15m", @@ -30,24 +39,28 @@ "time_field": "@timestamp" }, "custom_settings": { - "created_by": "ml-module-siem-auditbeat", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ] - } - } + "created_by": "ml-module-siem-auditbeat", + "custom_urls": [ + { + "url_name": "Host Details by process name", + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Host Details by user name", + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Hosts Overview by process name", + "url_value": "siem#/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Hosts Overview by user name", + "url_value": "siem#/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + } + ] + } + ] +} +] +} +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_port_activity_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_port_activity_ecs.json index 24f2690b27774..373e3c662a5d0 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_port_activity_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_port_activity_ecs.json @@ -34,20 +34,20 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(expression:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Host Details by user name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(expression:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Hosts Overview by process name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts?_g=()&query=(expression:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Hosts Overview by user name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts?_g=()&query=(expression:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" } ] } -} +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_service.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_service.json index 8a5deddbeb5dc..9ded51f09200b 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_service.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_network_service.json @@ -33,20 +33,20 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Host Details by user name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Hosts Overview by process name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Hosts Overview by user name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" } ] } -} +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_process_all_hosts_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_process_all_hosts_ecs.json index e0c310e33a1c9..a204828d2669c 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_process_all_hosts_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_process_all_hosts_ecs.json @@ -33,20 +33,20 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Host Details by user name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Hosts Overview by process name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Hosts Overview by user name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" } ] } -} +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_user_name_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_user_name_ecs.json index f7d4725d318e8..a3247f45ea41a 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_user_name_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_anomalous_user_name_ecs.json @@ -1,9 +1,19 @@ { - "job_type": "anomaly_detector", - "groups": [ - "siem", - "auditbeat", - "process" + "job_type": "anomaly_detector", + "groups": [ + "siem", + "auditbeat", + "process" + ], + "description": "SIEM Auditbeat: Rare and unusual users that are not normally active may indicate unauthorized changes or activity by an unauthorized user which may be credentialed access or lateral movement (beta)", + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "rare by \"user.name\"", + "function": "rare", + "by_field_name": "user.name" + } ], "description": "SIEM Auditbeat: Rare and unusual users that are not normally active may indicate unauthorized changes or activity by an unauthorized user which may be credentialed access or lateral movement (beta)", "analysis_config": { @@ -33,20 +43,22 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Host Details by user name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Hosts Overview by process name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Hosts Overview by user name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" } ] } } +} +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/rare_process_by_host_linux_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/rare_process_by_host_linux_ecs.json index a770300fb0601..aa9d49137c595 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/rare_process_by_host_linux_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/rare_process_by_host_linux_ecs.json @@ -34,20 +34,20 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Host Details by user name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Hosts Overview by process name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Hosts Overview by user name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" } ] } -} +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/suspicious_login_activity_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/suspicious_login_activity_ecs.json index d66ac51439340..30e9d6a570ad8 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/suspicious_login_activity_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/suspicious_login_activity_ecs.json @@ -1,7 +1,9 @@ { "job_type": "anomaly_detector", "description": "SIEM Auditbeat: Detect unusually high number of authentication attempts (beta)", - "groups": ["siem"], + "groups": [ + "siem" + ], "analysis_config": { "bucket_span": "15m", "detectors": [ @@ -19,7 +21,7 @@ }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb" + "model_memory_limit": "256mb" }, "data_description": { "time_field": "@timestamp", @@ -30,8 +32,8 @@ "custom_urls": [ { "url_name": "IP Address Details", - "url_value": "siem#/ml-network/ip/$source.ip$?_g=()&kqlQuery=(filterQuery:!n,queryLocation:network.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-network/ip/$source.ip$?_g=()&query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" } ] } -} +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json index 18f56a98202dd..4f48cd0ffc114 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json @@ -23,7 +23,7 @@ }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb" + "model_memory_limit": "256mb" }, "data_description": { "time_field": "@timestamp" @@ -33,8 +33,8 @@ "custom_urls": [ { "url_name": "IP Address Details", - "url_value": "siem#/ml-network/ip/$source.ip$?_g=()&kqlQuery=(filterQuery:!n,queryLocation:network.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-network/ip/$source.ip$?_g=()&query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" } ] } -} +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/rare_process_by_host_windows_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/rare_process_by_host_windows_ecs.json index ec6e1f8455312..a0480a94e5356 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/rare_process_by_host_windows_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/rare_process_by_host_windows_ecs.json @@ -3,7 +3,8 @@ "description": "SIEM Winlogbeat: Detect unusually rare processes on Windows (beta)", "groups": [ "siem", - "winlogbeat","process" + "winlogbeat", + "process" ], "analysis_config": { "bucket_span": "15m", @@ -33,20 +34,20 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Host Details by user name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Hosts Overview by process name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Hosts Overview by user name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" } ] } -} +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_network_activity_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_network_activity_ecs.json index 45a4e55e4ea05..860bcee14c6b0 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_network_activity_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_network_activity_ecs.json @@ -1,10 +1,19 @@ { - "job_type": "anomaly_detector", - "description": "SIEM Winlogbeat: Looks for unusual processes using the network which could indicate command-and-control, lateral movement, persistence, or data exfiltration activity (beta)", - "groups": [ - "siem", - "winlogbeat", - "network" + "job_type": "anomaly_detector", + "description": "SIEM Winlogbeat: Looks for unusual processes using the network which could indicate command-and-control, lateral movement, persistence, or data exfiltration activity (beta)", + "groups": [ + "siem", + "winlogbeat", + "network" + ], + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "rare by \"process.name\"", + "function": "rare", + "by_field_name": "process.name" + } ], "analysis_config": { "bucket_span": "15m", @@ -30,24 +39,28 @@ "time_field": "@timestamp" }, "custom_settings": { - "created_by": "ml-module-siem-winlogbeat", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ] - } - } + "created_by": "ml-module-siem-winlogbeat", + "custom_urls": [ + { + "url_name": "Host Details by process name", + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Host Details by user name", + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Hosts Overview by process name", + "url_value": "siem#/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Hosts Overview by user name", + "url_value": "siem#/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + } + ] + } + ] +} +] +} +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_path_activity_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_path_activity_ecs.json index 74b4271032f56..7133335c44765 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_path_activity_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_path_activity_ecs.json @@ -1,9 +1,9 @@ { "job_type": "anomaly_detector", "groups": [ - "siem", - "winlogbeat", - "process" + "siem", + "winlogbeat", + "process" ], "description": "SIEM Winlogbeat: Looks for activity in unusual paths that may indicate execution of malware or persistence mechanisms. Windows payloads often execute from user profile paths (beta)", "analysis_config": { @@ -33,20 +33,20 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Host Details by user name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Hosts Overview by process name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Hosts Overview by user name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" } ] } -} +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_all_hosts_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_all_hosts_ecs.json index 94a33a9abd1b5..e036dd6aff13e 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_all_hosts_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_all_hosts_ecs.json @@ -1,10 +1,19 @@ { - "job_type": "anomaly_detector", - "description": "SIEM Winlogbeat: Looks for processes that are unusual to all Windows hosts. Such unusual processes may indicate execution of unauthorized services, malware, or persistence mechanisms (beta)", - "groups": [ - "siem", - "winlogbeat", - "process" + "job_type": "anomaly_detector", + "description": "SIEM Winlogbeat: Looks for processes that are unusual to all Windows hosts. Such unusual processes may indicate execution of unauthorized services, malware, or persistence mechanisms (beta)", + "groups": [ + "siem", + "winlogbeat", + "process" + ], + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "rare by \"process.executable\"", + "function": "rare", + "by_field_name": "process.executable" + } ], "analysis_config": { "bucket_span": "15m", @@ -33,20 +42,22 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Host Details by user name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Hosts Overview by process name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Hosts Overview by user name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" } ] } } +} +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_creation.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_creation.json index b87603db74c50..98b17c2adb42e 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_creation.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_process_creation.json @@ -33,20 +33,20 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Host Details by user name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Hosts Overview by process name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Hosts Overview by user name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" } ] } -} +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json index 5feed1145ebfe..9d98855c8e2c5 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json @@ -33,12 +33,12 @@ "custom_urls": [ { "url_name": "Host Details by user name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Hosts Overview by user name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" } ] } -} +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_user_name_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_user_name_ecs.json index 19e594b078901..4fdbbf7a27813 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_user_name_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_user_name_ecs.json @@ -33,20 +33,20 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(expression:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Host Details by user name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(expression:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Hosts Overview by process name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts?_g=()&query=(expression:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Hosts Overview by user name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts?_g=()&query=(expression:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" } ] } -} +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_rare_user_runas_event.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_rare_user_runas_event.json index 4259f29a08b56..edc2e0c58d154 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_rare_user_runas_event.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_rare_user_runas_event.json @@ -1,10 +1,19 @@ { - "job_type": "anomaly_detector", - "description": "SIEM Winlogbeat: Unusual user context switches can be due to privilege escalation (beta)", - "groups": [ - "siem", - "winlogbeat", - "authentication" + "job_type": "anomaly_detector", + "description": "SIEM Winlogbeat: Unusual user context switches can be due to privilege escalation (beta)", + "groups": [ + "siem", + "winlogbeat", + "authentication" + ], + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "rare by \"user.name\"", + "function": "rare", + "by_field_name": "user.name" + } ], "analysis_config": { "bucket_span": "15m", @@ -33,20 +42,21 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Host Details by user name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Hosts Overview by process name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Hosts Overview by user name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" } ] } } +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/windows_rare_user_type10_remote_login.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/windows_rare_user_type10_remote_login.json index 14512ea0d136e..b9992a9e01182 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/windows_rare_user_type10_remote_login.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/windows_rare_user_type10_remote_login.json @@ -33,20 +33,20 @@ "custom_urls": [ { "url_name": "Host Details by process name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Host Details by user name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Hosts Overview by process name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" }, { "url_name": "Hosts Overview by user name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + "url_value": "siem#/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" } ] } -} +} \ No newline at end of file From 611409f1602bfdd32b601ada6a6422c07ccf6324 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 15 Oct 2019 17:33:08 -0700 Subject: [PATCH 04/48] [DOCS] Fixes link to node.js documentation (#48334) --- docs/developer/core/development-elasticsearch.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer/core/development-elasticsearch.asciidoc b/docs/developer/core/development-elasticsearch.asciidoc index fb5db3fc437fe..89f85cfc19fbf 100644 --- a/docs/developer/core/development-elasticsearch.asciidoc +++ b/docs/developer/core/development-elasticsearch.asciidoc @@ -3,7 +3,7 @@ Kibana exposes two clients on the server and browser for communicating with elasticsearch. There is an 'admin' client which is used for managing Kibana's state, and a 'data' client for all -other requests. The clients use the {jsclient}/javascript-api/current/index.html[elasticsearch.js library]. +other requests. The clients use the {jsclient-current}/index.html[elasticsearch.js library]. [float] [[client-server]] From 221ba8f87b867ed1158595b916023dd8de740cc0 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Tue, 15 Oct 2019 20:35:02 -0400 Subject: [PATCH 05/48] [ML] DataFrame Analytics: Regression UI - display MSE and rSquared in expanded row (#48261) * move loadEvalData functionality to shared file for re-use * load MSE and rSquared in analytics expanded row * disable regression results link in analytics list * update text for flyout button and error message * fix translation * fix generalization spelling * only fetch eval if job is complete * fix tests --- .../common/analytics.test.ts | 1 + .../data_frame_analytics/common/analytics.ts | 77 +++++++++- .../data_frame_analytics/common/index.ts | 3 + .../exploration/exploration.test.tsx | 1 + .../regression_exploration/error_callout.tsx | 2 +- .../regression_exploration/evaluate_panel.tsx | 72 +--------- .../analytics_list/action_delete.test.tsx | 1 + .../components/analytics_list/actions.tsx | 5 +- .../components/analytics_list/common.test.ts | 1 + .../analytics_list/expanded_row.tsx | 135 +++++++++++++++++- .../create_analytics_flyout.tsx | 10 +- 11 files changed, 230 insertions(+), 78 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.test.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.test.ts index 9182487cedb51..924e1228c27ab 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.test.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.test.ts @@ -5,6 +5,7 @@ */ import { getAnalysisType, isOutlierAnalysis } from './analytics'; +jest.mock('ui/new_platform'); describe('Data Frame Analytics: Analytics utils', () => { test('getAnalysisType()', () => { diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts index f84478a327354..9f9091db02cce 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/analytics.ts @@ -8,6 +8,10 @@ import { useEffect } from 'react'; import { BehaviorSubject } from 'rxjs'; import { filter, distinctUntilChanged } from 'rxjs/operators'; import { Subscription } from 'rxjs'; +import { idx } from '@kbn/elastic-idx'; +import { ml } from '../../services/ml_api_service'; +import { getErrorMessage } from '../pages/analytics_management/hooks/use_create_analytics_form'; +import { RegressionEvaluateResponse } from '../common'; export type IndexName = string; export type IndexPattern = string; @@ -24,6 +28,12 @@ interface RegressionAnalysis { }; } +export interface Eval { + meanSquaredError: number | ''; + rSquared: number | ''; + error: null | string; +} + export interface RegressionEvaluateResponse { regression: { mean_squared_error: { @@ -39,6 +49,12 @@ interface GenericAnalysis { [key: string]: Record; } +interface LoadEvaluateResult { + success: boolean; + eval: RegressionEvaluateResponse | null; + error: string | null; +} + type AnalysisConfig = OutlierAnalysis | RegressionAnalysis | GenericAnalysis; export enum ANALYSIS_CONFIG_TYPE { @@ -58,7 +74,7 @@ export const getAnalysisType = (analysis: AnalysisConfig) => { }; export const getDependentVar = (analysis: AnalysisConfig) => { - let depVar; + let depVar = ''; if (isRegressionAnalysis(analysis)) { depVar = analysis.regression.dependent_variable; } @@ -152,3 +168,62 @@ export const useRefreshAnalyticsList = ( }, }; }; + +const DEFAULT_SIG_FIGS = 3; + +export function getValuesFromResponse(response: RegressionEvaluateResponse) { + let meanSquaredError = idx(response, _ => _.regression.mean_squared_error.error) as number; + if (meanSquaredError) { + meanSquaredError = Number(meanSquaredError.toPrecision(DEFAULT_SIG_FIGS)); + } + + let rSquared = idx(response, _ => _.regression.r_squared.value) as number; + if (rSquared) { + rSquared = Number(rSquared.toPrecision(DEFAULT_SIG_FIGS)); + } + + return { meanSquaredError, rSquared }; +} + +export const loadEvalData = async ({ + isTraining, + index, + dependentVariable, +}: { + isTraining: boolean; + index: string; + dependentVariable: string; +}) => { + const results: LoadEvaluateResult = { success: false, eval: null, error: null }; + + const config = { + index, + query: { + term: { + 'ml.is_training': { + value: isTraining, + }, + }, + }, + evaluation: { + regression: { + actual_field: dependentVariable, + predicted_field: `ml.${dependentVariable}_prediction`, + metrics: { + r_squared: {}, + mean_squared_error: {}, + }, + }, + }, + }; + + try { + const evalResult = await ml.dataFrameAnalytics.evaluateDataFrameAnalytics(config); + results.success = true; + results.eval = evalResult; + return results; + } catch (e) { + results.error = getErrorMessage(e); + return results; + } +}; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/index.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/index.ts index 16c55a739711f..05f92548304c5 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/index.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/common/index.ts @@ -17,6 +17,9 @@ export { REFRESH_ANALYTICS_LIST_STATE, ANALYSIS_CONFIG_TYPE, RegressionEvaluateResponse, + getValuesFromResponse, + loadEvalData, + Eval, } from './analytics'; export { diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.test.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.test.tsx index cd4034406e485..ce39cecb5b5e6 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.test.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.test.tsx @@ -8,6 +8,7 @@ import { shallow } from 'enzyme'; import React from 'react'; jest.mock('../../../../../contexts/ui/use_ui_chrome_context'); +jest.mock('ui/new_platform'); import { Exploration } from './exploration'; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx index f7b17079e5046..d0633d586063a 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx @@ -57,7 +57,7 @@ export const ErrorCallout: FC = ({ error }) => {

{i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutBody', { defaultMessage: - 'The query for the index returned no results. Please make sure the job has been completed and the index contains documents.', + 'The query for the index returned no results. Please make sure the job has completed and the index contains documents.', })}

diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx index a78a4747e15ef..b12258500e6f0 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx @@ -7,12 +7,8 @@ import React, { FC, Fragment, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiStat, EuiTitle } from '@elastic/eui'; -import { idx } from '@kbn/elastic-idx'; -import { ml } from '../../../../../services/ml_api_service'; -import { getErrorMessage } from '../../../analytics_management/hooks/use_create_analytics_form'; -import { RegressionEvaluateResponse } from '../../../../common'; - import { ErrorCallout } from './error_callout'; +import { getValuesFromResponse, loadEvalData, Eval } from '../../../../common'; interface Props { jobId: string; @@ -20,18 +16,6 @@ interface Props { dependentVariable: string; } -interface LoadEvaluateResult { - success: boolean; - eval: RegressionEvaluateResponse | null; - error: string | null; -} - -interface Eval { - meanSquaredError: number | ''; - rSquared: number | ''; - error: null | string; -} - const meanSquaredErrorText = i18n.translate( 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText', { @@ -45,21 +29,6 @@ const rSquaredText = i18n.translate( } ); const defaultEval: Eval = { meanSquaredError: '', rSquared: '', error: null }; -const DEFAULT_SIG_FIGS = 3; - -function getValuesFromResponse(response: RegressionEvaluateResponse) { - let meanSquaredError = idx(response, _ => _.regression.mean_squared_error.error) as number; - if (meanSquaredError) { - meanSquaredError = Number(meanSquaredError.toPrecision(DEFAULT_SIG_FIGS)); - } - - let rSquared = idx(response, _ => _.regression.r_squared.value) as number; - if (rSquared) { - rSquared = Number(rSquared.toPrecision(DEFAULT_SIG_FIGS)); - } - - return { meanSquaredError, rSquared }; -} export const EvaluatePanel: FC = ({ jobId, index, dependentVariable }) => { const [trainingEval, setTrainingEval] = useState(defaultEval); @@ -67,46 +36,11 @@ export const EvaluatePanel: FC = ({ jobId, index, dependentVariable }) => const [isLoadingTraining, setIsLoadingTraining] = useState(false); const [isLoadingGeneralization, setIsLoadingGeneralization] = useState(false); - const loadEvalData = async (isTraining: boolean) => { - const results: LoadEvaluateResult = { success: false, eval: null, error: null }; - - const config = { - index, - query: { - term: { - 'ml.is_training': { - value: isTraining, - }, - }, - }, - evaluation: { - regression: { - actual_field: dependentVariable, - predicted_field: `ml.${dependentVariable}_prediction`, - metrics: { - r_squared: {}, - mean_squared_error: {}, - }, - }, - }, - }; - - try { - const evalResult = await ml.dataFrameAnalytics.evaluateDataFrameAnalytics(config); - results.success = true; - results.eval = evalResult; - return results; - } catch (e) { - results.error = getErrorMessage(e); - return results; - } - }; - const loadData = async () => { setIsLoadingGeneralization(true); setIsLoadingTraining(true); - const genErrorEval = await loadEvalData(false); + const genErrorEval = await loadEvalData({ isTraining: false, index, dependentVariable }); if (genErrorEval.success === true && genErrorEval.eval) { const { meanSquaredError, rSquared } = getValuesFromResponse(genErrorEval.eval); @@ -125,7 +59,7 @@ export const EvaluatePanel: FC = ({ jobId, index, dependentVariable }) => }); } - const trainingErrorEval = await loadEvalData(true); + const trainingErrorEval = await loadEvalData({ isTraining: true, index, dependentVariable }); if (trainingErrorEval.success === true && trainingErrorEval.eval) { const { meanSquaredError, rSquared } = getValuesFromResponse(trainingErrorEval.eval); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx index 097735bbfd903..6fd80c524f8ef 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx @@ -19,6 +19,7 @@ jest.mock('../../../../../privilege/check_privilege', () => ({ checkPermission: jest.fn(() => false), createPermissionFailureMessage: jest.fn(), })); +jest.mock('ui/new_platform'); describe('DeleteAction', () => { afterEach(cleanup); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx index 19a86836251f2..59496f00d8fe4 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx @@ -13,7 +13,7 @@ import { createPermissionFailureMessage, } from '../../../../../privilege/check_privilege'; -import { getAnalysisType, getDependentVar } from '../../../../common/analytics'; +import { isOutlierAnalysis, getAnalysisType, getDependentVar } from '../../../../common/analytics'; import { getResultsUrl, isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common'; import { stopAnalytics } from '../../services/analytics_service'; @@ -29,9 +29,10 @@ export const AnalyticsViewAction = { const dependentVariable = getDependentVar(item.config.analysis); const url = getResultsUrl(item.id, analysisType, destIndex, dependentVariable); - + // Disable 'View' link for regression until results view is complete return ( (window.location.href = url)} size="xs" color="text" diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/common.test.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/common.test.ts index 3caf09e736513..3da57a24cfcdf 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/common.test.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/common.test.ts @@ -5,6 +5,7 @@ */ import StatsMock from './__mocks__/analytics_stats.json'; +jest.mock('ui/new_platform'); import { isCompletedAnalyticsJob, diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx index 8c3d3803393b1..ab8ef81c593a2 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; +import React, { FC, Fragment, useState, useEffect } from 'react'; import moment from 'moment-timezone'; +import { idx } from '@kbn/elastic-idx'; -import { EuiTabbedContent } from '@elastic/eui'; +import { EuiIcon, EuiLoadingSpinner, EuiTabbedContent } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -17,6 +18,8 @@ import { DataFrameAnalyticsListRow } from './common'; import { ExpandedRowDetailsPane, SectionConfig } from './expanded_row_details_pane'; import { ExpandedRowJsonPane } from './expanded_row_json_pane'; import { ProgressBar } from './progress_bar'; +import { getDependentVar, getValuesFromResponse, loadEvalData, Eval } from '../../../../common'; +import { isCompletedAnalyticsJob } from './common'; // import { ExpandedRowMessagesPane } from './expanded_row_messages_pane'; function getItemDescription(value: any) { @@ -27,11 +30,94 @@ function getItemDescription(value: any) { return value.toString(); } +interface LoadedStatProps { + isLoading: boolean; + evalData: Eval; + resultProperty: 'meanSquaredError' | 'rSquared'; +} + +const LoadedStat: FC = ({ isLoading, evalData, resultProperty }) => { + return ( + + {isLoading === false && evalData.error !== null && } + {isLoading === true && } + {isLoading === false && evalData.error === null && evalData[resultProperty]} + + ); +}; + interface Props { item: DataFrameAnalyticsListRow; } +const defaultEval: Eval = { meanSquaredError: '', rSquared: '', error: null }; + export const ExpandedRow: FC = ({ item }) => { + const [trainingEval, setTrainingEval] = useState(defaultEval); + const [generalizationEval, setGeneralizationEval] = useState(defaultEval); + const [isLoadingTraining, setIsLoadingTraining] = useState(false); + const [isLoadingGeneralization, setIsLoadingGeneralization] = useState(false); + const index = idx(item, _ => _.config.dest.index) as string; + const dependentVariable = getDependentVar(item.config.analysis); + const jobIsCompleted = isCompletedAnalyticsJob(item.stats); + + const loadData = async () => { + setIsLoadingGeneralization(true); + setIsLoadingTraining(true); + + const genErrorEval = await loadEvalData({ + isTraining: false, + index, + dependentVariable, + }); + + if (genErrorEval.success === true && genErrorEval.eval) { + const { meanSquaredError, rSquared } = getValuesFromResponse(genErrorEval.eval); + setGeneralizationEval({ + meanSquaredError, + rSquared, + error: null, + }); + setIsLoadingGeneralization(false); + } else { + setIsLoadingGeneralization(false); + setGeneralizationEval({ + meanSquaredError: '', + rSquared: '', + error: genErrorEval.error, + }); + } + + const trainingErrorEval = await loadEvalData({ + isTraining: true, + index, + dependentVariable, + }); + + if (trainingErrorEval.success === true && trainingErrorEval.eval) { + const { meanSquaredError, rSquared } = getValuesFromResponse(trainingErrorEval.eval); + setTrainingEval({ + meanSquaredError, + rSquared, + error: null, + }); + setIsLoadingTraining(false); + } else { + setIsLoadingTraining(false); + setTrainingEval({ + meanSquaredError: '', + rSquared: '', + error: genErrorEval.error, + }); + } + }; + + useEffect(() => { + if (jobIsCompleted) { + loadData(); + } + }, [jobIsCompleted]); + const stateValues = { ...item.stats }; delete stateValues.progress; @@ -76,6 +162,51 @@ export const ExpandedRow: FC = ({ item }) => { position: 'right', }; + if (jobIsCompleted) { + stats.items.push( + { + title: 'generalization mean squared error', + description: ( + + ), + }, + { + title: 'generalization r squared', + description: ( + + ), + }, + { + title: 'training mean squared error', + description: ( + + ), + }, + { + title: 'training r squared', + description: ( + + ), + } + ); + } + const tabs = [ { id: 'ml-analytics-job-details', diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx index 0d722b9cf0090..4861c05687597 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx @@ -43,9 +43,13 @@ export const CreateAnalyticsFlyout: FC = ({ {(!isJobCreated || !isJobStarted) && ( - {i18n.translate('xpack.ml.dataframe.analytics.create.flyoutCancelButton', { - defaultMessage: 'Cancel', - })} + {isJobCreated === true + ? i18n.translate('xpack.ml.dataframe.analytics.create.flyoutCloseButton', { + defaultMessage: 'Close', + }) + : i18n.translate('xpack.ml.dataframe.analytics.create.flyoutCancelButton', { + defaultMessage: 'Cancel', + })} )} From 8cbae88afd11d583a7a6bc402c519507e9102932 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Tue, 15 Oct 2019 20:57:54 -0400 Subject: [PATCH 06/48] [Maps] bump ems-client to 1.0.5 to avoid IE11 crashes (#48292) Signed-off-by: Tyler Smalley --- package.json | 2 +- x-pack/package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index b1ba573ffaf5a..85ca5e2909b24 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "@babel/register": "^7.5.5", "@elastic/charts": "^13.5.1", "@elastic/datemath": "5.0.2", - "@elastic/ems-client": "^1.0.2", + "@elastic/ems-client": "1.0.5", "@elastic/eui": "14.5.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", diff --git a/x-pack/package.json b/x-pack/package.json index b62a9025984ca..d2317abb86b02 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -193,7 +193,7 @@ "@babel/runtime": "^7.5.5", "@elastic/ctags-langserver": "^0.1.11", "@elastic/datemath": "5.0.2", - "@elastic/ems-client": "1.0.2", + "@elastic/ems-client": "1.0.5", "@elastic/filesaver": "1.1.2", "@elastic/eui": "14.5.0", "@elastic/javascript-typescript-langserver": "^0.3.3", diff --git a/yarn.lock b/yarn.lock index d9ec9a667aaed..0b0e76698d3e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1118,10 +1118,10 @@ once "^1.4.0" pump "^3.0.0" -"@elastic/ems-client@1.0.2", "@elastic/ems-client@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@elastic/ems-client/-/ems-client-1.0.2.tgz#b4dcfcb1ce40a4f1d847e39f1d5d994ae7a9f04e" - integrity sha512-KYOQ44EEdoel+l1LntUjPvUDS4jfGF24ulojQD1GjStzA0sP8D7iIUY0H6o5Q06cy9lFkXarLKADjAxAqLATSw== +"@elastic/ems-client@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@elastic/ems-client/-/ems-client-1.0.5.tgz#e2c10816c5eebdf128590170d8f09fcc693db50b" + integrity sha512-J+jDjtpHfGnbsgdhuA1zp/JoZftCpx6/h/4bP5ik+2Ysa30sAHIpHs0D3921R75O9WDNs6SHVnV/oqdJKL/HCg== dependencies: lodash "^4.17.15" node-fetch "^1.7.3" From c5a1836bba4137a9cd0515e20a2d96bc9b18bf0f Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 15 Oct 2019 21:07:33 -0400 Subject: [PATCH 07/48] [Lens] Delete filter ratio (#48275) * [Lens] Remove filter ratio * Delete filter ratio code --- .../dimension_panel/dimension_panel.test.tsx | 2 +- .../indexpattern_plugin/filter_ratio.test.ts | 127 ------- .../indexpattern_plugin/filter_ratio.ts | 86 ----- .../definitions/filter_ratio.test.tsx | 309 ------------------ .../operations/definitions/filter_ratio.tsx | 185 ----------- .../operations/definitions/index.ts | 3 - .../operations/definitions/terms.test.tsx | 43 --- .../operations/definitions/terms.tsx | 2 +- .../operations/operations.test.ts | 4 - .../public/indexpattern_plugin/plugin.tsx | 2 - .../indexpattern_plugin/to_expression.ts | 26 +- 11 files changed, 3 insertions(+), 786 deletions(-) delete mode 100644 x-pack/legacy/plugins/lens/public/indexpattern_plugin/filter_ratio.test.ts delete mode 100644 x-pack/legacy/plugins/lens/public/indexpattern_plugin/filter_ratio.ts delete mode 100644 x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.test.tsx delete mode 100644 x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.tsx diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx index fce72b61a0d70..fa35888059520 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx @@ -887,7 +887,7 @@ describe('IndexPatternDimensionPanel', () => { .find(EuiSideNav) .prop('items')[0] .items.map(({ name }) => name) - ).toEqual(['Unique count', 'Average', 'Count', 'Filter ratio', 'Maximum', 'Minimum', 'Sum']); + ).toEqual(['Unique count', 'Average', 'Count', 'Maximum', 'Minimum', 'Sum']); }); it('should add a column on selection of a field', () => { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/filter_ratio.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/filter_ratio.test.ts deleted file mode 100644 index d03370d0b8137..0000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/filter_ratio.test.ts +++ /dev/null @@ -1,127 +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 { calculateFilterRatio } from './filter_ratio'; -import { KibanaDatatable } from 'src/legacy/core_plugins/interpreter/common'; - -describe('calculate_filter_ratio', () => { - it('should collapse two rows and columns into a single row and column', () => { - const input: KibanaDatatable = { - type: 'kibana_datatable', - columns: [{ id: 'bucket', name: 'A' }, { id: 'filter-ratio', name: 'B' }], - rows: [{ bucket: 'a', 'filter-ratio': 5 }, { bucket: 'b', 'filter-ratio': 10 }], - }; - - expect(calculateFilterRatio.fn(input, { id: 'bucket' }, {})).toEqual({ - columns: [ - { - id: 'bucket', - name: 'A', - formatHint: { - id: 'percent', - }, - }, - ], - rows: [{ bucket: 0.5 }], - type: 'kibana_datatable', - }); - }); - - it('should return 0 when the denominator is undefined', () => { - const input: KibanaDatatable = { - type: 'kibana_datatable', - columns: [{ id: 'bucket', name: 'A' }, { id: 'filter-ratio', name: 'B' }], - rows: [{ bucket: 'a', 'filter-ratio': 5 }, { bucket: 'b' }], - }; - - expect(calculateFilterRatio.fn(input, { id: 'bucket' }, {})).toEqual({ - columns: [ - { - id: 'bucket', - name: 'A', - formatHint: { - id: 'percent', - }, - }, - ], - rows: [{ bucket: 0 }], - type: 'kibana_datatable', - }); - }); - - it('should return 0 when the denominator is 0', () => { - const input: KibanaDatatable = { - type: 'kibana_datatable', - columns: [{ id: 'bucket', name: 'A' }, { id: 'filter-ratio', name: 'B' }], - rows: [{ bucket: 'a', 'filter-ratio': 5 }, { bucket: 'b', 'filter-ratio': 0 }], - }; - - expect(calculateFilterRatio.fn(input, { id: 'bucket' }, {})).toEqual({ - columns: [ - { - id: 'bucket', - name: 'A', - formatHint: { - id: 'percent', - }, - }, - ], - rows: [{ bucket: 0 }], - type: 'kibana_datatable', - }); - }); - - it('should keep columns which are not mapped', () => { - const input: KibanaDatatable = { - type: 'kibana_datatable', - columns: [ - { id: 'bucket', name: 'A' }, - { id: 'filter-ratio', name: 'B' }, - { id: 'extra', name: 'C' }, - ], - rows: [ - { bucket: 'a', 'filter-ratio': 5, extra: 'first' }, - { bucket: 'b', 'filter-ratio': 10, extra: 'second' }, - ], - }; - - expect(calculateFilterRatio.fn(input, { id: 'bucket' }, {})).toEqual({ - columns: [ - { - id: 'bucket', - name: 'A', - formatHint: { - id: 'percent', - }, - }, - { id: 'extra', name: 'C' }, - ], - rows: [ - { - bucket: 0.5, - extra: 'first', - }, - ], - type: 'kibana_datatable', - }); - }); - - it('should attach a percentage format hint to the ratio column', () => { - const input: KibanaDatatable = { - type: 'kibana_datatable', - columns: [{ id: 'bucket', name: 'A' }, { id: 'filter-ratio', name: 'B' }], - rows: [{ bucket: 'a', 'filter-ratio': 5 }, { bucket: 'b', 'filter-ratio': 10 }], - }; - - expect(calculateFilterRatio.fn(input, { id: 'bucket' }, {}).columns[0]).toEqual({ - id: 'bucket', - name: 'A', - formatHint: { - id: 'percent', - }, - }); - }); -}); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/filter_ratio.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/filter_ratio.ts deleted file mode 100644 index 79dbe1dbe6fb3..0000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/filter_ratio.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { ExpressionFunction, KibanaDatatable } from 'src/legacy/core_plugins/interpreter/public'; - -interface FilterRatioKey { - id: string; -} - -export const calculateFilterRatio: ExpressionFunction< - 'lens_calculate_filter_ratio', - KibanaDatatable, - FilterRatioKey, - KibanaDatatable -> = { - name: 'lens_calculate_filter_ratio', - type: 'kibana_datatable', - help: i18n.translate('xpack.lens.functions.calculateFilterRatio.help', { - defaultMessage: 'A helper to collapse two filter ratio rows into a single row', - }), - args: { - id: { - types: ['string'], - help: i18n.translate('xpack.lens.functions.calculateFilterRatio.id.help', { - defaultMessage: 'The column ID which has the filter ratio', - }), - }, - }, - context: { - types: ['kibana_datatable'], - }, - fn(data: KibanaDatatable, { id }: FilterRatioKey) { - const newRows: KibanaDatatable['rows'] = []; - - if (data.rows.length === 0) { - return data; - } - - if (data.rows.length % 2 === 1) { - throw new Error('Cannot divide an odd number of rows'); - } - - const [[valueKey]] = Object.entries(data.rows[0]).filter(([key]) => - key.includes('filter-ratio') - ); - - for (let i = 0; i < data.rows.length; i += 2) { - const row1 = data.rows[i]; - const row2 = data.rows[i + 1]; - - const calculatedRatio = row2[valueKey] - ? (row1[valueKey] as number) / (row2[valueKey] as number) - : 0; - - const result = { ...row1 }; - delete result[valueKey]; - - result[id] = calculatedRatio; - - newRows.push(result); - } - - const newColumns = data.columns - .filter(col => !col.id.includes('filter-ratio')) - .map(col => - col.id === id - ? { - ...col, - formatHint: { - id: 'percent', - }, - } - : col - ); - - return { - type: 'kibana_datatable', - rows: newRows, - columns: newColumns, - }; - }, -}; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.test.tsx deleted file mode 100644 index 5944742221792..0000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.test.tsx +++ /dev/null @@ -1,309 +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, { ReactElement } from 'react'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { act } from 'react-dom/test-utils'; -import { FilterRatioIndexPatternColumn } from './filter_ratio'; -import { filterRatioOperation } from '.'; -import { Storage } from 'ui/storage'; -import { - UiSettingsClientContract, - SavedObjectsClientContract, - HttpServiceBase, -} from 'src/core/public'; -import { QueryBarInput } from '../../../../../../../../src/legacy/core_plugins/data/public/query'; -import { createMockedIndexPattern } from '../../mocks'; -import { EuiFormRow } from '@elastic/eui'; -import { IndexPatternPrivateState } from '../../types'; - -jest.mock('ui/new_platform'); - -describe('filter_ratio', () => { - let state: IndexPatternPrivateState; - let storageMock: Storage; - const InlineOptions = filterRatioOperation.paramEditor!; - - beforeEach(() => { - state = { - indexPatternRefs: [], - existingFields: {}, - indexPatterns: { - 1: { - id: '1', - title: 'Mock Indexpattern', - fields: [], - }, - }, - currentIndexPatternId: '1', - showEmptyFields: false, - layers: { - first: { - indexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - label: 'Filter ratio', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'filter_ratio', - params: { - numerator: { query: '', language: 'kuery' }, - denominator: { query: '', language: 'kuery' }, - }, - }, - }, - }, - }, - }; - - storageMock = {} as Storage; - }); - - describe('buildColumn', () => { - it('should create column object with default params', () => { - const column = filterRatioOperation.buildColumn({ - layerId: 'first', - columns: {}, - suggestedPriority: undefined, - indexPattern: createMockedIndexPattern(), - }); - expect(column.params.numerator).toEqual({ query: '', language: 'kuery' }); - expect(column.params.denominator).toEqual({ query: '', language: 'kuery' }); - }); - }); - - describe('toEsAggsConfig', () => { - it('should reflect params correctly', () => { - const esAggsConfig = filterRatioOperation.toEsAggsConfig( - state.layers.first.columns.col1 as FilterRatioIndexPatternColumn, - 'col1' - ); - expect(esAggsConfig).toEqual( - expect.objectContaining({ - params: expect.objectContaining({ - filters: [ - { - input: { query: '', language: 'kuery' }, - label: '', - }, - { - input: { query: '', language: 'kuery' }, - label: '', - }, - ], - }), - }) - ); - }); - }); - - describe('param editor', () => { - it('should render current value', () => { - expect(() => { - shallowWithIntl( - - ); - }).not.toThrow(); - }); - - it('should show only the numerator by default', () => { - const wrapper = shallowWithIntl( - - ); - - expect(wrapper.find(QueryBarInput)).toHaveLength(1); - expect(wrapper.find(QueryBarInput).prop('indexPatterns')).toEqual(['Mock Indexpattern']); - }); - - it('should update the state when typing into the query bar', () => { - const setState = jest.fn(); - const wrapper = shallowWithIntl( - - ); - - wrapper.find(QueryBarInput).prop('onChange')!({ - query: 'geo.src : "US"', - language: 'kuery', - }); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - ...state.layers, - first: { - ...state.layers.first, - columns: { - col1: { - ...state.layers.first.columns.col1, - params: { - numerator: { query: 'geo.src : "US"', language: 'kuery' }, - denominator: { query: '', language: 'kuery' }, - }, - }, - }, - }, - }, - }); - }); - - it('should allow editing the denominator', () => { - const setState = jest.fn(); - const wrapper = shallowWithIntl( - - ); - - const formRow = wrapper - .find('[data-test-subj="lns-indexPatternFilterRatio-dividedByRow"]') - .find(EuiFormRow); - const labelAppend = shallowWithIntl(formRow.prop('labelAppend') as ReactElement); - - act(() => { - labelAppend - .find('[data-test-subj="lns-indexPatternFilterRatio-showDenominatorButton"]') - .first() - .simulate('click'); - }); - - expect(wrapper.find(QueryBarInput)).toHaveLength(2); - - wrapper - .find(QueryBarInput) - .at(1) - .prop('onChange')!({ - query: 'geo.src : "US"', - language: 'kuery', - }); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - ...state.layers, - first: { - ...state.layers.first, - columns: { - col1: { - ...state.layers.first.columns.col1, - params: { - numerator: { query: '', language: 'kuery' }, - denominator: { query: 'geo.src : "US"', language: 'kuery' }, - }, - }, - }, - }, - }, - }); - }); - }); - - it('should allow removing a set denominator', () => { - const setState = jest.fn(); - const currentColumn: FilterRatioIndexPatternColumn = { - ...(state.layers.first.columns.col1 as FilterRatioIndexPatternColumn), - params: { - numerator: { query: 'a: 123', language: 'kuery' }, - denominator: { query: 'b: 123', language: 'kuery' }, - }, - }; - - const wrapper = shallowWithIntl( - - ); - - const formRow = wrapper - .find('[data-test-subj="lns-indexPatternFilterRatio-dividedByRow"]') - .find(EuiFormRow); - const labelAppend = shallowWithIntl(formRow.prop('labelAppend') as ReactElement); - - act(() => { - labelAppend - .find('[data-test-subj="lns-indexPatternFilterRatio-hideDenominatorButton"]') - .first() - .simulate('click'); - }); - - expect(setState).toHaveBeenCalledWith({ - ...state, - layers: { - ...state.layers, - first: { - ...state.layers.first, - columns: { - col1: { - ...state.layers.first.columns.col1, - params: { - numerator: { query: 'a: 123', language: 'kuery' }, - denominator: { query: '', language: 'kuery' }, - }, - }, - }, - }, - }, - }); - }); -}); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.tsx deleted file mode 100644 index a6689210be858..0000000000000 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/filter_ratio.tsx +++ /dev/null @@ -1,185 +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, { useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiFormRow, EuiFieldText, EuiLink, EuiText } from '@elastic/eui'; -import { isEqual } from 'lodash'; -import { - Query, - QueryBarInput, -} from '../../../../../../../../src/legacy/core_plugins/data/public/query'; -import { updateColumnParam } from '../../state_helpers'; -import { OperationDefinition } from '.'; -import { BaseIndexPatternColumn } from './column_types'; - -const filterRatioLabel = i18n.translate('xpack.lens.indexPattern.filterRatio', { - defaultMessage: 'Filter ratio', -}); - -export interface FilterRatioIndexPatternColumn extends BaseIndexPatternColumn { - operationType: 'filter_ratio'; - params: { - numerator: Query; - denominator: Query; - }; -} - -const initialQuery = { language: 'kuery', query: '' }; - -export const filterRatioOperation: OperationDefinition = { - type: 'filter_ratio', - priority: 1, - displayName: i18n.translate('xpack.lens.indexPattern.filterRatio', { - defaultMessage: 'Filter ratio', - }), - getPossibleOperationForDocument: () => { - return { - dataType: 'number', - isBucketed: false, - scale: 'ratio', - }; - }, - buildColumn({ suggestedPriority }) { - return { - label: filterRatioLabel, - dataType: 'number', - operationType: 'filter_ratio', - suggestedPriority, - isBucketed: false, - scale: 'ratio', - params: { - numerator: initialQuery, - denominator: initialQuery, - }, - }; - }, - toEsAggsConfig: (column, columnId) => ({ - id: columnId, - enabled: true, - type: 'filters', - schema: 'segment', - params: { - filters: [ - { - input: column.params.numerator, - label: '', - }, - { - input: column.params.denominator, - label: '', - }, - ], - }, - }), - isTransferable: (column, newIndexPattern) => { - // TODO parse the KQL tree and check whether this would work out - return false; - }, - paramEditor: ({ state, setState, currentColumn, layerId }) => { - const [hasDenominator, setDenominator] = useState( - !isEqual(currentColumn.params.denominator, initialQuery) - ); - - return ( -
- - { - setState( - updateColumnParam({ - state, - layerId, - currentColumn, - paramName: 'numerator', - value: newQuery, - }) - ); - }} - /> - - - - {hasDenominator ? ( - { - setDenominator(false); - setState( - updateColumnParam({ - state, - layerId, - currentColumn, - paramName: 'denominator', - value: initialQuery, - }) - ); - }} - > - - - ) : ( - setDenominator(true)} - > - - - )} - - } - > - {hasDenominator ? ( - { - setState( - updateColumnParam({ - state, - layerId, - currentColumn, - paramName: 'denominator', - value: newQuery, - }) - ); - }} - /> - ) : ( - - )} - -
- ); - }, -}; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/index.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/index.ts index 73149a8d51ad4..fdb5419588e7e 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/index.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/index.ts @@ -15,7 +15,6 @@ import { cardinalityOperation } from './cardinality'; import { minOperation, averageOperation, sumOperation, maxOperation } from './metrics'; import { dateHistogramOperation } from './date_histogram'; import { countOperation } from './count'; -import { filterRatioOperation } from './filter_ratio'; import { DimensionPriority, StateSetter, OperationMetadata } from '../../../types'; import { BaseIndexPatternColumn, FieldBasedIndexPatternColumn } from './column_types'; import { IndexPatternPrivateState, IndexPattern, IndexPatternField } from '../../types'; @@ -32,14 +31,12 @@ const internalOperationDefinitions = [ cardinalityOperation, sumOperation, countOperation, - filterRatioOperation, ]; export { termsOperation } from './terms'; export { dateHistogramOperation } from './date_histogram'; export { minOperation, averageOperation, sumOperation, maxOperation } from './metrics'; export { countOperation } from './count'; -export { filterRatioOperation } from './filter_ratio'; /** * Properties passed to the operation-specific part of the popover editor diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx index fdbaf60586093..df5bfac6d03a4 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.test.tsx @@ -346,49 +346,6 @@ describe('terms', () => { ]); }); - it('should not show filter ratio column as sort target', () => { - const setStateSpy = jest.fn(); - const instance = shallow( - - ); - - const select = instance.find('[data-test-subj="indexPattern-terms-orderBy"]').find(EuiSelect); - - expect(select.prop('options').map(({ value }) => value)).toEqual(['alphabetical']); - }); - it('should update state with the order by value', () => { const setStateSpy = jest.fn(); const instance = shallow( diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.tsx index b26608086bd69..cb8893dd2f6dd 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/terms.tsx @@ -33,7 +33,7 @@ function ofName(name: string) { } function isSortableByColumn(column: IndexPatternColumn) { - return !column.isBucketed && column.operationType !== 'filter_ratio'; + return !column.isBucketed; } const DEFAULT_SIZE = 3; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts index b7a71fcba6d0c..82a93ee2a05e1 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.test.ts @@ -300,10 +300,6 @@ describe('getOperationTypesForField', () => { "operationType": "count", "type": "document", }, - Object { - "operationType": "filter_ratio", - "type": "document", - }, ], }, ] diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx index fb3f5230e928e..fee308c27859e 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx @@ -14,7 +14,6 @@ import { ExpressionFunction } from '../../../../../../src/legacy/core_plugins/in import { functionsRegistry } from '../../../../../../src/legacy/core_plugins/interpreter/public/registries'; import { getIndexPatternDatasource } from './indexpattern'; import { renameColumns } from './rename_columns'; -import { calculateFilterRatio } from './filter_ratio'; // TODO these are intermediary types because interpreter is not typed yet // They can get replaced by references to the real interfaces as soon as they @@ -37,7 +36,6 @@ class IndexPatternDatasourcePlugin { setup(core: CoreSetup, { interpreter }: IndexPatternDatasourceSetupPlugins) { interpreter.functionsRegistry.register(() => renameColumns); - interpreter.functionsRegistry.register(() => calculateFilterRatio); } stop() {} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts index 5cc640ec01553..8045ab89b63d4 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts @@ -6,7 +6,7 @@ import _ from 'lodash'; import { IndexPatternColumn } from './indexpattern'; -import { buildColumn, operationDefinitionMap } from './operations'; +import { operationDefinitionMap } from './operations'; import { IndexPattern, IndexPatternPrivateState } from './types'; function getExpressionForLayer( @@ -40,30 +40,6 @@ function getExpressionForLayer( {} as Record ); - const filterRatios = columnEntries.filter( - ([colId, col]) => col.operationType === 'filter_ratio' - ); - - if (filterRatios.length) { - const countColumn = buildColumn({ - op: 'count', - columns, - suggestedPriority: 2, - layerId, - indexPattern, - }); - aggs.push(getEsAggsConfig(countColumn, 'filter-ratio')); - - return `esaggs - index="${indexPattern.id}" - metricsAtAllLevels=false - partialRows=false - includeFormatHints=true - aggConfigs='${JSON.stringify(aggs)}' | lens_rename_columns idMap='${JSON.stringify( - idMap - )}' | ${filterRatios.map(([id]) => `lens_calculate_filter_ratio id=${id}`).join(' | ')}`; - } - return `esaggs index="${indexPattern.id}" metricsAtAllLevels=false From f4cd2883de9a7d86388eb789d1e3f3deb867fa45 Mon Sep 17 00:00:00 2001 From: Poff Poffenberger Date: Tue, 15 Oct 2019 20:08:12 -0500 Subject: [PATCH 08/48] [Canvas][i18n] Tags and Templates (#48325) * Adding template strings * i18n for tags * updating apply strings * adding tests * Test fix * copy paste fix --- .../templates/{index.js => index.ts} | 15 +++-- .../canvas_plugin_src/uis/tags/chart.ts | 6 +- .../canvas_plugin_src/uis/tags/filter.ts | 6 +- .../canvas_plugin_src/uis/tags/graphic.ts | 6 +- .../uis/tags/presentation.ts | 6 +- .../canvas_plugin_src/uis/tags/proportion.ts | 7 +- .../canvas_plugin_src/uis/tags/report.ts | 6 +- .../canvas/canvas_plugin_src/uis/tags/text.ts | 6 +- .../canvas/i18n/elements/apply_strings.ts | 15 ++++- .../i18n/elements/element_strings.test.ts | 13 ++++ x-pack/legacy/plugins/canvas/i18n/index.ts | 1 + x-pack/legacy/plugins/canvas/i18n/tags.ts | 38 +++++++++++ .../canvas/i18n/templates/apply_strings.ts | 50 +++++++++++++++ .../plugins/canvas/i18n/templates/index.ts | 8 +++ .../i18n/templates/template_strings.test.ts | 46 +++++++++++++ .../canvas/i18n/templates/template_strings.ts | 64 +++++++++++++++++++ x-pack/legacy/plugins/canvas/types/canvas.ts | 5 ++ 17 files changed, 283 insertions(+), 15 deletions(-) rename x-pack/legacy/plugins/canvas/canvas_plugin_src/templates/{index.js => index.ts} (51%) create mode 100644 x-pack/legacy/plugins/canvas/i18n/tags.ts create mode 100644 x-pack/legacy/plugins/canvas/i18n/templates/apply_strings.ts create mode 100644 x-pack/legacy/plugins/canvas/i18n/templates/index.ts create mode 100644 x-pack/legacy/plugins/canvas/i18n/templates/template_strings.test.ts create mode 100644 x-pack/legacy/plugins/canvas/i18n/templates/template_strings.ts diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/templates/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/templates/index.ts similarity index 51% rename from x-pack/legacy/plugins/canvas/canvas_plugin_src/templates/index.js rename to x-pack/legacy/plugins/canvas/canvas_plugin_src/templates/index.ts index d67c47ed1b518..eba0b47f7dc13 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/templates/index.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/templates/index.ts @@ -3,18 +3,19 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { applyTemplateStrings } from '../../i18n/templates'; -const darkTemplate = require('./theme_dark.json'); -const lightTemplate = require('./theme_light.json'); -const pitchTemplate = require('./pitch_presentation.json'); -const statusTemplate = require('./status_report.json'); -const summaryTemplate = require('./summary_report.json'); +import darkTemplate from './theme_dark.json'; +import lightTemplate from './theme_light.json'; +import pitchTemplate from './pitch_presentation.json'; +import statusTemplate from './status_report.json'; +import summaryTemplate from './summary_report.json'; // Registry expects a function that returns a spec object -export const templateSpecs = [ +export const templateSpecs = applyTemplateStrings([ darkTemplate, lightTemplate, pitchTemplate, statusTemplate, summaryTemplate, -].map(template => () => template); +]); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/chart.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/chart.ts index 418d1d11600a4..aca30780d77cd 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/chart.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/chart.ts @@ -5,5 +5,9 @@ */ import { TagFactory } from '../../../public/lib/tag'; +import { TagStrings as strings } from '../../../i18n'; -export const chart: TagFactory = () => ({ name: 'chart', color: '#FEB6DB' }); +export const chart: TagFactory = () => ({ + name: strings.chart(), + color: '#FEB6DB', +}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/filter.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/filter.ts index b65a3ddb5595c..d3d251026e9b0 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/filter.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/filter.ts @@ -5,5 +5,9 @@ */ import { TagFactory } from '../../../public/lib/tag'; +import { TagStrings as strings } from '../../../i18n'; -export const filter: TagFactory = () => ({ name: 'filter', color: '#3185FC' }); +export const filter: TagFactory = () => ({ + name: strings.filter(), + color: '#3185FC', +}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/graphic.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/graphic.ts index fc7e18f45ace8..325a531b219ee 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/graphic.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/graphic.ts @@ -5,5 +5,9 @@ */ import { TagFactory } from '../../../public/lib/tag'; +import { TagStrings as strings } from '../../../i18n'; -export const graphic: TagFactory = () => ({ name: 'graphic', color: '#E6C220' }); +export const graphic: TagFactory = () => ({ + name: strings.graphic(), + color: '#E6C220', +}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/presentation.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/presentation.ts index 6417a293838d7..f0dc469eefedb 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/presentation.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/presentation.ts @@ -5,5 +5,9 @@ */ import { TagFactory } from '../../../public/lib/tag'; +import { TagStrings as strings } from '../../../i18n'; -export const presentation: TagFactory = () => ({ name: 'presentation', color: '#017D73' }); +export const presentation: TagFactory = () => ({ + name: strings.presentation(), + color: '#017D73', +}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.ts index 7b9926d91bc53..e538b4bf53103 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.ts @@ -6,4 +6,9 @@ import { TagFactory } from '../../../public/lib/tag'; -export const proportion: TagFactory = () => ({ name: 'proportion', color: '#490092' }); +import { TagStrings as strings } from '../../../i18n'; + +export const proportion: TagFactory = () => ({ + name: strings.proportion(), + color: '#490092', +}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/report.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/report.ts index d79504e81bf42..5df30581cd070 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/report.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/report.ts @@ -5,5 +5,9 @@ */ import { TagFactory } from '../../../public/lib/tag'; +import { TagStrings as strings } from '../../../i18n'; -export const report: TagFactory = () => ({ name: 'report', color: '#DB1374' }); +export const report: TagFactory = () => ({ + name: strings.report(), + color: '#DB1374', +}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/text.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/text.ts index 8bedc2fb49344..f9faf2ad2e3ca 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/text.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/text.ts @@ -5,5 +5,9 @@ */ import { TagFactory } from '../../../public/lib/tag'; +import { TagStrings as strings } from '../../../i18n'; -export const text: TagFactory = () => ({ name: 'text', color: '#D3DAE6' }); +export const text: TagFactory = () => ({ + name: strings.text(), + color: '#D3DAE6', +}); diff --git a/x-pack/legacy/plugins/canvas/i18n/elements/apply_strings.ts b/x-pack/legacy/plugins/canvas/i18n/elements/apply_strings.ts index cfc4e750bc800..41f88a3c75f90 100644 --- a/x-pack/legacy/plugins/canvas/i18n/elements/apply_strings.ts +++ b/x-pack/legacy/plugins/canvas/i18n/elements/apply_strings.ts @@ -5,7 +5,9 @@ */ import { ElementFactory } from '../../types'; -import { getElementStrings } from './element_strings'; +import { getElementStrings } from './index'; + +import { TagStrings } from '../../i18n'; /** * This function takes a set of Canvas Element specification factories, runs them, @@ -32,6 +34,17 @@ export const applyElementStrings = (elements: ElementFactory[]) => { if (displayName) { result.displayName = displayName; } + + // Set translated tags + if (result.tags) { + result.tags = result.tags.map(tag => { + if (tag in TagStrings) { + return TagStrings[tag](); + } + + return tag; + }); + } } return () => result; diff --git a/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.test.ts b/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.test.ts index 14196702824db..a946fa87a58b3 100644 --- a/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.test.ts +++ b/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.test.ts @@ -7,6 +7,8 @@ import { getElementStrings } from './element_strings'; import { elementSpecs } from '../../canvas_plugin_src/elements'; +import { TagStrings } from '../tags'; + describe('ElementStrings', () => { const elementStrings = getElementStrings(); const elementNames = elementSpecs.map(spec => spec().name); @@ -33,4 +35,15 @@ describe('ElementStrings', () => { expect(value).toHaveProperty('help'); }); }); + + test('All elements should have tags that are defined', () => { + const tagNames = Object.keys(TagStrings); + + elementSpecs.forEach(spec => { + const element = spec(); + if (element.tags) { + element.tags.forEach((tagName: string) => expect(tagNames).toContain(tagName)); + } + }); + }); }); diff --git a/x-pack/legacy/plugins/canvas/i18n/index.ts b/x-pack/legacy/plugins/canvas/i18n/index.ts index 9e5ed624ccfd3..3be5eb89415b0 100644 --- a/x-pack/legacy/plugins/canvas/i18n/index.ts +++ b/x-pack/legacy/plugins/canvas/i18n/index.ts @@ -15,6 +15,7 @@ export * from './elements'; export * from './functions'; export * from './renderers'; export * from './shortcuts'; +export * from './tags'; export * from './transitions'; export * from './ui'; export * from './units'; diff --git a/x-pack/legacy/plugins/canvas/i18n/tags.ts b/x-pack/legacy/plugins/canvas/i18n/tags.ts new file mode 100644 index 0000000000000..41007c58d738d --- /dev/null +++ b/x-pack/legacy/plugins/canvas/i18n/tags.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const TagStrings: { [key: string]: () => string } = { + chart: () => + i18n.translate('xpack.canvas.tags.chartTag', { + defaultMessage: 'chart', + }), + filter: () => + i18n.translate('xpack.canvas.tags.filterTag', { + defaultMessage: 'filter', + }), + graphic: () => + i18n.translate('xpack.canvas.tags.graphicTag', { + defaultMessage: 'graphic', + }), + presentation: () => + i18n.translate('xpack.canvas.tags.presentationTag', { + defaultMessage: 'presentation', + }), + proportion: () => + i18n.translate('xpack.canvas.tags.proportionTag', { + defaultMessage: 'proportion', + }), + report: () => + i18n.translate('xpack.canvas.tags.reportTag', { + defaultMessage: 'report', + }), + text: () => + i18n.translate('xpack.canvas.tags.textTag', { + defaultMessage: 'text', + }), +}; diff --git a/x-pack/legacy/plugins/canvas/i18n/templates/apply_strings.ts b/x-pack/legacy/plugins/canvas/i18n/templates/apply_strings.ts new file mode 100644 index 0000000000000..4ddd3a61543c2 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/i18n/templates/apply_strings.ts @@ -0,0 +1,50 @@ +/* + * 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 { CanvasTemplate } from '../../types'; +import { getTemplateStrings } from './template_strings'; + +import { TagStrings } from '../../i18n'; + +/** + * This function takes a set of Canvas templates + * replaces tag strings with the translated versions. We do this + * so the specifications themselves have no dependency on i18n, for clarity for both + * our and external plugin developers. + */ +export const applyTemplateStrings = (templates: CanvasTemplate[]) => { + const templateStrings = getTemplateStrings(); + + return templates.map(template => { + const { name: templateName } = template; + const strings = templateStrings[templateName]; + + // If we have registered strings for this spec, we should replace any that are available. + if (strings) { + const { name, help } = strings; + // If the function has a registered help string, replace it on the spec. + if (help) { + template.help = help; + } + + if (name) { + template.name = name; + } + } + + if (template.tags) { + template.tags = template.tags.map(tag => { + if (TagStrings[tag]) { + return TagStrings[tag](); + } + + return tag; + }); + } + + return () => template; + }); +}; diff --git a/x-pack/legacy/plugins/canvas/i18n/templates/index.ts b/x-pack/legacy/plugins/canvas/i18n/templates/index.ts new file mode 100644 index 0000000000000..3589a0d67ccc7 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/i18n/templates/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './apply_strings'; +export * from './template_strings'; diff --git a/x-pack/legacy/plugins/canvas/i18n/templates/template_strings.test.ts b/x-pack/legacy/plugins/canvas/i18n/templates/template_strings.test.ts new file mode 100644 index 0000000000000..eeeb06732d397 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/i18n/templates/template_strings.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getTemplateStrings } from './template_strings'; +import { templateSpecs } from '../../canvas_plugin_src/templates'; + +import { TagStrings } from '../tags'; + +describe('TemplateStrings', () => { + const templateStrings = getTemplateStrings(); + const templateNames = templateSpecs.map(template => template().name); + const stringKeys = Object.keys(templateStrings); + + test('All template names should exist in the strings definition', () => { + templateNames.forEach((name: string) => expect(stringKeys).toContain(name)); + }); + + test('All string definitions should correspond to an existing template', () => { + stringKeys.forEach(key => expect(templateNames).toContain(key)); + }); + + const strings = Object.values(templateStrings); + + test('All templates should have a name string defined', () => { + strings.forEach(value => { + expect(value).toHaveProperty('name'); + }); + }); + + test('All templates should have a help string defined', () => { + strings.forEach(value => { + expect(value).toHaveProperty('help'); + }); + }); + + test('All templates should have tags that are defined', () => { + const tagNames = Object.keys(TagStrings); + + templateSpecs.forEach(template => { + template().tags.forEach((tagName: string) => expect(tagNames).toContain(tagName)); + }); + }); +}); diff --git a/x-pack/legacy/plugins/canvas/i18n/templates/template_strings.ts b/x-pack/legacy/plugins/canvas/i18n/templates/template_strings.ts new file mode 100644 index 0000000000000..261f67067cfaf --- /dev/null +++ b/x-pack/legacy/plugins/canvas/i18n/templates/template_strings.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +interface TemplateStrings { + name: string; + help: string; +} + +interface TemplateStringDict { + [templateName: string]: TemplateStrings; +} + +/** + * This function will return a dictionary of strings, organized by Canvas + * Element specification. This function requires that `i18nProvider` be + * properly initialized. + */ +export const getTemplateStrings = (): TemplateStringDict => ({ + Dark: { + name: i18n.translate('xpack.canvas.templates.darkName', { + defaultMessage: 'Dark', + }), + help: i18n.translate('xpack.canvas.templates.darkHelp', { + defaultMessage: 'Dark color themed presentation deck', + }), + }, + Light: { + name: i18n.translate('xpack.canvas.templates.lightName', { + defaultMessage: 'Light', + }), + help: i18n.translate('xpack.canvas.templates.lightHelp', { + defaultMessage: 'Light color themed presentation deck', + }), + }, + Pitch: { + name: i18n.translate('xpack.canvas.templates.pitchName', { + defaultMessage: 'Pitch', + }), + help: i18n.translate('xpack.canvas.templates.pitchHelp', { + defaultMessage: 'Branded presentation with large photos"', + }), + }, + Status: { + name: i18n.translate('xpack.canvas.templates.statusName', { + defaultMessage: 'Status', + }), + help: i18n.translate('xpack.canvas.templates.statusHelp', { + defaultMessage: 'Document-style report with live charts', + }), + }, + Summary: { + name: i18n.translate('xpack.canvas.templates.summaryDisplayName', { + defaultMessage: 'Summary', + }), + help: i18n.translate('xpack.canvas.templates.summaryHelp', { + defaultMessage: 'Infographic-style report with live charts', + }), + }, +}); diff --git a/x-pack/legacy/plugins/canvas/types/canvas.ts b/x-pack/legacy/plugins/canvas/types/canvas.ts index c454979ef548c..f0137479a0b7f 100644 --- a/x-pack/legacy/plugins/canvas/types/canvas.ts +++ b/x-pack/legacy/plugins/canvas/types/canvas.ts @@ -51,3 +51,8 @@ export interface CanvasWorkpad { pages: CanvasPage[]; width: number; } + +export type CanvasTemplate = CanvasWorkpad & { + help: string; + tags: string[]; +}; From ec38b0ed5f3a61f9c40ded3b8e4b604ad534016c Mon Sep 17 00:00:00 2001 From: Jake Landis Date: Tue, 15 Oct 2019 21:26:59 -0500 Subject: [PATCH 09/48] Add enrich policy endpoint definitions and support for registering X-Pack processors to Console (#48258) * Add template to put enrich policy override. * Expose addProcessorDefinition method on Console plugin API. * Add enrichProcessorDefinition. --- src/legacy/core_plugins/console/index.ts | 4 ++ .../console/server/api_server/api.js | 7 +++- .../console/server/api_server/es_6_0.js | 3 +- .../server/api_server/es_6_0/ingest.js | 11 ++--- .../plugins/console_extensions/index.js | 11 ++++- .../spec/generated/enrich.delete_policy.json | 10 +++++ .../spec/generated/enrich.execute_policy.json | 13 ++++++ .../spec/generated/enrich.get_policy.json | 11 +++++ .../spec/generated/enrich.put_policy.json | 10 +++++ .../spec/generated/enrich.stats.json | 10 +++++ .../console_extensions/spec/ingest/index.js | 42 +++++++++++++++++++ .../spec/overrides/enrich.put_policy.json | 26 ++++++++++++ 12 files changed, 148 insertions(+), 10 deletions(-) create mode 100644 x-pack/legacy/plugins/console_extensions/spec/generated/enrich.delete_policy.json create mode 100644 x-pack/legacy/plugins/console_extensions/spec/generated/enrich.execute_policy.json create mode 100644 x-pack/legacy/plugins/console_extensions/spec/generated/enrich.get_policy.json create mode 100644 x-pack/legacy/plugins/console_extensions/spec/generated/enrich.put_policy.json create mode 100644 x-pack/legacy/plugins/console_extensions/spec/generated/enrich.stats.json create mode 100644 x-pack/legacy/plugins/console_extensions/spec/ingest/index.js create mode 100644 x-pack/legacy/plugins/console_extensions/spec/overrides/enrich.put_policy.json diff --git a/src/legacy/core_plugins/console/index.ts b/src/legacy/core_plugins/console/index.ts index 78df2adb050df..30a85f4e7d342 100644 --- a/src/legacy/core_plugins/console/index.ts +++ b/src/legacy/core_plugins/console/index.ts @@ -23,6 +23,8 @@ import { resolve, join, sep } from 'path'; import url from 'url'; import { has, isEmpty, head, pick } from 'lodash'; +// @ts-ignore +import { addProcessorDefinition } from './server/api_server/es_6_0/ingest'; // @ts-ignore import { resolveApi } from './server/api_server/server'; // @ts-ignore @@ -119,6 +121,8 @@ export default function(kibana: any) { async init(server: any, options: any) { server.expose('addExtensionSpecFilePath', addExtensionSpecFilePath); + server.expose('addProcessorDefinition', addProcessorDefinition); + if (options.ssl && options.ssl.verify) { throw new Error('sense.ssl.verify is no longer supported.'); } diff --git a/src/legacy/core_plugins/console/server/api_server/api.js b/src/legacy/core_plugins/console/server/api_server/api.js index 4b955d015649d..7a24cf8ea8219 100644 --- a/src/legacy/core_plugins/console/server/api_server/api.js +++ b/src/legacy/core_plugins/console/server/api_server/api.js @@ -25,10 +25,12 @@ class Api { this.endpoints = {}; this.name = name; } - addGlobalAutocompleteRules = function (parentNode, rules) { + + addGlobalAutocompleteRules = (parentNode, rules) => { this.globalRules[parentNode] = rules; } - addEndpointDescription(endpoint, description = {}) { + + addEndpointDescription = (endpoint, description = {}) => { let copiedDescription = {}; if (this.endpoints[endpoint]) { copiedDescription = { ...this.endpoints[endpoint] }; @@ -54,6 +56,7 @@ class Api { patterns: [endpoint], methods: ['GET'] }); + this.endpoints[endpoint] = copiedDescription; } diff --git a/src/legacy/core_plugins/console/server/api_server/es_6_0.js b/src/legacy/core_plugins/console/server/api_server/es_6_0.js index 62bf0a9a38361..171d232407956 100644 --- a/src/legacy/core_plugins/console/server/api_server/es_6_0.js +++ b/src/legacy/core_plugins/console/server/api_server/es_6_0.js @@ -19,6 +19,7 @@ import Api from './api'; import { getSpec } from './spec'; +import { register } from './es_6_0/ingest'; const ES_6_0 = new Api('es_6_0'); const spec = getSpec(); @@ -33,7 +34,7 @@ require('./es_6_0/aggregations')(ES_6_0); require('./es_6_0/document')(ES_6_0); require('./es_6_0/filter')(ES_6_0); require('./es_6_0/globals')(ES_6_0); -require('./es_6_0/ingest')(ES_6_0); +register(ES_6_0); require('./es_6_0/mappings')(ES_6_0); require('./es_6_0/query')(ES_6_0); require('./es_6_0/reindex')(ES_6_0); diff --git a/src/legacy/core_plugins/console/server/api_server/es_6_0/ingest.js b/src/legacy/core_plugins/console/server/api_server/es_6_0/ingest.js index 04f2f12e003ed..0c072d6bbfeb0 100644 --- a/src/legacy/core_plugins/console/server/api_server/es_6_0/ingest.js +++ b/src/legacy/core_plugins/console/server/api_server/es_6_0/ingest.js @@ -429,8 +429,7 @@ const pipelineDefinition = { version: 123, }; -export default function (api) { - +export const register = api => { // Note: this isn't an actual API endpoint. It exists so the forEach processor's "processor" field // may recursively use the autocomplete rules for any processor. api.addEndpointDescription('_processor', { @@ -445,8 +444,6 @@ export default function (api) { data_autocomplete_rules: pipelineDefinition }); - - api.addEndpointDescription('ingest.simulate', { data_autocomplete_rules: { pipeline: pipelineDefinition, @@ -454,4 +451,8 @@ export default function (api) { ] } }); -} +}; + +export const addProcessorDefinition = processor => { + processorDefinition.__one_of.push(processor); +}; diff --git a/x-pack/legacy/plugins/console_extensions/index.js b/x-pack/legacy/plugins/console_extensions/index.js index 1a4c185733a2f..571d68e23dea5 100644 --- a/x-pack/legacy/plugins/console_extensions/index.js +++ b/x-pack/legacy/plugins/console_extensions/index.js @@ -5,6 +5,8 @@ */ import { join } from 'path'; +import { processors } from './spec/ingest'; + export function consoleExtensions(kibana) { return new kibana.Plugin({ id: 'console_extensions', @@ -26,11 +28,16 @@ export function consoleExtensions(kibana) { init: server => { if ( server.plugins.console && - server.plugins.console.addExtensionSpecFilePath + server.plugins.console.addExtensionSpecFilePath && + server.plugins.console.addProcessorDefinition ) { - server.plugins.console.addExtensionSpecFilePath( + const { addExtensionSpecFilePath, addProcessorDefinition } = server.plugins.console; + + addExtensionSpecFilePath( join(__dirname, 'spec/') ); + + processors.forEach(processor => addProcessorDefinition(processor)); } else { console.warn( 'Missing server.plugins.console.addExtensionSpecFilePath extension point.', diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/enrich.delete_policy.json b/x-pack/legacy/plugins/console_extensions/spec/generated/enrich.delete_policy.json new file mode 100644 index 0000000000000..3d3c40fa093a4 --- /dev/null +++ b/x-pack/legacy/plugins/console_extensions/spec/generated/enrich.delete_policy.json @@ -0,0 +1,10 @@ +{ + "enrich.delete_policy": { + "methods": [ + "DELETE" + ], + "patterns": [ + "_enrich/policy/{name}" + ] + } +} diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/enrich.execute_policy.json b/x-pack/legacy/plugins/console_extensions/spec/generated/enrich.execute_policy.json new file mode 100644 index 0000000000000..542b709c08ec6 --- /dev/null +++ b/x-pack/legacy/plugins/console_extensions/spec/generated/enrich.execute_policy.json @@ -0,0 +1,13 @@ +{ + "enrich.execute_policy": { + "url_params": { + "wait_for_completion": "__flag__" + }, + "methods": [ + "PUT" + ], + "patterns": [ + "_enrich/policy/{name}/_execute" + ] + } +} diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/enrich.get_policy.json b/x-pack/legacy/plugins/console_extensions/spec/generated/enrich.get_policy.json new file mode 100644 index 0000000000000..b59bf72670b6d --- /dev/null +++ b/x-pack/legacy/plugins/console_extensions/spec/generated/enrich.get_policy.json @@ -0,0 +1,11 @@ +{ + "enrich.get_policy": { + "methods": [ + "GET" + ], + "patterns": [ + "_enrich/policy/{name}", + "_enrich/policy" + ] + } +} diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/enrich.put_policy.json b/x-pack/legacy/plugins/console_extensions/spec/generated/enrich.put_policy.json new file mode 100644 index 0000000000000..96d854f04dcfc --- /dev/null +++ b/x-pack/legacy/plugins/console_extensions/spec/generated/enrich.put_policy.json @@ -0,0 +1,10 @@ +{ + "enrich.put_policy": { + "methods": [ + "PUT" + ], + "patterns": [ + "_enrich/policy/{name}" + ] + } +} diff --git a/x-pack/legacy/plugins/console_extensions/spec/generated/enrich.stats.json b/x-pack/legacy/plugins/console_extensions/spec/generated/enrich.stats.json new file mode 100644 index 0000000000000..e6d1b04d63e45 --- /dev/null +++ b/x-pack/legacy/plugins/console_extensions/spec/generated/enrich.stats.json @@ -0,0 +1,10 @@ +{ + "enrich.stats": { + "methods": [ + "GET" + ], + "patterns": [ + "_enrich/_stats" + ] + } +} diff --git a/x-pack/legacy/plugins/console_extensions/spec/ingest/index.js b/x-pack/legacy/plugins/console_extensions/spec/ingest/index.js new file mode 100644 index 0000000000000..c4dad8d6af224 --- /dev/null +++ b/x-pack/legacy/plugins/console_extensions/spec/ingest/index.js @@ -0,0 +1,42 @@ +/* + * 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: This is copy-pasted from es_6_0/ingest.js in OSS Console. +const commonPipelineParams = { + on_failure: [], + ignore_failure: { + __one_of: [ false, true ] + }, + if: '', + tag: '' +}; + +// Based on https://www.elastic.co/guide/en/elasticsearch/reference/master/enrich-processor.html +const enrichProcessorDefinition = { + enrich: { + __template: { + policy_name: '', + field: '', + target_field: '', + }, + policy_name: '', + field: '', + target_field: '', + ignore_missing: { + __one_of: [ false, true ] + }, + override: { + __one_of: [ true, false ] + }, + max_matches: 1, + shape_relation: 'INTERSECTS', + ...commonPipelineParams + } +}; + +export const processors = [ + enrichProcessorDefinition, +]; diff --git a/x-pack/legacy/plugins/console_extensions/spec/overrides/enrich.put_policy.json b/x-pack/legacy/plugins/console_extensions/spec/overrides/enrich.put_policy.json new file mode 100644 index 0000000000000..36bdeba521f7f --- /dev/null +++ b/x-pack/legacy/plugins/console_extensions/spec/overrides/enrich.put_policy.json @@ -0,0 +1,26 @@ +{ + "enrich.put_policy": { + "data_autocomplete_rules": { + "geo_match": { + "__template": { + "indices": "", + "match_field": "", + "enrich_fields": [] + }, + "indices": "", + "match_field": "", + "enrich_fields": [] + }, + "match": { + "__template": { + "indices": "", + "match_field": "", + "enrich_fields": [] + }, + "indices": "", + "match_field": "", + "enrich_fields": [] + } + } + } +} From fba41f685c0ea9dae4d0193fba042ef3a255bf45 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 15 Oct 2019 23:01:43 -0400 Subject: [PATCH 10/48] [Lens] Track actions in the UI by time (#47919) * [Lens] Track actions in the UI by time * Switch collector to use task data * Report summarized version of task data when requested * Track a more complete set of metrics * Collect suggestion events separately * Pre-aggregate by date in localStorage * Add integration tests * Fix test linter * Fix telemetry naming and run at midnight instead of every minute * Improve cleanup at app level * Fix lint errors * Remove unused mock * Fix tests * Fix types * Update event names and fix local tracking * Respond to review comments * Fix task time * Fix test --- .../plugins/apm/typings/elasticsearch.ts | 21 +- x-pack/legacy/plugins/lens/index.ts | 1 + x-pack/legacy/plugins/lens/mappings.json | 16 ++ .../plugins/lens/public/app_plugin/app.tsx | 14 ++ .../plugins/lens/public/app_plugin/plugin.tsx | 18 ++ .../lens/public/drag_drop/drag_drop.test.tsx | 26 +- .../lens/public/drag_drop/drag_drop.tsx | 4 +- .../editor_frame/chart_switch.tsx | 3 + .../editor_frame/suggestion_panel.tsx | 5 + .../editor_frame/workspace_panel.tsx | 57 +++-- .../change_indexpattern.tsx | 2 + .../public/indexpattern_plugin/datapanel.tsx | 10 +- .../dimension_panel/dimension_panel.tsx | 6 + .../dimension_panel/field_select.tsx | 3 + .../dimension_panel/popover_editor.tsx | 6 + .../public/indexpattern_plugin/field_item.tsx | 2 + .../public/lens_ui_telemetry/factory.test.ts | 110 ++++++++ .../lens/public/lens_ui_telemetry/factory.ts | 134 ++++++++++ .../lens/public/lens_ui_telemetry/index.ts | 7 + .../xy_config_panel.tsx | 8 +- x-pack/legacy/plugins/lens/server/plugin.tsx | 8 +- .../plugins/lens/server/routes/index.ts | 13 +- .../plugins/lens/server/routes/telemetry.ts | 107 ++++++++ .../plugins/lens/server/usage/collectors.ts | 121 ++++++++- .../legacy/plugins/lens/server/usage/index.ts | 1 + .../legacy/plugins/lens/server/usage/task.ts | 237 ++++++++++++++++++ .../legacy/plugins/lens/server/usage/types.ts | 30 ++- .../lens/server/usage/visualization_counts.ts | 16 +- .../legacy/plugins/xpack_main/xpack_main.d.ts | 1 + .../test/api_integration/apis/lens/index.ts | 1 + .../api_integration/apis/lens/telemetry.ts | 216 ++++++++++++++++ .../test/functional/apps/lens/smokescreen.ts | 32 --- 32 files changed, 1133 insertions(+), 103 deletions(-) create mode 100644 x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.test.ts create mode 100644 x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.ts create mode 100644 x-pack/legacy/plugins/lens/public/lens_ui_telemetry/index.ts create mode 100644 x-pack/legacy/plugins/lens/server/routes/telemetry.ts create mode 100644 x-pack/legacy/plugins/lens/server/usage/task.ts create mode 100644 x-pack/test/api_integration/apis/lens/telemetry.ts diff --git a/x-pack/legacy/plugins/apm/typings/elasticsearch.ts b/x-pack/legacy/plugins/apm/typings/elasticsearch.ts index 10cd7bcbf4f38..4da52c773a666 100644 --- a/x-pack/legacy/plugins/apm/typings/elasticsearch.ts +++ b/x-pack/legacy/plugins/apm/typings/elasticsearch.ts @@ -64,11 +64,22 @@ declare module 'elasticsearch' { // eslint-disable-next-line @typescript-eslint/prefer-interface type FiltersAggregation = { - buckets: Array< - { - doc_count: number; - } & SubAggregation - >; + // The filters aggregation can have named filters or anonymous filters, + // which changes the structure of the return + // https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-filters-aggregation.html + buckets: SubAggregationMap extends { + filters: { filters: Record }; + } + ? { + [key in keyof SubAggregationMap['filters']['filters']]: { + doc_count: number; + } & SubAggregation; + } + : Array< + { + doc_count: number; + } & SubAggregation + >; }; type SamplerAggregation = SubAggregation< diff --git a/x-pack/legacy/plugins/lens/index.ts b/x-pack/legacy/plugins/lens/index.ts index 7acd62502d428..a8984bd80fb1c 100644 --- a/x-pack/legacy/plugins/lens/index.ts +++ b/x-pack/legacy/plugins/lens/index.ts @@ -61,6 +61,7 @@ export const lens: LegacyPluginInitializer = kibana => { savedObjects: server.savedObjects, usage: server.usage, config: server.config(), + server, }); server.events.on('stop', () => { diff --git a/x-pack/legacy/plugins/lens/mappings.json b/x-pack/legacy/plugins/lens/mappings.json index 832d152eb77a1..8304cf9c9cb64 100644 --- a/x-pack/legacy/plugins/lens/mappings.json +++ b/x-pack/legacy/plugins/lens/mappings.json @@ -15,5 +15,21 @@ "type": "keyword" } } + }, + "lens-ui-telemetry": { + "properties": { + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "date": { + "type": "date" + }, + "count": { + "type": "integer" + } + } } } diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx index 640f1fdb99a21..1152a3de77181 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -25,6 +25,7 @@ import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_reac import { Document, SavedObjectStore } from '../persistence'; import { EditorFrameInstance } from '../types'; import { NativeRenderer } from '../native_renderer'; +import { trackUiEvent } from '../lens_ui_telemetry'; interface State { isLoading: boolean; @@ -84,6 +85,8 @@ export function App({ const subscription = dataShim.filter.filterManager.getUpdates$().subscribe({ next: () => { setState(s => ({ ...s, filters: dataShim.filter.filterManager.getFilters() })); + + trackUiEvent('app_filters_updated'); }, }); return () => { @@ -191,6 +194,16 @@ export function App({ screenTitle={'lens'} onQuerySubmit={payload => { const { dateRange, query } = payload; + + if ( + dateRange.from !== state.dateRange.fromDate || + dateRange.to !== state.dateRange.toDate + ) { + trackUiEvent('app_date_change'); + } else { + trackUiEvent('app_query_change'); + } + setState(s => ({ ...s, dateRange: { @@ -309,6 +322,7 @@ export function App({ } }) .catch(() => { + trackUiEvent('save_failed'); core.notifications.toasts.addDanger( i18n.translate('xpack.lens.app.docSavingError', { defaultMessage: 'Error saving document', diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx index 2668946fec47b..1f8779bb68b81 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx @@ -25,6 +25,12 @@ import { } from '../datatable_visualization_plugin'; import { App } from './app'; import { EditorFrameInstance } from '../types'; +import { + LensReportManager, + setReportManager, + stopReportManager, + trackUiEvent, +} from '../lens_ui_telemetry'; export interface LensPluginStartDependencies { data: DataPublicPluginStart; @@ -63,7 +69,16 @@ export class AppPlugin { this.instance = editorFrameStartInterface.createInstance({}); + setReportManager( + new LensReportManager({ + storage: new Storage(localStorage), + basePath: core.http.basePath.get(), + http: core.http, + }) + ); + const renderEditor = (routeProps: RouteComponentProps<{ id?: string }>) => { + trackUiEvent('loaded'); return ( ; } @@ -106,6 +122,8 @@ export class AppPlugin { this.instance.unmount(); } + stopReportManager(); + // TODO this will be handled by the plugin platform itself indexPatternDatasourceStop(); xyVisualizationStop(); diff --git a/x-pack/legacy/plugins/lens/public/drag_drop/drag_drop.test.tsx b/x-pack/legacy/plugins/lens/public/drag_drop/drag_drop.test.tsx index 7471039a482bf..17a6afa9b70ff 100644 --- a/x-pack/legacy/plugins/lens/public/drag_drop/drag_drop.test.tsx +++ b/x-pack/legacy/plugins/lens/public/drag_drop/drag_drop.test.tsx @@ -71,7 +71,7 @@ describe('DragDrop', () => { const component = mount( - + Hello! @@ -87,6 +87,30 @@ describe('DragDrop', () => { expect(onDrop).toBeCalledWith('hola'); }); + test('drop function is not called on droppable=false', async () => { + const preventDefault = jest.fn(); + const stopPropagation = jest.fn(); + const setDragging = jest.fn(); + const onDrop = jest.fn(); + + const component = mount( + + + Hello! + + + ); + + component + .find('[data-test-subj="lnsDragDrop"]') + .simulate('drop', { preventDefault, stopPropagation }); + + expect(preventDefault).toBeCalled(); + expect(stopPropagation).toBeCalled(); + expect(setDragging).toBeCalledWith(undefined); + expect(onDrop).not.toHaveBeenCalled(); + }); + test('droppable is reflected in the className', () => { const component = render( ; @@ -121,7 +122,8 @@ export function DragDrop(props: Props) { setState({ ...state, isActive: false }); setDragging(undefined); - if (onDrop) { + if (onDrop && droppable) { + trackUiEvent('drop_total'); onDrop(dragging); } }; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx index 1c94460034019..26db1224eb352 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx @@ -18,6 +18,7 @@ import { i18n } from '@kbn/i18n'; import { Visualization, FramePublicAPI, Datasource } from '../../types'; import { Action } from './state_management'; import { getSuggestions, switchToSuggestion, Suggestion } from './suggestion_helpers'; +import { trackUiEvent } from '../../lens_ui_telemetry'; interface VisualizationSelection { visualizationId: string; @@ -76,6 +77,8 @@ export function ChartSwitch(props: Props) { const commitSelection = (selection: VisualizationSelection) => { setFlyoutOpen(false); + trackUiEvent(`chart_switch`); + switchToSuggestion( props.framePublicAPI, props.dispatch, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx index de25c2f7f638b..7d1f400f484e1 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx @@ -27,6 +27,7 @@ import { getSuggestions, switchToSuggestion } from './suggestion_helpers'; import { ExpressionRenderer } from '../../../../../../../src/legacy/core_plugins/expressions/public'; import { prependDatasourceExpression, prependKibanaContext } from './expression_helpers'; import { debouncedComponent } from '../../debounced_component'; +import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry'; const MAX_SUGGESTIONS_DISPLAYED = 5; @@ -227,6 +228,7 @@ export function SuggestionPanel({ function rollbackToCurrentVisualization() { if (lastSelectedSuggestion !== -1) { + trackSuggestionEvent('back_to_current'); setLastSelectedSuggestion(-1); dispatch({ type: 'ROLLBACK_SUGGESTION', @@ -261,6 +263,7 @@ export function SuggestionPanel({ data-test-subj="lensSubmitSuggestion" size="xs" onClick={() => { + trackUiEvent('suggestion_confirmed'); dispatch({ type: 'SUBMIT_SUGGESTION', }); @@ -307,9 +310,11 @@ export function SuggestionPanel({ ExpressionRenderer={ExpressionRendererComponent} key={index} onSelect={() => { + trackUiEvent('suggestion_clicked'); if (lastSelectedSuggestion === index) { rollbackToCurrentVisualization(); } else { + trackSuggestionEvent(`position_${index}_of_${suggestions.length}`); setLastSelectedSuggestion(index); switchToSuggestion(frame, dispatch, suggestion); } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx index fce2144e93bcf..1fb1af46f6f0d 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx @@ -25,6 +25,7 @@ import { DragDrop, DragContext } from '../../drag_drop'; import { getSuggestions, switchToSuggestion } from './suggestion_helpers'; import { buildExpression } from './expression_helpers'; import { debouncedComponent } from '../../debounced_component'; +import { trackUiEvent } from '../../lens_ui_telemetry'; export interface WorkspacePanelProps { activeVisualizationId: string | null; @@ -91,8 +92,37 @@ export function InnerWorkspacePanel({ return suggestions.find(s => s.visualizationId === activeVisualizationId) || suggestions[0]; }, [dragDropContext.dragging]); + const [expressionError, setExpressionError] = useState(undefined); + + const activeVisualization = activeVisualizationId + ? visualizationMap[activeVisualizationId] + : null; + const expression = useMemo(() => { + try { + return buildExpression({ + visualization: activeVisualization, + visualizationState, + datasourceMap, + datasourceStates, + framePublicAPI, + }); + } catch (e) { + setExpressionError(e.toString()); + } + }, [ + activeVisualization, + visualizationState, + datasourceMap, + datasourceStates, + framePublicAPI.dateRange, + framePublicAPI.query, + framePublicAPI.filters, + ]); + function onDrop() { if (suggestionForDraggedField) { + trackUiEvent('drop_onto_workspace'); + trackUiEvent(expression ? 'drop_non_empty' : 'drop_empty'); switchToSuggestion( framePublicAPI, dispatch, @@ -146,33 +176,6 @@ export function InnerWorkspacePanel({ } function renderVisualization() { - const [expressionError, setExpressionError] = useState(undefined); - - const activeVisualization = activeVisualizationId - ? visualizationMap[activeVisualizationId] - : null; - const expression = useMemo(() => { - try { - return buildExpression({ - visualization: activeVisualization, - visualizationState, - datasourceMap, - datasourceStates, - framePublicAPI, - }); - } catch (e) { - setExpressionError(e.toString()); - } - }, [ - activeVisualization, - visualizationState, - datasourceMap, - datasourceStates, - framePublicAPI.dateRange, - framePublicAPI.query, - framePublicAPI.filters, - ]); - useEffect(() => { // reset expression error if component attempts to run it again if (expressionError) { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern.tsx index c101a8bfb7f08..dc42b97cce08d 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/change_indexpattern.tsx @@ -9,6 +9,7 @@ import React, { useState } from 'react'; import { EuiButtonEmpty, EuiPopover, EuiSelectable, EuiButtonEmptyProps } from '@elastic/eui'; import { EuiSelectableProps } from '@elastic/eui/src/components/selectable/selectable'; import { IndexPatternRef } from './types'; +import { trackUiEvent } from '../lens_ui_telemetry'; export type ChangeIndexPatternTriggerProps = EuiButtonEmptyProps & { label: string; @@ -74,6 +75,7 @@ export function ChangeIndexPattern({ const choice = (choices.find(({ checked }) => checked) as unknown) as { value: string; }; + trackUiEvent('indexpattern_changed'); onChangeIndexPattern(choice.value); setPopoverIsOpen(false); }} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx index 89daaad00f11d..7fa8175e1857c 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx @@ -35,6 +35,7 @@ import { IndexPatternField, IndexPatternRef, } from './types'; +import { trackUiEvent } from '../lens_ui_telemetry'; import { syncExistingFields } from './loader'; import { fieldExists } from './pure_helpers'; @@ -301,6 +302,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ defaultMessage: 'Clear name and type filters', }), onClick: () => { + trackUiEvent('indexpattern_filters_cleared'); setLocalState(s => ({ ...s, nameFilter: '', @@ -370,14 +372,15 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ key={type} icon={localState.typeFilter.includes(type) ? 'check' : 'empty'} data-test-subj={`typeFilter-${type}`} - onClick={() => + onClick={() => { + trackUiEvent('indexpattern_type_filter_toggled'); setLocalState(s => ({ ...s, typeFilter: localState.typeFilter.includes(type) ? localState.typeFilter.filter(t => t !== type) : [...localState.typeFilter, type], - })) - } + })); + }} > {fieldTypeNames[type]} @@ -388,6 +391,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ compressed checked={!showEmptyFields} onChange={() => { + trackUiEvent('indexpattern_existence_toggled'); onToggleEmptyFields(); }} label={i18n.translate('xpack.lens.indexPatterns.toggleEmptyFieldsSwitch', { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx index 81be4e393a8f2..c0c774a225642 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx @@ -22,6 +22,7 @@ import { DragContextState, ChildDragDropProvider, DragDrop } from '../../drag_dr import { changeColumn, deleteColumn } from '../state_helpers'; import { isDraggedField, hasField } from '../utils'; import { IndexPatternPrivateState, IndexPatternField } from '../types'; +import { trackUiEvent } from '../../lens_ui_telemetry'; export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { state: IndexPatternPrivateState; @@ -137,6 +138,10 @@ export const IndexPatternDimensionPanel = memo(function IndexPatternDimensionPan field: droppedItem.field, }); + trackUiEvent('drop_onto_dimension'); + const hasData = Object.values(props.state.layers).some(({ columns }) => columns.length); + trackUiEvent(hasData ? 'drop_non_empty' : 'drop_empty'); + props.setState( changeColumn({ state: props.state, @@ -170,6 +175,7 @@ export const IndexPatternDimensionPanel = memo(function IndexPatternDimensionPan defaultMessage: 'Remove configuration', })} onClick={() => { + trackUiEvent('indexpattern_dimension_removed'); props.setState( deleteColumn({ state: props.state, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx index b258df789748b..7eeffdff383e1 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/field_select.tsx @@ -18,6 +18,7 @@ import { LensFieldIcon } from '../lens_field_icon'; import { DataType } from '../../types'; import { OperationFieldSupportMatrix } from './dimension_panel'; import { IndexPattern, IndexPatternField, IndexPatternPrivateState } from '../types'; +import { trackUiEvent } from '../../lens_ui_telemetry'; import { fieldExists } from '../pure_helpers'; export type FieldChoice = @@ -170,6 +171,8 @@ export function FieldSelect({ return; } + trackUiEvent('indexpattern_dimension_field_changed'); + onChoose((choices[0].value as unknown) as FieldChoice); }} renderOption={(option, searchValue) => { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx index 080f80b1cc476..bc46143b5f3b5 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx @@ -34,6 +34,7 @@ import { FieldSelect } from './field_select'; import { hasField } from '../utils'; import { BucketNestingEditor } from './bucket_nesting_editor'; import { IndexPattern, IndexPatternField } from '../types'; +import { trackUiEvent } from '../../lens_ui_telemetry'; const operationPanels = getOperationDisplay(); @@ -162,10 +163,12 @@ export function PopoverEditor(props: PopoverEditorProps) { } else { setInvalidOperationType(operationType); } + trackUiEvent(`indexpattern_dimension_operation_${operationType}`); return; } if (!compatibleWithCurrentField) { setInvalidOperationType(operationType); + trackUiEvent(`indexpattern_dimension_operation_${operationType}`); return; } if (incompatibleSelectedOperationType) { @@ -182,6 +185,9 @@ export function PopoverEditor(props: PopoverEditorProps) { indexPattern: currentIndexPattern, field: hasField(selectedColumn) ? fieldMap[selectedColumn.sourceField] : undefined, }); + trackUiEvent( + `indexpattern_dimension_operation_from_${selectedColumn.operationType}_to_${operationType}` + ); setState( changeColumn({ state, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx index f9db1e9f4f550..41a4bd3549dc1 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx @@ -45,6 +45,7 @@ import { DatasourceDataPanelProps, DataType } from '../types'; import { BucketedAggregation, FieldStatsResponse } from '../../common'; import { IndexPattern, IndexPatternField } from './types'; import { LensFieldIcon, getColorForDataType } from './lens_field_icon'; +import { trackUiEvent } from '../lens_ui_telemetry'; export interface FieldItemProps { core: DatasourceDataPanelProps['core']; @@ -141,6 +142,7 @@ export function FieldItem(props: FieldItemProps) { function togglePopover() { setOpen(!infoIsOpen); if (!infoIsOpen) { + trackUiEvent('indexpattern_field_info_click'); fetchData(); } } diff --git a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.test.ts b/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.test.ts new file mode 100644 index 0000000000000..9b65cbefaf799 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.test.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + LensReportManager, + setReportManager, + stopReportManager, + trackUiEvent, + trackSuggestionEvent, +} from './factory'; +import { Storage } from 'src/legacy/core_plugins/data/public/types'; +import { coreMock } from 'src/core/public/mocks'; +import { HttpServiceBase } from 'kibana/public'; + +jest.useFakeTimers(); + +const createMockStorage = () => { + let lastData = { events: {}, suggestionEvents: {} }; + return { + get: jest.fn().mockImplementation(() => lastData), + set: jest.fn().mockImplementation((key, value) => { + lastData = value; + }), + remove: jest.fn(), + clear: jest.fn(), + }; +}; + +describe('Lens UI telemetry', () => { + let storage: jest.Mocked; + let http: jest.Mocked; + let dateSpy: jest.SpyInstance; + + beforeEach(() => { + dateSpy = jest + .spyOn(Date, 'now') + .mockImplementation(() => new Date(Date.UTC(2019, 9, 23)).valueOf()); + + storage = createMockStorage(); + http = coreMock.createSetup().http; + http.post.mockClear(); + const fakeManager = new LensReportManager({ + http, + storage, + basePath: '/basepath', + }); + setReportManager(fakeManager); + }); + + afterEach(() => { + stopReportManager(); + dateSpy.mockRestore(); + }); + + it('should write immediately and track local state', () => { + trackUiEvent('loaded'); + + expect(storage.set).toHaveBeenCalledWith('lens-ui-telemetry', { + events: expect.any(Object), + suggestionEvents: {}, + }); + + trackSuggestionEvent('reload'); + + expect(storage.set).toHaveBeenLastCalledWith('lens-ui-telemetry', { + events: expect.any(Object), + suggestionEvents: expect.any(Object), + }); + }); + + it('should post the results after waiting 10 seconds, if there is data', async () => { + jest.runOnlyPendingTimers(); + + http.post.mockResolvedValue({}); + + expect(http.post).not.toHaveBeenCalled(); + expect(storage.set).toHaveBeenCalledTimes(0); + + trackUiEvent('load'); + expect(storage.set).toHaveBeenCalledTimes(1); + + jest.runOnlyPendingTimers(); + + expect(http.post).toHaveBeenCalledWith(`/basepath/api/lens/telemetry`, { + body: JSON.stringify({ + events: { + '2019-10-23': { + load: 1, + }, + }, + suggestionEvents: {}, + }), + }); + }); + + it('should keep its local state after an http error', () => { + http.post.mockRejectedValue('http error'); + + trackUiEvent('load'); + expect(storage.set).toHaveBeenCalledTimes(1); + + jest.runOnlyPendingTimers(); + + expect(http.post).toHaveBeenCalled(); + expect(storage.set).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.ts b/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.ts new file mode 100644 index 0000000000000..d264e9c77c463 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/factory.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; +import { HttpServiceBase } from 'src/core/public'; + +import { Storage } from 'src/legacy/core_plugins/data/public/types'; +import { BASE_API_URL } from '../../common'; + +const STORAGE_KEY = 'lens-ui-telemetry'; + +let reportManager: LensReportManager; + +export function setReportManager(newManager: LensReportManager) { + if (reportManager) { + reportManager.stop(); + } + reportManager = newManager; +} + +export function stopReportManager() { + if (reportManager) { + reportManager.stop(); + } +} + +export function trackUiEvent(name: string) { + if (reportManager) { + reportManager.trackEvent(name); + } +} + +export function trackSuggestionEvent(name: string) { + if (reportManager) { + reportManager.trackSuggestionEvent(name); + } +} + +export class LensReportManager { + private events: Record> = {}; + private suggestionEvents: Record> = {}; + + private storage: Storage; + private http: HttpServiceBase; + private basePath: string; + private timer: ReturnType; + + constructor({ + storage, + http, + basePath, + }: { + storage: Storage; + http: HttpServiceBase; + basePath: string; + }) { + this.storage = storage; + this.http = http; + this.basePath = basePath; + + this.readFromStorage(); + + this.timer = setInterval(() => { + this.postToServer(); + }, 10000); + } + + public trackEvent(name: string) { + this.readFromStorage(); + this.trackTo(this.events, name); + } + + public trackSuggestionEvent(name: string) { + this.readFromStorage(); + this.trackTo(this.suggestionEvents, name); + } + + public stop() { + if (this.timer) { + clearInterval(this.timer); + } + } + + private readFromStorage() { + const data = this.storage.get(STORAGE_KEY); + if (data && typeof data.events === 'object' && typeof data.suggestionEvents === 'object') { + this.events = data.events; + this.suggestionEvents = data.suggestionEvents; + } + } + + private async postToServer() { + this.readFromStorage(); + if (Object.keys(this.events).length || Object.keys(this.suggestionEvents).length) { + try { + await this.http.post(`${this.basePath}${BASE_API_URL}/telemetry`, { + body: JSON.stringify({ + events: this.events, + suggestionEvents: this.suggestionEvents, + }), + }); + this.events = {}; + this.suggestionEvents = {}; + this.write(); + } catch (e) { + // Silent error because events will be reported during the next timer + } + } + } + + private trackTo(target: Record>, name: string) { + const date = moment() + .utc() + .format('YYYY-MM-DD'); + if (!target[date]) { + target[date] = { + [name]: 1, + }; + } else if (!target[date][name]) { + target[date][name] = 1; + } else { + target[date][name] += 1; + } + + this.write(); + } + + private write() { + this.storage.set(STORAGE_KEY, { events: this.events, suggestionEvents: this.suggestionEvents }); + } +} diff --git a/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/index.ts b/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/index.ts new file mode 100644 index 0000000000000..79575a59f565e --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/lens_ui_telemetry/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 './factory'; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx index d6a014d9f8050..9461229313f4c 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx @@ -27,6 +27,7 @@ import { NativeRenderer } from '../native_renderer'; import { MultiColumnEditor } from '../multi_column_editor'; import { generateId } from '../id_generator'; import { isHorizontalChart, isHorizontalSeries } from './state_helpers'; +import { trackUiEvent } from '../lens_ui_telemetry'; const isNumericMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; const isBucketed = (op: OperationMetadata) => op.isBucketed; @@ -105,7 +106,10 @@ function LayerSettings({ iconType: t.icon || 'empty', }))} idSelected={layer.seriesType} - onChange={seriesType => setSeriesType(seriesType as SeriesType)} + onChange={seriesType => { + trackUiEvent('xy_change_layer_display'); + setSeriesType(seriesType as SeriesType); + }} isIconOnly buttonSize="compressed" /> @@ -149,6 +153,7 @@ export function XYConfigPanel(props: VisualizationProps) { setState(updateLayer(state, { ...layer, seriesType }, index)) } removeLayer={() => { + trackUiEvent('xy_layer_removed'); frame.removeLayers([layer.layerId]); setState({ ...state, layers: state.layers.filter(l => l !== layer) }); }} @@ -258,6 +263,7 @@ export function XYConfigPanel(props: VisualizationProps) { defaultMessage: 'Add layer', })} onClick={() => { + trackUiEvent('xy_layer_added'); const usedSeriesTypes = _.uniq(state.layers.map(layer => layer.seriesType)); setState({ ...state, diff --git a/x-pack/legacy/plugins/lens/server/plugin.tsx b/x-pack/legacy/plugins/lens/server/plugin.tsx index 37a87d0530339..a4c8e9b268df5 100644 --- a/x-pack/legacy/plugins/lens/server/plugin.tsx +++ b/x-pack/legacy/plugins/lens/server/plugin.tsx @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaConfig } from 'src/legacy/server/kbn_server'; +import { Server, KibanaConfig } from 'src/legacy/server/kbn_server'; import { Plugin, CoreSetup, SavedObjectsLegacyService } from 'src/core/server'; import { setupRoutes } from './routes'; -import { registerLensUsageCollector } from './usage'; +import { registerLensUsageCollector, initializeLensTelemetry } from './usage'; export class LensServer implements Plugin<{}, {}, {}, {}> { setup( @@ -21,10 +21,12 @@ export class LensServer implements Plugin<{}, {}, {}, {}> { }; }; config: KibanaConfig; + server: Server; } ) { - setupRoutes(core); + setupRoutes(core, plugins); registerLensUsageCollector(core, plugins); + initializeLensTelemetry(core, plugins); return {}; } diff --git a/x-pack/legacy/plugins/lens/server/routes/index.ts b/x-pack/legacy/plugins/lens/server/routes/index.ts index 5aa54271aaa77..79690b40395d6 100644 --- a/x-pack/legacy/plugins/lens/server/routes/index.ts +++ b/x-pack/legacy/plugins/lens/server/routes/index.ts @@ -4,11 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup } from 'src/core/server'; +import { KibanaConfig } from 'src/legacy/server/kbn_server'; +import { CoreSetup, SavedObjectsLegacyService } from 'src/core/server'; import { existingFieldsRoute } from './existing_fields'; import { initFieldsRoute } from './field_stats'; +import { initLensUsageRoute } from './telemetry'; -export function setupRoutes(setup: CoreSetup) { +export function setupRoutes( + setup: CoreSetup, + plugins: { + savedObjects: SavedObjectsLegacyService; + config: KibanaConfig; + } +) { existingFieldsRoute(setup); initFieldsRoute(setup); + initLensUsageRoute(setup, plugins); } diff --git a/x-pack/legacy/plugins/lens/server/routes/telemetry.ts b/x-pack/legacy/plugins/lens/server/routes/telemetry.ts new file mode 100644 index 0000000000000..895b77cfa8978 --- /dev/null +++ b/x-pack/legacy/plugins/lens/server/routes/telemetry.ts @@ -0,0 +1,107 @@ +/* + * 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 Boom from 'boom'; +import { KibanaConfig } from 'src/legacy/server/kbn_server'; +import { CoreSetup, SavedObjectsLegacyService } from 'src/core/server'; +import { schema } from '@kbn/config-schema'; +import { BASE_API_URL } from '../../common'; + +export function getSavedObjectsClient( + savedObjects: SavedObjectsLegacyService, + callAsInternalUser: unknown +) { + const { SavedObjectsClient, getSavedObjectsRepository } = savedObjects; + const internalRepository = getSavedObjectsRepository(callAsInternalUser); + return new SavedObjectsClient(internalRepository); +} + +// This route is responsible for taking a batch of click events from the browser +// and writing them to saved objects +export async function initLensUsageRoute( + setup: CoreSetup, + plugins: { + savedObjects: SavedObjectsLegacyService; + config: KibanaConfig; + } +) { + const router = setup.http.createRouter(); + router.post( + { + path: `${BASE_API_URL}/telemetry`, + validate: { + body: schema.object({ + events: schema.mapOf(schema.string(), schema.mapOf(schema.string(), schema.number())), + suggestionEvents: schema.mapOf( + schema.string(), + schema.mapOf(schema.string(), schema.number()) + ), + }), + }, + }, + async (context, req, res) => { + const { dataClient } = context.core.elasticsearch; + + const { events, suggestionEvents } = req.body; + + try { + const client = getSavedObjectsClient(plugins.savedObjects, dataClient.callAsCurrentUser); + + const allEvents: Array<{ + type: 'lens-ui-telemetry'; + attributes: {}; + }> = []; + + events.forEach((subMap, date) => { + subMap.forEach((count, key) => { + allEvents.push({ + type: 'lens-ui-telemetry', + attributes: { + name: key, + date, + count, + type: 'regular', + }, + }); + }); + }); + suggestionEvents.forEach((subMap, date) => { + subMap.forEach((count, key) => { + allEvents.push({ + type: 'lens-ui-telemetry', + attributes: { + name: key, + date, + count, + type: 'suggestion', + }, + }); + }); + }); + + if (allEvents.length) { + await client.bulkCreate(allEvents); + } + + return res.ok({ body: {} }); + } catch (e) { + if (e.status === 404) { + return res.notFound(); + } + if (e.isBoom) { + if (e.output.statusCode === 404) { + return res.notFound(); + } + return res.internalError(e.output.message); + } else { + return res.internalError({ + body: Boom.internal(e.message || e.name), + }); + } + } + } + ); +} diff --git a/x-pack/legacy/plugins/lens/server/usage/collectors.ts b/x-pack/legacy/plugins/lens/server/usage/collectors.ts index 75f422088ed81..9a58026002ade 100644 --- a/x-pack/legacy/plugins/lens/server/usage/collectors.ts +++ b/x-pack/legacy/plugins/lens/server/usage/collectors.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaConfig } from 'src/legacy/server/kbn_server'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import moment from 'moment'; +import { get } from 'lodash'; +import { Server, KibanaConfig } from 'src/legacy/server/kbn_server'; import { CoreSetup, SavedObjectsLegacyService } from 'src/core/server'; -import { getVisualizationCounts } from './visualization_counts'; -import { LensUsage } from './types'; +import { LensUsage, LensTelemetryState } from './types'; export function registerLensUsageCollector( core: CoreSetup, @@ -21,25 +21,120 @@ export function registerLensUsageCollector( }; }; config: KibanaConfig; + server: Server; } ) { + let isCollectorReady = false; + async function determineIfTaskManagerIsReady() { + let isReady = false; + try { + isReady = await isTaskManagerReady(plugins.server); + } catch (err) {} // eslint-disable-line + + if (isReady) { + isCollectorReady = true; + } else { + setTimeout(determineIfTaskManagerIsReady, 500); + } + } + determineIfTaskManagerIsReady(); + const lensUsageCollector = plugins.usage.collectorSet.makeUsageCollector({ type: 'lens', - fetch: async (callCluster: CallCluster): Promise => { + fetch: async (): Promise => { try { - return getVisualizationCounts(callCluster, plugins.config); + const docs = await getLatestTaskState(plugins.server); + // get the accumulated state from the recurring task + const state: LensTelemetryState = get(docs, '[0].state'); + + const events = getDataByDate(state.byDate); + const suggestions = getDataByDate(state.suggestionsByDate); + + return { + ...state.saved, + events_30_days: events.last30, + events_90_days: events.last90, + suggestion_events_30_days: suggestions.last30, + suggestion_events_90_days: suggestions.last90, + }; } catch (err) { return { - saved_total: 0, - saved_last_30_days: 0, - saved_last_90_days: 0, - visualization_types_overall: {}, - visualization_types_last_30_days: {}, - visualization_types_last_90_days: {}, + saved_overall_total: 0, + saved_30_days_total: 0, + saved_90_days_total: 0, + saved_overall: {}, + saved_30_days: {}, + saved_90_days: {}, + + events_30_days: {}, + events_90_days: {}, + suggestion_events_30_days: {}, + suggestion_events_90_days: {}, }; } }, - isReady: () => true, + isReady: () => isCollectorReady, }); plugins.usage.collectorSet.register(lensUsageCollector); } + +function addEvents(prevEvents: Record, newEvents: Record) { + Object.keys(newEvents).forEach(key => { + prevEvents[key] = (prevEvents[key] || 0) + newEvents[key]; + }); +} + +async function isTaskManagerReady(server: Server) { + return (await getLatestTaskState(server)) !== null; +} + +async function getLatestTaskState(server: Server) { + const taskManager = server.plugins.task_manager!; + + try { + const result = await taskManager.fetch({ + query: { bool: { filter: { term: { _id: `task:Lens-lens_telemetry` } } } }, + }); + return result.docs; + } catch (err) { + const errMessage = err && err.message ? err.message : err.toString(); + /* + The usage service WILL to try to fetch from this collector before the task manager has been initialized, because the + task manager has to wait for all plugins to initialize first. It's fine to ignore it as next time around it will be + initialized (or it will throw a different type of error) + */ + if (!errMessage.includes('NotInitialized')) { + throw err; + } + } + + return null; +} + +function getDataByDate(dates: Record>) { + const byDate = Object.keys(dates || {}).map(dateStr => parseInt(dateStr, 10)); + + const last30: Record = {}; + const last90: Record = {}; + + const last30Timestamp = moment() + .subtract(30, 'days') + .unix(); + const last90Timestamp = moment() + .subtract(90, 'days') + .unix(); + + byDate.forEach(dateKey => { + if (dateKey >= last30Timestamp) { + addEvents(last30, dates[dateKey]); + addEvents(last90, dates[dateKey]); + } else if (dateKey > last90Timestamp) { + addEvents(last90, dates[dateKey]); + } + }); + + return { + last30, + last90, + }; +} diff --git a/x-pack/legacy/plugins/lens/server/usage/index.ts b/x-pack/legacy/plugins/lens/server/usage/index.ts index 4dd74057e0877..e7c09fdda3c05 100644 --- a/x-pack/legacy/plugins/lens/server/usage/index.ts +++ b/x-pack/legacy/plugins/lens/server/usage/index.ts @@ -5,3 +5,4 @@ */ export * from './collectors'; +export * from './task'; diff --git a/x-pack/legacy/plugins/lens/server/usage/task.ts b/x-pack/legacy/plugins/lens/server/usage/task.ts new file mode 100644 index 0000000000000..ba74bcd240886 --- /dev/null +++ b/x-pack/legacy/plugins/lens/server/usage/task.ts @@ -0,0 +1,237 @@ +/* + * 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 moment from 'moment'; +import KbnServer, { Server } from 'src/legacy/server/kbn_server'; +import { CoreSetup } from 'src/core/server'; +import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; +// This import has the side effect of allowing us to use the elasticsearch type +// extensions below. Without this import, the compiler is unable to find these +// in tests +import {} from '../../../apm/typings/elasticsearch'; +import { + SearchParams, + DeleteDocumentByQueryParams, + SearchResponse, + DeleteDocumentByQueryResponse, + AggregationSearchResponseWithTotalHitsAsInt, +} from 'elasticsearch'; +import { XPackMainPlugin } from '../../../xpack_main/xpack_main'; +import { RunContext } from '../../../task_manager'; +import { getVisualizationCounts } from './visualization_counts'; + +// This task is responsible for running daily and aggregating all the Lens click event objects +// into daily rolled-up documents, which will be used in reporting click stats + +const TELEMETRY_TASK_TYPE = 'lens_telemetry'; + +export const TASK_ID = `Lens-${TELEMETRY_TASK_TYPE}`; + +type ClusterSearchType = ( + endpoint: 'search', + params: SearchParams & { + rest_total_hits_as_int: boolean; + }, + options?: CallClusterOptions +) => Promise>; +type ClusterDeleteType = ( + endpoint: 'deleteByQuery', + params: DeleteDocumentByQueryParams, + options?: CallClusterOptions +) => Promise; + +export function initializeLensTelemetry(core: CoreSetup, { server }: { server: Server }) { + registerLensTelemetryTask(core, { server }); + scheduleTasks(server); +} + +function registerLensTelemetryTask(core: CoreSetup, { server }: { server: Server }) { + const taskManager = server.plugins.task_manager!; + taskManager.registerTaskDefinitions({ + [TELEMETRY_TASK_TYPE]: { + title: 'Lens telemetry fetch task', + type: TELEMETRY_TASK_TYPE, + timeout: '1m', + createTaskRunner: telemetryTaskRunner(server), + }, + }); +} + +function scheduleTasks(server: Server) { + const taskManager = server.plugins.task_manager; + const { kbnServer } = (server.plugins.xpack_main as XPackMainPlugin & { + status: { plugin: { kbnServer: KbnServer } }; + }).status.plugin; + + kbnServer.afterPluginsInit(() => { + // The code block below can't await directly within "afterPluginsInit" + // callback due to circular dependency The server isn't "ready" until + // this code block finishes. Migrations wait for server to be ready before + // executing. Saved objects repository waits for migrations to finish before + // finishing the request. To avoid this, we'll await within a separate + // function block. + (async () => { + try { + await taskManager!.schedule({ + id: TASK_ID, + taskType: TELEMETRY_TASK_TYPE, + state: { byDate: {}, suggestionsByDate: {}, saved: {}, runs: 0 }, + params: {}, + }); + } catch (e) { + server.log(['warning', 'telemetry'], `Error scheduling task, received ${e.message}`); + } + })(); + }); +} + +export async function getDailyEvents( + kibanaIndex: string, + callCluster: ClusterSearchType & ClusterDeleteType +): Promise<{ + byDate: Record>; + suggestionsByDate: Record>; +}> { + const aggs = { + daily: { + date_histogram: { + field: 'lens-ui-telemetry.date', + calendar_interval: '1d', + min_doc_count: 1, + }, + aggs: { + groups: { + filters: { + filters: { + suggestionEvents: { + bool: { + filter: { + term: { 'lens-ui-telemetry.type': 'suggestion' }, + }, + }, + }, + regularEvents: { + bool: { + must_not: { + term: { 'lens-ui-telemetry.type': 'suggestion' }, + }, + }, + }, + }, + }, + aggs: { + names: { + terms: { field: 'lens-ui-telemetry.name', size: 100 }, + aggs: { + sums: { sum: { field: 'lens-ui-telemetry.count' } }, + }, + }, + }, + }, + }, + }, + }; + + const metrics: AggregationSearchResponseWithTotalHitsAsInt< + unknown, + { + body: { aggs: typeof aggs }; + } + > = await callCluster('search', { + index: kibanaIndex, + rest_total_hits_as_int: true, + body: { + query: { + bool: { + filter: [ + { term: { type: 'lens-ui-telemetry' } }, + { range: { 'lens-ui-telemetry.date': { gte: 'now-90d/d' } } }, + ], + }, + }, + aggs, + }, + size: 0, + }); + + const byDateByType: Record> = {}; + const suggestionsByDate: Record> = {}; + + metrics.aggregations!.daily.buckets.forEach(daily => { + const byType: Record = byDateByType[daily.key] || {}; + daily.groups.buckets.regularEvents.names.buckets.forEach(bucket => { + byType[bucket.key] = (bucket.sums.value || 0) + (byType[daily.key] || 0); + }); + byDateByType[daily.key] = byType; + + const suggestionsByType: Record = suggestionsByDate[daily.key] || {}; + daily.groups.buckets.suggestionEvents.names.buckets.forEach(bucket => { + suggestionsByType[bucket.key] = + (bucket.sums.value || 0) + (suggestionsByType[daily.key] || 0); + }); + suggestionsByDate[daily.key] = suggestionsByType; + }); + + // Always delete old date because we don't report it + await callCluster('deleteByQuery', { + index: kibanaIndex, + waitForCompletion: true, + body: { + query: { + bool: { + filter: [ + { term: { type: 'lens-ui-telemetry' } }, + { range: { 'lens-ui-telemetry.date': { lt: 'now-90d/d' } } }, + ], + }, + }, + }, + }); + + return { + byDate: byDateByType, + suggestionsByDate, + }; +} + +export function telemetryTaskRunner(server: Server) { + return ({ taskInstance }: RunContext) => { + const { state } = taskInstance; + const callCluster = server.plugins.elasticsearch.getCluster('admin').callWithInternalUser; + + return { + async run() { + const kibanaIndex = server.config().get('kibana.index'); + + return Promise.all([ + getDailyEvents(kibanaIndex, callCluster), + getVisualizationCounts(callCluster, server.config()), + ]) + .then(([lensTelemetry, lensVisualizations]) => { + return { + state: { + runs: (state.runs || 0) + 1, + byDate: (lensTelemetry && lensTelemetry.byDate) || {}, + suggestionsByDate: (lensTelemetry && lensTelemetry.suggestionsByDate) || {}, + saved: lensVisualizations, + }, + runAt: getNextMidnight(), + }; + }) + .catch(errMsg => + server.log(['warning'], `Error executing lens telemetry task: ${errMsg}`) + ); + }, + }; + }; +} + +function getNextMidnight() { + return moment() + .add(1, 'day') + .startOf('day') + .toDate(); +} diff --git a/x-pack/legacy/plugins/lens/server/usage/types.ts b/x-pack/legacy/plugins/lens/server/usage/types.ts index 909566b09ac8f..364f265dacfb4 100644 --- a/x-pack/legacy/plugins/lens/server/usage/types.ts +++ b/x-pack/legacy/plugins/lens/server/usage/types.ts @@ -4,11 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface LensUsage { - visualization_types_overall: Record; - visualization_types_last_30_days: Record; - visualization_types_last_90_days: Record; - saved_total: number; - saved_last_30_days: number; - saved_last_90_days: number; +export interface LensTelemetryState { + runs: number; + byDate: Record>; + suggestionsByDate: Record>; + saved: LensVisualizationUsage; } + +export interface LensVisualizationUsage { + saved_overall: Record; + saved_30_days: Record; + saved_90_days: Record; + saved_overall_total: number; + saved_30_days_total: number; + saved_90_days_total: number; +} + +export interface LensClickUsage { + events_30_days: Record; + events_90_days: Record; + suggestion_events_30_days: Record; + suggestion_events_90_days: Record; +} + +export type LensUsage = LensVisualizationUsage & LensClickUsage; diff --git a/x-pack/legacy/plugins/lens/server/usage/visualization_counts.ts b/x-pack/legacy/plugins/lens/server/usage/visualization_counts.ts index 0558963374514..7c1dc5fca6d85 100644 --- a/x-pack/legacy/plugins/lens/server/usage/visualization_counts.ts +++ b/x-pack/legacy/plugins/lens/server/usage/visualization_counts.ts @@ -7,7 +7,7 @@ import { KibanaConfig } from 'src/legacy/server/kbn_server'; import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; import { SearchParams, SearchResponse } from 'elasticsearch'; -import { LensUsage } from './types'; +import { LensVisualizationUsage } from './types'; type ClusterSearchType = ( endpoint: 'search', @@ -20,7 +20,7 @@ type ClusterSearchType = ( export async function getVisualizationCounts( callCluster: ClusterSearchType, config: KibanaConfig -): Promise { +): Promise { const scriptedMetric = { scripted_metric: { // Each cluster collects its own type data in a key-value Map that looks like: @@ -82,11 +82,11 @@ export async function getVisualizationCounts( const buckets = results.aggregations.groups.buckets; return { - visualization_types_overall: buckets.overall.byType.value.types, - visualization_types_last_30_days: buckets.last30.byType.value.types, - visualization_types_last_90_days: buckets.last90.byType.value.types, - saved_total: buckets.overall.doc_count, - saved_last_30_days: buckets.last30.doc_count, - saved_last_90_days: buckets.last90.doc_count, + saved_overall: buckets.overall.byType.value.types, + saved_30_days: buckets.last30.byType.value.types, + saved_90_days: buckets.last90.byType.value.types, + saved_overall_total: buckets.overall.doc_count, + saved_30_days_total: buckets.last30.doc_count, + saved_90_days_total: buckets.last90.doc_count, }; } diff --git a/x-pack/legacy/plugins/xpack_main/xpack_main.d.ts b/x-pack/legacy/plugins/xpack_main/xpack_main.d.ts index 8ae2801dededb..2a197811cc032 100644 --- a/x-pack/legacy/plugins/xpack_main/xpack_main.d.ts +++ b/x-pack/legacy/plugins/xpack_main/xpack_main.d.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import KbnServer from 'src/legacy/server/kbn_server'; import { Feature, FeatureWithAllOrReadPrivileges } from '../../../plugins/features/server'; import { XPackInfo, XPackInfoOptions } from './server/lib/xpack_info'; export { XPackFeature } from './server/lib/xpack_info'; diff --git a/x-pack/test/api_integration/apis/lens/index.ts b/x-pack/test/api_integration/apis/lens/index.ts index e0dac4a9ad296..6d60f6d2d5ac2 100644 --- a/x-pack/test/api_integration/apis/lens/index.ts +++ b/x-pack/test/api_integration/apis/lens/index.ts @@ -10,5 +10,6 @@ export default function lensApiIntegrationTests({ loadTestFile }: FtrProviderCon describe('Lens', () => { loadTestFile(require.resolve('./existing_fields')); loadTestFile(require.resolve('./field_stats')); + loadTestFile(require.resolve('./telemetry')); }); } diff --git a/x-pack/test/api_integration/apis/lens/telemetry.ts b/x-pack/test/api_integration/apis/lens/telemetry.ts new file mode 100644 index 0000000000000..5bba8848fe1d9 --- /dev/null +++ b/x-pack/test/api_integration/apis/lens/telemetry.ts @@ -0,0 +1,216 @@ +/* + * 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 moment from 'moment'; +import expect from '@kbn/expect'; +import { KibanaConfig } from 'src/legacy/server/kbn_server'; +import { Client, SearchParams } from 'elasticsearch'; + +import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +import { getDailyEvents } from '../../../../legacy/plugins/lens/server/usage/task'; +import { getVisualizationCounts } from '../../../../legacy/plugins/lens/server/usage/visualization_counts'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es: Client = getService('es'); + const callCluster: CallCluster = (((path: 'search', searchParams: SearchParams) => { + return es[path].call(es, searchParams); + }) as unknown) as CallCluster; + + async function assertExpectedSavedObjects(num: number) { + // Make sure that new/deleted docs are available to search + await es.indices.refresh({ + index: '.kibana', + }); + + const { count } = await es.count({ + index: '.kibana', + q: 'type:lens-ui-telemetry', + }); + + expect(count).to.be(num); + } + + describe('lens telemetry', () => { + beforeEach(async () => { + await es.deleteByQuery({ + index: '.kibana', + q: 'type:lens-ui-telemetry', + waitForCompletion: true, + refresh: 'wait_for', + }); + }); + + afterEach(async () => { + await es.deleteByQuery({ + index: '.kibana', + q: 'type:lens-ui-telemetry', + waitForCompletion: true, + refresh: 'wait_for', + }); + }); + + it('should do nothing on empty post', async () => { + await supertest + .post('/api/lens/telemetry') + .set(COMMON_HEADERS) + .send({ + events: {}, + suggestionEvents: {}, + }) + .expect(200); + + await assertExpectedSavedObjects(0); + }); + + it('should write a document per results', async () => { + await supertest + .post('/api/lens/telemetry') + .set(COMMON_HEADERS) + .send({ + events: { + '2019-10-13': { loaded: 5, dragged: 2 }, + '2019-10-14': { loaded: 1 }, + }, + suggestionEvents: { + '2019-10-13': { switched: 2 }, + }, + }) + .expect(200); + + await assertExpectedSavedObjects(4); + }); + + it('should delete older telemetry documents while running', async () => { + const olderDate = moment() + .subtract(100, 'days') + .valueOf(); + await es.index({ + index: '.kibana', + type: '_doc', + body: { + type: 'lens-ui-telemetry', + 'lens-ui-telemetry': { + date: olderDate, + name: 'load', + type: 'regular', + count: 5, + }, + }, + refresh: 'wait_for', + }); + + const result = await getDailyEvents('.kibana', callCluster); + + expect(result).to.eql({ + byDate: {}, + suggestionsByDate: {}, + }); + + await assertExpectedSavedObjects(0); + }); + + it('should aggregate the individual events into daily totals by type', async () => { + // Dates are set to midnight in the aggregation, so let's make this easier for the test + const date1 = moment() + .utc() + .subtract(10, 'days') + .startOf('day') + .valueOf(); + const date2 = moment() + .utc() + .subtract(20, 'days') + .startOf('day') + .valueOf(); + + function getEvent(name: string, date: number, type = 'regular') { + return { + type: 'lens-ui-telemetry', + 'lens-ui-telemetry': { + date, + name, + type, + count: 5, + }, + }; + } + + await es.bulk({ + refresh: 'wait_for', + body: [ + { index: { _index: '.kibana' } }, + getEvent('load', date1), + { index: { _index: '.kibana' } }, + getEvent('load', date1), + { index: { _index: '.kibana' } }, + getEvent('load', date1), + { index: { _index: '.kibana' } }, + getEvent('load', date2), + { index: { _index: '.kibana' } }, + getEvent('revert', date1, 'suggestion'), + ], + }); + + const result = await getDailyEvents('.kibana', callCluster); + + expect(result).to.eql({ + byDate: { + [date1]: { + load: 15, + }, + [date2]: { + load: 5, + }, + }, + suggestionsByDate: { + [date1]: { + revert: 5, + }, + [date2]: {}, + }, + }); + + await assertExpectedSavedObjects(5); + }); + + it('should collect telemetry on saved visualization types with a painless script', async () => { + const esArchiver = getService('esArchiver'); + + await esArchiver.loadIfNeeded('lens/basic'); + + const results = await getVisualizationCounts(callCluster, { + // Fake KibanaConfig service + get() { + return '.kibana'; + }, + has: () => false, + } as KibanaConfig); + + expect(results).to.have.keys([ + 'saved_overall', + 'saved_30_days', + 'saved_90_days', + 'saved_overall_total', + 'saved_30_days_total', + 'saved_90_days_total', + ]); + + expect(results.saved_overall).to.eql({ + lnsMetric: 1, + }); + expect(results.saved_overall_total).to.eql(1); + + await esArchiver.unload('lens/basic'); + }); + }); +}; diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index c511fcd997217..3346f2ff77036 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -6,10 +6,7 @@ import _ from 'lodash'; import expect from '@kbn/expect'; -import { Client, SearchParams } from 'elasticsearch'; -import { KibanaConfig } from 'src/legacy/server/kbn_server'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { getVisualizationCounts } from '../../../../legacy/plugins/lens/server/usage/visualization_counts'; // eslint-disable-next-line import/no-default-export export default function({ getService, getPageObjects, ...rest }: FtrProviderContext) { @@ -113,34 +110,5 @@ export default function({ getService, getPageObjects, ...rest }: FtrProviderCont // legend item(s), so we're using a class selector here. expect(await find.allByCssSelector('.echLegendItem')).to.have.length(3); }); - - it('should collect telemetry on saved visualization types with a painless script', async () => { - const es: Client = getService('es'); - const callCluster = (path: 'search', searchParams: SearchParams) => - es[path].call(es, searchParams); - - const results = await getVisualizationCounts(callCluster, { - // Fake KibanaConfig service - get(key: string) { - return '.kibana'; - }, - has: () => false, - } as KibanaConfig); - - expect(results).to.have.keys([ - 'visualization_types_overall', - 'visualization_types_last_30_days', - 'visualization_types_last_90_days', - 'saved_total', - 'saved_last_30_days', - 'saved_last_90_days', - ]); - - expect(results.visualization_types_overall).to.eql({ - lnsMetric: 1, - bar_stacked: 1, - }); - expect(results.saved_total).to.eql(2); - }); }); } From 5023131088ee42cc66ed197ce14c2f45bd88603b Mon Sep 17 00:00:00 2001 From: Chris Davies Date: Tue, 15 Oct 2019 23:02:18 -0400 Subject: [PATCH 11/48] [Lens] Add date histogram interval to column name (#48271) --- .../expression.tsx | 11 +- .../visualization.tsx | 7 - .../public/indexpattern_plugin/auto_date.ts | 86 ++++++++ .../indexpattern_plugin/indexpattern.test.ts | 2 +- .../indexpattern_suggestions.test.tsx | 4 +- .../definitions/date_histogram.test.tsx | 4 +- .../operations/definitions/date_histogram.tsx | 17 +- .../public/indexpattern_plugin/plugin.tsx | 2 + .../rename_columns.test.ts | 186 +++++++++++------- .../indexpattern_plugin/rename_columns.ts | 46 ++++- .../indexpattern_plugin/to_expression.ts | 14 +- .../__snapshots__/xy_expression.test.tsx.snap | 14 +- .../xy_visualization_plugin/xy_expression.tsx | 4 +- .../es_archives/lens/basic/data.json.gz | Bin 2957 -> 3004 bytes .../es_archives/lens/basic/mappings.json | 153 +++++++++++++- .../es_archives/lens/reporting/data.json.gz | Bin 4464 -> 4542 bytes .../es_archives/lens/reporting/mappings.json | 144 +++++++++++++- 17 files changed, 563 insertions(+), 131 deletions(-) create mode 100644 x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx index d7d445b3f8df5..23b49ecc168cc 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx @@ -17,7 +17,6 @@ import { VisualizationContainer } from '../visualization_container'; export interface DatatableColumns { columnIds: string[]; - labels: string[]; } interface Args { @@ -95,11 +94,6 @@ export const datatableColumns: ExpressionFunction< multi: true, help: '', }, - labels: { - types: ['string'], - multi: true, - help: '', - }, }, fn: function fn(_context: unknown, args: DatatableColumns) { return { @@ -138,10 +132,11 @@ function DatatableComponent(props: DatatableProps & { formatFactory: FormatFacto className="lnsDataTable" data-test-subj="lnsDataTable" columns={props.args.columns.columnIds - .map((field, index) => { + .map(field => { + const col = firstTable.columns.find(c => c.id === field); return { field, - name: props.args.columns.labels[index], + name: (col && col.name) || '', }; }) .filter(({ field }) => !!field)} diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx index 18a6d112e14d6..ff809e9e4827c 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx @@ -212,13 +212,6 @@ export const datatableVisualization: Visualization< function: 'lens_datatable_columns', arguments: { columnIds: operations.map(o => o.columnId), - labels: operations.map( - o => - o.operation.label || - i18n.translate('xpack.lens.datatable.na', { - defaultMessage: 'N/A', - }) - ), }, }, ], diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts new file mode 100644 index 0000000000000..359c6d7c35c3a --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TimeBuckets } from 'ui/time_buckets'; +import dateMath from '@elastic/datemath'; +import { + ExpressionFunction, + KibanaContext, +} from '../../../../../../src/plugins/expressions/common'; + +interface LensAutoDateProps { + aggConfigs: string; +} + +export function getAutoInterval(ctx?: KibanaContext | null) { + if (!ctx || !ctx.timeRange) { + return; + } + + const { timeRange } = ctx; + const buckets = new TimeBuckets(); + buckets.setInterval('auto'); + buckets.setBounds({ + min: dateMath.parse(timeRange.from), + max: dateMath.parse(timeRange.to, { roundUp: true }), + }); + + return buckets.getInterval(); +} + +/** + * Convert all 'auto' date histograms into a concrete value (e.g. 2h). + * This allows us to support 'auto' on all date fields, and opens the + * door to future customizations (e.g. adjusting the level of detail, etc). + */ +export const autoDate: ExpressionFunction< + 'lens_auto_date', + KibanaContext | null, + LensAutoDateProps, + string +> = { + name: 'lens_auto_date', + aliases: [], + help: '', + context: { + types: ['kibana_context', 'null'], + }, + args: { + aggConfigs: { + types: ['string'], + default: '""', + help: '', + }, + }, + fn(ctx: KibanaContext, args: LensAutoDateProps) { + const interval = getAutoInterval(ctx); + + if (!interval) { + return args.aggConfigs; + } + + const configs = JSON.parse(args.aggConfigs) as Array<{ + type: string; + params: { interval: string }; + }>; + + const updatedConfigs = configs.map(c => { + if (c.type !== 'date_histogram' || !c.params || c.params.interval !== 'auto') { + return c; + } + + return { + ...c, + params: { + ...c.params, + interval: interval.expression, + }, + }; + }); + + return JSON.stringify(updatedConfigs); + }, +}; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts index 70a0372c028d6..845cfa29d5724 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts @@ -272,7 +272,7 @@ describe('IndexPattern Data Source', () => { metricsAtAllLevels=false partialRows=false includeFormatHints=true - aggConfigs='[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1d\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}}]' | lens_rename_columns idMap='{\\"col-0-col1\\":\\"col1\\",\\"col-1-col2\\":\\"col2\\"}'" + aggConfigs={lens_auto_date aggConfigs='[{\\"id\\":\\"col1\\",\\"enabled\\":true,\\"type\\":\\"count\\",\\"schema\\":\\"metric\\",\\"params\\":{}},{\\"id\\":\\"col2\\",\\"enabled\\":true,\\"type\\":\\"date_histogram\\",\\"schema\\":\\"segment\\",\\"params\\":{\\"field\\":\\"timestamp\\",\\"useNormalizedEsInterval\\":true,\\"interval\\":\\"1d\\",\\"drop_partials\\":false,\\"min_doc_count\\":0,\\"extended_bounds\\":{}}}]'} | lens_rename_columns idMap='{\\"col-0-col1\\":{\\"label\\":\\"Count of Documents\\",\\"dataType\\":\\"number\\",\\"isBucketed\\":false,\\"operationType\\":\\"count\\",\\"id\\":\\"col1\\"},\\"col-1-col2\\":{\\"label\\":\\"Date\\",\\"dataType\\":\\"date\\",\\"isBucketed\\":true,\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"timestamp\\",\\"params\\":{\\"interval\\":\\"1d\\"},\\"id\\":\\"col2\\"}}'" `); }); }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx index 08993c15f5c60..ff6911bd25f84 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx @@ -1056,7 +1056,7 @@ describe('IndexPattern Data Source suggestions', () => { { columnId: 'id1', operation: { - label: 'Date histogram of timestamp', + label: 'timestamp', dataType: 'date', isBucketed: true, scale: 'interval', @@ -1132,7 +1132,7 @@ describe('IndexPattern Data Source suggestions', () => { { columnId: 'id1', operation: { - label: 'Date histogram of timestamp', + label: 'timestamp', dataType: 'date', isBucketed: true, scale: 'interval', diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx index e9ea50c9273ee..f984597a8eb4b 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.test.tsx @@ -136,7 +136,7 @@ describe('date_histogram', () => { expect(column.params.interval).toEqual('auto'); }); - it('should create column object with manual interval for non-primary time fields', () => { + it('should create column object with auto interval for non-primary time fields', () => { const column = dateHistogramOperation.buildColumn({ columns: {}, suggestedPriority: 0, @@ -150,7 +150,7 @@ describe('date_histogram', () => { searchable: true, }, }); - expect(column.params.interval).toEqual('d'); + expect(column.params.interval).toEqual('auto'); }); it('should create column object with restrictions', () => { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx index fb91ac593650c..89c4899bb8e56 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/definitions/date_histogram.tsx @@ -29,13 +29,6 @@ const FixedEuiRange = (EuiRange as unknown) as React.ComponentType< } >; -function ofName(name: string) { - return i18n.translate('xpack.lens.indexPattern.dateHistogramOf', { - defaultMessage: 'Date histogram of {name}', - values: { name }, - }); -} - function supportsAutoInterval(fieldName: string, indexPattern: IndexPattern): boolean { return indexPattern.timeFieldName ? indexPattern.timeFieldName === fieldName : false; } @@ -67,7 +60,7 @@ export const dateHistogramOperation: OperationDefinition { return { ...oldColumn, - label: ofName(field.name), + label: field.name, sourceField: field.name, params: { ...oldColumn.params, @@ -172,8 +165,6 @@ export const dateHistogramOperation: OperationDefinition - {fieldAllowsAutoInterval && ( + {!intervalIsRestricted && ( renameColumns); + interpreter.functionsRegistry.register(() => autoDate); } stop() {} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.test.ts index 641b1ceb431fb..74de89d584d2b 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.test.ts @@ -16,43 +16,49 @@ describe('rename_columns', () => { }; const idMap = { - a: 'b', - b: 'c', + a: { + id: 'b', + label: 'Austrailia', + }, + b: { + id: 'c', + label: 'Boomerang', + }, }; expect(renameColumns.fn(input, { idMap: JSON.stringify(idMap) }, {})).toMatchInlineSnapshot(` -Object { - "columns": Array [ - Object { - "id": "b", - "name": "A", - }, - Object { - "id": "c", - "name": "B", - }, - ], - "rows": Array [ - Object { - "b": 1, - "c": 2, - }, - Object { - "b": 3, - "c": 4, - }, - Object { - "b": 5, - "c": 6, - }, - Object { - "b": 7, - "c": 8, - }, - ], - "type": "kibana_datatable", -} -`); + Object { + "columns": Array [ + Object { + "id": "b", + "name": "Austrailia", + }, + Object { + "id": "c", + "name": "Boomerang", + }, + ], + "rows": Array [ + Object { + "b": 1, + "c": 2, + }, + Object { + "b": 3, + "c": 4, + }, + Object { + "b": 5, + "c": 6, + }, + Object { + "b": 7, + "c": 8, + }, + ], + "type": "kibana_datatable", + } + `); }); it('should keep columns which are not mapped', () => { @@ -63,41 +69,87 @@ Object { }; const idMap = { - b: 'c', + b: { id: 'c', label: 'Catamaran' }, }; expect(renameColumns.fn(input, { idMap: JSON.stringify(idMap) }, {})).toMatchInlineSnapshot(` -Object { - "columns": Array [ - Object { - "id": "a", - "name": "A", - }, - Object { - "id": "c", - "name": "B", - }, - ], - "rows": Array [ - Object { - "a": 1, - "c": 2, - }, - Object { - "a": 3, - "c": 4, - }, - Object { - "a": 5, - "c": 6, - }, - Object { - "a": 7, - "c": 8, - }, - ], - "type": "kibana_datatable", -} -`); + Object { + "columns": Array [ + Object { + "id": "a", + "name": "A", + }, + Object { + "id": "c", + "name": "Catamaran", + }, + ], + "rows": Array [ + Object { + "a": 1, + "c": 2, + }, + Object { + "a": 3, + "c": 4, + }, + Object { + "a": 5, + "c": 6, + }, + Object { + "a": 7, + "c": 8, + }, + ], + "type": "kibana_datatable", + } + `); + }); + + it('should rename date histograms', () => { + const input: KibanaDatatable = { + type: 'kibana_datatable', + columns: [{ id: 'a', name: 'A' }, { id: 'b', name: 'banana per 30 seconds' }], + rows: [{ a: 1, b: 2 }, { a: 3, b: 4 }, { a: 5, b: 6 }, { a: 7, b: 8 }], + }; + + const idMap = { + b: { id: 'c', label: 'Apple', operationType: 'date_histogram', sourceField: 'banana' }, + }; + + expect(renameColumns.fn(input, { idMap: JSON.stringify(idMap) }, {})).toMatchInlineSnapshot(` + Object { + "columns": Array [ + Object { + "id": "a", + "name": "A", + }, + Object { + "id": "c", + "name": "Apple per 30 seconds", + }, + ], + "rows": Array [ + Object { + "a": 1, + "c": 2, + }, + Object { + "a": 3, + "c": 4, + }, + Object { + "a": 5, + "c": 6, + }, + Object { + "a": 7, + "c": 8, + }, + ], + "type": "kibana_datatable", + } + `); }); }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.ts index 4a54bcad56163..87e51cb62ba1d 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/rename_columns.ts @@ -5,12 +5,19 @@ */ import { i18n } from '@kbn/i18n'; -import { ExpressionFunction, KibanaDatatable } from 'src/legacy/core_plugins/interpreter/public'; +import { + ExpressionFunction, + KibanaDatatable, + KibanaDatatableColumn, +} from 'src/legacy/core_plugins/interpreter/public'; +import { IndexPatternColumn } from './operations'; interface RemapArgs { idMap: string; } +export type OriginalColumn = { id: string } & IndexPatternColumn; + export const renameColumns: ExpressionFunction< 'lens_rename_columns', KibanaDatatable, @@ -35,18 +42,19 @@ export const renameColumns: ExpressionFunction< types: ['kibana_datatable'], }, fn(data: KibanaDatatable, { idMap: encodedIdMap }: RemapArgs) { - const idMap = JSON.parse(encodedIdMap) as Record; + const idMap = JSON.parse(encodedIdMap) as Record; + return { type: 'kibana_datatable', rows: data.rows.map(row => { const mappedRow: Record = {}; Object.entries(idMap).forEach(([fromId, toId]) => { - mappedRow[toId] = row[fromId]; + mappedRow[toId.id] = row[fromId]; }); Object.entries(row).forEach(([id, value]) => { if (id in idMap) { - mappedRow[idMap[id]] = value; + mappedRow[idMap[id].id] = value; } else { mappedRow[id] = value; } @@ -54,10 +62,32 @@ export const renameColumns: ExpressionFunction< return mappedRow; }), - columns: data.columns.map(column => ({ - ...column, - id: idMap[column.id] ? idMap[column.id] : column.id, - })), + columns: data.columns.map(column => { + const mappedItem = idMap[column.id]; + + if (!mappedItem) { + return column; + } + + return { + ...column, + id: mappedItem.id, + name: getColumnName(mappedItem, column), + }; + }), }; }, }; + +function getColumnName(originalColumn: OriginalColumn, newColumn: KibanaDatatableColumn) { + if (originalColumn && originalColumn.operationType === 'date_histogram') { + const fieldName = originalColumn.sourceField; + + // HACK: This is a hack, and introduces some fragility into + // column naming. Eventually, this should be calculated and + // built more systematically. + return newColumn.name.replace(fieldName, originalColumn.label); + } + + return originalColumn.label; +} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts index 8045ab89b63d4..f4fa9b3862733 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts @@ -8,10 +8,10 @@ import _ from 'lodash'; import { IndexPatternColumn } from './indexpattern'; import { operationDefinitionMap } from './operations'; import { IndexPattern, IndexPatternPrivateState } from './types'; +import { OriginalColumn } from './rename_columns'; function getExpressionForLayer( indexPattern: IndexPattern, - layerId: string, columns: Record, columnOrder: string[] ) { @@ -34,10 +34,13 @@ function getExpressionForLayer( (currentIdMap, [colId], index) => { return { ...currentIdMap, - [`col-${index}-${colId}`]: colId, + [`col-${index}-${colId}`]: { + ...columns[colId], + id: colId, + }, }; }, - {} as Record + {} as Record ); return `esaggs @@ -45,7 +48,9 @@ function getExpressionForLayer( metricsAtAllLevels=false partialRows=false includeFormatHints=true - aggConfigs='${JSON.stringify(aggs)}' | lens_rename_columns idMap='${JSON.stringify(idMap)}'`; + aggConfigs={lens_auto_date aggConfigs='${JSON.stringify( + aggs + )}'} | lens_rename_columns idMap='${JSON.stringify(idMap)}'`; } return null; @@ -55,7 +60,6 @@ export function toExpression(state: IndexPatternPrivateState, layerId: string) { if (state.layers[layerId]) { return getExpressionForLayer( state.indexPatterns[state.layers[layerId].indexPatternId], - layerId, state.layers[layerId].columns, state.layers[layerId].columnOrder ); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap index 8a79240159b2e..473ee16ffe0c3 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap @@ -135,7 +135,7 @@ exports[`xy_expression XYChart component it renders area 1`] = ` position="bottom" showGridLines={false} tickFormat={[Function]} - title="" + title="c" /> 1 || data.tables[layers[0].layerId].columns.length > 2; const shouldRotate = isHorizontalChart(layers); + const xTitle = (xAxisColumn && xAxisColumn.name) || args.xTitle; + return ( xAxisFormatter.convert(d)} diff --git a/x-pack/test/functional/es_archives/lens/basic/data.json.gz b/x-pack/test/functional/es_archives/lens/basic/data.json.gz index 992bf7c85e9fd7046b0fe3a3cf63b0297070baea..a5079d92e77f082ca2a30559e9a8c1bbcace01ff 100644 GIT binary patch delta 2849 zcmch}=RXt<0|4;Lp1+Q>vt=IQY;su{XO%cJ84-78KN)wfGjd&4XVr;AWJXq0I(M?k zo*5z8L^A3!?s@k-|H1R-`}w8`k_8D83UNQ070MceZ`o*IzUZqdJZaDy4YpKz?ti-Y z{FUmnAiePMh;{(eY-fgr8MX!p&n)I=;ai;%#Iv7FR*oa~Ju(dTOVvs-0oi)CI@hJF5(4Mo*&0q*?9ygrU297zu;=4QV14H3it8-YGT4N};|yH)phOAfIPEbknif$vT7!{#I`Wo1Z@~7AorvECFPj z8xmYo!n zdlMeeFr35g)k*1=lp+Wk$8~5;L%@96?i*fB?P1ITw`VoGfM%iL1(d#yD;_+rD(6yh zz%=#u=T9oO#-y+}$Ou*tGgvw!Sy%MKVIpx-!7>F}%y#3CX!J7$l=vi<5K>XD^lF$T z@jGd=ASV#8AJq}afgQ+w?j*D=vn2p!e8utjvVdYC7GpOzSsnJj=n72Lr@$$ zsB;RAC@`LGJ0nTf*J@Mf?Lj-UZ*;{JgNkIWDUWy*CXR3oi(Nj;*wL zA+4Lum2M;A)(GDjQbZM_Noya^7(wLfriO}^AzBjvO%T}dvGq^e9>)W43h z@zJ6(HUU^CRf+dZT;6Etp3+p3$F8T?H|y^^O@-7Ujs9UtK$(I~QQ;a-GEeHWQ$B}u z#$HCt6o@6zmC97XgmbJaNjwSff150B1p5R-1w&GKk&rty(n;KyQsc#2A(&#vsyr+! zfC3Qvi^Ri`O07!B{42&K(+b5%8o!cPe~o7k`Cuxz_7$8L09|0RVf`QHaJK z^*Hr<_0@dhm4|={pR!!Aha&s(uTFtWmToGM#uUJ9%A?L$z&7Vd^}HhL;wtBe+%V*o zvm7gpAwn@qX~I;oUfa*Jztp{lt}K;-$Wj6HmC87*2xlNX;rP2_nbCwh#V4Qm#dEQm zhq1$LyS^DpW-tMewFoRiXA@_sSt&%k*F16kd3tBf;j{5D=u||BN^tRF>MEGr(!o3{ zbNQ?N;Oy#!&E4p~Y{_s_udNj1>(2>j*SAdBIt{kS5SM0e_w}+(1JGiAn(gq};&?-8 zd*;a1+wckP>m-K>#s7l+U;l5bl-lB(c06IJRs~yOv7dITz-BN)QK*R_6tcuEeeK(z zckAb{Cyo1egT$=t$MrMameBLHBtf(j{#qhfh{TL-BRpKPhd6rvbuFSF9nAEEYjqYp z@&-4lXtNH^O00M{qHux2&6tpw^Uh=WsV~~Ou5-s9TUreM*uC8~6A}%r)zO(7`uPDU z!03W?>y{|_X&@okLOf*eg_nN0d2nyCALzgH9p}`k@bc7^mA>6o@o?&=ju18|q3?G< zA^T21R6q!I3@Y_SEa(aJR@rzYTVBYj({|ic#d>_Pz2OQ7Q zv63+#X+1`IgEP1-rTBLkb7O#eHh!=0ezUb4f4Sk+F616Y>X+OwY&MXafjHrOdHb{l zTjt8Oozc{`;Bad3PLV~m(FozyK0C)15Cgnz}-(lUh zi>j}#_U3r8p3XFMp464>l+q*t{5-EA`_YHpqmHUKXp|xqoWz8XG*8?b9mBdq;A#H zdIE!0l(e&Y8I|{Nnqg|A@KwR&+5LY~*IZHs_^xXmp;fd?C`A$1Jd^JWBwy$ui&PCsje;CG->J8%ki}oH+A9-Y8v^O8V zP7R&-X3UjSaxt~W7ff*!eCaj= zg@$~IuKfdb{_9f%h5D2q>GF-VTl5&>)s)fx!l#4vG@{Y1qdkikh?}MtV8GU+Z5mC< zw9>M?_xxjjBCqUd55!NaAaF=|xUqa_K;5u54;r>g=I4B6PYsWz7E*Ux$z+ru?IX(> z#=^})q(YqcYpZ|{J^x?iu7Sp4WVgmN$|q}yRmPFBzhoQ#;i$@~IJ2(zd<@|#MRkob z)w2DwyO|(+KgBM+Wr_3hiPV}PU2XZ=c@X7^{yDaBqzM)54WW3}IYe@&`{$aF6Rg8G ztll!so1CgHrn8lJRw4}8+e zV(@{D4BULs`_I@=!~9EreU%tT5VCKxEi9sDMl@eMMrv&$rqi!W+1~B2xy)4fg_@a` z2SqEEktMTM|H6v6!tz!*0!+yR?#d@!lmK0gW%Y?ZqU!1q#v;09?hl?tyyi$V#WqyC z4#Eb=q{zpAcVDV+NSllG{zzwcdpuTwXeaHf48^rB(1lC*$sS`ChpTC zrN*Vbz?B`sl9sT0&@Ev_LTBBJE{{?tO79I+oq)DO5H%9y({G@6cCcLEj7YKXi3V&y znrzR;C9Jil5oE^}MMB`;?h#!LCeC`_e@VrO5~op`@o%{O+t_|o_5eETFYM@QN^<$k zaru|&v33}M^Mdc*llz$(2F(>t|Nf{ucGskXTY5O(L^sc-?8Ht3$o=q!#F4AKIYAS| z<;BM0&L33xv&wg=5@h~{QKo}MdFDY`fnQwo5Vkt#W3z@9sdAf}qh*uhiN^*}iV25A z+azVRsgp-iQrqIgeC*=I+G&iOgKv_$)R#vZ;(G-U!9*K=$?O~EK#RnhYx1Kl0@mpnReGqcQ~=;V+y-uU delta 2823 zcmch}=Rec|0|0O%q9}W>vvt>ynUQt&Str}st0*Ky_}MF@BP)_!oIP@!QOX%{_BylB zIoWcYF3-E?`465q-_IAroz9&pEFH7cCcQ9UDN~t$A126?4V^C}SR;oj>@G+WGJ~)E z1_LKUzEEA7=zpyb1zvzq;<{ia>7iyE3;RipRn_zlw0P+643n<9xwL|1o_>}-NPN5k z8J_FpSMzrFW{NQr<5!!^va0s@vh!*NBuiPWwe<0kf+*j4iSLJL+NtYT?FkAOZ^Ozth3SoHlBd!F=Ld3e|sEkG}ga|eLz$%;o7o8)}@f|!)Z<0 zS&?-xDzOs)Z0VP5-k4QC=)b9bT&scS%zf|=$L&qmSRAOz7Wo0&EbiN4e z!AK|=ZE_F4spYBrCX1J-cO?`P%xjqSXM|#)@+r-NK`P9T$IQbP~Mcyu^kcHWSl~i z5iOSW2F_AvL61d4jXJR6WtDdfJQ=lQU_lY!>wH)GVIn#GZUQWcj4ZjY`#bMdBQ@1B zSS5j$8@bmVX~+%gMWLMA+P6aMGxjS2PZn^_8@wCdEL4^pl4E8^4Br^^jH9l|V5!%s zCQ6yupffn?8!@0aM8fJF(|qf3>T(WL`K)B0bfe_ao0X31(zSOx?`+IS8C7(gDN*h7 z62_c$TI?;itn7Y9T;uD~V?_+tn^RjpiDUzyYw$YAUn&E1upz5S`Aiv=H2NB_Lzxz~ z0mVstqBkxaKxxvOxlzV_zZPd6>O_k@Y@MS&V|sQbz;+~r!f7@?ts)3!<22u@4QD## z3#`kHRV;oqzg+QkAv;Va_le+zy|(DIqyrVwSigKgPIMZBVi_!#I1uT9lH$N>cY%k5 zt#>g?SW&CwY=~rzd^X|91oLYjxa=J>zmG2!1ydZhn5)(zK5XfcT2h|K<<##Jxww5^ zWj@O*XdR}VSUNan<)}^Uo#Omdd+X}1=Qx}B@_=G3+I2qhanak#Jy=ALqM|wpS3I0^ z#aR33nVkimBiP8I>c0U0>;G@$50;LWcqNxsJ41jh=VgNc!DBB)vJ($pQ~R@=(fME7 zc1$!bOqX)sHV7}6LAKC!V<`(^jW!9V?Cr7RcKh5+TQUH$m5dOxiO6e51? z@DT4N=QFb_k~Bi(t@NBbL2sO~ShTm@|JkqN>2?Vf|0>dOII3f zc5Gz|*g9aVoUq-jD!Lnh!}Uo&m7%!@B^1oKLDyYB#b7%B;|-@|iNq^%2X5V5Gex^@ zc6NOn@;E`p!lkP(IcvClHAHgI-bNwc*xHTwW6kj$vHNJ!)P1d$c9hI{-cN(}bM5b1 z7U0x>L3loQ(^*tG({0dW-a0GtySFyalz%3aTBb6%`u=wMWO^0}U?;2dYR=tM{S|vM z0>Se%2Xnf%wV+4L7QV^$I#YayBos)L{`73a`CF=*S1SwUI(=QUaqT^@7~hdN{u zM29?q*15q#B?xtNsBsNL^_tEN0iKaYo}1FCkBFi6etHP4ttbq~284q)0|d_UXmOaw zH0Mp*gy0`HOal%sCbZzEFxu~Ny$uio@3LANvnm6OyMOmmK}`27h3%67kP@?KQ(jwJV;MF1UAn$>XcROGP7m=JRBn48YS>w_0i!W5wX6X!U_oaAbty~rZ>JC;-V$gr8txAy%1OKX~o3x{vcM~5K z>@RU&%^A39e~%b@JM*`OY|m*D>&2e$k;&8n@HU|?@Z(?dFvky0$5eOIQi)ZrAEB(S zr8=`TK`fW)8#XQbQmEylw^Z~5V4E5Ioh5Ru@d73_Dn0FS@lqNE=G0jMdo(D-8(d~C zaO{c1{4(SZ1I4yl?@LW+9lnTK%6<=l4ShIi`y^eqP8VvI&Im|2#`c|yCXT{Oyj((f z@|PD2`Pp%9&%5SiV+~#Sf?jNnx={T#RKpX?2Mf6Hn1>eLrs?e*i+&EXXOnH!Mk56a zD(Uvg&`0CtaFd@ze`^S|eAX(m5WAa0(_KNfNBbu<3eB&|T#C%0v zo*n_4iD}u)n^(=&df*h+-+V3jDjCmo^&XcJ8>nHoNzU&|4hB;f(XMEu&s_tZi%Ww zbI8_M$(L~uFuTJXa+9n%Invj*l&Ug=sr^jCpROASSP_I%w=|kaL;#{i(Wm3?dVwlx z-P#{Tbtz3=zO51AfnUg4w_V@CLJtKmC1XZp$^A{vYs~7bI`wikjsD7BJKNh)9wFmt ziA<;rY{G<-cO!xa2V3P`i?}N%fMrZSTTDpghrYX)5TKY3BNbJ8n^3eOr;;{!v?jj$ zMS}>Bts2rdhjU_pDpMZxwKOLFh9#uj9|-N&ATLjRYthFo7^2>0%*0{9qVX`Gqs`Yf z@Y+&t1n7{rk-0gUBl0u+HnGpQ(P+PUyH8&=VQ$5ZCvw5YWpB;!B3}c~#X97HD8Yyi z?s(=ndq*d$|MXd1wha0xE>p?}Jw4v{ z{V(3e&~>zz5V&w zVdmQDf^(q6?N)+lEbYVQgEoNQ%A(-Ss0wn^|I2m<#d$X|rC}edDzzAIF3;Ghv3-t| zcCL-yv}5`V-q`r!J2!QTTLm4ndXtAlCES`j{7t)xlB**Aztwq% z0tSvudN<{{SNd~FYJ{@(oWEr3DvUk=3b92!Z+Cbd62Rsn-ll!i8L60h(1>|0oALgD zfJu3pI@NS#^tX>&e{a7po8%$5DYc_YsLNo}7e25skmiP_w`uV#(Iyx+4XGy22o$b@ zR*}@1QYiDaX*XE~6phZngtA|$ieMY6J8{^RkOUD|N+!9?36GiEey_o|PHM9cRXDa3 zLcMDUrRXz@UDi=}64djh_^f2XvY9;zdKKRX&1i+HR8vG8Mf!IVVxR(cfXXC{b=7pQ zkM!HwV)?WkH@KXnOcIfksLfA?4@%(l*nUqDXC_pBd zXiMmcGM>>`EJ?JOpEgga3(L9-x4#2tkuJibS?`h-CWMvOH)hC^0~n!;&}r6#)53_H zOu%mu`7BQXsyW8`NL)<;riOrA;j&3EcDlBC2Hh>q7(U%;Wi))Z9Ve6^(U{#>B;$XU7O znW-kqUKw9l&uiWg71d~^cUD@*X~QP@N7Dn`RCYY2>*R7^g5%q`xOMIGxB2?@OfEj$ue)wJ$|H1@F46l%&xEK?vg6{- z#XEMgk4YJW>LK$O#LBa|=%?1-%nsDM7)1ZkR>j3RVslD7%#84h$$wLH9`-GV9^JE_ z65RZn^7d3GeJs5kR*yOmFANpEe2Vf-JXTkn>hwN!?*^8a{Hr>3D)x-8u6t zp?N@R8tZ-(motibe7Df}xNOWVRru^+&1}H7VwB4(NgX?RT&fX#R|d4N&ucV(4!)Hu zM~VMxKC>gt8uKVu<=IpFF%6Tz{awsA#wjcKsGeOYmgQ1rNH*sYb6%{oR{h?(d>NV6 zAfR>+j!8Eb^JoFb(O>$sKm}X0scr|uNX@Uyb($MX`ApYeXLuv+_$5HCwLZg*`SNBS z;TGz2HPuR$S~eD^AH3ZQ-*m?}dpx@4Qj8v(os*2&E?-ux4zbLIjao_Gh)pm?yMKS= z8R6sZpiB0h<#kLtT>1>DyILP;dDEnHU2(VOQHa2RMDTp#D@Fx)4 zyuih`#jR5?<&*osRY&LafuZeKpkP9J1aNua?DmuE-*1B*-9@X95e#0Dp7^p2Y)ZD| zST+?3r}9jImjp$ss>7wi*-W}mx_IoX3#D--l$(`vw)8~mCrehJK~(yYqqf&q_GJm- zi{m6BAq1L(`f)XXobJ;!ctwktgxVe6JHg#Y9%Col)sNm|>_@JSmi#UttG9pWZLDf& zs>JoSBeEupRBf0ObFep=Ezka;Kat~KcCk`jxDzMn^9|y)g;uqTt>(U%ocHe>ehkhQ zCoCEKnb-HKBem$&P79|Ve7=}1q2wVDR4`?x^*v*U`t<|i+@5N1d);L)G>pJz{hD;u zS77g6??RbvH?)e)_0|i}(QWq$b$5P_X&*tGPO}n2NI48!+e@f1ii>5(6Z}Cg<|e2a z)*KZPMQT{%Hf~FHdSIvyoT3ePLVB~Gl2$?@#OZ~ z(wyVa(^v0HN2t#rX=_bvatJ3i$CE*U(AjY7%?av2>nc8cwI-emL~~MD?}jY$d$Hqe zndAw%iWTk>5A@%L3u9?sd)UW70l3V>(UJQW`g``{i@#-aLAqTx+Bw3X#Nz>7eJOZ1 zKl%zB>d(!59!i8)mSU(ES1i{wHO{8?zgBZCXvNOX%%l*zKYCO7}8663mHMY{e-(ZO-fhFfiwHs&JUweMGd&#(Rs+AC+VSuqGO zbI5($S@t4;leDU43|9S&uF%4VbAl!%mU2J;zO|xf|AiNbaG3r>;v$XAhnErddx82Z z_n?O(0wJUborGsAR}635hCo|9C@(WB*76p@XbI-DBMCDY=Q|YDkLFh%gpk|LxX(KT z!H}s%8Pu;7X>9kp1pM8|*8c#}%wZ>>u)@iZ?;o5LXMf6m7bmKk{tL0mWJ5dWbDr;j z&}+?c@p85DE)!{UtFFq_*0;9i*UWo@_@oXoTEp`OyZ`E)<;h7u+Mtgz)V`Jt2<5lw zuU4|r@`a^*szE9Pj=C4^yG&HESTBIjV9(&#ICLsx@_ex++rQ{jC^?0X42c~RBAjlR`d#d? zS?_UiY0J32r$<&vQTO*zGC8pvTX zB%-;#eqQ7KB6!4Qz%8%Z5;TXjeaG{GG4G`OY*|T~Xa>za$BGGUHZ=^Y+%Ty~ZLY{m z33bs!d(7V7J*K_y=hhN~w2-B5@4Ck2B9wly4K7N2EZR4W91Af;3(G8z62_{k(y_DJ zPx(iJE$D*_R3oX#Syy0f|REzn**v9ciO?-VA{#6zhnhz zL$5Y?cJ4r}{!H;pH+5F+(x)DQs01IQsKL&$q^K%;QSNmBhM{OcMd!I6&B>SvLET=7ylAm3`+hj-Ia~y4Swi%8*k!}M= zk*|=Or#-`Xw6k3bjGr}lzIcwv;AKjYdPD1Rs+NAW@r(=DC(o|}cx$PSK!B8 zg-&yUbwaX2@Q#gRn?7WrJiqF#GNEtR3SCutxY2c!Pd` zvm8f`>ZSdDAeJ0@Are9{g~Hi;$sBO(yEHHt1UYSCUPIpJfRh7ja~*)H}X?acvT6l*eUvIxHuX7##;Rr=G0RDgRsqutSRAu3!07 zum5?NuNQfeGTUYK5VKb5z6LYE&H7;R=eY|~EC%t!;jp_&a^~1KSyspn_)!>qXrsIr zL0|4=Nxii6`>v;1o}7FguY6xM8lWjptY}4kSA%O4T<+Hk3>v7w8TRv-^NHM~^px?- z!@m(M@A1{$egTiS9+n0wtOtI-S6O)57H(eKcD8xEfa_`3LB+G7&chk*;-#(FMruCq zLGwhIKd+Oqui+Lc$^-3rx}Jm z>!?xLoe}Tvm+Osb=XYW6!(fChP_8=Illzv zM+PZfipJ2Z4ab8BzK)yPCRIm|+g}~K>hv-@0ZwkIf>nP!C=+%7@!{Oo3eYJQky|N( zc~`xB{^5bq>48yI^Me<2sd2OCxESpi1!=rnc1ryyyYL+rwm)eP#R`obkb+XX2+xy? z3l0ND94;a8n!lBhGUW%fmKQx6%AbUlen~6>9QyoJeiggG(-7SwDdOhsYY9oa+cz)H z(HounyPi`K3!Ik;&iQn!LrdWh)$lkMl3ht;PoIn2f*vto(;{CHc)MZaTeUgiW-&5s zev7YGPS^!=R_ETZ6x82Qjol0YtE$2uF7{`QWpUd~Xs8cOG=d7)j{;T5FT*8|o~0i% zD&Wm~r;V6WMKGnrO*h~~tLPRvbf^87ce2{1B}Wv-%)pMS^^?I8P_juK*R6Fzof{C4 z!w(0ktcm4;zT*ChsSLfr0TZOTc)Ma`ITK`jtG>ae ze$q`Ly|+dt6@We~X&j!M3M8w6oF44VFVs~C2ODU%KGidHWYIW&zK_ES?dueiC%8e# zTtW8smRjB$T-!o>56?^NmoSsmqG9$xG@uKN+cW#|xasx46{*}#j=_Xj|8z6|_Sx2E z0$U>If7&0(qJfG`Ay8(9JEEPOIMM!W*W)QMvOx#2DLI&pt$o3Vy~UMJ*-OQ+eB;io5T=>A#4J9Mx60DdNz`Ph%|Thk+%lW^)GWJ zpXV371wP8L`@E`6w1NOh3AH;AKdZc`z6!5Zk*#Z_=yQOZ*BnXUF&n(xDo%Jzk=2DQ^(MN@5hX1n!EJc zT=TfOxHoO1j0OZii5H60k(6U!s{g$J_z)>kGHJ^(JjYBDTX=Et+r?sc!G*rQP?Sl2 zyQULQnUDleE_lH;iZ;CDEBnAchJ@EzZ~ zig$ysl2y1!Q647oFEq{Ak4BwR4$nujrq&cr%8Knp)jOpDO=*m2#u|MrOFz}sRrkh- z^OR;Zy{4S{Jl)WXT@|4jPQ!IxP%*h$hMB_t=1Z!S6I8^2f0{Hqg&FcAN`9ay-=r-% z8kDexF^7uy#vzV_#mUFJf~^wMad$|QMLqs zxevoyW3{mjLK+c{z~tlCnV9fw;>{+1UC}H<3-;k+CI}X#S)PXRLX_cT6X&cDvfm39 zP{KY2D9R`=qA0~njxsA0Jey6>Vv*7X%FEKKX-X0KAx&1V68sAXBCG1Q7hM!hQ}pE# zsVYmmh~iayH05ZKNdyrmM=pFq#!-RQWsIfLh&B7-jsh~p=jxk-xpC^m*5#-3EeEi#-W@i!TGZ2HLkiiq~ z>A~{zH>QQ&@l(lO&S>Eod^uNs^KB&+o z896c`v}CQ=qa`vHD~e=Vam(aUIW`m_MvYA9>>IC7x0^k-={k^6f*h$hCB;4;h4E*J z6pkQACN8ZIUWPH9CK7?Pqe(`jQnZpq1+C~RNpIz$CPz~?U@8sAI6*0mka&zV(#@&b zWHg-yVVdPdxqQS$sEf9zldko0Etj|#A$)0sy_ECslQ`!w2$bd_j;OavNEn2a%y|@2 zjsWBlO4l1d3bW;NAT^^nmnw8{nogN^(Ib@RXdykCj7Eu(Ocq|_98g*?+hod(XSkB; z@b3tL&j3SjNyEibqNv7cI-digUqX^E}b<;F4(^GIqO1%Z-d z__*h*4~LMEB{zbbG?E!WMxMm@=G3QVIuhs74?GR$U72pQOPgpn zWA+NKZsYo;OXbL8PK~Kk zgGd@vr^eK&hoUp4PK~KkL#P;2r^eK&^CC5-PK~KkW9rnHIyI(Fjj2KmXr zrcRv!%rSLpOr4Sz$}x3nOr4Sw%`tWAJUGh!s_>XPbp`~bdYN!coq7Zm$JD7ugY)cL6yQ>UI&>J&$+{sydO zR?_4dXIEW^*#<@7?*bk#1oS4%9^sqAT94r)+Eab1_DoMx8Y9(&ALM>DxWwxVia(%h zO5XdwQk=i#Z!#28`aw|tZT04C+nYWggy{fgmhsmpyvDDyiCFXziWeKXz>3F)|pjC;tqhdk0AB6NV4eExO4 z$nLXSi2f%P*Za>(6)VdWCU**Zl)_XB&6*3j&@t_upuHEcMt{!ARoyPI=T@H>h#*Rnu3A0ReJ+AE+vIf$QJ^ zXuc0E8dy4o{^X$hXUc(WXOkYbin*gskJ3;GddJ)vyqYi$O!lg`2P0MTvtFhsqlbi4 z;&eFLKH7&eQdTe(gqPj4wo&M6vOl4H0=p)0&}6$0!;52|cE2~jp1Nyl^+qa86Vae$ zbJSE<%fizjykl2<|IZ)!0`2FAA6hHFwW#Q{Qf#2wSE}uqhNoFm-FD7>Jxme@;lgvK za0>6EXo~)AO;c=Yp&v$Je(RO%^JN^aggKQRepk7_2_y1uvtl(agtqFsnrYYubls-; zz`^-JY>;05J16g7SKqh3yuA1>^4Ashr$Smi8C1^hB!}2v<4Scr!}c^|YFp}=ITW!4 z=h;?(4JMBO2Zm9oF5lI45D>@EY#8VWtM_3aAH2;xdb3VB_ljMj7-1IwPbtU3{IKpO zR}>+;mosL_%^Ei9jBk+aKe4{}FBkm3->Ofdj{n=vCJG;wKYzZW>4MHhm@`u%bW79M zBx6HkOGOR}e1KHP1V(@;K+rNk;OmZI1r|0;Rg9x3Md!<}G~>pPA|PNR>_mXIE9{x< zW&SdX-f*KWyA;Y=S@Sp2QS@{2r78(y9BoJ`ZT|@4JiFu~tFXbp7b3yQ`zWT)OrdIG z8Ya+npaXPQW%Rm%0eszuz|m~Wrasd2z^*Z=tfpCQH_b}RsTyvDZtB*h{Zv;Krdd^6 zno%=hNX;r!s%l)^Suqpl3WK2pok>6Q6c|6F%_?Syk+*1Fv6B@72>MQE(A>BD?~d_& z8griGmMcQ3G78?Jn{c&RDM_G|w{0kum3-OrIW4cO%Q)e>1Qn}+dquBMhuu-yKAcNJ zrTkQQq5HNF|I@Kip7f9_Ept5hUj1ore4m;d-$nO)gqd|{X%t$a>$a}49&b`65_xl~ zukV_}L@d@LW=)^fd(46;r1Cwt?mOTv8}$)|L(`&VC>%VpdoKMScdU2GqN`Hl7fxNL zZ6w-;S>baWw`%LA!d>NK?ysBUqbF-xgpw+9n5H@2I>V@tQqku`J0X7J%t;vAf`-n< z2S#l<0@10ZAsYd#nJ$x(-&cG3H8LcZSL*0tl?!^m@5AWna{Xj+Zji>^UQLu`vKIP zuKBVW=6J^wF4=At%-BhpRVG~6O`de4eH8B|Z|kA@vbQn$ZXS1(cG6L({$o1*A--2@ z3+fIPw3O`;m-7zKtm&Igjz0c)$W~NWCWNrt`)KlueG-LPDx88o8(UZRo~A=ZYFBg7 zpqHNPlmyM9fen!hu%p|+FboVF$D+XUi3?3iZCy7HC8A@)d>|fTh>WHo&8lUKQrboI zBwI5t@@0d2rh$oNPy#f>Tp`1BRe%DAu~4%#!-R%uxt#;uGzaOW2v6H$`y3WQ<}S-B z%zg=(H!RdZmnNL7%gD6Y0(fBhblhewoY$0+4~GDDMn<{=`poCaayHg=m~67>_H&$k ziBTpT!gkDlx2&p1MRE34nC|y>tXq#c;a0nz0X@^1I+pe*ZnfdVKs6wsrlyw>K@^xk zW5T3TmHN=qpU|y7^qVhEd)vL-YM*Gjs~Z%U*oMHcO$YcywSlg?5FsB@$G|eV)xPCe zrf%6lvk6=9OkV@n#uONqjvOt}k!@)M-0HS{aLmK5PAU4i*o`}Bs}7rtzT@*`Ylo7* zWv|QazfD-!Eam@kBcmRIq{(_-qF6k-Jec4SfH3BgKQB&hc%53zp9g3OTT4ExASJH6X5QIfe#oNZ3|@2xOvfBA~0fjdWu9x`_s$TGKHwTrn^v(N`9R3l1M&1$|K& z1Ys-YadTV7J8rJZfQPVqm)&+R!{)YX_dSQ0S2MnExN!2&NU`l)eyzqXJ{PM=sdt`Y znN|DDWqtm+6|1~3P8wtI&y~jLuP~M&q2)ra_u9*zTwv4uz%6CI!(57My(qu;NV#d2 z7H^)iS=CNLF|U@1aLL4;2CP3(b_NC~1Eq5YDf3B4Bcj9=PA@i2ltL=65^)Y#N#!7U z>0n?f9b>%e>El&>r2Ikgsy++hhP704#tDw-%P~&S_nG}f>ItK5huU%g zbA>t5JK{FRR~{T+IctEUmuPmf0{Y0-nNJTi>}bF+9SgXQX#q#osH+_^bH|@nh8a7I2<7M*Y?`cT5b(9@nNQWU4=Hv#tX<0nZLx6!z3Va+Gz`}-V zX})0xT5xB`q}QJCyh0{Ty!%H^zC$VN*e6Kx@BchrfJB7=YFDb^8J1_5Q`0&3n;{}V z;uyLH1Na9EkU*rmno9xIEX1Rv1R#seJg`h3p#T%8nKA`PT-Q`};!xnKP~*W&l`U^* z0BSo-6=D_JhC9HON7sudeea;}bOz1+?)MH}UNakFL65w5(Cayj*9qZc1)C@u-$IbW ztLw3rALP~b5sWi>X1yNwgjW0hDjHz3GrjXo&&Wsnet>=7JU{IvgU8QLdo9WS=GZ$X z#e0(WWT#d;qs;bHb!tKQ7)F_^Yo_m#KbBEu7y;Ip*8;G^oetfBfXx#El?IlIUDI?_ zM<%1pW)iNM)CK`!?x=xW1Gt9f14DJ0yJ!)UxufH$)zDKb7}$n^947TP_wNmkDs>$Y z1We}D09!N|V(NaQQ>(tRFkEnoQ>(tH41%y_F>|v)EGx0V%BhwoMs-~cLGiO#x4ot_ zX?5vE;Y2AC_3l1EtJ`%Qpp8c_5*X=ybo+yh^gcp(?C7PBG@jAG@AbGpa#;Qdn|iT$ zJhIx&Lgpi@(KQ?3nhiO6*{Pq-gO_{j$$o09TO%6OaWVEm;FuT~F2n#~r~-s+9YMA$ z1h^xO1~i!>x$;EIXx%Pcg5KZ3hn{8ELR^5%)# z@@FXeQ`5C9)zCozO$ZD~eg0-NQwy$PST0r#&BE1{6pW77#T_EOJMv2-m}No;Uj>B^ zU0rG+#%o?X zOZCCCtXL@uidp)WMr@*6z|k>cn%wk(BIDD2Gd)mQWg+J5k zu82K5bf;$DFs-Mgbe*L6??*11IZ&vlZ%u0I7;$4#$F+t1uR|K}GI`mrx+tlX@$Ng2 zgtG6_CLJUUIIis^L47z01A>#CBxt=zIJ!0NSlVxYbUf3VLfgH2fBZjp8I_4hz5oE` CAf1T- diff --git a/x-pack/test/functional/es_archives/lens/reporting/mappings.json b/x-pack/test/functional/es_archives/lens/reporting/mappings.json index 14882ee507261..0321d57bc2df6 100644 --- a/x-pack/test/functional/es_archives/lens/reporting/mappings.json +++ b/x-pack/test/functional/es_archives/lens/reporting/mappings.json @@ -18,10 +18,12 @@ "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", "index-pattern": "66eccb05066c5a89924f48a9e9736499", "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", + "inventory-view": "84b320fd67209906333ffce261128462", "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", - "lens": "d69713426be87ba23283776aab149b9a", + "lens": "21c3ea0763beb1ecb0162529706b88c5", "map": "23d7aa4a720d4938ccde3983f87bd58d", "maps-telemetry": "a4229f8b16a6820c6d724b7e0c1f729d", + "metrics-explorer-view": "53c5365793677328df0ccb6138bf3cdd", "migrationVersion": "4a1746014a75ade3a714e1db5763276f", "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", "namespace": "2f4316de49999235636386fe51dc06c1", @@ -455,6 +457,77 @@ } } }, + "inventory-view": { + "properties": { + "autoBounds": { + "type": "boolean" + }, + "autoReload": { + "type": "boolean" + }, + "boundsOverride": { + "properties": { + "max": { + "type": "integer" + }, + "min": { + "type": "integer" + } + } + }, + "customOptions": { + "properties": { + "field": { + "type": "keyword" + }, + "text": { + "type": "keyword" + } + }, + "type": "nested" + }, + "filterQuery": { + "properties": { + "expression": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "groupBy": { + "properties": { + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "metric": { + "properties": { + "type": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "nodeType": { + "type": "keyword" + }, + "time": { + "type": "integer" + }, + "view": { + "type": "keyword" + } + } + }, "kql-telemetry": { "properties": { "optInCount": { @@ -472,8 +545,7 @@ "type": "keyword" }, "state": { - "enabled": false, - "type": "object" + "type": "flattened" }, "title": { "type": "text" @@ -556,6 +628,72 @@ } } }, + "metrics-explorer-view": { + "properties": { + "chartOptions": { + "properties": { + "stack": { + "type": "boolean" + }, + "type": { + "type": "keyword" + }, + "yAxisMode": { + "type": "keyword" + } + } + }, + "currentTimerange": { + "properties": { + "from": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "to": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "options": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "filterQuery": { + "type": "keyword" + }, + "groupBy": { + "type": "keyword" + }, + "limit": { + "type": "integer" + }, + "metrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + } + } + } + } + }, "migrationVersion": { "dynamic": "true", "properties": { From de1f5d02ea713a3c462369ba6b7bfec7e24b2ae9 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 15 Oct 2019 20:50:56 -0700 Subject: [PATCH 12/48] [SIEM] - Top countries by source/dest tables (#48179) --- .../public/components/page/network/index.tsx | 2 + .../__snapshots__/index.test.tsx.snap | 291 ++++++++++++ .../network_top_countries_table/columns.tsx | 176 ++++++++ .../index.test.tsx | 150 ++++++ .../network_top_countries_table/index.tsx | 206 +++++++++ .../network_top_countries_table/mock.ts | 56 +++ .../translations.ts | 67 +++ .../network_top_n_flow_table/columns.tsx | 10 +- .../network_top_n_flow_table/index.tsx | 24 +- .../components/paginated_table/index.tsx | 6 + .../source_destination/country_flag.tsx | 33 +- .../network_top_countries/index.gql_query.ts | 68 +++ .../network_top_countries/index.tsx | 156 +++++++ .../network_top_n_flow/index.gql_query.ts | 2 +- .../containers/network_top_n_flow/index.tsx | 4 +- .../siem/public/graphql/introspection.json | 426 ++++++++++++++++-- .../plugins/siem/public/graphql/types.ts | 223 ++++++++- .../plugins/siem/public/mock/global_state.ts | 30 +- .../siem/public/pages/network/ip_details.tsx | 91 ++++ .../navigation/countries_query_tab_body.tsx | 68 +++ .../pages/network/navigation/nav_tabs.tsx | 7 + .../network/navigation/network_routes.tsx | 15 + .../public/pages/network/navigation/types.ts | 5 +- .../public/pages/network/navigation/utils.ts | 3 +- .../siem/public/pages/network/translations.ts | 13 +- .../siem/public/store/network/actions.ts | 17 +- .../siem/public/store/network/helpers.test.ts | 78 +++- .../siem/public/store/network/helpers.ts | 16 + .../siem/public/store/network/model.ts | 40 +- .../siem/public/store/network/reducer.ts | 122 ++++- .../siem/public/store/network/selectors.ts | 26 +- .../siem/server/graphql/network/resolvers.ts | 15 + .../siem/server/graphql/network/schema.gql.ts | 58 ++- .../plugins/siem/server/graphql/types.ts | 392 +++++++++++++--- .../lib/network/elasticsearch_adapter.ts | 93 +++- .../plugins/siem/server/lib/network/index.ts | 18 +- .../plugins/siem/server/lib/network/mock.ts | 6 +- .../lib/network/query_top_countries.dsl.ts | 149 ++++++ .../lib/network/query_top_n_flow.dsl.ts | 18 +- .../plugins/siem/server/lib/network/types.ts | 20 +- .../apis/siem/network_top_n_flow.ts | 10 +- 41 files changed, 2993 insertions(+), 217 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/__snapshots__/index.test.tsx.snap create mode 100644 x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/columns.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/mock.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/network_top_countries/index.gql_query.ts create mode 100644 x-pack/legacy/plugins/siem/public/containers/network_top_countries/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/network/navigation/countries_query_tab_body.tsx create mode 100644 x-pack/legacy/plugins/siem/server/lib/network/query_top_countries.dsl.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/index.tsx index 9758477a4ee2a..40460f81fa83c 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/index.tsx @@ -6,4 +6,6 @@ export { IpOverview } from './ip_overview'; export { KpiNetworkComponent } from './kpi_network'; +export { NetworkDnsTable } from './network_dns_table'; +export { NetworkTopCountriesTable } from './network_top_countries_table'; export { NetworkTopNFlowTable } from './network_top_n_flow_table'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..1127528c776b7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/__snapshots__/index.test.tsx.snap @@ -0,0 +1,291 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NetworkTopCountries Table Component rendering it renders the IP Details NetworkTopCountries table 1`] = ` + +`; + +exports[`NetworkTopCountries Table Component rendering it renders the default NetworkTopCountries table 1`] = ` + +`; diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/columns.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/columns.tsx new file mode 100644 index 0000000000000..290891def3da8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/columns.tsx @@ -0,0 +1,176 @@ +/* + * 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 { get } from 'lodash/fp'; +import numeral from '@elastic/numeral'; +import React from 'react'; +import { StaticIndexPattern } from 'ui/index_patterns'; + +import { CountryFlagAndName } from '../../../source_destination/country_flag'; +import { + FlowTargetSourceDest, + NetworkTopCountriesEdges, + TopNetworkTablesEcsField, +} from '../../../../graphql/types'; +import { networkModel } from '../../../../store'; +import { DragEffects, DraggableWrapper } from '../../../drag_and_drop/draggable_wrapper'; +import { escapeDataProviderId } from '../../../drag_and_drop/helpers'; +import { getEmptyTagValue } from '../../../empty_value'; +import { Columns } from '../../../paginated_table'; +import { IS_OPERATOR } from '../../../timeline/data_providers/data_provider'; +import { Provider } from '../../../timeline/data_providers/provider'; +import * as i18n from './translations'; +import { PreferenceFormattedBytes } from '../../../formatted_bytes'; + +export type NetworkTopCountriesColumns = [ + Columns, + Columns, + Columns, + Columns, + Columns, + Columns +]; + +export type NetworkTopCountriesColumnsIpDetails = [ + Columns, + Columns, + Columns, + Columns, + Columns +]; + +export const getNetworkTopCountriesColumns = ( + indexPattern: StaticIndexPattern, + flowTarget: FlowTargetSourceDest, + type: networkModel.NetworkType, + tableId: string +): NetworkTopCountriesColumns => [ + { + name: i18n.COUNTRY, + render: ({ node }) => { + const geo = get(`${flowTarget}.country`, node); + const geoAttr = `${flowTarget}.geo.country_iso_code`; + const id = escapeDataProviderId(`${tableId}-table-${flowTarget}-country-${geo}`); + if (geo != null) { + return ( + + snapshot.isDragging ? ( + + + + ) : ( + <> + + + ) + } + /> + ); + } else { + return getEmptyTagValue(); + } + }, + width: '20%', + }, + { + align: 'right', + field: 'node.network.bytes_in', + name: i18n.BYTES_IN, + sortable: true, + render: bytes => { + if (bytes != null) { + return ; + } else { + return getEmptyTagValue(); + } + }, + }, + { + align: 'right', + field: 'node.network.bytes_out', + name: i18n.BYTES_OUT, + sortable: true, + render: bytes => { + if (bytes != null) { + return ; + } else { + return getEmptyTagValue(); + } + }, + }, + { + align: 'right', + field: `node.${flowTarget}.flows`, + name: i18n.FLOWS, + sortable: true, + render: flows => { + if (flows != null) { + return numeral(flows).format('0,000'); + } else { + return getEmptyTagValue(); + } + }, + }, + { + align: 'right', + field: `node.${flowTarget}.${flowTarget}_ips`, + name: flowTarget === FlowTargetSourceDest.source ? i18n.SOURCE_IPS : i18n.DESTINATION_IPS, + sortable: true, + render: ips => { + if (ips != null) { + return numeral(ips).format('0,000'); + } else { + return getEmptyTagValue(); + } + }, + }, + { + align: 'right', + field: `node.${flowTarget}.${getOppositeField(flowTarget)}_ips`, + name: flowTarget === FlowTargetSourceDest.source ? i18n.SOURCE_IPS : i18n.DESTINATION_IPS, + sortable: true, + render: ips => { + if (ips != null) { + return numeral(ips).format('0,000'); + } else { + return getEmptyTagValue(); + } + }, + }, +]; + +export const getCountriesColumnsCurated = ( + indexPattern: StaticIndexPattern, + flowTarget: FlowTargetSourceDest, + type: networkModel.NetworkType, + tableId: string +): NetworkTopCountriesColumns | NetworkTopCountriesColumnsIpDetails => { + const columns = getNetworkTopCountriesColumns(indexPattern, flowTarget, type, tableId); + + // Columns to exclude from host details pages + if (type === networkModel.NetworkType.details) { + columns.pop(); + return columns; + } + + return columns; +}; + +const getOppositeField = (flowTarget: FlowTargetSourceDest): FlowTargetSourceDest => + flowTarget === FlowTargetSourceDest.source + ? FlowTargetSourceDest.destination + : FlowTargetSourceDest.source; diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx new file mode 100644 index 0000000000000..4fa1bceac9888 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount, shallow } from 'enzyme'; +import toJson from 'enzyme-to-json'; +import { getOr } from 'lodash/fp'; +import * as React from 'react'; +import { MockedProvider } from 'react-apollo/test-utils'; +import { Provider as ReduxStoreProvider } from 'react-redux'; + +import { FlowTargetSourceDest } from '../../../../graphql/types'; +import { + apolloClientObservable, + mockGlobalState, + mockIndexPattern, + TestProviders, +} from '../../../../mock'; +import { createStore, networkModel, State } from '../../../../store'; + +import { NetworkTopCountriesTable } from '.'; +import { mockData } from './mock'; +jest.mock('../../../../lib/settings/use_kibana_ui_setting'); +describe('NetworkTopCountries Table Component', () => { + const loadPage = jest.fn(); + const state: State = mockGlobalState; + + let store = createStore(state, apolloClientObservable); + + beforeEach(() => { + store = createStore(state, apolloClientObservable); + }); + + describe('rendering', () => { + test('it renders the default NetworkTopCountries table', () => { + const wrapper = shallow( + + + + ); + + expect(toJson(wrapper)).toMatchSnapshot(); + }); + test('it renders the IP Details NetworkTopCountries table', () => { + const wrapper = shallow( + + + + ); + + expect(toJson(wrapper)).toMatchSnapshot(); + }); + }); + + describe('Sorting on Table', () => { + test('when you click on the column header, you should show the sorting icon', () => { + const wrapper = mount( + + + + + + ); + expect(store.getState().network.page.queries.topCountriesSource.topCountriesSort).toEqual({ + direction: 'desc', + field: 'bytes_out', + }); + + wrapper + .find('.euiTable thead tr th button') + .at(1) + .simulate('click'); + + wrapper.update(); + + expect(store.getState().network.page.queries.topCountriesSource.topCountriesSort).toEqual({ + direction: 'asc', + field: 'bytes_out', + }); + expect( + wrapper + .find('.euiTable thead tr th button') + .first() + .text() + ).toEqual('Bytes inClick to sort in ascending order'); + expect( + wrapper + .find('.euiTable thead tr th button') + .at(1) + .text() + ).toEqual('Bytes outClick to sort in descending order'); + expect( + wrapper + .find('.euiTable thead tr th button') + .at(1) + .find('svg') + ).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.tsx new file mode 100644 index 0000000000000..3972bb80b760b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.tsx @@ -0,0 +1,206 @@ +/* + * 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 { isEqual, last } from 'lodash/fp'; +import React from 'react'; +import { connect } from 'react-redux'; +import { ActionCreator } from 'typescript-fsa'; +import { StaticIndexPattern } from 'ui/index_patterns'; + +import { networkActions } from '../../../../store/actions'; +import { + Direction, + FlowTargetSourceDest, + NetworkTopCountriesEdges, + NetworkTopTablesFields, + NetworkTopTablesSortField, +} from '../../../../graphql/types'; +import { networkModel, networkSelectors, State } from '../../../../store'; +import { Criteria, ItemsPerRow, PaginatedTable } from '../../../paginated_table'; + +import { getCountriesColumnsCurated } from './columns'; +import * as i18n from './translations'; + +interface OwnProps { + data: NetworkTopCountriesEdges[]; + fakeTotalCount: number; + flowTargeted: FlowTargetSourceDest; + id: string; + indexPattern: StaticIndexPattern; + ip?: string; + isInspect: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + showMorePagesIndicator: boolean; + totalCount: number; + type: networkModel.NetworkType; +} + +interface NetworkTopCountriesTableReduxProps { + activePage: number; + limit: number; + topCountriesSort: NetworkTopTablesSortField; +} + +interface NetworkTopCountriesTableDispatchProps { + updateIpDetailsTableActivePage: ActionCreator<{ + activePage: number; + tableType: networkModel.IpDetailsTableType; + }>; + updateNetworkPageTableActivePage: ActionCreator<{ + activePage: number; + tableType: networkModel.NetworkTableType; + }>; + updateTopCountriesLimit: ActionCreator<{ + limit: number; + networkType: networkModel.NetworkType; + tableType: networkModel.TopCountriesTableType; + }>; + updateTopCountriesSort: ActionCreator<{ + topCountriesSort: NetworkTopTablesSortField; + networkType: networkModel.NetworkType; + tableType: networkModel.TopCountriesTableType; + }>; +} + +type NetworkTopCountriesTableProps = OwnProps & + NetworkTopCountriesTableReduxProps & + NetworkTopCountriesTableDispatchProps; + +const rowItems: ItemsPerRow[] = [ + { + text: i18n.ROWS_5, + numberOfRow: 5, + }, + { + text: i18n.ROWS_10, + numberOfRow: 10, + }, +]; + +export const NetworkTopCountriesTableId = 'networkTopCountries-top-talkers'; + +const NetworkTopCountriesTableComponent = React.memo( + ({ + activePage, + data, + fakeTotalCount, + flowTargeted, + id, + indexPattern, + isInspect, + limit, + loading, + loadPage, + showMorePagesIndicator, + topCountriesSort, + totalCount, + type, + updateIpDetailsTableActivePage, + updateTopCountriesLimit, + updateTopCountriesSort, + updateNetworkPageTableActivePage, + }) => { + const onChange = (criteria: Criteria, tableType: networkModel.TopCountriesTableType) => { + if (criteria.sort != null) { + const splitField = criteria.sort.field.split('.'); + const field = last(splitField); + const newSortDirection = + field !== topCountriesSort.field ? Direction.desc : criteria.sort.direction; // sort by desc on init click + const newTopCountriesSort: NetworkTopTablesSortField = { + field: field as NetworkTopTablesFields, + direction: newSortDirection, + }; + if (!isEqual(newTopCountriesSort, topCountriesSort)) { + updateTopCountriesSort({ + topCountriesSort: newTopCountriesSort, + networkType: type, + tableType, + }); + } + } + }; + + let tableType: networkModel.TopCountriesTableType; + const headerTitle: string = + flowTargeted === FlowTargetSourceDest.source + ? i18n.SOURCE_COUNTRIES + : i18n.DESTINATION_COUNTRIES; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let updateTableActivePage: any; + if (type === networkModel.NetworkType.page) { + tableType = + flowTargeted === FlowTargetSourceDest.source + ? networkModel.NetworkTableType.topCountriesSource + : networkModel.NetworkTableType.topCountriesDestination; + updateTableActivePage = updateNetworkPageTableActivePage; + } else { + tableType = + flowTargeted === FlowTargetSourceDest.source + ? networkModel.IpDetailsTableType.topCountriesSource + : networkModel.IpDetailsTableType.topCountriesDestination; + updateTableActivePage = updateIpDetailsTableActivePage; + } + + const field = + topCountriesSort.field === NetworkTopTablesFields.bytes_out || + topCountriesSort.field === NetworkTopTablesFields.bytes_in + ? `node.network.${topCountriesSort.field}` + : `node.${flowTargeted}.${topCountriesSort.field}`; + + return ( + loadPage(newActivePage)} + onChange={criteria => onChange(criteria, tableType)} + pageOfItems={data} + showMorePagesIndicator={showMorePagesIndicator} + sorting={{ field, direction: topCountriesSort.direction }} + totalCount={fakeTotalCount} + updateActivePage={newPage => + updateTableActivePage({ + activePage: newPage, + tableType, + }) + } + updateLimitPagination={newLimit => + updateTopCountriesLimit({ limit: newLimit, networkType: type, tableType }) + } + /> + ); + } +); + +NetworkTopCountriesTableComponent.displayName = 'NetworkTopCountriesTableComponent'; + +const mapStateToProps = (state: State, ownProps: OwnProps) => + networkSelectors.topCountriesSelector(ownProps.flowTargeted, ownProps.type); + +export const NetworkTopCountriesTable = connect( + mapStateToProps, + { + updateTopCountriesLimit: networkActions.updateTopCountriesLimit, + updateTopCountriesSort: networkActions.updateTopCountriesSort, + updateNetworkPageTableActivePage: networkActions.updateNetworkPageTableActivePage, + updateIpDetailsTableActivePage: networkActions.updateIpDetailsTableActivePage, + } +)(NetworkTopCountriesTableComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/mock.ts b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/mock.ts new file mode 100644 index 0000000000000..42b933c7fba6d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/mock.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { NetworkTopCountriesData } from '../../../../graphql/types'; + +export const mockData: { NetworkTopCountries: NetworkTopCountriesData } = { + NetworkTopCountries: { + totalCount: 524, + edges: [ + { + node: { + source: { + country: 'DE', + destination_ips: 12, + flows: 12345, + source_ips: 55, + }, + destination: null, + network: { + bytes_in: 3826633497, + bytes_out: 1083495734, + }, + }, + cursor: { + value: '8.8.8.8', + }, + }, + { + node: { + source: { + flows: 12345, + destination_ips: 12, + source_ips: 55, + country: 'US', + }, + destination: null, + network: { + bytes_in: 3826633497, + bytes_out: 1083495734, + }, + }, + cursor: { + value: '9.9.9.9', + }, + }, + ], + pageInfo: { + activePage: 1, + fakeTotalCount: 50, + showMorePagesIndicator: true, + }, + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/translations.ts b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/translations.ts new file mode 100644 index 0000000000000..2fad2687daf1e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/translations.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 { i18n } from '@kbn/i18n'; + +export const UNIT = (totalCount: number) => + i18n.translate('xpack.siem.networkTopCountriesTable.heading.unit', { + values: { totalCount }, + defaultMessage: `{totalCount, plural, =1 {Country} other {Countries}}`, + }); + +export const COUNTRY = i18n.translate('xpack.siem.networkTopCountriesTable.column.countryTitle', { + defaultMessage: 'Country', +}); + +export const BYTES_IN = i18n.translate('xpack.siem.networkTopCountriesTable.column.bytesInTitle', { + defaultMessage: 'Bytes in', +}); + +export const BYTES_OUT = i18n.translate( + 'xpack.siem.networkTopCountriesTable.column.bytesOutTitle', + { + defaultMessage: 'Bytes out', + } +); + +export const FLOWS = i18n.translate('xpack.siem.networkTopCountriesTable.column.flows', { + defaultMessage: 'Flows', +}); + +export const DESTINATION_COUNTRIES = i18n.translate( + 'xpack.siem.networkTopCountriesTable.heading.destinationCountries', + { + defaultMessage: 'Top Destination Countries', + } +); + +export const SOURCE_COUNTRIES = i18n.translate( + 'xpack.siem.networkTopCountriesTable.heading.sourceCountries', + { + defaultMessage: 'Top Source Countries', + } +); + +export const DESTINATION_IPS = i18n.translate( + 'xpack.siem.networkTopCountriesTable.column.destinationIps', + { + defaultMessage: 'Destination IPs', + } +); + +export const SOURCE_IPS = i18n.translate('xpack.siem.networkTopCountriesTable.column.sourceIps', { + defaultMessage: 'Source IPs', +}); + +export const ROWS_5 = i18n.translate('xpack.siem.networkTopCountriesTable.rows', { + values: { numRows: 5 }, + defaultMessage: '{numRows} {numRows, plural, =0 {rows} =1 {row} other {rows}}', +}); + +export const ROWS_10 = i18n.translate('xpack.siem.networkTopCountriesTable.rows', { + values: { numRows: 10 }, + defaultMessage: '{numRows} {numRows, plural, =0 {rows} =1 {row} other {rows}}', +}); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/columns.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/columns.tsx index bdb57a3323d4e..abb4d9ac29f1b 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/columns.tsx @@ -14,7 +14,7 @@ import { AutonomousSystemItem, FlowTargetSourceDest, NetworkTopNFlowEdges, - TopNFlowNetworkEcsField, + TopNetworkTablesEcsField, } from '../../../../graphql/types'; import { networkModel } from '../../../../store'; import { DragEffects, DraggableWrapper } from '../../../drag_and_drop/draggable_wrapper'; @@ -32,8 +32,8 @@ export type NetworkTopNFlowColumns = [ Columns, Columns, Columns, - Columns, - Columns, + Columns, + Columns, Columns, Columns ]; @@ -42,8 +42,8 @@ export type NetworkTopNFlowColumnsIpDetails = [ Columns, Columns, Columns, - Columns, - Columns, + Columns, + Columns, Columns ]; diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.tsx index 507f8b75b842a..9e6e3cf844119 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.tsx @@ -3,11 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexItem } from '@elastic/eui'; import { isEqual, last } from 'lodash/fp'; import React from 'react'; import { connect } from 'react-redux'; -import styled from 'styled-components'; import { ActionCreator } from 'typescript-fsa'; import { StaticIndexPattern } from 'ui/index_patterns'; @@ -16,8 +14,8 @@ import { Direction, FlowTargetSourceDest, NetworkTopNFlowEdges, - NetworkTopNFlowFields, - NetworkTopNFlowSortField, + NetworkTopTablesFields, + NetworkTopTablesSortField, } from '../../../../graphql/types'; import { networkModel, networkSelectors, State } from '../../../../store'; import { Criteria, ItemsPerRow, PaginatedTable } from '../../../paginated_table'; @@ -42,7 +40,7 @@ interface OwnProps { interface NetworkTopNFlowTableReduxProps { activePage: number; limit: number; - topNFlowSort: NetworkTopNFlowSortField; + topNFlowSort: NetworkTopTablesSortField; } interface NetworkTopNFlowTableDispatchProps { @@ -60,7 +58,7 @@ interface NetworkTopNFlowTableDispatchProps { tableType: networkModel.TopNTableType; }>; updateTopNFlowSort: ActionCreator<{ - topNFlowSort: NetworkTopNFlowSortField; + topNFlowSort: NetworkTopTablesSortField; networkType: networkModel.NetworkType; tableType: networkModel.TopNTableType; }>; @@ -110,8 +108,8 @@ const NetworkTopNFlowTableComponent = React.memo( const field = last(splitField); const newSortDirection = field !== topNFlowSort.field ? Direction.desc : criteria.sort.direction; // sort by desc on init click - const newTopNFlowSort: NetworkTopNFlowSortField = { - field: field as NetworkTopNFlowFields, + const newTopNFlowSort: NetworkTopTablesSortField = { + field: field as NetworkTopTablesFields, direction: newSortDirection, }; if (!isEqual(newTopNFlowSort, topNFlowSort)) { @@ -145,8 +143,8 @@ const NetworkTopNFlowTableComponent = React.memo( } const field = - topNFlowSort.field === NetworkTopNFlowFields.bytes_out || - topNFlowSort.field === NetworkTopNFlowFields.bytes_in + topNFlowSort.field === NetworkTopTablesFields.bytes_out || + topNFlowSort.field === NetworkTopTablesFields.bytes_in ? `node.network.${topNFlowSort.field}` : `node.${flowTargeted}.${topNFlowSort.field}`; @@ -197,9 +195,3 @@ export const NetworkTopNFlowTable = connect( updateIpDetailsTableActivePage: networkActions.updateIpDetailsTableActivePage, } )(NetworkTopNFlowTableComponent); - -const SelectTypeItem = styled(EuiFlexItem)` - min-width: 180px; -`; - -SelectTypeItem.displayName = 'SelectTypeItem'; diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx index 1529648b7133e..4de64c6b32aa9 100644 --- a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx @@ -27,6 +27,10 @@ import { NetworkTopNFlowColumns, NetworkTopNFlowColumnsIpDetails, } from '../page/network/network_top_n_flow_table/columns'; +import { + NetworkTopCountriesColumns, + NetworkTopCountriesColumnsIpDetails, +} from '../page/network/network_top_countries_table/columns'; import { TlsColumns } from '../page/network/tls_table/columns'; import { UncommonProcessTableColumns } from '../page/hosts/uncommon_process_table'; import { UsersColumns } from '../page/network/users_table/columns'; @@ -68,6 +72,8 @@ declare type BasicTableColumns = | HostsTableColumns | HostsTableColumnsTest | NetworkDnsColumns + | NetworkTopCountriesColumns + | NetworkTopCountriesColumnsIpDetails | NetworkTopNFlowColumns | NetworkTopNFlowColumnsIpDetails | TlsColumns diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/country_flag.tsx b/x-pack/legacy/plugins/siem/public/components/source_destination/country_flag.tsx index 98639e0d38514..4de678f7f636e 100644 --- a/x-pack/legacy/plugins/siem/public/components/source_destination/country_flag.tsx +++ b/x-pack/legacy/plugins/siem/public/components/source_destination/country_flag.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, useEffect } from 'react'; +import React, { memo, useEffect, useState } from 'react'; import { isEmpty } from 'lodash/fp'; import { EuiToolTip } from '@elastic/eui'; import countries from 'i18n-iso-countries'; @@ -22,7 +22,7 @@ export const getFlag = (countryCode: string): string | null => .replace(/./g, c => String.fromCharCode(55356, 56741 + c.charCodeAt(0))) : null; -/** Renders an emjoi flag for the specified country code */ +/** Renders an emoji flag for the specified country code */ export const CountryFlag = memo<{ countryCode: string; displayCountryNameOnHover?: boolean; @@ -47,3 +47,32 @@ export const CountryFlag = memo<{ }); CountryFlag.displayName = 'CountryFlag'; + +/** Renders an emjoi flag with country name for the specified country code */ +export const CountryFlagAndName = memo<{ + countryCode: string; + displayCountryNameOnHover?: boolean; +}>(({ countryCode, displayCountryNameOnHover = false }) => { + const [localesLoaded, setLocalesLoaded] = useState(false); + useEffect(() => { + if (isEmpty(countries.getNames('en'))) { + countries.registerLocale(countryJson); + } + setLocalesLoaded(true); + }, []); + + const flag = getFlag(countryCode); + + if (flag !== null && localesLoaded) { + return displayCountryNameOnHover ? ( + + {flag} + + ) : ( + {`${flag} ${countries.getName(countryCode, 'en')}`} + ); + } + return null; +}); + +CountryFlagAndName.displayName = 'CountryFlagAndName'; diff --git a/x-pack/legacy/plugins/siem/public/containers/network_top_countries/index.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/network_top_countries/index.gql_query.ts new file mode 100644 index 0000000000000..5850246ceecec --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/network_top_countries/index.gql_query.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import gql from 'graphql-tag'; + +export const networkTopCountriesQuery = gql` + query GetNetworkTopCountriesQuery( + $sourceId: ID! + $ip: String + $filterQuery: String + $pagination: PaginationInputPaginated! + $sort: NetworkTopTablesSortField! + $flowTarget: FlowTargetSourceDest! + $timerange: TimerangeInput! + $defaultIndex: [String!]! + $inspect: Boolean! + ) { + source(id: $sourceId) { + id + NetworkTopCountries( + filterQuery: $filterQuery + flowTarget: $flowTarget + ip: $ip + pagination: $pagination + sort: $sort + timerange: $timerange + defaultIndex: $defaultIndex + ) { + totalCount + edges { + node { + source { + country + destination_ips + flows + source_ips + } + destination { + country + destination_ips + flows + source_ips + } + network { + bytes_in + bytes_out + } + } + cursor { + value + } + } + pageInfo { + activePage + fakeTotalCount + showMorePagesIndicator + } + inspect @include(if: $inspect) { + dsl + response + } + } + } + } +`; diff --git a/x-pack/legacy/plugins/siem/public/containers/network_top_countries/index.tsx b/x-pack/legacy/plugins/siem/public/containers/network_top_countries/index.tsx new file mode 100644 index 0000000000000..8e3804ab77478 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/network_top_countries/index.tsx @@ -0,0 +1,156 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; + +import chrome from 'ui/chrome'; +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { + FlowTargetSourceDest, + GetNetworkTopCountriesQuery, + NetworkTopCountriesEdges, + NetworkTopTablesSortField, + PageInfoPaginated, +} from '../../graphql/types'; +import { inputsModel, inputsSelectors, networkModel, networkSelectors, State } from '../../store'; +import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; +import { networkTopCountriesQuery } from './index.gql_query'; + +const ID = 'networkTopCountriesQuery'; + +export interface NetworkTopCountriesArgs { + id: string; + ip?: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + networkTopCountries: NetworkTopCountriesEdges[]; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + totalCount: number; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: NetworkTopCountriesArgs) => React.ReactNode; + flowTarget: FlowTargetSourceDest; + ip?: string; + type: networkModel.NetworkType; +} + +export interface NetworkTopCountriesComponentReduxProps { + activePage: number; + isInspected: boolean; + limit: number; + topCountriesSort: NetworkTopTablesSortField; +} + +type NetworkTopCountriesProps = OwnProps & NetworkTopCountriesComponentReduxProps; + +class NetworkTopCountriesComponentQuery extends QueryTemplatePaginated< + NetworkTopCountriesProps, + GetNetworkTopCountriesQuery.Query, + GetNetworkTopCountriesQuery.Variables +> { + public render() { + const { + activePage, + children, + endDate, + flowTarget, + filterQuery, + id = `${ID}-${flowTarget}`, + ip, + isInspected, + limit, + skip, + sourceId, + startDate, + topCountriesSort, + } = this.props; + const variables: GetNetworkTopCountriesQuery.Variables = { + defaultIndex: chrome.getUiSettingsClient().get(DEFAULT_INDEX_KEY), + filterQuery: createFilter(filterQuery), + flowTarget, + inspect: isInspected, + ip, + pagination: generateTablePaginationOptions(activePage, limit), + sort: topCountriesSort, + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + }; + return ( + + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + query={networkTopCountriesQuery} + skip={skip} + variables={variables} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + const networkTopCountries = getOr([], `source.NetworkTopCountries.edges`, data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + NetworkTopCountries: { + ...fetchMoreResult.source.NetworkTopCountries, + edges: [...fetchMoreResult.source.NetworkTopCountries.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + id, + inspect: getOr(null, 'source.NetworkTopCountries.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + networkTopCountries, + pageInfo: getOr({}, 'source.NetworkTopCountries.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + totalCount: getOr(-1, 'source.NetworkTopCountries.totalCount', data), + }); + }} + + ); + } +} + +const mapStateToProps = ( + state: State, + { flowTarget, id = `${ID}-${flowTarget}`, type }: OwnProps +) => { + const getNetworkTopCountriesSelector = networkSelectors.topCountriesSelector(flowTarget, type); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const { isInspected } = getQuery(state, id); + return { + ...getNetworkTopCountriesSelector(state), + isInspected, + }; +}; + +export const NetworkTopCountriesQuery = connect(mapStateToProps)(NetworkTopCountriesComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.gql_query.ts index 81a94bc94652b..a73f9ff9256ff 100644 --- a/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.gql_query.ts +++ b/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.gql_query.ts @@ -12,7 +12,7 @@ export const networkTopNFlowQuery = gql` $ip: String $filterQuery: String $pagination: PaginationInputPaginated! - $sort: NetworkTopNFlowSortField! + $sort: NetworkTopTablesSortField! $flowTarget: FlowTargetSourceDest! $timerange: TimerangeInput! $defaultIndex: [String!]! diff --git a/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.tsx b/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.tsx index eba9c5640fb58..32629853480a0 100644 --- a/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.tsx @@ -15,7 +15,7 @@ import { FlowTargetSourceDest, GetNetworkTopNFlowQuery, NetworkTopNFlowEdges, - NetworkTopNFlowSortField, + NetworkTopTablesSortField, PageInfoPaginated, } from '../../graphql/types'; import { inputsModel, inputsSelectors, networkModel, networkSelectors, State } from '../../store'; @@ -50,7 +50,7 @@ export interface NetworkTopNFlowComponentReduxProps { activePage: number; isInspected: boolean; limit: number; - topNFlowSort: NetworkTopNFlowSortField; + topNFlowSort: NetworkTopTablesSortField; } type NetworkTopNFlowProps = OwnProps & NetworkTopNFlowComponentReduxProps; diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/legacy/plugins/siem/public/graphql/introspection.json index b8bdf27bed7fa..c012af5fd667f 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json +++ b/x-pack/legacy/plugins/siem/public/graphql/introspection.json @@ -1542,9 +1542,106 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "NetworkTopCountries", + "description": "", + "args": [ + { + "name": "id", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "filterQuery", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "ip", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "flowTarget", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "ENUM", "name": "FlowTargetSourceDest", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "pagination", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "PaginationInputPaginated", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "sort", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "NetworkTopTablesSortField", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "timerange", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "defaultIndex", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "NetworkTopCountriesData", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "NetworkTopNFlow", - "description": "Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified", + "description": "", "args": [ { "name": "id", @@ -1596,7 +1693,7 @@ "name": null, "ofType": { "kind": "INPUT_OBJECT", - "name": "NetworkTopNFlowSortField", + "name": "NetworkTopTablesSortField", "ofType": null } }, @@ -7204,7 +7301,7 @@ }, { "kind": "INPUT_OBJECT", - "name": "NetworkTopNFlowSortField", + "name": "NetworkTopTablesSortField", "description": "", "fields": null, "inputFields": [ @@ -7214,7 +7311,7 @@ "type": { "kind": "NON_NULL", "name": null, - "ofType": { "kind": "ENUM", "name": "NetworkTopNFlowFields", "ofType": null } + "ofType": { "kind": "ENUM", "name": "NetworkTopTablesFields", "ofType": null } }, "defaultValue": null }, @@ -7235,7 +7332,7 @@ }, { "kind": "ENUM", - "name": "NetworkTopNFlowFields", + "name": "NetworkTopTablesFields", "description": "", "fields": null, "inputFields": null, @@ -7271,7 +7368,7 @@ }, { "kind": "OBJECT", - "name": "NetworkTopNFlowData", + "name": "NetworkTopCountriesData", "description": "", "fields": [ { @@ -7287,7 +7384,7 @@ "ofType": { "kind": "NON_NULL", "name": null, - "ofType": { "kind": "OBJECT", "name": "NetworkTopNFlowEdges", "ofType": null } + "ofType": { "kind": "OBJECT", "name": "NetworkTopCountriesEdges", "ofType": null } } } }, @@ -7334,7 +7431,7 @@ }, { "kind": "OBJECT", - "name": "NetworkTopNFlowEdges", + "name": "NetworkTopCountriesEdges", "description": "", "fields": [ { @@ -7344,7 +7441,7 @@ "type": { "kind": "NON_NULL", "name": null, - "ofType": { "kind": "OBJECT", "name": "NetworkTopNFlowItem", "ofType": null } + "ofType": { "kind": "OBJECT", "name": "NetworkTopCountriesItem", "ofType": null } }, "isDeprecated": false, "deprecationReason": null @@ -7369,7 +7466,7 @@ }, { "kind": "OBJECT", - "name": "NetworkTopNFlowItem", + "name": "NetworkTopCountriesItem", "description": "", "fields": [ { @@ -7384,7 +7481,7 @@ "name": "source", "description": "", "args": [], - "type": { "kind": "OBJECT", "name": "TopNFlowItemSource", "ofType": null }, + "type": { "kind": "OBJECT", "name": "TopCountriesItemSource", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, @@ -7392,7 +7489,7 @@ "name": "destination", "description": "", "args": [], - "type": { "kind": "OBJECT", "name": "TopNFlowItemDestination", "ofType": null }, + "type": { "kind": "OBJECT", "name": "TopCountriesItemDestination", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, @@ -7400,7 +7497,7 @@ "name": "network", "description": "", "args": [], - "type": { "kind": "OBJECT", "name": "TopNFlowNetworkEcsField", "ofType": null }, + "type": { "kind": "OBJECT", "name": "TopNetworkTablesEcsField", "ofType": null }, "isDeprecated": false, "deprecationReason": null } @@ -7412,38 +7509,30 @@ }, { "kind": "OBJECT", - "name": "TopNFlowItemSource", + "name": "TopCountriesItemSource", "description": "", "fields": [ { - "name": "autonomous_system", + "name": "country", "description": "", "args": [], - "type": { "kind": "OBJECT", "name": "AutonomousSystemItem", "ofType": null }, + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "domain", + "name": "destination_ips", "description": "", "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - }, + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "ip", + "name": "flows", "description": "", "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, @@ -7456,12 +7545,58 @@ "deprecationReason": null }, { - "name": "flows", + "name": "source_ips", "description": "", "args": [], "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, "isDeprecated": false, "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "GeoItem", + "description": "", + "fields": [ + { + "name": "geo", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "GeoEcsFields", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "flowTarget", + "description": "", + "args": [], + "type": { "kind": "ENUM", "name": "FlowTargetSourceDest", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TopCountriesItemDestination", + "description": "", + "fields": [ + { + "name": "country", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null }, { "name": "destination_ips", @@ -7470,6 +7605,30 @@ "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "flows", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "location", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "GeoItem", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "source_ips", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -7479,19 +7638,19 @@ }, { "kind": "OBJECT", - "name": "AutonomousSystemItem", + "name": "TopNetworkTablesEcsField", "description": "", "fields": [ { - "name": "name", + "name": "bytes_in", "description": "", "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "number", + "name": "bytes_out", "description": "", "args": [], "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, @@ -7506,22 +7665,58 @@ }, { "kind": "OBJECT", - "name": "GeoItem", + "name": "NetworkTopNFlowData", "description": "", "fields": [ { - "name": "geo", + "name": "edges", "description": "", "args": [], - "type": { "kind": "OBJECT", "name": "GeoEcsFields", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "NetworkTopNFlowEdges", "ofType": null } + } + } + }, "isDeprecated": false, "deprecationReason": null }, { - "name": "flowTarget", + "name": "totalCount", "description": "", "args": [], - "type": { "kind": "ENUM", "name": "FlowTargetSourceDest", "ofType": null }, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "PageInfoPaginated", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inspect", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, "isDeprecated": false, "deprecationReason": null } @@ -7533,7 +7728,85 @@ }, { "kind": "OBJECT", - "name": "TopNFlowItemDestination", + "name": "NetworkTopNFlowEdges", + "description": "", + "fields": [ + { + "name": "node", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "NetworkTopNFlowItem", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cursor", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "CursorType", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "NetworkTopNFlowItem", + "description": "", + "fields": [ + { + "name": "_id", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "source", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "TopNFlowItemSource", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "destination", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "TopNFlowItemDestination", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "network", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "TopNetworkTablesEcsField", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TopNFlowItemSource", "description": "", "fields": [ { @@ -7585,7 +7858,7 @@ "deprecationReason": null }, { - "name": "source_ips", + "name": "destination_ips", "description": "", "args": [], "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, @@ -7600,19 +7873,86 @@ }, { "kind": "OBJECT", - "name": "TopNFlowNetworkEcsField", + "name": "AutonomousSystemItem", "description": "", "fields": [ { - "name": "bytes_in", + "name": "name", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "number", "description": "", "args": [], "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, "isDeprecated": false, "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TopNFlowItemDestination", + "description": "", + "fields": [ + { + "name": "autonomous_system", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "AutonomousSystemItem", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null }, { - "name": "bytes_out", + "name": "domain", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ip", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "location", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "GeoItem", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "flows", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "source_ips", "description": "", "args": [], "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts index 7514259caa2a7..b8ed6f2ad47fe 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/public/graphql/types.ts @@ -79,8 +79,8 @@ export interface UsersSortField { direction: Direction; } -export interface NetworkTopNFlowSortField { - field: NetworkTopNFlowFields; +export interface NetworkTopTablesSortField { + field: NetworkTopTablesFields; direction: Direction; } @@ -260,7 +260,7 @@ export enum FlowTargetSourceDest { source = 'source', } -export enum NetworkTopNFlowFields { +export enum NetworkTopTablesFields { bytes_in = 'bytes_in', bytes_out = 'bytes_out', flows = 'flows', @@ -426,7 +426,9 @@ export interface Source { KpiHosts: KpiHostsData; KpiHostDetails: KpiHostDetailsData; - /** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */ + + NetworkTopCountries: NetworkTopCountriesData; + NetworkTopNFlow: NetworkTopNFlowData; NetworkDns: NetworkDnsData; @@ -1456,6 +1458,68 @@ export interface KpiHostDetailsData { inspect?: Maybe; } +export interface NetworkTopCountriesData { + edges: NetworkTopCountriesEdges[]; + + totalCount: number; + + pageInfo: PageInfoPaginated; + + inspect?: Maybe; +} + +export interface NetworkTopCountriesEdges { + node: NetworkTopCountriesItem; + + cursor: CursorType; +} + +export interface NetworkTopCountriesItem { + _id?: Maybe; + + source?: Maybe; + + destination?: Maybe; + + network?: Maybe; +} + +export interface TopCountriesItemSource { + country?: Maybe; + + destination_ips?: Maybe; + + flows?: Maybe; + + location?: Maybe; + + source_ips?: Maybe; +} + +export interface GeoItem { + geo?: Maybe; + + flowTarget?: Maybe; +} + +export interface TopCountriesItemDestination { + country?: Maybe; + + destination_ips?: Maybe; + + flows?: Maybe; + + location?: Maybe; + + source_ips?: Maybe; +} + +export interface TopNetworkTablesEcsField { + bytes_in?: Maybe; + + bytes_out?: Maybe; +} + export interface NetworkTopNFlowData { edges: NetworkTopNFlowEdges[]; @@ -1479,7 +1543,7 @@ export interface NetworkTopNFlowItem { destination?: Maybe; - network?: Maybe; + network?: Maybe; } export interface TopNFlowItemSource { @@ -1502,12 +1566,6 @@ export interface AutonomousSystemItem { number?: Maybe; } -export interface GeoItem { - geo?: Maybe; - - flowTarget?: Maybe; -} - export interface TopNFlowItemDestination { autonomous_system?: Maybe; @@ -1522,12 +1580,6 @@ export interface TopNFlowItemDestination { source_ips?: Maybe; } -export interface TopNFlowNetworkEcsField { - bytes_in?: Maybe; - - bytes_out?: Maybe; -} - export interface NetworkDnsData { edges: NetworkDnsEdges[]; @@ -2061,6 +2113,23 @@ export interface KpiHostDetailsSourceArgs { defaultIndex: string[]; } +export interface NetworkTopCountriesSourceArgs { + id?: Maybe; + + filterQuery?: Maybe; + + ip?: Maybe; + + flowTarget: FlowTargetSourceDest; + + pagination: PaginationInputPaginated; + + sort: NetworkTopTablesSortField; + + timerange: TimerangeInput; + + defaultIndex: string[]; +} export interface NetworkTopNFlowSourceArgs { id?: Maybe; @@ -2072,7 +2141,7 @@ export interface NetworkTopNFlowSourceArgs { pagination: PaginationInputPaginated; - sort: NetworkTopNFlowSortField; + sort: NetworkTopTablesSortField; timerange: TimerangeInput; @@ -3071,13 +3140,127 @@ export namespace GetNetworkDnsQuery { }; } +export namespace GetNetworkTopCountriesQuery { + export type Variables = { + sourceId: string; + ip?: Maybe; + filterQuery?: Maybe; + pagination: PaginationInputPaginated; + sort: NetworkTopTablesSortField; + flowTarget: FlowTargetSourceDest; + timerange: TimerangeInput; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + NetworkTopCountries: NetworkTopCountries; + }; + + export type NetworkTopCountries = { + __typename?: 'NetworkTopCountriesData'; + + totalCount: number; + + edges: Edges[]; + + pageInfo: PageInfo; + + inspect: Maybe; + }; + + export type Edges = { + __typename?: 'NetworkTopCountriesEdges'; + + node: Node; + + cursor: Cursor; + }; + + export type Node = { + __typename?: 'NetworkTopCountriesItem'; + + source: Maybe<_Source>; + + destination: Maybe; + + network: Maybe; + }; + + export type _Source = { + __typename?: 'TopCountriesItemSource'; + + country: Maybe; + + destination_ips: Maybe; + + flows: Maybe; + + source_ips: Maybe; + }; + + export type Destination = { + __typename?: 'TopCountriesItemDestination'; + + country: Maybe; + + destination_ips: Maybe; + + flows: Maybe; + + source_ips: Maybe; + }; + + export type Network = { + __typename?: 'TopNetworkTablesEcsField'; + + bytes_in: Maybe; + + bytes_out: Maybe; + }; + + export type Cursor = { + __typename?: 'CursorType'; + + value: Maybe; + }; + + export type PageInfo = { + __typename?: 'PageInfoPaginated'; + + activePage: number; + + fakeTotalCount: number; + + showMorePagesIndicator: boolean; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + export namespace GetNetworkTopNFlowQuery { export type Variables = { sourceId: string; ip?: Maybe; filterQuery?: Maybe; pagination: PaginationInputPaginated; - sort: NetworkTopNFlowSortField; + sort: NetworkTopTablesSortField; flowTarget: FlowTargetSourceDest; timerange: TimerangeInput; defaultIndex: string[]; @@ -3225,7 +3408,7 @@ export namespace GetNetworkTopNFlowQuery { }; export type Network = { - __typename?: 'TopNFlowNetworkEcsField'; + __typename?: 'TopNetworkTablesEcsField'; bytes_in: Maybe; diff --git a/x-pack/legacy/plugins/siem/public/mock/global_state.ts b/x-pack/legacy/plugins/siem/public/mock/global_state.ts index 78315e0d3ddfb..ac6dfbf08ca95 100644 --- a/x-pack/legacy/plugins/siem/public/mock/global_state.ts +++ b/x-pack/legacy/plugins/siem/public/mock/global_state.ts @@ -10,7 +10,7 @@ import { FlowTarget, HostsFields, NetworkDnsFields, - NetworkTopNFlowFields, + NetworkTopTablesFields, TlsFields, UsersFields, } from '../graphql/types'; @@ -65,15 +65,25 @@ export const mockGlobalState: State = { network: { page: { queries: { + [networkModel.NetworkTableType.topCountriesDestination]: { + activePage: 0, + limit: 10, + topCountriesSort: { field: NetworkTopTablesFields.bytes_out, direction: Direction.desc }, + }, + [networkModel.NetworkTableType.topCountriesSource]: { + activePage: 0, + limit: 10, + topCountriesSort: { field: NetworkTopTablesFields.bytes_out, direction: Direction.desc }, + }, [networkModel.NetworkTableType.topNFlowSource]: { activePage: 0, limit: 10, - topNFlowSort: { field: NetworkTopNFlowFields.bytes_out, direction: Direction.desc }, + topNFlowSort: { field: NetworkTopTablesFields.bytes_out, direction: Direction.desc }, }, [networkModel.NetworkTableType.topNFlowDestination]: { activePage: 0, limit: 10, - topNFlowSort: { field: NetworkTopNFlowFields.bytes_out, direction: Direction.desc }, + topNFlowSort: { field: NetworkTopTablesFields.bytes_out, direction: Direction.desc }, }, [networkModel.NetworkTableType.dns]: { activePage: 0, @@ -86,15 +96,25 @@ export const mockGlobalState: State = { details: { flowTarget: FlowTarget.source, queries: { + [networkModel.IpDetailsTableType.topCountriesDestination]: { + activePage: 0, + limit: 10, + topCountriesSort: { field: NetworkTopTablesFields.bytes_out, direction: Direction.desc }, + }, + [networkModel.IpDetailsTableType.topCountriesSource]: { + activePage: 0, + limit: 10, + topCountriesSort: { field: NetworkTopTablesFields.bytes_out, direction: Direction.desc }, + }, [networkModel.IpDetailsTableType.topNFlowSource]: { activePage: 0, limit: 10, - topNFlowSort: { field: NetworkTopNFlowFields.bytes_out, direction: Direction.desc }, + topNFlowSort: { field: NetworkTopTablesFields.bytes_out, direction: Direction.desc }, }, [networkModel.IpDetailsTableType.topNFlowDestination]: { activePage: 0, limit: 10, - topNFlowSort: { field: NetworkTopNFlowFields.bytes_out, direction: Direction.desc }, + topNFlowSort: { field: NetworkTopTablesFields.bytes_out, direction: Direction.desc }, }, [networkModel.IpDetailsTableType.tls]: { activePage: 0, diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details.tsx b/x-pack/legacy/plugins/siem/public/pages/network/ip_details.tsx index 77364fa3a17b3..27d88e8475a65 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/ip_details.tsx @@ -40,6 +40,8 @@ import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '. import { setIpDetailsTablesActivePageToZero as dispatchIpDetailsTablesActivePageToZero } from '../../store/network/actions'; import { SpyRoute } from '../../utils/route/spy_routes'; import { NetworkEmptyPage } from './network_empty_page'; +import { NetworkTopCountriesQuery } from '../../containers/network_top_countries'; +import { NetworkTopCountriesTable } from '../../components/page/network/network_top_countries_table'; import * as i18n from './translations'; import { IPDetailsComponentProps } from './types'; @@ -47,6 +49,7 @@ const TlsTableManage = manageQuery(TlsTable); const UsersTableManage = manageQuery(UsersTable); const IpOverviewManage = manageQuery(IpOverview); const NetworkTopNFlowTableManage = manageQuery(NetworkTopNFlowTable); +const NetworkTopCountriesTableManage = manageQuery(NetworkTopCountriesTable); export const IPDetailsComponent = React.memo( ({ @@ -222,6 +225,94 @@ export const IPDetailsComponent = React.memo( + + + + {({ + id, + inspect, + isInspected, + loading, + loadPage, + networkTopCountries, + pageInfo, + refetch, + totalCount, + }) => ( + + )} + + + + + + {({ + id, + inspect, + isInspected, + loading, + loadPage, + networkTopCountries, + pageInfo, + refetch, + totalCount, + }) => ( + + )} + + + + + + ( + + {({ + id, + inspect, + isInspected, + loading, + loadPage, + networkTopCountries, + pageInfo, + refetch, + totalCount, + }) => ( + + )} + +); + +CountriesQueryTabBody.displayName = 'CountriesQueryTabBody'; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/nav_tabs.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/nav_tabs.tsx index c77eafe759752..d3a3feecc0d9d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/nav_tabs.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/nav_tabs.tsx @@ -19,6 +19,13 @@ export const navTabsNetwork = (hasMlUserPermissions: boolean): NetworkNavTab => disabled: false, urlKey: 'network', }, + [NetworkRouteType.countries]: { + id: NetworkRouteType.countries, + name: i18n.NAVIGATION_COUNTRIES_TITLE, + href: getTabsOnNetworkUrl(NetworkRouteType.countries), + disabled: false, + urlKey: 'network', + }, [NetworkRouteType.dns]: { id: NetworkRouteType.dns, name: i18n.NAVIGATION_DNS_TITLE, diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx index 0ed652804cc8e..cddd2d7aafed8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx @@ -12,6 +12,7 @@ import { FlowTargetSourceDest } from '../../../graphql/types'; import { scoreIntervalToDateTime } from '../../../components/ml/score/score_interval_to_datetime'; import { IPsQueryTabBody } from './ips_query_tab_body'; +import { CountriesQueryTabBody } from './countries_query_tab_body'; import { AnomaliesQueryTabBody } from './anomalies_query_tab_body'; import { DnsQueryTabBody } from './dns_query_tab_body'; import { ConditionalFlexGroup } from './conditional_flex_group'; @@ -79,6 +80,20 @@ export const NetworkRoutes = ({ )} /> + ( + + + + + + + + + + )} + /> } diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts b/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts index bcdc0ef7aa790..3d36193b7ea12 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts @@ -41,7 +41,9 @@ export type NetworkRoutesProps = GlobalTimeArgs & { setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; }; -export type KeyNetworkNavTabWithoutMlPermission = NetworkRouteType.ips & NetworkRouteType.dns; +export type KeyNetworkNavTabWithoutMlPermission = NetworkRouteType.countries & + NetworkRouteType.dns & + NetworkRouteType.ips; type KeyNetworkNavTabWithMlPermission = KeyNetworkNavTabWithoutMlPermission & NetworkRouteType.anomalies; @@ -52,6 +54,7 @@ export type NetworkNavTab = Record; export enum NetworkRouteType { ips = 'ips', + countries = 'countries', dns = 'dns', anomalies = 'anomalies', } diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/utils.ts b/x-pack/legacy/plugins/siem/public/pages/network/navigation/utils.ts index 9203c9d376db2..d6ad28757aee1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/utils.ts +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/utils.ts @@ -12,13 +12,14 @@ export const getNetworkRoutePath: GetNetworkRoutePath = ( hasMlUserPermission ) => { if (capabilitiesFetched && !hasMlUserPermission) { - return `${pagePath}/:tabName(${NetworkRouteType.ips}|${NetworkRouteType.dns})`; + return `${pagePath}/:tabName(${NetworkRouteType.ips}|${NetworkRouteType.dns}|${NetworkRouteType.countries})`; } return ( `${pagePath}/:tabName(` + `${NetworkRouteType.ips}|` + `${NetworkRouteType.dns}|` + + `${NetworkRouteType.countries}|` + `${NetworkRouteType.anomalies})` ); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/translations.ts b/x-pack/legacy/plugins/siem/public/pages/network/translations.ts index ad60e3df86b9a..14a74f5666f0e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/network/translations.ts @@ -27,16 +27,23 @@ export const EMPTY_ACTION_SECONDARY = i18n.translate('xpack.siem.network.emptyAc defaultMessage: 'Go to documentation', }); -export const NAVIGATION_IPS_TITLE = i18n.translate('xpack.siem.netowork.navigation.ipsTitle', { +export const NAVIGATION_IPS_TITLE = i18n.translate('xpack.siem.network.navigation.ipsTitle', { defaultMessage: 'IPs', }); -export const NAVIGATION_DNS_TITLE = i18n.translate('xpack.siem.netowork.navigation.dnsTitle', { +export const NAVIGATION_COUNTRIES_TITLE = i18n.translate( + 'xpack.siem.network.navigation.countriesTitle', + { + defaultMessage: 'Top Countries', + } +); + +export const NAVIGATION_DNS_TITLE = i18n.translate('xpack.siem.network.navigation.dnsTitle', { defaultMessage: 'DNS', }); export const NAVIGATION_ANOMALIES_TITLE = i18n.translate( - 'xpack.siem.netowork.navigation.anomaliesTitle', + 'xpack.siem.network.navigation.anomaliesTitle', { defaultMessage: 'Anomalies', } diff --git a/x-pack/legacy/plugins/siem/public/store/network/actions.ts b/x-pack/legacy/plugins/siem/public/store/network/actions.ts index 17d0dd2c5695c..3688e257344fa 100644 --- a/x-pack/legacy/plugins/siem/public/store/network/actions.ts +++ b/x-pack/legacy/plugins/siem/public/store/network/actions.ts @@ -9,7 +9,7 @@ import actionCreatorFactory from 'typescript-fsa'; import { FlowTarget, NetworkDnsSortField, - NetworkTopNFlowSortField, + NetworkTopTablesSortField, TlsSortField, UsersSortField, } from '../../graphql/types'; @@ -57,12 +57,23 @@ export const updateTopNFlowLimit = actionCreator<{ }>('UPDATE_TOP_N_FLOW_LIMIT'); export const updateTopNFlowSort = actionCreator<{ - topNFlowSort: NetworkTopNFlowSortField; + topNFlowSort: NetworkTopTablesSortField; networkType: networkModel.NetworkType; tableType: networkModel.TopNTableType; }>('UPDATE_TOP_N_FLOW_SORT'); -// IP Details Actions +export const updateTopCountriesLimit = actionCreator<{ + limit: number; + networkType: networkModel.NetworkType; + tableType: networkModel.TopCountriesTableType; +}>('UPDATE_TOP_COUNTRIES_LIMIT'); + +export const updateTopCountriesSort = actionCreator<{ + topCountriesSort: NetworkTopTablesSortField; + networkType: networkModel.NetworkType; + tableType: networkModel.TopCountriesTableType; +}>('UPDATE_TOP_COUNTRIES_SORT'); + export const updateIpDetailsFlowTarget = actionCreator<{ flowTarget: FlowTarget; }>('UPDATE_IP_DETAILS_TARGET'); diff --git a/x-pack/legacy/plugins/siem/public/store/network/helpers.test.ts b/x-pack/legacy/plugins/siem/public/store/network/helpers.test.ts index f76939c5d3e3d..31db0cea80dc9 100644 --- a/x-pack/legacy/plugins/siem/public/store/network/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/store/network/helpers.test.ts @@ -6,7 +6,7 @@ import { Direction, - NetworkTopNFlowFields, + NetworkTopTablesFields, NetworkDnsFields, TlsFields, UsersFields, @@ -19,11 +19,27 @@ import { setNetworkQueriesActivePageToZero } from './helpers'; export const mockNetworkState: NetworkModel = { page: { queries: { + [NetworkTableType.topCountriesSource]: { + activePage: 7, + limit: DEFAULT_TABLE_LIMIT, + topCountriesSort: { + field: NetworkTopTablesFields.bytes_out, + direction: Direction.desc, + }, + }, + [NetworkTableType.topCountriesDestination]: { + activePage: 3, + limit: DEFAULT_TABLE_LIMIT, + topCountriesSort: { + field: NetworkTopTablesFields.bytes_out, + direction: Direction.desc, + }, + }, [NetworkTableType.topNFlowSource]: { activePage: 7, limit: DEFAULT_TABLE_LIMIT, topNFlowSort: { - field: NetworkTopNFlowFields.bytes_out, + field: NetworkTopTablesFields.bytes_out, direction: Direction.desc, }, }, @@ -31,7 +47,7 @@ export const mockNetworkState: NetworkModel = { activePage: 3, limit: DEFAULT_TABLE_LIMIT, topNFlowSort: { - field: NetworkTopNFlowFields.bytes_out, + field: NetworkTopTablesFields.bytes_out, direction: Direction.desc, }, }, @@ -48,11 +64,27 @@ export const mockNetworkState: NetworkModel = { }, details: { queries: { + [IpDetailsTableType.topCountriesSource]: { + activePage: 7, + limit: DEFAULT_TABLE_LIMIT, + topCountriesSort: { + field: NetworkTopTablesFields.bytes_out, + direction: Direction.desc, + }, + }, + [IpDetailsTableType.topCountriesDestination]: { + activePage: 3, + limit: DEFAULT_TABLE_LIMIT, + topCountriesSort: { + field: NetworkTopTablesFields.bytes_out, + direction: Direction.desc, + }, + }, [IpDetailsTableType.topNFlowSource]: { activePage: 7, limit: DEFAULT_TABLE_LIMIT, topNFlowSort: { - field: NetworkTopNFlowFields.bytes_out, + field: NetworkTopTablesFields.bytes_out, direction: Direction.desc, }, }, @@ -60,7 +92,7 @@ export const mockNetworkState: NetworkModel = { activePage: 3, limit: DEFAULT_TABLE_LIMIT, topNFlowSort: { - field: NetworkTopNFlowFields.bytes_out, + field: NetworkTopTablesFields.bytes_out, direction: Direction.desc, }, }, @@ -87,7 +119,7 @@ export const mockNetworkState: NetworkModel = { describe('Network redux store', () => { describe('#setNetworkQueriesActivePageToZero', () => { - test('set activePage to zero for all queries in hosts page ', () => { + test('set activePage to zero for all queries in network page', () => { expect(setNetworkQueriesActivePageToZero(mockNetworkState, NetworkType.page)).toEqual({ [NetworkTableType.topNFlowSource]: { activePage: 0, @@ -105,10 +137,26 @@ describe('Network redux store', () => { dnsSortField: { field: 'uniqueDomains', direction: 'desc' }, isPtrIncluded: false, }, + [NetworkTableType.topCountriesDestination]: { + activePage: 0, + limit: 10, + topCountriesSort: { + direction: 'desc', + field: 'bytes_out', + }, + }, + [NetworkTableType.topCountriesSource]: { + activePage: 0, + limit: 10, + topCountriesSort: { + direction: 'desc', + field: 'bytes_out', + }, + }, }); }); - test('set activePage to zero for all queries in host details ', () => { + test('set activePage to zero for all queries in ip details ', () => { expect(setNetworkQueriesActivePageToZero(mockNetworkState, NetworkType.details)).toEqual({ [IpDetailsTableType.topNFlowSource]: { activePage: 0, @@ -120,6 +168,22 @@ describe('Network redux store', () => { limit: 10, topNFlowSort: { field: 'bytes_out', direction: 'desc' }, }, + [IpDetailsTableType.topCountriesDestination]: { + activePage: 0, + limit: 10, + topCountriesSort: { + direction: 'desc', + field: 'bytes_out', + }, + }, + [IpDetailsTableType.topCountriesSource]: { + activePage: 0, + limit: 10, + topCountriesSort: { + direction: 'desc', + field: 'bytes_out', + }, + }, [IpDetailsTableType.tls]: { activePage: 0, limit: 10, diff --git a/x-pack/legacy/plugins/siem/public/store/network/helpers.ts b/x-pack/legacy/plugins/siem/public/store/network/helpers.ts index b9876457625fb..60f4d6d1b4106 100644 --- a/x-pack/legacy/plugins/siem/public/store/network/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/store/network/helpers.ts @@ -16,6 +16,14 @@ import { DEFAULT_TABLE_ACTIVE_PAGE } from '../constants'; export const setNetworkPageQueriesActivePageToZero = (state: NetworkModel): NetworkQueries => ({ ...state.page.queries, + [NetworkTableType.topCountriesSource]: { + ...state.page.queries[NetworkTableType.topCountriesSource], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [NetworkTableType.topCountriesDestination]: { + ...state.page.queries[NetworkTableType.topCountriesDestination], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, [NetworkTableType.topNFlowSource]: { ...state.page.queries[NetworkTableType.topNFlowSource], activePage: DEFAULT_TABLE_ACTIVE_PAGE, @@ -34,6 +42,14 @@ export const setNetworkDetailsQueriesActivePageToZero = ( state: NetworkModel ): IpOverviewQueries => ({ ...state.details.queries, + [IpDetailsTableType.topCountriesSource]: { + ...state.details.queries[IpDetailsTableType.topCountriesSource], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [IpDetailsTableType.topCountriesDestination]: { + ...state.details.queries[IpDetailsTableType.topCountriesDestination], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, [IpDetailsTableType.topNFlowSource]: { ...state.details.queries[IpDetailsTableType.topNFlowSource], activePage: DEFAULT_TABLE_ACTIVE_PAGE, diff --git a/x-pack/legacy/plugins/siem/public/store/network/model.ts b/x-pack/legacy/plugins/siem/public/store/network/model.ts index deaca981b1e0d..294177bd426b2 100644 --- a/x-pack/legacy/plugins/siem/public/store/network/model.ts +++ b/x-pack/legacy/plugins/siem/public/store/network/model.ts @@ -7,7 +7,7 @@ import { FlowTarget, NetworkDnsSortField, - NetworkTopNFlowSortField, + NetworkTopTablesSortField, TlsSortField, UsersSortField, } from '../../graphql/types'; @@ -19,20 +19,30 @@ export enum NetworkType { export enum NetworkTableType { dns = 'dns', - topNFlowSource = 'topNFlowSource', + topCountriesDestination = 'topCountriesDestination', + topCountriesSource = 'topCountriesSource', topNFlowDestination = 'topNFlowDestination', + topNFlowSource = 'topNFlowSource', } export type TopNTableType = - | NetworkTableType.topNFlowDestination - | NetworkTableType.topNFlowSource | IpDetailsTableType.topNFlowDestination - | IpDetailsTableType.topNFlowSource; + | IpDetailsTableType.topNFlowSource + | NetworkTableType.topNFlowDestination + | NetworkTableType.topNFlowSource; + +export type TopCountriesTableType = + | IpDetailsTableType.topCountriesDestination + | IpDetailsTableType.topCountriesSource + | NetworkTableType.topCountriesDestination + | NetworkTableType.topCountriesSource; export enum IpDetailsTableType { - topNFlowSource = 'topNFlowSourceIp', - topNFlowDestination = 'topNFlowDestinationIp', tls = 'tls', + topCountriesDestination = 'topCountriesDestinationIp', + topCountriesSource = 'topCountriesSourceIp', + topNFlowDestination = 'topNFlowDestinationIp', + topNFlowSource = 'topNFlowSourceIp', users = 'users', } @@ -43,7 +53,11 @@ export interface BasicQueryPaginated { // Network Page Models export interface TopNFlowQuery extends BasicQueryPaginated { - topNFlowSort: NetworkTopNFlowSortField; + topNFlowSort: NetworkTopTablesSortField; +} + +export interface TopCountriesQuery extends BasicQueryPaginated { + topCountriesSort: NetworkTopTablesSortField; } export interface DnsQuery extends BasicQueryPaginated { @@ -53,8 +67,10 @@ export interface DnsQuery extends BasicQueryPaginated { export interface NetworkQueries { [NetworkTableType.dns]: DnsQuery; - [NetworkTableType.topNFlowSource]: TopNFlowQuery; + [NetworkTableType.topCountriesDestination]: TopCountriesQuery; + [NetworkTableType.topCountriesSource]: TopCountriesQuery; [NetworkTableType.topNFlowDestination]: TopNFlowQuery; + [NetworkTableType.topNFlowSource]: TopNFlowQuery; } export interface NetworkPageModel { @@ -72,9 +88,11 @@ export interface UsersQuery extends BasicQueryPaginated { } export interface IpOverviewQueries { - [IpDetailsTableType.topNFlowSource]: TopNFlowQuery; - [IpDetailsTableType.topNFlowDestination]: TopNFlowQuery; [IpDetailsTableType.tls]: TlsQuery; + [IpDetailsTableType.topCountriesDestination]: TopCountriesQuery; + [IpDetailsTableType.topCountriesSource]: TopCountriesQuery; + [IpDetailsTableType.topNFlowDestination]: TopNFlowQuery; + [IpDetailsTableType.topNFlowSource]: TopNFlowQuery; [IpDetailsTableType.users]: UsersQuery; } diff --git a/x-pack/legacy/plugins/siem/public/store/network/reducer.ts b/x-pack/legacy/plugins/siem/public/store/network/reducer.ts index 74281bc2a4a5a..6a7c014707cbb 100644 --- a/x-pack/legacy/plugins/siem/public/store/network/reducer.ts +++ b/x-pack/legacy/plugins/siem/public/store/network/reducer.ts @@ -10,7 +10,7 @@ import { Direction, FlowTarget, NetworkDnsFields, - NetworkTopNFlowFields, + NetworkTopTablesFields, TlsFields, UsersFields, } from '../../graphql/types'; @@ -24,6 +24,8 @@ import { updateIpDetailsTableActivePage, updateIsPtrIncluded, updateNetworkPageTableActivePage, + updateTopCountriesLimit, + updateTopCountriesSort, updateTlsLimit, updateTlsSort, updateTopNFlowLimit, @@ -46,7 +48,7 @@ export const initialNetworkState: NetworkState = { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, topNFlowSort: { - field: NetworkTopNFlowFields.bytes_out, + field: NetworkTopTablesFields.bytes_out, direction: Direction.desc, }, }, @@ -54,7 +56,7 @@ export const initialNetworkState: NetworkState = { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, topNFlowSort: { - field: NetworkTopNFlowFields.bytes_out, + field: NetworkTopTablesFields.bytes_out, direction: Direction.desc, }, }, @@ -67,15 +69,47 @@ export const initialNetworkState: NetworkState = { }, isPtrIncluded: false, }, + [NetworkTableType.topCountriesSource]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + topCountriesSort: { + field: NetworkTopTablesFields.bytes_out, + direction: Direction.desc, + }, + }, + [NetworkTableType.topCountriesDestination]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + topCountriesSort: { + field: NetworkTopTablesFields.bytes_out, + direction: Direction.desc, + }, + }, }, }, details: { queries: { + [IpDetailsTableType.topCountriesSource]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + topCountriesSort: { + field: NetworkTopTablesFields.bytes_out, + direction: Direction.desc, + }, + }, + [IpDetailsTableType.topCountriesDestination]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + topCountriesSort: { + field: NetworkTopTablesFields.bytes_out, + direction: Direction.desc, + }, + }, [IpDetailsTableType.topNFlowSource]: { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, topNFlowSort: { - field: NetworkTopNFlowFields.bytes_out, + field: NetworkTopTablesFields.bytes_out, direction: Direction.desc, }, }, @@ -83,7 +117,7 @@ export const initialNetworkState: NetworkState = { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, topNFlowSort: { - field: NetworkTopNFlowFields.bytes_out, + field: NetworkTopTablesFields.bytes_out, direction: Direction.desc, }, }, @@ -263,6 +297,84 @@ export const networkReducer = reducerWithInitialState(initialNetworkState) } return state; }) + .case(updateTopCountriesLimit, (state, { limit, networkType, tableType }) => { + if ( + networkType === NetworkType.page && + (tableType === NetworkTableType.topCountriesSource || + tableType === NetworkTableType.topCountriesDestination) + ) { + return { + ...state, + [networkType]: { + ...state[networkType], + queries: { + ...state[networkType].queries, + [tableType]: { + ...state[networkType].queries[tableType], + limit, + }, + }, + }, + }; + } else if ( + tableType === IpDetailsTableType.topCountriesDestination || + tableType === IpDetailsTableType.topCountriesSource + ) { + return { + ...state, + [NetworkType.details]: { + ...state[NetworkType.details], + queries: { + ...state[NetworkType.details].queries, + [tableType]: { + ...state[NetworkType.details].queries[tableType], + limit, + }, + }, + }, + }; + } + return state; + }) + .case(updateTopCountriesSort, (state, { topCountriesSort, networkType, tableType }) => { + if ( + networkType === NetworkType.page && + (tableType === NetworkTableType.topCountriesSource || + tableType === NetworkTableType.topCountriesDestination) + ) { + return { + ...state, + [networkType]: { + ...state[networkType], + queries: { + ...state[networkType].queries, + [tableType]: { + ...state[networkType].queries[tableType], + topCountriesSort, + }, + }, + }, + }; + } else if ( + tableType === IpDetailsTableType.topCountriesDestination || + tableType === IpDetailsTableType.topCountriesSource + ) { + return { + ...state, + [NetworkType.details]: { + ...state[NetworkType.details], + queries: { + ...state[NetworkType.details].queries, + [tableType]: { + ...state[NetworkType.details].queries[tableType], + topCountriesSort, + }, + }, + }, + }; + } + return state; + }) .case(updateIpDetailsFlowTarget, (state, { flowTarget }) => ({ ...state, [NetworkType.details]: { diff --git a/x-pack/legacy/plugins/siem/public/store/network/selectors.ts b/x-pack/legacy/plugins/siem/public/store/network/selectors.ts index 9987d4d4044b3..8f86cad3da2e2 100644 --- a/x-pack/legacy/plugins/siem/public/store/network/selectors.ts +++ b/x-pack/legacy/plugins/siem/public/store/network/selectors.ts @@ -23,8 +23,10 @@ export const dnsSelector = () => ); export enum NetworkTableType { dns = 'dns', - topNFlowSource = 'topNFlowSource', + topCountriesDestination = 'topCountriesDestination', + topCountriesSource = 'topCountriesSource', topNFlowDestination = 'topNFlowDestination', + topNFlowSource = 'topNFlowSource', } export const topNFlowSelector = (flowTarget: FlowTargetSourceDest, networkType: NetworkType) => { if (networkType === NetworkType.page) { @@ -45,6 +47,28 @@ export const topNFlowSelector = (flowTarget: FlowTargetSourceDest, networkType: ); }; +export const topCountriesSelector = ( + flowTarget: FlowTargetSourceDest, + networkType: NetworkType +) => { + if (networkType === NetworkType.page) { + return createSelector( + selectNetworkPage, + network => + flowTarget === FlowTargetSourceDest.source + ? network.queries[NetworkTableType.topCountriesSource] + : network.queries[NetworkTableType.topCountriesDestination] + ); + } + return createSelector( + selectNetworkDetails, + network => + flowTarget === FlowTargetSourceDest.source + ? network.queries[IpDetailsTableType.topCountriesSource] + : network.queries[IpDetailsTableType.topCountriesDestination] + ); +}; + // IP Details Selectors export const ipDetailsFlowTargetSelector = () => createSelector( diff --git a/x-pack/legacy/plugins/siem/server/graphql/network/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/network/resolvers.ts index 23a8d4694ccae..7ce88ba4880be 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/network/resolvers.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/network/resolvers.ts @@ -10,6 +10,11 @@ import { Network } from '../../lib/network'; import { createOptionsPaginated } from '../../utils/build_query/create_options'; import { QuerySourceResolver } from '../sources/resolvers'; +type QueryNetworkTopCountriesResolver = ChildResolverOf< + AppResolverOf, + QuerySourceResolver +>; + type QueryNetworkTopNFlowResolver = ChildResolverOf< AppResolverOf, QuerySourceResolver @@ -28,11 +33,21 @@ export const createNetworkResolvers = ( libs: NetworkResolversDeps ): { Source: { + NetworkTopCountries: QueryNetworkTopCountriesResolver; NetworkTopNFlow: QueryNetworkTopNFlowResolver; NetworkDns: QueryDnsResolver; }; } => ({ Source: { + async NetworkTopCountries(source, args, { req }, info) { + const options = { + ...createOptionsPaginated(source, args, info), + flowTarget: args.flowTarget, + networkTopCountriesSort: args.sort, + ip: args.ip, + }; + return libs.network.getNetworkTopCountries(req, options); + }, async NetworkTopNFlow(source, args, { req }, info) { const options = { ...createOptionsPaginated(source, args, info), diff --git a/x-pack/legacy/plugins/siem/server/graphql/network/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/network/schema.gql.ts index acd19b6efc0ed..36b57ec9368ab 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/network/schema.gql.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/network/schema.gql.ts @@ -18,7 +18,7 @@ export const networkSchema = gql` unknown } - type TopNFlowNetworkEcsField { + type TopNetworkTablesEcsField { bytes_in: Float bytes_out: Float } @@ -33,6 +33,41 @@ export const networkSchema = gql` number: Float } + type TopCountriesItemSource { + country: String + destination_ips: Float + flows: Float + location: GeoItem + source_ips: Float + } + + type TopCountriesItemDestination { + country: String + destination_ips: Float + flows: Float + location: GeoItem + source_ips: Float + } + + type NetworkTopCountriesItem { + _id: String + source: TopCountriesItemSource + destination: TopCountriesItemDestination + network: TopNetworkTablesEcsField + } + + type NetworkTopCountriesEdges { + node: NetworkTopCountriesItem! + cursor: CursorType! + } + + type NetworkTopCountriesData { + edges: [NetworkTopCountriesEdges!]! + totalCount: Float! + pageInfo: PageInfoPaginated! + inspect: Inspect + } + type TopNFlowItemSource { autonomous_system: AutonomousSystemItem domain: [String!] @@ -51,7 +86,7 @@ export const networkSchema = gql` source_ips: Float } - enum NetworkTopNFlowFields { + enum NetworkTopTablesFields { bytes_in bytes_out flows @@ -59,8 +94,8 @@ export const networkSchema = gql` source_ips } - input NetworkTopNFlowSortField { - field: NetworkTopNFlowFields! + input NetworkTopTablesSortField { + field: NetworkTopTablesFields! direction: Direction! } @@ -68,7 +103,7 @@ export const networkSchema = gql` _id: String source: TopNFlowItemSource destination: TopNFlowItemDestination - network: TopNFlowNetworkEcsField + network: TopNetworkTablesEcsField } type NetworkTopNFlowEdges { @@ -118,14 +153,23 @@ export const networkSchema = gql` } extend type Source { - "Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified" + NetworkTopCountries( + id: String + filterQuery: String + ip: String + flowTarget: FlowTargetSourceDest! + pagination: PaginationInputPaginated! + sort: NetworkTopTablesSortField! + timerange: TimerangeInput! + defaultIndex: [String!]! + ): NetworkTopCountriesData! NetworkTopNFlow( id: String filterQuery: String ip: String flowTarget: FlowTargetSourceDest! pagination: PaginationInputPaginated! - sort: NetworkTopNFlowSortField! + sort: NetworkTopTablesSortField! timerange: TimerangeInput! defaultIndex: [String!]! ): NetworkTopNFlowData! diff --git a/x-pack/legacy/plugins/siem/server/graphql/types.ts b/x-pack/legacy/plugins/siem/server/graphql/types.ts index 8505d3efc4341..bce1f0139c49e 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/types.ts @@ -81,8 +81,8 @@ export interface UsersSortField { direction: Direction; } -export interface NetworkTopNFlowSortField { - field: NetworkTopNFlowFields; +export interface NetworkTopTablesSortField { + field: NetworkTopTablesFields; direction: Direction; } @@ -262,7 +262,7 @@ export enum FlowTargetSourceDest { source = 'source', } -export enum NetworkTopNFlowFields { +export enum NetworkTopTablesFields { bytes_in = 'bytes_in', bytes_out = 'bytes_out', flows = 'flows', @@ -428,7 +428,9 @@ export interface Source { KpiHosts: KpiHostsData; KpiHostDetails: KpiHostDetailsData; - /** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */ + + NetworkTopCountries: NetworkTopCountriesData; + NetworkTopNFlow: NetworkTopNFlowData; NetworkDns: NetworkDnsData; @@ -1458,6 +1460,68 @@ export interface KpiHostDetailsData { inspect?: Maybe; } +export interface NetworkTopCountriesData { + edges: NetworkTopCountriesEdges[]; + + totalCount: number; + + pageInfo: PageInfoPaginated; + + inspect?: Maybe; +} + +export interface NetworkTopCountriesEdges { + node: NetworkTopCountriesItem; + + cursor: CursorType; +} + +export interface NetworkTopCountriesItem { + _id?: Maybe; + + source?: Maybe; + + destination?: Maybe; + + network?: Maybe; +} + +export interface TopCountriesItemSource { + country?: Maybe; + + destination_ips?: Maybe; + + flows?: Maybe; + + location?: Maybe; + + source_ips?: Maybe; +} + +export interface GeoItem { + geo?: Maybe; + + flowTarget?: Maybe; +} + +export interface TopCountriesItemDestination { + country?: Maybe; + + destination_ips?: Maybe; + + flows?: Maybe; + + location?: Maybe; + + source_ips?: Maybe; +} + +export interface TopNetworkTablesEcsField { + bytes_in?: Maybe; + + bytes_out?: Maybe; +} + export interface NetworkTopNFlowData { edges: NetworkTopNFlowEdges[]; @@ -1481,7 +1545,7 @@ export interface NetworkTopNFlowItem { destination?: Maybe; - network?: Maybe; + network?: Maybe; } export interface TopNFlowItemSource { @@ -1504,12 +1568,6 @@ export interface AutonomousSystemItem { number?: Maybe; } -export interface GeoItem { - geo?: Maybe; - - flowTarget?: Maybe; -} - export interface TopNFlowItemDestination { autonomous_system?: Maybe; @@ -1524,12 +1582,6 @@ export interface TopNFlowItemDestination { source_ips?: Maybe; } -export interface TopNFlowNetworkEcsField { - bytes_in?: Maybe; - - bytes_out?: Maybe; -} - export interface NetworkDnsData { edges: NetworkDnsEdges[]; @@ -2063,6 +2115,23 @@ export interface KpiHostDetailsSourceArgs { defaultIndex: string[]; } +export interface NetworkTopCountriesSourceArgs { + id?: Maybe; + + filterQuery?: Maybe; + + ip?: Maybe; + + flowTarget: FlowTargetSourceDest; + + pagination: PaginationInputPaginated; + + sort: NetworkTopTablesSortField; + + timerange: TimerangeInput; + + defaultIndex: string[]; +} export interface NetworkTopNFlowSourceArgs { id?: Maybe; @@ -2074,7 +2143,7 @@ export interface NetworkTopNFlowSourceArgs { pagination: PaginationInputPaginated; - sort: NetworkTopNFlowSortField; + sort: NetworkTopTablesSortField; timerange: TimerangeInput; @@ -2542,7 +2611,13 @@ export namespace SourceResolvers { KpiHosts?: KpiHostsResolver; KpiHostDetails?: KpiHostDetailsResolver; - /** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */ + + NetworkTopCountries?: NetworkTopCountriesResolver< + NetworkTopCountriesData, + TypeParent, + TContext + >; + NetworkTopNFlow?: NetworkTopNFlowResolver; NetworkDns?: NetworkDnsResolver; @@ -2802,6 +2877,29 @@ export namespace SourceResolvers { defaultIndex: string[]; } + export type NetworkTopCountriesResolver< + R = NetworkTopCountriesData, + Parent = Source, + TContext = SiemContext + > = Resolver; + export interface NetworkTopCountriesArgs { + id?: Maybe; + + filterQuery?: Maybe; + + ip?: Maybe; + + flowTarget: FlowTargetSourceDest; + + pagination: PaginationInputPaginated; + + sort: NetworkTopTablesSortField; + + timerange: TimerangeInput; + + defaultIndex: string[]; + } + export type NetworkTopNFlowResolver< R = NetworkTopNFlowData, Parent = Source, @@ -2818,7 +2916,7 @@ export namespace SourceResolvers { pagination: PaginationInputPaginated; - sort: NetworkTopNFlowSortField; + sort: NetworkTopTablesSortField; timerange: TimerangeInput; @@ -6305,6 +6403,209 @@ export namespace KpiHostDetailsDataResolvers { > = Resolver; } +export namespace NetworkTopCountriesDataResolvers { + export interface Resolvers { + edges?: EdgesResolver; + + totalCount?: TotalCountResolver; + + pageInfo?: PageInfoResolver; + + inspect?: InspectResolver, TypeParent, TContext>; + } + + export type EdgesResolver< + R = NetworkTopCountriesEdges[], + Parent = NetworkTopCountriesData, + TContext = SiemContext + > = Resolver; + export type TotalCountResolver< + R = number, + Parent = NetworkTopCountriesData, + TContext = SiemContext + > = Resolver; + export type PageInfoResolver< + R = PageInfoPaginated, + Parent = NetworkTopCountriesData, + TContext = SiemContext + > = Resolver; + export type InspectResolver< + R = Maybe, + Parent = NetworkTopCountriesData, + TContext = SiemContext + > = Resolver; +} + +export namespace NetworkTopCountriesEdgesResolvers { + export interface Resolvers { + node?: NodeResolver; + + cursor?: CursorResolver; + } + + export type NodeResolver< + R = NetworkTopCountriesItem, + Parent = NetworkTopCountriesEdges, + TContext = SiemContext + > = Resolver; + export type CursorResolver< + R = CursorType, + Parent = NetworkTopCountriesEdges, + TContext = SiemContext + > = Resolver; +} + +export namespace NetworkTopCountriesItemResolvers { + export interface Resolvers { + _id?: _IdResolver, TypeParent, TContext>; + + source?: SourceResolver, TypeParent, TContext>; + + destination?: DestinationResolver, TypeParent, TContext>; + + network?: NetworkResolver, TypeParent, TContext>; + } + + export type _IdResolver< + R = Maybe, + Parent = NetworkTopCountriesItem, + TContext = SiemContext + > = Resolver; + export type SourceResolver< + R = Maybe, + Parent = NetworkTopCountriesItem, + TContext = SiemContext + > = Resolver; + export type DestinationResolver< + R = Maybe, + Parent = NetworkTopCountriesItem, + TContext = SiemContext + > = Resolver; + export type NetworkResolver< + R = Maybe, + Parent = NetworkTopCountriesItem, + TContext = SiemContext + > = Resolver; +} + +export namespace TopCountriesItemSourceResolvers { + export interface Resolvers { + country?: CountryResolver, TypeParent, TContext>; + + destination_ips?: DestinationIpsResolver, TypeParent, TContext>; + + flows?: FlowsResolver, TypeParent, TContext>; + + location?: LocationResolver, TypeParent, TContext>; + + source_ips?: SourceIpsResolver, TypeParent, TContext>; + } + + export type CountryResolver< + R = Maybe, + Parent = TopCountriesItemSource, + TContext = SiemContext + > = Resolver; + export type DestinationIpsResolver< + R = Maybe, + Parent = TopCountriesItemSource, + TContext = SiemContext + > = Resolver; + export type FlowsResolver< + R = Maybe, + Parent = TopCountriesItemSource, + TContext = SiemContext + > = Resolver; + export type LocationResolver< + R = Maybe, + Parent = TopCountriesItemSource, + TContext = SiemContext + > = Resolver; + export type SourceIpsResolver< + R = Maybe, + Parent = TopCountriesItemSource, + TContext = SiemContext + > = Resolver; +} + +export namespace GeoItemResolvers { + export interface Resolvers { + geo?: GeoResolver, TypeParent, TContext>; + + flowTarget?: FlowTargetResolver, TypeParent, TContext>; + } + + export type GeoResolver< + R = Maybe, + Parent = GeoItem, + TContext = SiemContext + > = Resolver; + export type FlowTargetResolver< + R = Maybe, + Parent = GeoItem, + TContext = SiemContext + > = Resolver; +} + +export namespace TopCountriesItemDestinationResolvers { + export interface Resolvers { + country?: CountryResolver, TypeParent, TContext>; + + destination_ips?: DestinationIpsResolver, TypeParent, TContext>; + + flows?: FlowsResolver, TypeParent, TContext>; + + location?: LocationResolver, TypeParent, TContext>; + + source_ips?: SourceIpsResolver, TypeParent, TContext>; + } + + export type CountryResolver< + R = Maybe, + Parent = TopCountriesItemDestination, + TContext = SiemContext + > = Resolver; + export type DestinationIpsResolver< + R = Maybe, + Parent = TopCountriesItemDestination, + TContext = SiemContext + > = Resolver; + export type FlowsResolver< + R = Maybe, + Parent = TopCountriesItemDestination, + TContext = SiemContext + > = Resolver; + export type LocationResolver< + R = Maybe, + Parent = TopCountriesItemDestination, + TContext = SiemContext + > = Resolver; + export type SourceIpsResolver< + R = Maybe, + Parent = TopCountriesItemDestination, + TContext = SiemContext + > = Resolver; +} + +export namespace TopNetworkTablesEcsFieldResolvers { + export interface Resolvers { + bytes_in?: BytesInResolver, TypeParent, TContext>; + + bytes_out?: BytesOutResolver, TypeParent, TContext>; + } + + export type BytesInResolver< + R = Maybe, + Parent = TopNetworkTablesEcsField, + TContext = SiemContext + > = Resolver; + export type BytesOutResolver< + R = Maybe, + Parent = TopNetworkTablesEcsField, + TContext = SiemContext + > = Resolver; +} + export namespace NetworkTopNFlowDataResolvers { export interface Resolvers { edges?: EdgesResolver; @@ -6365,7 +6666,7 @@ export namespace NetworkTopNFlowItemResolvers { destination?: DestinationResolver, TypeParent, TContext>; - network?: NetworkResolver, TypeParent, TContext>; + network?: NetworkResolver, TypeParent, TContext>; } export type _IdResolver< @@ -6384,7 +6685,7 @@ export namespace NetworkTopNFlowItemResolvers { TContext = SiemContext > = Resolver; export type NetworkResolver< - R = Maybe, + R = Maybe, Parent = NetworkTopNFlowItem, TContext = SiemContext > = Resolver; @@ -6456,25 +6757,6 @@ export namespace AutonomousSystemItemResolvers { > = Resolver; } -export namespace GeoItemResolvers { - export interface Resolvers { - geo?: GeoResolver, TypeParent, TContext>; - - flowTarget?: FlowTargetResolver, TypeParent, TContext>; - } - - export type GeoResolver< - R = Maybe, - Parent = GeoItem, - TContext = SiemContext - > = Resolver; - export type FlowTargetResolver< - R = Maybe, - Parent = GeoItem, - TContext = SiemContext - > = Resolver; -} - export namespace TopNFlowItemDestinationResolvers { export interface Resolvers { autonomous_system?: AutonomousSystemResolver, TypeParent, TContext>; @@ -6522,25 +6804,6 @@ export namespace TopNFlowItemDestinationResolvers { > = Resolver; } -export namespace TopNFlowNetworkEcsFieldResolvers { - export interface Resolvers { - bytes_in?: BytesInResolver, TypeParent, TContext>; - - bytes_out?: BytesOutResolver, TypeParent, TContext>; - } - - export type BytesInResolver< - R = Maybe, - Parent = TopNFlowNetworkEcsField, - TContext = SiemContext - > = Resolver; - export type BytesOutResolver< - R = Maybe, - Parent = TopNFlowNetworkEcsField, - TContext = SiemContext - > = Resolver; -} - export namespace NetworkDnsDataResolvers { export interface Resolvers { edges?: EdgesResolver; @@ -7922,14 +8185,19 @@ export type IResolvers = { KpiHostsData?: KpiHostsDataResolvers.Resolvers; KpiHostHistogramData?: KpiHostHistogramDataResolvers.Resolvers; KpiHostDetailsData?: KpiHostDetailsDataResolvers.Resolvers; + NetworkTopCountriesData?: NetworkTopCountriesDataResolvers.Resolvers; + NetworkTopCountriesEdges?: NetworkTopCountriesEdgesResolvers.Resolvers; + NetworkTopCountriesItem?: NetworkTopCountriesItemResolvers.Resolvers; + TopCountriesItemSource?: TopCountriesItemSourceResolvers.Resolvers; + GeoItem?: GeoItemResolvers.Resolvers; + TopCountriesItemDestination?: TopCountriesItemDestinationResolvers.Resolvers; + TopNetworkTablesEcsField?: TopNetworkTablesEcsFieldResolvers.Resolvers; NetworkTopNFlowData?: NetworkTopNFlowDataResolvers.Resolvers; NetworkTopNFlowEdges?: NetworkTopNFlowEdgesResolvers.Resolvers; NetworkTopNFlowItem?: NetworkTopNFlowItemResolvers.Resolvers; TopNFlowItemSource?: TopNFlowItemSourceResolvers.Resolvers; AutonomousSystemItem?: AutonomousSystemItemResolvers.Resolvers; - GeoItem?: GeoItemResolvers.Resolvers; TopNFlowItemDestination?: TopNFlowItemDestinationResolvers.Resolvers; - TopNFlowNetworkEcsField?: TopNFlowNetworkEcsFieldResolvers.Resolvers; NetworkDnsData?: NetworkDnsDataResolvers.Resolvers; NetworkDnsEdges?: NetworkDnsEdgesResolvers.Resolvers; NetworkDnsItem?: NetworkDnsItemResolvers.Resolvers; diff --git a/x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts index 0b787a08cec17..5a871a3f9c9b4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/network/elasticsearch_adapter.ts @@ -12,6 +12,8 @@ import { GeoItem, NetworkDnsData, NetworkDnsEdges, + NetworkTopCountriesData, + NetworkTopCountriesEdges, NetworkTopNFlowData, NetworkTopNFlowEdges, } from '../../graphql/types'; @@ -20,14 +22,63 @@ import { DatabaseSearchResponse, FrameworkAdapter, FrameworkRequest } from '../f import { TermAggregation } from '../types'; import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants'; -import { NetworkDnsRequestOptions, NetworkTopNFlowRequestOptions } from './index'; +import { + NetworkDnsRequestOptions, + NetworkTopCountriesRequestOptions, + NetworkTopNFlowRequestOptions, +} from './index'; import { buildDnsQuery } from './query_dns.dsl'; import { buildTopNFlowQuery, getOppositeField } from './query_top_n_flow.dsl'; -import { NetworkAdapter, NetworkDnsBuckets, NetworkTopNFlowBuckets } from './types'; +import { buildTopCountriesQuery } from './query_top_countries.dsl'; +import { + NetworkAdapter, + NetworkDnsBuckets, + NetworkTopCountriesBuckets, + NetworkTopNFlowBuckets, +} from './types'; export class ElasticsearchNetworkAdapter implements NetworkAdapter { constructor(private readonly framework: FrameworkAdapter) {} + public async getNetworkTopCountries( + request: FrameworkRequest, + options: NetworkTopCountriesRequestOptions + ): Promise { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + const dsl = buildTopCountriesQuery(options); + const response = await this.framework.callWithRequest( + request, + 'search', + dsl + ); + const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + const totalCount = getOr(0, 'aggregations.top_countries_count.value', response); + const networkTopCountriesEdges: NetworkTopCountriesEdges[] = getTopCountriesEdges( + response, + options + ); + const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; + const edges = networkTopCountriesEdges.splice(cursorStart, querySize - cursorStart); + const inspect = { + dsl: [inspectStringifyObject(dsl)], + response: [inspectStringifyObject(response)], + }; + const showMorePagesIndicator = totalCount > fakeTotalCount; + + return { + edges, + inspect, + pageInfo: { + activePage: activePage ? activePage : 0, + fakeTotalCount, + showMorePagesIndicator, + }, + totalCount, + }; + } + public async getNetworkTopNFlow( request: FrameworkRequest, options: NetworkTopNFlowRequestOptions @@ -112,6 +163,16 @@ const getTopNFlowEdges = ( ); }; +const getTopCountriesEdges = ( + response: DatabaseSearchResponse, + options: NetworkTopCountriesRequestOptions +): NetworkTopCountriesEdges[] => { + return formatTopCountriesEdges( + getOr([], `aggregations.${options.flowTarget}.buckets`, response), + options.flowTarget + ); +}; + const getFlowTargetFromString = (flowAsString: string) => flowAsString === 'source' ? FlowTargetSourceDest.source : FlowTargetSourceDest.destination; @@ -181,6 +242,34 @@ const formatTopNFlowEdges = ( }, })); +const formatTopCountriesEdges = ( + buckets: NetworkTopCountriesBuckets[], + flowTarget: FlowTargetSourceDest +): NetworkTopCountriesEdges[] => + buckets.map((bucket: NetworkTopCountriesBuckets) => ({ + node: { + _id: bucket.key, + [flowTarget]: { + country: bucket.key, + flows: getOr(0, 'flows.value', bucket), + [`${getOppositeField(flowTarget)}_ips`]: getOr( + 0, + `${getOppositeField(flowTarget)}_ips.value`, + bucket + ), + [`${flowTarget}_ips`]: getOr(0, `${flowTarget}_ips.value`, bucket), + }, + network: { + bytes_in: getOr(0, 'bytes_in.value', bucket), + bytes_out: getOr(0, 'bytes_out.value', bucket), + }, + }, + cursor: { + value: bucket.key, + tiebreaker: null, + }, + })); + const formatDnsEdges = (buckets: NetworkDnsBuckets[]): NetworkDnsEdges[] => buckets.map((bucket: NetworkDnsBuckets) => ({ node: { diff --git a/x-pack/legacy/plugins/siem/server/lib/network/index.ts b/x-pack/legacy/plugins/siem/server/lib/network/index.ts index c183122af998f..e391dd922ce31 100644 --- a/x-pack/legacy/plugins/siem/server/lib/network/index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/network/index.ts @@ -8,8 +8,9 @@ import { FlowTargetSourceDest, Maybe, NetworkDnsSortField, + NetworkTopCountriesData, NetworkTopNFlowData, - NetworkTopNFlowSortField, + NetworkTopTablesSortField, } from '../../graphql/types'; import { FrameworkRequest, RequestOptionsPaginated } from '../framework'; export * from './elasticsearch_adapter'; @@ -18,7 +19,13 @@ import { NetworkAdapter } from './types'; export * from './types'; export interface NetworkTopNFlowRequestOptions extends RequestOptionsPaginated { - networkTopNFlowSort: NetworkTopNFlowSortField; + networkTopNFlowSort: NetworkTopTablesSortField; + flowTarget: FlowTargetSourceDest; + ip?: Maybe; +} + +export interface NetworkTopCountriesRequestOptions extends RequestOptionsPaginated { + networkTopCountriesSort: NetworkTopTablesSortField; flowTarget: FlowTargetSourceDest; ip?: Maybe; } @@ -31,6 +38,13 @@ export interface NetworkDnsRequestOptions extends RequestOptionsPaginated { export class Network { constructor(private readonly adapter: NetworkAdapter) {} + public async getNetworkTopCountries( + req: FrameworkRequest, + options: NetworkTopCountriesRequestOptions + ): Promise { + return this.adapter.getNetworkTopCountries(req, options); + } + public async getNetworkTopNFlow( req: FrameworkRequest, options: NetworkTopNFlowRequestOptions diff --git a/x-pack/legacy/plugins/siem/server/lib/network/mock.ts b/x-pack/legacy/plugins/siem/server/lib/network/mock.ts index b0df45ab60a2c..21b00bf188d20 100644 --- a/x-pack/legacy/plugins/siem/server/lib/network/mock.ts +++ b/x-pack/legacy/plugins/siem/server/lib/network/mock.ts @@ -5,7 +5,7 @@ */ import { defaultIndexPattern } from '../../../default_index_pattern'; -import { Direction, FlowTargetSourceDest, NetworkTopNFlowFields } from '../../graphql/types'; +import { Direction, FlowTargetSourceDest, NetworkTopTablesFields } from '../../graphql/types'; import { NetworkTopNFlowRequestOptions } from '.'; @@ -54,7 +54,7 @@ export const mockOptions: NetworkTopNFlowRequestOptions = { 'pageInfo.__typename', '__typename', ], - networkTopNFlowSort: { field: NetworkTopNFlowFields.bytes_out, direction: Direction.desc }, + networkTopNFlowSort: { field: NetworkTopTablesFields.bytes_out, direction: Direction.desc }, flowTarget: FlowTargetSourceDest.source, }; @@ -80,7 +80,7 @@ export const mockRequest = { $ip: String $filterQuery: String $pagination: PaginationInputPaginated! - $sort: NetworkTopNFlowSortField! + $sort: NetworkTopTablesSortField! $flowTarget: FlowTargetSourceDest! $timerange: TimerangeInput! $defaultIndex: [String!]! diff --git a/x-pack/legacy/plugins/siem/server/lib/network/query_top_countries.dsl.ts b/x-pack/legacy/plugins/siem/server/lib/network/query_top_countries.dsl.ts new file mode 100644 index 0000000000000..40bee7eee8155 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/network/query_top_countries.dsl.ts @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + Direction, + FlowTargetSourceDest, + NetworkTopTablesSortField, + NetworkTopTablesFields, +} from '../../graphql/types'; +import { assertUnreachable, createQueryFilterClauses } from '../../utils/build_query'; + +import { NetworkTopCountriesRequestOptions } from './index'; + +const getCountAgg = (flowTarget: FlowTargetSourceDest) => ({ + top_countries_count: { + cardinality: { + field: `${flowTarget}.geo.country_iso_code`, + }, + }, +}); + +export const buildTopCountriesQuery = ({ + defaultIndex, + filterQuery, + flowTarget, + networkTopCountriesSort, + pagination: { querySize }, + sourceConfiguration: { + fields: { timestamp }, + }, + timerange: { from, to }, + ip, +}: NetworkTopCountriesRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { range: { [timestamp]: { gte: from, lte: to } } }, + ]; + + const dslQuery = { + allowNoIndices: true, + index: defaultIndex, + ignoreUnavailable: true, + body: { + aggregations: { + ...getCountAgg(flowTarget), + ...getFlowTargetAggs(networkTopCountriesSort, flowTarget, querySize), + }, + query: { + bool: ip + ? { + filter, + should: [ + { + term: { + [`${getOppositeField(flowTarget)}.ip`]: ip, + }, + }, + ], + minimum_should_match: 1, + } + : { + filter, + }, + }, + }, + size: 0, + track_total_hits: false, + }; + return dslQuery; +}; + +const getFlowTargetAggs = ( + networkTopCountriesSortField: NetworkTopTablesSortField, + flowTarget: FlowTargetSourceDest, + querySize: number +) => ({ + [flowTarget]: { + terms: { + field: `${flowTarget}.geo.country_iso_code`, + size: querySize, + order: { + ...getQueryOrder(networkTopCountriesSortField), + }, + }, + aggs: { + bytes_in: { + sum: { + field: `${getOppositeField(flowTarget)}.bytes`, + }, + }, + bytes_out: { + sum: { + field: `${flowTarget}.bytes`, + }, + }, + flows: { + cardinality: { + field: 'network.community_id', + }, + }, + source_ips: { + cardinality: { + field: 'source.ip', + }, + }, + destination_ips: { + cardinality: { + field: 'destination.ip', + }, + }, + }, + }, +}); + +export const getOppositeField = (flowTarget: FlowTargetSourceDest): FlowTargetSourceDest => { + switch (flowTarget) { + case FlowTargetSourceDest.source: + return FlowTargetSourceDest.destination; + case FlowTargetSourceDest.destination: + return FlowTargetSourceDest.source; + } + assertUnreachable(flowTarget); +}; + +type QueryOrder = + | { bytes_in: Direction } + | { bytes_out: Direction } + | { flows: Direction } + | { destination_ips: Direction } + | { source_ips: Direction }; + +const getQueryOrder = (networkTopCountriesSortField: NetworkTopTablesSortField): QueryOrder => { + switch (networkTopCountriesSortField.field) { + case NetworkTopTablesFields.bytes_in: + return { bytes_in: networkTopCountriesSortField.direction }; + case NetworkTopTablesFields.bytes_out: + return { bytes_out: networkTopCountriesSortField.direction }; + case NetworkTopTablesFields.flows: + return { flows: networkTopCountriesSortField.direction }; + case NetworkTopTablesFields.destination_ips: + return { destination_ips: networkTopCountriesSortField.direction }; + case NetworkTopTablesFields.source_ips: + return { source_ips: networkTopCountriesSortField.direction }; + } + assertUnreachable(networkTopCountriesSortField.field); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/network/query_top_n_flow.dsl.ts b/x-pack/legacy/plugins/siem/server/lib/network/query_top_n_flow.dsl.ts index 5fcd5bf5d7187..47bbabf5505ca 100644 --- a/x-pack/legacy/plugins/siem/server/lib/network/query_top_n_flow.dsl.ts +++ b/x-pack/legacy/plugins/siem/server/lib/network/query_top_n_flow.dsl.ts @@ -7,8 +7,8 @@ import { Direction, FlowTargetSourceDest, - NetworkTopNFlowSortField, - NetworkTopNFlowFields, + NetworkTopTablesSortField, + NetworkTopTablesFields, } from '../../graphql/types'; import { assertUnreachable, createQueryFilterClauses } from '../../utils/build_query'; @@ -73,7 +73,7 @@ export const buildTopNFlowQuery = ({ }; const getFlowTargetAggs = ( - networkTopNFlowSortField: NetworkTopNFlowSortField, + networkTopNFlowSortField: NetworkTopTablesSortField, flowTarget: FlowTargetSourceDest, querySize: number ) => ({ @@ -172,17 +172,17 @@ type QueryOrder = | { destination_ips: Direction } | { source_ips: Direction }; -const getQueryOrder = (networkTopNFlowSortField: NetworkTopNFlowSortField): QueryOrder => { +const getQueryOrder = (networkTopNFlowSortField: NetworkTopTablesSortField): QueryOrder => { switch (networkTopNFlowSortField.field) { - case NetworkTopNFlowFields.bytes_in: + case NetworkTopTablesFields.bytes_in: return { bytes_in: networkTopNFlowSortField.direction }; - case NetworkTopNFlowFields.bytes_out: + case NetworkTopTablesFields.bytes_out: return { bytes_out: networkTopNFlowSortField.direction }; - case NetworkTopNFlowFields.flows: + case NetworkTopTablesFields.flows: return { flows: networkTopNFlowSortField.direction }; - case NetworkTopNFlowFields.destination_ips: + case NetworkTopTablesFields.destination_ips: return { destination_ips: networkTopNFlowSortField.direction }; - case NetworkTopNFlowFields.source_ips: + case NetworkTopTablesFields.source_ips: return { source_ips: networkTopNFlowSortField.direction }; } assertUnreachable(networkTopNFlowSortField.field); diff --git a/x-pack/legacy/plugins/siem/server/lib/network/types.ts b/x-pack/legacy/plugins/siem/server/lib/network/types.ts index c90c5e0ea24ea..e2e443fb50259 100644 --- a/x-pack/legacy/plugins/siem/server/lib/network/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/network/types.ts @@ -4,11 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { NetworkDnsData, NetworkTopNFlowData } from '../../graphql/types'; +import { NetworkTopCountriesData, NetworkDnsData, NetworkTopNFlowData } from '../../graphql/types'; import { FrameworkRequest, RequestOptionsPaginated } from '../framework'; import { TotalValue } from '../types'; export interface NetworkAdapter { + getNetworkTopCountries( + req: FrameworkRequest, + options: RequestOptionsPaginated + ): Promise; getNetworkTopNFlow( req: FrameworkRequest, options: RequestOptionsPaginated @@ -75,6 +79,20 @@ export interface NetworkTopNFlowBuckets { source_ips?: number; } +export interface NetworkTopCountriesBuckets { + country: string; + key: string; + bytes_in: { + value: number; + }; + bytes_out: { + value: number; + }; + flows: number; + destination_ips: number; + source_ips: number; +} + export interface NetworkDnsBuckets { key: string; doc_count: number; diff --git a/x-pack/test/api_integration/apis/siem/network_top_n_flow.ts b/x-pack/test/api_integration/apis/siem/network_top_n_flow.ts index efa0dc9c72d9c..ee4344bb0f1ee 100644 --- a/x-pack/test/api_integration/apis/siem/network_top_n_flow.ts +++ b/x-pack/test/api_integration/apis/siem/network_top_n_flow.ts @@ -10,7 +10,7 @@ import { Direction, FlowTargetSourceDest, GetNetworkTopNFlowQuery, - NetworkTopNFlowFields, + NetworkTopTablesFields, } from '../../../../legacy/plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -39,7 +39,7 @@ export default function({ getService }: FtrProviderContext) { from: FROM, }, flowTarget: FlowTargetSourceDest.source, - sort: { field: NetworkTopNFlowFields.bytes_in, direction: Direction.desc }, + sort: { field: NetworkTopTablesFields.bytes_in, direction: Direction.desc }, pagination: { activePage: 0, cursorStart: 0, @@ -76,7 +76,7 @@ export default function({ getService }: FtrProviderContext) { from: FROM, }, flowTarget: FlowTargetSourceDest.source, - sort: { field: NetworkTopNFlowFields.bytes_in, direction: Direction.asc }, + sort: { field: NetworkTopTablesFields.bytes_in, direction: Direction.asc }, pagination: { activePage: 0, cursorStart: 0, @@ -112,7 +112,7 @@ export default function({ getService }: FtrProviderContext) { to: TO, from: FROM, }, - sort: { field: NetworkTopNFlowFields.bytes_in, direction: Direction.desc }, + sort: { field: NetworkTopTablesFields.bytes_in, direction: Direction.desc }, flowTarget: FlowTargetSourceDest.destination, pagination: { activePage: 0, @@ -146,7 +146,7 @@ export default function({ getService }: FtrProviderContext) { to: TO, from: FROM, }, - sort: { field: NetworkTopNFlowFields.bytes_in, direction: Direction.desc }, + sort: { field: NetworkTopTablesFields.bytes_in, direction: Direction.desc }, flowTarget: FlowTargetSourceDest.source, pagination: { activePage: 1, From 76e139821545199fb6a1de950aee692365bfdf1f Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Wed, 16 Oct 2019 00:42:58 -0400 Subject: [PATCH 13/48] [Lens] Make operation order more clear to users (#48305) * [Lens] Make operation nesting more clear to users * Improve date wording * Update per comments --- .../visualization.test.tsx | 34 ++++++++ .../visualization.tsx | 7 +- .../bucket_nesting_editor.test.tsx | 41 ++++++++-- .../dimension_panel/bucket_nesting_editor.tsx | 81 ++++++++++++++----- .../dimension_panel/popover_editor.tsx | 34 ++++---- .../indexpattern_suggestions.ts | 4 +- x-pack/legacy/plugins/lens/public/types.ts | 5 ++ .../xy_config_panel.tsx | 1 + 8 files changed, 162 insertions(+), 45 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx index f649564b2231a..083ceff3bb444 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx @@ -176,5 +176,39 @@ describe('Datatable Visualization', () => { ], }); }); + + it('reorders the rendered colums based on the order from the datasource', () => { + const datasource = createMockDatasource(); + const layer = { layerId: 'a', columns: ['b', 'c'] }; + const frame = mockFrame(); + frame.datasourceLayers = { a: datasource.publicAPIMock }; + const component = mount( + {} }} + frame={frame} + layer={layer} + setState={jest.fn()} + state={{ layers: [layer] }} + /> + ); + + const accessors = component + .find('[data-test-subj="datatable_multicolumnEditor"]') + .first() + .prop('accessors') as string[]; + + expect(accessors).toEqual(['b', 'c']); + + component.setProps({ + layer: { layerId: 'a', columns: ['c', 'b'] }, + }); + + const newAccessors = component + .find('[data-test-subj="datatable_multicolumnEditor"]') + .first() + .prop('accessors') as string[]; + + expect(newAccessors).toEqual(['c', 'b']); + }); }); }); diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx index ff809e9e4827c..65f97b6d4e4b5 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx @@ -58,6 +58,11 @@ export function DataTableLayer({ dragDropContext, }: { layer: LayerState } & VisualizationProps) { const datasource = frame.datasourceLayers[layer.layerId]; + + const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId); + // When we add a column it could be empty, and therefore have no order + const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns))); + return ( { return result as IndexPatternColumn; } - it('should display an unchecked switch if there are two buckets and it is the root', () => { + it('should display the top level grouping when at the root', () => { const component = mount( { setColumns={jest.fn()} /> ); - const control = component.find('[data-test-subj="indexPattern-nesting-switch"]').first(); + const control1 = component.find('[data-test-subj="indexPattern-nesting-topLevel"]').first(); + const control2 = component.find('[data-test-subj="indexPattern-nesting-bottomLevel"]').first(); - expect(control.prop('checked')).toBeFalsy(); + expect(control1.prop('checked')).toBeTruthy(); + expect(control2.prop('checked')).toBeFalsy(); }); - it('should display a checked switch if there are two buckets and it is not the root', () => { + it('should display the bottom level grouping when appropriate', () => { const component = mount( { setColumns={jest.fn()} /> ); - const control = component.find('[data-test-subj="indexPattern-nesting-switch"]').first(); - expect(control.prop('checked')).toBeTruthy(); + const control1 = component.find('[data-test-subj="indexPattern-nesting-topLevel"]').first(); + const control2 = component.find('[data-test-subj="indexPattern-nesting-bottomLevel"]').first(); + + expect(control1.prop('checked')).toBeFalsy(); + expect(control2.prop('checked')).toBeTruthy(); }); it('should reorder the columns when toggled', () => { @@ -88,11 +93,31 @@ describe('BucketNestingEditor', () => { setColumns={setColumns} /> ); - const control = component.find('[data-test-subj="indexPattern-nesting-switch"]').first(); + const control1 = component.find('[data-test-subj="indexPattern-nesting-topLevel"]').first(); - (control.prop('onChange') as () => {})(); + (control1.prop('onChange') as () => {})(); + expect(setColumns).toHaveBeenCalledTimes(1); expect(setColumns).toHaveBeenCalledWith(['a', 'b', 'c']); + + component.setProps({ + layer: { + columnOrder: ['a', 'b', 'c'], + columns: { + a: mockCol({ suggestedPriority: 0 }), + b: mockCol({ suggestedPriority: 1 }), + c: mockCol({ suggestedPriority: 2, operationType: 'min', isBucketed: false }), + }, + indexPatternId: 'foo', + }, + }); + + const control2 = component.find('[data-test-subj="indexPattern-nesting-bottomLevel"]').first(); + + (control2.prop('onChange') as () => {})(); + + expect(setColumns).toHaveBeenCalledTimes(2); + expect(setColumns).toHaveBeenLastCalledWith(['b', 'a', 'c']); }); it('should display nothing if there are no buckets', () => { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/bucket_nesting_editor.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/bucket_nesting_editor.tsx index 71f5722151802..04e13fead6fca 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/bucket_nesting_editor.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/bucket_nesting_editor.tsx @@ -7,8 +7,11 @@ import _ from 'lodash'; import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiFormRow, EuiHorizontalRule, EuiSwitch, EuiSelect } from '@elastic/eui'; +import { EuiFormRow, EuiHorizontalRule, EuiRadio, EuiSelect, htmlIdGenerator } from '@elastic/eui'; import { IndexPatternLayer } from '../types'; +import { hasField } from '../utils'; + +const generator = htmlIdGenerator('lens-nesting'); function nestColumn(columnOrder: string[], outer: string, inner: string) { const result = columnOrder.filter(c => c !== inner); @@ -32,35 +35,75 @@ export function BucketNestingEditor({ const columns = Object.entries(layer.columns); const aggColumns = columns .filter(([id, c]) => id !== columnId && c.isBucketed) - .map(([value, c]) => ({ value, text: c.label })); + .map(([value, c]) => ({ + value, + text: c.label, + fieldName: hasField(c) ? c.sourceField : '', + })); if (!column || !column.isBucketed || !aggColumns.length) { return null; } + const fieldName = hasField(column) ? column.sourceField : ''; + const prevColumn = layer.columnOrder[layer.columnOrder.indexOf(columnId) - 1]; if (aggColumns.length === 1) { const [target] = aggColumns; + function toggleNesting() { + if (prevColumn) { + setColumns(nestColumn(layer.columnOrder, columnId, target.value)); + } else { + setColumns(nestColumn(layer.columnOrder, target.value, columnId)); + } + } + return ( <> - { - if (prevColumn) { - setColumns(nestColumn(layer.columnOrder, columnId, target.value)); - } else { - setColumns(nestColumn(layer.columnOrder, target.value, columnId)); - } - }} - /> + > + <> + + + + ); } @@ -69,8 +112,8 @@ export function BucketNestingEditor({ <> @@ -81,7 +124,7 @@ export function BucketNestingEditor({ { value: '', text: i18n.translate('xpack.lens.xyChart.nestUnderRoot', { - defaultMessage: 'Top level', + defaultMessage: 'Entire data set', }), }, ...aggColumns, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx index bc46143b5f3b5..b9baa489f3382 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx @@ -70,6 +70,7 @@ export function PopoverEditor(props: PopoverEditorProps) { layerId, currentIndexPattern, uniqueLabel, + hideGrouping, } = props; const { operationByDocument, operationByField, fieldByOperation } = operationFieldSupportMatrix; const [isPopoverOpen, setPopoverOpen] = useState(false); @@ -399,22 +400,25 @@ export function PopoverEditor(props: PopoverEditorProps) { /> )} - { - setState({ - ...state, - layers: { - ...state.layers, - [props.layerId]: { - ...state.layers[props.layerId], - columnOrder, + + {!hideGrouping && ( + { + setState({ + ...state, + layers: { + ...state.layers, + [props.layerId]: { + ...state.layers[props.layerId], + columnOrder, + }, }, - }, - }); - }} - /> + }); + }} + /> + )} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts index f3e4c32103abf..49944614edbb4 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts @@ -460,9 +460,9 @@ function createMetricSuggestion( function getNestedTitle([outerBucket, innerBucket]: IndexPatternColumn[]) { return i18n.translate('xpack.lens.indexpattern.suggestions.nestingChangeLabel', { - defaultMessage: '{innerOperation} per each {outerOperation}', + defaultMessage: '{innerOperation} for each {outerOperation}', values: { - innerOperation: innerBucket.label, + innerOperation: hasField(innerBucket) ? innerBucket.sourceField : innerBucket.label, outerOperation: hasField(outerBucket) ? outerBucket.sourceField : outerBucket.label, }, }); diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 2713a44f53980..1efa123f4f0a3 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -189,6 +189,11 @@ export interface DatasourceDimensionPanelProps { // affects the default ordering of the query suggestedPriority?: DimensionPriority; onRemove?: (accessor: string) => void; + + // Some dimension editors will allow users to change the operation grouping + // from the panel, and this lets the visualization hint that it doesn't want + // users to have that level of control + hideGrouping?: boolean; } export interface DatasourceLayerPanelProps { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx index 9461229313f4c..322e1f1ea4a0e 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx @@ -186,6 +186,7 @@ export function XYConfigPanel(props: VisualizationProps) { filterOperations: isBucketed, suggestedPriority: 1, layerId: layer.layerId, + hideGrouping: true, }} /> From cb091e8f923e9b489b0e83f369df1e71b7b80916 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Wed, 16 Oct 2019 07:33:14 +0100 Subject: [PATCH 14/48] [SIEM] Add TLS to Newtowrk overview page (#48062) * add TLS table to network overview page * isolate TLS parser * add unit test * add integration test * fix types * revert not necessary change * remove variables for domains table * fix for review * fix tlsSelector * update tls selector * apply updateTlsLimit * update selected property for tls selector * add networkType as the 2nd param of updateTlsSort * correcting pagetype * check the page type for updateTableActivePage * hard coded the targeting table name * remove tls table param as property * fix types --- .../public/components/link_to/link_to.tsx | 1 - .../page/network/tls_table/index.tsx | 28 +- .../public/containers/tls/index.gql_query.ts | 2 +- .../siem/public/containers/tls/index.tsx | 23 +- .../siem/public/graphql/introspection.json | 744 +++++++++--------- .../plugins/siem/public/graphql/types.ts | 130 +-- .../plugins/siem/public/mock/global_state.ts | 5 + .../siem/public/pages/network/ip_details.tsx | 2 +- .../pages/network/navigation/nav_tabs.tsx | 7 + .../network/navigation/network_routes.tsx | 5 + .../network/navigation/tls_query_tab_body.tsx | 52 ++ .../public/pages/network/navigation/types.ts | 10 +- .../public/pages/network/navigation/utils.ts | 5 +- .../siem/public/pages/network/translations.ts | 4 + .../siem/public/store/network/actions.ts | 2 + .../siem/public/store/network/helpers.test.ts | 16 + .../siem/public/store/network/helpers.ts | 4 + .../siem/public/store/network/model.ts | 13 +- .../siem/public/store/network/reducer.ts | 32 +- .../siem/public/store/network/selectors.ts | 22 +- .../plugins/siem/server/graphql/index.ts | 2 + .../server/graphql/ip_details/resolvers.ts | 17 +- .../server/graphql/ip_details/schema.gql.ts | 43 +- .../plugins/siem/server/graphql/tls/index.ts | 8 + .../siem/server/graphql/tls/resolvers.ts | 40 + .../siem/server/graphql/tls/schema.gql.ts | 48 ++ .../plugins/siem/server/graphql/types.ts | 398 +++++----- .../legacy/plugins/siem/server/init_server.ts | 2 + .../plugins/siem/server/lib/compose/kibana.ts | 7 +- .../lib/ip_details/elasticsearch_adapter.ts | 74 +- .../siem/server/lib/ip_details/index.ts | 18 +- .../siem/server/lib/ip_details/types.ts | 31 +- .../lib/tls/elasticsearch_adapter.test.ts | 66 ++ .../server/lib/tls/elasticsearch_adapter.ts | 83 ++ .../plugins/siem/server/lib/tls/index.ts | 26 + .../plugins/siem/server/lib/tls/mock.ts | 663 ++++++++++++++++ .../lib/{ip_details => tls}/query_tls.dsl.ts | 5 +- .../plugins/siem/server/lib/tls/types.ts | 40 + .../legacy/plugins/siem/server/lib/types.ts | 2 + x-pack/test/api_integration/apis/siem/tls.ts | 126 ++- 40 files changed, 1936 insertions(+), 870 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/network/navigation/tls_query_tab_body.tsx create mode 100644 x-pack/legacy/plugins/siem/server/graphql/tls/index.ts create mode 100644 x-pack/legacy/plugins/siem/server/graphql/tls/resolvers.ts create mode 100644 x-pack/legacy/plugins/siem/server/graphql/tls/schema.gql.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/tls/elasticsearch_adapter.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/tls/elasticsearch_adapter.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/tls/index.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/tls/mock.ts rename x-pack/legacy/plugins/siem/server/lib/{ip_details => tls}/query_tls.dsl.ts (94%) create mode 100644 x-pack/legacy/plugins/siem/server/lib/tls/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx index d0857e6ff8b48..1fdae4ba9b301 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx @@ -13,7 +13,6 @@ import { RedirectToNetworkPage } from './redirect_to_network'; import { RedirectToOverviewPage } from './redirect_to_overview'; import { RedirectToTimelinesPage } from './redirect_to_timelines'; import { HostsTableType } from '../../store/hosts/model'; - interface LinkToPageProps { match: RouteMatch<{}>; } diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.tsx index 69e8525418b0b..fe2a2d8c5da7d 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.tsx @@ -36,10 +36,14 @@ interface TlsTableReduxProps { } interface TlsTableDispatchProps { - updateTableActivePage: ActionCreator<{ + updateIpDetailsTableActivePage: ActionCreator<{ activePage: number; tableType: networkModel.IpDetailsTableType; }>; + updateNetworkPageTableActivePage: ActionCreator<{ + activePage: number; + tableType: networkModel.NetworkTableType; + }>; updateTlsLimit: ActionCreator<{ limit: number; networkType: networkModel.NetworkType; @@ -80,9 +84,18 @@ class TlsTableComponent extends React.PureComponent { tlsSortField, totalCount, type, - updateTableActivePage, + updateIpDetailsTableActivePage, + updateNetworkPageTableActivePage, updateTlsLimit, } = this.props; + + const updateTableActivePage: ActionCreator<{ + activePage: number; + tableType: networkModel.NetworkTableType | networkModel.IpDetailsTableType; + }> = + type === networkModel.NetworkType.page + ? updateNetworkPageTableActivePage + : updateIpDetailsTableActivePage; return ( { }; } -const makeMapStateToProps = () => { - const getTlsSelector = networkSelectors.tlsSelector(); - return (state: State) => ({ - ...getTlsSelector(state), - }); -}; +const makeMapStateToProps = (state: State, ownProps: OwnProps) => + networkSelectors.tlsSelector(ownProps.type); export const TlsTable = connect( makeMapStateToProps, { - updateTableActivePage: networkActions.updateIpDetailsTableActivePage, + updateNetworkPageTableActivePage: networkActions.updateNetworkPageTableActivePage, + updateIpDetailsTableActivePage: networkActions.updateIpDetailsTableActivePage, updateTlsLimit: networkActions.updateTlsLimit, updateTlsSort: networkActions.updateTlsSort, } diff --git a/x-pack/legacy/plugins/siem/public/containers/tls/index.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/tls/index.gql_query.ts index 15d5a8eb8a483..bbb92282bee83 100644 --- a/x-pack/legacy/plugins/siem/public/containers/tls/index.gql_query.ts +++ b/x-pack/legacy/plugins/siem/public/containers/tls/index.gql_query.ts @@ -10,7 +10,7 @@ export const tlsQuery = gql` query GetTlsQuery( $sourceId: ID! $filterQuery: String - $flowTarget: FlowTarget! + $flowTarget: FlowTargetSourceDest! $ip: String! $pagination: PaginationInputPaginated! $sort: TlsSortField! diff --git a/x-pack/legacy/plugins/siem/public/containers/tls/index.tsx b/x-pack/legacy/plugins/siem/public/containers/tls/index.tsx index 74943717c5087..60860d30a0d8f 100644 --- a/x-pack/legacy/plugins/siem/public/containers/tls/index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/tls/index.tsx @@ -12,11 +12,11 @@ import { connect } from 'react-redux'; import chrome from 'ui/chrome'; import { DEFAULT_INDEX_KEY } from '../../../common/constants'; import { - FlowTarget, PageInfoPaginated, TlsEdges, TlsSortField, GetTlsQuery, + FlowTargetSourceDest, } from '../../graphql/types'; import { inputsModel, networkModel, networkSelectors, State, inputsSelectors } from '../../store'; import { createFilter, getDefaultFetchPolicy } from '../helpers'; @@ -40,7 +40,7 @@ export interface TlsArgs { export interface OwnProps extends QueryTemplatePaginatedProps { children: (args: TlsArgs) => React.ReactNode; - flowTarget: FlowTarget; + flowTarget: FlowTargetSourceDest; ip: string; type: networkModel.NetworkType; } @@ -139,18 +139,15 @@ class TlsComponentQuery extends QueryTemplatePaginated< } } -const makeMapStateToProps = () => { - const getTlsSelector = networkSelectors.tlsSelector(); +const mapStateToProps = (state: State, { id = ID, type }: OwnProps) => { + const getTlsSelector = networkSelectors.tlsSelector(type); const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getTlsSelector(state), - isInspected, - }; - }; + const { isInspected } = getQuery(state, id); - return mapStateToProps; + return { + ...getTlsSelector(state), + isInspected, + }; }; -export const TlsQuery = connect(makeMapStateToProps)(TlsComponentQuery); +export const TlsQuery = connect(mapStateToProps)(TlsComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/legacy/plugins/siem/public/graphql/introspection.json index c012af5fd667f..0348b283ed318 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json +++ b/x-pack/legacy/plugins/siem/public/graphql/introspection.json @@ -1193,103 +1193,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "Tls", - "description": "", - "args": [ - { - "name": "filterQuery", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "id", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "ip", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "pagination", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "PaginationInputPaginated", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "sort", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "INPUT_OBJECT", "name": "TlsSortField", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "flowTarget", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "ENUM", "name": "FlowTarget", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "timerange", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "defaultIndex", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "TlsData", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "Users", "description": "", @@ -1925,6 +1828,103 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "Tls", + "description": "", + "args": [ + { + "name": "filterQuery", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "id", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "ip", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "pagination", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "PaginationInputPaginated", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "sort", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "INPUT_OBJECT", "name": "TlsSortField", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "flowTarget", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "ENUM", "name": "FlowTargetSourceDest", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "timerange", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "defaultIndex", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "TlsData", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "UncommonProcesses", "description": "Gets UncommonProcesses based on a timerange, or all UncommonProcesses if no criteria is specified", @@ -6380,7 +6380,7 @@ }, { "kind": "INPUT_OBJECT", - "name": "TlsSortField", + "name": "UsersSortField", "description": "", "fields": null, "inputFields": [ @@ -6390,7 +6390,7 @@ "type": { "kind": "NON_NULL", "name": null, - "ofType": { "kind": "ENUM", "name": "TlsFields", "ofType": null } + "ofType": { "kind": "ENUM", "name": "UsersFields", "ofType": null } }, "defaultValue": null }, @@ -6411,13 +6411,14 @@ }, { "kind": "ENUM", - "name": "TlsFields", + "name": "UsersFields", "description": "", "fields": null, "inputFields": null, "interfaces": null, "enumValues": [ - { "name": "_id", "description": "", "isDeprecated": false, "deprecationReason": null } + { "name": "name", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "count", "description": "", "isDeprecated": false, "deprecationReason": null } ], "possibleTypes": null }, @@ -6443,7 +6444,7 @@ }, { "kind": "OBJECT", - "name": "TlsData", + "name": "UsersData", "description": "", "fields": [ { @@ -6459,7 +6460,7 @@ "ofType": { "kind": "NON_NULL", "name": null, - "ofType": { "kind": "OBJECT", "name": "TlsEdges", "ofType": null } + "ofType": { "kind": "OBJECT", "name": "UsersEdges", "ofType": null } } } }, @@ -6506,7 +6507,7 @@ }, { "kind": "OBJECT", - "name": "TlsEdges", + "name": "UsersEdges", "description": "", "fields": [ { @@ -6516,7 +6517,7 @@ "type": { "kind": "NON_NULL", "name": null, - "ofType": { "kind": "OBJECT", "name": "TlsNode", "ofType": null } + "ofType": { "kind": "OBJECT", "name": "UsersNode", "ofType": null } }, "isDeprecated": false, "deprecationReason": null @@ -6541,7 +6542,7 @@ }, { "kind": "OBJECT", - "name": "TlsNode", + "name": "UsersNode", "description": "", "fields": [ { @@ -6561,283 +6562,34 @@ "deprecationReason": null }, { - "name": "alternativeNames", + "name": "user", "description": "", "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - }, + "type": { "kind": "OBJECT", "name": "UsersItem", "ofType": null }, "isDeprecated": false, "deprecationReason": null - }, + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "UsersItem", + "description": "", + "fields": [ { - "name": "notAfter", + "name": "name", "description": "", "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - }, + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "commonNames", - "description": "", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ja3", - "description": "", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "issuerNames", - "description": "", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "UsersSortField", - "description": "", - "fields": null, - "inputFields": [ - { - "name": "field", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "ENUM", "name": "UsersFields", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "direction", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "ENUM", "name": "Direction", "ofType": null } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "UsersFields", - "description": "", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { "name": "name", "description": "", "isDeprecated": false, "deprecationReason": null }, - { "name": "count", "description": "", "isDeprecated": false, "deprecationReason": null } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "UsersData", - "description": "", - "fields": [ - { - "name": "edges", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "UsersEdges", "ofType": null } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "totalCount", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "PageInfoPaginated", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "inspect", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "UsersEdges", - "description": "", - "fields": [ - { - "name": "node", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "UsersNode", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "cursor", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "CursorType", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "UsersNode", - "description": "", - "fields": [ - { - "name": "_id", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "timestamp", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Date", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "user", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "UsersItem", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "UsersItem", - "description": "", - "fields": [ - { - "name": "name", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", + "name": "id", "description": "", "args": [], "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, @@ -8424,6 +8176,254 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "TlsSortField", + "description": "", + "fields": null, + "inputFields": [ + { + "name": "field", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "ENUM", "name": "TlsFields", "ofType": null } + }, + "defaultValue": null + }, + { + "name": "direction", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "ENUM", "name": "Direction", "ofType": null } + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "TlsFields", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { "name": "_id", "description": "", "isDeprecated": false, "deprecationReason": null } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TlsData", + "description": "", + "fields": [ + { + "name": "edges", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "TlsEdges", "ofType": null } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "totalCount", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "PageInfoPaginated", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inspect", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TlsEdges", + "description": "", + "fields": [ + { + "name": "node", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "TlsNode", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cursor", + "description": "", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "OBJECT", "name": "CursorType", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "TlsNode", + "description": "", + "fields": [ + { + "name": "_id", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timestamp", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Date", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "alternativeNames", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notAfter", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "commonNames", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ja3", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "issuerNames", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "UncommonProcessesData", diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts index b8ed6f2ad47fe..50fb6bd9e8a8a 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/public/graphql/types.ts @@ -67,12 +67,6 @@ export interface HostsSortField { direction: Direction; } -export interface TlsSortField { - field: TlsFields; - - direction: Direction; -} - export interface UsersSortField { field: UsersFields; @@ -91,6 +85,12 @@ export interface NetworkDnsSortField { direction: Direction; } +export interface TlsSortField { + field: TlsFields; + + direction: Direction; +} + export interface PageInfoTimeline { pageIndex: number; @@ -239,8 +239,9 @@ export enum HostsFields { lastSeen = 'lastSeen', } -export enum TlsFields { - _id = '_id', +export enum UsersFields { + name = 'name', + count = 'count', } export enum FlowTarget { @@ -250,11 +251,6 @@ export enum FlowTarget { source = 'source', } -export enum UsersFields { - name = 'name', - count = 'count', -} - export enum FlowTargetSourceDest { destination = 'destination', source = 'source', @@ -276,6 +272,10 @@ export enum NetworkDnsFields { dnsBytesOut = 'dnsBytesOut', } +export enum TlsFields { + _id = '_id', +} + export enum SortFieldTimeline { title = 'title', description = 'description', @@ -417,8 +417,6 @@ export interface Source { IpOverview?: Maybe; - Tls: TlsData; - Users: UsersData; KpiNetwork?: Maybe; @@ -436,6 +434,8 @@ export interface Source { OverviewNetwork?: Maybe; OverviewHost?: Maybe; + + Tls: TlsData; /** Gets UncommonProcesses based on a timerange, or all UncommonProcesses if no criteria is specified */ UncommonProcesses: UncommonProcessesData; /** Just a simple example to get the app name */ @@ -1314,38 +1314,6 @@ export interface AutonomousSystemOrganization { name?: Maybe; } -export interface TlsData { - edges: TlsEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe; -} - -export interface TlsEdges { - node: TlsNode; - - cursor: CursorType; -} - -export interface TlsNode { - _id?: Maybe; - - timestamp?: Maybe; - - alternativeNames?: Maybe; - - notAfter?: Maybe; - - commonNames?: Maybe; - - ja3?: Maybe; - - issuerNames?: Maybe; -} - export interface UsersData { edges: UsersEdges[]; @@ -1666,6 +1634,38 @@ export interface OverviewHostData { inspect?: Maybe; } +export interface TlsData { + edges: TlsEdges[]; + + totalCount: number; + + pageInfo: PageInfoPaginated; + + inspect?: Maybe; +} + +export interface TlsEdges { + node: TlsNode; + + cursor: CursorType; +} + +export interface TlsNode { + _id?: Maybe; + + timestamp?: Maybe; + + alternativeNames?: Maybe; + + notAfter?: Maybe; + + commonNames?: Maybe; + + ja3?: Maybe; + + issuerNames?: Maybe; +} + export interface UncommonProcessesData { edges: UncommonProcessesEdges[]; @@ -2052,23 +2052,6 @@ export interface IpOverviewSourceArgs { defaultIndex: string[]; } -export interface TlsSourceArgs { - filterQuery?: Maybe; - - id?: Maybe; - - ip: string; - - pagination: PaginationInputPaginated; - - sort: TlsSortField; - - flowTarget: FlowTarget; - - timerange: TimerangeInput; - - defaultIndex: string[]; -} export interface UsersSourceArgs { filterQuery?: Maybe; @@ -2180,6 +2163,23 @@ export interface OverviewHostSourceArgs { defaultIndex: string[]; } +export interface TlsSourceArgs { + filterQuery?: Maybe; + + id?: Maybe; + + ip: string; + + pagination: PaginationInputPaginated; + + sort: TlsSortField; + + flowTarget: FlowTargetSourceDest; + + timerange: TimerangeInput; + + defaultIndex: string[]; +} export interface UncommonProcessesSourceArgs { timerange: TimerangeInput; @@ -5113,7 +5113,7 @@ export namespace GetTlsQuery { export type Variables = { sourceId: string; filterQuery?: Maybe; - flowTarget: FlowTarget; + flowTarget: FlowTargetSourceDest; ip: string; pagination: PaginationInputPaginated; sort: TlsSortField; diff --git a/x-pack/legacy/plugins/siem/public/mock/global_state.ts b/x-pack/legacy/plugins/siem/public/mock/global_state.ts index ac6dfbf08ca95..335dd140a9ba1 100644 --- a/x-pack/legacy/plugins/siem/public/mock/global_state.ts +++ b/x-pack/legacy/plugins/siem/public/mock/global_state.ts @@ -91,6 +91,11 @@ export const mockGlobalState: State = { dnsSortField: { field: NetworkDnsFields.queryCount, direction: Direction.desc }, isPtrIncluded: false, }, + [networkModel.NetworkTableType.tls]: { + activePage: 0, + limit: 10, + tlsSortField: { field: TlsFields._id, direction: Direction.desc }, + }, }, }, details: { diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details.tsx b/x-pack/legacy/plugins/siem/public/pages/network/ip_details.tsx index 27d88e8475a65..ef3af241a2267 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/ip_details.tsx @@ -357,7 +357,7 @@ export const IPDetailsComponent = React.memo( disabled: false, urlKey: 'network', }, + [NetworkRouteType.tls]: { + id: NetworkRouteType.tls, + name: i18n.NAVIGATION_TLS_TITLE, + href: getTabsOnNetworkUrl(NetworkRouteType.tls), + disabled: false, + urlKey: 'network', + }, [NetworkRouteType.anomalies]: { id: NetworkRouteType.anomalies, name: i18n.NAVIGATION_ANOMALIES_TITLE, diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx index cddd2d7aafed8..136a4f29359f5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx @@ -17,6 +17,7 @@ import { AnomaliesQueryTabBody } from './anomalies_query_tab_body'; import { DnsQueryTabBody } from './dns_query_tab_body'; import { ConditionalFlexGroup } from './conditional_flex_group'; import { NetworkRoutesProps, NetworkRouteType } from './types'; +import { TlsQueryTabBody } from './tls_query_tab_body'; export const NetworkRoutes = ({ networkPagePath, @@ -80,6 +81,10 @@ export const NetworkRoutes = ({ )} /> + } + /> ( diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/tls_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/tls_query_tab_body.tsx new file mode 100644 index 0000000000000..1f93e293be865 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/tls_query_tab_body.tsx @@ -0,0 +1,52 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import { manageQuery } from '../../../components/page/manage_query'; +import { TlsQuery } from '../../../containers/tls'; +import { TlsTable } from '../../../components/page/network/tls_table'; +import { TlsQueryTabBodyProps } from './types'; + +const TlsTableManage = manageQuery(TlsTable); + +export const TlsQueryTabBody = ({ + to, + filterQuery, + flowTarget, + ip = '', + setQuery, + isInitializing, + from, + type, +}: TlsQueryTabBodyProps) => ( + + {({ id, inspect, isInspected, tls, totalCount, pageInfo, loading, loadPage, refetch }) => ( + + )} + +); diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts b/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts index 3d36193b7ea12..3e5d00c51151a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts @@ -28,6 +28,12 @@ export type IPsQueryTabBodyProps = QueryTabBodyProps & flowTarget: FlowTargetSourceDest; }; +export type TlsQueryTabBodyProps = QueryTabBodyProps & + GlobalTimeArgs & { + flowTarget: FlowTargetSourceDest; + ip?: string; + }; + export type AnomaliesQueryTabBodyProps = QueryTabBodyProps & Pick & { narrowDateRange: NarrowDateRange; @@ -43,7 +49,8 @@ export type NetworkRoutesProps = GlobalTimeArgs & { export type KeyNetworkNavTabWithoutMlPermission = NetworkRouteType.countries & NetworkRouteType.dns & - NetworkRouteType.ips; + NetworkRouteType.ips & + NetworkRouteType.tls; type KeyNetworkNavTabWithMlPermission = KeyNetworkNavTabWithoutMlPermission & NetworkRouteType.anomalies; @@ -57,6 +64,7 @@ export enum NetworkRouteType { countries = 'countries', dns = 'dns', anomalies = 'anomalies', + tls = 'tls', } export type GetNetworkRoutePath = ( diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/utils.ts b/x-pack/legacy/plugins/siem/public/pages/network/navigation/utils.ts index d6ad28757aee1..a46925c6b4504 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/utils.ts +++ b/x-pack/legacy/plugins/siem/public/pages/network/navigation/utils.ts @@ -12,7 +12,7 @@ export const getNetworkRoutePath: GetNetworkRoutePath = ( hasMlUserPermission ) => { if (capabilitiesFetched && !hasMlUserPermission) { - return `${pagePath}/:tabName(${NetworkRouteType.ips}|${NetworkRouteType.dns}|${NetworkRouteType.countries})`; + return `${pagePath}/:tabName(${NetworkRouteType.ips}|${NetworkRouteType.dns}|${NetworkRouteType.countries}|${NetworkRouteType.tls})`; } return ( @@ -20,6 +20,7 @@ export const getNetworkRoutePath: GetNetworkRoutePath = ( `${NetworkRouteType.ips}|` + `${NetworkRouteType.dns}|` + `${NetworkRouteType.countries}|` + - `${NetworkRouteType.anomalies})` + `${NetworkRouteType.anomalies}|` + + `${NetworkRouteType.tls})` ); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/translations.ts b/x-pack/legacy/plugins/siem/public/pages/network/translations.ts index 14a74f5666f0e..d3945c3aca59c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/network/translations.ts @@ -42,6 +42,10 @@ export const NAVIGATION_DNS_TITLE = i18n.translate('xpack.siem.network.navigatio defaultMessage: 'DNS', }); +export const NAVIGATION_TLS_TITLE = i18n.translate('xpack.siem.network.navigation.tlsTitle', { + defaultMessage: 'TLS', +}); + export const NAVIGATION_ANOMALIES_TITLE = i18n.translate( 'xpack.siem.network.navigation.anomaliesTitle', { diff --git a/x-pack/legacy/plugins/siem/public/store/network/actions.ts b/x-pack/legacy/plugins/siem/public/store/network/actions.ts index 3688e257344fa..63b08926a15e5 100644 --- a/x-pack/legacy/plugins/siem/public/store/network/actions.ts +++ b/x-pack/legacy/plugins/siem/public/store/network/actions.ts @@ -81,10 +81,12 @@ export const updateIpDetailsFlowTarget = actionCreator<{ // TLS Table Actions export const updateTlsSort = actionCreator<{ tlsSortField: TlsSortField; + networkType: networkModel.NetworkType; }>('UPDATE_TLS_SORT'); export const updateTlsLimit = actionCreator<{ limit: number; + networkType: networkModel.NetworkType; }>('UPDATE_TLS_LIMIT'); // Users Table Actions diff --git a/x-pack/legacy/plugins/siem/public/store/network/helpers.test.ts b/x-pack/legacy/plugins/siem/public/store/network/helpers.test.ts index 31db0cea80dc9..6299b625f08a9 100644 --- a/x-pack/legacy/plugins/siem/public/store/network/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/store/network/helpers.test.ts @@ -60,6 +60,14 @@ export const mockNetworkState: NetworkModel = { }, isPtrIncluded: false, }, + [NetworkTableType.tls]: { + activePage: 2, + limit: DEFAULT_TABLE_LIMIT, + tlsSortField: { + field: TlsFields._id, + direction: Direction.desc, + }, + }, }, }, details: { @@ -137,6 +145,14 @@ describe('Network redux store', () => { dnsSortField: { field: 'uniqueDomains', direction: 'desc' }, isPtrIncluded: false, }, + [NetworkTableType.tls]: { + activePage: 0, + limit: 10, + tlsSortField: { + direction: 'desc', + field: '_id', + }, + }, [NetworkTableType.topCountriesDestination]: { activePage: 0, limit: 10, diff --git a/x-pack/legacy/plugins/siem/public/store/network/helpers.ts b/x-pack/legacy/plugins/siem/public/store/network/helpers.ts index 60f4d6d1b4106..c438fb6b492ce 100644 --- a/x-pack/legacy/plugins/siem/public/store/network/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/store/network/helpers.ts @@ -36,6 +36,10 @@ export const setNetworkPageQueriesActivePageToZero = (state: NetworkModel): Netw ...state.page.queries[NetworkTableType.dns], activePage: DEFAULT_TABLE_ACTIVE_PAGE, }, + [NetworkTableType.tls]: { + ...state.page.queries[NetworkTableType.tls], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, }); export const setNetworkDetailsQueriesActivePageToZero = ( diff --git a/x-pack/legacy/plugins/siem/public/store/network/model.ts b/x-pack/legacy/plugins/siem/public/store/network/model.ts index 294177bd426b2..e1c5f6843d784 100644 --- a/x-pack/legacy/plugins/siem/public/store/network/model.ts +++ b/x-pack/legacy/plugins/siem/public/store/network/model.ts @@ -23,6 +23,7 @@ export enum NetworkTableType { topCountriesSource = 'topCountriesSource', topNFlowDestination = 'topNFlowDestination', topNFlowSource = 'topNFlowSource', + tls = 'tls', } export type TopNTableType = @@ -65,24 +66,23 @@ export interface DnsQuery extends BasicQueryPaginated { isPtrIncluded: boolean; } +export interface TlsQuery extends BasicQueryPaginated { + tlsSortField: TlsSortField; +} + export interface NetworkQueries { [NetworkTableType.dns]: DnsQuery; [NetworkTableType.topCountriesDestination]: TopCountriesQuery; [NetworkTableType.topCountriesSource]: TopCountriesQuery; [NetworkTableType.topNFlowDestination]: TopNFlowQuery; [NetworkTableType.topNFlowSource]: TopNFlowQuery; + [NetworkTableType.tls]: TlsQuery; } export interface NetworkPageModel { queries: NetworkQueries; } -// IP Details Models - -export interface TlsQuery extends BasicQueryPaginated { - tlsSortField: TlsSortField; -} - export interface UsersQuery extends BasicQueryPaginated { usersSortField: UsersSortField; } @@ -101,7 +101,6 @@ export interface NetworkDetailsModel { queries: IpOverviewQueries; } -// Network Model export interface NetworkModel { [NetworkType.page]: NetworkPageModel; [NetworkType.details]: NetworkDetailsModel; diff --git a/x-pack/legacy/plugins/siem/public/store/network/reducer.ts b/x-pack/legacy/plugins/siem/public/store/network/reducer.ts index 6a7c014707cbb..96427d968aa13 100644 --- a/x-pack/legacy/plugins/siem/public/store/network/reducer.ts +++ b/x-pack/legacy/plugins/siem/public/store/network/reducer.ts @@ -69,6 +69,14 @@ export const initialNetworkState: NetworkState = { }, isPtrIncluded: false, }, + [NetworkTableType.tls]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + tlsSortField: { + field: TlsFields._id, + direction: Direction.desc, + }, + }, [NetworkTableType.topCountriesSource]: { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, @@ -382,27 +390,27 @@ export const networkReducer = reducerWithInitialState(initialNetworkState) flowTarget, }, })) - .case(updateTlsLimit, (state, { limit }) => ({ + .case(updateTlsLimit, (state, { limit, networkType }) => ({ ...state, - [NetworkType.details]: { - ...state[NetworkType.details], + [networkType]: { + ...state[networkType], queries: { - ...state[NetworkType.details].queries, - [IpDetailsTableType.tls]: { - ...state[NetworkType.details].queries.tls, + ...state[networkType].queries, + tls: { + ...state[networkType].queries.tls, limit, }, }, }, })) - .case(updateTlsSort, (state, { tlsSortField }) => ({ + .case(updateTlsSort, (state, { tlsSortField, networkType }) => ({ ...state, - [NetworkType.details]: { - ...state[NetworkType.details], + [networkType]: { + ...state[networkType], queries: { - ...state[NetworkType.details].queries, - [IpDetailsTableType.tls]: { - ...state[NetworkType.details].queries.tls, + ...state[networkType].queries, + tls: { + ...state[networkType].queries.tls, tlsSortField, }, }, diff --git a/x-pack/legacy/plugins/siem/public/store/network/selectors.ts b/x-pack/legacy/plugins/siem/public/store/network/selectors.ts index 8f86cad3da2e2..416d8e7cb6100 100644 --- a/x-pack/legacy/plugins/siem/public/store/network/selectors.ts +++ b/x-pack/legacy/plugins/siem/public/store/network/selectors.ts @@ -22,12 +22,14 @@ export const dnsSelector = () => network => network.queries.dns ); export enum NetworkTableType { + tls = 'tls', dns = 'dns', topCountriesDestination = 'topCountriesDestination', topCountriesSource = 'topCountriesSource', topNFlowDestination = 'topNFlowDestination', topNFlowSource = 'topNFlowSource', } + export const topNFlowSelector = (flowTarget: FlowTargetSourceDest, networkType: NetworkType) => { if (networkType === NetworkType.page) { return createSelector( @@ -47,6 +49,20 @@ export const topNFlowSelector = (flowTarget: FlowTargetSourceDest, networkType: ); }; +export const tlsSelector = (networkType: NetworkType) => { + if (networkType === NetworkType.page) { + return createSelector( + selectNetworkPage, + network => network.queries[NetworkTableType.tls] + ); + } + + return createSelector( + selectNetworkDetails, + network => network.queries[IpDetailsTableType.tls] + ); +}; + export const topCountriesSelector = ( flowTarget: FlowTargetSourceDest, networkType: NetworkType @@ -76,12 +92,6 @@ export const ipDetailsFlowTargetSelector = () => network => network.flowTarget ); -export const tlsSelector = () => - createSelector( - selectNetworkDetails, - network => network.queries.tls - ); - export const usersSelector = () => createSelector( selectNetworkDetails, diff --git a/x-pack/legacy/plugins/siem/server/graphql/index.ts b/x-pack/legacy/plugins/siem/server/graphql/index.ts index 4fdf3d3fb8582..110a390c19531 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/index.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/index.ts @@ -25,6 +25,7 @@ import { toNumberSchema } from './scalar_to_number_array'; import { sourceStatusSchema } from './source_status'; import { sourcesSchema } from './sources'; import { timelineSchema } from './timeline'; +import { tlsSchema } from './tls'; import { uncommonProcessesSchema } from './uncommon_processes'; import { whoAmISchema } from './who_am_i'; export const schemas = [ @@ -48,6 +49,7 @@ export const schemas = [ sourceStatusSchema, sharedSchema, timelineSchema, + tlsSchema, uncommonProcessesSchema, whoAmISchema, ]; diff --git a/x-pack/legacy/plugins/siem/server/graphql/ip_details/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/ip_details/resolvers.ts index fb65883de12d8..a12e82e744ad4 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/ip_details/resolvers.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/ip_details/resolvers.ts @@ -6,7 +6,7 @@ import { SourceResolvers } from '../../graphql/types'; import { AppResolverOf, ChildResolverOf } from '../../lib/framework'; -import { IpDetails, TlsRequestOptions, UsersRequestOptions } from '../../lib/ip_details'; +import { IpDetails, UsersRequestOptions } from '../../lib/ip_details'; import { createOptions, createOptionsPaginated } from '../../utils/build_query/create_options'; import { QuerySourceResolver } from '../sources/resolvers'; @@ -15,11 +15,6 @@ export type QueryIpOverviewResolver = ChildResolverOf< QuerySourceResolver >; -export type QueryTlsResolver = ChildResolverOf< - AppResolverOf, - QuerySourceResolver ->; - export type QueryUsersResolver = ChildResolverOf< AppResolverOf, QuerySourceResolver @@ -34,7 +29,6 @@ export const createIpDetailsResolvers = ( ): { Source: { IpOverview: QueryIpOverviewResolver; - Tls: QueryTlsResolver; Users: QueryUsersResolver; }; } => ({ @@ -43,15 +37,6 @@ export const createIpDetailsResolvers = ( const options = { ...createOptions(source, args, info), ip: args.ip }; return libs.ipDetails.getIpOverview(req, options); }, - async Tls(source, args, { req }, info) { - const options: TlsRequestOptions = { - ...createOptionsPaginated(source, args, info), - ip: args.ip, - tlsSortField: args.sort, - flowTarget: args.flowTarget, - }; - return libs.ipDetails.getTls(req, options); - }, async Users(source, args, { req }, info) { const options: UsersRequestOptions = { ...createOptionsPaginated(source, args, info), diff --git a/x-pack/legacy/plugins/siem/server/graphql/ip_details/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/ip_details/schema.gql.ts index 3acbed3977d73..4684449c1b80f 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/ip_details/schema.gql.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/ip_details/schema.gql.ts @@ -42,47 +42,6 @@ const ipOverviewSchema = gql` } `; -const tlsSchema = gql` - enum TlsFields { - _id - } - type TlsNode { - _id: String - timestamp: Date - alternativeNames: [String!] - notAfter: [String!] - commonNames: [String!] - ja3: [String!] - issuerNames: [String!] - } - input TlsSortField { - field: TlsFields! - direction: Direction! - } - type TlsEdges { - node: TlsNode! - cursor: CursorType! - } - type TlsData { - edges: [TlsEdges!]! - totalCount: Float! - pageInfo: PageInfoPaginated! - inspect: Inspect - } - extend type Source { - Tls( - filterQuery: String - id: String - ip: String! - pagination: PaginationInputPaginated! - sort: TlsSortField! - flowTarget: FlowTarget! - timerange: TimerangeInput! - defaultIndex: [String!]! - ): TlsData! - } -`; - const usersSchema = gql` enum UsersFields { name @@ -134,4 +93,4 @@ const usersSchema = gql` } `; -export const ipDetailsSchemas = [ipOverviewSchema, tlsSchema, usersSchema]; +export const ipDetailsSchemas = [ipOverviewSchema, usersSchema]; diff --git a/x-pack/legacy/plugins/siem/server/graphql/tls/index.ts b/x-pack/legacy/plugins/siem/server/graphql/tls/index.ts new file mode 100644 index 0000000000000..7d745742090a6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/graphql/tls/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { createTlsResolvers } from './resolvers'; +export { tlsSchema } from './schema.gql'; diff --git a/x-pack/legacy/plugins/siem/server/graphql/tls/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/tls/resolvers.ts new file mode 100644 index 0000000000000..0cc2a839bfe40 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/graphql/tls/resolvers.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 { SourceResolvers } from '../../graphql/types'; +import { AppResolverOf, ChildResolverOf } from '../../lib/framework'; +import { TLS, TlsRequestOptions } from '../../lib/tls'; +import { createOptionsPaginated } from '../../utils/build_query/create_options'; +import { QuerySourceResolver } from '../sources/resolvers'; + +export type QueryTlsResolver = ChildResolverOf< + AppResolverOf, + QuerySourceResolver +>; + +export interface TlsResolversDeps { + tls: TLS; +} + +export const createTlsResolvers = ( + libs: TlsResolversDeps +): { + Source: { + Tls: QueryTlsResolver; + }; +} => ({ + Source: { + async Tls(source, args, { req }, info) { + const options: TlsRequestOptions = { + ...createOptionsPaginated(source, args, info), + ip: args.ip, + tlsSortField: args.sort, + flowTarget: args.flowTarget, + }; + return libs.tls.getTls(req, options); + }, + }, +}); diff --git a/x-pack/legacy/plugins/siem/server/graphql/tls/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/tls/schema.gql.ts new file mode 100644 index 0000000000000..301960cea33ef --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/graphql/tls/schema.gql.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 gql from 'graphql-tag'; + +export const tlsSchema = gql` + enum TlsFields { + _id + } + type TlsNode { + _id: String + timestamp: Date + alternativeNames: [String!] + notAfter: [String!] + commonNames: [String!] + ja3: [String!] + issuerNames: [String!] + } + input TlsSortField { + field: TlsFields! + direction: Direction! + } + type TlsEdges { + node: TlsNode! + cursor: CursorType! + } + type TlsData { + edges: [TlsEdges!]! + totalCount: Float! + pageInfo: PageInfoPaginated! + inspect: Inspect + } + extend type Source { + Tls( + filterQuery: String + id: String + ip: String! + pagination: PaginationInputPaginated! + sort: TlsSortField! + flowTarget: FlowTargetSourceDest! + timerange: TimerangeInput! + defaultIndex: [String!]! + ): TlsData! + } +`; diff --git a/x-pack/legacy/plugins/siem/server/graphql/types.ts b/x-pack/legacy/plugins/siem/server/graphql/types.ts index bce1f0139c49e..776efeebd6ddb 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/types.ts @@ -69,12 +69,6 @@ export interface HostsSortField { direction: Direction; } -export interface TlsSortField { - field: TlsFields; - - direction: Direction; -} - export interface UsersSortField { field: UsersFields; @@ -93,6 +87,12 @@ export interface NetworkDnsSortField { direction: Direction; } +export interface TlsSortField { + field: TlsFields; + + direction: Direction; +} + export interface PageInfoTimeline { pageIndex: number; @@ -241,8 +241,9 @@ export enum HostsFields { lastSeen = 'lastSeen', } -export enum TlsFields { - _id = '_id', +export enum UsersFields { + name = 'name', + count = 'count', } export enum FlowTarget { @@ -252,11 +253,6 @@ export enum FlowTarget { source = 'source', } -export enum UsersFields { - name = 'name', - count = 'count', -} - export enum FlowTargetSourceDest { destination = 'destination', source = 'source', @@ -278,6 +274,10 @@ export enum NetworkDnsFields { dnsBytesOut = 'dnsBytesOut', } +export enum TlsFields { + _id = '_id', +} + export enum SortFieldTimeline { title = 'title', description = 'description', @@ -419,8 +419,6 @@ export interface Source { IpOverview?: Maybe; - Tls: TlsData; - Users: UsersData; KpiNetwork?: Maybe; @@ -438,6 +436,8 @@ export interface Source { OverviewNetwork?: Maybe; OverviewHost?: Maybe; + + Tls: TlsData; /** Gets UncommonProcesses based on a timerange, or all UncommonProcesses if no criteria is specified */ UncommonProcesses: UncommonProcessesData; /** Just a simple example to get the app name */ @@ -1316,38 +1316,6 @@ export interface AutonomousSystemOrganization { name?: Maybe; } -export interface TlsData { - edges: TlsEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe; -} - -export interface TlsEdges { - node: TlsNode; - - cursor: CursorType; -} - -export interface TlsNode { - _id?: Maybe; - - timestamp?: Maybe; - - alternativeNames?: Maybe; - - notAfter?: Maybe; - - commonNames?: Maybe; - - ja3?: Maybe; - - issuerNames?: Maybe; -} - export interface UsersData { edges: UsersEdges[]; @@ -1668,6 +1636,38 @@ export interface OverviewHostData { inspect?: Maybe; } +export interface TlsData { + edges: TlsEdges[]; + + totalCount: number; + + pageInfo: PageInfoPaginated; + + inspect?: Maybe; +} + +export interface TlsEdges { + node: TlsNode; + + cursor: CursorType; +} + +export interface TlsNode { + _id?: Maybe; + + timestamp?: Maybe; + + alternativeNames?: Maybe; + + notAfter?: Maybe; + + commonNames?: Maybe; + + ja3?: Maybe; + + issuerNames?: Maybe; +} + export interface UncommonProcessesData { edges: UncommonProcessesEdges[]; @@ -2054,23 +2054,6 @@ export interface IpOverviewSourceArgs { defaultIndex: string[]; } -export interface TlsSourceArgs { - filterQuery?: Maybe; - - id?: Maybe; - - ip: string; - - pagination: PaginationInputPaginated; - - sort: TlsSortField; - - flowTarget: FlowTarget; - - timerange: TimerangeInput; - - defaultIndex: string[]; -} export interface UsersSourceArgs { filterQuery?: Maybe; @@ -2182,6 +2165,23 @@ export interface OverviewHostSourceArgs { defaultIndex: string[]; } +export interface TlsSourceArgs { + filterQuery?: Maybe; + + id?: Maybe; + + ip: string; + + pagination: PaginationInputPaginated; + + sort: TlsSortField; + + flowTarget: FlowTargetSourceDest; + + timerange: TimerangeInput; + + defaultIndex: string[]; +} export interface UncommonProcessesSourceArgs { timerange: TimerangeInput; @@ -2602,8 +2602,6 @@ export namespace SourceResolvers { IpOverview?: IpOverviewResolver, TypeParent, TContext>; - Tls?: TlsResolver; - Users?: UsersResolver; KpiNetwork?: KpiNetworkResolver, TypeParent, TContext>; @@ -2625,6 +2623,8 @@ export namespace SourceResolvers { OverviewNetwork?: OverviewNetworkResolver, TypeParent, TContext>; OverviewHost?: OverviewHostResolver, TypeParent, TContext>; + + Tls?: TlsResolver; /** Gets UncommonProcesses based on a timerange, or all UncommonProcesses if no criteria is specified */ UncommonProcesses?: UncommonProcessesResolver; /** Just a simple example to get the app name */ @@ -2784,30 +2784,6 @@ export namespace SourceResolvers { defaultIndex: string[]; } - export type TlsResolver = Resolver< - R, - Parent, - TContext, - TlsArgs - >; - export interface TlsArgs { - filterQuery?: Maybe; - - id?: Maybe; - - ip: string; - - pagination: PaginationInputPaginated; - - sort: TlsSortField; - - flowTarget: FlowTarget; - - timerange: TimerangeInput; - - defaultIndex: string[]; - } - export type UsersResolver = Resolver< R, Parent, @@ -2974,6 +2950,30 @@ export namespace SourceResolvers { defaultIndex: string[]; } + export type TlsResolver = Resolver< + R, + Parent, + TContext, + TlsArgs + >; + export interface TlsArgs { + filterQuery?: Maybe; + + id?: Maybe; + + ip: string; + + pagination: PaginationInputPaginated; + + sort: TlsSortField; + + flowTarget: FlowTargetSourceDest; + + timerange: TimerangeInput; + + defaultIndex: string[]; + } + export type UncommonProcessesResolver< R = UncommonProcessesData, Parent = Source, @@ -5879,112 +5879,6 @@ export namespace AutonomousSystemOrganizationResolvers { > = Resolver; } -export namespace TlsDataResolvers { - export interface Resolvers { - edges?: EdgesResolver; - - totalCount?: TotalCountResolver; - - pageInfo?: PageInfoResolver; - - inspect?: InspectResolver, TypeParent, TContext>; - } - - export type EdgesResolver = Resolver< - R, - Parent, - TContext - >; - export type TotalCountResolver = Resolver< - R, - Parent, - TContext - >; - export type PageInfoResolver< - R = PageInfoPaginated, - Parent = TlsData, - TContext = SiemContext - > = Resolver; - export type InspectResolver< - R = Maybe, - Parent = TlsData, - TContext = SiemContext - > = Resolver; -} - -export namespace TlsEdgesResolvers { - export interface Resolvers { - node?: NodeResolver; - - cursor?: CursorResolver; - } - - export type NodeResolver = Resolver< - R, - Parent, - TContext - >; - export type CursorResolver = Resolver< - R, - Parent, - TContext - >; -} - -export namespace TlsNodeResolvers { - export interface Resolvers { - _id?: _IdResolver, TypeParent, TContext>; - - timestamp?: TimestampResolver, TypeParent, TContext>; - - alternativeNames?: AlternativeNamesResolver, TypeParent, TContext>; - - notAfter?: NotAfterResolver, TypeParent, TContext>; - - commonNames?: CommonNamesResolver, TypeParent, TContext>; - - ja3?: Ja3Resolver, TypeParent, TContext>; - - issuerNames?: IssuerNamesResolver, TypeParent, TContext>; - } - - export type _IdResolver, Parent = TlsNode, TContext = SiemContext> = Resolver< - R, - Parent, - TContext - >; - export type TimestampResolver< - R = Maybe, - Parent = TlsNode, - TContext = SiemContext - > = Resolver; - export type AlternativeNamesResolver< - R = Maybe, - Parent = TlsNode, - TContext = SiemContext - > = Resolver; - export type NotAfterResolver< - R = Maybe, - Parent = TlsNode, - TContext = SiemContext - > = Resolver; - export type CommonNamesResolver< - R = Maybe, - Parent = TlsNode, - TContext = SiemContext - > = Resolver; - export type Ja3Resolver, Parent = TlsNode, TContext = SiemContext> = Resolver< - R, - Parent, - TContext - >; - export type IssuerNamesResolver< - R = Maybe, - Parent = TlsNode, - TContext = SiemContext - > = Resolver; -} - export namespace UsersDataResolvers { export interface Resolvers { edges?: EdgesResolver; @@ -7095,6 +6989,112 @@ export namespace OverviewHostDataResolvers { > = Resolver; } +export namespace TlsDataResolvers { + export interface Resolvers { + edges?: EdgesResolver; + + totalCount?: TotalCountResolver; + + pageInfo?: PageInfoResolver; + + inspect?: InspectResolver, TypeParent, TContext>; + } + + export type EdgesResolver = Resolver< + R, + Parent, + TContext + >; + export type TotalCountResolver = Resolver< + R, + Parent, + TContext + >; + export type PageInfoResolver< + R = PageInfoPaginated, + Parent = TlsData, + TContext = SiemContext + > = Resolver; + export type InspectResolver< + R = Maybe, + Parent = TlsData, + TContext = SiemContext + > = Resolver; +} + +export namespace TlsEdgesResolvers { + export interface Resolvers { + node?: NodeResolver; + + cursor?: CursorResolver; + } + + export type NodeResolver = Resolver< + R, + Parent, + TContext + >; + export type CursorResolver = Resolver< + R, + Parent, + TContext + >; +} + +export namespace TlsNodeResolvers { + export interface Resolvers { + _id?: _IdResolver, TypeParent, TContext>; + + timestamp?: TimestampResolver, TypeParent, TContext>; + + alternativeNames?: AlternativeNamesResolver, TypeParent, TContext>; + + notAfter?: NotAfterResolver, TypeParent, TContext>; + + commonNames?: CommonNamesResolver, TypeParent, TContext>; + + ja3?: Ja3Resolver, TypeParent, TContext>; + + issuerNames?: IssuerNamesResolver, TypeParent, TContext>; + } + + export type _IdResolver, Parent = TlsNode, TContext = SiemContext> = Resolver< + R, + Parent, + TContext + >; + export type TimestampResolver< + R = Maybe, + Parent = TlsNode, + TContext = SiemContext + > = Resolver; + export type AlternativeNamesResolver< + R = Maybe, + Parent = TlsNode, + TContext = SiemContext + > = Resolver; + export type NotAfterResolver< + R = Maybe, + Parent = TlsNode, + TContext = SiemContext + > = Resolver; + export type CommonNamesResolver< + R = Maybe, + Parent = TlsNode, + TContext = SiemContext + > = Resolver; + export type Ja3Resolver, Parent = TlsNode, TContext = SiemContext> = Resolver< + R, + Parent, + TContext + >; + export type IssuerNamesResolver< + R = Maybe, + Parent = TlsNode, + TContext = SiemContext + > = Resolver; +} + export namespace UncommonProcessesDataResolvers { export interface Resolvers { edges?: EdgesResolver; @@ -8173,9 +8173,6 @@ export type IResolvers = { Overview?: OverviewResolvers.Resolvers; AutonomousSystem?: AutonomousSystemResolvers.Resolvers; AutonomousSystemOrganization?: AutonomousSystemOrganizationResolvers.Resolvers; - TlsData?: TlsDataResolvers.Resolvers; - TlsEdges?: TlsEdgesResolvers.Resolvers; - TlsNode?: TlsNodeResolvers.Resolvers; UsersData?: UsersDataResolvers.Resolvers; UsersEdges?: UsersEdgesResolvers.Resolvers; UsersNode?: UsersNodeResolvers.Resolvers; @@ -8203,6 +8200,9 @@ export type IResolvers = { NetworkDnsItem?: NetworkDnsItemResolvers.Resolvers; OverviewNetworkData?: OverviewNetworkDataResolvers.Resolvers; OverviewHostData?: OverviewHostDataResolvers.Resolvers; + TlsData?: TlsDataResolvers.Resolvers; + TlsEdges?: TlsEdgesResolvers.Resolvers; + TlsNode?: TlsNodeResolvers.Resolvers; UncommonProcessesData?: UncommonProcessesDataResolvers.Resolvers; UncommonProcessesEdges?: UncommonProcessesEdgesResolvers.Resolvers; UncommonProcessItem?: UncommonProcessItemResolvers.Resolvers; diff --git a/x-pack/legacy/plugins/siem/server/init_server.ts b/x-pack/legacy/plugins/siem/server/init_server.ts index 2b75f7dcc5d45..865ec4e4204b2 100644 --- a/x-pack/legacy/plugins/siem/server/init_server.ts +++ b/x-pack/legacy/plugins/siem/server/init_server.ts @@ -28,6 +28,7 @@ import { createUncommonProcessesResolvers } from './graphql/uncommon_processes'; import { createWhoAmIResolvers } from './graphql/who_am_i'; import { AppBackendLibs } from './lib/types'; import { Logger } from './utils/logger'; +import { createTlsResolvers } from './graphql/tls'; export interface Config { mocking: boolean; @@ -56,6 +57,7 @@ export const initServer = (libs: AppBackendLibs, config: Config) => { createSourcesResolvers(libs) as IResolvers, createSourceStatusResolvers(libs) as IResolvers, createTimelineResolvers(libs) as IResolvers, + createTlsResolvers(libs) as IResolvers, createUncommonProcessesResolvers(libs) as IResolvers, createWhoAmIResolvers() as IResolvers, createKpiHostsResolvers(libs) as IResolvers, diff --git a/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts b/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts index 593d33feb5772..757c16679361d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts +++ b/x-pack/legacy/plugins/siem/server/lib/compose/kibana.ts @@ -16,7 +16,9 @@ import { KpiHosts } from '../kpi_hosts'; import { ElasticsearchKpiHostsAdapter } from '../kpi_hosts/elasticsearch_adapter'; import { ElasticsearchIndexFieldAdapter, IndexFields } from '../index_fields'; -import { ElasticsearchIpOverviewAdapter, IpDetails } from '../ip_details'; +import { ElasticsearchIpDetailsAdapter, IpDetails } from '../ip_details'; +import { ElasticsearchTlsAdapter, TLS } from '../tls'; + import { KpiNetwork } from '../kpi_network'; import { ElasticsearchKpiNetworkAdapter } from '../kpi_network/elasticsearch_adapter'; import { ElasticsearchNetworkAdapter, Network } from '../network'; @@ -45,7 +47,8 @@ export function compose(server: Server): AppBackendLibs { events: new Events(new ElasticsearchEventsAdapter(framework)), fields: new IndexFields(new ElasticsearchIndexFieldAdapter(framework)), hosts: new Hosts(new ElasticsearchHostsAdapter(framework)), - ipDetails: new IpDetails(new ElasticsearchIpOverviewAdapter(framework)), + ipDetails: new IpDetails(new ElasticsearchIpDetailsAdapter(framework)), + tls: new TLS(new ElasticsearchTlsAdapter(framework)), kpiHosts: new KpiHosts(new ElasticsearchKpiHostsAdapter(framework)), kpiNetwork: new KpiNetwork(new ElasticsearchKpiNetworkAdapter(framework)), network: new Network(new ElasticsearchNetworkAdapter(framework)), diff --git a/x-pack/legacy/plugins/siem/server/lib/ip_details/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/ip_details/elasticsearch_adapter.ts index 5443ff62f6227..90803ca302bd4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/ip_details/elasticsearch_adapter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/ip_details/elasticsearch_adapter.ts @@ -11,8 +11,6 @@ import { GeoEcsFields, HostEcsFields, IpOverviewData, - TlsData, - TlsEdges, UsersData, UsersEdges, } from '../../graphql/types'; @@ -20,22 +18,19 @@ import { inspectStringifyObject } from '../../utils/build_query'; import { DatabaseSearchResponse, FrameworkAdapter, FrameworkRequest } from '../framework'; import { TermAggregation } from '../types'; import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants'; -import { IpOverviewRequestOptions, TlsRequestOptions, UsersRequestOptions } from './index'; +import { IpOverviewRequestOptions, UsersRequestOptions } from './index'; import { buildOverviewQuery } from './query_overview.dsl'; +import { buildUsersQuery } from './query_users.dsl'; + import { IpDetailsAdapter, IpOverviewHit, OverviewHit, OverviewHostHit, - TlsBuckets, UsersBucketsItem, } from './types'; -import { buildTlsQuery } from './query_tls.dsl'; - -import { buildUsersQuery } from './query_users.dsl'; - -export class ElasticsearchIpOverviewAdapter implements IpDetailsAdapter { +export class ElasticsearchIpDetailsAdapter implements IpDetailsAdapter { constructor(private readonly framework: FrameworkAdapter) {} public async getIpDetails( @@ -62,39 +57,6 @@ export class ElasticsearchIpOverviewAdapter implements IpDetailsAdapter { }; } - public async getTls(request: FrameworkRequest, options: TlsRequestOptions): Promise { - if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { - throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); - } - const dsl = buildTlsQuery(options); - const response = await this.framework.callWithRequest( - request, - 'search', - dsl - ); - - const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; - const totalCount = getOr(0, 'aggregations.count.value', response); - const tlsEdges: TlsEdges[] = getTlsEdges(response, options); - const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; - const edges = tlsEdges.splice(cursorStart, querySize - cursorStart); - const inspect = { - dsl: [inspectStringifyObject(dsl)], - response: [inspectStringifyObject(response)], - }; - const showMorePagesIndicator = totalCount > fakeTotalCount; - return { - edges, - inspect, - pageInfo: { - activePage: activePage ? activePage : 0, - fakeTotalCount, - showMorePagesIndicator, - }, - totalCount, - }; - } - public async getUsers( request: FrameworkRequest, options: UsersRequestOptions @@ -173,34 +135,6 @@ export const getIpOverviewHostAgg = (overviewHostHit: OverviewHostHit | {}) => { }; }; -const getTlsEdges = ( - response: DatabaseSearchResponse, - options: TlsRequestOptions -): TlsEdges[] => { - return formatTlsEdges(getOr([], 'aggregations.sha1.buckets', response)); -}; - -export const formatTlsEdges = (buckets: TlsBuckets[]): TlsEdges[] => { - return buckets.map((bucket: TlsBuckets) => { - const edge: TlsEdges = { - node: { - _id: bucket.key, - alternativeNames: bucket.alternative_names.buckets.map(({ key }) => key), - commonNames: bucket.common_names.buckets.map(({ key }) => key), - ja3: bucket.ja3.buckets.map(({ key }) => key), - issuerNames: bucket.issuer_names.buckets.map(({ key }) => key), - // eslint-disable-next-line @typescript-eslint/camelcase - notAfter: bucket.not_after.buckets.map(({ key_as_string }) => key_as_string), - }, - cursor: { - value: bucket.key, - tiebreaker: null, - }, - }; - return edge; - }); -}; - export const getUsersEdges = ( response: DatabaseSearchResponse ): UsersEdges[] => diff --git a/x-pack/legacy/plugins/siem/server/lib/ip_details/index.ts b/x-pack/legacy/plugins/siem/server/lib/ip_details/index.ts index 4a16664790a05..d1e1ed1dcc422 100644 --- a/x-pack/legacy/plugins/siem/server/lib/ip_details/index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/ip_details/index.ts @@ -4,14 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - FlowTarget, - IpOverviewData, - TlsSortField, - TlsData, - UsersData, - UsersSortField, -} from '../../graphql/types'; +import { FlowTarget, IpOverviewData, UsersData, UsersSortField } from '../../graphql/types'; import { FrameworkRequest, RequestOptions, RequestOptionsPaginated } from '../framework'; import { IpDetailsAdapter } from './types'; @@ -22,11 +15,6 @@ export interface IpOverviewRequestOptions extends RequestOptions { ip: string; } -export interface TlsRequestOptions extends RequestOptionsPaginated { - ip: string; - tlsSortField: TlsSortField; - flowTarget: FlowTarget; -} export interface UsersRequestOptions extends RequestOptionsPaginated { ip: string; usersSortField: UsersSortField; @@ -43,10 +31,6 @@ export class IpDetails { return this.adapter.getIpDetails(req, options); } - public async getTls(req: FrameworkRequest, options: TlsRequestOptions): Promise { - return this.adapter.getTls(req, options); - } - public async getUsers(req: FrameworkRequest, options: UsersRequestOptions): Promise { return this.adapter.getUsers(req, options); } diff --git a/x-pack/legacy/plugins/siem/server/lib/ip_details/types.ts b/x-pack/legacy/plugins/siem/server/lib/ip_details/types.ts index be7519e64b651..d137d919932f7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/ip_details/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/ip_details/types.ts @@ -4,13 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TlsData, IpOverviewData, UsersData } from '../../graphql/types'; +import { IpOverviewData, UsersData } from '../../graphql/types'; import { FrameworkRequest, RequestBasicOptions } from '../framework'; import { Hit, ShardsResponse, TotalValue } from '../types'; export interface IpDetailsAdapter { getIpDetails(request: FrameworkRequest, options: RequestBasicOptions): Promise; - getTls(request: FrameworkRequest, options: RequestBasicOptions): Promise; getUsers(request: FrameworkRequest, options: RequestBasicOptions): Promise; } @@ -81,34 +80,6 @@ export interface IpOverviewHit { timeout: number; } -export interface TlsBuckets { - key: string; - timestamp?: { - value: number; - value_as_string: string; - }; - - alternative_names: { - buckets: Readonly>; - }; - - common_names: { - buckets: Readonly>; - }; - - ja3: { - buckets: Readonly>; - }; - - issuer_names: { - buckets: Readonly>; - }; - - not_after: { - buckets: Readonly>; - }; -} - // Users Table export interface UsersResponse { diff --git a/x-pack/legacy/plugins/siem/server/lib/tls/elasticsearch_adapter.test.ts b/x-pack/legacy/plugins/siem/server/lib/tls/elasticsearch_adapter.test.ts new file mode 100644 index 0000000000000..0ba0dfcb516fd --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/tls/elasticsearch_adapter.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { buildTlsQuery } from './query_tls.dsl'; +import { ElasticsearchTlsAdapter } from './elasticsearch_adapter'; +import expect from '@kbn/expect'; +import { FrameworkRequest, FrameworkAdapter } from '../framework'; +import { mockRequest, mockResponse, mockOptions, expectedTlsEdges, mockTlsQuery } from './mock'; +import { TlsData } from '../../graphql/types'; + +jest.mock('./query_tls.dsl', () => { + return { + buildTlsQuery: jest.fn(), + }; +}); + +describe('elasticsearch_adapter', () => { + describe('#getTls', () => { + let data: TlsData; + const mockCallWithRequest = jest.fn(); + const mockFramework: FrameworkAdapter = { + version: 'mock', + callWithRequest: mockCallWithRequest, + exposeStaticDir: jest.fn(), + registerGraphQLEndpoint: jest.fn(), + getIndexPatternsService: jest.fn(), + getSavedObjectsService: jest.fn(), + }; + + beforeAll(async () => { + (buildTlsQuery as jest.Mock).mockReset(); + (buildTlsQuery as jest.Mock).mockReturnValue(mockTlsQuery); + + mockCallWithRequest.mockResolvedValue(mockResponse); + jest.doMock('../framework', () => ({ + callWithRequest: mockCallWithRequest, + })); + + const EsTls = new ElasticsearchTlsAdapter(mockFramework); + data = await EsTls.getTls(mockRequest as FrameworkRequest, mockOptions); + }); + + afterAll(() => { + mockCallWithRequest.mockRestore(); + (buildTlsQuery as jest.Mock).mockClear(); + }); + + test('buildTlsQuery', () => { + expect((buildTlsQuery as jest.Mock).mock.calls[0][0]).to.eql(mockOptions); + }); + + test('will return tlsEdges correctly', () => { + expect(data.edges).to.eql(expectedTlsEdges); + }); + + test('will return inspect data', () => { + expect(data.inspect).to.eql({ + dsl: [JSON.stringify(mockTlsQuery, null, 2)], + response: [JSON.stringify(mockResponse, null, 2)], + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/tls/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/tls/elasticsearch_adapter.ts new file mode 100644 index 0000000000000..716eea3f8df5b --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/tls/elasticsearch_adapter.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; + +import { TlsData, TlsEdges } from '../../graphql/types'; +import { inspectStringifyObject } from '../../utils/build_query'; +import { DatabaseSearchResponse, FrameworkAdapter, FrameworkRequest } from '../framework'; +import { TermAggregation } from '../types'; +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants'; +import { TlsRequestOptions } from './index'; + +import { TlsAdapter, TlsBuckets } from './types'; + +import { buildTlsQuery } from './query_tls.dsl'; + +export class ElasticsearchTlsAdapter implements TlsAdapter { + constructor(private readonly framework: FrameworkAdapter) {} + + public async getTls(request: FrameworkRequest, options: TlsRequestOptions): Promise { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + const dsl = buildTlsQuery(options); + const response = await this.framework.callWithRequest( + request, + 'search', + dsl + ); + + const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + const totalCount = getOr(0, 'aggregations.count.value', response); + const tlsEdges: TlsEdges[] = getTlsEdges(response, options); + const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; + const edges = tlsEdges.splice(cursorStart, querySize - cursorStart); + const inspect = { + dsl: [inspectStringifyObject(dsl)], + response: [inspectStringifyObject(response)], + }; + const showMorePagesIndicator = totalCount > fakeTotalCount; + return { + edges, + inspect, + pageInfo: { + activePage: activePage ? activePage : 0, + fakeTotalCount, + showMorePagesIndicator, + }, + totalCount, + }; + } +} + +const getTlsEdges = ( + response: DatabaseSearchResponse, + options: TlsRequestOptions +): TlsEdges[] => { + return formatTlsEdges(getOr([], 'aggregations.sha1.buckets', response)); +}; + +export const formatTlsEdges = (buckets: TlsBuckets[]): TlsEdges[] => { + return buckets.map((bucket: TlsBuckets) => { + const edge: TlsEdges = { + node: { + _id: bucket.key, + alternativeNames: bucket.alternative_names.buckets.map(({ key }) => key), + commonNames: bucket.common_names.buckets.map(({ key }) => key), + ja3: bucket.ja3.buckets.map(({ key }) => key), + issuerNames: bucket.issuer_names.buckets.map(({ key }) => key), + // eslint-disable-next-line @typescript-eslint/camelcase + notAfter: bucket.not_after.buckets.map(({ key_as_string }) => key_as_string), + }, + cursor: { + value: bucket.key, + tiebreaker: null, + }, + }; + return edge; + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/tls/index.ts b/x-pack/legacy/plugins/siem/server/lib/tls/index.ts new file mode 100644 index 0000000000000..9a28437feae81 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/tls/index.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 { FlowTargetSourceDest, TlsSortField, TlsData } from '../../graphql/types'; +import { FrameworkRequest, RequestOptionsPaginated } from '../framework'; + +import { TlsAdapter } from './types'; + +export * from './elasticsearch_adapter'; + +export interface TlsRequestOptions extends RequestOptionsPaginated { + ip?: string; + tlsSortField: TlsSortField; + flowTarget: FlowTargetSourceDest; +} + +export class TLS { + constructor(private readonly adapter: TlsAdapter) {} + + public async getTls(req: FrameworkRequest, options: TlsRequestOptions): Promise { + return this.adapter.getTls(req, options); + } +} diff --git a/x-pack/legacy/plugins/siem/server/lib/tls/mock.ts b/x-pack/legacy/plugins/siem/server/lib/tls/mock.ts new file mode 100644 index 0000000000000..04f3526b811dc --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/tls/mock.ts @@ -0,0 +1,663 @@ +/* + * 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 { Direction, TlsFields, FlowTargetSourceDest } from '../../graphql/types'; + +export const mockTlsQuery = { + allowNoIndices: true, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + ignoreUnavailable: true, + body: { + aggs: { + count: { cardinality: { field: 'tls.server_certificate.fingerprint.sha1' } }, + sha1: { + terms: { + field: 'tls.server_certificate.fingerprint.sha1', + size: 10, + order: { _key: 'desc' }, + }, + aggs: { + issuer_names: { terms: { field: 'tls.server_certificate.issuer.common_name' } }, + common_names: { terms: { field: 'tls.server_certificate.subject.common_name' } }, + alternative_names: { terms: { field: 'tls.server_certificate.alternative_names' } }, + not_after: { terms: { field: 'tls.server_certificate.not_after' } }, + ja3: { terms: { field: 'tls.fingerprints.ja3.hash' } }, + }, + }, + }, + query: { + bool: { filter: [{ range: { '@timestamp': { gte: 1570719927430, lte: 1570806327431 } } }] }, + }, + size: 0, + track_total_hits: false, + }, +}; + +export const expectedTlsEdges = [ + { + cursor: { + tiebreaker: null, + value: 'fff8dc95436e0e25ce46b1526a1a547e8cf3bb82', + }, + node: { + _id: 'fff8dc95436e0e25ce46b1526a1a547e8cf3bb82', + alternativeNames: [ + '*.1.nflxso.net', + '*.a.nflxso.net', + 'assets.nflxext.com', + 'cast.netflix.com', + 'codex.nflxext.com', + 'tvui.netflix.com', + ], + commonNames: ['*.1.nflxso.net'], + issuerNames: ['DigiCert SHA2 Secure Server CA'], + ja3: ['95d2dd53a89b334cddd5c22e81e7fe61'], + notAfter: ['2019-10-27T12:00:00.000Z'], + }, + }, + { + cursor: { + tiebreaker: null, + value: 'fd8440c4b20978b173e0910e2639d114f0d405c5', + }, + node: { + _id: 'fd8440c4b20978b173e0910e2639d114f0d405c5', + alternativeNames: ['*.cogocast.net', 'cogocast.net'], + commonNames: ['cogocast.net'], + issuerNames: ['Amazon'], + ja3: ['a111d93cdf31f993c40a8a9ef13e8d7e'], + notAfter: ['2020-02-01T12:00:00.000Z'], + }, + }, + { + cursor: { tiebreaker: null, value: 'fcdc16645ebb3386adc96e7ba735c4745709b9dd' }, + node: { + _id: 'fcdc16645ebb3386adc96e7ba735c4745709b9dd', + alternativeNames: [ + 'player-devintever2-imperva.mountain.siriusxm.com', + 'player-devintever2.mountain.siriusxm.com', + ], + commonNames: ['player-devintever2.mountain.siriusxm.com'], + issuerNames: ['Trustwave Organization Validation SHA256 CA, Level 1'], + ja3: ['6fa3244afc6bb6f9fad207b6b52af26b'], + notAfter: ['2020-03-06T21:57:09.000Z'], + }, + }, + { + cursor: { tiebreaker: null, value: 'fccf375789cb7e671502a7b0cc969f218a4b2c70' }, + node: { + _id: 'fccf375789cb7e671502a7b0cc969f218a4b2c70', + alternativeNames: [ + 'appleid-nc-s.apple.com', + 'appleid-nwk-s.apple.com', + 'appleid-prn-s.apple.com', + 'appleid-rno-s.apple.com', + 'appleid.apple.com', + ], + commonNames: ['appleid.apple.com'], + issuerNames: ['DigiCert SHA2 Extended Validation Server CA'], + ja3: ['6fa3244afc6bb6f9fad207b6b52af26b'], + notAfter: ['2020-07-04T12:00:00.000Z'], + }, + }, + { + cursor: { tiebreaker: null, value: 'fc4a296b706fa18ac50b96f5c0327c69db4a8981' }, + node: { + _id: 'fc4a296b706fa18ac50b96f5c0327c69db4a8981', + alternativeNames: [ + 'api.itunes.apple.com', + 'appsto.re', + 'ax.init.itunes.apple.com', + 'bag.itunes.apple.com', + 'bookkeeper.itunes.apple.com', + 'c.itunes.apple.com', + 'carrierbundle.itunes.apple.com', + 'client-api.itunes.apple.com', + 'cma.itunes.apple.com', + 'courses.apple.com', + ], + commonNames: ['itunes.apple.com'], + issuerNames: ['DigiCert SHA2 Extended Validation Server CA'], + ja3: ['a441a33aaee795f498d6b764cc78989a'], + notAfter: ['2020-03-24T12:00:00.000Z'], + }, + }, + { + cursor: { tiebreaker: null, value: 'fc2cbc41f6a0e9c0118de4fe40f299f7207b797e' }, + node: { + _id: 'fc2cbc41f6a0e9c0118de4fe40f299f7207b797e', + alternativeNames: [ + '*.adlercasino.com', + '*.allaustraliancasino.com', + '*.alletf.com', + '*.appareldesignpartners.com', + '*.atmosfir.net', + '*.cityofboston.gov', + '*.cp.mytoyotaentune.com', + '*.decathlon.be', + '*.decathlon.co.uk', + '*.decathlon.de', + ], + commonNames: ['incapsula.com'], + issuerNames: ['GlobalSign CloudSSL CA - SHA256 - G3'], + ja3: ['6fa3244afc6bb6f9fad207b6b52af26b'], + notAfter: ['2020-04-04T14:05:06.000Z'], + }, + }, + { + cursor: { tiebreaker: null, value: 'fb70d78ffa663a3a4374d841b3288d2de9759566' }, + node: { + _id: 'fb70d78ffa663a3a4374d841b3288d2de9759566', + alternativeNames: ['*.siriusxm.com', 'siriusxm.com'], + commonNames: ['*.siriusxm.com'], + issuerNames: ['DigiCert Baltimore CA-2 G2'], + ja3: ['535aca3d99fc247509cd50933cd71d37', '6fa3244afc6bb6f9fad207b6b52af26b'], + notAfter: ['2021-10-27T12:00:00.000Z'], + }, + }, + { + cursor: { tiebreaker: null, value: 'fb59038dcec33ab3a01a6ae60d0835ad0e04ccf0' }, + node: { + _id: 'fb59038dcec33ab3a01a6ae60d0835ad0e04ccf0', + alternativeNames: [ + 'photos.amazon.co.uk', + 'photos.amazon.de', + 'photos.amazon.es', + 'photos.amazon.eu', + 'photos.amazon.fr', + 'photos.amazon.it', + ], + commonNames: ['photos.amazon.eu'], + issuerNames: ['Amazon'], + ja3: ['6fa3244afc6bb6f9fad207b6b52af26b'], + notAfter: ['2020-04-23T12:00:00.000Z'], + }, + }, + { + cursor: { tiebreaker: null, value: 'f9815293c883a6006f0b2d95a4895bdc501fd174' }, + node: { + _id: 'f9815293c883a6006f0b2d95a4895bdc501fd174', + alternativeNames: [ + '*.api.cdn.hbo.com', + '*.artist.cdn.hbo.com', + '*.cdn.hbo.com', + '*.lv3.cdn.hbo.com', + 'artist.api.cdn.hbo.com', + 'artist.api.lv3.cdn.hbo.com', + 'artist.staging.cdn.hbo.com', + 'artist.staging.hurley.lv3.cdn.hbo.com', + 'atv.api.lv3.cdn.hbo.com', + 'atv.staging.hurley.lv3.cdn.hbo.com', + ], + commonNames: ['cdn.hbo.com'], + issuerNames: ['Sectigo RSA Organization Validation Secure Server CA'], + ja3: ['6fa3244afc6bb6f9fad207b6b52af26b'], + notAfter: ['2021-02-10T23:59:59.000Z'], + }, + }, + { + cursor: { tiebreaker: null, value: 'f8db6a69797e383dca2529727369595733123386' }, + node: { + _id: 'f8db6a69797e383dca2529727369595733123386', + alternativeNames: ['www.google.com'], + commonNames: ['www.google.com'], + issuerNames: ['GTS CA 1O1'], + ja3: ['a111d93cdf31f993c40a8a9ef13e8d7e'], + notAfter: ['2019-12-10T13:32:54.000Z'], + }, + }, +]; + +export const mockRequest = { + params: {}, + payload: { + operationName: 'GetTlsQuery', + variables: { + defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + filterQuery: '', + flowTarget: 'source', + inspect: false, + ip: '', + pagination: { activePage: 0, cursorStart: 0, fakePossibleCount: 50, querySize: 10 }, + sort: { field: '_id', direction: 'desc' }, + sourceId: 'default', + timerange: { interval: '12h', from: 1570716261267, to: 1570802661267 }, + }, + query: + 'query GetTlsQuery($sourceId: ID!, $filterQuery: String, $flowTarget: FlowTarget!, $ip: String!, $pagination: PaginationInputPaginated!, $sort: TlsSortField!, $timerange: TimerangeInput!, $defaultIndex: [String!]!, $inspect: Boolean!) {\n source(id: $sourceId) {\n id\n Tls(filterQuery: $filterQuery, flowTarget: $flowTarget, ip: $ip, pagination: $pagination, sort: $sort, timerange: $timerange, defaultIndex: $defaultIndex) {\n totalCount\n edges {\n node {\n _id\n alternativeNames\n commonNames\n ja3\n issuerNames\n notAfter\n __typename\n }\n cursor {\n value\n __typename\n }\n __typename\n }\n pageInfo {\n activePage\n fakeTotalCount\n showMorePagesIndicator\n __typename\n }\n inspect @include(if: $inspect) {\n dsl\n response\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', + }, + query: {}, +}; + +export const mockResponse = { + took: 92, + timed_out: false, + _shards: { total: 33, successful: 33, skipped: 0, failed: 0 }, + hits: { max_score: null, hits: [] }, + aggregations: { + sha1: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 4597, + buckets: [ + { + key: 'fff8dc95436e0e25ce46b1526a1a547e8cf3bb82', + doc_count: 1, + not_after: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 1572177600000, key_as_string: '2019-10-27T12:00:00.000Z', doc_count: 1 }, + ], + }, + issuer_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'DigiCert SHA2 Secure Server CA', doc_count: 1 }], + }, + common_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: '*.1.nflxso.net', doc_count: 1 }], + }, + alternative_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: '*.1.nflxso.net', doc_count: 1 }, + { key: '*.a.nflxso.net', doc_count: 1 }, + { key: 'assets.nflxext.com', doc_count: 1 }, + { key: 'cast.netflix.com', doc_count: 1 }, + { key: 'codex.nflxext.com', doc_count: 1 }, + { key: 'tvui.netflix.com', doc_count: 1 }, + ], + }, + ja3: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: '95d2dd53a89b334cddd5c22e81e7fe61', doc_count: 1 }], + }, + }, + { + key: 'fd8440c4b20978b173e0910e2639d114f0d405c5', + doc_count: 1, + not_after: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 1580558400000, key_as_string: '2020-02-01T12:00:00.000Z', doc_count: 1 }, + ], + }, + issuer_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'Amazon', doc_count: 1 }], + }, + common_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'cogocast.net', doc_count: 1 }], + }, + alternative_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: '*.cogocast.net', doc_count: 1 }, + { key: 'cogocast.net', doc_count: 1 }, + ], + }, + ja3: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'a111d93cdf31f993c40a8a9ef13e8d7e', doc_count: 1 }], + }, + }, + { + key: 'fcdc16645ebb3386adc96e7ba735c4745709b9dd', + doc_count: 1, + not_after: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 1583531829000, key_as_string: '2020-03-06T21:57:09.000Z', doc_count: 1 }, + ], + }, + issuer_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'Trustwave Organization Validation SHA256 CA, Level 1', doc_count: 1 }, + ], + }, + common_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'player-devintever2.mountain.siriusxm.com', doc_count: 1 }], + }, + alternative_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'player-devintever2-imperva.mountain.siriusxm.com', doc_count: 1 }, + { key: 'player-devintever2.mountain.siriusxm.com', doc_count: 1 }, + ], + }, + ja3: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: '6fa3244afc6bb6f9fad207b6b52af26b', doc_count: 1 }], + }, + }, + { + key: 'fccf375789cb7e671502a7b0cc969f218a4b2c70', + doc_count: 1, + not_after: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 1593864000000, key_as_string: '2020-07-04T12:00:00.000Z', doc_count: 1 }, + ], + }, + issuer_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'DigiCert SHA2 Extended Validation Server CA', doc_count: 1 }], + }, + common_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'appleid.apple.com', doc_count: 1 }], + }, + alternative_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'appleid-nc-s.apple.com', doc_count: 1 }, + { key: 'appleid-nwk-s.apple.com', doc_count: 1 }, + { key: 'appleid-prn-s.apple.com', doc_count: 1 }, + { key: 'appleid-rno-s.apple.com', doc_count: 1 }, + { key: 'appleid.apple.com', doc_count: 1 }, + ], + }, + ja3: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: '6fa3244afc6bb6f9fad207b6b52af26b', doc_count: 1 }], + }, + }, + { + key: 'fc4a296b706fa18ac50b96f5c0327c69db4a8981', + doc_count: 2, + not_after: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 1585051200000, key_as_string: '2020-03-24T12:00:00.000Z', doc_count: 2 }, + ], + }, + issuer_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'DigiCert SHA2 Extended Validation Server CA', doc_count: 2 }], + }, + common_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'itunes.apple.com', doc_count: 2 }], + }, + alternative_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 156, + buckets: [ + { key: 'api.itunes.apple.com', doc_count: 2 }, + { key: 'appsto.re', doc_count: 2 }, + { key: 'ax.init.itunes.apple.com', doc_count: 2 }, + { key: 'bag.itunes.apple.com', doc_count: 2 }, + { key: 'bookkeeper.itunes.apple.com', doc_count: 2 }, + { key: 'c.itunes.apple.com', doc_count: 2 }, + { key: 'carrierbundle.itunes.apple.com', doc_count: 2 }, + { key: 'client-api.itunes.apple.com', doc_count: 2 }, + { key: 'cma.itunes.apple.com', doc_count: 2 }, + { key: 'courses.apple.com', doc_count: 2 }, + ], + }, + ja3: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'a441a33aaee795f498d6b764cc78989a', doc_count: 2 }], + }, + }, + { + key: 'fc2cbc41f6a0e9c0118de4fe40f299f7207b797e', + doc_count: 1, + not_after: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 1586009106000, key_as_string: '2020-04-04T14:05:06.000Z', doc_count: 1 }, + ], + }, + issuer_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'GlobalSign CloudSSL CA - SHA256 - G3', doc_count: 1 }], + }, + common_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'incapsula.com', doc_count: 1 }], + }, + alternative_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 110, + buckets: [ + { key: '*.adlercasino.com', doc_count: 1 }, + { key: '*.allaustraliancasino.com', doc_count: 1 }, + { key: '*.alletf.com', doc_count: 1 }, + { key: '*.appareldesignpartners.com', doc_count: 1 }, + { key: '*.atmosfir.net', doc_count: 1 }, + { key: '*.cityofboston.gov', doc_count: 1 }, + { key: '*.cp.mytoyotaentune.com', doc_count: 1 }, + { key: '*.decathlon.be', doc_count: 1 }, + { key: '*.decathlon.co.uk', doc_count: 1 }, + { key: '*.decathlon.de', doc_count: 1 }, + ], + }, + ja3: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: '6fa3244afc6bb6f9fad207b6b52af26b', doc_count: 1 }], + }, + }, + { + key: 'fb70d78ffa663a3a4374d841b3288d2de9759566', + doc_count: 325, + not_after: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 1635336000000, key_as_string: '2021-10-27T12:00:00.000Z', doc_count: 325 }, + ], + }, + issuer_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'DigiCert Baltimore CA-2 G2', doc_count: 325 }], + }, + common_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: '*.siriusxm.com', doc_count: 325 }], + }, + alternative_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: '*.siriusxm.com', doc_count: 325 }, + { key: 'siriusxm.com', doc_count: 325 }, + ], + }, + ja3: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: '535aca3d99fc247509cd50933cd71d37', doc_count: 284 }, + { key: '6fa3244afc6bb6f9fad207b6b52af26b', doc_count: 39 }, + ], + }, + }, + { + key: 'fb59038dcec33ab3a01a6ae60d0835ad0e04ccf0', + doc_count: 5, + not_after: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 1587643200000, key_as_string: '2020-04-23T12:00:00.000Z', doc_count: 5 }, + ], + }, + issuer_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'Amazon', doc_count: 5 }], + }, + common_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'photos.amazon.eu', doc_count: 5 }], + }, + alternative_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'photos.amazon.co.uk', doc_count: 5 }, + { key: 'photos.amazon.de', doc_count: 5 }, + { key: 'photos.amazon.es', doc_count: 5 }, + { key: 'photos.amazon.eu', doc_count: 5 }, + { key: 'photos.amazon.fr', doc_count: 5 }, + { key: 'photos.amazon.it', doc_count: 5 }, + ], + }, + ja3: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: '6fa3244afc6bb6f9fad207b6b52af26b', doc_count: 5 }], + }, + }, + { + key: 'f9815293c883a6006f0b2d95a4895bdc501fd174', + doc_count: 29, + not_after: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 1613001599000, key_as_string: '2021-02-10T23:59:59.000Z', doc_count: 29 }, + ], + }, + issuer_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'Sectigo RSA Organization Validation Secure Server CA', doc_count: 29 }, + ], + }, + common_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'cdn.hbo.com', doc_count: 29 }], + }, + alternative_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 29, + buckets: [ + { key: '*.api.cdn.hbo.com', doc_count: 29 }, + { key: '*.artist.cdn.hbo.com', doc_count: 29 }, + { key: '*.cdn.hbo.com', doc_count: 29 }, + { key: '*.lv3.cdn.hbo.com', doc_count: 29 }, + { key: 'artist.api.cdn.hbo.com', doc_count: 29 }, + { key: 'artist.api.lv3.cdn.hbo.com', doc_count: 29 }, + { key: 'artist.staging.cdn.hbo.com', doc_count: 29 }, + { key: 'artist.staging.hurley.lv3.cdn.hbo.com', doc_count: 29 }, + { key: 'atv.api.lv3.cdn.hbo.com', doc_count: 29 }, + { key: 'atv.staging.hurley.lv3.cdn.hbo.com', doc_count: 29 }, + ], + }, + ja3: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: '6fa3244afc6bb6f9fad207b6b52af26b', doc_count: 26 }], + }, + }, + { + key: 'f8db6a69797e383dca2529727369595733123386', + doc_count: 5, + not_after: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 1575984774000, key_as_string: '2019-12-10T13:32:54.000Z', doc_count: 5 }, + ], + }, + issuer_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'GTS CA 1O1', doc_count: 5 }], + }, + common_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'www.google.com', doc_count: 5 }], + }, + alternative_names: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'www.google.com', doc_count: 5 }], + }, + ja3: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'a111d93cdf31f993c40a8a9ef13e8d7e', doc_count: 5 }], + }, + }, + ], + }, + count: { value: 364 }, + }, +}; + +export const mockOptions = { + defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + sourceConfiguration: { + fields: { + container: 'docker.container.name', + host: 'beat.hostname', + message: ['message', '@message'], + pod: 'kubernetes.pod.name', + tiebreaker: '_doc', + timestamp: '@timestamp', + }, + }, + timerange: { interval: '12h', to: 1570801871626, from: 1570715471626 }, + pagination: { activePage: 0, cursorStart: 0, fakePossibleCount: 50, querySize: 10 }, + filterQuery: {}, + fields: [ + 'totalCount', + '_id', + 'alternativeNames', + 'commonNames', + 'ja3', + 'issuerNames', + 'notAfter', + 'edges.cursor.value', + 'pageInfo.activePage', + 'pageInfo.fakeTotalCount', + 'pageInfo.showMorePagesIndicator', + 'inspect.dsl', + 'inspect.response', + ], + ip: '', + tlsSortField: { field: TlsFields._id, direction: Direction.desc }, + flowTarget: FlowTargetSourceDest.source, +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/ip_details/query_tls.dsl.ts b/x-pack/legacy/plugins/siem/server/lib/tls/query_tls.dsl.ts similarity index 94% rename from x-pack/legacy/plugins/siem/server/lib/ip_details/query_tls.dsl.ts rename to x-pack/legacy/plugins/siem/server/lib/tls/query_tls.dsl.ts index 562746bb9645c..c3545219a2d3c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/ip_details/query_tls.dsl.ts +++ b/x-pack/legacy/plugins/siem/server/lib/tls/query_tls.dsl.ts @@ -65,12 +65,13 @@ export const buildTlsQuery = ({ }, timerange: { from, to }, }: TlsRequestOptions) => { - const filter = [ + const defaultFilter = [ ...createQueryFilterClauses(filterQuery), { range: { [timestamp]: { gte: from, lte: to } } }, - { term: { [`${flowTarget}.ip`]: ip } }, ]; + const filter = ip ? [...defaultFilter, { term: { [`${flowTarget}.ip`]: ip } }] : defaultFilter; + const dslQuery = { allowNoIndices: true, index: defaultIndex, diff --git a/x-pack/legacy/plugins/siem/server/lib/tls/types.ts b/x-pack/legacy/plugins/siem/server/lib/tls/types.ts new file mode 100644 index 0000000000000..bac5426f72e08 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/tls/types.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 { FrameworkRequest, RequestBasicOptions } from '../framework'; +import { TlsData } from '../../../public/graphql/types'; + +export interface TlsAdapter { + getTls(request: FrameworkRequest, options: RequestBasicOptions): Promise; +} + +export interface TlsBuckets { + key: string; + timestamp?: { + value: number; + value_as_string: string; + }; + + alternative_names: { + buckets: Readonly>; + }; + + common_names: { + buckets: Readonly>; + }; + + ja3: { + buckets: Readonly>; + }; + + issuer_names: { + buckets: Readonly>; + }; + + not_after: { + buckets: Readonly>; + }; +} diff --git a/x-pack/legacy/plugins/siem/server/lib/types.ts b/x-pack/legacy/plugins/siem/server/lib/types.ts index 062ece95e933c..b3e44f7882070 100644 --- a/x-pack/legacy/plugins/siem/server/lib/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/types.ts @@ -21,6 +21,7 @@ import { UncommonProcesses } from './uncommon_processes'; import { Note } from './note/saved_object'; import { PinnedEvent } from './pinned_event/saved_object'; import { Timeline } from './timeline/saved_object'; +import { TLS } from './tls'; export * from './hosts'; @@ -35,6 +36,7 @@ export interface AppDomainLibs { overview: Overview; uncommonProcesses: UncommonProcesses; kpiHosts: KpiHosts; + tls: TLS; } export interface AppBackendLibs extends AppDomainLibs { diff --git a/x-pack/test/api_integration/apis/siem/tls.ts b/x-pack/test/api_integration/apis/siem/tls.ts index aec10cf1f9689..949ed530e9b27 100644 --- a/x-pack/test/api_integration/apis/siem/tls.ts +++ b/x-pack/test/api_integration/apis/siem/tls.ts @@ -29,11 +29,68 @@ const expectedResult = { notAfter: ['2019-05-22T12:00:00.000Z'], }; +const expectedOverviewDestinationResult = { + __typename: 'TlsData', + edges: [ + { + __typename: 'TlsEdges', + cursor: { + __typename: 'CursorType', + value: '61749734b3246f1584029deb4f5276c64da00ada', + }, + node: { + __typename: 'TlsNode', + _id: '61749734b3246f1584029deb4f5276c64da00ada', + alternativeNames: ['api.snapcraft.io'], + commonNames: ['api.snapcraft.io'], + issuerNames: ['DigiCert SHA2 Secure Server CA'], + ja3: ['839868ad711dc55bde0d37a87f14740d'], + notAfter: ['2019-05-22T12:00:00.000Z'], + }, + }, + ], + pageInfo: { + __typename: 'PageInfoPaginated', + activePage: 0, + fakeTotalCount: 1, + showMorePagesIndicator: false, + }, + totalCount: 1, +}; +const expectedOverviewSourceResult = { + __typename: 'TlsData', + edges: [ + { + __typename: 'TlsEdges', + cursor: { + __typename: 'CursorType', + value: '61749734b3246f1584029deb4f5276c64da00ada', + }, + node: { + __typename: 'TlsNode', + _id: '61749734b3246f1584029deb4f5276c64da00ada', + alternativeNames: ['api.snapcraft.io'], + commonNames: ['api.snapcraft.io'], + issuerNames: ['DigiCert SHA2 Secure Server CA'], + ja3: ['839868ad711dc55bde0d37a87f14740d'], + notAfter: ['2019-05-22T12:00:00.000Z'], + }, + }, + ], + pageInfo: { + __typename: 'PageInfoPaginated', + activePage: 0, + fakeTotalCount: 1, + showMorePagesIndicator: false, + }, + totalCount: 1, +}; + export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const client = getService('siemGraphQLClient'); - describe('Tls Test', () => { - describe('With packetbeat', () => { + describe('Tls Test with Packetbeat', () => { + describe('Tls Test', () => { before(() => esArchiver.load('packetbeat/default')); after(() => esArchiver.unload('packetbeat/default')); @@ -101,5 +158,70 @@ export default function({ getService }: FtrProviderContext) { }); }); }); + + describe('Tls Overview Test', () => { + before(() => esArchiver.load('packetbeat/default')); + after(() => esArchiver.unload('packetbeat/default')); + + it('Ensure data is returned for FlowTarget.Source', () => { + return client + .query({ + query: tlsQuery, + variables: { + sourceId: 'default', + timerange: { + interval: '12h', + to: TO, + from: FROM, + }, + ip: '', + flowTarget: FlowTarget.source, + sort: { field: TlsFields._id, direction: Direction.desc }, + pagination: { + activePage: 0, + cursorStart: 0, + fakePossibleCount: 30, + querySize: 10, + }, + defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + inspect: false, + }, + }) + .then(resp => { + const tls = resp.data.source.Tls; + expect(tls).to.eql(expectedOverviewSourceResult); + }); + }); + + it('Ensure data is returned for FlowTarget.Destination', () => { + return client + .query({ + query: tlsQuery, + variables: { + sourceId: 'default', + timerange: { + interval: '12h', + to: TO, + from: FROM, + }, + ip: '', + flowTarget: FlowTarget.destination, + sort: { field: TlsFields._id, direction: Direction.desc }, + pagination: { + activePage: 0, + cursorStart: 0, + fakePossibleCount: 30, + querySize: 10, + }, + defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + inspect: false, + }, + }) + .then(resp => { + const tls = resp.data.source.Tls; + expect(tls).to.eql(expectedOverviewDestinationResult); + }); + }); + }); }); } From 68a2fdc7c3fb741c3dd6839b6e820be244533590 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 16 Oct 2019 08:08:08 +0100 Subject: [PATCH 15/48] [ML] Fixing overview page max anomaly score (#48110) * [ML] Fixing overview page max anomaly score * removing unnecessary copy of maxScore --- .../anomaly_detection_panel/anomaly_detection_panel.tsx | 6 +++--- .../ml/server/models/results_service/results_service.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx b/x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx index 3517c86a6109f..90b5be2c25406 100644 --- a/x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx @@ -98,13 +98,13 @@ export const AnomalyDetectionPanel: FC = () => { return ml.results.getMaxAnomalyScore(group.jobIds, twentyFourHoursAgo, latestTimestamp); }); - const results = await Promise.all(promises.map(p => p.catch(() => undefined))); + const results = await Promise.all(promises); const tempGroups = { ...groupsObject }; // Check results for each group's promise index and update state Object.keys(scores).forEach(groupId => { const resultsIndex = scores[groupId] && scores[groupId].index; - scores[groupId] = resultsIndex !== undefined && results[resultsIndex]; - tempGroups[groupId].max_anomaly_score = resultsIndex !== undefined && results[resultsIndex]; + const { maxScore } = resultsIndex !== undefined && results[resultsIndex]; + tempGroups[groupId].max_anomaly_score = maxScore || undefined; }); setGroups(tempGroups); diff --git a/x-pack/legacy/plugins/ml/server/models/results_service/results_service.js b/x-pack/legacy/plugins/ml/server/models/results_service/results_service.js index 3fd20308b2f9b..9501389af195d 100644 --- a/x-pack/legacy/plugins/ml/server/models/results_service/results_service.js +++ b/x-pack/legacy/plugins/ml/server/models/results_service/results_service.js @@ -264,7 +264,7 @@ export function resultsServiceProvider(callWithRequest) { const resp = await callWithRequest('search', query); const maxScore = _.get(resp, ['aggregations', 'max_score', 'value'], null); - return maxScore; + return { maxScore }; } // Obtains the latest bucket result timestamp by job ID. From a42a767285f3991cff5be12c18f3c8b03200fb73 Mon Sep 17 00:00:00 2001 From: Ahmad Bamieh Date: Wed, 16 Oct 2019 10:24:20 +0300 Subject: [PATCH 16/48] [Telemetry] Move to OSS (#45769) * update paths and licenses * localization collector in oss * node-crypto typings in oss * update telemetry i18n labels * update translation rc files * remove duplicate components in home for telemetry * update tests * finalize collection + move csp collector to oss * self review * use apm instead of beats * xpack collection * fix collection tests * remove space specific settings * mock npSetup and npStart * disable banner in tests * remove commented mock * monitoring np telemetry fixes * replace telemetryOptedIn in oss instead of xpack_main * fix telemetry OptIn test --- .i18nrc.json | 1 + docs/settings/monitoring-settings.asciidoc | 2 +- .../resources/bin/kibana-docker | 2 +- src/legacy/core_plugins/kibana/index.js | 2 + .../telemetry_opt_in_card.tsx | 2 +- .../kibana/public/home/kibana_services.js | 2 +- .../csp_collector.test.ts | 39 +++-- .../lib/csp_usage_collector/csp_collector.ts | 49 ++++++ .../server/lib/csp_usage_collector/index.ts | 20 +++ .../telemetry/common/constants.ts | 66 ++++++++ .../get_xpack_config_with_deprecated.ts | 41 +++++ .../legacy/core_plugins}/telemetry/index.ts | 49 ++++-- .../core_plugins}/telemetry/mappings.json | 0 .../core_plugins/telemetry/package.json | 4 + .../opt_in_details_component.test.tsx.snap | 4 +- .../__snapshots__/telemetry_form.test.js.snap | 6 +- .../telemetry/public/components/index.ts | 24 +++ .../components/opt_in_banner_component.tsx | 31 ++-- .../opt_in_details_component.test.tsx | 34 ++++ .../components}/opt_in_details_component.tsx | 12 +- .../public/components}/opt_in_message.tsx | 36 ++--- .../public/components/telemetry_form.js | 38 +++-- .../public/components/telemetry_form.test.js | 21 ++- .../public/hacks/__tests__/fetch_telemetry.js | 19 ++- .../public/hacks/__tests__/telemetry.js | 28 ++++ .../telemetry/public/hacks/fetch_telemetry.js | 39 +++++ .../telemetry/public/hacks/telemetry.js | 19 ++- .../telemetry/public/hacks/telemetry.test.js | 19 ++- .../telemetry/public/hacks/telemetry_init.ts | 45 ++++++ .../public/hacks/telemetry_opt_in.js | 24 +++ .../hacks/welcome_banner/click_banner.js | 25 ++- .../hacks/welcome_banner/click_banner.test.js | 19 ++- .../welcome_banner/handle_old_settings.js | 19 ++- .../handle_old_settings.test.js | 19 ++- .../public/hacks/welcome_banner/index.js | 20 +++ .../hacks/welcome_banner/inject_banner.js | 25 ++- .../hacks/welcome_banner/render_banner.js | 19 ++- .../welcome_banner/render_banner.test.js | 44 +++++ .../welcome_banner/should_show_banner.js | 33 ++++ .../welcome_banner/should_show_banner.test.js | 21 ++- .../telemetry/public/services/index.ts | 21 +++ .../telemetry/public/services/path.ts | 25 +++ .../public/services/telemetry_opt_in.test.js | 20 ++- .../services/telemetry_opt_in.test.mocks.js | 50 ++++++ .../public/services/telemetry_opt_in.ts} | 28 ++-- .../public/views/management/index.js | 20 +++ .../public/views/management/management.js | 42 +++++ .../telemetry/server/collection_manager.ts | 49 ++++++ .../collectors/encryption/encrypt.test.ts | 19 ++- .../server/collectors/encryption/encrypt.ts | 32 ++++ .../server/collectors/encryption/index.ts | 20 +++ .../collectors/encryption/telemetry_jwks.ts | 19 ++- .../telemetry/server/collectors/index.ts | 23 +++ .../localization/file_integrity.test.mocks.ts | 41 +++++ .../localization/file_integrity.test.ts | 19 ++- .../collectors/localization/file_integrity.ts | 19 ++- .../server/collectors/localization/index.ts | 20 +++ .../telemetry_localization_collector.test.ts | 19 ++- .../telemetry_localization_collector.ts | 19 ++- .../server/collectors/ui_metric/index.ts | 20 +++ .../telemetry_ui_metric_collector.ts | 19 ++- .../usage/ensure_deep_object.test.ts | 19 ++- .../collectors/usage/ensure_deep_object.ts | 19 ++- .../server/collectors/usage/index.ts | 20 +++ .../usage/telemetry_usage_collector.test.ts | 23 ++- .../usage/telemetry_usage_collector.ts | 19 ++- .../telemetry/server/get_telemetry_opt_in.ts | 19 ++- .../core_plugins/telemetry/server/index.ts | 29 ++++ .../core_plugins/telemetry/server/plugin.ts | 30 ++++ .../telemetry/server/routes/index.ts | 27 ++++ .../telemetry/server/routes/opt_in.ts | 19 ++- .../server/routes/telemetry_stats.ts | 28 +++- .../__tests__/get_cluster_info.js | 41 +++++ .../__tests__/get_cluster_stats.js | 49 ++++++ .../__tests__/get_local_stats.js | 52 +++--- .../server/telemetry_collection/constants.ts | 23 +++ .../telemetry_collection/get_cluster_info.js | 30 ++++ .../telemetry_collection/get_cluster_stats.js | 34 ++++ .../server/telemetry_collection/get_kibana.js | 58 +++++++ .../telemetry_collection}/get_local_stats.js | 44 ++--- .../server/telemetry_collection/get_stats.ts | 45 ++++++ .../server/telemetry_collection/index.ts | 24 +++ test/common/config.js | 1 + typings/elastic__node_crypto.d.ts | 20 +++ x-pack/.i18nrc.json | 1 - x-pack/index.js | 2 - .../public/lib/telemetry.js | 8 +- .../plugins/monitoring/common/constants.js | 49 +++++- .../__tests__/bulk_uploader.js | 3 - .../server/kibana_monitoring/bulk_uploader.js | 6 +- .../lib/send_bulk_payload.js | 3 +- .../plugins/monitoring/server/plugin.js | 6 +- .../__tests__/create_query.js | 0 .../fixtures/beats_stats_results.json | 0 .../__tests__/get_all_stats.js | 6 +- .../__tests__/get_beats_stats.js | 0 .../__tests__/get_cluster_uuids.js | 0 .../__tests__/get_es_stats.js | 0 .../__tests__/get_high_level_stats.js | 0 .../__tests__/get_kibana_stats.js | 0 .../telemetry_collection}/create_query.js | 0 .../telemetry_collection}/get_all_stats.js | 28 ++-- .../telemetry_collection}/get_beats_stats.js | 10 +- .../get_cluster_uuids.js | 2 +- .../telemetry_collection}/get_es_stats.js | 2 +- .../get_high_level_stats.js | 12 +- .../telemetry_collection}/get_kibana_stats.js | 2 +- .../get_stats_with_monitoring.ts} | 25 ++- .../server/telemetry_collection}/index.ts | 2 +- .../lib/collectors/csp/csp_collector.ts | 39 ----- .../server/lib/collectors/index.ts | 2 - .../plugins/telemetry/common/constants.ts | 89 ----------- .../get_xpack_config_with_deprecated.ts | 15 -- .../telemetry/public/components/index.ts | 11 -- .../opt_in_details_component.test.tsx | 21 --- .../components/opt_in_details_component.tsx | 150 ------------------ .../public/components/opt_in_message.tsx | 97 ----------- .../public/hacks/__tests__/telemetry.js | 15 -- .../telemetry/public/hacks/fetch_telemetry.js | 26 --- .../telemetry/public/hacks/telemetry_init.ts | 34 ---- .../public/hacks/telemetry_opt_in.js | 11 -- .../public/hacks/welcome_banner/index.js | 7 - .../welcome_banner/render_banner.test.js | 30 ---- .../welcome_banner/should_show_banner.js | 20 --- .../services/telemetry_opt_in.test.mocks.js | 27 ---- .../public/services/telemetry_opt_in.ts | 8 - .../public/views/management/index.js | 7 - .../public/views/management/management.js | 38 ----- .../server/collectors/encryption/encrypt.ts | 19 --- .../telemetry/server/collectors/index.ts | 15 -- .../local/__tests__/get_cluster_info.js | 28 ---- .../local/__tests__/get_cluster_stats.js | 36 ----- .../collectors/local/get_cluster_info.js | 17 -- .../collectors/local/get_cluster_stats.js | 21 --- .../server/collectors/local/get_kibana.js | 49 ------ .../server/collectors/local/index.js | 7 - .../localization/file_integrity.test.mocks.ts | 28 ---- .../server/collectors/localization/index.ts | 7 - .../server/collectors/monitoring/index.js | 7 - .../server/collectors/ui_metric/index.ts | 7 - .../server/collectors/usage/index.ts | 7 - .../legacy/plugins/telemetry/server/index.ts | 14 -- .../legacy/plugins/telemetry/server/plugin.ts | 14 -- .../plugins/telemetry/server/routes/index.ts | 14 -- x-pack/legacy/plugins/xpack_main/index.js | 34 ++-- .../lib/__tests__/replace_injected_vars.js | 18 +-- .../server/lib/replace_injected_vars.js | 2 - .../__tests__/get_xpack.js | 0 .../server/telemetry_collection/constants.ts} | 0 .../get_stats_with_xpack.ts | 42 +++++ .../server/telemetry_collection}/get_xpack.js | 1 - .../server/telemetry_collection}/index.ts | 2 +- .../translations/translations/ja-JP.json | 95 +++-------- .../translations/translations/zh-CN.json | 95 +++-------- x-pack/test/functional/config.js | 2 +- 155 files changed, 2128 insertions(+), 1406 deletions(-) rename {x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/csp => src/legacy/core_plugins/kibana/server/lib/csp_usage_collector}/csp_collector.test.ts (67%) create mode 100644 src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts create mode 100644 src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/index.ts create mode 100644 src/legacy/core_plugins/telemetry/common/constants.ts create mode 100644 src/legacy/core_plugins/telemetry/common/get_xpack_config_with_deprecated.ts rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/index.ts (61%) rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/mappings.json (100%) create mode 100644 src/legacy/core_plugins/telemetry/package.json rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/public/components/__snapshots__/opt_in_details_component.test.tsx.snap (90%) rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap (89%) create mode 100644 src/legacy/core_plugins/telemetry/public/components/index.ts rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/public/components/opt_in_banner_component.tsx (54%) create mode 100644 src/legacy/core_plugins/telemetry/public/components/opt_in_details_component.test.tsx rename src/legacy/core_plugins/{kibana/public/home/components/telemetry_opt_in => telemetry/public/components}/opt_in_details_component.tsx (90%) rename src/legacy/core_plugins/{kibana/public/home/components/telemetry_opt_in => telemetry/public/components}/opt_in_message.tsx (68%) rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/public/components/telemetry_form.js (77%) rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/public/components/telemetry_form.test.js (52%) rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/public/hacks/__tests__/fetch_telemetry.js (52%) create mode 100644 src/legacy/core_plugins/telemetry/public/hacks/__tests__/telemetry.js create mode 100644 src/legacy/core_plugins/telemetry/public/hacks/fetch_telemetry.js rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/public/hacks/telemetry.js (79%) rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/public/hacks/telemetry.test.js (92%) create mode 100644 src/legacy/core_plugins/telemetry/public/hacks/telemetry_init.ts create mode 100644 src/legacy/core_plugins/telemetry/public/hacks/telemetry_opt_in.js rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/public/hacks/welcome_banner/click_banner.js (60%) rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/public/hacks/welcome_banner/click_banner.test.js (80%) rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/public/hacks/welcome_banner/handle_old_settings.js (66%) rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js (89%) create mode 100644 src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/index.js rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/public/hacks/welcome_banner/inject_banner.js (64%) rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/public/hacks/welcome_banner/render_banner.js (51%) create mode 100644 src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.test.js create mode 100644 src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.js rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/public/hacks/welcome_banner/should_show_banner.test.js (73%) create mode 100644 src/legacy/core_plugins/telemetry/public/services/index.ts create mode 100644 src/legacy/core_plugins/telemetry/public/services/path.ts rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/public/services/telemetry_opt_in.test.js (75%) create mode 100644 src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js rename src/legacy/core_plugins/{kibana/public/home/telemetry_opt_in.js => telemetry/public/services/telemetry_opt_in.ts} (75%) create mode 100644 src/legacy/core_plugins/telemetry/public/views/management/index.js create mode 100644 src/legacy/core_plugins/telemetry/public/views/management/management.js create mode 100644 src/legacy/core_plugins/telemetry/server/collection_manager.ts rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/server/collectors/encryption/encrypt.test.ts (53%) create mode 100644 src/legacy/core_plugins/telemetry/server/collectors/encryption/encrypt.ts create mode 100644 src/legacy/core_plugins/telemetry/server/collectors/encryption/index.ts rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/server/collectors/encryption/telemetry_jwks.ts (57%) create mode 100644 src/legacy/core_plugins/telemetry/server/collectors/index.ts create mode 100644 src/legacy/core_plugins/telemetry/server/collectors/localization/file_integrity.test.mocks.ts rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/server/collectors/localization/file_integrity.test.ts (56%) rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/server/collectors/localization/file_integrity.ts (52%) create mode 100644 src/legacy/core_plugins/telemetry/server/collectors/localization/index.ts rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/server/collectors/localization/telemetry_localization_collector.test.ts (51%) rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/server/collectors/localization/telemetry_localization_collector.ts (64%) create mode 100644 src/legacy/core_plugins/telemetry/server/collectors/ui_metric/index.ts rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts (61%) rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/server/collectors/usage/ensure_deep_object.test.ts (72%) rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/server/collectors/usage/ensure_deep_object.ts (63%) create mode 100644 src/legacy/core_plugins/telemetry/server/collectors/usage/index.ts rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts (80%) rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/server/collectors/usage/telemetry_usage_collector.ts (77%) rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/server/get_telemetry_opt_in.ts (52%) create mode 100644 src/legacy/core_plugins/telemetry/server/index.ts create mode 100644 src/legacy/core_plugins/telemetry/server/plugin.ts create mode 100644 src/legacy/core_plugins/telemetry/server/routes/index.ts rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/server/routes/opt_in.ts (52%) rename {x-pack/legacy/plugins => src/legacy/core_plugins}/telemetry/server/routes/telemetry_stats.ts (54%) create mode 100644 src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_cluster_info.js create mode 100644 src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_cluster_stats.js rename {x-pack/legacy/plugins/telemetry/server/collectors/local => src/legacy/core_plugins/telemetry/server/telemetry_collection}/__tests__/get_local_stats.js (86%) create mode 100644 src/legacy/core_plugins/telemetry/server/telemetry_collection/constants.ts create mode 100644 src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_info.js create mode 100644 src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_stats.js create mode 100644 src/legacy/core_plugins/telemetry/server/telemetry_collection/get_kibana.js rename {x-pack/legacy/plugins/telemetry/server/collectors/local => src/legacy/core_plugins/telemetry/server/telemetry_collection}/get_local_stats.js (62%) create mode 100644 src/legacy/core_plugins/telemetry/server/telemetry_collection/get_stats.ts create mode 100644 src/legacy/core_plugins/telemetry/server/telemetry_collection/index.ts create mode 100644 typings/elastic__node_crypto.d.ts rename x-pack/legacy/plugins/{telemetry/server/collectors/monitoring => monitoring/server/telemetry_collection}/__tests__/create_query.js (100%) rename x-pack/legacy/plugins/{telemetry/server/collectors/monitoring => monitoring/server/telemetry_collection}/__tests__/fixtures/beats_stats_results.json (100%) rename x-pack/legacy/plugins/{telemetry/server/collectors/monitoring => monitoring/server/telemetry_collection}/__tests__/get_all_stats.js (95%) rename x-pack/legacy/plugins/{telemetry/server/collectors/monitoring => monitoring/server/telemetry_collection}/__tests__/get_beats_stats.js (100%) rename x-pack/legacy/plugins/{telemetry/server/collectors/monitoring => monitoring/server/telemetry_collection}/__tests__/get_cluster_uuids.js (100%) rename x-pack/legacy/plugins/{telemetry/server/collectors/monitoring => monitoring/server/telemetry_collection}/__tests__/get_es_stats.js (100%) rename x-pack/legacy/plugins/{telemetry/server/collectors/monitoring => monitoring/server/telemetry_collection}/__tests__/get_high_level_stats.js (100%) rename x-pack/legacy/plugins/{telemetry/server/collectors/monitoring => monitoring/server/telemetry_collection}/__tests__/get_kibana_stats.js (100%) rename x-pack/legacy/plugins/{telemetry/server/collectors/monitoring => monitoring/server/telemetry_collection}/create_query.js (100%) rename x-pack/legacy/plugins/{telemetry/server/collectors/monitoring => monitoring/server/telemetry_collection}/get_all_stats.js (84%) rename x-pack/legacy/plugins/{telemetry/server/collectors/monitoring => monitoring/server/telemetry_collection}/get_beats_stats.js (97%) rename x-pack/legacy/plugins/{telemetry/server/collectors/monitoring => monitoring/server/telemetry_collection}/get_cluster_uuids.js (96%) rename x-pack/legacy/plugins/{telemetry/server/collectors/monitoring => monitoring/server/telemetry_collection}/get_es_stats.js (96%) rename x-pack/legacy/plugins/{telemetry/server/collectors/monitoring => monitoring/server/telemetry_collection}/get_high_level_stats.js (97%) rename x-pack/legacy/plugins/{telemetry/server/collectors/monitoring => monitoring/server/telemetry_collection}/get_kibana_stats.js (98%) rename x-pack/legacy/plugins/{telemetry/server/collectors/get_stats.ts => monitoring/server/telemetry_collection/get_stats_with_monitoring.ts} (58%) rename x-pack/legacy/plugins/{telemetry/server/collectors/encryption => monitoring/server/telemetry_collection}/index.ts (77%) delete mode 100644 x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/csp/csp_collector.ts delete mode 100644 x-pack/legacy/plugins/telemetry/common/constants.ts delete mode 100644 x-pack/legacy/plugins/telemetry/common/get_xpack_config_with_deprecated.ts delete mode 100644 x-pack/legacy/plugins/telemetry/public/components/index.ts delete mode 100644 x-pack/legacy/plugins/telemetry/public/components/opt_in_details_component.test.tsx delete mode 100644 x-pack/legacy/plugins/telemetry/public/components/opt_in_details_component.tsx delete mode 100644 x-pack/legacy/plugins/telemetry/public/components/opt_in_message.tsx delete mode 100644 x-pack/legacy/plugins/telemetry/public/hacks/__tests__/telemetry.js delete mode 100644 x-pack/legacy/plugins/telemetry/public/hacks/fetch_telemetry.js delete mode 100644 x-pack/legacy/plugins/telemetry/public/hacks/telemetry_init.ts delete mode 100644 x-pack/legacy/plugins/telemetry/public/hacks/telemetry_opt_in.js delete mode 100644 x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/index.js delete mode 100644 x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/render_banner.test.js delete mode 100644 x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/should_show_banner.js delete mode 100644 x-pack/legacy/plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js delete mode 100644 x-pack/legacy/plugins/telemetry/public/services/telemetry_opt_in.ts delete mode 100644 x-pack/legacy/plugins/telemetry/public/views/management/index.js delete mode 100644 x-pack/legacy/plugins/telemetry/public/views/management/management.js delete mode 100644 x-pack/legacy/plugins/telemetry/server/collectors/encryption/encrypt.ts delete mode 100644 x-pack/legacy/plugins/telemetry/server/collectors/index.ts delete mode 100644 x-pack/legacy/plugins/telemetry/server/collectors/local/__tests__/get_cluster_info.js delete mode 100644 x-pack/legacy/plugins/telemetry/server/collectors/local/__tests__/get_cluster_stats.js delete mode 100644 x-pack/legacy/plugins/telemetry/server/collectors/local/get_cluster_info.js delete mode 100644 x-pack/legacy/plugins/telemetry/server/collectors/local/get_cluster_stats.js delete mode 100644 x-pack/legacy/plugins/telemetry/server/collectors/local/get_kibana.js delete mode 100644 x-pack/legacy/plugins/telemetry/server/collectors/local/index.js delete mode 100644 x-pack/legacy/plugins/telemetry/server/collectors/localization/file_integrity.test.mocks.ts delete mode 100644 x-pack/legacy/plugins/telemetry/server/collectors/localization/index.ts delete mode 100644 x-pack/legacy/plugins/telemetry/server/collectors/monitoring/index.js delete mode 100644 x-pack/legacy/plugins/telemetry/server/collectors/ui_metric/index.ts delete mode 100644 x-pack/legacy/plugins/telemetry/server/collectors/usage/index.ts delete mode 100644 x-pack/legacy/plugins/telemetry/server/index.ts delete mode 100644 x-pack/legacy/plugins/telemetry/server/plugin.ts delete mode 100644 x-pack/legacy/plugins/telemetry/server/routes/index.ts rename x-pack/legacy/plugins/{telemetry/server/collectors/local => xpack_main/server/telemetry_collection}/__tests__/get_xpack.js (100%) rename x-pack/legacy/plugins/{telemetry/server/collectors/local/constants.js => xpack_main/server/telemetry_collection/constants.ts} (100%) create mode 100644 x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_stats_with_xpack.ts rename x-pack/legacy/plugins/{telemetry/server/collectors/local => xpack_main/server/telemetry_collection}/get_xpack.js (99%) rename x-pack/legacy/plugins/{oss_telemetry/server/lib/collectors/csp => xpack_main/server/telemetry_collection}/index.ts (80%) diff --git a/.i18nrc.json b/.i18nrc.json index 4a43d0a87a036..b8bd8729139c0 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -28,6 +28,7 @@ "kbnESQuery": "packages/kbn-es-query", "inspector": "src/plugins/inspector", "kibana-react": "src/plugins/kibana_react", + "telemetry": "src/legacy/core_plugins/telemetry", "esUi": "src/plugins/es_ui_shared", "uiActions": "src/plugins/ui_actions" }, diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index 97fb891c95bdb..f08ae8e942f4a 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -54,7 +54,7 @@ Specifies the password that {kib} uses for authentication when it retrieves data from the monitoring cluster. If not set, {kib} uses the value of the `elasticsearch.password` setting. -`xpack.telemetry.enabled`:: +`telemetry.enabled`:: Set to `true` (default) to send cluster statistics to Elastic. Reporting your cluster statistics helps us improve your user experience. Your data is never shared with anyone. Set to `false` to disable statistics reporting from any diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 60f94f7f38056..0926ef365c894 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -180,7 +180,7 @@ kibana_vars=( xpack.security.encryptionKey xpack.security.secureCookies xpack.security.sessionTimeout - xpack.telemetry.enabled + telemetry.enabled ) longopts='' diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index bb041924215dd..8b12f71660844 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -38,6 +38,7 @@ import * as systemApi from './server/lib/system_api'; import mappings from './mappings.json'; import { getUiSettingDefaults } from './ui_setting_defaults'; import { makeKQLUsageCollector } from './server/lib/kql_usage_collector'; +import { registerCspCollector } from './server/lib/csp_usage_collector'; import { injectVars } from './inject_vars'; import { i18n } from '@kbn/i18n'; @@ -344,6 +345,7 @@ export default function (kibana) { registerFieldFormats(server); registerTutorials(server); makeKQLUsageCollector(server); + registerCspCollector(server); server.expose('systemApi', systemApi); server.injectUiAppVars('kibana', () => injectVars(server)); }, diff --git a/src/legacy/core_plugins/kibana/public/home/components/telemetry_opt_in/telemetry_opt_in_card.tsx b/src/legacy/core_plugins/kibana/public/home/components/telemetry_opt_in/telemetry_opt_in_card.tsx index 5fa842291b028..e0f37277dd226 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/telemetry_opt_in/telemetry_opt_in_card.tsx +++ b/src/legacy/core_plugins/kibana/public/home/components/telemetry_opt_in/telemetry_opt_in_card.tsx @@ -24,7 +24,7 @@ import { EuiCard, EuiButton, } from '@elastic/eui'; -import { OptInMessage } from './opt_in_message'; +import { OptInMessage } from '../../../../../telemetry/public/components/opt_in_message'; export interface Props { urlBasePath: string; diff --git a/src/legacy/core_plugins/kibana/public/home/kibana_services.js b/src/legacy/core_plugins/kibana/public/home/kibana_services.js index c5480e16491a9..792c5e09435a4 100644 --- a/src/legacy/core_plugins/kibana/public/home/kibana_services.js +++ b/src/legacy/core_plugins/kibana/public/home/kibana_services.js @@ -20,7 +20,7 @@ import { uiModules } from 'ui/modules'; import { npStart } from 'ui/new_platform'; import { createUiStatsReporter, METRIC_TYPE } from '../../../ui_metric/public'; -import { TelemetryOptInProvider } from './telemetry_opt_in'; +import { TelemetryOptInProvider } from '../../../telemetry/public/services'; export let indexPatternService; export let shouldShowTelemetryOptIn; diff --git a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/csp/csp_collector.test.ts b/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.test.ts similarity index 67% rename from x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/csp/csp_collector.test.ts rename to src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.test.ts index caea9fd49fed6..36e7dc81d4708 100644 --- a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/csp/csp_collector.test.ts +++ b/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.test.ts @@ -1,18 +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. + * 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 sinon from 'sinon'; -import { DEFAULT_CSP_RULES } from '../../../../../../../../src/legacy/server/csp'; -import { - getMockCallWithInternal, - getMockKbnServer, - getMockTaskFetch, -} from '../../../../test_utils'; +import { Server } from 'hapi'; +import { DEFAULT_CSP_RULES } from '../../../../../server/csp'; import { createCspCollector } from './csp_collector'; +interface MockConfig { + get: (x: string) => any; +} + +const getMockKbnServer = (mockConfig: MockConfig) => ({ + config: () => mockConfig, +}); + test('fetches whether strict mode is enabled', async () => { const { collector, mockConfig } = setupCollector(); @@ -72,7 +89,7 @@ function setupCollector() { mockConfig.get.withArgs('csp.strict').returns(true); mockConfig.get.withArgs('csp.warnLegacyBrowsers').returns(true); - const mockKbnServer = getMockKbnServer(getMockCallWithInternal(), getMockTaskFetch(), mockConfig); + const mockKbnServer = getMockKbnServer(mockConfig); - return { mockConfig, collector: createCspCollector(mockKbnServer) }; + return { mockConfig, collector: createCspCollector(mockKbnServer as Server) }; } diff --git a/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts b/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts new file mode 100644 index 0000000000000..3ff39c1a4eb8c --- /dev/null +++ b/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/csp_collector.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Server } from 'hapi'; +import { createCSPRuleString, DEFAULT_CSP_RULES } from '../../../../../server/csp'; + +export function createCspCollector(server: Server) { + return { + type: 'csp', + isReady: () => true, + async fetch() { + const config = server.config(); + + // It's important that we do not send the value of csp.rules here as it + // can be customized with values that can be identifiable to given + // installs, such as URLs + const defaultRulesString = createCSPRuleString([...DEFAULT_CSP_RULES]); + const actualRulesString = createCSPRuleString(config.get('csp.rules')); + + return { + strict: config.get('csp.strict'), + warnLegacyBrowsers: config.get('csp.warnLegacyBrowsers'), + rulesChangedFromDefault: defaultRulesString !== actualRulesString, + }; + }, + }; +} + +export function registerCspCollector(server: Server): void { + const { collectorSet } = server.usage; + const collector = collectorSet.makeUsageCollector(createCspCollector(server)); + collectorSet.register(collector); +} diff --git a/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/index.ts b/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/index.ts new file mode 100644 index 0000000000000..517b21d3d73ff --- /dev/null +++ b/src/legacy/core_plugins/kibana/server/lib/csp_usage_collector/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { registerCspCollector } from './csp_collector'; diff --git a/src/legacy/core_plugins/telemetry/common/constants.ts b/src/legacy/core_plugins/telemetry/common/constants.ts new file mode 100644 index 0000000000000..ab1397b2cc232 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/common/constants.ts @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; + +/* + * config options opt into telemetry + * @type {string} + */ +export const CONFIG_TELEMETRY = 'telemetry:optIn'; +/* + * config description for opting into telemetry + * @type {string} + */ +export const getConfigTelemetryDesc = () => { + return i18n.translate('telemetry.telemetryConfigDescription', { + defaultMessage: + 'Help us improve the Elastic Stack by providing usage statistics for basic features. We will not share this data outside of Elastic.', + }); +}; + +/** + * The amount of time, in milliseconds, to wait between reports when enabled. + * + * Currently 24 hours. + * @type {Number} + */ +export const REPORT_INTERVAL_MS = 86400000; + +/* + * Key for the localStorage service + */ +export const LOCALSTORAGE_KEY = 'telemetry.data'; + +/** + * Link to the Elastic Telemetry privacy statement. + */ +export const PRIVACY_STATEMENT_URL = `https://www.elastic.co/legal/telemetry-privacy-statement`; + +/** + * The type name used within the Monitoring index to publish localization stats. + * @type {string} + */ +export const KIBANA_LOCALIZATION_STATS_TYPE = 'localization'; + +/** + * UI metric usage type + * @type {string} + */ +export const UI_METRIC_USAGE_TYPE = 'ui_metric'; diff --git a/src/legacy/core_plugins/telemetry/common/get_xpack_config_with_deprecated.ts b/src/legacy/core_plugins/telemetry/common/get_xpack_config_with_deprecated.ts new file mode 100644 index 0000000000000..3f7a8d3410993 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/common/get_xpack_config_with_deprecated.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { KibanaConfig } from 'src/legacy/server/kbn_server'; + +export function getXpackConfigWithDeprecated(config: KibanaConfig, configPath: string) { + try { + const deprecatedXpackmainConfig = config.get(`xpack.xpack_main.${configPath}`); + if (typeof deprecatedXpackmainConfig !== 'undefined') { + return deprecatedXpackmainConfig; + } + } catch (err) { + // swallow error + } + try { + const deprecatedXpackConfig = config.get(`xpack.${configPath}`); + if (typeof deprecatedXpackConfig !== 'undefined') { + return deprecatedXpackConfig; + } + } catch (err) { + // swallow error + } + + return config.get(configPath); +} diff --git a/x-pack/legacy/plugins/telemetry/index.ts b/src/legacy/core_plugins/telemetry/index.ts similarity index 61% rename from x-pack/legacy/plugins/telemetry/index.ts rename to src/legacy/core_plugins/telemetry/index.ts index 71e80cb2bd02f..3271373449eb3 100644 --- a/x-pack/legacy/plugins/telemetry/index.ts +++ b/src/legacy/core_plugins/telemetry/index.ts @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ import { resolve } from 'path'; @@ -9,10 +22,11 @@ import JoiNamespace from 'joi'; import { Server } from 'hapi'; import { CoreSetup, PluginInitializerContext } from 'src/core/server'; import { i18n } from '@kbn/i18n'; +// @ts-ignore import mappings from './mappings.json'; -import { CONFIG_TELEMETRY, getConfigTelemetryDesc, REPORT_INTERVAL_MS } from './common/constants'; +import { CONFIG_TELEMETRY, getConfigTelemetryDesc } from './common/constants'; import { getXpackConfigWithDeprecated } from './common/get_xpack_config_with_deprecated'; -import { telemetryPlugin } from './server'; +import { telemetryPlugin, getTelemetryOptIn } from './server'; import { createLocalizationUsageCollector, @@ -22,10 +36,10 @@ import { const ENDPOINT_VERSION = 'v2'; -export const telemetry = (kibana: any) => { +const telemetry = (kibana: any) => { return new kibana.Plugin({ id: 'telemetry', - configPrefix: 'xpack.telemetry', + configPrefix: 'telemetry', publicDir: resolve(__dirname, 'public'), require: ['elasticsearch'], config(Joi: typeof JoiNamespace) { @@ -49,7 +63,7 @@ export const telemetry = (kibana: any) => { managementSections: ['plugins/telemetry/views/management'], uiSettingDefaults: { [CONFIG_TELEMETRY]: { - name: i18n.translate('xpack.telemetry.telemetryConfigTitle', { + name: i18n.translate('telemetry.telemetryConfigTitle', { defaultMessage: 'Telemetry opt-in', }), description: getConfigTelemetryDesc(), @@ -62,11 +76,20 @@ export const telemetry = (kibana: any) => { isNamespaceAgnostic: true, }, }, + async replaceInjectedVars(originalInjectedVars: any, request: any) { + const telemetryOptedIn = await getTelemetryOptIn(request); + + return { + ...originalInjectedVars, + telemetryOptedIn, + }; + }, injectDefaultVars(server: Server) { const config = server.config(); return { + telemetryEnabled: getXpackConfigWithDeprecated(config, 'telemetry.enabled'), telemetryUrl: getXpackConfigWithDeprecated(config, 'telemetry.url'), - telemetryBanner: config.get('xpack.telemetry.banner'), + telemetryBanner: getXpackConfigWithDeprecated(config, 'telemetry.banner'), telemetryOptedIn: null, }; }, @@ -75,8 +98,10 @@ export const telemetry = (kibana: any) => { }, init(server: Server) { const initializerContext = {} as PluginInitializerContext; + const coreSetup = ({ http: { server }, + log: server.log, } as any) as CoreSetup; telemetryPlugin(initializerContext).setup(coreSetup); @@ -85,9 +110,9 @@ export const telemetry = (kibana: any) => { server.usage.collectorSet.register(createLocalizationUsageCollector(server)); server.usage.collectorSet.register(createTelemetryUsageCollector(server)); server.usage.collectorSet.register(createUiMetricUsageCollector(server)); - - // expose - server.expose('telemetryCollectionInterval', REPORT_INTERVAL_MS); }, }); }; + +// eslint-disable-next-line import/no-default-export +export default telemetry; diff --git a/x-pack/legacy/plugins/telemetry/mappings.json b/src/legacy/core_plugins/telemetry/mappings.json similarity index 100% rename from x-pack/legacy/plugins/telemetry/mappings.json rename to src/legacy/core_plugins/telemetry/mappings.json diff --git a/src/legacy/core_plugins/telemetry/package.json b/src/legacy/core_plugins/telemetry/package.json new file mode 100644 index 0000000000000..979e68cce742f --- /dev/null +++ b/src/legacy/core_plugins/telemetry/package.json @@ -0,0 +1,4 @@ +{ + "name": "telemetry", + "version": "kibana" +} diff --git a/x-pack/legacy/plugins/telemetry/public/components/__snapshots__/opt_in_details_component.test.tsx.snap b/src/legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_details_component.test.tsx.snap similarity index 90% rename from x-pack/legacy/plugins/telemetry/public/components/__snapshots__/opt_in_details_component.test.tsx.snap rename to src/legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_details_component.test.tsx.snap index 36f8c5ca5905e..2601f691cd184 100644 --- a/x-pack/legacy/plugins/telemetry/public/components/__snapshots__/opt_in_details_component.test.tsx.snap +++ b/src/legacy/core_plugins/telemetry/public/components/__snapshots__/opt_in_details_component.test.tsx.snap @@ -15,7 +15,7 @@ exports[`OptInDetailsComponent renders as expected 1`] = `

@@ -26,7 +26,7 @@ exports[`OptInDetailsComponent renders as expected 1`] = ` diff --git a/x-pack/legacy/plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap b/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap similarity index 89% rename from x-pack/legacy/plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap rename to src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap index 68a1b92666ea9..c1ad6276aee25 100644 --- a/x-pack/legacy/plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap +++ b/src/legacy/core_plugins/telemetry/public/components/__snapshots__/telemetry_form.test.js.snap @@ -16,7 +16,7 @@ exports[`TelemetryForm renders as expected 1`] = `

@@ -43,7 +43,7 @@ exports[`TelemetryForm renders as expected 1`] = ` > @@ -55,7 +55,7 @@ exports[`TelemetryForm renders as expected 1`] = ` > diff --git a/src/legacy/core_plugins/telemetry/public/components/index.ts b/src/legacy/core_plugins/telemetry/public/components/index.ts new file mode 100644 index 0000000000000..1fc55eadd1e10 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/public/components/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// @ts-ignore +export { TelemetryForm } from './telemetry_form'; +export { OptInExampleFlyout } from './opt_in_details_component'; +export { OptInBanner } from './opt_in_banner_component'; +export { OptInMessage } from './opt_in_message'; diff --git a/x-pack/legacy/plugins/telemetry/public/components/opt_in_banner_component.tsx b/src/legacy/core_plugins/telemetry/public/components/opt_in_banner_component.tsx similarity index 54% rename from x-pack/legacy/plugins/telemetry/public/components/opt_in_banner_component.tsx rename to src/legacy/core_plugins/telemetry/public/components/opt_in_banner_component.tsx index 19754504c081e..0029eaf666b2d 100644 --- a/x-pack/legacy/plugins/telemetry/public/components/opt_in_banner_component.tsx +++ b/src/legacy/core_plugins/telemetry/public/components/opt_in_banner_component.tsx @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 * as React from 'react'; @@ -21,7 +34,7 @@ export class OptInBanner extends React.PureComponent { render() { const title = ( ); @@ -32,18 +45,12 @@ export class OptInBanner extends React.PureComponent { this.props.optInClick(true)}> - + this.props.optInClick(false)}> - + diff --git a/src/legacy/core_plugins/telemetry/public/components/opt_in_details_component.test.tsx b/src/legacy/core_plugins/telemetry/public/components/opt_in_details_component.test.tsx new file mode 100644 index 0000000000000..3676430b55a41 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/public/components/opt_in_details_component.test.tsx @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { OptInExampleFlyout } from './opt_in_details_component'; + +describe('OptInDetailsComponent', () => { + it('renders as expected', () => { + expect( + shallowWithIntl( + ({ data: [] }))} + onClose={jest.fn()} + /> + ) + ).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/home/components/telemetry_opt_in/opt_in_details_component.tsx b/src/legacy/core_plugins/telemetry/public/components/opt_in_details_component.tsx similarity index 90% rename from src/legacy/core_plugins/kibana/public/home/components/telemetry_opt_in/opt_in_details_component.tsx rename to src/legacy/core_plugins/telemetry/public/components/opt_in_details_component.tsx index d90f54b2bcb54..12ab780e75990 100644 --- a/src/legacy/core_plugins/kibana/public/home/components/telemetry_opt_in/opt_in_details_component.tsx +++ b/src/legacy/core_plugins/telemetry/public/components/opt_in_details_component.tsx @@ -91,7 +91,7 @@ export class OptInExampleFlyout extends React.PureComponent { } @@ -99,7 +99,7 @@ export class OptInExampleFlyout extends React.PureComponent { iconType="cross" > @@ -111,7 +111,7 @@ export class OptInExampleFlyout extends React.PureComponent { } @@ -119,7 +119,7 @@ export class OptInExampleFlyout extends React.PureComponent { iconType="cross" > {

@@ -147,7 +147,7 @@ export class OptInExampleFlyout extends React.PureComponent { { }; render() { - const { fetchTelemetry } = this.props; const { showDetails, showExample } = this.state; const getDetails = () => ( ), telemetryPrivacyStatementLink: ( - + @@ -78,14 +78,14 @@ export class OptInMessage extends React.PureComponent { const getFlyoutDetails = () => ( this.setState({ showExample: false })} - fetchTelemetry={fetchTelemetry} + fetchTelemetry={this.props.fetchTelemetry} /> ); const getReadMore = () => ( this.setState({ showDetails: true })}> @@ -93,13 +93,13 @@ export class OptInMessage extends React.PureComponent { return ( - {' '} - {!showDetails && getReadMore()} - {showDetails && getDetails()} - {showDetails && showExample && getFlyoutDetails()} + {getConfigTelemetryDesc()} {!showDetails && getReadMore()} + {showDetails && ( + + {getDetails()} + {showExample && getFlyoutDetails()} + + )} ); } diff --git a/x-pack/legacy/plugins/telemetry/public/components/telemetry_form.js b/src/legacy/core_plugins/telemetry/public/components/telemetry_form.js similarity index 77% rename from x-pack/legacy/plugins/telemetry/public/components/telemetry_form.js rename to src/legacy/core_plugins/telemetry/public/components/telemetry_form.js index dc3739bb7ae8e..c2dcd48ee57da 100644 --- a/x-pack/legacy/plugins/telemetry/public/components/telemetry_form.js +++ b/src/legacy/core_plugins/telemetry/public/components/telemetry_form.js @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ import React, { Component, Fragment } from 'react'; @@ -28,8 +41,7 @@ export class TelemetryForm extends Component { telemetryOptInProvider: PropTypes.object.isRequired, query: PropTypes.object, onQueryMatchChange: PropTypes.func.isRequired, - spacesEnabled: PropTypes.bool.isRequired, - activeSpace: PropTypes.object, + showAppliesSettingMessage: PropTypes.bool.isRequired, enableSaving: PropTypes.bool.isRequired, }; @@ -85,7 +97,7 @@ export class TelemetryForm extends Component {

@@ -93,7 +105,7 @@ export class TelemetryForm extends Component {
- {this.maybeGetSpacesWarning()} + {this.maybeGetAppliesSettingMessage()} { - if (!this.props.spacesEnabled) { + maybeGetAppliesSettingMessage = () => { + if (!this.props.showAppliesSettingMessage) { return null; } return ( @@ -123,13 +135,13 @@ export class TelemetryForm extends Component { title={

@@ -148,7 +160,7 @@ export class TelemetryForm extends Component {

@@ -156,7 +168,7 @@ export class TelemetryForm extends Component {

diff --git a/x-pack/legacy/plugins/telemetry/public/components/telemetry_form.test.js b/src/legacy/core_plugins/telemetry/public/components/telemetry_form.test.js similarity index 52% rename from x-pack/legacy/plugins/telemetry/public/components/telemetry_form.test.js rename to src/legacy/core_plugins/telemetry/public/components/telemetry_form.test.js index 4df9cc0da1695..4d2c1dec27176 100644 --- a/x-pack/legacy/plugins/telemetry/public/components/telemetry_form.test.js +++ b/src/legacy/core_plugins/telemetry/public/components/telemetry_form.test.js @@ -1,14 +1,27 @@ /* - * 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. + * 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 '../services/telemetry_opt_in.test.mocks'; import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { TelemetryForm } from './telemetry_form'; -import { TelemetryOptInProvider } from '../services/telemetry_opt_in'; +import { TelemetryOptInProvider } from '../services'; const buildTelemetryOptInProvider = () => { const mockHttp = { diff --git a/x-pack/legacy/plugins/telemetry/public/hacks/__tests__/fetch_telemetry.js b/src/legacy/core_plugins/telemetry/public/hacks/__tests__/fetch_telemetry.js similarity index 52% rename from x-pack/legacy/plugins/telemetry/public/hacks/__tests__/fetch_telemetry.js rename to src/legacy/core_plugins/telemetry/public/hacks/__tests__/fetch_telemetry.js index 26269ef97fa64..8fb8b6e44f0e3 100644 --- a/x-pack/legacy/plugins/telemetry/public/hacks/__tests__/fetch_telemetry.js +++ b/src/legacy/core_plugins/telemetry/public/hacks/__tests__/fetch_telemetry.js @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ import expect from '@kbn/expect'; diff --git a/src/legacy/core_plugins/telemetry/public/hacks/__tests__/telemetry.js b/src/legacy/core_plugins/telemetry/public/hacks/__tests__/telemetry.js new file mode 100644 index 0000000000000..374927486867e --- /dev/null +++ b/src/legacy/core_plugins/telemetry/public/hacks/__tests__/telemetry.js @@ -0,0 +1,28 @@ +/* + * 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 { uiModules } from 'ui/modules'; + +// This overrides settings for other UI tests +uiModules.get('kibana') + // disable stat reporting while running tests, + // MockInjector used in these tests is not impacted + .constant('telemetryEnabled', false) + .constant('telemetryOptedIn', null) + .constant('telemetryUrl', 'not.a.valid.url.0'); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/fetch_telemetry.js b/src/legacy/core_plugins/telemetry/public/hacks/fetch_telemetry.js new file mode 100644 index 0000000000000..43ec0c2ce571f --- /dev/null +++ b/src/legacy/core_plugins/telemetry/public/hacks/fetch_telemetry.js @@ -0,0 +1,39 @@ +/* + * 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 uiChrome from 'ui/chrome'; +import moment from 'moment'; + +/** + * Fetch Telemetry data by calling the Kibana API. + * + * @param {Object} $http The HTTP handler + * @param {String} basePath The base URI + * @param {Function} _moment moment.js, but injectable for tests + * @return {Promise} An array of cluster Telemetry objects. + */ +export function fetchTelemetry($http, { basePath = uiChrome.getBasePath(), _moment = moment, unencrypted = false } = { }) { + return $http.post(`${basePath}/api/telemetry/v2/clusters/_stats`, { + unencrypted, + timeRange: { + min: _moment().subtract(20, 'minutes').toISOString(), + max: _moment().toISOString() + } + }); +} diff --git a/x-pack/legacy/plugins/telemetry/public/hacks/telemetry.js b/src/legacy/core_plugins/telemetry/public/hacks/telemetry.js similarity index 79% rename from x-pack/legacy/plugins/telemetry/public/hacks/telemetry.js rename to src/legacy/core_plugins/telemetry/public/hacks/telemetry.js index 168bb6d7eec97..4febd8cd9e9a8 100644 --- a/x-pack/legacy/plugins/telemetry/public/hacks/telemetry.js +++ b/src/legacy/core_plugins/telemetry/public/hacks/telemetry.js @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 { diff --git a/x-pack/legacy/plugins/telemetry/public/hacks/telemetry.test.js b/src/legacy/core_plugins/telemetry/public/hacks/telemetry.test.js similarity index 92% rename from x-pack/legacy/plugins/telemetry/public/hacks/telemetry.test.js rename to src/legacy/core_plugins/telemetry/public/hacks/telemetry.test.js index d08d89d738015..ffb72701f835d 100644 --- a/x-pack/legacy/plugins/telemetry/public/hacks/telemetry.test.js +++ b/src/legacy/core_plugins/telemetry/public/hacks/telemetry.test.js @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 { Telemetry } from './telemetry'; diff --git a/src/legacy/core_plugins/telemetry/public/hacks/telemetry_init.ts b/src/legacy/core_plugins/telemetry/public/hacks/telemetry_init.ts new file mode 100644 index 0000000000000..364871380a529 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/public/hacks/telemetry_init.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { npStart } from 'ui/new_platform'; +// @ts-ignore +import { uiModules } from 'ui/modules'; +import { isUnauthenticated } from '../services'; +// @ts-ignore +import { Telemetry } from './telemetry'; +// @ts-ignore +import { fetchTelemetry } from './fetch_telemetry'; + +function telemetryInit($injector: any) { + const $http = $injector.get('$http'); + + const telemetryEnabled = npStart.core.injectedMetadata.getInjectedVar('telemetryEnabled'); + + if (telemetryEnabled) { + // no telemetry for non-logged in users + if (isUnauthenticated()) { + return; + } + + const sender = new Telemetry($injector, () => fetchTelemetry($http)); + sender.start(); + } +} + +uiModules.get('telemetry/hacks').run(telemetryInit); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/telemetry_opt_in.js b/src/legacy/core_plugins/telemetry/public/hacks/telemetry_opt_in.js new file mode 100644 index 0000000000000..4e53c7ecd7030 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/public/hacks/telemetry_opt_in.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { uiModules } from 'ui/modules'; + +import { injectBanner } from './welcome_banner'; + +uiModules.get('telemetry/hacks').run(injectBanner); diff --git a/x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/click_banner.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.js similarity index 60% rename from x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/click_banner.js rename to src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.js index f337a8025e01d..a20c99b7eee55 100644 --- a/x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/click_banner.js +++ b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.js @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ import React from 'react'; @@ -41,7 +54,7 @@ export async function clickBanner( _toastNotifications.addDanger({ title: ( ), @@ -49,13 +62,13 @@ export async function clickBanner(

diff --git a/x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/click_banner.test.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.test.js similarity index 80% rename from x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/click_banner.test.js rename to src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.test.js index 751a8f5498ee5..9b17d51d6ea61 100644 --- a/x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/click_banner.test.js +++ b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/click_banner.test.js @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 { mockInjectedMetadata } from '../../services/telemetry_opt_in.test.mocks'; diff --git a/x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.js similarity index 66% rename from x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.js rename to src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.js index 4188676bad1e0..31091e1952053 100644 --- a/x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.js +++ b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.js @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 { CONFIG_TELEMETRY } from '../../../common/constants'; diff --git a/x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js similarity index 89% rename from x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js rename to src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js index 40e3bb042fa88..fd21a5122b594 100644 --- a/x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js +++ b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/handle_old_settings.test.js @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 { mockInjectedMetadata } from '../../services/telemetry_opt_in.test.mocks'; diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/index.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/index.js new file mode 100644 index 0000000000000..ffb0e88c60a0d --- /dev/null +++ b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/index.js @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { injectBanner } from './inject_banner'; diff --git a/x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/inject_banner.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/inject_banner.js similarity index 64% rename from x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/inject_banner.js rename to src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/inject_banner.js index 384086dfed3c7..13417a137e6c3 100644 --- a/x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/inject_banner.js +++ b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/inject_banner.js @@ -1,15 +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. + * 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 chrome from 'ui/chrome'; -import { Path } from 'plugins/xpack_main/services/path'; + import { fetchTelemetry } from '../fetch_telemetry'; import { renderBanner } from './render_banner'; import { shouldShowBanner } from './should_show_banner'; -import { TelemetryOptInProvider } from '../../services/telemetry_opt_in'; +import { TelemetryOptInProvider, isUnauthenticated } from '../../services'; import { npStart } from 'ui/new_platform'; /** @@ -26,7 +39,7 @@ async function asyncInjectBanner($injector) { const config = $injector.get('config'); // and no banner for non-logged in users - if (Path.isUnauthenticated()) { + if (isUnauthenticated()) { return; } diff --git a/x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/render_banner.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.js similarity index 51% rename from x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/render_banner.js rename to src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.js index 9143d13069316..835d1a8cba62f 100644 --- a/x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/render_banner.js +++ b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.js @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ import React from 'react'; diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.test.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.test.js new file mode 100644 index 0000000000000..9db5103cc9b2f --- /dev/null +++ b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/render_banner.test.js @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import '../../services/telemetry_opt_in.test.mocks'; +import { renderBanner } from './render_banner'; + +describe('render_banner', () => { + + it('adds a banner to banners with priority of 10000', () => { + const bannerID = 'brucer-banner'; + + const telemetryOptInProvider = { setBannerId: jest.fn() }; + const banners = { add: jest.fn().mockReturnValue(bannerID) }; + const fetchTelemetry = jest.fn(); + + renderBanner(telemetryOptInProvider, fetchTelemetry, { _banners: banners }); + + expect(banners.add).toBeCalledTimes(1); + expect(fetchTelemetry).toBeCalledTimes(0); + expect(telemetryOptInProvider.setBannerId).toBeCalledWith(bannerID); + + const bannerConfig = banners.add.mock.calls[0][0]; + + expect(bannerConfig.component).not.toBe(undefined); + expect(bannerConfig.priority).toBe(10000); + }); + +}); diff --git a/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.js new file mode 100644 index 0000000000000..47c6e58422e4c --- /dev/null +++ b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.js @@ -0,0 +1,33 @@ +/* + * 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 { handleOldSettings } from './handle_old_settings'; + +/** + * Determine if the banner should be displayed. + * + * This method can have side-effects related to deprecated config settings. + * + * @param {Object} config The advanced settings config object. + * @param {Object} _handleOldSettings handleOldSettings function, but overridable for tests. + * @return {Boolean} {@code true} if the banner should be displayed. {@code false} otherwise. + */ +export async function shouldShowBanner(telemetryOptInProvider, config, { _handleOldSettings = handleOldSettings } = {}) { + return telemetryOptInProvider.getOptIn() === null && await _handleOldSettings(config, telemetryOptInProvider); +} diff --git a/x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js similarity index 73% rename from x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js rename to src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js index 1bfe7e954738e..19e7ccbe61866 100644 --- a/x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js +++ b/src/legacy/core_plugins/telemetry/public/hacks/welcome_banner/should_show_banner.test.js @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 { mockInjectedMetadata } from '../../services/telemetry_opt_in.test.mocks'; @@ -10,7 +23,7 @@ import sinon from 'sinon'; import { CONFIG_TELEMETRY } from '../../../common/constants'; import { shouldShowBanner } from './should_show_banner'; -import { TelemetryOptInProvider } from '../../services/telemetry_opt_in'; +import { TelemetryOptInProvider } from '../../services'; const getMockInjector = () => { const get = sinon.stub(); diff --git a/src/legacy/core_plugins/telemetry/public/services/index.ts b/src/legacy/core_plugins/telemetry/public/services/index.ts new file mode 100644 index 0000000000000..8b02f8ce4c5b0 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/public/services/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 { TelemetryOptInProvider } from './telemetry_opt_in'; +export { isUnauthenticated } from './path'; diff --git a/src/legacy/core_plugins/telemetry/public/services/path.ts b/src/legacy/core_plugins/telemetry/public/services/path.ts new file mode 100644 index 0000000000000..4af545e982eaa --- /dev/null +++ b/src/legacy/core_plugins/telemetry/public/services/path.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import chrome from 'ui/chrome'; + +export function isUnauthenticated() { + const path = (chrome as any).removeBasePath(window.location.pathname); + return path === '/login' || path === '/logout' || path === '/logged_out' || path === '/status'; +} diff --git a/x-pack/legacy/plugins/telemetry/public/services/telemetry_opt_in.test.js b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.js similarity index 75% rename from x-pack/legacy/plugins/telemetry/public/services/telemetry_opt_in.test.js rename to src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.js index 5b93f84eabf4a..0034fa4438238 100644 --- a/x-pack/legacy/plugins/telemetry/public/services/telemetry_opt_in.test.js +++ b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.js @@ -1,8 +1,22 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 { mockInjectedMetadata } from './telemetry_opt_in.test.mocks'; import { TelemetryOptInProvider } from './telemetry_opt_in'; diff --git a/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js new file mode 100644 index 0000000000000..f98f5e16e00c3 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js @@ -0,0 +1,50 @@ +/* + * 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 { + injectedMetadataServiceMock, + notificationServiceMock, + overlayServiceMock, +} from '../../../../../core/public/mocks'; +const injectedMetadataMock = injectedMetadataServiceMock.createStartContract(); + +export function mockInjectedMetadata({ telemetryOptedIn }) { + const mockGetInjectedVar = jest.fn().mockImplementation((key) => { + switch (key) { + case 'telemetryOptedIn': return telemetryOptedIn; + default: throw new Error(`unexpected injectedVar ${key}`); + } + }); + + injectedMetadataMock.getInjectedVar = mockGetInjectedVar; +} + +jest.doMock('ui/new_platform', () => ({ + npSetup: { + core: { + notifications: notificationServiceMock.createSetupContract(), + } + }, + npStart: { + core: { + injectedMetadata: injectedMetadataMock, + overlays: overlayServiceMock.createStartContract(), + }, + }, +})); diff --git a/src/legacy/core_plugins/kibana/public/home/telemetry_opt_in.js b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.ts similarity index 75% rename from src/legacy/core_plugins/kibana/public/home/telemetry_opt_in.js rename to src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.ts index 274820844da45..f4462ffea7a33 100644 --- a/src/legacy/core_plugins/kibana/public/home/telemetry_opt_in.js +++ b/src/legacy/core_plugins/telemetry/public/services/telemetry_opt_in.ts @@ -23,16 +23,20 @@ import { toastNotifications } from 'ui/notify'; import { npStart } from 'ui/new_platform'; import { i18n } from '@kbn/i18n'; -export function TelemetryOptInProvider($injector, chrome) { - let currentOptInStatus = npStart.core.injectedMetadata.getInjectedVar('telemetryOptedIn'); - let bannerId = null; +let bannerId: string | null = null; +let currentOptInStatus = false; + +export function TelemetryOptInProvider($injector: any, chrome: any) { + currentOptInStatus = npStart.core.injectedMetadata.getInjectedVar('telemetryOptedIn') as boolean; setCanTrackUiMetrics(currentOptInStatus); const provider = { getBannerId: () => bannerId, getOptIn: () => currentOptInStatus, - setBannerId(id) { bannerId = id; }, - setOptIn: async (enabled) => { + setBannerId(id: string) { + bannerId = id; + }, + setOptIn: async (enabled: boolean) => { setCanTrackUiMetrics(enabled); const $http = $injector.get('$http'); @@ -41,10 +45,10 @@ export function TelemetryOptInProvider($injector, chrome) { currentOptInStatus = enabled; } catch (error) { toastNotifications.addError(error, { - title: i18n.translate('kbn.home.telemetry.optInErrorToastTitle', { + title: i18n.translate('telemetry.optInErrorToastTitle', { defaultMessage: 'Error', }), - toastMessage: i18n.translate('kbn.home.telemetry.optInErrorToastText', { + toastMessage: i18n.translate('telemetry.optInErrorToastText', { defaultMessage: 'An error occured while trying to set the usage statistics preference.', }), }); @@ -58,11 +62,13 @@ export function TelemetryOptInProvider($injector, chrome) { return $http.post(chrome.addBasePath(`/api/telemetry/v2/clusters/_stats`), { unencrypted: true, timeRange: { - min: moment().subtract(20, 'minutes').toISOString(), - max: moment().toISOString() - } + min: moment() + .subtract(20, 'minutes') + .toISOString(), + max: moment().toISOString(), + }, }); - } + }, }; return provider; diff --git a/src/legacy/core_plugins/telemetry/public/views/management/index.js b/src/legacy/core_plugins/telemetry/public/views/management/index.js new file mode 100644 index 0000000000000..2e9f064ec80d8 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/public/views/management/index.js @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import './management'; diff --git a/src/legacy/core_plugins/telemetry/public/views/management/management.js b/src/legacy/core_plugins/telemetry/public/views/management/management.js new file mode 100644 index 0000000000000..9f6e96c925cda --- /dev/null +++ b/src/legacy/core_plugins/telemetry/public/views/management/management.js @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import routes from 'ui/routes'; + +import { registerSettingsComponent, PAGE_FOOTER_COMPONENT } from 'ui/management'; +import { TelemetryOptInProvider } from '../../services'; +import { TelemetryForm } from '../../components'; + +routes.defaults(/\/management/, { + resolve: { + telemetryManagementSection: function (Private) { + const telemetryOptInProvider = Private(TelemetryOptInProvider); + + const Component = (props) => ( + + ); + + registerSettingsComponent(PAGE_FOOTER_COMPONENT, Component, true); + } + } +}); diff --git a/src/legacy/core_plugins/telemetry/server/collection_manager.ts b/src/legacy/core_plugins/telemetry/server/collection_manager.ts new file mode 100644 index 0000000000000..fef0a9b0f9f40 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/collection_manager.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +class TelemetryCollectionManager { + private getterMethod?: any; + private collectionTitle?: string; + private getterMethodPriority = 0; + + public setStatsGetter = (statsGetter: any, title: string, priority = 0) => { + if (priority >= this.getterMethodPriority) { + this.getterMethod = statsGetter; + this.collectionTitle = title; + this.getterMethodPriority = priority; + } + }; + + getCollectionTitle = () => { + return this.collectionTitle; + }; + + public getStatsGetter = () => { + if (!this.getterMethod) { + throw Error('Stats getter method not set.'); + } + return { + getStats: this.getterMethod, + priority: this.getterMethodPriority, + title: this.collectionTitle, + }; + }; +} + +export const telemetryCollectionManager = new TelemetryCollectionManager(); diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/encryption/encrypt.test.ts b/src/legacy/core_plugins/telemetry/server/collectors/encryption/encrypt.test.ts similarity index 53% rename from x-pack/legacy/plugins/telemetry/server/collectors/encryption/encrypt.test.ts rename to src/legacy/core_plugins/telemetry/server/collectors/encryption/encrypt.test.ts index ed13b5569d987..4a4ba7aa1f321 100644 --- a/x-pack/legacy/plugins/telemetry/server/collectors/encryption/encrypt.test.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/encryption/encrypt.test.ts @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 { telemetryJWKS } from './telemetry_jwks'; diff --git a/src/legacy/core_plugins/telemetry/server/collectors/encryption/encrypt.ts b/src/legacy/core_plugins/telemetry/server/collectors/encryption/encrypt.ts new file mode 100644 index 0000000000000..c20f4b768b7dc --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/collectors/encryption/encrypt.ts @@ -0,0 +1,32 @@ +/* + * 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 { createRequestEncryptor } from '@elastic/request-crypto'; +import { telemetryJWKS } from './telemetry_jwks'; + +export function getKID(isProd = false): string { + return isProd ? 'kibana' : 'kibana_dev'; +} + +export async function encryptTelemetry(payload: any, isProd = false): Promise { + const kid = getKID(isProd); + const encryptor = await createRequestEncryptor(telemetryJWKS); + const clusters = [].concat(payload); + return Promise.all(clusters.map((cluster: any) => encryptor.encrypt(kid, cluster))); +} diff --git a/src/legacy/core_plugins/telemetry/server/collectors/encryption/index.ts b/src/legacy/core_plugins/telemetry/server/collectors/encryption/index.ts new file mode 100644 index 0000000000000..08636d45861e4 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/collectors/encryption/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { encryptTelemetry } from './encrypt'; diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/encryption/telemetry_jwks.ts b/src/legacy/core_plugins/telemetry/server/collectors/encryption/telemetry_jwks.ts similarity index 57% rename from x-pack/legacy/plugins/telemetry/server/collectors/encryption/telemetry_jwks.ts rename to src/legacy/core_plugins/telemetry/server/collectors/encryption/telemetry_jwks.ts index b4983ad2d4db7..8615ba2f9d605 100644 --- a/x-pack/legacy/plugins/telemetry/server/collectors/encryption/telemetry_jwks.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/encryption/telemetry_jwks.ts @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 { PublicJWKS } from '@elastic/request-crypto'; diff --git a/src/legacy/core_plugins/telemetry/server/collectors/index.ts b/src/legacy/core_plugins/telemetry/server/collectors/index.ts new file mode 100644 index 0000000000000..0bc1d50fab1be --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/collectors/index.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { encryptTelemetry } from './encryption'; +export { createTelemetryUsageCollector } from './usage'; +export { createUiMetricUsageCollector } from './ui_metric'; +export { createLocalizationUsageCollector } from './localization'; diff --git a/src/legacy/core_plugins/telemetry/server/collectors/localization/file_integrity.test.mocks.ts b/src/legacy/core_plugins/telemetry/server/collectors/localization/file_integrity.test.mocks.ts new file mode 100644 index 0000000000000..cb77ce581eff2 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/collectors/localization/file_integrity.test.mocks.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Readable } from 'stream'; + +jest.doMock('fs', () => ({ + createReadStream(filepath: string): Readable { + if (filepath === 'ERROR') { + throw new Error('MOCK ERROR - Invalid Path'); + } + const readableStream = new Readable(); + const streamData = filepath.split(''); + let cursor = 0; + + readableStream._read = function(size) { + const current = streamData[cursor++]; + if (typeof current === 'undefined') { + return this.push(null); + } + this.push(current); + }; + + return readableStream; + }, +})); diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/localization/file_integrity.test.ts b/src/legacy/core_plugins/telemetry/server/collectors/localization/file_integrity.test.ts similarity index 56% rename from x-pack/legacy/plugins/telemetry/server/collectors/localization/file_integrity.test.ts rename to src/legacy/core_plugins/telemetry/server/collectors/localization/file_integrity.test.ts index 668b0d0c21088..a0b566537f5b4 100644 --- a/x-pack/legacy/plugins/telemetry/server/collectors/localization/file_integrity.test.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/localization/file_integrity.test.ts @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 './file_integrity.test.mocks'; diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/localization/file_integrity.ts b/src/legacy/core_plugins/telemetry/server/collectors/localization/file_integrity.ts similarity index 52% rename from x-pack/legacy/plugins/telemetry/server/collectors/localization/file_integrity.ts rename to src/legacy/core_plugins/telemetry/server/collectors/localization/file_integrity.ts index 1db3795828fff..a852fba4a1c5a 100644 --- a/x-pack/legacy/plugins/telemetry/server/collectors/localization/file_integrity.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/localization/file_integrity.ts @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 { createHash } from 'crypto'; diff --git a/src/legacy/core_plugins/telemetry/server/collectors/localization/index.ts b/src/legacy/core_plugins/telemetry/server/collectors/localization/index.ts new file mode 100644 index 0000000000000..3b289752ce39f --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/collectors/localization/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { createLocalizationUsageCollector } from './telemetry_localization_collector'; diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/localization/telemetry_localization_collector.test.ts b/src/legacy/core_plugins/telemetry/server/collectors/localization/telemetry_localization_collector.test.ts similarity index 51% rename from x-pack/legacy/plugins/telemetry/server/collectors/localization/telemetry_localization_collector.test.ts rename to src/legacy/core_plugins/telemetry/server/collectors/localization/telemetry_localization_collector.test.ts index 2cd3938e58ece..eec5cc8a065e4 100644 --- a/x-pack/legacy/plugins/telemetry/server/collectors/localization/telemetry_localization_collector.test.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/localization/telemetry_localization_collector.test.ts @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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. */ interface TranslationsMock { diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/localization/telemetry_localization_collector.ts b/src/legacy/core_plugins/telemetry/server/collectors/localization/telemetry_localization_collector.ts similarity index 64% rename from x-pack/legacy/plugins/telemetry/server/collectors/localization/telemetry_localization_collector.ts rename to src/legacy/core_plugins/telemetry/server/collectors/localization/telemetry_localization_collector.ts index b66fe75e7af71..74c93931096b2 100644 --- a/x-pack/legacy/plugins/telemetry/server/collectors/localization/telemetry_localization_collector.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/localization/telemetry_localization_collector.ts @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 { i18nLoader } from '@kbn/i18n'; diff --git a/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/index.ts b/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/index.ts new file mode 100644 index 0000000000000..e1ac7a1f5af12 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { createUiMetricUsageCollector } from './telemetry_ui_metric_collector'; diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts b/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts similarity index 61% rename from x-pack/legacy/plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts rename to src/legacy/core_plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts index a931400399b44..fa3159669c33c 100644 --- a/x-pack/legacy/plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 { UI_METRIC_USAGE_TYPE } from '../../../common/constants'; diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/usage/ensure_deep_object.test.ts b/src/legacy/core_plugins/telemetry/server/collectors/usage/ensure_deep_object.test.ts similarity index 72% rename from x-pack/legacy/plugins/telemetry/server/collectors/usage/ensure_deep_object.test.ts rename to src/legacy/core_plugins/telemetry/server/collectors/usage/ensure_deep_object.test.ts index 4fad392302444..5a520fbeef316 100644 --- a/x-pack/legacy/plugins/telemetry/server/collectors/usage/ensure_deep_object.test.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/usage/ensure_deep_object.test.ts @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 { ensureDeepObject } from './ensure_deep_object'; diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/usage/ensure_deep_object.ts b/src/legacy/core_plugins/telemetry/server/collectors/usage/ensure_deep_object.ts similarity index 63% rename from x-pack/legacy/plugins/telemetry/server/collectors/usage/ensure_deep_object.ts rename to src/legacy/core_plugins/telemetry/server/collectors/usage/ensure_deep_object.ts index 5b00aab010d5c..6594c7f8e7a6f 100644 --- a/x-pack/legacy/plugins/telemetry/server/collectors/usage/ensure_deep_object.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/usage/ensure_deep_object.ts @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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. */ // diff --git a/src/legacy/core_plugins/telemetry/server/collectors/usage/index.ts b/src/legacy/core_plugins/telemetry/server/collectors/usage/index.ts new file mode 100644 index 0000000000000..a1b3d5a7b1982 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/collectors/usage/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { createTelemetryUsageCollector } from './telemetry_usage_collector'; diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts similarity index 80% rename from x-pack/legacy/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts rename to src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts index d99f9dba334c9..3806dfc77120f 100644 --- a/x-pack/legacy/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 { writeFileSync, unlinkSync } from 'fs'; @@ -28,8 +41,8 @@ const serverWithConfig = (configPath: string): KibanaHapiServer & Server => { ...getMockServer(), config: () => ({ get: (key: string) => { - if (key !== 'xpack.telemetry.config' && key !== 'xpack.xpack_main.telemetry.config') { - throw new Error('Expected `xpack.telemetry.config`'); + if (key !== 'telemetry.config' && key !== 'xpack.xpack_main.telemetry.config') { + throw new Error('Expected `telemetry.config`'); } return configPath; diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts similarity index 77% rename from x-pack/legacy/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts rename to src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts index a20f4198b3ad3..c927453641193 100644 --- a/x-pack/legacy/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 { accessSync, constants, readFileSync, statSync } from 'fs'; diff --git a/x-pack/legacy/plugins/telemetry/server/get_telemetry_opt_in.ts b/src/legacy/core_plugins/telemetry/server/get_telemetry_opt_in.ts similarity index 52% rename from x-pack/legacy/plugins/telemetry/server/get_telemetry_opt_in.ts rename to src/legacy/core_plugins/telemetry/server/get_telemetry_opt_in.ts index 8f5af83985f14..9b365d6dd7ae5 100644 --- a/x-pack/legacy/plugins/telemetry/server/get_telemetry_opt_in.ts +++ b/src/legacy/core_plugins/telemetry/server/get_telemetry_opt_in.ts @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 async function getTelemetryOptIn(request: any) { diff --git a/src/legacy/core_plugins/telemetry/server/index.ts b/src/legacy/core_plugins/telemetry/server/index.ts new file mode 100644 index 0000000000000..b8ae5fc231fba --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/index.ts @@ -0,0 +1,29 @@ +/* + * 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 { PluginInitializerContext } from 'src/core/server'; +import { TelemetryPlugin } from './plugin'; +import * as constants from '../common/constants'; + +export { getTelemetryOptIn } from './get_telemetry_opt_in'; +export { telemetryCollectionManager } from './collection_manager'; + +export const telemetryPlugin = (initializerContext: PluginInitializerContext) => + new TelemetryPlugin(); +export { constants }; diff --git a/src/legacy/core_plugins/telemetry/server/plugin.ts b/src/legacy/core_plugins/telemetry/server/plugin.ts new file mode 100644 index 0000000000000..70de51b2abe99 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/plugin.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 { CoreSetup } from 'src/core/server'; +import { registerRoutes } from './routes'; +import { telemetryCollectionManager } from './collection_manager'; +import { getStats } from './telemetry_collection'; + +export class TelemetryPlugin { + public setup(core: CoreSetup) { + telemetryCollectionManager.setStatsGetter(getStats, 'local'); + registerRoutes(core); + } +} diff --git a/src/legacy/core_plugins/telemetry/server/routes/index.ts b/src/legacy/core_plugins/telemetry/server/routes/index.ts new file mode 100644 index 0000000000000..12ba541d699f9 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/routes/index.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreSetup } from 'src/core/server'; +import { registerOptInRoutes } from './opt_in'; +import { registerTelemetryDataRoutes } from './telemetry_stats'; + +export function registerRoutes(core: CoreSetup) { + registerOptInRoutes(core); + registerTelemetryDataRoutes(core); +} diff --git a/x-pack/legacy/plugins/telemetry/server/routes/opt_in.ts b/src/legacy/core_plugins/telemetry/server/routes/opt_in.ts similarity index 52% rename from x-pack/legacy/plugins/telemetry/server/routes/opt_in.ts rename to src/legacy/core_plugins/telemetry/server/routes/opt_in.ts index 72c602e84f90a..aabc0259f08fc 100644 --- a/x-pack/legacy/plugins/telemetry/server/routes/opt_in.ts +++ b/src/legacy/core_plugins/telemetry/server/routes/opt_in.ts @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 Joi from 'joi'; diff --git a/x-pack/legacy/plugins/telemetry/server/routes/telemetry_stats.ts b/src/legacy/core_plugins/telemetry/server/routes/telemetry_stats.ts similarity index 54% rename from x-pack/legacy/plugins/telemetry/server/routes/telemetry_stats.ts rename to src/legacy/core_plugins/telemetry/server/routes/telemetry_stats.ts index 8ab4e68008e82..8a91d24b34ed2 100644 --- a/x-pack/legacy/plugins/telemetry/server/routes/telemetry_stats.ts +++ b/src/legacy/core_plugins/telemetry/server/routes/telemetry_stats.ts @@ -1,13 +1,27 @@ /* - * 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. + * 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 Joi from 'joi'; import { boomify } from 'boom'; import { CoreSetup } from 'src/core/server'; -import { getStats, encryptTelemetry } from '../collectors'; +import { encryptTelemetry } from '../collectors'; +import { telemetryCollectionManager } from '../collection_manager'; export function registerTelemetryDataRoutes(core: CoreSetup) { const { server } = core.http as any; @@ -34,13 +48,17 @@ export function registerTelemetryDataRoutes(core: CoreSetup) { const isDev = config.get('env.dev'); try { + const { getStats, title } = telemetryCollectionManager.getStatsGetter(); + server.log(['debug', 'telemetry'], `Using Stats Getter: ${title}`); + const usageData = await getStats(req, config, start, end, unencrypted); + if (unencrypted) return usageData; return encryptTelemetry(usageData, isDev); } catch (err) { if (isDev) { // don't ignore errors when running in dev mode - return boomify(err, { statusCode: err.status }); + return boomify(err, { statusCode: err.status || 500 }); } else { const statusCode = unencrypted && err.status === 403 ? 403 : 200; // ignore errors and return empty set diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_cluster_info.js b/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_cluster_info.js new file mode 100644 index 0000000000000..566e5b7019ddd --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_cluster_info.js @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import sinon from 'sinon'; + +import { getClusterInfo } from '../get_cluster_info'; + +export function mockGetClusterInfo(callCluster, clusterInfo, req) { + callCluster.withArgs(req, 'info').returns(clusterInfo); + callCluster.withArgs('info').returns(clusterInfo); +} + +describe('get_cluster_info', () => { + + it('uses callCluster to get info API', () => { + const callCluster = sinon.stub(); + const response = Promise.resolve({}); + + mockGetClusterInfo(callCluster, response); + + expect(getClusterInfo(callCluster)).to.be(response); + }); + +}); diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_cluster_stats.js b/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_cluster_stats.js new file mode 100644 index 0000000000000..9ca609cd88778 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_cluster_stats.js @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; +import sinon from 'sinon'; + +import { TIMEOUT } from '../constants'; +import { getClusterStats } from '../get_cluster_stats'; + +export function mockGetClusterStats(callCluster, clusterStats, req) { + callCluster.withArgs(req, 'cluster.stats', { + timeout: TIMEOUT + }) + .returns(clusterStats); + + callCluster.withArgs('cluster.stats', { + timeout: TIMEOUT + }) + .returns(clusterStats); +} + +describe('get_cluster_stats', () => { + + it('uses callCluster to get cluster.stats API', () => { + const callCluster = sinon.stub(); + const response = Promise.resolve({}); + + mockGetClusterStats(callCluster, response); + + expect(getClusterStats(callCluster)).to.be(response); + }); + +}); diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/local/__tests__/get_local_stats.js b/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js similarity index 86% rename from x-pack/legacy/plugins/telemetry/server/collectors/local/__tests__/get_local_stats.js rename to src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js index e0879d208d6f4..d0de9cc365a71 100644 --- a/x-pack/legacy/plugins/telemetry/server/collectors/local/__tests__/get_local_stats.js +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ import expect from '@kbn/expect'; @@ -9,7 +22,6 @@ import sinon from 'sinon'; import { mockGetClusterInfo } from './get_cluster_info'; import { mockGetClusterStats } from './get_cluster_stats'; -import { mockGetXPack } from './get_xpack'; import { omit } from 'lodash'; import { @@ -22,17 +34,25 @@ const getMockServer = (getCluster = sinon.stub(), kibanaUsage = {}) => ({ log(tags, message) { console.log({ tags, message }); }, + config() { + return { + get(item) { + switch(item) { + case 'pkg.version': return '8675309-snapshot'; + default: throw Error(`unexpected config.get('${item}') received.`); + } + } + }; + }, usage: { collectorSet: { bulkFetch: () => kibanaUsage, toObject: data => data } }, plugins: { - xpack_main: { status: { plugin: { kbnServer: { version: '8675309-snapshot' } } } }, elasticsearch: { getCluster }, }, }); -function mockGetLocalStats(callCluster, clusterInfo, clusterStats, license, usage, req) { +function mockGetLocalStats(callCluster, clusterInfo, clusterStats, req) { mockGetClusterInfo(callCluster, clusterInfo, req); mockGetClusterStats(callCluster, clusterStats, req); - mockGetXPack(callCluster, license, usage, req); } describe('get_local_stats', () => { @@ -53,8 +73,6 @@ describe('get_local_stats', () => { nodes: { yup: 'abc' }, random: 123 }; - const license = { fancy: 'license' }; - const xpack = { also: 'fancy' }; const kibana = { kibana: { great: 'googlymoogly', @@ -81,9 +99,6 @@ describe('get_local_stats', () => { collection: 'local', cluster_uuid: clusterUuid, cluster_name: clusterName, - license: { - fancy: 'license' - }, version, cluster_stats: omit(clusterStats, '_nodes', 'cluster_name'), stack_stats: { @@ -108,7 +123,6 @@ describe('get_local_stats', () => { snow: { chances: 0 }, } }, - xpack: { also: 'fancy' }, } }; @@ -121,11 +135,11 @@ describe('get_local_stats', () => { expect(result.version).to.be('2.3.4'); expect(result.collection).to.be('local'); expect(result.license).to.be(undefined); - expect(result.stack_stats).to.eql({ kibana: undefined, xpack: undefined }); + expect(result.stack_stats).to.eql({ kibana: undefined }); }); it('returns expected object with xpack', () => { - const result = handleLocalStats(getMockServer(), clusterInfo, clusterStats, license, xpack); + const result = handleLocalStats(getMockServer(), clusterInfo, clusterStats); const { stack_stats: stack, ...cluster } = result; expect(cluster.collection).to.be(combinedStatsResult.collection); expect(cluster.cluster_uuid).to.be(combinedStatsResult.cluster_uuid); @@ -147,8 +161,6 @@ describe('get_local_stats', () => { callClusterUsageFailed, Promise.resolve(clusterInfo), Promise.resolve(clusterStats), - Promise.resolve(license), - Promise.reject('usage failed') ); const result = await getLocalStatsWithCaller(getMockServer(), callClusterUsageFailed); @@ -170,8 +182,6 @@ describe('get_local_stats', () => { callCluster, Promise.resolve(clusterInfo), Promise.resolve(clusterStats), - Promise.resolve(license), - Promise.resolve(xpack) ); const result = await getLocalStatsWithCaller(getMockServer(callCluster, kibana), callCluster); @@ -192,8 +202,6 @@ describe('get_local_stats', () => { callWithInternalUser, Promise.resolve(clusterInfo), Promise.resolve(clusterStats), - Promise.resolve(license), - Promise.resolve(xpack) ); const result = await getLocalStats(req, { useInternalUser: true }); @@ -213,8 +221,6 @@ describe('get_local_stats', () => { callWithRequest, Promise.resolve(clusterInfo), Promise.resolve(clusterStats), - Promise.resolve(license), - Promise.resolve(xpack), req ); diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/constants.ts b/src/legacy/core_plugins/telemetry/server/telemetry_collection/constants.ts new file mode 100644 index 0000000000000..ad14bfe7430da --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/constants.ts @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * The timeout used by each request, whenever a timeout can be specified. + */ +export const TIMEOUT = '30s'; diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_info.js b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_info.js new file mode 100644 index 0000000000000..2e4ed0b36ed26 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_info.js @@ -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. + */ + +/** + * Get the cluster info from the connected cluster. + * + * This is the equivalent to GET / + * + * @param {function} callCluster The callWithInternalUser handler (exposed for testing) + * @return {Promise} The response from Elasticsearch. + */ +export function getClusterInfo(callCluster) { + return callCluster('info'); +} diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_stats.js b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_stats.js new file mode 100644 index 0000000000000..a840c39812e2c --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_cluster_stats.js @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TIMEOUT } from './constants'; + +/** + * Get the cluster stats from the connected cluster. + * + * This is the equivalent to GET /_cluster/stats?timeout=30s. + * + * @param {function} callCluster The callWithInternalUser handler (exposed for testing) + * @return {Promise} The response from Elasticsearch equivalent to GET /_cluster/stats. + */ +export function getClusterStats(callCluster) { + return callCluster('cluster.stats', { + timeout: TIMEOUT + }); +} diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_kibana.js b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_kibana.js new file mode 100644 index 0000000000000..051ef370fcde5 --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_kibana.js @@ -0,0 +1,58 @@ +/* + * 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 { get, omit } from 'lodash'; + +export function handleKibanaStats(server, response) { + if (!response) { + server.log(['warning', 'telemetry', 'local-stats'], 'No Kibana stats returned from usage collectors'); + return; + } + + const { kibana, kibana_stats: kibanaStats, ...plugins } = response; + + const platform = get(kibanaStats, 'os.platform', 'unknown'); + const platformRelease = get(kibanaStats, 'os.platformRelease', 'unknown'); + + const version = server.config().get('pkg.version').replace(/-snapshot/i, ''); + + // combine core stats (os types, saved objects) with plugin usage stats + // organize the object into the same format as monitoring-enabled telemetry + return { + ...omit(kibana, 'index'), // discard index + count: 1, + indices: 1, + os: { + platforms: [{ platform, count: 1 }], + platformReleases: [{ platformRelease, count: 1 }], + }, + versions: [{ version, count: 1 }], + plugins, + }; +} + +/* + * Check user privileges for read access to monitoring + * Pass callWithInternalUser to bulkFetchUsage + */ +export async function getKibana(server, callWithInternalUser) { + const { collectorSet } = server.usage; + const usage = await collectorSet.bulkFetch(callWithInternalUser); + return collectorSet.toObject(usage); +} diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/local/get_local_stats.js b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.js similarity index 62% rename from x-pack/legacy/plugins/telemetry/server/collectors/local/get_local_stats.js rename to src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.js index 5f6f738b6e501..67fc721306c21 100644 --- a/x-pack/legacy/plugins/telemetry/server/collectors/local/get_local_stats.js +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_local_stats.js @@ -1,13 +1,25 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * 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 { get, omit } from 'lodash'; import { getClusterInfo } from './get_cluster_info'; import { getClusterStats } from './get_cluster_stats'; -import { getXPack } from './get_xpack'; import { getKibana, handleKibanaStats } from './get_kibana'; /** @@ -16,10 +28,9 @@ import { getKibana, handleKibanaStats } from './get_kibana'; * * @param {Object} clusterInfo Cluster info (GET /) * @param {Object} clusterStats Cluster stats (GET /_cluster/stats) - * @param {Object} xpack License and X-Pack details * @return {Object} A combined object containing the different responses. */ -export function handleLocalStats(server, clusterInfo, clusterStats, license, xpack, kibana) { +export function handleLocalStats(server, clusterInfo, clusterStats, kibana) { return { timestamp: (new Date()).toISOString(), cluster_uuid: get(clusterInfo, 'cluster_uuid'), @@ -27,10 +38,8 @@ export function handleLocalStats(server, clusterInfo, clusterStats, license, xpa version: get(clusterInfo, 'version.number'), cluster_stats: omit(clusterStats, '_nodes', 'cluster_name'), collection: 'local', - license, stack_stats: { kibana: handleKibanaStats(server, kibana), - xpack, } }; } @@ -42,18 +51,17 @@ export function handleLocalStats(server, clusterInfo, clusterStats, license, xpa * @param {function} callCluster The callWithInternalUser handler (exposed for testing) * @return {Promise} The object containing the current Elasticsearch cluster's telemetry. */ -export function getLocalStatsWithCaller(server, callCluster) { - return Promise.all([ +export async function getLocalStatsWithCaller(server, callCluster) { + const [ clusterInfo, clusterStats, kibana ] = await Promise.all([ getClusterInfo(callCluster), // cluster info getClusterStats(callCluster), // cluster stats (not to be confused with cluster _state_) - getXPack(callCluster), // { license, xpack } - getKibana(server, callCluster) - ]).then(([clusterInfo, clusterStats, { license, xpack }, kibana]) => { - return handleLocalStats(server, clusterInfo, clusterStats, license, xpack, kibana); - } - ); + getKibana(server, callCluster), + ]); + + return handleLocalStats(server, clusterInfo, clusterStats, kibana); } + /** * Get statistics for the connected Elasticsearch cluster. * @@ -61,10 +69,10 @@ export function getLocalStatsWithCaller(server, callCluster) { * @param {Boolean} useRequestUser callWithRequest, otherwise callWithInternalUser * @return {Promise} The cluster object containing telemetry. */ -export function getLocalStats(req, { useInternalUser = false } = {}) { +export async function getLocalStats(req, { useInternalUser = false } = {}) { const { server } = req; const { callWithRequest, callWithInternalUser } = server.plugins.elasticsearch.getCluster('data'); const callCluster = useInternalUser ? callWithInternalUser : (...args) => callWithRequest(req, ...args); - return getLocalStatsWithCaller(server, callCluster); + return await getLocalStatsWithCaller(server, callCluster); } diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_stats.ts b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_stats.ts new file mode 100644 index 0000000000000..024272e0f805c --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/get_stats.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// @ts-ignore +import { getLocalStats } from './get_local_stats'; + +/** + * Get the telemetry data. + * + * @param {Object} req The incoming request. + * @param {Object} config Kibana config. + * @param {String} start The start time of the request (likely 20m ago). + * @param {String} end The end time of the request. + * @param {Boolean} unencrypted Is the request payload going to be unencrypted. + * @return {Promise} An array of telemetry objects. + */ +export async function getStats( + req: any, + config: any, + start: string, + end: string, + unencrypted: boolean +) { + return [ + await getLocalStats(req, { + useInternalUser: !unencrypted, + }), + ]; +} diff --git a/src/legacy/core_plugins/telemetry/server/telemetry_collection/index.ts b/src/legacy/core_plugins/telemetry/server/telemetry_collection/index.ts new file mode 100644 index 0000000000000..f33727d82f44c --- /dev/null +++ b/src/legacy/core_plugins/telemetry/server/telemetry_collection/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// @ts-ignore +export { getLocalStats } from './get_local_stats'; + +// @ts-ignore +export { getStats } from './get_stats'; diff --git a/test/common/config.js b/test/common/config.js index 452413fe4e4eb..44e4bef99bf62 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -55,6 +55,7 @@ export default function () { `--elasticsearch.username=${servers.elasticsearch.username}`, `--elasticsearch.password=${servers.elasticsearch.password}`, `--kibana.disableWelcomeScreen=true`, + '--telemetry.banner=false', `--server.maxPayloadBytes=1679958`, ], }, diff --git a/typings/elastic__node_crypto.d.ts b/typings/elastic__node_crypto.d.ts new file mode 100644 index 0000000000000..8d4b47da96b73 --- /dev/null +++ b/typings/elastic__node_crypto.d.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +declare module '@elastic/node-crypto'; diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 872d4ed9c29d1..e0ba455552966 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -25,7 +25,6 @@ "xpack.ml": "legacy/plugins/ml", "xpack.logstash": "legacy/plugins/logstash", "xpack.main": "legacy/plugins/xpack_main", - "xpack.telemetry": "legacy/plugins/telemetry", "xpack.monitoring": "legacy/plugins/monitoring", "xpack.remoteClusters": "legacy/plugins/remote_clusters", "xpack.reporting": "legacy/plugins/reporting", diff --git a/x-pack/index.js b/x-pack/index.js index f2e602ec05586..756d9b4d3127a 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -38,7 +38,6 @@ import { upgradeAssistant } from './legacy/plugins/upgrade_assistant'; import { uptime } from './legacy/plugins/uptime'; import { ossTelemetry } from './legacy/plugins/oss_telemetry'; import { fileUpload } from './legacy/plugins/file_upload'; -import { telemetry } from './legacy/plugins/telemetry'; import { encryptedSavedObjects } from './legacy/plugins/encrypted_saved_objects'; import { snapshotRestore } from './legacy/plugins/snapshot_restore'; import { transform } from './legacy/plugins/transform'; @@ -49,7 +48,6 @@ import { lens } from './legacy/plugins/lens'; module.exports = function (kibana) { return [ xpackMain(kibana), - telemetry(kibana), graph(kibana), monitoring(kibana), reporting(kibana), diff --git a/x-pack/legacy/plugins/license_management/public/lib/telemetry.js b/x-pack/legacy/plugins/license_management/public/lib/telemetry.js index 08928549916eb..bf8bed05aabed 100644 --- a/x-pack/legacy/plugins/license_management/public/lib/telemetry.js +++ b/x-pack/legacy/plugins/license_management/public/lib/telemetry.js @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fetchTelemetry } from '../../../telemetry/public/hacks/fetch_telemetry'; -export { PRIVACY_STATEMENT_URL } from '../../../telemetry/common/constants'; -export { TelemetryOptInProvider } from '../../../telemetry/public/services/telemetry_opt_in'; -export { OptInExampleFlyout } from '../../../telemetry/public/components'; +import { fetchTelemetry } from '../../../../../../src/legacy/core_plugins/telemetry/public/hacks/fetch_telemetry'; +export { PRIVACY_STATEMENT_URL } from '../../../../../../src/legacy/core_plugins/telemetry/common/constants'; +export { TelemetryOptInProvider } from '../../../../../../src/legacy/core_plugins/telemetry/public/services'; +export { OptInExampleFlyout } from '../../../../../../src/legacy/core_plugins/telemetry/public/components'; let telemetryEnabled; let httpClient; diff --git a/x-pack/legacy/plugins/monitoring/common/constants.js b/x-pack/legacy/plugins/monitoring/common/constants.js index f953741cd2e02..e40941396b661 100644 --- a/x-pack/legacy/plugins/monitoring/common/constants.js +++ b/x-pack/legacy/plugins/monitoring/common/constants.js @@ -155,10 +155,7 @@ export const METRICBEAT_INDEX_NAME_UNIQUE_TOKEN = '-mb-'; // We use this for metricbeat migration to identify specific products that we do not have constants for export const ELASTICSEARCH_SYSTEM_ID = 'elasticsearch'; -export const KIBANA_SYSTEM_ID = 'kibana'; -export const BEATS_SYSTEM_ID = 'beats'; -export const APM_SYSTEM_ID = 'apm'; -export const LOGSTASH_SYSTEM_ID = 'logstash'; + /** * The id of the infra source owned by the monitoring plugin. */ @@ -181,3 +178,47 @@ export const CODE_PATH_LOGSTASH = 'logstash'; export const CODE_PATH_APM = 'apm'; export const CODE_PATH_LICENSE = 'license'; export const CODE_PATH_LOGS = 'logs'; + +/** + * The header sent by telemetry service when hitting Elasticsearch to identify query source + * @type {string} + */ +export const TELEMETRY_QUERY_SOURCE = 'TELEMETRY'; + +/** + * The name of the Kibana System ID used to publish and look up Kibana stats through the Monitoring system. + * @type {string} + */ +export const KIBANA_SYSTEM_ID = 'kibana'; + +/** + * The name of the Beats System ID used to publish and look up Beats stats through the Monitoring system. + * @type {string} + */ +export const BEATS_SYSTEM_ID = 'beats'; + +/** + * The name of the Apm System ID used to publish and look up Apm stats through the Monitoring system. + * @type {string} + */ +export const APM_SYSTEM_ID = 'apm'; + +/** + * The name of the Kibana System ID used to look up Logstash stats through the Monitoring system. + * @type {string} + */ +export const LOGSTASH_SYSTEM_ID = 'logstash'; + +/** + * The name of the Kibana System ID used to look up Reporting stats through the Monitoring system. + * @type {string} + */ +export const REPORTING_SYSTEM_ID = 'reporting'; + +/** + * The amount of time, in milliseconds, to wait between collecting kibana stats from es. + * + * Currently 24 hours kept in sync with reporting interval. + * @type {Number} + */ +export const TELEMETRY_COLLECTION_INTERVAL = 86400000; diff --git a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/__tests__/bulk_uploader.js b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/__tests__/bulk_uploader.js index d494963c4492d..3cacf00016f0d 100644 --- a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/__tests__/bulk_uploader.js +++ b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/__tests__/bulk_uploader.js @@ -49,9 +49,6 @@ describe('BulkUploader', () => { server = { log: sinon.spy(), - xpackMainPlugin: { - telemetryCollectionInterval: 3000, - }, elasticsearchPlugin: { createCluster: () => cluster, getCluster: () => cluster, diff --git a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js index 015af8f3d633a..da23d4b77a323 100644 --- a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js +++ b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/bulk_uploader.js @@ -10,7 +10,9 @@ import { callClusterFactory } from '../../../xpack_main'; import { LOGGING_TAG, KIBANA_MONITORING_LOGGING_TAG, + TELEMETRY_COLLECTION_INTERVAL, } from '../../common/constants'; + import { sendBulkPayload, monitoringBulk, @@ -36,7 +38,7 @@ const LOGGING_TAGS = [LOGGING_TAG, KIBANA_MONITORING_LOGGING_TAG]; * @param {Object} xpackInfo server.plugins.xpack_main.info object */ export class BulkUploader { - constructor({ config, log, interval, xpackMainPlugin, elasticsearchPlugin, kbnServerStatus, kbnServerVersion }) { + constructor({ config, log, interval, elasticsearchPlugin, kbnServerStatus, kbnServerVersion }) { if (typeof interval !== 'number') { throw new Error('interval number of milliseconds is required'); } @@ -44,7 +46,7 @@ export class BulkUploader { this._timer = null; this._interval = interval; this._lastFetchUsageTime = null; - this._usageInterval = xpackMainPlugin.telemetryCollectionInterval; + this._usageInterval = TELEMETRY_COLLECTION_INTERVAL; this._log = { debug: message => log(['debug', ...LOGGING_TAGS], message), diff --git a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/lib/send_bulk_payload.js b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/lib/send_bulk_payload.js index 208fb6305457b..3a286e4d7a513 100644 --- a/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/lib/send_bulk_payload.js +++ b/x-pack/legacy/plugins/monitoring/server/kibana_monitoring/lib/send_bulk_payload.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MONITORING_SYSTEM_API_VERSION } from '../../../common/constants'; -import { KIBANA_SYSTEM_ID } from '../../../../telemetry/common/constants'; +import { MONITORING_SYSTEM_API_VERSION, KIBANA_SYSTEM_ID } from '../../../common/constants'; /* * Send the Kibana usage data to the ES Monitoring Bulk endpoint diff --git a/x-pack/legacy/plugins/monitoring/server/plugin.js b/x-pack/legacy/plugins/monitoring/server/plugin.js index 349e705434bfa..ab7813aa26566 100644 --- a/x-pack/legacy/plugins/monitoring/server/plugin.js +++ b/x-pack/legacy/plugins/monitoring/server/plugin.js @@ -10,6 +10,9 @@ import { requireUIRoutes } from './routes'; import { instantiateClient } from './es_client/instantiate_client'; import { initMonitoringXpackInfo } from './init_monitoring_xpack_info'; import { initBulkUploader } from './kibana_monitoring'; +import { telemetryCollectionManager } from '../../../../../src/legacy/core_plugins/telemetry/server'; +import { getStatsWithMonitoring } from './telemetry_collection'; + import { getKibanaUsageCollector, getOpsStatsCollector, @@ -36,6 +39,8 @@ export class Plugin { collectorSet.register(getKibanaUsageCollector({ collectorSet, config })); collectorSet.register(getSettingsCollector({ collectorSet, config })); + telemetryCollectionManager.setStatsGetter(getStatsWithMonitoring, 'monitoring', 2); + /* * Instantiate and start the internal background task that calls collector * fetch methods and uploads to the ES monitoring bulk endpoint @@ -90,7 +95,6 @@ export class Plugin { const bulkUploader = initBulkUploader({ elasticsearchPlugin: plugins.elasticsearch, - xpackMainPlugin: plugins.xpack_main, config, log: core.log, kbnServerStatus: kbnServer.status, diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/monitoring/__tests__/create_query.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/create_query.js similarity index 100% rename from x-pack/legacy/plugins/telemetry/server/collectors/monitoring/__tests__/create_query.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/create_query.js diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/monitoring/__tests__/fixtures/beats_stats_results.json b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/fixtures/beats_stats_results.json similarity index 100% rename from x-pack/legacy/plugins/telemetry/server/collectors/monitoring/__tests__/fixtures/beats_stats_results.json rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/fixtures/beats_stats_results.json diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/monitoring/__tests__/get_all_stats.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_all_stats.js similarity index 95% rename from x-pack/legacy/plugins/telemetry/server/collectors/monitoring/__tests__/get_all_stats.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_all_stats.js index fff281a0d87ef..0d147b747f2d0 100644 --- a/x-pack/legacy/plugins/telemetry/server/collectors/monitoring/__tests__/get_all_stats.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_all_stats.js @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; -import { addStackStats, getAllStats, getAllStatsForServer, handleAllStats } from '../get_all_stats'; +import { addStackStats, getAllStats, handleAllStats } from '../get_all_stats'; describe('get_all_stats', () => { const size = 123; @@ -106,7 +106,7 @@ describe('get_all_stats', () => { } ]; - describe('getAllStats / getAllStatsForServer return the same results', () => { + describe('getAllStats', () => { it('returns clusters', async () => { const clusterUuidsResponse = { aggregations: { cluster_uuids: { buckets: [ { key: 'a' } ] } } @@ -201,7 +201,6 @@ describe('get_all_stats', () => { .onCall(3).returns(Promise.resolve(logstashStatsResponse)); expect(await getAllStats(req, start, end)).to.eql(allClusters); - expect(await getAllStatsForServer(server, start, end)).to.eql(allClusters); }); it('returns empty clusters', async () => { @@ -213,7 +212,6 @@ describe('get_all_stats', () => { callWithInternalUser.withArgs('search').returns(Promise.resolve(clusterUuidsResponse)); expect(await getAllStats(req, start, end)).to.eql([]); - expect(await getAllStatsForServer(server, start, end)).to.eql([]); }); }); diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/monitoring/__tests__/get_beats_stats.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_beats_stats.js similarity index 100% rename from x-pack/legacy/plugins/telemetry/server/collectors/monitoring/__tests__/get_beats_stats.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_beats_stats.js diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/monitoring/__tests__/get_cluster_uuids.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js similarity index 100% rename from x-pack/legacy/plugins/telemetry/server/collectors/monitoring/__tests__/get_cluster_uuids.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_cluster_uuids.js diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/monitoring/__tests__/get_es_stats.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_es_stats.js similarity index 100% rename from x-pack/legacy/plugins/telemetry/server/collectors/monitoring/__tests__/get_es_stats.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_es_stats.js diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/monitoring/__tests__/get_high_level_stats.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_high_level_stats.js similarity index 100% rename from x-pack/legacy/plugins/telemetry/server/collectors/monitoring/__tests__/get_high_level_stats.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_high_level_stats.js diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/monitoring/__tests__/get_kibana_stats.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_kibana_stats.js similarity index 100% rename from x-pack/legacy/plugins/telemetry/server/collectors/monitoring/__tests__/get_kibana_stats.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/__tests__/get_kibana_stats.js diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/monitoring/create_query.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/create_query.js similarity index 100% rename from x-pack/legacy/plugins/telemetry/server/collectors/monitoring/create_query.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/create_query.js diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/monitoring/get_all_stats.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.js similarity index 84% rename from x-pack/legacy/plugins/telemetry/server/collectors/monitoring/get_all_stats.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.js index bc93dad72214d..b1e8db1b96005 100644 --- a/x-pack/legacy/plugins/telemetry/server/collectors/monitoring/get_all_stats.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_all_stats.js @@ -6,7 +6,11 @@ import { get, set, merge } from 'lodash'; -import { constants } from '../../'; +import { + LOGSTASH_SYSTEM_ID, + KIBANA_SYSTEM_ID, + BEATS_SYSTEM_ID, +} from '../../common/constants'; import { getClusterUuids } from './get_cluster_uuids'; import { getElasticsearchStats } from './get_es_stats'; import { getKibanaStats } from './get_kibana_stats'; @@ -30,20 +34,6 @@ export function getAllStats(req, start, end, { useInternalUser = false } = {}) { return getAllStatsWithCaller(server, callCluster, start, end); } -/** - * Get statistics for all products joined by Elasticsearch cluster. - * - * @param {Object} server The server instance used to call ES as the internal user - * @param {Date} start The starting range to request data - * @param {Date} end The ending range to request data - * @return {Promise} The array of clusters joined with the Kibana and Logstash instances. - */ -export function getAllStatsForServer(server, start, end) { - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('monitoring'); - - return getAllStatsWithCaller(server, callWithInternalUser, start, end); -} - /** * Get statistics for all products joined by Elasticsearch cluster. * @@ -64,7 +54,7 @@ function getAllStatsWithCaller(server, callCluster, start, end) { return Promise.all([ getElasticsearchStats(server, callCluster, clusterUuids), // cluster_stats, stack_stats.xpack, cluster_name/uuid, license, version getKibanaStats(server, callCluster, clusterUuids, start, end), // stack_stats.kibana - getHighLevelStats(server, callCluster, clusterUuids, start, end, constants.LOGSTASH_SYSTEM_ID), // stack_stats.logstash + getHighLevelStats(server, callCluster, clusterUuids, start, end, LOGSTASH_SYSTEM_ID), // stack_stats.logstash getBeatsStats(server, callCluster, clusterUuids, start, end), // stack_stats.beats ]) .then(([esClusters, kibana, logstash, beats]) => handleAllStats(esClusters, { kibana, logstash, beats })); @@ -83,9 +73,9 @@ function getAllStatsWithCaller(server, callCluster, start, end) { export function handleAllStats(clusters, { kibana, logstash, beats }) { return clusters.map(cluster => { // if they are using Kibana or Logstash, then add it to the cluster details under cluster.stack_stats - addStackStats(cluster, kibana, constants.KIBANA_SYSTEM_ID); - addStackStats(cluster, logstash, constants.LOGSTASH_SYSTEM_ID); - addStackStats(cluster, beats, constants.BEATS_SYSTEM_ID); + addStackStats(cluster, kibana, KIBANA_SYSTEM_ID); + addStackStats(cluster, logstash, LOGSTASH_SYSTEM_ID); + addStackStats(cluster, beats, BEATS_SYSTEM_ID); mergeXPackStats(cluster, kibana, 'graph_workspace', 'graph'); // copy graph_workspace info out of kibana, merge it into stack_stats.xpack.graph return cluster; diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/monitoring/get_beats_stats.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_beats_stats.js similarity index 97% rename from x-pack/legacy/plugins/telemetry/server/collectors/monitoring/get_beats_stats.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_beats_stats.js index b8688e8b7a55e..247e4e991125c 100644 --- a/x-pack/legacy/plugins/telemetry/server/collectors/monitoring/get_beats_stats.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_beats_stats.js @@ -6,7 +6,7 @@ import { get } from 'lodash'; import { createQuery } from './create_query'; -import { INDEX_PATTERN_BEATS } from '../../../../monitoring/common/constants'; +import { INDEX_PATTERN_BEATS } from '../../common/constants'; const HITS_SIZE = 10000; // maximum hits to receive from ES with each search @@ -118,9 +118,13 @@ export function processResults(results = [], { clusters, clusterHostSets, cluste clusterHb.monitors += heartbeatState.monitors; clusterHb.endpoints += heartbeatState.endpoints; for (const proto in heartbeatState) { - if (!heartbeatState.hasOwnProperty(proto)) continue; + if (!heartbeatState.hasOwnProperty(proto)) { + continue; + } const val = heartbeatState[proto]; - if (typeof val !== 'object') continue; + if (typeof val !== 'object') { + continue; + } if (!clusterHb.hasOwnProperty(proto)) { clusterHb[proto] = { diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/monitoring/get_cluster_uuids.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.js similarity index 96% rename from x-pack/legacy/plugins/telemetry/server/collectors/monitoring/get_cluster_uuids.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.js index 9ea8de80e05d4..9caf4cac50e09 100644 --- a/x-pack/legacy/plugins/telemetry/server/collectors/monitoring/get_cluster_uuids.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_cluster_uuids.js @@ -6,7 +6,7 @@ import { get } from 'lodash'; import { createQuery } from './create_query'; -import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../monitoring/common/constants'; +import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; /** * Get a list of Cluster UUIDs that exist within the specified timespan. diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/monitoring/get_es_stats.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_es_stats.js similarity index 96% rename from x-pack/legacy/plugins/telemetry/server/collectors/monitoring/get_es_stats.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_es_stats.js index 0a54871165338..f12c337c30604 100644 --- a/x-pack/legacy/plugins/telemetry/server/collectors/monitoring/get_es_stats.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_es_stats.js @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { INDEX_PATTERN_ELASTICSEARCH } from '../../../../monitoring/common/constants'; +import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; /** * Get statistics for all selected Elasticsearch clusters. diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/monitoring/get_high_level_stats.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_high_level_stats.js similarity index 97% rename from x-pack/legacy/plugins/telemetry/server/collectors/monitoring/get_high_level_stats.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_high_level_stats.js index ecd6ccc2d2404..889efe37e7a8f 100644 --- a/x-pack/legacy/plugins/telemetry/server/collectors/monitoring/get_high_level_stats.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_high_level_stats.js @@ -6,8 +6,16 @@ import { get } from 'lodash'; import { createQuery } from './create_query'; -import { INDEX_PATTERN_KIBANA, INDEX_PATTERN_BEATS, INDEX_PATTERN_LOGSTASH } from '../../../../monitoring/common/constants'; -import { KIBANA_SYSTEM_ID, BEATS_SYSTEM_ID, APM_SYSTEM_ID, LOGSTASH_SYSTEM_ID, TELEMETRY_QUERY_SOURCE } from '../../../common/constants'; +import { + INDEX_PATTERN_KIBANA, + INDEX_PATTERN_BEATS, + INDEX_PATTERN_LOGSTASH, + KIBANA_SYSTEM_ID, + BEATS_SYSTEM_ID, + APM_SYSTEM_ID, + LOGSTASH_SYSTEM_ID, + TELEMETRY_QUERY_SOURCE, +} from '../../common/constants'; /** * Update a counter associated with the {@code key}. diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/monitoring/get_kibana_stats.js b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_kibana_stats.js similarity index 98% rename from x-pack/legacy/plugins/telemetry/server/collectors/monitoring/get_kibana_stats.js rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_kibana_stats.js index d92593811b4cd..9364392ab33ae 100644 --- a/x-pack/legacy/plugins/telemetry/server/collectors/monitoring/get_kibana_stats.js +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_kibana_stats.js @@ -5,7 +5,7 @@ */ import { get, isEmpty, omit } from 'lodash'; -import { KIBANA_SYSTEM_ID } from '../../../common/constants'; +import { KIBANA_SYSTEM_ID } from '../../common/constants'; import { fetchHighLevelStats, handleHighLevelStatsResponse } from './get_high_level_stats'; export function rollUpTotals(rolledUp, addOn, field) { diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/get_stats.ts b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_stats_with_monitoring.ts similarity index 58% rename from x-pack/legacy/plugins/telemetry/server/collectors/get_stats.ts rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_stats_with_monitoring.ts index 7b1af9e06b14d..fdf46122f13b7 100644 --- a/x-pack/legacy/plugins/telemetry/server/collectors/get_stats.ts +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/get_stats_with_monitoring.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getAllStats, getLocalStats } from './'; +// @ts-ignore +import { getAllStats } from './get_all_stats'; +import { getStatsWithXpack } from '../../../xpack_main/server/telemetry_collection'; /** * Get the telemetry data. @@ -16,30 +18,25 @@ import { getAllStats, getLocalStats } from './'; * @param {Boolean} unencrypted Is the request payload going to be unencrypted. * @return {Promise} An array of telemetry objects. */ -export async function getStats( +export async function getStatsWithMonitoring( req: any, config: any, start: string, end: string, - unencrypted: boolean, - statsGetters: any = {} + unencrypted: boolean ) { - const { _getAllStats = getAllStats, _getLocalStats = getLocalStats } = statsGetters; let response = []; const useInternalUser = !unencrypted; - if (config.get('xpack.monitoring.enabled')) { - try { - // attempt to collect stats from multiple clusters in monitoring data - response = await _getAllStats(req, start, end, { useInternalUser }); - } catch (err) { - // no-op - } + try { + // attempt to collect stats from multiple clusters in monitoring data + response = await getAllStats(req, start, end, { useInternalUser }); + } catch (err) { + // no-op } if (!Array.isArray(response) || response.length === 0) { - // return it as an array for a consistent API response - response = [await _getLocalStats(req, { useInternalUser })]; + response = await getStatsWithXpack(req, config, start, end, unencrypted); } return response; diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/encryption/index.ts b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/index.ts similarity index 77% rename from x-pack/legacy/plugins/telemetry/server/collectors/encryption/index.ts rename to x-pack/legacy/plugins/monitoring/server/telemetry_collection/index.ts index 0c3ad5a5e3019..db2301932cfdc 100644 --- a/x-pack/legacy/plugins/telemetry/server/collectors/encryption/index.ts +++ b/x-pack/legacy/plugins/monitoring/server/telemetry_collection/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { encryptTelemetry } from './encrypt'; +export { getStatsWithMonitoring } from './get_stats_with_monitoring'; diff --git a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/csp/csp_collector.ts b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/csp/csp_collector.ts deleted file mode 100644 index 3bacde22bdbc4..0000000000000 --- a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/csp/csp_collector.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. - */ - -import { - createCSPRuleString, - DEFAULT_CSP_RULES, -} from '../../../../../../../../src/legacy/server/csp'; -import { HapiServer } from '../../../../'; - -export function createCspCollector(server: HapiServer) { - return { - type: 'csp', - isReady: () => true, - async fetch() { - const config = server.config(); - - // It's important that we do not send the value of csp.rules here as it - // can be customized with values that can be identifiable to given - // installs, such as URLs - const defaultRulesString = createCSPRuleString([...DEFAULT_CSP_RULES]); - const actualRulesString = createCSPRuleString(config.get('csp.rules')); - - return { - strict: config.get('csp.strict'), - warnLegacyBrowsers: config.get('csp.warnLegacyBrowsers'), - rulesChangedFromDefault: defaultRulesString !== actualRulesString, - }; - }, - }; -} - -export function registerCspCollector(server: HapiServer): void { - const { usage } = server; - const collector = usage.collectorSet.makeUsageCollector(createCspCollector(server)); - usage.collectorSet.register(collector); -} diff --git a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/index.ts b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/index.ts index abe96fe061faa..8b825b13178f2 100644 --- a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/index.ts +++ b/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/index.ts @@ -5,10 +5,8 @@ */ import { HapiServer } from '../../../'; -import { registerCspCollector } from './csp'; import { registerVisualizationsCollector } from './visualizations/register_usage_collector'; export function registerCollectors(server: HapiServer) { registerVisualizationsCollector(server); - registerCspCollector(server); } diff --git a/x-pack/legacy/plugins/telemetry/common/constants.ts b/x-pack/legacy/plugins/telemetry/common/constants.ts deleted file mode 100644 index c50f36ac94497..0000000000000 --- a/x-pack/legacy/plugins/telemetry/common/constants.ts +++ /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 { i18n } from '@kbn/i18n'; - -/* - * config options opt into telemetry - * @type {string} - */ -export const CONFIG_TELEMETRY = 'telemetry:optIn'; -/* - * config description for opting into telemetry - * @type {string} - */ -export const getConfigTelemetryDesc = () => { - return i18n.translate('xpack.telemetry.telemetryConfigDescription', { - defaultMessage: - 'Help us improve the Elastic Stack by providing usage statistics for basic features. We will not share this data outside of Elastic.', - }); -}; - -/** - * The name of the Kibana System ID used to publish and look up Kibana stats through the Monitoring system. - * @type {string} - */ -export const KIBANA_SYSTEM_ID = 'kibana'; - -/** - * The name of the Beats System ID used to publish and look up Beats stats through the Monitoring system. - * @type {string} - */ -export const BEATS_SYSTEM_ID = 'beats'; - -/** - * The name of the Apm System ID used to publish and look up Apm stats through the Monitoring system. - * @type {string} - */ -export const APM_SYSTEM_ID = 'beats'; - -/** - * The name of the Kibana System ID used to look up Logstash stats through the Monitoring system. - * @type {string} - */ -export const LOGSTASH_SYSTEM_ID = 'logstash'; - -/** - * The name of the Kibana System ID used to look up Reporting stats through the Monitoring system. - * @type {string} - */ -export const REPORTING_SYSTEM_ID = 'reporting'; - -/** - * The amount of time, in milliseconds, to wait between reports when enabled. - * - * Currently 24 hours. - * @type {Number} - */ -export const REPORT_INTERVAL_MS = 86400000; - -/* - * Key for the localStorage service - */ -export const LOCALSTORAGE_KEY = 'xpack.data'; - -/** - * Link to the Elastic Telemetry privacy statement. - */ -export const PRIVACY_STATEMENT_URL = `https://www.elastic.co/legal/telemetry-privacy-statement`; - -/** - * The type name used within the Monitoring index to publish localization stats. - * @type {string} - */ -export const KIBANA_LOCALIZATION_STATS_TYPE = 'localization'; - -/** - * The header sent by telemetry service when hitting Elasticsearch to identify query source - * @type {string} - */ -export const TELEMETRY_QUERY_SOURCE = 'TELEMETRY'; - -/** - * UI metric usage type - * @type {string} - */ -export const UI_METRIC_USAGE_TYPE = 'ui_metric'; diff --git a/x-pack/legacy/plugins/telemetry/common/get_xpack_config_with_deprecated.ts b/x-pack/legacy/plugins/telemetry/common/get_xpack_config_with_deprecated.ts deleted file mode 100644 index 9fa8ead5c8a09..0000000000000 --- a/x-pack/legacy/plugins/telemetry/common/get_xpack_config_with_deprecated.ts +++ /dev/null @@ -1,15 +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 { KibanaConfig } from 'src/legacy/server/kbn_server'; - -export function getXpackConfigWithDeprecated(config: KibanaConfig, configPath: string) { - const deprecatedConfig = config.get(`xpack.xpack_main.${configPath}`); - if (typeof deprecatedConfig !== 'undefined') { - return deprecatedConfig; - } - return config.get(`xpack.${configPath}`); -} diff --git a/x-pack/legacy/plugins/telemetry/public/components/index.ts b/x-pack/legacy/plugins/telemetry/public/components/index.ts deleted file mode 100644 index 9675bd997b183..0000000000000 --- a/x-pack/legacy/plugins/telemetry/public/components/index.ts +++ /dev/null @@ -1,11 +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. - */ - -// @ts-ignore -export { TelemetryForm } from './telemetry_form'; -export { OptInExampleFlyout } from './opt_in_details_component'; -export { OptInBanner } from './opt_in_banner_component'; -export { OptInMessage } from './opt_in_message'; diff --git a/x-pack/legacy/plugins/telemetry/public/components/opt_in_details_component.test.tsx b/x-pack/legacy/plugins/telemetry/public/components/opt_in_details_component.test.tsx deleted file mode 100644 index c58927c66756b..0000000000000 --- a/x-pack/legacy/plugins/telemetry/public/components/opt_in_details_component.test.tsx +++ /dev/null @@ -1,21 +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 { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { OptInExampleFlyout } from './opt_in_details_component'; - -describe('OptInDetailsComponent', () => { - it('renders as expected', () => { - expect( - shallowWithIntl( - ({ data: [] }))} - onClose={jest.fn()} - /> - ) - ).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/telemetry/public/components/opt_in_details_component.tsx b/x-pack/legacy/plugins/telemetry/public/components/opt_in_details_component.tsx deleted file mode 100644 index 9cfb55af1dab3..0000000000000 --- a/x-pack/legacy/plugins/telemetry/public/components/opt_in_details_component.tsx +++ /dev/null @@ -1,150 +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 * as React from 'react'; - -import { - EuiCallOut, - EuiCodeBlock, - EuiFlexGroup, - EuiFlexItem, - EuiFlyout, - EuiFlyoutHeader, - EuiFlyoutBody, - EuiLoadingSpinner, - EuiPortal, // EuiPortal is a temporary requirement to use EuiFlyout with "ownFocus" - EuiText, - EuiTextColor, - EuiTitle, -} from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -interface Props { - fetchTelemetry: () => Promise; - onClose: () => void; -} - -interface State { - isLoading: boolean; - hasPrivilegeToRead: boolean; - data: any[] | null; -} - -/** - * React component for displaying the example data associated with the Telemetry opt-in banner. - */ -export class OptInExampleFlyout extends React.PureComponent { - public readonly state: State = { - data: null, - isLoading: true, - hasPrivilegeToRead: false, - }; - - componentDidMount() { - this.props - .fetchTelemetry() - .then(response => - this.setState({ - data: Array.isArray(response.data) ? response.data : null, - isLoading: false, - hasPrivilegeToRead: true, - }) - ) - .catch(err => { - this.setState({ - isLoading: false, - hasPrivilegeToRead: err.status !== 403, - }); - }); - } - - renderBody({ data, isLoading, hasPrivilegeToRead }: State) { - if (isLoading) { - return ( - - - - - - ); - } - - if (!hasPrivilegeToRead) { - return ( - - } - color="danger" - iconType="cross" - > - - - ); - } - - if (data === null) { - return ( - - } - color="danger" - iconType="cross" - > - - - ); - } - - return {JSON.stringify(data, null, 2)}; - } - - render() { - return ( - - - - -

- -

-
- - - - - -
- {this.renderBody(this.state)} -
-
- ); - } -} diff --git a/x-pack/legacy/plugins/telemetry/public/components/opt_in_message.tsx b/x-pack/legacy/plugins/telemetry/public/components/opt_in_message.tsx deleted file mode 100644 index 5be20f8de32c1..0000000000000 --- a/x-pack/legacy/plugins/telemetry/public/components/opt_in_message.tsx +++ /dev/null @@ -1,97 +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 * as React from 'react'; -import { EuiLink, EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { getConfigTelemetryDesc, PRIVACY_STATEMENT_URL } from '../../common/constants'; -import { OptInExampleFlyout } from './opt_in_details_component'; - -interface Props { - fetchTelemetry: () => Promise; -} - -interface State { - showDetails: boolean; - showExample: boolean; -} - -export class OptInMessage extends React.PureComponent { - public readonly state: State = { - showDetails: false, - showExample: false, - }; - - toggleShowExample = () => { - this.setState(prevState => ({ - showExample: !prevState.showExample, - })); - }; - - render() { - const { showDetails, showExample } = this.state; - - const getDetails = () => ( - -

- - - - ), - telemetryPrivacyStatementLink: ( - - - - ), - }} - /> -

-
- ); - - const getFlyoutDetails = () => ( - this.setState({ showExample: false })} - fetchTelemetry={this.props.fetchTelemetry} - /> - ); - - const getReadMore = () => ( - this.setState({ showDetails: true })}> - - - ); - - return ( - - -

- {getConfigTelemetryDesc()} {!showDetails && getReadMore()} -

-
- {showDetails && getDetails()} - {showDetails && showExample && getFlyoutDetails()} -
- ); - } -} diff --git a/x-pack/legacy/plugins/telemetry/public/hacks/__tests__/telemetry.js b/x-pack/legacy/plugins/telemetry/public/hacks/__tests__/telemetry.js deleted file mode 100644 index 2c14538d77374..0000000000000 --- a/x-pack/legacy/plugins/telemetry/public/hacks/__tests__/telemetry.js +++ /dev/null @@ -1,15 +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 { uiModules } from 'ui/modules'; - -// This overrides settings for other UI tests -uiModules.get('kibana') - // disable stat reporting while running tests, - // MockInjector used in these tests is not impacted - .constant('telemetryEnabled', false) - .constant('telemetryOptedIn', null) - .constant('telemetryUrl', 'not.a.valid.url.0'); diff --git a/x-pack/legacy/plugins/telemetry/public/hacks/fetch_telemetry.js b/x-pack/legacy/plugins/telemetry/public/hacks/fetch_telemetry.js deleted file mode 100644 index ced23c58566b9..0000000000000 --- a/x-pack/legacy/plugins/telemetry/public/hacks/fetch_telemetry.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import uiChrome from 'ui/chrome'; -import moment from 'moment'; - -/** - * Fetch Telemetry data by calling the Kibana API. - * - * @param {Object} $http The HTTP handler - * @param {String} basePath The base URI - * @param {Function} _moment moment.js, but injectable for tests - * @return {Promise} An array of cluster Telemetry objects. - */ -export function fetchTelemetry($http, { basePath = uiChrome.getBasePath(), _moment = moment, unencrypted = false } = { }) { - return $http.post(`${basePath}/api/telemetry/v2/clusters/_stats`, { - unencrypted, - timeRange: { - min: _moment().subtract(20, 'minutes').toISOString(), - max: _moment().toISOString() - } - }); -} diff --git a/x-pack/legacy/plugins/telemetry/public/hacks/telemetry_init.ts b/x-pack/legacy/plugins/telemetry/public/hacks/telemetry_init.ts deleted file mode 100644 index c44da2b36bf1e..0000000000000 --- a/x-pack/legacy/plugins/telemetry/public/hacks/telemetry_init.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -// @ts-ignore -import { uiModules } from 'ui/modules'; -// @ts-ignore -import { Path } from 'plugins/xpack_main/services/path'; -// @ts-ignore -import { npStart } from 'ui/new_platform'; -// @ts-ignore -import { Telemetry } from './telemetry'; -// @ts-ignore -import { fetchTelemetry } from './fetch_telemetry'; - -function telemetryInit($injector: any) { - const $http = $injector.get('$http'); - - const telemetryEnabled = npStart.core.injectedMetadata.getInjectedVar('telemetryEnabled'); - - if (telemetryEnabled) { - // no telemetry for non-logged in users - if (Path.isUnauthenticated()) { - return; - } - - const sender = new Telemetry($injector, () => fetchTelemetry($http)); - sender.start(); - } -} - -uiModules.get('telemetry/hacks').run(telemetryInit); diff --git a/x-pack/legacy/plugins/telemetry/public/hacks/telemetry_opt_in.js b/x-pack/legacy/plugins/telemetry/public/hacks/telemetry_opt_in.js deleted file mode 100644 index a7c36516d004c..0000000000000 --- a/x-pack/legacy/plugins/telemetry/public/hacks/telemetry_opt_in.js +++ /dev/null @@ -1,11 +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 { uiModules } from 'ui/modules'; - -import { injectBanner } from './welcome_banner'; - -uiModules.get('telemetry/hacks').run(injectBanner); diff --git a/x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/index.js b/x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/index.js deleted file mode 100644 index 2eabe7496b1f2..0000000000000 --- a/x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { injectBanner } from './inject_banner'; diff --git a/x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/render_banner.test.js b/x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/render_banner.test.js deleted file mode 100644 index a55027703f951..0000000000000 --- a/x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/render_banner.test.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { renderBanner } from './render_banner'; - -describe('render_banner', () => { - - it('adds a banner to banners with priority of 10000', () => { - const bannerID = 'brucer-banner'; - - const telemetryOptInProvider = { setBannerId: jest.fn() }; - const banners = { add: jest.fn().mockReturnValue(bannerID) }; - const fetchTelemetry = jest.fn(); - - renderBanner(telemetryOptInProvider, fetchTelemetry, { _banners: banners }); - - expect(banners.add).toBeCalledTimes(1); - expect(fetchTelemetry).toBeCalledTimes(0); - expect(telemetryOptInProvider.setBannerId).toBeCalledWith(bannerID); - - const bannerConfig = banners.add.mock.calls[0][0]; - - expect(bannerConfig.component).not.toBe(undefined); - expect(bannerConfig.priority).toBe(10000); - }); - -}); diff --git a/x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/should_show_banner.js b/x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/should_show_banner.js deleted file mode 100644 index 5685132a95061..0000000000000 --- a/x-pack/legacy/plugins/telemetry/public/hacks/welcome_banner/should_show_banner.js +++ /dev/null @@ -1,20 +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 { handleOldSettings } from './handle_old_settings'; - -/** - * Determine if the banner should be displayed. - * - * This method can have side-effects related to deprecated config settings. - * - * @param {Object} config The advanced settings config object. - * @param {Object} _handleOldSettings handleOldSettings function, but overridable for tests. - * @return {Boolean} {@code true} if the banner should be displayed. {@code false} otherwise. - */ -export async function shouldShowBanner(telemetryOptInProvider, config, { _handleOldSettings = handleOldSettings } = {}) { - return telemetryOptInProvider.getOptIn() === null && await _handleOldSettings(config, telemetryOptInProvider); -} diff --git a/x-pack/legacy/plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js b/x-pack/legacy/plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js deleted file mode 100644 index 63bfcb4b2a327..0000000000000 --- a/x-pack/legacy/plugins/telemetry/public/services/telemetry_opt_in.test.mocks.js +++ /dev/null @@ -1,27 +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 { injectedMetadataServiceMock } from '../../../../../../src/core/public/mocks'; -const injectedMetadataMock = injectedMetadataServiceMock.createStartContract(); - -export function mockInjectedMetadata({ telemetryOptedIn }) { - const mockGetInjectedVar = jest.fn().mockImplementation((key) => { - switch (key) { - case 'telemetryOptedIn': return telemetryOptedIn; - default: throw new Error(`unexpected injectedVar ${key}`); - } - }); - - injectedMetadataMock.getInjectedVar = mockGetInjectedVar; -} - -jest.doMock('ui/new_platform', () => ({ - npStart: { - core: { - injectedMetadata: injectedMetadataMock - }, - }, -})); diff --git a/x-pack/legacy/plugins/telemetry/public/services/telemetry_opt_in.ts b/x-pack/legacy/plugins/telemetry/public/services/telemetry_opt_in.ts deleted file mode 100644 index 9c1bfde1d27b9..0000000000000 --- a/x-pack/legacy/plugins/telemetry/public/services/telemetry_opt_in.ts +++ /dev/null @@ -1,8 +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. - */ - -// @ts-ignore -export { TelemetryOptInProvider } from '../../../../../../src/legacy/core_plugins/kibana/public/home/telemetry_opt_in'; // eslint-disable-line diff --git a/x-pack/legacy/plugins/telemetry/public/views/management/index.js b/x-pack/legacy/plugins/telemetry/public/views/management/index.js deleted file mode 100644 index 0ed6fe09ef80a..0000000000000 --- a/x-pack/legacy/plugins/telemetry/public/views/management/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import './management'; diff --git a/x-pack/legacy/plugins/telemetry/public/views/management/management.js b/x-pack/legacy/plugins/telemetry/public/views/management/management.js deleted file mode 100644 index 9e580f109d536..0000000000000 --- a/x-pack/legacy/plugins/telemetry/public/views/management/management.js +++ /dev/null @@ -1,38 +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 routes from 'ui/routes'; - -import { registerSettingsComponent, PAGE_FOOTER_COMPONENT } from 'ui/management'; -import { TelemetryOptInProvider } from '../../services/telemetry_opt_in'; -import { TelemetryForm } from '../../components'; - -routes.defaults(/\/management/, { - resolve: { - telemetryManagementSection: function (Private, spacesEnabled, activeSpace) { - const telemetryOptInProvider = Private(TelemetryOptInProvider); - - const spaceProps = { - spacesEnabled, - }; - - if (spacesEnabled) { - spaceProps.activeSpace = activeSpace ? activeSpace.space : null; - } - - const Component = (props) => ( - - ); - - registerSettingsComponent(PAGE_FOOTER_COMPONENT, Component, true); - } - } -}); diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/encryption/encrypt.ts b/x-pack/legacy/plugins/telemetry/server/collectors/encryption/encrypt.ts deleted file mode 100644 index 8907292df8445..0000000000000 --- a/x-pack/legacy/plugins/telemetry/server/collectors/encryption/encrypt.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { createRequestEncryptor } from '@elastic/request-crypto'; -import { telemetryJWKS } from './telemetry_jwks'; - -export function getKID(isProd = false): string { - return isProd ? 'kibana' : 'kibana_dev'; -} - -export async function encryptTelemetry(payload: any, isProd = false): Promise { - const kid = getKID(isProd); - const encryptor = await createRequestEncryptor(telemetryJWKS); - const clusters = [].concat(payload); - return Promise.all(clusters.map((cluster: any) => encryptor.encrypt(kid, cluster))); -} diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/index.ts b/x-pack/legacy/plugins/telemetry/server/collectors/index.ts deleted file mode 100644 index c9b94a8ea5d5e..0000000000000 --- a/x-pack/legacy/plugins/telemetry/server/collectors/index.ts +++ /dev/null @@ -1,15 +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. - */ - -// @ts-ignore -export { getAllStats } from './monitoring'; -// @ts-ignore -export { getLocalStats } from './local'; -export { getStats } from './get_stats'; -export { encryptTelemetry } from './encryption'; -export { createTelemetryUsageCollector } from './usage'; -export { createUiMetricUsageCollector } from './ui_metric'; -export { createLocalizationUsageCollector } from './localization'; diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/local/__tests__/get_cluster_info.js b/x-pack/legacy/plugins/telemetry/server/collectors/local/__tests__/get_cluster_info.js deleted file mode 100644 index dfe7998718a9e..0000000000000 --- a/x-pack/legacy/plugins/telemetry/server/collectors/local/__tests__/get_cluster_info.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -import { getClusterInfo } from '../get_cluster_info'; - -export function mockGetClusterInfo(callCluster, clusterInfo, req) { - callCluster.withArgs(req, 'info').returns(clusterInfo); - callCluster.withArgs('info').returns(clusterInfo); -} - -describe('get_cluster_info', () => { - - it('uses callCluster to get info API', () => { - const callCluster = sinon.stub(); - const response = Promise.resolve({}); - - mockGetClusterInfo(callCluster, response); - - expect(getClusterInfo(callCluster)).to.be(response); - }); - -}); diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/local/__tests__/get_cluster_stats.js b/x-pack/legacy/plugins/telemetry/server/collectors/local/__tests__/get_cluster_stats.js deleted file mode 100644 index 29e7e620e3313..0000000000000 --- a/x-pack/legacy/plugins/telemetry/server/collectors/local/__tests__/get_cluster_stats.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -import { TIMEOUT } from '../constants'; -import { getClusterStats } from '../get_cluster_stats'; - -export function mockGetClusterStats(callCluster, clusterStats, req) { - callCluster.withArgs(req, 'cluster.stats', { - timeout: TIMEOUT - }) - .returns(clusterStats); - - callCluster.withArgs('cluster.stats', { - timeout: TIMEOUT - }) - .returns(clusterStats); -} - -describe('get_cluster_stats', () => { - - it('uses callCluster to get cluster.stats API', () => { - const callCluster = sinon.stub(); - const response = Promise.resolve({}); - - mockGetClusterStats(callCluster, response); - - expect(getClusterStats(callCluster)).to.be(response); - }); - -}); diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/local/get_cluster_info.js b/x-pack/legacy/plugins/telemetry/server/collectors/local/get_cluster_info.js deleted file mode 100644 index 550af3692b97f..0000000000000 --- a/x-pack/legacy/plugins/telemetry/server/collectors/local/get_cluster_info.js +++ /dev/null @@ -1,17 +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. - */ - -/** - * Get the cluster info from the connected cluster. - * - * This is the equivalent to GET / - * - * @param {function} callCluster The callWithInternalUser handler (exposed for testing) - * @return {Promise} The response from Elasticsearch. - */ -export function getClusterInfo(callCluster) { - return callCluster('info'); -} diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/local/get_cluster_stats.js b/x-pack/legacy/plugins/telemetry/server/collectors/local/get_cluster_stats.js deleted file mode 100644 index 34152f8fea4e7..0000000000000 --- a/x-pack/legacy/plugins/telemetry/server/collectors/local/get_cluster_stats.js +++ /dev/null @@ -1,21 +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 { TIMEOUT } from './constants'; - -/** - * Get the cluster stats from the connected cluster. - * - * This is the equivalent to GET /_cluster/stats?timeout=30s. - * - * @param {function} callCluster The callWithInternalUser handler (exposed for testing) - * @return {Promise} The response from Elasticsearch equivalent to GET /_cluster/stats. - */ -export function getClusterStats(callCluster) { - return callCluster('cluster.stats', { - timeout: TIMEOUT - }); -} diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/local/get_kibana.js b/x-pack/legacy/plugins/telemetry/server/collectors/local/get_kibana.js deleted file mode 100644 index 50e68e2b74d68..0000000000000 --- a/x-pack/legacy/plugins/telemetry/server/collectors/local/get_kibana.js +++ /dev/null @@ -1,49 +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 { get, omit } from 'lodash'; - -export function handleKibanaStats(server, response) { - if (!response) { - server.log(['warning', 'telemetry', 'local-stats'], 'No Kibana stats returned from usage collectors'); - return; - } - - const { kibana, kibana_stats: stats, ...plugins } = response; - - const platform = get(stats, 'os.platform', 'unknown'); - const platformRelease = get(stats, 'os.platformRelease', 'unknown'); - - let version; - const { kbnServer } = get(server, 'plugins.xpack_main.status.plugin'); - if (kbnServer) { - version = kbnServer.version.replace(/-snapshot/i, ''); - } - - // combine core stats (os types, saved objects) with plugin usage stats - // organize the object into the same format as monitoring-enabled telemetry - return { - ...omit(kibana, 'index'), // discard index - count: 1, - indices: 1, - os: { - platforms: [{ platform, count: 1 }], - platformReleases: [{ platformRelease, count: 1 }], - }, - versions: [{ version, count: 1 }], - plugins, - }; -} - -/* - * Check user privileges for read access to monitoring - * Pass callWithInternalUser to bulkFetchUsage - */ -export async function getKibana(server, callWithInternalUser) { - const { collectorSet } = server.usage; - const usage = await collectorSet.bulkFetch(callWithInternalUser); - return collectorSet.toObject(usage); -} diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/local/index.js b/x-pack/legacy/plugins/telemetry/server/collectors/local/index.js deleted file mode 100644 index b78fca22c1af7..0000000000000 --- a/x-pack/legacy/plugins/telemetry/server/collectors/local/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { getLocalStats } from './get_local_stats'; diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/localization/file_integrity.test.mocks.ts b/x-pack/legacy/plugins/telemetry/server/collectors/localization/file_integrity.test.mocks.ts deleted file mode 100644 index 36f76516e188b..0000000000000 --- a/x-pack/legacy/plugins/telemetry/server/collectors/localization/file_integrity.test.mocks.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Readable } from 'stream'; - -jest.doMock('fs', () => ({ - createReadStream(filepath: string): Readable { - if (filepath === 'ERROR') { - throw new Error('MOCK ERROR - Invalid Path'); - } - const readableStream = new Readable(); - const streamData = filepath.split(''); - let cursor = 0; - - readableStream._read = function(size) { - const current = streamData[cursor++]; - if (typeof current === 'undefined') { - return this.push(null); - } - this.push(current); - }; - - return readableStream; - }, -})); diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/localization/index.ts b/x-pack/legacy/plugins/telemetry/server/collectors/localization/index.ts deleted file mode 100644 index dd8a3a9a10400..0000000000000 --- a/x-pack/legacy/plugins/telemetry/server/collectors/localization/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { createLocalizationUsageCollector } from './telemetry_localization_collector'; diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/monitoring/index.js b/x-pack/legacy/plugins/telemetry/server/collectors/monitoring/index.js deleted file mode 100644 index 4500385367d1d..0000000000000 --- a/x-pack/legacy/plugins/telemetry/server/collectors/monitoring/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { getAllStats } from './get_all_stats'; diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/ui_metric/index.ts b/x-pack/legacy/plugins/telemetry/server/collectors/ui_metric/index.ts deleted file mode 100644 index f5a49587d49c8..0000000000000 --- a/x-pack/legacy/plugins/telemetry/server/collectors/ui_metric/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { createUiMetricUsageCollector } from './telemetry_ui_metric_collector'; diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/usage/index.ts b/x-pack/legacy/plugins/telemetry/server/collectors/usage/index.ts deleted file mode 100644 index 960fed00152b4..0000000000000 --- a/x-pack/legacy/plugins/telemetry/server/collectors/usage/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { createTelemetryUsageCollector } from './telemetry_usage_collector'; diff --git a/x-pack/legacy/plugins/telemetry/server/index.ts b/x-pack/legacy/plugins/telemetry/server/index.ts deleted file mode 100644 index 5748106be8b29..0000000000000 --- a/x-pack/legacy/plugins/telemetry/server/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { PluginInitializerContext } from 'src/core/server'; -import { TelemetryPlugin } from './plugin'; -import * as constants from '../common/constants'; - -export { getTelemetryOptIn } from './get_telemetry_opt_in'; -export const telemetryPlugin = (initializerContext: PluginInitializerContext) => - new TelemetryPlugin(); -export { constants }; diff --git a/x-pack/legacy/plugins/telemetry/server/plugin.ts b/x-pack/legacy/plugins/telemetry/server/plugin.ts deleted file mode 100644 index 13aeb774d4a68..0000000000000 --- a/x-pack/legacy/plugins/telemetry/server/plugin.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { CoreSetup } from 'src/core/server'; -import { registerRoutes } from './routes'; - -export class TelemetryPlugin { - public setup(core: CoreSetup) { - registerRoutes(core); - } -} diff --git a/x-pack/legacy/plugins/telemetry/server/routes/index.ts b/x-pack/legacy/plugins/telemetry/server/routes/index.ts deleted file mode 100644 index f6880b644699c..0000000000000 --- a/x-pack/legacy/plugins/telemetry/server/routes/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { CoreSetup } from 'src/core/server'; -import { registerOptInRoutes } from './opt_in'; -import { registerTelemetryDataRoutes } from './telemetry_stats'; - -export function registerRoutes(core: CoreSetup) { - registerOptInRoutes(core); - registerTelemetryDataRoutes(core); -} diff --git a/x-pack/legacy/plugins/xpack_main/index.js b/x-pack/legacy/plugins/xpack_main/index.js index e589caa2819e6..9af4448a8d615 100644 --- a/x-pack/legacy/plugins/xpack_main/index.js +++ b/x-pack/legacy/plugins/xpack_main/index.js @@ -9,7 +9,6 @@ import dedent from 'dedent'; import { XPACK_INFO_API_DEFAULT_POLL_FREQUENCY_IN_MILLIS } from '../../server/lib/constants'; -import { getXpackConfigWithDeprecated } from '../telemetry/common/get_xpack_config_with_deprecated'; import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; import { replaceInjectedVars } from './server/lib/replace_injected_vars'; import { setupXPackMain } from './server/lib/setup_xpack_main'; @@ -17,15 +16,10 @@ import { xpackInfoRoute, settingsRoute } from './server/routes/api/v1'; import { has } from 'lodash'; -function movedToTelemetry(configPath) { - return (settings, log) => { - if (has(settings, configPath)) { - log(`Config key ${configPath} is deprecated. Use "xpack.telemetry.${configPath}" instead.`); - } - }; -} - export { callClusterFactory } from './server/lib/call_cluster_factory'; +import { getStatsWithXpack } from './server/telemetry_collection'; +import { telemetryCollectionManager } from '../../../../src/legacy/core_plugins/telemetry/server'; + export const xpackMain = (kibana) => { return new kibana.Plugin({ id: 'xpack_main', @@ -62,7 +56,6 @@ export const xpackMain = (kibana) => { const config = server.config(); return { - telemetryEnabled: getXpackConfigWithDeprecated(config, 'telemetry.enabled'), activeSpace: null, spacesEnabled: config.get('xpack.spaces.enabled'), }; @@ -86,6 +79,8 @@ export const xpackMain = (kibana) => { mirrorPluginStatus(server.plugins.elasticsearch, this, 'yellow', 'red'); + telemetryCollectionManager.setStatsGetter(getStatsWithXpack, 'local_xpack', 1); + featuresPlugin.registerLegacyAPI({ xpackInfo: setupXPackMain(server), savedObjectTypes: server.savedObjects.types @@ -95,10 +90,19 @@ export const xpackMain = (kibana) => { xpackInfoRoute(server); settingsRoute(server, this.kbnServer); }, - deprecations: () => [ - movedToTelemetry('telemetry.config'), - movedToTelemetry('telemetry.url'), - movedToTelemetry('telemetry.enabled'), - ], + deprecations: () => { + function movedToTelemetry(configPath) { + return (settings, log) => { + if (has(settings, configPath)) { + log(`Config key "xpack.xpack_main.${configPath}" is deprecated. Use "telemetry.${configPath}" instead.`); + } + }; + } + return [ + movedToTelemetry('telemetry.config'), + movedToTelemetry('telemetry.url'), + movedToTelemetry('telemetry.enabled'), + ]; + }, }); }; diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js index c926170a2a4de..f094f588cf858 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js +++ b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js @@ -10,13 +10,8 @@ import expect from '@kbn/expect'; import { replaceInjectedVars } from '../replace_injected_vars'; import { KibanaRequest } from '../../../../../../../src/core/server'; -const buildRequest = (telemetryOptedIn = null, path = '/app/kibana') => { +const buildRequest = (path = '/app/kibana') => { const get = sinon.stub(); - if (telemetryOptedIn === null) { - get.withArgs('telemetry', 'telemetry').rejects(new Error('not found exception')); - } else { - get.withArgs('telemetry', 'telemetry').resolves({ attributes: { enabled: telemetryOptedIn } }); - } return { path, @@ -50,7 +45,6 @@ describe('replaceInjectedVars uiExport', () => { const newVars = await replaceInjectedVars(originalInjectedVars, request, server); expect(newVars).to.eql({ a: 1, - telemetryOptedIn: null, xpackInitialInfo: { b: 1 }, @@ -73,7 +67,6 @@ describe('replaceInjectedVars uiExport', () => { const newVars = await replaceInjectedVars(originalInjectedVars, request, server); expect(newVars).to.eql({ a: 1, - telemetryOptedIn: null, xpackInitialInfo: { b: 1 }, @@ -89,7 +82,6 @@ describe('replaceInjectedVars uiExport', () => { const newVars = await replaceInjectedVars(originalInjectedVars, request, server); expect(newVars).to.eql({ a: 1, - telemetryOptedIn: null, xpackInitialInfo: { b: 1 }, @@ -98,14 +90,13 @@ describe('replaceInjectedVars uiExport', () => { it('respects the telemetry opt-in document when opted-out', async () => { const originalInjectedVars = { a: 1 }; - const request = buildRequest(false); + const request = buildRequest(); const server = mockServer(); server.plugins.xpack_main.info.license.isOneOf.returns(true); const newVars = await replaceInjectedVars(originalInjectedVars, request, server); expect(newVars).to.eql({ a: 1, - telemetryOptedIn: false, xpackInitialInfo: { b: 1 }, @@ -114,14 +105,13 @@ describe('replaceInjectedVars uiExport', () => { it('respects the telemetry opt-in document when opted-in', async () => { const originalInjectedVars = { a: 1 }; - const request = buildRequest(true); + const request = buildRequest(); const server = mockServer(); server.plugins.xpack_main.info.license.isOneOf.returns(true); const newVars = await replaceInjectedVars(originalInjectedVars, request, server); expect(newVars).to.eql({ a: 1, - telemetryOptedIn: true, xpackInitialInfo: { b: 1 }, @@ -137,7 +127,6 @@ describe('replaceInjectedVars uiExport', () => { const newVars = await replaceInjectedVars(originalInjectedVars, request, server); expect(newVars).to.eql({ a: 1, - telemetryOptedIn: false, xpackInitialInfo: { b: 1 }, @@ -174,7 +163,6 @@ describe('replaceInjectedVars uiExport', () => { const newVars = await replaceInjectedVars(originalInjectedVars, request, server); expect(newVars).to.eql({ a: 1, - telemetryOptedIn: null, xpackInitialInfo: undefined, uiCapabilities: { navLinks: { foo: true }, diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/replace_injected_vars.js b/x-pack/legacy/plugins/xpack_main/server/lib/replace_injected_vars.js index 9def7da5e7e4f..cd111240261c4 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/replace_injected_vars.js +++ b/x-pack/legacy/plugins/xpack_main/server/lib/replace_injected_vars.js @@ -5,14 +5,12 @@ */ import { KibanaRequest } from '../../../../../../src/core/server'; -import { getTelemetryOptIn } from '../../../telemetry/server'; export async function replaceInjectedVars(originalInjectedVars, request, server) { const xpackInfo = server.plugins.xpack_main.info; const withXpackInfo = async () => ({ ...originalInjectedVars, - telemetryOptedIn: await getTelemetryOptIn(request), xpackInitialInfo: xpackInfo.isAvailable() ? xpackInfo.toJSON() : undefined, }); diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/local/__tests__/get_xpack.js b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/__tests__/get_xpack.js similarity index 100% rename from x-pack/legacy/plugins/telemetry/server/collectors/local/__tests__/get_xpack.js rename to x-pack/legacy/plugins/xpack_main/server/telemetry_collection/__tests__/get_xpack.js diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/local/constants.js b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/constants.ts similarity index 100% rename from x-pack/legacy/plugins/telemetry/server/collectors/local/constants.js rename to x-pack/legacy/plugins/xpack_main/server/telemetry_collection/constants.ts diff --git a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_stats_with_xpack.ts b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_stats_with_xpack.ts new file mode 100644 index 0000000000000..f19695ca06525 --- /dev/null +++ b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_stats_with_xpack.ts @@ -0,0 +1,42 @@ +/* + * 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. + */ + +// @ts-ignore +import { getXPack } from './get_xpack'; +import { getLocalStats } from '../../../../../../src/legacy/core_plugins/telemetry/server/telemetry_collection'; + +/** + * Get the telemetry data. + * + * @param {Object} req The incoming request. + * @param {Object} config Kibana config. + * @param {String} start The start time of the request (likely 20m ago). + * @param {String} end The end time of the request. + * @param {Boolean} unencrypted Is the request payload going to be unencrypted. + * @return {Promise} An array of telemetry objects. + */ +export async function getStatsWithXpack( + req: any, + config: any, + start: string, + end: string, + unencrypted: boolean +) { + const useInternalUser = !unencrypted; + const { server } = req; + const { callWithRequest, callWithInternalUser } = server.plugins.elasticsearch.getCluster('data'); + const callCluster = useInternalUser + ? callWithInternalUser + : (...args: any[]) => callWithRequest(req, ...args); + + const localStats = await getLocalStats(req, { useInternalUser }); + const { license, xpack } = await getXPack(callCluster); + + localStats.license = license; + localStats.stack_stats.xpack = xpack; + + return [localStats]; +} diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/local/get_xpack.js b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_xpack.js similarity index 99% rename from x-pack/legacy/plugins/telemetry/server/collectors/local/get_xpack.js rename to x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_xpack.js index 3909741c8c556..07d635ef52e22 100644 --- a/x-pack/legacy/plugins/telemetry/server/collectors/local/get_xpack.js +++ b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/get_xpack.js @@ -83,4 +83,3 @@ export function getXPack(callCluster) { }) .catch(() => { return {}; }); } - diff --git a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/csp/index.ts b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/index.ts similarity index 80% rename from x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/csp/index.ts rename to x-pack/legacy/plugins/xpack_main/server/telemetry_collection/index.ts index a7c1088f9961b..553f8dc0c4188 100644 --- a/x-pack/legacy/plugins/oss_telemetry/server/lib/collectors/csp/index.ts +++ b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { registerCspCollector } from './csp_collector'; +export { getStatsWithXpack } from './get_stats_with_xpack'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f3253132604c7..59ac561065fa0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2464,8 +2464,6 @@ "kbn.visualize.wizard.step1Breadcrumb": "作成", "kbn.visualize.wizard.step2Breadcrumb": "作成", "kbn.visualizeTitle": "可視化", - "kbn.home.telemetry.optInErrorToastText": "使用状況統計設定の設定中にエラーが発生しました。", - "kbn.home.telemetry.optInErrorToastTitle": "エラー", "kbn.advancedSettings.discover.searchOnPageLoadText": "ディスカバリの最初の読み込み時に検索を実行するかを制御します。この設定は、保存された検索の読み込み時には影響しません。", "kbn.advancedSettings.discover.searchOnPageLoadTitle": "ページの読み込み時の検索", "kbn.advancedSettings.visualization.heatmap.maxBucketsText": "1 つのデータソースが返せるバケットの最大数です。値が大きいとブラウザのレンダリング速度が下がる可能性があります。", @@ -2480,17 +2478,6 @@ "kbn.home.addData.siem.addSiemEventsButtonLabel": "セキュリティイベントを追加", "kbn.home.addData.siem.nameDescription": "即利用可能なビジュアライゼーションで、セキュリティイベントをまとめてインタラクティブな調査を可能にします。", "kbn.home.addData.siem.nameTitle": "SIEM", - "kbn.home.telemetry.callout.clusterStatisticsDescription": "これは収集される基本的なクラスター統計の例です。インデックス、チャート、ノードの数が含まれます。監視がオンになっているかどうかなどのハイレベルの使用統計も含まれます。", - "kbn.home.telemetry.callout.clusterStatisticsTitle": "クラスター統計", - "kbn.home.telemetry.callout.errorLoadingClusterStatisticsDescription": "クラスター統計の取得中に予期せぬエラーが発生しました。Elasticsearch、Kibana、またはネットワークのエラーが原因の可能性があります。Kibana を確認し、ページを再読み込みして再試行してください。", - "kbn.home.telemetry.callout.errorLoadingClusterStatisticsTitle": "クラスター統計の読み込みエラー", - "kbn.home.telemetry.callout.errorUnprivilegedUserDescription": "暗号化されていないクラスター統計を表示するアクセス権がありません。", - "kbn.home.telemetry.callout.errorUnprivilegedUserTitle": "クラスター統計の表示エラー", - "kbn.home.telemetry.optInMessage.detailsDescription": "ユーザーが処理したり保管したりするデータに関する情報は一切送信されません。この機能は定期的に基本的な機能利用に関する統計情報を送信します。{exampleLink} をご覧いただくか、{telemetryPrivacyStatementLink} をお読みください。この機能はいつでも無効にできます。", - "kbn.home.telemetry.optInMessage.detailsExampleLinkText": "例", - "kbn.home.telemetry.optInMessage.detailsTelemetryPrivacyStatementLinkText": "遠隔測定に関するプライバシーステートメント", - "kbn.home.telemetry.optInMessage.readMoreLinkText": "さらに詳しく", - "kbn.home.telemetry.optInMessageDescription": "基本的な機能利用に関する統計情報を提供して Elastic Stack の改善にご協力ください。このデータは Elastic 社外と共有されません。", "kbn.home.telemtery.optInCardConfirmButtonLabel": "はい", "kbn.home.telemtery.optInCardDeclineButtonLabel": "いいえ", "kbn.home.telemtery.optInCardTitle": "Elastic Stack の改善にご協力ください", @@ -10127,66 +10114,28 @@ "xpack.spaces.spaceSelector.noSpacesMatchSearchCriteriaDescription": "検索条件に一致するスペースがありません", "xpack.spaces.spaceSelector.selectSpacesTitle": "スペースの選択", "xpack.spaces.spacesTitle": "スペース", - "xpack.spaces.management.copyToSpace.actionDescription": "この保存されたオブジェクトを 1 つまたは複数のスペースにコピーします。", - "xpack.spaces.management.copyToSpace.actionTitle": "スペースにコピー", - "xpack.spaces.management.copyToSpace.automaticallyOverwrite": "すべての保存されたオブジェクトを自動的に上書き", - "xpack.spaces.management.copyToSpace.copyDetail.overwriteButton": "上書き", - "xpack.spaces.management.copyToSpace.copyDetail.skipOverwriteButton": "スキップ", - "xpack.spaces.management.copyToSpace.copyErrorTitle": "保存されたオブジェクトのコピー中にエラーが発生", - "xpack.spaces.management.copyToSpace.copyResultsLabel": "コピー結果", - "xpack.spaces.management.copyToSpace.copyStatus.conflictsMessage": "このスペースには同じ ID 「{id}」の保存されたオブジェクトが既に存在します。", - "xpack.spaces.management.copyToSpace.copyStatus.conflictsOverwriteMessage": "「上書き」をクリックしてこのバージョンをコピーされたバージョンに置き換えます。", - "xpack.spaces.management.copyToSpace.copyStatus.pendingOverwriteMessage": "保存されたオブジェクトは上書きされます。「スキップ」をクリックしてこの操作をキャンセルします。", - "xpack.spaces.management.copyToSpace.copyStatus.successMessage": "保存されたオブジェクトがコピーされました。", - "xpack.spaces.management.copyToSpace.copyStatus.unresolvableErrorMessage": "この保存されたオブジェクトのコピー中にエラーが発生しました。", - "xpack.spaces.management.copyToSpace.copyStatusSummary.conflictsMessage": "{space} スペースに 1 つまたは複数の矛盾が検出されました。解決するにはこのセクションを拡張してください。", - "xpack.spaces.management.copyToSpace.copyStatusSummary.failedMessage": "{space} スペースへのコピーに失敗しました。詳細はこのセクションを拡張してください。", - "xpack.spaces.management.copyToSpace.copyStatusSummary.successMessage": "{space} スペースにコピーされました。", - "xpack.spaces.management.copyToSpace.copyToSpacesButton": "{spaceCount} 個の{spaceCount, plural, one {スペース} other {スペース}}にコピー", - "xpack.spaces.management.copyToSpace.disabledCopyToSpacesButton": "コピー", - "xpack.spaces.management.copyToSpace.dontIncludeRelatedLabel": "関連性のある保存されたオブジェクトを含みません", - "xpack.spaces.management.copyToSpace.dontOverwriteLabel": "保存されたオブジェクトを上書きしません", - "xpack.spaces.management.copyToSpace.finishCopyToSpacesButton": "終了", - "xpack.spaces.management.copyToSpace.finishedButtonLabel": "コピーが完了しました。", - "xpack.spaces.management.copyToSpace.finishPendingOverwritesCopyToSpacesButton": "{overwriteCount} 件のオブジェクトを上書き", - "xpack.spaces.management.copyToSpace.includeRelatedFormLabel": "関連性のある保存されたオブジェクトを含みます", - "xpack.spaces.management.copyToSpace.includeRelatedLabel": "関連性のある保存されたオブジェクトを含みます", - "xpack.spaces.management.copyToSpace.inProgressButtonLabel": "コピーが進行中です。お待ちください。", - "xpack.spaces.management.copyToSpace.noSpacesBody": "コピーできるスペースがありません。", - "xpack.spaces.management.copyToSpace.noSpacesTitle": "スペースがありません", - "xpack.spaces.management.copyToSpace.overwriteLabel": "保存されたオブジェクトを自動的に上書きしています", - "xpack.spaces.management.copyToSpace.resolveCopyErrorTitle": "保存されたオブジェクトの矛盾の解決中にエラーが発生", - "xpack.spaces.management.copyToSpace.resolveCopySuccessTitle": "上書き成功", - "xpack.spaces.management.copyToSpace.selectSpacesLabel": "コピー先のスペースを選択してください", - "xpack.spaces.management.copyToSpace.spacesLoadErrorTitle": "利用可能なスペースを読み込み中にエラーが発生", - "xpack.spaces.management.copyToSpaceFlyoutFooter.conflictCount": "スキップ", - "xpack.spaces.management.copyToSpaceFlyoutFooter.errorCount": "エラー", - "xpack.spaces.management.copyToSpaceFlyoutFooter.pendingCount": "保留中", - "xpack.spaces.management.copyToSpaceFlyoutFooter.successCount": "コピー完了", - "xpack.spaces.management.copyToSpaceFlyoutHeader": "保存されたオブジェクトのスペースへのコピー", - "xpack.telemetry.callout.appliesSettingTitle": "この設定は {allOfKibanaText} に適用されます", - "xpack.telemetry.callout.appliesSettingTitle.allOfKibanaText": "Kibana のすべて", - "xpack.telemetry.callout.clusterStatisticsDescription": "これは収集される基本的なクラスター統計の例です。インデックス、チャート、ノードの数が含まれます。監視がオンになっているかどうかなどのハイレベルの使用統計も含まれます。", - "xpack.telemetry.callout.clusterStatisticsTitle": "クラスター統計", - "xpack.telemetry.callout.errorLoadingClusterStatisticsDescription": "クラスター統計の取得中に予期せぬエラーが発生しました。Elasticsearch、Kibana、またはネットワークのエラーが原因の可能性があります。Kibana を確認し、ページを再読み込みして再試行してください。", - "xpack.telemetry.callout.errorLoadingClusterStatisticsTitle": "クラスター統計の読み込みエラー", - "xpack.telemetry.callout.errorUnprivilegedUserDescription": "暗号化されていないクラスター統計を表示するアクセス権がありません。", - "xpack.telemetry.callout.errorUnprivilegedUserTitle": "クラスター統計の表示エラー", - "xpack.telemetry.readOurUsageDataPrivacyStatementLinkText": "使用データのプライバシーステートメントをお読みください", - "xpack.telemetry.seeExampleOfWhatWeCollectLinkText": "収集されるデータの例を見る", - "xpack.telemetry.telemetryConfigDescription": "基本的な機能利用に関する統計情報を提供して Elastic Stack の改善にご協力ください。このデータは Elastic 社外と共有されません。", - "xpack.telemetry.telemetryConfigTitle": "遠隔測定オプトイン", - "xpack.telemetry.telemetryErrorNotificationMessageDescription.tryAgainText": "Kibana と Elasticsearch が現在も実行中であることを確認し、再試行してください。", - "xpack.telemetry.telemetryErrorNotificationMessageDescription.unableToSaveTelemetryPreferenceText": "遠隔測定設定を保存できません。", - "xpack.telemetry.telemetryErrorNotificationMessageTitle": "遠隔測定エラー", - "xpack.telemetry.usageDataTitle": "使用データ", - "xpack.telemetry.welcomeBanner.noButtonLabel": "いいえ", - "xpack.telemetry.welcomeBanner.telemetryConfigDescription.readMoreLinkText": "続きを読む", - "xpack.telemetry.welcomeBanner.telemetryConfigDetailsDescription": "ユーザーが処理したり保管したりするデータに関する情報は一切送信されません。この機能は定期的に基本的な機能利用に関する統計情報を送信します。{exampleLink} をご覧いただくか、{telemetryPrivacyStatementLink} をお読みください。この機能はいつでも無効にできます。", - "xpack.telemetry.welcomeBanner.telemetryConfigDetailsDescription.exampleLinkText": "例", - "xpack.telemetry.welcomeBanner.telemetryConfigDetailsDescription.telemetryPrivacyStatementLinkText": "遠隔測定に関するプライバシーステートメント", - "xpack.telemetry.welcomeBanner.yesButtonLabel": "はい", - "xpack.telemetry.welcomeBanner.title": "Elastic Stack の改善にご協力ください!", + "telemetry.callout.appliesSettingTitle": "この設定は {allOfKibanaText} に適用されます", + "telemetry.callout.appliesSettingTitle.allOfKibanaText": "Kibana のすべて", + "telemetry.callout.clusterStatisticsDescription": "これは収集される基本的なクラスター統計の例です。インデックス、チャート、ノードの数が含まれます。監視がオンになっているかどうかなどのハイレベルの使用統計も含まれます。", + "telemetry.callout.clusterStatisticsTitle": "クラスター統計", + "telemetry.callout.errorLoadingClusterStatisticsDescription": "クラスター統計の取得中に予期せぬエラーが発生しました。Elasticsearch、Kibana、またはネットワークのエラーが原因の可能性があります。Kibana を確認し、ページを再読み込みして再試行してください。", + "telemetry.callout.errorLoadingClusterStatisticsTitle": "クラスター統計の読み込みエラー", + "telemetry.callout.errorUnprivilegedUserDescription": "暗号化されていないクラスター統計を表示するアクセス権がありません。", + "telemetry.callout.errorUnprivilegedUserTitle": "クラスター統計の表示エラー", + "telemetry.readOurUsageDataPrivacyStatementLinkText": "使用データのプライバシーステートメントをお読みください", + "telemetry.seeExampleOfWhatWeCollectLinkText": "収集されるデータの例を見る", + "telemetry.telemetryConfigDescription": "基本的な機能利用に関する統計情報を提供して Elastic Stack の改善にご協力ください。このデータは Elastic 社外と共有されません。", + "telemetry.telemetryConfigTitle": "遠隔測定オプトイン", + "telemetry.telemetryErrorNotificationMessageDescription.tryAgainText": "Kibana と Elasticsearch が現在も実行中であることを確認し、再試行してください。", + "telemetry.telemetryErrorNotificationMessageDescription.unableToSaveTelemetryPreferenceText": "遠隔測定設定を保存できません。", + "telemetry.telemetryErrorNotificationMessageTitle": "遠隔測定エラー", + "telemetry.usageDataTitle": "使用データ", + "telemetry.welcomeBanner.noButtonLabel": "いいえ", + "telemetry.welcomeBanner.telemetryConfigDescription.readMoreLinkText": "続きを読む", + "telemetry.welcomeBanner.telemetryConfigDetailsDescription": "ユーザーが処理したり保管したりするデータに関する情報は一切送信されません。この機能は定期的に基本的な機能利用に関する統計情報を送信します。{exampleLink} をご覧いただくか、{telemetryPrivacyStatementLink} をお読みください。この機能はいつでも無効にできます。", + "telemetry.welcomeBanner.telemetryConfigDetailsDescription.exampleLinkText": "例", + "telemetry.welcomeBanner.telemetryConfigDetailsDescription.telemetryPrivacyStatementLinkText": "遠隔測定に関するプライバシーステートメント", + "telemetry.welcomeBanner.yesButtonLabel": "はい", "xpack.upgradeAssistant.appTitle": "{version} アップグレードアシスタント", "xpack.upgradeAssistant.checkupTab.backUpCallout.calloutBody.calloutDetail": "{snapshotRestoreDocsButton} でデータをバックアップします。", "xpack.upgradeAssistant.checkupTab.backUpCallout.calloutBody.snapshotRestoreDocsButtonLabel": "API のスナップショットと復元", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4e8a81028aa88..cf40c5cb6e973 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2465,8 +2465,6 @@ "kbn.visualize.wizard.step1Breadcrumb": "创建", "kbn.visualize.wizard.step2Breadcrumb": "创建", "kbn.visualizeTitle": "可视化", - "kbn.home.telemetry.optInErrorToastText": "尝试设置使用统计信息首选项时发生错误。", - "kbn.home.telemetry.optInErrorToastTitle": "错误", "kbn.advancedSettings.discover.searchOnPageLoadText": "控制在 Discover 首次加载时是否执行搜索。加载已保存搜索时,此设置无效。", "kbn.advancedSettings.discover.searchOnPageLoadTitle": "在页面加载时搜索", "kbn.advancedSettings.visualization.heatmap.maxBucketsText": "单个数据源可以返回的最大存储桶数目。较高的数目可能对浏览器呈现性能有负面影响", @@ -2481,17 +2479,6 @@ "kbn.home.addData.siem.addSiemEventsButtonLabel": "添加安全事件", "kbn.home.addData.siem.nameDescription": "集中安全事件,以通过即用型可视化实现交互式调查。", "kbn.home.addData.siem.nameTitle": "SIEM", - "kbn.home.telemetry.callout.clusterStatisticsDescription": "这是我们将收集的基本集群统计信息的示例。其包括索引、分片和节点的数目。还包括高级使用情况统计信息,例如监测是否打开。", - "kbn.home.telemetry.callout.clusterStatisticsTitle": "集群统计信息", - "kbn.home.telemetry.callout.errorLoadingClusterStatisticsDescription": "尝试提取集群统计信息时发生意外错误。发生此问题的原因可能是 Elasticsearch 出故障、Kibana 出故障,或者有网络错误。检查 Kibana,然后重新加载页面并重试。", - "kbn.home.telemetry.callout.errorLoadingClusterStatisticsTitle": "加载集群统计信息时出错", - "kbn.home.telemetry.callout.errorUnprivilegedUserDescription": "您无权查看未加密的集群统计信息。", - "kbn.home.telemetry.callout.errorUnprivilegedUserTitle": "显示集群统计信息时出错", - "kbn.home.telemetry.optInMessage.detailsDescription": "不会发送有关您处理或存储的数据的信息。此功能将定期发送基本功能使用情况统计信息。请参阅{exampleLink}或阅读我们的{telemetryPrivacyStatementLink}。您可以随时禁用此功能。", - "kbn.home.telemetry.optInMessage.detailsExampleLinkText": "示例", - "kbn.home.telemetry.optInMessage.detailsTelemetryPrivacyStatementLinkText": "遥测隐私声明", - "kbn.home.telemetry.optInMessage.readMoreLinkText": "阅读更多内容", - "kbn.home.telemetry.optInMessageDescription": "通过提供基本功能的使用情况统计信息,来帮助我们改进 Elastic Stack。我们不会在 Elastic 之外共享此数据。", "kbn.home.telemtery.optInCardConfirmButtonLabel": "是", "kbn.home.telemtery.optInCardDeclineButtonLabel": "否", "kbn.home.telemtery.optInCardTitle": "帮助我们改进 Elastic Stack", @@ -10283,66 +10270,28 @@ "xpack.spaces.spaceSelector.noSpacesMatchSearchCriteriaDescription": "没有匹配搜索条件的空间", "xpack.spaces.spaceSelector.selectSpacesTitle": "选择您的空间", "xpack.spaces.spacesTitle": "工作区", - "xpack.spaces.management.copyToSpace.actionDescription": "将此已保存对象复制到一个或多个工作区", - "xpack.spaces.management.copyToSpace.actionTitle": "复制到工作区", - "xpack.spaces.management.copyToSpace.automaticallyOverwrite": "自动覆盖所有已保存对象", - "xpack.spaces.management.copyToSpace.copyDetail.overwriteButton": "覆盖", - "xpack.spaces.management.copyToSpace.copyDetail.skipOverwriteButton": "跳过", - "xpack.spaces.management.copyToSpace.copyErrorTitle": "复制已保存对象时出错", - "xpack.spaces.management.copyToSpace.copyResultsLabel": "复制结果", - "xpack.spaces.management.copyToSpace.copyStatus.conflictsMessage": "具有匹配 ID ({id}) 的已保存对象在此工作区中已存在。", - "xpack.spaces.management.copyToSpace.copyStatus.conflictsOverwriteMessage": "单击“覆盖”可将此版本替换为复制的版本。", - "xpack.spaces.management.copyToSpace.copyStatus.pendingOverwriteMessage": "已保存对象将被覆盖。单击“跳过”可取消此操作。", - "xpack.spaces.management.copyToSpace.copyStatus.successMessage": "已保存对象成功复制。", - "xpack.spaces.management.copyToSpace.copyStatus.unresolvableErrorMessage": "复制此已保存对象时出错。", - "xpack.spaces.management.copyToSpace.copyStatusSummary.conflictsMessage": "在 {space} 工作区中检测到一个或多个冲突。展开此部分以进行解决。", - "xpack.spaces.management.copyToSpace.copyStatusSummary.failedMessage": "复制到 {space} 工作区失败。展开此部分以获取详情。", - "xpack.spaces.management.copyToSpace.copyStatusSummary.successMessage": "已成功复制到 {space} 工作区。", - "xpack.spaces.management.copyToSpace.copyToSpacesButton": "复制到 {spaceCount} 个 {spaceCount, plural, one {工作区} other {工作区}}", - "xpack.spaces.management.copyToSpace.disabledCopyToSpacesButton": "复制", - "xpack.spaces.management.copyToSpace.dontIncludeRelatedLabel": "不包括相关已保存对象", - "xpack.spaces.management.copyToSpace.dontOverwriteLabel": "未覆盖已保存对象", - "xpack.spaces.management.copyToSpace.finishCopyToSpacesButton": "完成", - "xpack.spaces.management.copyToSpace.finishedButtonLabel": "复制已完成。", - "xpack.spaces.management.copyToSpace.finishPendingOverwritesCopyToSpacesButton": "覆盖 {overwriteCount} 个对象", - "xpack.spaces.management.copyToSpace.includeRelatedFormLabel": "包括相关已保存对象", - "xpack.spaces.management.copyToSpace.includeRelatedLabel": "包括相关已保存对象", - "xpack.spaces.management.copyToSpace.inProgressButtonLabel": "复制正在进行中。请稍候。", - "xpack.spaces.management.copyToSpace.noSpacesBody": "没有要复制到的合格工作区。", - "xpack.spaces.management.copyToSpace.noSpacesTitle": "没有可用的工作区", - "xpack.spaces.management.copyToSpace.overwriteLabel": "正在自动覆盖已保存对象", - "xpack.spaces.management.copyToSpace.resolveCopyErrorTitle": "解决已保存对象冲突时出错", - "xpack.spaces.management.copyToSpace.resolveCopySuccessTitle": "覆盖成功", - "xpack.spaces.management.copyToSpace.selectSpacesLabel": "选择要复制到的工作区", - "xpack.spaces.management.copyToSpace.spacesLoadErrorTitle": "加载可用工作区时出错", - "xpack.spaces.management.copyToSpaceFlyoutFooter.conflictCount": "已跳过", - "xpack.spaces.management.copyToSpaceFlyoutFooter.errorCount": "错误", - "xpack.spaces.management.copyToSpaceFlyoutFooter.pendingCount": "待处理", - "xpack.spaces.management.copyToSpaceFlyoutFooter.successCount": "已复制", - "xpack.spaces.management.copyToSpaceFlyoutHeader": "将已保存对象复制到工作区", - "xpack.telemetry.callout.appliesSettingTitle": "此设置适用于{allOfKibanaText}", - "xpack.telemetry.callout.appliesSettingTitle.allOfKibanaText": "所有 Kibana。", - "xpack.telemetry.callout.clusterStatisticsDescription": "这是我们将收集的基本集群统计信息的示例。其包括索引、分片和节点的数目。还包括高级使用情况统计信息,例如监测是否打开。", - "xpack.telemetry.callout.clusterStatisticsTitle": "集群统计信息", - "xpack.telemetry.callout.errorLoadingClusterStatisticsDescription": "尝试提取集群统计信息时发生意外错误。发生此问题的原因可能是 Elasticsearch 出故障、Kibana 出故障,或者有网络错误。检查 Kibana,然后重新加载页面并重试。", - "xpack.telemetry.callout.errorLoadingClusterStatisticsTitle": "加载集群统计信息时出错", - "xpack.telemetry.callout.errorUnprivilegedUserDescription": "您无权查看未加密的集群统计信息。", - "xpack.telemetry.callout.errorUnprivilegedUserTitle": "显示集群统计信息时出错", - "xpack.telemetry.readOurUsageDataPrivacyStatementLinkText": "阅读我们的使用情况数据隐私声明", - "xpack.telemetry.seeExampleOfWhatWeCollectLinkText": "查看我们收集的内容示例", - "xpack.telemetry.telemetryConfigDescription": "通过提供基本功能的使用情况统计信息,来帮助我们改进 Elastic Stack。我们不会在 Elastic 之外共享此数据。", - "xpack.telemetry.telemetryConfigTitle": "遥测选择加入", - "xpack.telemetry.telemetryErrorNotificationMessageDescription.tryAgainText": "确认 Kibana 和 Elasticsearch 仍在运行,然后重试。", - "xpack.telemetry.telemetryErrorNotificationMessageDescription.unableToSaveTelemetryPreferenceText": "无法保存遥测首选项。", - "xpack.telemetry.telemetryErrorNotificationMessageTitle": "遥测错误", - "xpack.telemetry.usageDataTitle": "使用情况数据", - "xpack.telemetry.welcomeBanner.noButtonLabel": "否", - "xpack.telemetry.welcomeBanner.telemetryConfigDescription.readMoreLinkText": "阅读更多内容", - "xpack.telemetry.welcomeBanner.telemetryConfigDetailsDescription": "不会发送有关您处理或存储的数据的信息。此功能将定期发送基本功能使用情况统计信息。请参阅{exampleLink}或阅读我们的{telemetryPrivacyStatementLink}。您可以随时禁用此功能。", - "xpack.telemetry.welcomeBanner.telemetryConfigDetailsDescription.exampleLinkText": "示例", - "xpack.telemetry.welcomeBanner.telemetryConfigDetailsDescription.telemetryPrivacyStatementLinkText": "遥测隐私声明", - "xpack.telemetry.welcomeBanner.yesButtonLabel": "是", - "xpack.telemetry.welcomeBanner.title": "帮助我们改进 Elastic Stack!", + "telemetry.callout.appliesSettingTitle": "此设置适用于{allOfKibanaText}", + "telemetry.callout.appliesSettingTitle.allOfKibanaText": "所有 Kibana。", + "telemetry.callout.clusterStatisticsDescription": "这是我们将收集的基本集群统计信息的示例。其包括索引、分片和节点的数目。还包括高级使用情况统计信息,例如监测是否打开。", + "telemetry.callout.clusterStatisticsTitle": "集群统计信息", + "telemetry.callout.errorLoadingClusterStatisticsDescription": "尝试提取集群统计信息时发生意外错误。发生此问题的原因可能是 Elasticsearch 出故障、Kibana 出故障,或者有网络错误。检查 Kibana,然后重新加载页面并重试。", + "telemetry.callout.errorLoadingClusterStatisticsTitle": "加载集群统计信息时出错", + "telemetry.callout.errorUnprivilegedUserDescription": "您无权查看未加密的集群统计信息。", + "telemetry.callout.errorUnprivilegedUserTitle": "显示集群统计信息时出错", + "telemetry.readOurUsageDataPrivacyStatementLinkText": "阅读我们的使用情况数据隐私声明", + "telemetry.seeExampleOfWhatWeCollectLinkText": "查看我们收集的内容示例", + "telemetry.telemetryConfigDescription": "通过提供基本功能的使用情况统计信息,来帮助我们改进 Elastic Stack。我们不会在 Elastic 之外共享此数据。", + "telemetry.telemetryConfigTitle": "遥测选择加入", + "telemetry.telemetryErrorNotificationMessageDescription.tryAgainText": "确认 Kibana 和 Elasticsearch 仍在运行,然后重试。", + "telemetry.telemetryErrorNotificationMessageDescription.unableToSaveTelemetryPreferenceText": "无法保存遥测首选项。", + "telemetry.telemetryErrorNotificationMessageTitle": "遥测错误", + "telemetry.usageDataTitle": "使用情况数据", + "telemetry.welcomeBanner.noButtonLabel": "否", + "telemetry.welcomeBanner.telemetryConfigDescription.readMoreLinkText": "阅读更多内容", + "telemetry.welcomeBanner.telemetryConfigDetailsDescription": "不会发送有关您处理或存储的数据的信息。此功能将定期发送基本功能使用情况统计信息。请参阅{exampleLink}或阅读我们的{telemetryPrivacyStatementLink}。您可以随时禁用此功能。", + "telemetry.welcomeBanner.telemetryConfigDetailsDescription.exampleLinkText": "示例", + "telemetry.welcomeBanner.telemetryConfigDetailsDescription.telemetryPrivacyStatementLinkText": "遥测隐私声明", + "telemetry.welcomeBanner.yesButtonLabel": "是", "xpack.upgradeAssistant.appTitle": "{version} 升级助手", "xpack.upgradeAssistant.checkupTab.backUpCallout.calloutBody.calloutDetail": "使用 {snapshotRestoreDocsButton} 备份您的数据。", "xpack.upgradeAssistant.checkupTab.backUpCallout.calloutBody.snapshotRestoreDocsButtonLabel": "快照和还原 API", diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index d920b368922cd..3ec86a7fc1722 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -79,12 +79,12 @@ export default async function ({ readConfigFile }) { '--server.uuid=5b2de169-2785-441b-ae8c-186a1936b17d', '--xpack.maps.showMapsInspectorAdapter=true', '--xpack.maps.preserveDrawingBuffer=true', - '--xpack.telemetry.banner=false', '--xpack.reporting.queue.pollInterval=3000', // make it explicitly the default '--xpack.reporting.csv.maxSizeBytes=2850', // small-ish limit for cutting off a 1999 byte report '--stats.maximumWaitTimeForAllCollectorsInS=1', '--xpack.security.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', // server restarts should not invalidate active sessions '--xpack.encrypted_saved_objects.encryptionKey="DkdXazszSCYexXqz4YktBGHCRkV6hyNK"', + '--telemetry.banner=false', '--timelion.ui.enabled=true', ], }, From c39eee8eea4a5c1365bd9795e459ca1852127a11 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 16 Oct 2019 09:35:53 +0200 Subject: [PATCH 17/48] Fix saved query in app state in discvoer (#47849) --- .../kibana/public/dashboard/dashboard_app_controller.tsx | 2 +- .../kibana/public/discover/controllers/discover.js | 8 ++++++-- .../core_plugins/kibana/public/visualize/editor/editor.js | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx index 60004374eb33e..eb49277fbc7b4 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx @@ -503,7 +503,7 @@ export class DashboardAppController { $scope.savedQuery = undefined; return; } - if ($scope.savedQuery && newSavedQueryId !== $scope.savedQuery.id) { + if (!$scope.savedQuery || newSavedQueryId !== $scope.savedQuery.id) { savedQueryService.getSavedQuery(newSavedQueryId).then((savedQuery: SavedQuery) => { $scope.$evalAsync(() => { $scope.savedQuery = savedQuery; diff --git a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js index 8a6570781e87d..e517b2a02a31c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js @@ -220,7 +220,6 @@ function discoverController( $scope.minimumVisibleRows = 50; $scope.fetchStatus = fetchStatuses.UNINITIALIZED; $scope.refreshInterval = timefilter.getRefreshInterval(); - $scope.savedQuery = $route.current.locals.savedQuery; $scope.showSaveQuery = uiCapabilities.discover.saveQuery; $scope.$watch(() => uiCapabilities.discover.saveQuery, (newCapability) => { @@ -549,6 +548,11 @@ function discoverController( }; const shouldSearchOnPageLoad = () => { + // If a saved query is referenced in the app state, omit the initial load because the saved query will + // be fetched separately and trigger a reload + if ($scope.state.savedQuery) { + return false; + } // A saved search is created on every page load, so we check the ID to see if we're loading a // previously saved search or if it is just transient return config.get('discover:searchOnPageLoad') @@ -980,7 +984,7 @@ function discoverController( return; } - if ($scope.savedQuery && newSavedQueryId !== $scope.savedQuery.id) { + if (!$scope.savedQuery || newSavedQueryId !== $scope.savedQuery.id) { savedQueryService.getSavedQuery(newSavedQueryId).then((savedQuery) => { $scope.$evalAsync(() => { $scope.savedQuery = savedQuery; diff --git a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js index 85604448dc8d0..2b6636165169f 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js @@ -547,7 +547,7 @@ function VisEditor( $scope.savedQuery = undefined; return; } - if ($scope.savedQuery && newSavedQueryId !== $scope.savedQuery.id) { + if (!$scope.savedQuery || newSavedQueryId !== $scope.savedQuery.id) { savedQueryService.getSavedQuery(newSavedQueryId).then((savedQuery) => { $scope.$evalAsync(() => { $scope.savedQuery = savedQuery; From 5dda3b1d6abbbf5e15fc85c0afa94ba9d58ba7e1 Mon Sep 17 00:00:00 2001 From: Sebastian Grodzicki Date: Wed, 16 Oct 2019 10:10:42 +0200 Subject: [PATCH 18/48] Update Logs & Metrics UI team name (#47942) --- .github/CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ccfc58bd084f1..4c06d2aee017b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -27,8 +27,8 @@ /x-pack/test/functional/apps/code/ @teams/code /x-pack/test/api_integration/apis/code/ @teams/code -# Infrastructure and Logs UI -/x-pack/legacy/plugins/infra/ @elastic/infra-logs-ui +# Logs & Metrics UI +/x-pack/legacy/plugins/infra/ @elastic/logs-metrics-ui # Machine Learning /x-pack/legacy/plugins/ml/ @elastic/ml-ui From 314ba8269cea1e2836c48fec918d5c5e39400cc7 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Wed, 16 Oct 2019 10:23:58 +0200 Subject: [PATCH 19/48] Remove unused console app file (#48001) --- .../console/public/quarantined/src/app.js | 94 ------------------- 1 file changed, 94 deletions(-) delete mode 100644 src/legacy/core_plugins/console/public/quarantined/src/app.js diff --git a/src/legacy/core_plugins/console/public/quarantined/src/app.js b/src/legacy/core_plugins/console/public/quarantined/src/app.js deleted file mode 100644 index e6bb96726dad9..0000000000000 --- a/src/legacy/core_plugins/console/public/quarantined/src/app.js +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -const $ = require('jquery'); - -const DEFAULT_INPUT_VALUE = `GET _search -{ - "query": { - "match_all": {} - } -}`; - -export default function init(input, output, history, sourceLocation = 'stored') { - $(document.body).removeClass('fouc'); - - // set the value of the input and clear the output - function resetToValues(content) { - input.update(content != null ? content : DEFAULT_INPUT_VALUE); - output.update(''); - } - - function setupAutosave() { - let timer; - const saveDelay = 500; - - input.getSession().on('change', function onChange() { - if (timer) { - timer = clearTimeout(timer); - } - timer = setTimeout(saveCurrentState, saveDelay); - }); - } - - function saveCurrentState() { - try { - const content = input.getValue(); - history.updateCurrentState(content); - } catch (e) { - console.log('Ignoring saving error: ' + e); - } - } - function loadSavedState() { - const previousSaveState = history.getSavedEditorState(); - - if (sourceLocation === 'stored') { - if (previousSaveState) { - resetToValues(previousSaveState.content); - } else { - resetToValues(); - } - } else if (/^https?:\/\//.test(sourceLocation)) { - const loadFrom = { - url: sourceLocation, - // Having dataType here is required as it doesn't allow jQuery to `eval` content - // coming from the external source thereby preventing XSS attack. - dataType: 'text', - kbnXsrfToken: false, - }; - - if (/https?:\/\/api.github.com/.test(sourceLocation)) { - loadFrom.headers = { Accept: 'application/vnd.github.v3.raw' }; - } - - $.ajax(loadFrom).done(data => { - resetToValues(data); - input.moveToNextRequestEdge(true); - input.highlightCurrentRequestsAndUpdateActionBar(); - input.updateActionsBar(); - }); - } else { - resetToValues(); - } - input.moveToNextRequestEdge(true); - } - - setupAutosave(); - loadSavedState(); -} From 730ba21ed4d0758cb4d6cb6fb179568f11b4aaf7 Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Wed, 16 Oct 2019 02:30:06 -0600 Subject: [PATCH 20/48] [SIEM] Endgame Row Renderers: DNS, File (FIM), Network, Security (Authentication), Process (#48277) ## [SIEM] Endgame Row Renderers: DNS, File (FIM), Network, Security (Authentication), Process This PR renders Endgame events via _row renderers_ in the Timeline, per the following screenshot: ![endgame-row-renderers](https://user-images.githubusercontent.com/4459398/66854649-fa6d7900-ef3e-11e9-97cc-5b229041f186.png) The following Endgame event types / subtypes will be rendered via row renderers in the Timeline: * DNS (`dns_event`) - [X] `request_event` * File (FIM) (`file_event`) - [X] `file_create_event` - [X] `file_delete_event` * Network (`network_event`) - [X] `ipv4_connection_accept_event` - [X] `ipv6_connection_accept_event` - [X] `ipv4_disconnect_received_event` - [X] `ipv6_disconnect_received_event` * Security (Authentication) (`security_event`) - [X] `user_logon` - [X] `admin_logon` - [X] `explicit_user_logon` - [X] `user_logoff` * Process (`process_event`) - [X] `creation_event` - [X] `termination_event` This PR also adds row rendering support for some non-Endgame events that conform to the [Elastic Common Schema](https://www.elastic.co/guide/en/ecs/current/index.html) (ECS): * DNS requests * FIM file creation events * FIM file deletion events RELEASE NOTE: To view Endgame events in existing SIEM deployments, you must manually add `endgame-*` to the SIEM index pattern in `Kibana Management > Advanced Settings > SIEM > Elasticsearch indices`. ## DNS Request events Endgame DNS events with the following event type and subtype will be rendered in the Timeline via row renderers: ``` endgame.event_type_full: dns_event and endgame.event_subtype_full: request_event ``` _To view these Endgame DNS events in a timeline, add `endgame-*` to the `SIEM` > `Elasticsearch indices` setting in Kibana `Advanced Settings`, then paste the query above into a timeline to view events._ ### Runtime matching criteria All DNS events, including Endgame and non-Endgame DNS events matching the following criteria will be rendered: ``` dns.question.type: * and dns.question.name: * ``` _The query above can be executed in a timeline to view all data that will be rendered via the (new) DNS event row renderer._ ### Sample rendered DNS event ![endgame-dns-event](https://user-images.githubusercontent.com/4459398/66856414-643b5200-ef42-11e9-8d50-894b7f7abf3d.png) Each field with `this formatting` will be draggable (to pivot a search) in the row-rendered event: `Arun` \ `Anvi-Acer` @ `HD-obe-8bf77f54` asked for `clients4.google.com` with question type `A`, which resolved to `10.58.197.78` (response code: `NOERROR`) via `chrome.exe` `(11620)` [![windows-logo](https://user-images.githubusercontent.com/4459398/66249835-e3d15180-e6f6-11e9-89c3-5517c5ed1596.png) `3008`] ### Fields in a DNS event The following fields will be used to render a DNS event: `user.name` \ `user.domain` @ `host.name` asked for `dns.question.name` with question type `dns.question.type`, which resolved to `dns.resolved_ip` (resp code: `dns.response_code`) via `process.name` `(process.pid)` [![windows-logo](https://user-images.githubusercontent.com/4459398/66249835-e3d15180-e6f6-11e9-89c3-5517c5ed1596.png) `event.code | winlog.event_id`] Note: At the time of this writing, Endgame DNS events do not populate `dns.response_code`. Row renderers are designed to still render partial results when fields are missing. In this case the following text: > (resp code: `dns.response_code`) will NOT be rendered, but the other (populated) fields in the DNS event will be rendered. ### Additional Rendering of DNS events by the Netflow row renderer In addition to being rendered by the new DNS renderer described above, DNS events will also be rendered by the Netflow row renderer. The Neflow row renderer shows the directionality, protocol, and flow of data between a source and destination ### Non-Endgame DNS events The following screenshot shows a DNS event from `packetbeat` rendered by the new DNS row renderer: ![non-endgame-dns-event](https://user-images.githubusercontent.com/4459398/66857061-b7fa6b00-ef43-11e9-894a-d717539db96c.png) _A non-Endgame DNS event that conforms to ECS_ ## File (FIM) Creation events Endgame File (FIM) Creation events with the following event type and subtype will be rendered in the Timeline via row renderers: ``` endgame.event_type_full: file_event and endgame.event_subtype_full: file_create_event ``` ### Runtime matching criteria All file creation events, including Endgame and non-Endgame events matching the following criteria will be rendered: ``` (event.category: file and event.action: file_create_event) or (event.dataset: file and event.action: created) ``` ### Sample rendered File (FIM) Creation event ![file-create-event](https://user-images.githubusercontent.com/4459398/66857794-3f94a980-ef45-11e9-9030-fff35403e8f4.png) `Arun` \ `Anvi-Acer` @ `HD-obe-8bf77f54` created file `the-real-index~RFa99cd75.TMP` in `C:\Users\Arun\AppData\Local\Google\Chrome\User Data\Default\Service Worker\CacheStorage\579544fd7d0441717f082c9eb123588966aa57ac\d81a98b1-59b9-43b2-a228-b3daf7da56df\index-dir\the-real-index~RFa99cd75.TMP` via `chrome.exe` `(11620)` ### Fields in a File (FIM) Creation event `user.name` \ `user.domain` @ `host.name` created file `file.name | endgame.file_name` in `file.path | endgame.file_path` via `process.name | endgame.process_name` `(process.pid | endgame.pid)` ## File (FIM) Deletion events Endgame File (FIM) Deletion events with the following event type and subtype will be rendered in the Timeline via row renderers: ``` endgame.event_type_full: file_event and endgame.event_subtype_full: file_delete_event ``` ### Runtime matching criteria All file deletion events, including Endgame and non-Endgame events matching the following criteria will be rendered: ``` (event.category: file and event.action: file_delete_event) or (event.dataset: file and event.action: deleted) ``` ### Sample rendered File (FIM) Deletion event ![file-delete-event](https://user-images.githubusercontent.com/4459398/66857970-9a2e0580-ef45-11e9-97bb-219c8673a2f2.png) `SYSTEM` \ `NT AUTHORITY` @ `HD-v1s-d2118419` deleted file `tmp0000031a` in `C:\Windows\TEMP\tmp00000404\tmp0000031a` via `AmSvc.exe` `(1084)` ### Fields in a File (FIM) Deletion event `user.name` \ `user.domain` @ `host.name` deleted file `file.name | endgame.file_name` in `file.path | endgame.file_path` via `process.name | endgame.process_name` `(process.pid | endgame.pid)` ## Network Connection Accepted events Endgame Network Connection Accepted events with the following event type and subtype will be rendered in the Timeline via row renderers: ``` (endgame.event_type_full: network_event and endgame.event_subtype_full: ipv4_connection_accept_event) or (endgame.event_type_full: network_event and endgame.event_subtype_full: ipv6_connection_accept_event) ```` ### Runtime matching criteria All Endgame Connection Accepted events, and existing "socket opened" events matching the following criteria will be rendered: ``` event.action: ipv4_connection_accept_event or event.action: ipv6_connection_accept_event or event.action: socket_opened ``` ### Sample rendered Network Connection Accepted event ![ipv4-connection-accept-event](https://user-images.githubusercontent.com/4459398/66858241-16c0e400-ef46-11e9-9fa8-f8b852490bd8.png) `SYSTEM` \ `NT AUTHORITY` @ `HD-gqf-0af7b4fe` accepted a connection via `AmSvc.exe` `(1084)` Network Connection Accepted events are also be rendered with the Netflow row renderer, like the `event.action: socket_opened` events are rendered today. The Network Connection Accepted row renderer displays information about the principal actors in the event (i.e. `user.name`, `host.name`, `process.name`), and the Netflow row renderer displays information about the directionality, source / destination, protocol, etc. ### Fields in a Network Connection Accepted event `user.name` \ `user.domain` @ `host.name` accepted a connection via `process.name` `(process.pid)` ## Network Disconnect Received events Endgame Network Disconnect Received events with the following event type and subtype will be rendered in the Timeline via row renderers: ``` (endgame.event_type_full: network_event and endgame.event_subtype_full: ipv4_disconnect_received_event) or (endgame.event_type_full: network_event and endgame.event_subtype_full: ipv6_disconnect_received_event) ```` ### Runtime matching criteria All Endgame Network Disconnect Received events, and existing "socket closed" events matching the following criteria will be rendered: ``` event.action: ipv4_disconnect_received_event or event.action: ipv6_disconnect_received_event or event.action: socket_closed ``` ### Sample rendered Network Disconnect Received event ![ipv4-disconnect-received-event](https://user-images.githubusercontent.com/4459398/66859155-fa25ab80-ef47-11e9-995c-7628fc0885bf.png) `SYSTEM` \ `NT AUTHORITY` @ `HD-gqf-0af7b4fe` disconnected via `AmSvc.exe` `(1084)` The existing row renderer for `event.action: socket_closed` will be enhanced to display additional fields: - `user.domain` - `process.pid` Network Disconnect Received events will also be rendered with the Netflow row renderer, like the `event.action: socket_closed` events are rendered today. The Network Connection Accepted row renderer displays information about the principal actors in the event (i.e. `user.name`, `host.name`, `process.name`), and the Netflow row renderer displays information about the directionality, source / destination, protocol, etc. ### Fields in a Network Disconnect Received event `user.name` \ `user.domain` @ `host.name` disconnected via `process.name` `(process.pid)` ## Security (Authentication) User Logon events Endgame Security (Authentication) User Logon events with the following event type and subtype will be rendered in the Timeline via row renderers: ``` endgame.event_type_full: security_event and endgame.event_subtype_full: user_logon ``` ### Runtime matching criteria Security (Authentication) User Logon events matching the following criteria will be rendered: ``` event.category: authentication and event.action: user_logon ``` ### Sample rendered Security (Authentication) User Logon event ![user-logon](https://user-images.githubusercontent.com/4459398/66859339-525cad80-ef48-11e9-851c-c08c302df0fc.png) `SYSTEM` \ `NT AUTHORITY` @ `HD-v1s-d2118419` successfully logged in using logon type `5 - Service` (target logon ID `0x3e7`) via `C:\Windows\System32\services.exe` (`432`) as requested by subject `WIN-Q3DOP1UKA81$` \ `WORKGROUP` (source logon ID `0x3e7`) [![windows-logo](https://user-images.githubusercontent.com/4459398/66249835-e3d15180-e6f6-11e9-89c3-5517c5ed1596.png) `4624`] ### Fields in an Security (Authentication) User Logon event `user.name` \ `user.domain` @ `host.name` successfully logged in using logon type `endgame.logon_type` (target logon ID `endgame.target_logon_id`) via `process.name | process.executable` (`process.pid`) as requested by subject `endgame.subject_user_name` \ `endgame.subject_domain_name` (subject logon ID `endgame.subject_logon_id`) [![windows-logo](https://user-images.githubusercontent.com/4459398/66249835-e3d15180-e6f6-11e9-89c3-5517c5ed1596.png) `event.code | winlog.event_id`] ### Reference: LogonType Enumerations The following enumerated values will humanize the numeric `endgame.logon_type` field: ``` 2 - Interactive 3 - Network 4 - Batch 5 - Service 7 - Unlock 8 - Network Cleartext 9 - New Credentials 10 - Remote Interactive 11 - Cached Interactive ``` ## Security (Authentication) Admin Logon events Endgame Security (Authentication) Admin Logon events with the following event type and subtype will be rendered in the Timeline via row renderers: ``` endgame.event_type_full: security_event and endgame.event_subtype_full: admin_logon ``` ### Runtime matching criteria Security (Authentication) Admin Logon events matching the following criteria will be rendered: ``` event.category: authentication and event.action: admin_logon ``` ### Sample rendered Security (Authentication) Admin Logon event ![admin-logon](https://user-images.githubusercontent.com/4459398/66860598-bc765200-ef4a-11e9-9e58-a96c2b97f4e1.png) With special privileges, `SYSTEM` \ `NT AUTHORITY` @ `HD-v1s-d2118419` successfully logged in via `C:\Windows\System32\services.exe` (`964`) as requested by subject `SYSTEM` \ `NT AUTHORITY` (subject logon ID `0x3e7`) [![windows-logo](https://user-images.githubusercontent.com/4459398/66249835-e3d15180-e6f6-11e9-89c3-5517c5ed1596.png) `4672`] ### Fields in a Security (Authentication) Admin Logon event With special privileges, `user.name` \ `user.domain` @ `host.name` successfully logged in via `process.name | process.executable` (`process.pid`) as requested by subject `endgame.subject_user_name` \ `endgame.subject_domain_name` (subject logon ID `endgame.subject_logon_id`) [![windows-logo](https://user-images.githubusercontent.com/4459398/66249835-e3d15180-e6f6-11e9-89c3-5517c5ed1596.png) `event.code | winlog.event_id`] ## Security (Authentication) Explicit User Logon events Endgame Security (Authentication) Explicit User Logon events with the following event type and subtype will be rendered in the Timeline via row renderers: ``` endgame.event_type_full: security_event and endgame.event_subtype_full: explicit_user_logon ``` ### Runtime matching criteria Security (Authentication) Explicit User Logon events matching the following criteria will be rendered: ``` event.category: authentication and event.action: explicit_user_logon ``` ### Sample rendered Security (Authentication) Explicit User Logon event ![explicit-user-logon](https://user-images.githubusercontent.com/4459398/66860797-170fae00-ef4b-11e9-88c5-befd3dcab070.png) A login was attempted using explicit credentials `Arun` \ `Anvi-Acer` to `HD-v1s-d2118419` via `C:\Windows\System32\services.exe` (`1736`) as requested by subject `ANVI-ACER$` \ `WORKGROUP` (subject logon ID `0x3e7`) [![windows-logo](https://user-images.githubusercontent.com/4459398/66249835-e3d15180-e6f6-11e9-89c3-5517c5ed1596.png) `4648`] ### Fields in an Security (Authentication) Explicit User Logon event A login was attempted using explicit credentials `endgame.target_user_name` \ `endgame.target_domain_name` to `host.name` via `process.name | process.executable` (`process.pid`) as requested by subject `endgame.subject_user_name` \ `endgame.subject_domain_name` (subject logon ID `endgame.subject_logon_id`) [![windows-logo](https://user-images.githubusercontent.com/4459398/66249835-e3d15180-e6f6-11e9-89c3-5517c5ed1596.png) `event.code | winlog.event_id`] ## Security (Authentication) User Logoff events Endgame Security (Authentication) User Logoff events with the following event type and subtype will be rendered in the Timeline via row renderers: ``` endgame.event_type_full: security_event and endgame.event_subtype_full: user_logoff ``` ### Runtime matching criteria Security (Authentication) User Logoff events matching the following criteria will be rendered: ``` event.category: authentication and event.action: user_logoff ``` ### Sample rendered Security (Authentication) User Logoff event ![user-logoff](https://user-images.githubusercontent.com/4459398/66861089-9a310400-ef4b-11e9-9f71-b148409c75a7.png) `Arun` \ `Anvi-Acer` @ `HD-55b-3ec87f66` logged off using logon type `2 - Interactive` (target logon ID `0x16db41e`) via `C:\Windows\System32\services.exe` (`964`) [![windows-logo](https://user-images.githubusercontent.com/4459398/66249835-e3d15180-e6f6-11e9-89c3-5517c5ed1596.png) `4634` ] ### Fields in Security (Authentication) User Logoff event `endgame.target_user_name` \ `endgame.target_domain_name` @ `host.name` logged off using logon type `endgame.logon_type` (target logon ID `endgame.target_logon_id`) via `process.name | process.executable` (`process.pid`) [![windows-logo](https://user-images.githubusercontent.com/4459398/66249835-e3d15180-e6f6-11e9-89c3-5517c5ed1596.png) `event.code | winlog.event_id`] ## Process Creation events Endgame Process Creation events with the following event type and subtype will be rendered in the Timeline via row renderers: ``` endgame.event_type_full: process_event and endgame.event_subtype_full: creation_event ``` ### Runtime matching criteria Process Creation events matching the following criteria will be rendered: ``` event.category: process and event.action: creation_event ``` ### Sample rendered Process Creation event ![creation-event](https://user-images.githubusercontent.com/4459398/66861295-fbf16e00-ef4b-11e9-9455-8a1f13463974.png) `Arun` \ `Anvi-Acer` @ `HD-obe-8bf77f54` started process `Microsoft.Photos.exe` (`441684`) `-ServerName:App.AppXzst44mncqdg84v7sv6p7yznqwssy6f7f.mca` via parent process `svchost.exe` (`8`) `sha256 d4c97ed46046893141652e2ec0056a698f6445109949d7fcabbce331146889ee` `sha1 12563599116157778a22600d2a163d8112aed845` `md5 62d06d7235b37895b68de56687895743` ### Fields in a Process Creation event The following fields will be used to render a Process Creation event: `user.name` \ `user.domain` @ `host.name` started process `process.name` (`process.pid`) `process.args` via parent process `endgame.parent_process_name` (`process.ppid`) `process.hash.sha256` `process.hash.sha1` `process.hash.md5` ## Process Termination events Endgame Process Termination events with the following event type and subtype will be rendered in the Timeline via row renderers: ``` endgame.event_type_full: process_event and endgame.event_subtype_full: termination_event ``` ### Runtime matching criteria Process Termination events matching the following criteria will be rendered: ``` event.category: process and event.action: termination_event ``` ### Sample rendered Process Termination event ![termination-event](https://user-images.githubusercontent.com/4459398/66861495-57bbf700-ef4c-11e9-8e6e-923e9c6bab3e.png) `Arun` \ `Anvi-Acer` @ `HD-obe-8bf77f54` terminated process `RuntimeBroker.exe` (`442384`) with exit code `0` `sha256 87976f3430cc99bc939e0694247c0759961a49832b87218f4313d6fc0bc3a776` `sha1 797255e72d5ed5c058d4785950eba7abaa057653` `md5 bd4401441a21bf1abce6404f4231db4d` ### Fields in a Process Termination event The following fields will be used to render a Process Termination event: `user.name` \ `user.domain` @ `host.name` terminated process `process.name` (`process.pid`) with exit code `endgame.exit_code` `process.hash.sha256` `process.hash.sha1` `process.hash.md5` ## Testing Desk tested in: * Dark / light mode * Chrome `77.0.3865.90` * Firefox `69.0.3` * Safari `13.0.1` * NOT tested in IE11 (due to current blocker) https://github.com/elastic/ecs-dev/issues/178 --- .../__snapshots__/args.test.tsx.snap | 55 +- .../process_draggable.test.tsx.snap | 2 + .../user_host_working_dir.test.tsx.snap | 1 + .../timeline/body/renderers/args.test.tsx | 51 +- .../timeline/body/renderers/args.tsx | 52 +- .../renderers/auditd/generic_details.test.tsx | 46 +- .../body/renderers/auditd/generic_details.tsx | 9 +- .../auditd/generic_file_details.test.tsx | 46 +- .../renderers/auditd/generic_file_details.tsx | 9 +- .../auditd/generic_row_renderer.test.tsx | 4 +- .../dns/dns_request_event_details.test.tsx | 37 ++ .../dns/dns_request_event_details.tsx | 64 ++ .../dns_request_event_details_line.test.tsx | 383 ++++++++++++ .../dns/dns_request_event_details_line.tsx | 179 ++++++ .../body/renderers/dns/translations.ts | 39 ++ .../endgame_security_event_details.test.tsx | 90 +++ .../endgame_security_event_details.tsx | 81 +++ ...dgame_security_event_details_line.test.tsx | 589 ++++++++++++++++++ .../endgame_security_event_details_line.tsx | 255 ++++++++ .../body/renderers/endgame/helpers.test.tsx | 208 +++++++ .../body/renderers/endgame/helpers.ts | 61 ++ .../body/renderers/endgame/translations.ts | 134 ++++ .../body/renderers/exit_code_draggable.tsx | 47 ++ .../body/renderers/file_draggable.tsx | 93 +++ .../body/renderers/get_row_renderer.test.tsx | 4 +- .../timeline/body/renderers/helpers.test.tsx | 68 +- .../timeline/body/renderers/helpers.tsx | 24 + .../renderers/parent_process_draggable.tsx | 74 +++ .../timeline/body/renderers/process.hash.tsx | 79 +++ .../body/renderers/process_draggable.test.tsx | 168 ++++- .../body/renderers/process_draggable.tsx | 138 ++-- .../renderers/system/generic_details.test.tsx | 56 +- .../body/renderers/system/generic_details.tsx | 9 +- .../system/generic_file_details.test.tsx | 516 ++++++++++++++- .../renderers/system/generic_file_details.tsx | 120 +++- .../system/generic_row_renderer.test.tsx | 4 +- .../renderers/system/generic_row_renderer.tsx | 237 ++++++- .../body/renderers/system/translations.ts | 35 ++ .../timeline/body/renderers/translations.ts | 2 +- .../renderers/user_host_working_dir.test.tsx | 35 +- .../body/renderers/user_host_working_dir.tsx | 43 +- .../siem/public/mock/mock_endgame_ecs_data.ts | 579 +++++++++++++++++ 42 files changed, 4478 insertions(+), 248 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/translations.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.tsx create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process.hash.tsx create mode 100644 x-pack/legacy/plugins/siem/public/mock/mock_endgame_ecs_data.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/args.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/args.test.tsx.snap index 8b5996b06e4e2..7017ffa6fd9f1 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/args.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/args.test.tsx.snap @@ -1,10 +1,53 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Args rendering it renders against shallow snapshot 1`] = ` - + + + + + + + + + + + + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/process_draggable.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/process_draggable.test.tsx.snap index 807c945e13747..0e1634821fb92 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/process_draggable.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/process_draggable.test.tsx.snap @@ -3,6 +3,8 @@ exports[`ProcessDraggable rendering it renders against shallow snapshot 1`] = ` diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.test.tsx index 9583fef3a5737..284cd0b49cb58 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.test.tsx @@ -10,7 +10,6 @@ import * as React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { TestProviders } from '../../../../mock'; -import { getEmptyString } from '../../../empty_value'; import { Args } from './args'; describe('Args', () => { @@ -20,28 +19,60 @@ describe('Args', () => { ); expect(toJson(wrapper)).toMatchSnapshot(); }); - test('it returns null if args is undefined', () => { + test('it returns an empty string when both args and process title are undefined', () => { const wrapper = mountWithIntl( + + ); + expect(wrapper.text()).toEqual(''); + }); + + test('it returns an empty string when both args and process title are null', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual(''); + }); + + test('it returns an empty string when args is an empty array, and title is an empty string', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual(''); + }); + + test('it returns args when args are provided, and process title is NOT provided', () => { + const wrapper = mountWithIntl( + + ); - expect(wrapper.isEmptyRender()).toBeTruthy(); + expect(wrapper.text()).toEqual('arg1arg2arg3'); }); - test('it returns null if args is null', () => { + test('it returns process title when process title is provided, and args is NOT provided', () => { const wrapper = mountWithIntl( { /> ); - expect(wrapper.isEmptyRender()).toBeTruthy(); + expect(wrapper.text()).toEqual('process-title-1'); }); - test('it returns empty string if args happens to be an empty string', () => { + test('it returns both args and process title, when both are provided', () => { const wrapper = mountWithIntl( ); - expect(wrapper.text()).toEqual(getEmptyString()); + expect(wrapper.text()).toEqual('arg1arg2arg3process-title-1'); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.tsx index b7c19c3f7639d..aad8ef52feeea 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.tsx @@ -5,30 +5,48 @@ */ import * as React from 'react'; -import { pure } from 'recompose'; import { DraggableBadge } from '../../../draggables'; -import { TokensFlexItem } from './helpers'; +import { isNillEmptyOrNotFinite, TokensFlexItem } from './helpers'; interface Props { - eventId: string; + args: string[] | null | undefined; contextId: string; - args: string | null | undefined; + eventId: string; processTitle: string | null | undefined; } -export const Args = pure(({ eventId, contextId, args, processTitle }) => - args != null ? ( - - - - ) : null -); +export const Args = React.memo(({ args, contextId, eventId, processTitle }) => { + if (isNillEmptyOrNotFinite(args) && isNillEmptyOrNotFinite(processTitle)) { + return null; + } + + return ( + <> + {args != null && + args.map((arg, i) => ( + + + + ))} + + {!isNillEmptyOrNotFinite(processTitle) && ( + + + + )} + + ); +}); Args.displayName = 'Args'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx index 3270aca83c4cb..90698cc3bf5c9 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx @@ -44,7 +44,7 @@ describe('GenericDetails', () => { ); expect(wrapper.text()).toEqual( - 'Sessionalice@zeek-sanfranin/generic-text-123gpgconf--list-dirs agent-socket' + 'Sessionalice@zeek-sanfranin/generic-text-123gpgconf(5402)gpgconf--list-dirsagent-socketgpgconf --list-dirs agent-socket' ); }); @@ -82,13 +82,13 @@ describe('GenericDetails', () => { processExecutable="process-1" processTitle="process-title-1" workingDirectory="working-directory-1" - args="arg1 arg2 arg3" + args={['arg1', 'arg2', 'arg3']} result="success" /> ); expect(wrapper.text()).toEqual( - 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123process-name-1arg1 arg2 arg3with resultsuccess' + 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' ); }); @@ -109,13 +109,13 @@ describe('GenericDetails', () => { processExecutable="process-1" processTitle="process-title-1" workingDirectory="working-directory-1" - args="arg1 arg2 arg3" + args={['arg1', 'arg2', 'arg3']} result="success" /> ); expect(wrapper.text()).toEqual( - 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123process-name-1arg1 arg2 arg3with resultsuccess' + 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' ); }); @@ -136,13 +136,13 @@ describe('GenericDetails', () => { processExecutable="process-1" processTitle="process-title-1" workingDirectory="working-directory-1" - args="arg1 arg2 arg3" + args={['arg1', 'arg2', 'arg3']} result="success" /> ); expect(wrapper.text()).toEqual( - 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123process-name-1arg1 arg2 arg3with resultsuccess' + 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' ); }); @@ -163,13 +163,13 @@ describe('GenericDetails', () => { processExecutable="process-1" processTitle="process-title-1" workingDirectory="working-directory-1" - args="arg1 arg2 arg3" + args={['arg1', 'arg2', 'arg3']} result="success" /> ); expect(wrapper.text()).toEqual( - 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123process-name-1arg1 arg2 arg3with resultsuccess' + 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' ); }); @@ -190,13 +190,13 @@ describe('GenericDetails', () => { processExecutable="process-1" processTitle="process-title-1" workingDirectory="working-directory-1" - args="arg1 arg2 arg3" + args={['arg1', 'arg2', 'arg3']} result="success" /> ); expect(wrapper.text()).toEqual( - 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123process-name-1arg1 arg2 arg3with resultsuccess' + 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' ); }); @@ -217,13 +217,13 @@ describe('GenericDetails', () => { processExecutable="process-1" processTitle="process-title-1" workingDirectory="working-directory-1" - args="arg1 arg2 arg3" + args={['arg1', 'arg2', 'arg3']} result="success" /> ); expect(wrapper.text()).toEqual( - 'Sessionsession-1[username-2]as[username-3]@host-1inworking-directory-1generic-text-123process-name-1arg1 arg2 arg3with resultsuccess' + 'Sessionsession-1[username-2]as[username-3]@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' ); }); @@ -244,13 +244,13 @@ describe('GenericDetails', () => { processExecutable="process-1" processTitle="process-title-1" workingDirectory="working-directory-1" - args="arg1 arg2 arg3" + args={['arg1', 'arg2', 'arg3']} result="success" /> ); expect(wrapper.text()).toEqual( - 'Sessionsession-1[username-1]as[username-2]@host-1inworking-directory-1generic-text-123process-name-1arg1 arg2 arg3with resultsuccess' + 'Sessionsession-1[username-1]as[username-2]@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' ); }); @@ -271,13 +271,13 @@ describe('GenericDetails', () => { processExecutable="process-1" processTitle="process-title-1" workingDirectory="working-directory-1" - args="arg1 arg2 arg3" + args={['arg1', 'arg2', 'arg3']} result="success" /> ); expect(wrapper.text()).toEqual( - 'Sessionsession-1[username-primary]@host-1inworking-directory-1generic-text-123process-name-1arg1 arg2 arg3with resultsuccess' + 'Sessionsession-1[username-primary]@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' ); }); @@ -298,13 +298,13 @@ describe('GenericDetails', () => { processExecutable="process-1" processTitle="process-title-1" workingDirectory="working-directory-1" - args="arg1 arg2 arg3" + args={['arg1', 'arg2', 'arg3']} result="success" /> ); expect(wrapper.text()).toEqual( - 'Sessionsession-1[username-primary]@host-1inworking-directory-1generic-text-123process-name-1arg1 arg2 arg3with resultsuccess' + 'Sessionsession-1[username-primary]@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' ); }); @@ -408,7 +408,7 @@ describe('GenericDetails', () => { expect(wrapper.text()).toEqual('Sessiongeneric-text-123some-process-name'); }); - test('it returns only session and user name if process title with id is given', () => { + test('it returns session, user name, and process title if process title with id is given', () => { const wrapper = mountWithIntl( { /> ); - expect(wrapper.text()).toEqual('Sessionsome-user-name'); + expect(wrapper.text()).toEqual('Sessionsome-user-namesome-process-title'); }); test('it returns only a working directory if that is all that is given with a id', () => { @@ -465,7 +465,7 @@ describe('GenericDetails', () => { id="hello-i-am-an-id" contextId="contextid-123" text="generic-text-123" - args="arg1 arg2 arg 3" + args={['arg1', 'arg2', 'arg 3']} userName={undefined} secondary={undefined} session={undefined} @@ -480,7 +480,7 @@ describe('GenericDetails', () => { /> ); - expect(wrapper.text()).toEqual('Sessionarg1 arg2 arg 3'); + expect(wrapper.text()).toEqual('Sessionarg1arg2arg 3'); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.tsx index f6fa3ea63de0b..127d88a2a2c6d 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.tsx @@ -34,7 +34,7 @@ interface Props { processExecutable: string | null | undefined; processTitle: string | null | undefined; workingDirectory: string | null | undefined; - args: string | null | undefined; + args: string[] | null | undefined; session: string | null | undefined; } @@ -56,7 +56,7 @@ export const AuditdGenericLine = pure( session, text, }) => ( - + ( ( const workingDirectory: string | null | undefined = get('process.working_directory[0]', data); const primary: string | null | undefined = get('auditd.summary.actor.primary[0]', data); const secondary: string | null | undefined = get('auditd.summary.actor.secondary[0]', data); - const rawArgs: string[] | null | undefined = get('process.args', data); - const args: string | null = rawArgs != null ? rawArgs.slice(1).join(' ') : null; + const args: string[] | null | undefined = get('process.args', data); if (data.process != null) { return (
diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx index 629f58ee78183..7630202293f8f 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx @@ -48,7 +48,7 @@ describe('GenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - 'Sessionalice@zeek-sanfranin/generic-text-123usinggpgconf--list-dirs agent-socket' + 'Sessionalice@zeek-sanfranin/generic-text-123usinggpgconf(5402)gpgconf--list-dirsagent-socketgpgconf --list-dirs agent-socket' ); }); @@ -87,7 +87,7 @@ describe('GenericFileDetails', () => { processExecutable="process-1" processTitle="process-title-1" workingDirectory="working-directory-1" - args="arg1 arg2 arg3" + args={['arg1', 'arg2', 'arg3']} filePath="/somepath" fileIcon="document" result="success" @@ -95,7 +95,7 @@ describe('GenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1arg1 arg2 arg3with resultsuccess' + 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' ); }); @@ -116,7 +116,7 @@ describe('GenericFileDetails', () => { processExecutable="process-1" processTitle="process-title-1" workingDirectory="working-directory-1" - args="arg1 arg2 arg3" + args={['arg1', 'arg2', 'arg3']} filePath="/somepath" fileIcon="document" result="success" @@ -124,7 +124,7 @@ describe('GenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1arg1 arg2 arg3with resultsuccess' + 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' ); }); @@ -145,7 +145,7 @@ describe('GenericFileDetails', () => { processExecutable="process-1" processTitle="process-title-1" workingDirectory="working-directory-1" - args="arg1 arg2 arg3" + args={['arg1', 'arg2', 'arg3']} filePath="/somepath" fileIcon="document" result="success" @@ -153,7 +153,7 @@ describe('GenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1arg1 arg2 arg3with resultsuccess' + 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' ); }); @@ -174,7 +174,7 @@ describe('GenericFileDetails', () => { processExecutable="process-1" processTitle="process-title-1" workingDirectory="working-directory-1" - args="arg1 arg2 arg3" + args={['arg1', 'arg2', 'arg3']} filePath="/somepath" fileIcon="document" result="success" @@ -182,7 +182,7 @@ describe('GenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1arg1 arg2 arg3with resultsuccess' + 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' ); }); @@ -203,7 +203,7 @@ describe('GenericFileDetails', () => { processExecutable="process-1" processTitle="process-title-1" workingDirectory="working-directory-1" - args="arg1 arg2 arg3" + args={['arg1', 'arg2', 'arg3']} filePath="/somepath" fileIcon="document" result="success" @@ -211,7 +211,7 @@ describe('GenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1arg1 arg2 arg3with resultsuccess' + 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' ); }); @@ -232,7 +232,7 @@ describe('GenericFileDetails', () => { processExecutable="process-1" processTitle="process-title-1" workingDirectory="working-directory-1" - args="arg1 arg2 arg3" + args={['arg1', 'arg2', 'arg3']} filePath="/somepath" fileIcon="document" result="success" @@ -240,7 +240,7 @@ describe('GenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - 'Sessionsession-1[username-2]as[username-3]@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1arg1 arg2 arg3with resultsuccess' + 'Sessionsession-1[username-2]as[username-3]@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' ); }); @@ -261,7 +261,7 @@ describe('GenericFileDetails', () => { processExecutable="process-1" processTitle="process-title-1" workingDirectory="working-directory-1" - args="arg1 arg2 arg3" + args={['arg1', 'arg2', 'arg3']} filePath="/somepath" fileIcon="document" result="success" @@ -269,7 +269,7 @@ describe('GenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - 'Sessionsession-1[username-1]as[username-2]@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1arg1 arg2 arg3with resultsuccess' + 'Sessionsession-1[username-1]as[username-2]@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' ); }); @@ -290,7 +290,7 @@ describe('GenericFileDetails', () => { processExecutable="process-1" processTitle="process-title-1" workingDirectory="working-directory-1" - args="arg1 arg2 arg3" + args={['arg1', 'arg2', 'arg3']} filePath="/somepath" fileIcon="document" result="success" @@ -298,7 +298,7 @@ describe('GenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - 'Sessionsession-1[username-primary]@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1arg1 arg2 arg3with resultsuccess' + 'Sessionsession-1[username-primary]@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' ); }); @@ -319,7 +319,7 @@ describe('GenericFileDetails', () => { processExecutable="process-1" processTitle="process-title-1" workingDirectory="working-directory-1" - args="arg1 arg2 arg3" + args={['arg1', 'arg2', 'arg3']} filePath="/somepath" fileIcon="document" result="success" @@ -327,7 +327,7 @@ describe('GenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - 'Sessionsession-1[username-primary]@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1arg1 arg2 arg3with resultsuccess' + 'Sessionsession-1[username-primary]@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' ); }); @@ -439,7 +439,7 @@ describe('GenericFileDetails', () => { expect(wrapper.text()).toEqual('Sessiongeneric-text-123usingsome-process-name'); }); - test('it returns only session and user name if process title with id is given', () => { + test('it returns session user name and title if process title with id is given', () => { const wrapper = mountWithIntl( { /> ); - expect(wrapper.text()).toEqual('Sessionsome-user-name'); + expect(wrapper.text()).toEqual('Sessionsome-user-namesome-process-title'); }); test('it returns only a working directory if that is all that is given with a id', () => { @@ -500,7 +500,7 @@ describe('GenericFileDetails', () => { id="hello-i-am-an-id" contextId="contextid-123" text="generic-text-123" - args="arg1 arg2 arg 3" + args={['arg1', 'arg2', 'arg 3']} fileIcon="document" userName={undefined} secondary={undefined} @@ -517,7 +517,7 @@ describe('GenericFileDetails', () => { /> ); - expect(wrapper.text()).toEqual('Sessionarg1 arg2 arg 3'); + expect(wrapper.text()).toEqual('Sessionarg1arg2arg 3'); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.tsx index 318a3abe9c00f..076c605ecb89f 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.tsx @@ -36,7 +36,7 @@ interface Props { processExecutable: string | null | undefined; processTitle: string | null | undefined; workingDirectory: string | null | undefined; - args: string | null | undefined; + args: string[] | null | undefined; session: string | null | undefined; } @@ -60,7 +60,7 @@ export const AuditdGenericFileLine = pure( text, fileIcon, }) => ( - + ( ( const filePath: string | null | undefined = get('file.path[0]', data); const primary: string | null | undefined = get('auditd.summary.actor.primary[0]', data); const secondary: string | null | undefined = get('auditd.summary.actor.secondary[0]', data); - const rawArgs: string[] | null | undefined = get('process.args', data); - const args: string | null = rawArgs != null ? rawArgs.slice(1).join(' ') : null; + const args: string[] | null | undefined = get('process.args', data); if (data.process != null) { return ( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx index cb2f14eeaa265..cf5f57f031da7 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx @@ -92,7 +92,7 @@ describe('GenericRowRenderer', () => { ); expect(wrapper.text()).toContain( - 'some children Session246alice@zeek-londonsome textwgetwith resultsuccessDestination93.184.216.34:80' + 'some children Session246alice@zeek-londonsome textwget(1490)wget www.example.comwith resultsuccessDestination93.184.216.34:80' ); }); }); @@ -171,7 +171,7 @@ describe('GenericRowRenderer', () => { ); expect(wrapper.text()).toContain( - 'some children Sessionunsetroot@zeek-londonin/some text/proc/15990/attr/currentusingsystemd-journalwith resultsuccess' + 'some children Sessionunsetroot@zeek-londonin/some text/proc/15990/attr/currentusingsystemd-journal(27244)/lib/systemd/systemd-journaldwith resultsuccess' ); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx new file mode 100644 index 0000000000000..805888afd2964 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* + * 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 React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; + +import { TestProviders } from '../../../../../mock'; +import { mockBrowserFields } from '../../../../../../public/containers/source/mock'; +import { mockEndgameDnsRequest } from '../../../../../../public/mock/mock_endgame_ecs_data'; + +import { DnsRequestEventDetails } from './dns_request_event_details'; + +describe('DnsRequestEventDetails', () => { + test('it renders the expected text given an Endgame DNS request_event', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + 'SYSTEM\\NT AUTHORITY@HD-obe-8bf77f54asked forupdate.googleapis.comwith question typeA, which resolved to10.100.197.67viaGoogleUpdate.exe(443192)3008dns' + ); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.tsx new file mode 100644 index 0000000000000..752163901de2e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.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 { EuiSpacer } from '@elastic/eui'; +import { get } from 'lodash/fp'; +import * as React from 'react'; + +import { BrowserFields } from '../../../../../containers/source'; +import { Details } from '../helpers'; +import { Ecs } from '../../../../../graphql/types'; +import { NetflowRenderer } from '../netflow'; + +import { DnsRequestEventDetailsLine } from './dns_request_event_details_line'; + +interface Props { + browserFields: BrowserFields; + contextId: string; + data: Ecs; + timelineId: string; +} + +export const DnsRequestEventDetails = React.memo(({ data, contextId, timelineId }) => { + const dnsQuestionName: string | null | undefined = get('dns.question.name[0]', data); + const dnsQuestionType: string | null | undefined = get('dns.question.type[0]', data); + const dnsResolvedIp: string | null | undefined = get('dns.resolved_ip[0]', data); + const dnsResponseCode: string | null | undefined = get('dns.response_code[0]', data); + const eventCode: string | null | undefined = get('event.code[0]', data); + const hostName: string | null | undefined = get('host.name[0]', data); + const id = data._id; + const processExecutable: string | null | undefined = get('process.executable[0]', data); + const processName: string | null | undefined = get('process.name[0]', data); + const processPid: number | null | undefined = get('process.pid[0]', data); + const userDomain: string | null | undefined = get('user.domain[0]', data); + const userName: string | null | undefined = get('user.name[0]', data); + const winlogEventId: string | null | undefined = get('winlog.event_id[0]', data); + + return ( +
+ + + +
+ ); +}); + +DnsRequestEventDetails.displayName = 'DnsRequestEventDetails'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx new file mode 100644 index 0000000000000..4c45846574d7c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx @@ -0,0 +1,383 @@ +/* + * 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. + */ +/* + * 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 React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; + +import { TestProviders } from '../../../../../mock'; + +import { DnsRequestEventDetailsLine } from './dns_request_event_details_line'; + +describe('DnsRequestEventDetailsLine', () => { + test('it renders the expected text when all properties are provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + '[userName]\\[userDomain]@[hostName]asked for[dnsQuestionName]with question type[dnsQuestionType], which resolved to[dnsResolvedIp](response code:[dnsResponseCode])via[processName](123)[eventCode]' + ); + }); + + test('it renders the expected text when dnsQuestionName is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + '[userName]\\[userDomain]@[hostName]with question type[dnsQuestionType], which resolved to[dnsResolvedIp](response code:[dnsResponseCode])via[processName](123)[eventCode]' + ); + }); + + test('it renders the expected text when dnsQuestionType is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + '[userName]\\[userDomain]@[hostName]asked for[dnsQuestionName], which resolved to[dnsResolvedIp](response code:[dnsResponseCode])via[processName](123)[eventCode]' + ); + }); + + test('it renders the expected text when dnsResolvedIp is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + '[userName]\\[userDomain]@[hostName]asked for[dnsQuestionName]with question type[dnsQuestionType](response code:[dnsResponseCode])via[processName](123)[eventCode]' + ); + }); + + test('it renders the expected text when dnsResponseCode is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + '[userName]\\[userDomain]@[hostName]asked for[dnsQuestionName]with question type[dnsQuestionType], which resolved to[dnsResolvedIp]via[processName](123)[eventCode]' + ); + }); + + test('it renders the expected text when eventCode is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + '[userName]\\[userDomain]@[hostName]asked for[dnsQuestionName]with question type[dnsQuestionType], which resolved to[dnsResolvedIp](response code:[dnsResponseCode])via[processName](123)[winlogEventId]' + ); + }); + + test('it renders the expected text when hostName is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + '[userName]\\[userDomain]asked for[dnsQuestionName]with question type[dnsQuestionType], which resolved to[dnsResolvedIp](response code:[dnsResponseCode])via[processName](123)[eventCode]' + ); + }); + + test('it renders the expected text when processExecutable is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + '[userName]\\[userDomain]@[hostName]asked for[dnsQuestionName]with question type[dnsQuestionType], which resolved to[dnsResolvedIp](response code:[dnsResponseCode])via[processName](123)[eventCode]' + ); + }); + + test('it renders the expected text when processName is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + '[userName]\\[userDomain]@[hostName]asked for[dnsQuestionName]with question type[dnsQuestionType], which resolved to[dnsResolvedIp](response code:[dnsResponseCode])via[processExecutable](123)[eventCode]' + ); + }); + + test('it renders the expected text when processPid is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + '[userName]\\[userDomain]@[hostName]asked for[dnsQuestionName]with question type[dnsQuestionType], which resolved to[dnsResolvedIp](response code:[dnsResponseCode])via[processName][eventCode]' + ); + }); + + test('it renders the expected text when userDomain is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + '[userName]@[hostName]asked for[dnsQuestionName]with question type[dnsQuestionType], which resolved to[dnsResolvedIp](response code:[dnsResponseCode])via[processName](123)[eventCode]' + ); + }); + + test('it renders the expected text when userName is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + '\\[userDomain][hostName]asked for[dnsQuestionName]with question type[dnsQuestionType], which resolved to[dnsResolvedIp](response code:[dnsResponseCode])via[processName](123)[eventCode]' + ); + }); + + test('it renders the expected text when winlogEventId is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + '[userName]\\[userDomain]@[hostName]asked for[dnsQuestionName]with question type[dnsQuestionType], which resolved to[dnsResolvedIp](response code:[dnsResponseCode])via[processName](123)[eventCode]' + ); + }); + + test('it renders the expected text when both eventCode and winlogEventId are NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + '[userName]\\[userDomain]@[hostName]asked for[dnsQuestionName]with question type[dnsQuestionType], which resolved to[dnsResolvedIp](response code:[dnsResponseCode])via[processName](123)' + ); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx new file mode 100644 index 0000000000000..fd49395379e24 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx @@ -0,0 +1,179 @@ +/* + * 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 { EuiFlexGroup } from '@elastic/eui'; +import * as React from 'react'; + +import { DraggableBadge } from '../../../../draggables'; +import { isNillEmptyOrNotFinite, TokensFlexItem } from '../helpers'; +import { ProcessDraggableWithNonExistentProcess } from '../process_draggable'; +import { UserHostWorkingDir } from '../user_host_working_dir'; + +import * as i18n from './translations'; + +interface Props { + contextId: string; + dnsQuestionName: string | null | undefined; + dnsQuestionType: string | null | undefined; + dnsResolvedIp: string | null | undefined; + dnsResponseCode: string | null | undefined; + eventCode: string | null | undefined; + hostName: string | null | undefined; + id: string; + processExecutable: string | null | undefined; + processName: string | null | undefined; + processPid: number | null | undefined; + userDomain: string | null | undefined; + userName: string | null | undefined; + winlogEventId: string | null | undefined; +} + +export const DnsRequestEventDetailsLine = React.memo( + ({ + contextId, + dnsQuestionName, + dnsQuestionType, + dnsResolvedIp, + dnsResponseCode, + eventCode, + hostName, + id, + processExecutable, + processName, + processPid, + userDomain, + userName, + winlogEventId, + }) => { + return ( + <> + + + + {!isNillEmptyOrNotFinite(dnsQuestionName) && ( + <> + + {i18n.ASKED_FOR} + + + + + + )} + + {!isNillEmptyOrNotFinite(dnsQuestionType) && ( + <> + + {i18n.WITH_QUESTION_TYPE} + + + + + + )} + + {!isNillEmptyOrNotFinite(dnsResolvedIp) && ( + <> + + {i18n.WHICH_RESOLVED_TO} + + + + + + )} + + {!isNillEmptyOrNotFinite(dnsResponseCode) && ( + <> + + {'('} + + + {i18n.RESPONSE_CODE} + + + + + + {')'} + + + )} + + + {i18n.VIA} + + + + + + + {(!isNillEmptyOrNotFinite(eventCode) || !isNillEmptyOrNotFinite(winlogEventId)) && ( + <> + {!isNillEmptyOrNotFinite(eventCode) ? ( + + + + ) : ( + + + + )} + + )} + + + ); + } +); + +DnsRequestEventDetailsLine.displayName = 'DnsRequestEventDetailsLine'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/translations.ts b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/translations.ts new file mode 100644 index 0000000000000..b86b88370cdf5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/translations.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 { i18n } from '@kbn/i18n'; + +export const ASKED_FOR = i18n.translate( + 'xpack.siem.timeline.body.renderers.dns.askedForDescription', + { + defaultMessage: 'asked for', + } +); + +export const RESPONSE_CODE = i18n.translate( + 'xpack.siem.timeline.body.renderers.dns.responseCodeDescription', + { + defaultMessage: 'response code:', + } +); + +export const VIA = i18n.translate('xpack.siem.timeline.body.renderers.dns.viaDescription', { + defaultMessage: 'via', +}); + +export const WHICH_RESOLVED_TO = i18n.translate( + 'xpack.siem.timeline.body.renderers.dns.whichResolvedToDescription', + { + defaultMessage: ', which resolved to', + } +); + +export const WITH_QUESTION_TYPE = i18n.translate( + 'xpack.siem.timeline.body.renderers.dns.withQuestionTypeDescription', + { + defaultMessage: 'with question type', + } +); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx new file mode 100644 index 0000000000000..e1da17ad2904b --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx @@ -0,0 +1,90 @@ +/* + * 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. + */ +/* + * 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 React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; + +import { TestProviders } from '../../../../../mock'; +import { mockBrowserFields } from '../../../../../../public/containers/source/mock'; +import { + mockEndgameAdminLogon, + mockEndgameExplicitUserLogon, + mockEndgameUserLogon, + mockEndgameUserLogoff, +} from '../../../../../../public/mock/mock_endgame_ecs_data'; + +import { EndgameSecurityEventDetails } from './endgame_security_event_details'; + +describe('EndgameSecurityEventDetails', () => { + test('it renders the expected text given an Endgame Security user_logon event', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + 'SYSTEM\\NT AUTHORITY@HD-v1s-d2118419successfully logged inusing logon type5 - Service(target logon ID0x3e7)viaC:\\Windows\\System32\\services.exe(432)as requested by subjectWIN-Q3DOP1UKA81$(subject logon ID0x3e7)4624' + ); + }); + + test('it renders the expected text given an Endgame Security admin_logon event', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + 'With special privileges,SYSTEM\\NT AUTHORITY@HD-obe-8bf77f54successfully logged inviaC:\\Windows\\System32\\lsass.exe(964)as requested by subjectSYSTEM\\NT AUTHORITY4672' + ); + }); + + test('it renders the expected text given an Endgame Security explicit_user_logon event', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + 'A login was attempted using explicit credentialsArun\\Anvi-AcertoHD-55b-3ec87f66viaC:\\Windows\\System32\\svchost.exe(1736)as requested by subjectANVI-ACER$\\WORKGROUP(subject logon ID0x3e7)4648' + ); + }); + + test('it renders the expected text given an Endgame Security user_logoff event', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + 'Arun\\Anvi-Acer@HD-55b-3ec87f66logged offusing logon type2 - Interactive(target logon ID0x16db41e)viaC:\\Windows\\System32\\lsass.exe(964)4634' + ); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx new file mode 100644 index 0000000000000..10f9c4ad9e545 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx @@ -0,0 +1,81 @@ +/* + * 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 { EuiSpacer } from '@elastic/eui'; +import { get } from 'lodash/fp'; +import * as React from 'react'; + +import { BrowserFields } from '../../../../../containers/source'; +import { Ecs } from '../../../../../graphql/types'; +import { NetflowRenderer } from '../netflow'; + +import { EndgameSecurityEventDetailsLine } from './endgame_security_event_details_line'; +import { Details } from '../helpers'; + +interface Props { + browserFields: BrowserFields; + contextId: string; + data: Ecs; + timelineId: string; +} + +export const EndgameSecurityEventDetails = React.memo(({ data, contextId, timelineId }) => { + const endgameLogonType: number | null | undefined = get('endgame.logon_type[0]', data); + const endgameSubjectDomainName: string | null | undefined = get( + 'endgame.subject_domain_name[0]', + data + ); + const endgameSubjectLogonId: string | null | undefined = get('endgame.subject_logon_id[0]', data); + const endgameSubjectUserName: string | null | undefined = get( + 'endgame.subject_user_name[0]', + data + ); + const endgameTargetLogonId: string | null | undefined = get('endgame.target_logon_id[0]', data); + const endgameTargetDomainName: string | null | undefined = get( + 'endgame.target_domain_name[0]', + data + ); + const endgameTargetUserName: string | null | undefined = get('endgame.target_user_name[0]', data); + const eventAction: string | null | undefined = get('event.action[0]', data); + const eventCode: string | null | undefined = get('event.code[0]', data); + const hostName: string | null | undefined = get('host.name[0]', data); + const id = data._id; + const processExecutable: string | null | undefined = get('process.executable[0]', data); + const processName: string | null | undefined = get('process.name[0]', data); + const processPid: number | null | undefined = get('process.pid[0]', data); + const userDomain: string | null | undefined = get('user.domain[0]', data); + const userName: string | null | undefined = get('user.name[0]', data); + const winlogEventId: string | null | undefined = get('winlog.event_id[0]', data); + + return ( +
+ + + +
+ ); +}); + +EndgameSecurityEventDetails.displayName = 'EndgameSecurityEventDetails'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx new file mode 100644 index 0000000000000..80635e9f63ac7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx @@ -0,0 +1,589 @@ +/* + * 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. + */ +/* + * 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 React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; + +import { TestProviders } from '../../../../../mock'; + +import { EndgameSecurityEventDetailsLine } from './endgame_security_event_details_line'; + +describe('EndgameSecurityEventDetailsLine', () => { + test('it renders the expected text when all properties are provided and event action is admin_logon', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + 'With special privileges,[userName]\\[userDomain]@[hostName]successfully logged inusing logon type2 - Interactive(target logon ID[endgameTargetLogonId])via[processName](123)as requested by subject[endgameSubjectUserName]\\[endgameSubjectDomainName](subject logon ID[endgameSubjectLogonId])[eventCode]' + ); + }); + + test('it renders the expected text when all properties are provided and event action is explicit_user_logon', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + 'A login was attempted using explicit credentials[endgameTargetUserName]\\[endgameTargetDomainName]to[hostName]using logon type2 - Interactive(target logon ID[endgameTargetLogonId])via[processName](123)as requested by subject[endgameSubjectUserName]\\[endgameSubjectDomainName](subject logon ID[endgameSubjectLogonId])[eventCode]' + ); + }); + + test('it renders the expected text when endgameLogonType is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + 'A login was attempted using explicit credentials[endgameTargetUserName]\\[endgameTargetDomainName]to[hostName](target logon ID[endgameTargetLogonId])via[processName](123)as requested by subject[endgameSubjectUserName]\\[endgameSubjectDomainName](subject logon ID[endgameSubjectLogonId])[eventCode]' + ); + }); + + test('it renders the expected text when endgameSubjectDomainName is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + 'A login was attempted using explicit credentials[endgameTargetUserName]\\[endgameTargetDomainName]to[hostName]using logon type2 - Interactive(target logon ID[endgameTargetLogonId])via[processName](123)as requested by subject[endgameSubjectUserName](subject logon ID[endgameSubjectLogonId])[eventCode]' + ); + }); + + test('it renders the expected text when endgameSubjectLogonId is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + 'A login was attempted using explicit credentials[endgameTargetUserName]\\[endgameTargetDomainName]to[hostName]using logon type2 - Interactive(target logon ID[endgameTargetLogonId])via[processName](123)as requested by subject[endgameSubjectUserName]\\[endgameSubjectDomainName][eventCode]' + ); + }); + + test('it renders the expected text when when endgameSubjectUserName is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + 'A login was attempted using explicit credentials[endgameTargetUserName]\\[endgameTargetDomainName]to[hostName]using logon type2 - Interactive(target logon ID[endgameTargetLogonId])via[processName](123)\\[endgameSubjectDomainName](subject logon ID[endgameSubjectLogonId])[eventCode]' + ); + }); + + test('it renders the expected text when endgameTargetDomainName is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + 'A login was attempted using explicit credentials[endgameTargetUserName]to[hostName]using logon type2 - Interactive(target logon ID[endgameTargetLogonId])via[processName](123)as requested by subject[endgameSubjectUserName]\\[endgameSubjectDomainName](subject logon ID[endgameSubjectLogonId])[eventCode]' + ); + }); + + test('it renders the expected text when endgameTargetLogonId is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + 'A login was attempted using explicit credentials[endgameTargetUserName]\\[endgameTargetDomainName]to[hostName]using logon type2 - Interactivevia[processName](123)as requested by subject[endgameSubjectUserName]\\[endgameSubjectDomainName](subject logon ID[endgameSubjectLogonId])[eventCode]' + ); + }); + + test('it renders the expected text when endgameTargetUserName is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + 'A login was attempted using explicit credentials\\[endgameTargetDomainName][hostName]using logon type2 - Interactive(target logon ID[endgameTargetLogonId])via[processName](123)as requested by subject[endgameSubjectUserName]\\[endgameSubjectDomainName](subject logon ID[endgameSubjectLogonId])[eventCode]' + ); + }); + + test('it renders the expected text when eventAction is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + '[userName]\\[userDomain]@[hostName]successfully logged inusing logon type2 - Interactive(target logon ID[endgameTargetLogonId])via[processName](123)as requested by subject[endgameSubjectUserName]\\[endgameSubjectDomainName](subject logon ID[endgameSubjectLogonId])[eventCode]' + ); + }); + + test('it renders the expected text when eventCode is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + 'A login was attempted using explicit credentials[endgameTargetUserName]\\[endgameTargetDomainName]to[hostName]using logon type2 - Interactive(target logon ID[endgameTargetLogonId])via[processName](123)as requested by subject[endgameSubjectUserName]\\[endgameSubjectDomainName](subject logon ID[endgameSubjectLogonId])[winlogEventId]' + ); + }); + + test('it renders the expected text when hostName is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + 'A login was attempted using explicit credentials[endgameTargetUserName]\\[endgameTargetDomainName]using logon type2 - Interactive(target logon ID[endgameTargetLogonId])via[processName](123)as requested by subject[endgameSubjectUserName]\\[endgameSubjectDomainName](subject logon ID[endgameSubjectLogonId])[eventCode]' + ); + }); + + test('it renders the expected text when processExecutable is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + 'A login was attempted using explicit credentials[endgameTargetUserName]\\[endgameTargetDomainName]to[hostName]using logon type2 - Interactive(target logon ID[endgameTargetLogonId])via[processName](123)as requested by subject[endgameSubjectUserName]\\[endgameSubjectDomainName](subject logon ID[endgameSubjectLogonId])[eventCode]' + ); + }); + + test('it renders the expected text when processName is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + 'A login was attempted using explicit credentials[endgameTargetUserName]\\[endgameTargetDomainName]to[hostName]using logon type2 - Interactive(target logon ID[endgameTargetLogonId])via[processExecutable](123)as requested by subject[endgameSubjectUserName]\\[endgameSubjectDomainName](subject logon ID[endgameSubjectLogonId])[eventCode]' + ); + }); + + test('it renders the expected text when processPid is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + 'A login was attempted using explicit credentials[endgameTargetUserName]\\[endgameTargetDomainName]to[hostName]using logon type2 - Interactive(target logon ID[endgameTargetLogonId])via[processName]as requested by subject[endgameSubjectUserName]\\[endgameSubjectDomainName](subject logon ID[endgameSubjectLogonId])[eventCode]' + ); + }); + + test('it renders the expected text when userDomain is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + 'With special privileges,[userName]@[hostName]successfully logged inusing logon type2 - Interactive(target logon ID[endgameTargetLogonId])via[processName](123)as requested by subject[endgameSubjectUserName]\\[endgameSubjectDomainName](subject logon ID[endgameSubjectLogonId])[eventCode]' + ); + }); + + test('it renders the expected text when userName is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + 'With special privileges,\\[userDomain][hostName]successfully logged inusing logon type2 - Interactive(target logon ID[endgameTargetLogonId])via[processName](123)as requested by subject[endgameSubjectUserName]\\[endgameSubjectDomainName](subject logon ID[endgameSubjectLogonId])[eventCode]' + ); + }); + + test('it renders the expected text when winlogEventId is NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + 'With special privileges,[userName]\\[userDomain]@[hostName]successfully logged inusing logon type2 - Interactive(target logon ID[endgameTargetLogonId])via[processName](123)as requested by subject[endgameSubjectUserName]\\[endgameSubjectDomainName](subject logon ID[endgameSubjectLogonId])[eventCode]' + ); + }); + + test('it renders the expected text when BOTH eventCode and winlogEventId are NOT provided', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual( + 'With special privileges,[userName]\\[userDomain]@[hostName]successfully logged inusing logon type2 - Interactive(target logon ID[endgameTargetLogonId])via[processName](123)as requested by subject[endgameSubjectUserName]\\[endgameSubjectDomainName](subject logon ID[endgameSubjectLogonId])' + ); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx new file mode 100644 index 0000000000000..97138580618ba --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx @@ -0,0 +1,255 @@ +/* + * 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 { EuiFlexGroup } from '@elastic/eui'; +import * as React from 'react'; + +import { DraggableBadge } from '../../../../draggables'; +import { isNillEmptyOrNotFinite, TokensFlexItem } from '../helpers'; +import { ProcessDraggableWithNonExistentProcess } from '../process_draggable'; +import { UserHostWorkingDir } from '../user_host_working_dir'; + +import { + getEventDetails, + getHostNameSeparator, + getHumanReadableLogonType, + getUserDomainField, + getUserNameField, + useTargetUserAndTargetDomain, +} from './helpers'; + +import * as i18n from './translations'; + +interface Props { + contextId: string; + endgameLogonType: number | null | undefined; + endgameSubjectDomainName: string | null | undefined; + endgameSubjectLogonId: string | null | undefined; + endgameSubjectUserName: string | null | undefined; + endgameTargetDomainName: string | null | undefined; + endgameTargetLogonId: string | null | undefined; + endgameTargetUserName: string | null | undefined; + eventAction: string | null | undefined; + eventCode: string | null | undefined; + hostName: string | null | undefined; + id: string; + processExecutable: string | null | undefined; + processName: string | null | undefined; + processPid: number | null | undefined; + userDomain: string | null | undefined; + userName: string | null | undefined; + winlogEventId: string | null | undefined; +} + +export const EndgameSecurityEventDetailsLine = React.memo( + ({ + contextId, + endgameLogonType, + endgameSubjectDomainName, + endgameSubjectLogonId, + endgameSubjectUserName, + endgameTargetDomainName, + endgameTargetLogonId, + endgameTargetUserName, + eventAction, + eventCode, + hostName, + id, + processExecutable, + processName, + processPid, + userDomain, + userName, + winlogEventId, + }) => { + const domain = useTargetUserAndTargetDomain(eventAction) ? endgameTargetDomainName : userDomain; + const eventDetails = getEventDetails(eventAction); + const hostNameSeparator = getHostNameSeparator(eventAction); + const user = useTargetUserAndTargetDomain(eventAction) ? endgameTargetUserName : userName; + const userDomainField = getUserDomainField(eventAction); + const userNameField = getUserNameField(eventAction); + + return ( + <> + + {eventAction === 'admin_logon' && ( + + {i18n.WITH_SPECIAL_PRIVILEGES} + + )} + + {eventAction === 'explicit_user_logon' && ( + + {i18n.A_LOGIN_WAS_ATTEMPTED_USING_EXPLICIT_CREDENTIALS} + + )} + + + + + {eventDetails} + + + {!isNillEmptyOrNotFinite(endgameLogonType) && ( + <> + + {i18n.USING_LOGON_TYPE} + + + + + + )} + + {!isNillEmptyOrNotFinite(endgameTargetLogonId) && ( + <> + + {'('} + + + {i18n.TARGET_LOGON_ID} + + + + + + {')'} + + + )} + + + {i18n.VIA} + + + + + + + {!isNillEmptyOrNotFinite(endgameSubjectUserName) && ( + <> + + {i18n.AS_REQUESTED_BY_SUBJECT} + + + + + + + )} + + {endgameSubjectDomainName != null && ( + <> + + {'\\'} + + + + + + )} + + {!isNillEmptyOrNotFinite(endgameSubjectLogonId) && ( + <> + + {'('} + + + {i18n.SUBJECT_LOGON_ID} + + + + + + {')'} + + + )} + + {(!isNillEmptyOrNotFinite(eventCode) || !isNillEmptyOrNotFinite(winlogEventId)) && ( + <> + {!isNillEmptyOrNotFinite(eventCode) ? ( + + + + ) : ( + + + + )} + + )} + + + ); + } +); + +EndgameSecurityEventDetailsLine.displayName = 'EndgameSecurityEventDetailsLine'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.test.tsx new file mode 100644 index 0000000000000..06a24730242a8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.test.tsx @@ -0,0 +1,208 @@ +/* + * 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 { + getHostNameSeparator, + getHumanReadableLogonType, + useTargetUserAndTargetDomain, + getUserDomainField, + getUserNameField, + getEventDetails, +} from './helpers'; + +describe('helpers', () => { + describe('#getHumanReadableLogonType', () => { + test('it returns an empty string when endgameLogonType is undefined', () => { + expect(getHumanReadableLogonType(undefined)).toEqual(''); + }); + + test('it returns an empty string when endgameLogonType is null', () => { + expect(getHumanReadableLogonType(null)).toEqual(''); + }); + + test('it returns an empty string when endgameLogonType is NaN', () => { + expect(getHumanReadableLogonType(NaN)).toEqual(''); + }); + + test('it returns an empty string when endgameLogonType is Infinity', () => { + expect(getHumanReadableLogonType(Infinity)).toEqual(''); + }); + + test('it returns a string "0" given 0, an unknown logon type', () => { + expect(getHumanReadableLogonType(0)).toEqual('0'); + }); + + test('it returns a string "-1" given -1, an unknown logon type', () => { + expect(getHumanReadableLogonType(-1)).toEqual('-1'); + }); + + test('it returns "Interactive" given 2', () => { + expect(getHumanReadableLogonType(2)).toEqual('Interactive'); + }); + + test('it returns "Network" given 3', () => { + expect(getHumanReadableLogonType(3)).toEqual('Network'); + }); + + test('it returns "Batch" given 4', () => { + expect(getHumanReadableLogonType(4)).toEqual('Batch'); + }); + + test('it returns "Service" given 5', () => { + expect(getHumanReadableLogonType(5)).toEqual('Service'); + }); + + test('it returns "Unlock" given 7', () => { + expect(getHumanReadableLogonType(7)).toEqual('Unlock'); + }); + + test('it returns "Network Cleartext" given 8', () => { + expect(getHumanReadableLogonType(8)).toEqual('Network Cleartext'); + }); + + test('it returns "New Credentials" given 9', () => { + expect(getHumanReadableLogonType(9)).toEqual('New Credentials'); + }); + + test('it returns "Remote Interactive" given 10', () => { + expect(getHumanReadableLogonType(10)).toEqual('Remote Interactive'); + }); + + test('it returns "Cached Interactive" given 11', () => { + expect(getHumanReadableLogonType(11)).toEqual('Cached Interactive'); + }); + }); + + describe('#getHostNameSeparator', () => { + test('it returns "@" when eventAction is undefined', () => { + expect(getHostNameSeparator(undefined)).toEqual('@'); + }); + + test('it returns "@" when eventAction is null', () => { + expect(getHostNameSeparator(null)).toEqual('@'); + }); + + test('it returns "@" when eventAction is an empty string', () => { + expect(getHostNameSeparator('')).toEqual('@'); + }); + + test('it returns "@" when eventAction is a random value', () => { + expect(getHostNameSeparator('a random value')).toEqual('@'); + }); + + test('it returns "@" when eventAction is "user_logoff"', () => { + expect(getHostNameSeparator('user_logoff')).toEqual('@'); + }); + + test('it returns "to" when eventAction is "explicit_user_logon"', () => { + expect(getHostNameSeparator('explicit_user_logon')).toEqual('to'); + }); + }); + + describe('#useTargetUserAndTargetDomain', () => { + test('it returns false when eventAction is undefined', () => { + expect(useTargetUserAndTargetDomain(undefined)).toEqual(false); + }); + + test('it returns false when eventAction is null', () => { + expect(useTargetUserAndTargetDomain(null)).toEqual(false); + }); + + test('it returns false when eventAction is an empty string', () => { + expect(useTargetUserAndTargetDomain('')).toEqual(false); + }); + + test('it returns false when eventAction is a random value', () => { + expect(useTargetUserAndTargetDomain('a random value')).toEqual(false); + }); + + test('it returns true when eventAction is "explicit_user_logon"', () => { + expect(useTargetUserAndTargetDomain('explicit_user_logon')).toEqual(true); + }); + + test('it returns true when eventAction is "user_logoff"', () => { + expect(useTargetUserAndTargetDomain('user_logoff')).toEqual(true); + }); + }); + + describe('#getUserDomainField', () => { + test('it returns user.domain when eventAction is undefined', () => { + expect(getUserDomainField(undefined)).toEqual('user.domain'); + }); + + test('it returns user.domain when eventAction is null', () => { + expect(getUserDomainField(null)).toEqual('user.domain'); + }); + + test('it returns user.domain when eventAction is an empty string', () => { + expect(getUserDomainField('')).toEqual('user.domain'); + }); + + test('it returns user.domain when eventAction is a random value', () => { + expect(getUserDomainField('a random value')).toEqual('user.domain'); + }); + + test('it returns endgame.target_domain_name when eventAction is "explicit_user_logon"', () => { + expect(getUserDomainField('explicit_user_logon')).toEqual('endgame.target_domain_name'); + }); + + test('it returns endgame.target_domain_name when eventAction is "user_logoff"', () => { + expect(getUserDomainField('user_logoff')).toEqual('endgame.target_domain_name'); + }); + }); + + describe('#getUserNameField', () => { + test('it returns user.name when eventAction is undefined', () => { + expect(getUserNameField(undefined)).toEqual('user.name'); + }); + + test('it returns user.name when eventAction is null', () => { + expect(getUserNameField(null)).toEqual('user.name'); + }); + + test('it returns user.name when eventAction is an empty string', () => { + expect(getUserNameField('')).toEqual('user.name'); + }); + + test('it returns user.name when eventAction is a random value', () => { + expect(getUserNameField('a random value')).toEqual('user.name'); + }); + + test('it returns endgame.target_user_name when eventAction is "explicit_user_logon"', () => { + expect(getUserNameField('explicit_user_logon')).toEqual('endgame.target_user_name'); + }); + + test('it returns endgame.target_user_name when eventAction is "user_logoff"', () => { + expect(getUserNameField('user_logoff')).toEqual('endgame.target_user_name'); + }); + }); + + describe('#getEventDetails', () => { + test('it returns successfully logged in when eventAction is undefined', () => { + expect(getEventDetails(undefined)).toEqual('successfully logged in'); + }); + + test('it returns successfully logged in when eventAction is null', () => { + expect(getEventDetails(null)).toEqual('successfully logged in'); + }); + + test('it returns successfully logged in when eventAction is an empty string', () => { + expect(getEventDetails('')).toEqual('successfully logged in'); + }); + + test('it returns successfully logged in when eventAction is a random value', () => { + expect(getEventDetails('a random value')).toEqual('successfully logged in'); + }); + + test('it returns an empty string when eventAction is "explicit_user_logon"', () => { + expect(getEventDetails('explicit_user_logon')).toEqual(''); + }); + + test('it returns logged off when eventAction is "user_logoff"', () => { + expect(getEventDetails('user_logoff')).toEqual('logged off'); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.ts b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.ts new file mode 100644 index 0000000000000..7470c3618fe5c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isNillEmptyOrNotFinite } from '../helpers'; + +import * as i18n from './translations'; + +export const getHumanReadableLogonType = (endgameLogonType: number | null | undefined): string => { + if (isNillEmptyOrNotFinite(endgameLogonType)) { + return ''; + } + + switch (endgameLogonType) { + case 2: + return i18n.LOGON_TYPE_INTERACTIVE; + case 3: + return i18n.LOGON_TYPE_NETWORK; + case 4: + return i18n.LOGON_TYPE_BATCH; + case 5: + return i18n.LOGON_TYPE_SERVICE; + case 7: + return i18n.LOGON_TYPE_UNLOCK; + case 8: + return i18n.LOGON_TYPE_NETWORK_CLEARTEXT; + case 9: + return i18n.LOGON_TYPE_NEW_CREDENTIALS; + case 10: + return i18n.LOGON_TYPE_REMOTE_INTERACTIVE; + case 11: + return i18n.LOGON_TYPE_CACHED_INTERACTIVE; + default: + return `${endgameLogonType}`; + } +}; + +export const getHostNameSeparator = (eventAction: string | null | undefined): string => + eventAction === 'explicit_user_logon' ? i18n.TO : '@'; + +export const useTargetUserAndTargetDomain = (eventAction: string | null | undefined): boolean => + eventAction === 'explicit_user_logon' || eventAction === 'user_logoff'; + +export const getUserDomainField = (eventAction: string | null | undefined): string => + useTargetUserAndTargetDomain(eventAction) ? 'endgame.target_domain_name' : 'user.domain'; + +export const getUserNameField = (eventAction: string | null | undefined): string => + useTargetUserAndTargetDomain(eventAction) ? 'endgame.target_user_name' : 'user.name'; + +export const getEventDetails = (eventAction: string | null | undefined): string => { + switch (eventAction) { + case 'explicit_user_logon': + return ''; // no details + case 'user_logoff': + return i18n.LOGGED_OFF; + default: + return i18n.SUCCESSFULLY_LOGGED_IN; + } +}; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/translations.ts b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/translations.ts new file mode 100644 index 0000000000000..b8c534bc1356c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/translations.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const A_LOGIN_WAS_ATTEMPTED_USING_EXPLICIT_CREDENTIALS = i18n.translate( + 'xpack.siem.timeline.body.renderers.endgame.aLoginWasAttemptedUsingExplicitCredentialsDescription', + { + defaultMessage: 'A login was attempted using explicit credentials', + } +); + +export const AS_REQUESTED_BY_SUBJECT = i18n.translate( + 'xpack.siem.timeline.body.renderers.endgame.asRequestedBySubjectDescription', + { + defaultMessage: 'as requested by subject', + } +); + +export const LOGGED_OFF = i18n.translate( + 'xpack.siem.timeline.body.renderers.endgame.loggedOffDescription', + { + defaultMessage: 'logged off', + } +); + +export const LOGON_TYPE_BATCH = i18n.translate( + 'xpack.siem.timeline.body.renderers.endgame.logonTypeBatchDescription', + { + defaultMessage: 'Batch', + } +); + +export const LOGON_TYPE_CACHED_INTERACTIVE = i18n.translate( + 'xpack.siem.timeline.body.renderers.endgame.logonTypeCachedInteractiveDescription', + { + defaultMessage: 'Cached Interactive', + } +); + +export const LOGON_TYPE_INTERACTIVE = i18n.translate( + 'xpack.siem.timeline.body.renderers.endgame.logonTypeInteractiveDescription', + { + defaultMessage: 'Interactive', + } +); + +export const LOGON_TYPE_NETWORK = i18n.translate( + 'xpack.siem.timeline.body.renderers.endgame.logonTypeNetworkDescription', + { + defaultMessage: 'Network', + } +); + +export const LOGON_TYPE_NETWORK_CLEARTEXT = i18n.translate( + 'xpack.siem.timeline.body.renderers.endgame.logonTypeNetworkCleartextDescription', + { + defaultMessage: 'Network Cleartext', + } +); + +export const LOGON_TYPE_NEW_CREDENTIALS = i18n.translate( + 'xpack.siem.timeline.body.renderers.endgame.logonTypeNewCredentialsDescription', + { + defaultMessage: 'New Credentials', + } +); + +export const LOGON_TYPE_REMOTE_INTERACTIVE = i18n.translate( + 'xpack.siem.timeline.body.renderers.endgame.logonTypeRemoteInteractiveDescription', + { + defaultMessage: 'Remote Interactive', + } +); + +export const LOGON_TYPE_SERVICE = i18n.translate( + 'xpack.siem.timeline.body.renderers.endgame.logonTypeServiceDescription', + { + defaultMessage: 'Service', + } +); + +export const LOGON_TYPE_UNLOCK = i18n.translate( + 'xpack.siem.timeline.body.renderers.endgame.logonTypeUnlockDescription', + { + defaultMessage: 'Unlock', + } +); + +export const SUBJECT_LOGON_ID = i18n.translate( + 'xpack.siem.timeline.body.renderers.endgame.subjectLogonIdDescription', + { + defaultMessage: 'subject logon ID', + } +); + +export const SUCCESSFULLY_LOGGED_IN = i18n.translate( + 'xpack.siem.timeline.body.renderers.endgame.successfullyLoggedInDescription', + { + defaultMessage: 'successfully logged in', + } +); + +export const TARGET_LOGON_ID = i18n.translate( + 'xpack.siem.timeline.body.renderers.endgame.targetLogonIdDescription', + { + defaultMessage: 'target logon ID', + } +); + +export const TO = i18n.translate('xpack.siem.timeline.body.renderers.endgame.toDescription', { + defaultMessage: 'to', +}); + +export const USING_LOGON_TYPE = i18n.translate( + 'xpack.siem.timeline.body.renderers.endgame.usingLogonTypeDescription', + { + defaultMessage: 'using logon type', + } +); + +export const VIA = i18n.translate('xpack.siem.timeline.body.renderers.endgame.viaDescription', { + defaultMessage: 'via', +}); + +export const WITH_SPECIAL_PRIVILEGES = i18n.translate( + 'xpack.siem.timeline.body.renderers.endgame.withSpecialPrivilegesDescription', + { + defaultMessage: 'With special privileges,', + } +); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.tsx new file mode 100644 index 0000000000000..9ea5f2cdd99fa --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.tsx @@ -0,0 +1,47 @@ +/* + * 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 React from 'react'; + +import { DraggableBadge } from '../../../draggables'; + +import { isNillEmptyOrNotFinite, TokensFlexItem } from './helpers'; + +interface Props { + contextId: string; + endgameExitCode: string | null | undefined; + eventId: string; + text: string | null | undefined; +} + +export const ExitCodeDraggable = React.memo( + ({ contextId, endgameExitCode, eventId, text }) => { + if (isNillEmptyOrNotFinite(endgameExitCode)) { + return null; + } + + return ( + <> + {!isNillEmptyOrNotFinite(text) && ( + + {text} + + )} + + + + + + ); + } +); + +ExitCodeDraggable.displayName = 'ExitCodeDraggable'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.tsx new file mode 100644 index 0000000000000..af8876927d235 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.tsx @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; + +import { DraggableBadge } from '../../../draggables'; + +import { isNillEmptyOrNotFinite, TokensFlexItem } from './helpers'; +import * as i18n from './translations'; + +interface Props { + contextId: string; + endgameFileName: string | null | undefined; + endgameFilePath: string | null | undefined; + eventId: string; + fileName: string | null | undefined; + filePath: string | null | undefined; +} + +export const FileDraggable = React.memo( + ({ contextId, endgameFileName, endgameFilePath, eventId, fileName, filePath }) => { + if ( + isNillEmptyOrNotFinite(fileName) && + isNillEmptyOrNotFinite(endgameFileName) && + isNillEmptyOrNotFinite(filePath) && + isNillEmptyOrNotFinite(endgameFilePath) + ) { + return null; + } + + const fileNameIsKnown = + !isNillEmptyOrNotFinite(fileName) || !isNillEmptyOrNotFinite(endgameFileName); + + return ( + <> + {!isNillEmptyOrNotFinite(fileName) ? ( + + + + ) : !isNillEmptyOrNotFinite(endgameFileName) ? ( + + + + ) : null} + + {fileNameIsKnown && ( + + {i18n.IN} + + )} + + {!isNillEmptyOrNotFinite(filePath) ? ( + + + + ) : !isNillEmptyOrNotFinite(endgameFilePath) ? ( + + + + ) : null} + + ); + } +); + +FileDraggable.displayName = 'FileDraggable'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx index 61b4910b935e2..f7a5462be6e8f 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx @@ -131,7 +131,7 @@ describe('get_column_renderer', () => { ); expect(wrapper.text()).toContain( - 'some child Braden@zeek-londonattempted a login via6278with resultfailureSource128.199.212.120' + 'some child Braden@zeek-londonattempted a login via(6278)with resultfailureSource128.199.212.120' ); }); @@ -150,7 +150,7 @@ describe('get_column_renderer', () => { ); expect(wrapper.text()).toContain( - 'some child Sessionalice@zeek-sanfranin/executedgpgconf--list-dirs agent-socket' + 'some child Sessionalice@zeek-sanfranin/executedgpgconf(5402)gpgconf--list-dirsagent-socketgpgconf --list-dirs agent-socket' ); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/helpers.test.tsx index faaba7b92d962..2cd75344deaf6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/helpers.test.tsx @@ -8,7 +8,7 @@ import { cloneDeep } from 'lodash/fp'; import { TimelineNonEcsData } from '../../../../graphql/types'; import { mockTimelineData } from '../../../../mock'; -import { deleteItemIdx, findItem, getValues } from './helpers'; +import { deleteItemIdx, findItem, getValues, isNillEmptyOrNotFinite, showVia } from './helpers'; describe('helpers', () => { describe('#deleteItemIdx', () => { @@ -123,4 +123,70 @@ describe('helpers', () => { expect(getValues('user.name', nullValue)).toBeUndefined(); }); }); + + describe('#isNillEmptyOrNotFinite', () => { + test('undefined returns true', () => { + expect(isNillEmptyOrNotFinite(undefined)).toEqual(true); + }); + + test('null returns true', () => { + expect(isNillEmptyOrNotFinite(null)).toEqual(true); + }); + + test('empty string returns true', () => { + expect(isNillEmptyOrNotFinite('')).toEqual(true); + }); + + test('empty array returns true', () => { + expect(isNillEmptyOrNotFinite([])).toEqual(true); + }); + + test('NaN returns true', () => { + expect(isNillEmptyOrNotFinite(NaN)).toEqual(true); + }); + + test('Infinity returns true', () => { + expect(isNillEmptyOrNotFinite(Infinity)).toEqual(true); + }); + + test('a single space string returns false', () => { + expect(isNillEmptyOrNotFinite(' ')).toEqual(false); + }); + + test('a simple string returns false', () => { + expect(isNillEmptyOrNotFinite('a simple string')).toEqual(false); + }); + + test('the number 0 returns false', () => { + expect(isNillEmptyOrNotFinite(0)).toEqual(false); + }); + + test('a non-empty array return false', () => { + expect(isNillEmptyOrNotFinite(['non empty array'])).toEqual(false); + }); + }); + + describe('#showVia', () => { + test('undefined returns false', () => { + expect(showVia(undefined)).toEqual(false); + }); + + test('null returns false', () => { + expect(showVia(undefined)).toEqual(false); + }); + + test('empty string false', () => { + expect(showVia('')).toEqual(false); + }); + + test('a random string returns false', () => { + expect(showVia('a random string')).toEqual(false); + }); + + ['file_create_event', 'created', 'file_delete_event', 'deleted'].forEach(eventAction => { + test(`${eventAction} returns true`, () => { + expect(showVia(eventAction)).toEqual(true); + }); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/helpers.tsx index 92c7207257a14..b8e55fd29930e 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/helpers.tsx @@ -5,6 +5,7 @@ */ import { EuiFlexItem } from '@elastic/eui'; +import { isNumber, isEmpty } from 'lodash/fp'; import styled from 'styled-components'; import { TimelineNonEcsData } from '../../../../graphql/types'; @@ -27,6 +28,9 @@ export const getValues = (field: string, data: TimelineNonEcsData[]): string[] | export const Details = styled.div` margin: 5px 0 5px 10px; + & .euiBadge { + margin: 2px 0 2px 0; + } `; Details.displayName = 'Details'; @@ -34,3 +38,23 @@ export const TokensFlexItem = styled(EuiFlexItem)` margin-left: 3px; `; TokensFlexItem.displayName = 'TokensFlexItem'; + +export function isNillEmptyOrNotFinite(value: string | number | T[] | null | undefined) { + return isNumber(value) ? !isFinite(value) : isEmpty(value); +} + +export const isFimEvent = ({ + eventCategory, + eventDataset, +}: { + eventCategory: string | null | undefined; + eventDataset: string | null | undefined; +}) => + (eventCategory != null && eventCategory.toLowerCase() === 'file') || + (eventDataset != null && eventDataset.toLowerCase() === 'file'); + +export const isProcessStoppedOrTerminationEvent = (eventAction: string | null | undefined) => + eventAction === 'process_stopped' || eventAction === 'termination_event'; + +export const showVia = (eventAction: string | null | undefined) => + ['file_create_event', 'created', 'file_delete_event', 'deleted'].includes(`${eventAction}`); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.tsx new file mode 100644 index 0000000000000..1423ccad7211e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; + +import { DraggableBadge } from '../../../draggables'; + +import { + isNillEmptyOrNotFinite, + isProcessStoppedOrTerminationEvent, + TokensFlexItem, +} from './helpers'; + +interface Props { + contextId: string; + endgameParentProcessName: string | null | undefined; + eventAction: string | null | undefined; + eventId: string; + processPpid: number | undefined | null; + text: string | null | undefined; +} + +export const ParentProcessDraggable = React.memo( + ({ contextId, endgameParentProcessName, eventAction, eventId, processPpid, text }) => { + if ( + (isNillEmptyOrNotFinite(endgameParentProcessName) && isNillEmptyOrNotFinite(processPpid)) || + isProcessStoppedOrTerminationEvent(eventAction) + ) { + return null; + } + + return ( + <> + {!isNillEmptyOrNotFinite(text) && ( + + {text} + + )} + + {!isNillEmptyOrNotFinite(endgameParentProcessName) && ( + + + + )} + + {!isNillEmptyOrNotFinite(processPpid) && ( + + + + )} + + ); + } +); + +ParentProcessDraggable.displayName = 'ParentProcessDraggable'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process.hash.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process.hash.tsx new file mode 100644 index 0000000000000..cb0b40bdd8fca --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process.hash.tsx @@ -0,0 +1,79 @@ +/* + * 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 { EuiFlexGroup } from '@elastic/eui'; +import * as React from 'react'; +import styled from 'styled-components'; + +import { DraggableBadge } from '../../../draggables'; + +import { isNillEmptyOrNotFinite, TokensFlexItem } from './helpers'; + +const HashFlexGroup = styled(EuiFlexGroup)` + margin: ${({ theme }) => theme.eui.euiSizeXS}; +`; + +interface Props { + contextId: string; + eventId: string; + processHashMd5: string | null | undefined; + processHashSha1: string | null | undefined; + processHashSha256: string | null | undefined; +} + +export const ProcessHash = React.memo( + ({ contextId, eventId, processHashMd5, processHashSha1, processHashSha256 }) => { + if ( + isNillEmptyOrNotFinite(processHashSha256) && + isNillEmptyOrNotFinite(processHashSha1) && + isNillEmptyOrNotFinite(processHashMd5) + ) { + return null; + } + + return ( + + {!isNillEmptyOrNotFinite(processHashSha256) && ( + + + + )} + + {!isNillEmptyOrNotFinite(processHashSha1) && ( + + + + )} + + {!isNillEmptyOrNotFinite(processHashMd5) && ( + + + + )} + + ); + } +); + +ProcessHash.displayName = 'ProcessHash'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx index 3cbe5b50f2cb2..826af00c39011 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx @@ -10,7 +10,7 @@ import * as React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { TestProviders } from '../../../../mock'; -import { ProcessDraggable, isNillOrEmptyString } from './process_draggable'; +import { ProcessDraggable } from './process_draggable'; describe('ProcessDraggable', () => { describe('rendering', () => { @@ -18,6 +18,8 @@ describe('ProcessDraggable', () => { const wrapper = shallow( { { { { { { /> ); - expect(wrapper.text()).toEqual('123'); + expect(wrapper.text()).toEqual('(123)'); }); - test('it returns process name if everything else is an empty string', () => { + test('it returns just process name if process.pid and endgame.pid are NaN', () => { const wrapper = mountWithIntl( { expect(wrapper.text()).toEqual('[process-name]'); }); - test('it returns process executable if everything else is an empty string', () => { + test('it returns just process executable if process.pid and endgame.pid are NaN', () => { const wrapper = mountWithIntl( { expect(wrapper.text()).toEqual('[process-executable]'); }); - test('it returns process pid if everything else is an empty string', () => { + test('it returns process executable if everything else is an empty string or NaN', () => { const wrapper = mountWithIntl( + + ); + expect(wrapper.text()).toEqual('[process-executable]'); + }); + + test('it returns endgame.process_name if everything else is an empty string or NaN', () => { + const wrapper = mountWithIntl( + + ); - expect(wrapper.text()).toEqual('123'); + expect(wrapper.text()).toEqual('[endgame-process_name]'); }); - test('it returns process name if everything is filled', () => { + test('it returns endgame.process_name and endgame.pid if everything else is an empty string or undefined', () => { const wrapper = mountWithIntl( ); - expect(wrapper.text()).toEqual('[process-name]'); + expect(wrapper.text()).toEqual('[endgame-process_name](456)'); }); - test('it returns process executable if process name is undefined', () => { + test('it returns process pid if everything else is an empty string', () => { const wrapper = mountWithIntl( ); - expect(wrapper.text()).toEqual('[process-executable]'); + expect(wrapper.text()).toEqual('(123)'); }); - test('it returns process executable if process name is an empty string', () => { + test('it returns endgame.pid if everything else is an empty string', () => { const wrapper = mountWithIntl( ); - expect(wrapper.text()).toEqual('[process-executable]'); + expect(wrapper.text()).toEqual('(456)'); }); - }); - describe('isNillOrEmptyString', () => { - test('undefined returns true', () => { - expect(isNillOrEmptyString(undefined)).toEqual(true); + test('it returns pid and process name if everything is filled', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('[process-name](123)'); }); - test('null returns true', () => { - expect(isNillOrEmptyString(null)).toEqual(true); + test('it returns process pid and executable and if process name and endgame process name are null', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('[process-executable](123)'); }); - test('empty string returns true', () => { - expect(isNillOrEmptyString('')).toEqual(true); + test('it returns endgame pid and executable and if process name and endgame process name are null', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('[process-executable](456)'); }); - test('single space string returns false', () => { - expect(isNillOrEmptyString(' ')).toEqual(false); + test('it returns process pid and executable and if process name is undefined', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('[process-executable](123)'); }); - test('regular value returns false', () => { - expect(isNillOrEmptyString('[process-name-1]')).toEqual(false); + test('it returns process pid and executable if process name is an empty string', () => { + const wrapper = mountWithIntl( + + + + ); + expect(wrapper.text()).toEqual('[process-executable](123)'); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.tsx index a02b5099d3608..440feca8f3e56 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.tsx @@ -4,88 +4,120 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isNumber, isString } from 'lodash/fp'; import * as React from 'react'; import { pure } from 'recompose'; import { DraggableBadge } from '../../../draggables'; +import { isNillEmptyOrNotFinite } from './helpers'; import * as i18n from './translations'; -export const isNillOrEmptyString = (value: string | number | null | undefined) => { - if (value == null) { - return true; - } else if (isString(value)) { - return value === ''; - } else if (isNumber(value)) { - return !isFinite(value); - } -}; - interface Props { contextId: string; + endgamePid: number | null | undefined; + endgameProcessName: string | null | undefined; eventId: string; processExecutable: string | undefined | null; - processPid?: number | undefined | null; - processName?: string | undefined | null; + processPid: number | undefined | null; + processName: string | undefined | null; } export const ProcessDraggable = pure( - ({ contextId, eventId, processExecutable, processName, processPid }) => { + ({ + contextId, + endgamePid, + endgameProcessName, + eventId, + processExecutable, + processName, + processPid, + }) => { if ( - !isNillOrEmptyString(processName) || - (processName === '' && - isNillOrEmptyString(processExecutable) && - isNillOrEmptyString(processPid)) - ) { - return ( - - ); - } else if ( - !isNillOrEmptyString(processExecutable) || - (processExecutable === '' && isNillOrEmptyString(processPid)) + isNillEmptyOrNotFinite(processName) && + isNillEmptyOrNotFinite(processExecutable) && + isNillEmptyOrNotFinite(endgameProcessName) && + isNillEmptyOrNotFinite(processPid) && + isNillEmptyOrNotFinite(endgamePid) ) { - return ( - - ); - } else if (processPid != null) { - return ( - - ); - } else { return null; } + + return ( +
+ {!isNillEmptyOrNotFinite(processName) ? ( + + ) : !isNillEmptyOrNotFinite(processExecutable) ? ( + + ) : !isNillEmptyOrNotFinite(endgameProcessName) ? ( + + ) : null} + + {!isNillEmptyOrNotFinite(processPid) ? ( + + ) : !isNillEmptyOrNotFinite(endgamePid) ? ( + + ) : null} +
+ ); } ); ProcessDraggable.displayName = 'ProcessDraggable'; export const ProcessDraggableWithNonExistentProcess = pure( - ({ contextId, eventId, processExecutable, processName, processPid }) => { - if (processExecutable == null && processName == null && processPid == null) { + ({ + contextId, + endgamePid, + endgameProcessName, + eventId, + processExecutable, + processName, + processPid, + }) => { + if ( + endgamePid == null && + endgameProcessName == null && + processExecutable == null && + processName == null && + processPid == null + ) { return <>{i18n.NON_EXISTENT}; } else { return ( {
); expect(wrapper.text()).toEqual( - 'Braden@zeek-london[generic-text-123]6278with resultfailureSource128.199.212.120' + 'Braden@zeek-london[generic-text-123](6278)with resultfailureSource128.199.212.120' ); }); }); @@ -69,6 +69,7 @@ describe('SystemGenericDetails', () => { sshMethod="[sshMethod-123]" sshSignature="[sshSignature-123]" text="[generic-text-123]" + userDomain="[userDomain-123]" userName="[username-123]" workingDirectory="[working-directory-123]" /> @@ -76,7 +77,7 @@ describe('SystemGenericDetails', () => {
); expect(wrapper.text()).toEqual( - '[username-123]@[hostname-123]in[working-directory-123][generic-text-123][processName-123]with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][generic-text-123][processName-123](123)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' ); }); @@ -99,6 +100,7 @@ describe('SystemGenericDetails', () => { sshMethod={null} sshSignature={null} text={null} + userDomain={null} userName={null} workingDirectory={null} /> @@ -127,6 +129,7 @@ describe('SystemGenericDetails', () => { sshMethod={null} sshSignature={null} text={null} + userDomain={null} userName={null} workingDirectory={null} /> @@ -155,6 +158,7 @@ describe('SystemGenericDetails', () => { sshMethod={null} sshSignature={null} text={null} + userDomain={null} userName={null} workingDirectory={null} /> @@ -183,6 +187,7 @@ describe('SystemGenericDetails', () => { sshMethod={null} sshSignature={null} text={null} + userDomain={null} userName={null} workingDirectory={null} /> @@ -211,6 +216,7 @@ describe('SystemGenericDetails', () => { sshMethod={null} sshSignature={null} text={null} + userDomain={null} userName={null} workingDirectory={null} /> @@ -241,6 +247,7 @@ describe('SystemGenericDetails', () => { sshMethod={null} sshSignature={null} text={null} + userDomain={null} userName={null} workingDirectory={null} /> @@ -271,6 +278,7 @@ describe('SystemGenericDetails', () => { sshMethod={null} sshSignature={null} text={null} + userDomain={null} userName={null} workingDirectory={null} /> @@ -295,12 +303,13 @@ describe('SystemGenericDetails', () => { packageName="[packageName-123]" packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" - processExecutable="[packageVersion-123]" + processExecutable="[processExecutable-123]" processPid={null} processName={null} sshMethod={null} sshSignature={null} text={null} + userDomain={null} userName={null} workingDirectory={null} /> @@ -308,7 +317,7 @@ describe('SystemGenericDetails', () => {
); expect(wrapper.text()).toEqual( - '[hostname-123][packageVersion-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + '[hostname-123][processExecutable-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' ); }); @@ -325,12 +334,13 @@ describe('SystemGenericDetails', () => { packageName="[packageName-123]" packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" - processExecutable="[packageVersion-123]" + processExecutable="[processExecutable-123]" processPid={123} processName={null} sshMethod={null} sshSignature={null} text={null} + userDomain={null} userName={null} workingDirectory={null} /> @@ -338,7 +348,7 @@ describe('SystemGenericDetails', () => {
); expect(wrapper.text()).toEqual( - '[hostname-123][packageVersion-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + '[hostname-123][processExecutable-123](123)with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' ); }); @@ -355,12 +365,13 @@ describe('SystemGenericDetails', () => { packageName="[packageName-123]" packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" - processExecutable="[packageVersion-123]" + processExecutable="[processExecutable-123]" processPid={123} processName="[processName-123]" sshMethod={null} sshSignature={null} text={null} + userDomain={null} userName={null} workingDirectory={null} /> @@ -368,7 +379,7 @@ describe('SystemGenericDetails', () => {
); expect(wrapper.text()).toEqual( - '[hostname-123][processName-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + '[hostname-123][processName-123](123)with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' ); }); @@ -385,12 +396,13 @@ describe('SystemGenericDetails', () => { packageName="[packageName-123]" packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" - processExecutable="[packageVersion-123]" + processExecutable="[processExecutable-123]" processPid={123} processName="[processName-123]" sshMethod="[sshMethod-123]" sshSignature={null} text={null} + userDomain={null} userName={null} workingDirectory={null} /> @@ -398,7 +410,7 @@ describe('SystemGenericDetails', () => {
); expect(wrapper.text()).toEqual( - '[hostname-123][processName-123]with result[outcome-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + '[hostname-123][processName-123](123)with result[outcome-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' ); }); @@ -415,12 +427,13 @@ describe('SystemGenericDetails', () => { packageName="[packageName-123]" packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" - processExecutable="[packageVersion-123]" + processExecutable="[processExecutable-123]" processPid={123} processName="[processName-123]" sshMethod="[sshMethod-123]" sshSignature="[sshSignature-123]" text={null} + userDomain={null} userName={null} workingDirectory={null} /> @@ -428,7 +441,7 @@ describe('SystemGenericDetails', () => { ); expect(wrapper.text()).toEqual( - '[hostname-123][processName-123]with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + '[hostname-123][processName-123](123)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' ); }); @@ -445,12 +458,13 @@ describe('SystemGenericDetails', () => { packageName="[packageName-123]" packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" - processExecutable="[packageVersion-123]" + processExecutable="[processExecutable-123]" processPid={123} processName="[processName-123]" sshMethod="[sshMethod-123]" sshSignature="[sshSignature-123]" text="[text-123]" + userDomain={null} userName={null} workingDirectory={null} /> @@ -458,11 +472,11 @@ describe('SystemGenericDetails', () => { ); expect(wrapper.text()).toEqual( - '[hostname-123][text-123][processName-123]with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + '[hostname-123][text-123][processName-123](123)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' ); }); - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature, text, username', () => { + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature, text, userDomain, username', () => { const wrapper = mountWithIntl(
@@ -475,12 +489,13 @@ describe('SystemGenericDetails', () => { packageName="[packageName-123]" packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" - processExecutable="[packageVersion-123]" + processExecutable="[processExecutable-123]" processPid={123} processName="[processName-123]" sshMethod="[sshMethod-123]" sshSignature="[sshSignature-123]" text="[text-123]" + userDomain="[userDomain-123]" userName="[username-123]" workingDirectory={null} /> @@ -488,11 +503,11 @@ describe('SystemGenericDetails', () => { ); expect(wrapper.text()).toEqual( - '[username-123]@[hostname-123][text-123][processName-123]with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + '[username-123]\\[userDomain-123]@[hostname-123][text-123][processName-123](123)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' ); }); - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature, text, username, working-directory', () => { + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory', () => { const wrapper = mountWithIntl(
@@ -505,12 +520,13 @@ describe('SystemGenericDetails', () => { packageName="[packageName-123]" packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" - processExecutable="[packageVersion-123]" + processExecutable="[processExecutable-123]" processPid={123} processName="[processName-123]" sshMethod="[sshMethod-123]" sshSignature="[sshSignature-123]" text="[text-123]" + userDomain="[userDomain-123]" userName="[username-123]" workingDirectory="[working-directory-123]" /> @@ -518,7 +534,7 @@ describe('SystemGenericDetails', () => { ); expect(wrapper.text()).toEqual( - '[username-123]@[hostname-123]in[working-directory-123][text-123][processName-123]with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][text-123][processName-123](123)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' ); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx index 3284519f984fe..764a93a7294f4 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx @@ -38,6 +38,7 @@ interface Props { sshMethod: string | null | undefined; sshSignature: string | null | undefined; text: string | null | undefined; + userDomain: string | null | undefined; userName: string | null | undefined; workingDirectory: string | null | undefined; } @@ -58,14 +59,16 @@ export const SystemGenericLine = pure( sshSignature, sshMethod, text, + userDomain, userName, workingDirectory, }) => ( <> - + ( ( const id = data._id; const message: string | null = data.message != null ? data.message[0] : null; const hostName: string | null | undefined = get('host.name[0]', data); + const userDomain: string | null | undefined = get('user.domain[0]', data); const userName: string | null | undefined = get('user.name[0]', data); const outcome: string | null | undefined = get('event.outcome[0]', data); const packageName: string | null | undefined = get('system.audit.package.name[0]', data); @@ -170,6 +176,7 @@ export const SystemGenericDetails = pure( sshMethod={sshMethod} sshSignature={sshSignature} text={text} + userDomain={userDomain} userName={userName} workingDirectory={workingDirectory} /> diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.test.tsx index 57657614c4373..91a848c8e02ed 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.test.tsx @@ -46,7 +46,7 @@ describe('SystemGenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - 'Evan@zeek-london[generic-text-123]6278with resultfailureSource128.199.212.120' + 'Evan@zeek-london[generic-text-123](6278)with resultfailureSource128.199.212.120' ); }); }); @@ -59,20 +59,35 @@ describe('SystemGenericFileDetails', () => { @@ -80,7 +95,7 @@ describe('SystemGenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - '[username-123]@[hostname-123]in[working-directory-123][generic-text-123][processName-123][arg-1] [arg-2] [arg-3]with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][generic-text-123][fileName-123]in[filePath-123][processName-123](123)[arg-1][arg-2][arg-3][some-title-123]with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' ); }); @@ -90,6 +105,15 @@ describe('SystemGenericFileDetails', () => {
{ packageSummary={null} packageVersion={null} processExecutable={null} + processHashMd5={null} + processHashSha1={null} + processHashSha256={null} processPid={null} + processPpid={null} processName={null} + showMessage={true} sshMethod={null} processTitle={null} args={null} sshSignature={null} text={null} + userDomain={null} userName={null} workingDirectory={null} />
); - expect(wrapper.text()).toEqual('to an unknown process'); + expect(wrapper.text()).toEqual('an unknown process'); }); test('it can return only the host name', () => { @@ -120,6 +150,15 @@ describe('SystemGenericFileDetails', () => {
{ packageSummary={null} packageVersion={null} processExecutable={null} + processHashMd5={null} + processHashSha1={null} + processHashSha256={null} processPid={null} + processPpid={null} processName={null} + showMessage={true} sshMethod={null} sshSignature={null} text={null} + userDomain={null} userName={null} workingDirectory={null} processTitle={null} @@ -141,7 +186,7 @@ describe('SystemGenericFileDetails', () => {
); - expect(wrapper.text()).toEqual('[hostname-123]to an unknown process'); + expect(wrapper.text()).toEqual('[hostname-123]an unknown process'); }); test('it can return the host, message', () => { @@ -150,6 +195,15 @@ describe('SystemGenericFileDetails', () => {
{ packageSummary={null} packageVersion={null} processExecutable={null} + processHashMd5={null} + processHashSha1={null} + processHashSha256={null} processPid={null} + processPpid={null} processName={null} + showMessage={true} sshMethod={null} sshSignature={null} text={null} + userDomain={null} userName={null} workingDirectory={null} processTitle={null} @@ -171,7 +231,7 @@ describe('SystemGenericFileDetails', () => {
); - expect(wrapper.text()).toEqual('[hostname-123]to an unknown process[message-123]'); + expect(wrapper.text()).toEqual('[hostname-123]an unknown process[message-123]'); }); test('it can return the host, message, outcome', () => { @@ -180,6 +240,15 @@ describe('SystemGenericFileDetails', () => {
{ packageSummary={null} packageVersion={null} processExecutable={null} + processHashMd5={null} + processHashSha1={null} + processHashSha256={null} processPid={null} + processPpid={null} processName={null} + showMessage={true} sshMethod={null} sshSignature={null} text={null} + userDomain={null} userName={null} workingDirectory={null} processTitle={null} @@ -202,7 +277,7 @@ describe('SystemGenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - '[hostname-123]to an unknown processwith result[outcome-123][message-123]' + '[hostname-123]an unknown processwith result[outcome-123][message-123]' ); }); @@ -212,6 +287,15 @@ describe('SystemGenericFileDetails', () => {
{ packageSummary={null} packageVersion={null} processExecutable={null} + processHashMd5={null} + processHashSha1={null} + processHashSha256={null} processPid={null} + processPpid={null} processName={null} + showMessage={true} sshMethod={null} sshSignature={null} text={null} + userDomain={null} userName={null} workingDirectory={null} processTitle={null} @@ -234,7 +324,7 @@ describe('SystemGenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - '[hostname-123]to an unknown processwith result[outcome-123][packageName-123][message-123]' + '[hostname-123]an unknown processwith result[outcome-123][packageName-123][message-123]' ); }); @@ -244,6 +334,15 @@ describe('SystemGenericFileDetails', () => {
{ packageSummary="[packageSummary-123]" packageVersion={null} processExecutable={null} + processHashMd5={null} + processHashSha1={null} + processHashSha256={null} processPid={null} + processPpid={null} processName={null} + showMessage={true} sshMethod={null} sshSignature={null} text={null} + userDomain={null} userName={null} workingDirectory={null} processTitle={null} @@ -266,7 +371,7 @@ describe('SystemGenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - '[hostname-123]to an unknown processwith result[outcome-123][packageName-123][packageSummary-123][message-123]' + '[hostname-123]an unknown processwith result[outcome-123][packageName-123][packageSummary-123][message-123]' ); }); @@ -276,6 +381,15 @@ describe('SystemGenericFileDetails', () => {
{ packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" processExecutable={null} + processHashMd5={null} + processHashSha1={null} + processHashSha256={null} processPid={null} + processPpid={null} processName={null} + showMessage={true} sshMethod={null} sshSignature={null} text={null} + userDomain={null} userName={null} workingDirectory={null} processTitle={null} @@ -298,7 +418,7 @@ describe('SystemGenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - '[hostname-123]to an unknown processwith result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + '[hostname-123]an unknown processwith result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' ); }); @@ -308,6 +428,15 @@ describe('SystemGenericFileDetails', () => {
{ packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" processExecutable="[packageVersion-123]" + processHashMd5={null} + processHashSha1={null} + processHashSha256={null} processPid={null} + processPpid={null} processName={null} + showMessage={true} sshMethod={null} sshSignature={null} text={null} + userDomain={null} userName={null} workingDirectory={null} processTitle={null} @@ -334,12 +469,21 @@ describe('SystemGenericFileDetails', () => { ); }); - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid', () => { + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5', () => { const wrapper = mountWithIntl(
{ packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" processExecutable="[packageVersion-123]" + processHashMd5="[processHashMd5-123]" + processHashSha1={null} + processHashSha256={null} + processPid={null} + processPpid={null} + processName={null} + showMessage={true} + sshMethod={null} + sshSignature={null} + text={null} + userDomain={null} + userName={null} + workingDirectory={null} + processTitle={null} + args={null} + /> +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123][packageVersion-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashMd5-123][message-123]' + ); + }); + + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1', () => { + const wrapper = mountWithIntl( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123][packageVersion-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha1-123][processHashMd5-123][message-123]' + ); + }); + + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256', () => { + const wrapper = mountWithIntl( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123][packageVersion-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + ); + }); + + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid', () => { + const wrapper = mountWithIntl( + +
+ { ); expect(wrapper.text()).toEqual( - '[hostname-123][packageVersion-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + '[hostname-123][processExecutable-123](123)with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' ); }); - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName', () => { + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName', () => { const wrapper = mountWithIntl(
{ packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" processExecutable="[packageVersion-123]" + processHashMd5="[processHashMd5-123]" + processHashSha1="[processHashSha1-123]" + processHashSha256="[processHashSha256-123]" processPid={123} + processPpid={456} processName="[processName-123]" + showMessage={true} sshMethod={null} sshSignature={null} text={null} + userDomain={null} userName={null} workingDirectory={null} processTitle={null} @@ -394,16 +700,25 @@ describe('SystemGenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - '[hostname-123][processName-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + '[hostname-123][processName-123](123)via parent process(456)with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' ); }); - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod', () => { + test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod', () => { const wrapper = mountWithIntl(
{ packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" processExecutable="[packageVersion-123]" + processHashMd5="[processHashMd5-123]" + processHashSha1="[processHashSha1-123]" + processHashSha256="[processHashSha256-123]" processPid={123} + processPpid={456} processName="[processName-123]" + showMessage={true} sshMethod="[sshMethod-123]" sshSignature={null} text={null} + userDomain={null} userName={null} workingDirectory={null} processTitle={null} @@ -426,16 +747,25 @@ describe('SystemGenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - '[hostname-123][processName-123]with result[outcome-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + '[hostname-123][processName-123](123)with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' ); }); - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature', () => { + test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature', () => { const wrapper = mountWithIntl(
{ packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" processExecutable="[packageVersion-123]" + processHashMd5="[processHashMd5-123]" + processHashSha1="[processHashSha1-123]" + processHashSha256="[processHashSha256-123]" processPid={123} + processPpid={456} processName="[processName-123]" + showMessage={true} sshMethod="[sshMethod-123]" sshSignature="[sshSignature-123]" text={null} + userDomain={null} + userName={null} + workingDirectory={null} + processTitle={null} + args={null} + /> +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123][processName-123](123)with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + ); + }); + + test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text', () => { + const wrapper = mountWithIntl( + +
+ { ); expect(wrapper.text()).toEqual( - '[hostname-123][processName-123]with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + '[hostname-123][text-123][processName-123](123)with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' ); }); - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature, text', () => { + test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain', () => { const wrapper = mountWithIntl(
{ packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" processExecutable="[packageVersion-123]" + processHashMd5="[processHashMd5-123]" + processHashSha1="[processHashSha1-123]" + processHashSha256="[processHashSha256-123]" processPid={123} + processPpid={456} processName="[processName-123]" + showMessage={true} sshMethod="[sshMethod-123]" sshSignature="[sshSignature-123]" text="[text-123]" + userDomain="[userDomain-123]" userName={null} workingDirectory={null} processTitle={null} @@ -490,16 +888,25 @@ describe('SystemGenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - '[hostname-123][text-123][processName-123]with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + '\\[userDomain-123][hostname-123][text-123][processName-123](123)with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' ); }); - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature, text, username', () => { + test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username', () => { const wrapper = mountWithIntl(
{ packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" processExecutable="[packageVersion-123]" + processHashMd5="[processHashMd5-123]" + processHashSha1="[processHashSha1-123]" + processHashSha256="[processHashSha256-123]" processPid={123} + processPpid={456} processName="[processName-123]" + showMessage={true} sshMethod="[sshMethod-123]" sshSignature="[sshSignature-123]" text="[text-123]" + userDomain="[userDomain-123]" userName="[username-123]" workingDirectory={null} processTitle={null} @@ -522,16 +935,25 @@ describe('SystemGenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - '[username-123]@[hostname-123][text-123][processName-123]with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + '[username-123]\\[userDomain-123]@[hostname-123][text-123][processName-123](123)with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' ); }); - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature, text, username, working-directory', () => { + test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory', () => { const wrapper = mountWithIntl(
{ packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" processExecutable="[packageVersion-123]" + processHashMd5="[processHashMd5-123]" + processHashSha1="[processHashSha1-123]" + processHashSha256="[processHashSha256-123]" processPid={123} + processPpid={456} processName="[processName-123]" + showMessage={true} sshMethod="[sshMethod-123]" sshSignature="[sshSignature-123]" text="[text-123]" + userDomain="[userDomain-123]" userName="[username-123]" workingDirectory="[working-directory-123]" processTitle={null} @@ -554,16 +982,25 @@ describe('SystemGenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - '[username-123]@[hostname-123]in[working-directory-123][text-123][processName-123]with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][text-123][processName-123](123)with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' ); }); - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature, text, username, working-directory, process-title', () => { + test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory, process-title', () => { const wrapper = mountWithIntl(
{ packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" processExecutable="[packageVersion-123]" + processHashMd5="[processHashMd5-123]" + processHashSha1="[processHashSha1-123]" + processHashSha256="[processHashSha256-123]" processPid={123} + processPpid={456} processName="[processName-123]" + showMessage={true} sshMethod="[sshMethod-123]" sshSignature="[sshSignature-123]" text="[text-123]" + userDomain="[userDomain-123]" userName="[username-123]" workingDirectory="[working-directory-123]" processTitle="[process-title-123]" @@ -586,16 +1029,25 @@ describe('SystemGenericFileDetails', () => { ); expect(wrapper.text()).toEqual( - '[username-123]@[hostname-123]in[working-directory-123][text-123][processName-123]with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][text-123][processName-123](123)[process-title-123]with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' ); }); - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature, text, username, working-directory, process-title, args', () => { + test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory, process-title, args', () => { const wrapper = mountWithIntl(
{ packageSummary="[packageSummary-123]" packageVersion="[packageVersion-123]" processExecutable="[packageVersion-123]" + processHashMd5="[processHashMd5-123]" + processHashSha1="[processHashSha1-123]" + processHashSha256="[processHashSha256-123]" processPid={123} + processPpid={456} processName="[processName-123]" + showMessage={true} sshMethod="[sshMethod-123]" sshSignature="[sshSignature-123]" text="[text-123]" + userDomain="[userDomain-123]" userName="[username-123]" workingDirectory="[working-directory-123]" processTitle="[process-title-123]" - args="[args-1] [args-2] [args-3]" + args={['[arg-1]', '[arg-2]', '[arg-3]']} />
); expect(wrapper.text()).toEqual( - '[username-123]@[hostname-123]in[working-directory-123][text-123][processName-123][args-1] [args-2] [args-3]with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][text-123][processName-123](123)[arg-1][arg-2][arg-3][process-title-123]with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' ); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx index e2884205a7b60..4d87543dd64f2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx @@ -17,16 +17,29 @@ import { OverflowField } from '../../../../tables/helpers'; import * as i18n from './translations'; import { NetflowRenderer } from '../netflow'; import { UserHostWorkingDir } from '../user_host_working_dir'; -import { Details, TokensFlexItem } from '../helpers'; +import { Details, showVia, TokensFlexItem } from '../helpers'; import { ProcessDraggableWithNonExistentProcess } from '../process_draggable'; import { Args } from '../args'; import { AuthSsh } from './auth_ssh'; +import { ExitCodeDraggable } from '../exit_code_draggable'; +import { FileDraggable } from '../file_draggable'; import { Package } from './package'; import { Badge } from '../../../../page'; +import { ParentProcessDraggable } from '../parent_process_draggable'; +import { ProcessHash } from '../process.hash'; interface Props { - args: string | null | undefined; + args: string[] | null | undefined; contextId: string; + endgameExitCode: string | null | undefined; + endgameFileName: string | null | undefined; + endgameFilePath: string | null | undefined; + endgameParentProcessName: string | null | undefined; + endgamePid: number | null | undefined; + endgameProcessName: string | null | undefined; + eventAction: string | null | undefined; + fileName: string | null | undefined; + filePath: string | null | undefined; hostName: string | null | undefined; id: string; message: string | null | undefined; @@ -36,11 +49,17 @@ interface Props { packageVersion: string | null | undefined; processName: string | null | undefined; processPid: number | null | undefined; + processPpid: number | null | undefined; processExecutable: string | null | undefined; + processHashMd5: string | null | undefined; + processHashSha1: string | null | undefined; + processHashSha256: string | null | undefined; processTitle: string | null | undefined; + showMessage: boolean; sshSignature: string | null | undefined; sshMethod: string | null | undefined; text: string | null | undefined; + userDomain: string | null | undefined; userName: string | null | undefined; workingDirectory: string | null | undefined; } @@ -49,6 +68,15 @@ export const SystemGenericFileLine = pure( ({ args, contextId, + endgameExitCode, + endgameFileName, + endgameFilePath, + endgameParentProcessName, + endgamePid, + endgameProcessName, + eventAction, + fileName, + filePath, hostName, id, message, @@ -57,20 +85,27 @@ export const SystemGenericFileLine = pure( packageSummary, packageVersion, processExecutable, + processHashMd5, + processHashSha1, + processHashSha256, processName, processPid, + processPpid, processTitle, + showMessage, sshSignature, sshMethod, text, + userDomain, userName, workingDirectory, }) => ( <> - + ( {text} + + {showVia(eventAction) && ( + + {i18n.VIA} + + )} - + + + {outcome != null && ( {i18n.WITH_RESULT} @@ -116,7 +180,15 @@ export const SystemGenericFileLine = pure( packageVersion={packageVersion} /> - {message != null && ( + + + {message != null && showMessage && ( <> @@ -138,29 +210,46 @@ interface GenericDetailsProps { browserFields: BrowserFields; data: Ecs; contextId: string; + showMessage?: boolean; text: string; timelineId: string; } export const SystemGenericFileDetails = pure( - ({ data, contextId, text, timelineId }) => { + ({ data, contextId, showMessage = true, text, timelineId }) => { const id = data._id; const message: string | null = data.message != null ? data.message[0] : null; const hostName: string | null | undefined = get('host.name[0]', data); + const endgameExitCode: string | null | undefined = get('endgame.exit_code[0]', data); + const endgameFileName: string | null | undefined = get('endgame.file_name[0]', data); + const endgameFilePath: string | null | undefined = get('endgame.file_path[0]', data); + const endgameParentProcessName: string | null | undefined = get( + 'endgame.parent_process_name[0]', + data + ); + const endgamePid: number | null | undefined = get('endgame.pid[0]', data); + const endgameProcessName: string | null | undefined = get('endgame.process_name[0]', data); + const eventAction: string | null | undefined = get('event.action[0]', data); + const fileName: string | null | undefined = get('file.name[0]', data); + const filePath: string | null | undefined = get('file.path[0]', data); + const userDomain: string | null | undefined = get('user.domain[0]', data); const userName: string | null | undefined = get('user.name[0]', data); const outcome: string | null | undefined = get('event.outcome[0]', data); const packageName: string | null | undefined = get('system.audit.package.name[0]', data); const packageSummary: string | null | undefined = get('system.audit.package.summary[0]', data); const packageVersion: string | null | undefined = get('system.audit.package.version[0]', data); + const processHashMd5: string | null | undefined = get('process.hash.md5[0]', data); + const processHashSha1: string | null | undefined = get('process.hash.sha1[0]', data); + const processHashSha256: string | null | undefined = get('process.hash.sha256', data); const processPid: number | null | undefined = get('process.pid[0]', data); + const processPpid: number | null | undefined = get('process.ppid[0]', data); const processName: string | null | undefined = get('process.name[0]', data); const sshSignature: string | null | undefined = get('system.auth.ssh.signature[0]', data); const sshMethod: string | null | undefined = get('system.auth.ssh.method[0]', data); const processExecutable: string | null | undefined = get('process.executable[0]', data); const processTitle: string | null | undefined = get('process.title[0]', data); const workingDirectory: string | null | undefined = get('process.working_directory[0]', data); - const rawArgs: string[] | null | undefined = get('process.args', data); - const args: string | null = rawArgs != null ? rawArgs.slice(1).join(' ') : null; + const args: string[] | null | undefined = get('process.args', data); return (
@@ -169,6 +258,16 @@ export const SystemGenericFileDetails = pure( contextId={contextId} text={text} hostName={hostName} + endgameExitCode={endgameExitCode} + endgameFileName={endgameFileName} + endgameFilePath={endgameFilePath} + endgameParentProcessName={endgameParentProcessName} + endgamePid={endgamePid} + endgameProcessName={endgameProcessName} + eventAction={eventAction} + fileName={fileName} + filePath={filePath} + userDomain={userDomain} userName={userName} message={message} processTitle={processTitle} @@ -177,9 +276,14 @@ export const SystemGenericFileDetails = pure( packageName={packageName} packageSummary={packageSummary} packageVersion={packageVersion} + processHashMd5={processHashMd5} + processHashSha1={processHashSha1} + processHashSha256={processHashSha256} processName={processName} processPid={processPid} + processPpid={processPpid} processExecutable={processExecutable} + showMessage={showMessage} sshSignature={sshSignature} sshMethod={sshMethod} outcome={outcome} diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx index fe25c7b62a27d..595e8f0327db8 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx @@ -77,7 +77,7 @@ describe('GenericRowRenderer', () => { ); expect(wrapper.text()).toContain( - 'some children Evan@zeek-londonsome text6278with resultfailureSource128.199.212.120' + 'some children Evan@zeek-londonsome text(6278)with resultfailureSource128.199.212.120' ); }); }); @@ -140,7 +140,7 @@ describe('GenericRowRenderer', () => { ); expect(wrapper.text()).toContain( - 'some children Braden@zeek-londonsome text6278with resultfailureSource128.199.212.120' + 'some children Braden@zeek-londonsome text(6278)with resultfailureSource128.199.212.120' ); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx index 1b3e415fa4ccb..9000e23ee0d3a 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx @@ -7,7 +7,11 @@ import { get } from 'lodash/fp'; import React from 'react'; +import { DnsRequestEventDetails } from '../dns/dns_request_event_details'; +import { EndgameSecurityEventDetails } from '../endgame/endgame_security_event_details'; +import { isFimEvent, isNillEmptyOrNotFinite } from '../helpers'; import { RowRenderer, RowRendererContainer } from '../row_renderer'; + import { SystemGenericDetails } from './generic_details'; import { SystemGenericFileDetails } from './generic_file_details'; import * as i18n from './translations'; @@ -45,6 +49,74 @@ export const createGenericSystemRowRenderer = ({ ), }); +export const createEndgameProcessRowRenderer = ({ + actionName, + text, +}: { + actionName: string; + text: string; +}): RowRenderer => ({ + isInstance: ecs => { + const action: string | null | undefined = get('event.action[0]', ecs); + const category: string | null | undefined = get('event.category[0]', ecs); + return ( + category != null && + category.toLowerCase() === 'process' && + action != null && + action.toLowerCase() === actionName + ); + }, + renderRow: ({ browserFields, data, children, timelineId }) => ( + <> + {children} + + + + + ), +}); + +export const createFimRowRenderer = ({ + actionName, + text, +}: { + actionName: string; + text: string; +}): RowRenderer => ({ + isInstance: ecs => { + const action: string | null | undefined = get('event.action[0]', ecs); + const category: string | null | undefined = get('event.category[0]', ecs); + const dataset: string | null | undefined = get('event.dataset[0]', ecs); + return ( + isFimEvent({ eventCategory: category, eventDataset: dataset }) && + action != null && + action.toLowerCase() === actionName + ); + }, + renderRow: ({ browserFields, data, children, timelineId }) => ( + <> + {children} + + + + + ), +}); + export const createGenericFileRowRenderer = ({ actionName, text, @@ -78,6 +150,84 @@ export const createGenericFileRowRenderer = ({ ), }); +export const createSocketRowRenderer = ({ + actionName, + text, +}: { + actionName: string; + text: string; +}): RowRenderer => ({ + isInstance: ecs => { + const action: string | null | undefined = get('event.action[0]', ecs); + return action != null && action.toLowerCase() === actionName; + }, + renderRow: ({ browserFields, data, children, timelineId }) => ( + <> + {children} + + + + + ), +}); + +export const createSecurityEventRowRenderer = ({ + actionName, +}: { + actionName: string; +}): RowRenderer => ({ + isInstance: ecs => { + const category: string | null | undefined = get('event.category[0]', ecs); + const action: string | null | undefined = get('event.action[0]', ecs); + return ( + category != null && + category.toLowerCase() === 'authentication' && + action != null && + action.toLowerCase() === actionName + ); + }, + renderRow: ({ browserFields, data, children, timelineId }) => ( + <> + {children} + + + + + ), +}); + +export const createDnsRowRenderer = (): RowRenderer => ({ + isInstance: ecs => { + const dnsQuestionType: string | null | undefined = get('dns.question.type[0]', ecs); + const dnsQuestionName: string | null | undefined = get('dns.question.name[0]', ecs); + return !isNillEmptyOrNotFinite(dnsQuestionType) && !isNillEmptyOrNotFinite(dnsQuestionName); + }, + renderRow: ({ browserFields, data, children, timelineId }) => ( + <> + {children} + + + + + ), +}); + const systemLoginRowRenderer = createGenericSystemRowRenderer({ actionName: 'user_login', text: i18n.ATTEMPTED_LOGIN, @@ -88,26 +238,94 @@ const systemProcessStartedRowRenderer = createGenericFileRowRenderer({ text: i18n.PROCESS_STARTED, }); +const endgameProcessStartedRowRenderer = createEndgameProcessRowRenderer({ + actionName: 'creation_event', + text: i18n.PROCESS_STARTED, +}); + const systemProcessStoppedRowRenderer = createGenericFileRowRenderer({ actionName: 'process_stopped', text: i18n.PROCESS_STOPPED, }); +const endgameProcessTerminationRowRenderer = createEndgameProcessRowRenderer({ + actionName: 'termination_event', + text: i18n.TERMINATED_PROCESS, +}); + +const endgameFileCreateEventRowRenderer = createFimRowRenderer({ + actionName: 'file_create_event', + text: i18n.CREATED_FILE, +}); + +const fimFileCreateEventRowRenderer = createFimRowRenderer({ + actionName: 'created', + text: i18n.CREATED_FILE, +}); + +const endgameFileDeleteEventRowRenderer = createFimRowRenderer({ + actionName: 'file_delete_event', + text: i18n.DELETED_FILE, +}); + +const fimFileDeletedEventRowRenderer = createFimRowRenderer({ + actionName: 'deleted', + text: i18n.DELETED_FILE, +}); + const systemExistingRowRenderer = createGenericFileRowRenderer({ actionName: 'existing_process', text: i18n.EXISTING_PROCESS, }); -const systemSocketOpenedRowRenderer = createGenericFileRowRenderer({ +const systemSocketOpenedRowRenderer = createSocketRowRenderer({ actionName: 'socket_opened', text: i18n.SOCKET_OPENED, }); -const systemSocketClosedRowRenderer = createGenericFileRowRenderer({ +const systemSocketClosedRowRenderer = createSocketRowRenderer({ actionName: 'socket_closed', text: i18n.SOCKET_CLOSED, }); +const endgameIpv4ConnectionAcceptEventRowRenderer = createSocketRowRenderer({ + actionName: 'ipv4_connection_accept_event', + text: i18n.ACCEPTED_A_CONNECTION_VIA, +}); + +const endgameIpv6ConnectionAcceptEventRowRenderer = createSocketRowRenderer({ + actionName: 'ipv6_connection_accept_event', + text: i18n.ACCEPTED_A_CONNECTION_VIA, +}); + +const endgameIpv4DisconnectReceivedEventRowRenderer = createSocketRowRenderer({ + actionName: 'ipv4_disconnect_received_event', + text: i18n.DISCONNECTED_VIA, +}); + +const endgameIpv6DisconnectReceivedEventRowRenderer = createSocketRowRenderer({ + actionName: 'ipv6_disconnect_received_event', + text: i18n.DISCONNECTED_VIA, +}); + +const endgameAdminLogonRowRenderer = createSecurityEventRowRenderer({ + actionName: 'admin_logon', +}); + +const endgameExplicitUserLogonRowRenderer = createSecurityEventRowRenderer({ + actionName: 'explicit_user_logon', +}); + +const endgameUserLogoffRowRenderer = createSecurityEventRowRenderer({ + actionName: 'user_logoff', +}); + +const endgameUserLogonRowRenderer = createSecurityEventRowRenderer({ + actionName: 'user_logon', +}); + +const dnsRowRenderer = createDnsRowRenderer(); + const systemExistingUserRowRenderer = createGenericSystemRowRenderer({ actionName: 'existing_user', text: i18n.EXISTING_USER, @@ -195,6 +413,21 @@ const systemUserRemovedRowRenderer = createGenericSystemRowRenderer({ }); export const systemRowRenderers: RowRenderer[] = [ + dnsRowRenderer, + endgameAdminLogonRowRenderer, + endgameExplicitUserLogonRowRenderer, + endgameFileCreateEventRowRenderer, + endgameFileDeleteEventRowRenderer, + endgameIpv4ConnectionAcceptEventRowRenderer, + endgameIpv6ConnectionAcceptEventRowRenderer, + endgameIpv4DisconnectReceivedEventRowRenderer, + endgameIpv6DisconnectReceivedEventRowRenderer, + endgameProcessStartedRowRenderer, + endgameProcessTerminationRowRenderer, + endgameUserLogoffRowRenderer, + endgameUserLogonRowRenderer, + fimFileCreateEventRowRenderer, + fimFileDeletedEventRowRenderer, systemAcceptedRowRenderer, systemBootRowRenderer, systemErrorRowRenderer, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/translations.ts b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/translations.ts index be4ce4a16af87..25a23fcfea783 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/translations.ts @@ -32,10 +32,21 @@ export const WAS_AUTHORIZED_TO_USE = i18n.translate( } ); +export const ACCEPTED_A_CONNECTION_VIA = i18n.translate( + 'xpack.siem.system.acceptedAConnectionViaDescription', + { + defaultMessage: 'accepted a connection via', + } +); + export const ATTEMPTED_LOGIN = i18n.translate('xpack.siem.system.attemptedLoginDescription', { defaultMessage: 'attempted a login via', }); +export const DISCONNECTED_VIA = i18n.translate('xpack.siem.system.disconnectedViaDescription', { + defaultMessage: 'disconnected via', +}); + export const LOGGED_OUT = i18n.translate('xpack.siem.system.loggedOutDescription', { defaultMessage: 'logged out via', }); @@ -52,6 +63,18 @@ export const PROCESS_STOPPED = i18n.translate('xpack.siem.system.processStoppedD defaultMessage: 'stopped process', }); +export const TERMINATED_PROCESS = i18n.translate('xpack.siem.system.terminatedProcessDescription', { + defaultMessage: 'terminated process', +}); + +export const CREATED_FILE = i18n.translate('xpack.siem.system.createdFileDescription', { + defaultMessage: 'created a file', +}); + +export const DELETED_FILE = i18n.translate('xpack.siem.system.deletedFileDescription', { + defaultMessage: 'deleted a file', +}); + export const EXISTING_PROCESS = i18n.translate('xpack.siem.system.existingProcessDescription', { defaultMessage: 'is running process', }); @@ -123,3 +146,15 @@ export const PACKAGE_REMOVED = i18n.translate('xpack.siem.system.packageRemovedD export const USER_REMOVED = i18n.translate('xpack.siem.system.userRemovedDescription', { defaultMessage: 'was removed', }); + +export const VIA = i18n.translate('xpack.siem.system.viaDescription', { + defaultMessage: 'via', +}); + +export const VIA_PARENT_PROCESS = i18n.translate('xpack.siem.system.viaParentProcessDescription', { + defaultMessage: 'via parent process', +}); + +export const WITH_EXIT_CODE = i18n.translate('xpack.siem.system.withExitCodeDescription', { + defaultMessage: 'with exit code', +}); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/translations.ts b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/translations.ts index 304b8a23aa23b..2c3c3efdb2993 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/translations.ts @@ -27,5 +27,5 @@ export const IN = i18n.translate('xpack.siem.auditd.inDescription', { }); export const NON_EXISTENT = i18n.translate('xpack.siem.auditd.nonExistentDescription', { - defaultMessage: 'to an unknown process', + defaultMessage: 'an unknown process', }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx index 8b0df5c64d45a..cf03213fc34f3 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx @@ -19,6 +19,7 @@ describe('UserHostWorkingDir', () => { { expect(toJson(wrapper)).toMatchSnapshot(); }); - test('it returns null if userName, hostName, and workingDirectory are all null', () => { + test('it returns null if userDomain, userName, hostName, and workingDirectory are all null', () => { const wrapper = mountWithIntl( { expect(wrapper.isEmptyRender()).toBeTruthy(); }); - test('it returns null if userName, hostName, and workingDirectory are all undefined', () => { + test('it returns null if userDomain, userName, hostName, and workingDirectory are all undefined', () => { const wrapper = mountWithIntl( { expect(wrapper.isEmptyRender()).toBeTruthy(); }); + test('it returns userDomain if that is the only attribute defined', () => { + const wrapper = mountWithIntl( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual('\\[user-domain-123]'); + }); + test('it returns userName if that is the only attribute defined', () => { const wrapper = mountWithIntl( @@ -64,6 +85,7 @@ describe('UserHostWorkingDir', () => { { { { { { expect(wrapper.text()).toEqual('[host-name-123]in[working-directory-123]'); }); - test('it returns userName, hostName', () => { + test('it returns userName, userDomain, hostName', () => { const wrapper = mountWithIntl(
{
); - expect(wrapper.text()).toEqual('[user-name-123]@[host-name-123]'); + expect(wrapper.text()).toEqual('[user-name-123]\\[user-domain-123]@[host-name-123]'); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.tsx index 076dbbf324766..f9ee78aaa5bde 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.tsx @@ -14,27 +14,62 @@ import { HostWorkingDir } from './host_working_dir'; interface Props { contextId: string; eventId: string; + userDomain: string | null | undefined; + userDomainField?: string; userName: string | null | undefined; + userNameField?: string; hostName: string | null | undefined; + hostNameSeparator?: string; workingDirectory: string | null | undefined; } export const UserHostWorkingDir = pure( - ({ contextId, eventId, userName, hostName, workingDirectory }) => - userName != null || hostName != null || workingDirectory != null ? ( + ({ + contextId, + eventId, + hostName, + hostNameSeparator = '@', + userDomain, + userDomainField = 'user.domain', + userName, + userNameField = 'user.name', + workingDirectory, + }) => + userName != null || userDomain != null || hostName != null || workingDirectory != null ? ( <> + + {userDomain != null && ( + <> + + {'\\'} + + + + + + )} + {hostName != null && userName != null && ( - {'@'} + {hostNameSeparator} )} Date: Wed, 16 Oct 2019 10:36:40 +0200 Subject: [PATCH 21/48] Server saved objects client through request context (#44143) * Expose Saved Objects client in request context * API Integration test for savedobjects in req context * SavedObjectsClient docs * SavedObjectsClient#find remove dependency on indexPatterns And use the saved objects mappings instead * Review comments * Review comments, fixes and tests * Use correct type for KQL syntax check --- ...a-plugin-public.savedobjectsclient.find.md | 2 +- ...kibana-plugin-public.savedobjectsclient.md | 2 +- ...gin-server.callapioptions.wrap401errors.md | 2 +- .../core/server/kibana-plugin-server.md | 5 +- ...lugin-server.requesthandlercontext.core.md | 3 + ...ana-plugin-server.requesthandlercontext.md | 4 +- ...server.savedobjectsclient._constructor_.md | 20 +++ ...in-server.savedobjectsclient.bulkcreate.md | 25 +++ ...lugin-server.savedobjectsclient.bulkget.md | 29 ++++ ...plugin-server.savedobjectsclient.create.md | 26 ++++ ...plugin-server.savedobjectsclient.delete.md | 26 ++++ ...plugin-server.savedobjectsclient.errors.md | 11 ++ ...a-plugin-server.savedobjectsclient.find.md | 24 +++ ...na-plugin-server.savedobjectsclient.get.md | 26 ++++ ...kibana-plugin-server.savedobjectsclient.md | 38 +++++ ...plugin-server.savedobjectsclient.update.md | 27 ++++ ...lugin-server.savedobjectsclientcontract.md | 2 +- packages/kbn-es-query/src/kuery/ast/ast.d.ts | 2 +- src/core/public/public.api.md | 2 +- src/core/server/elasticsearch/api_types.ts | 2 +- .../integration_tests/core_services.test.ts | 6 - src/core/server/index.ts | 13 ++ src/core/server/legacy/legacy_service.test.ts | 11 +- src/core/server/legacy/legacy_service.ts | 1 + .../kibana/__mocks__/kibana_migrator.ts | 26 ++++ .../migrations/kibana/kibana_migrator.mock.ts | 18 ++- .../saved_objects_service.mock.ts | 4 +- .../saved_objects_service.test.ts | 34 +++-- .../saved_objects/saved_objects_service.ts | 59 ++++++- .../server/saved_objects/service/index.ts | 10 +- .../service/lib/cache_index_patterns.test.ts | 108 ------------- .../service/lib/cache_index_patterns.ts | 82 ---------- .../service/lib/filter_utils.test.ts | 134 ++++++---------- .../saved_objects/service/lib/filter_utils.ts | 76 +++++---- .../server/saved_objects/service/lib/index.ts | 5 +- .../service/lib/repository.test.js | 144 +++++++++++------- .../saved_objects/service/lib/repository.ts | 34 ++--- .../lib/scoped_client_provider.mock.ts | 30 ++++ .../lib/scoped_client_provider.test.js | 16 +- .../service/lib/scoped_client_provider.ts | 19 ++- .../lib/search_dsl/query_params.test.ts | 39 ----- .../service/lib/search_dsl/query_params.ts | 5 +- .../service/lib/search_dsl/search_dsl.ts | 4 - .../service/saved_objects_client.ts | 2 +- src/core/server/saved_objects/types.ts | 1 + src/core/server/server.api.md | 16 +- src/core/server/server.ts | 45 ++++-- .../saved_objects/saved_objects_mixin.js | 27 +--- .../saved_objects/saved_objects_mixin.test.js | 25 +-- src/plugins/data/server/search/routes.test.ts | 6 +- src/plugins/testbed/server/index.ts | 19 ++- test/api_integration/apis/core/index.js | 14 +- test/api_integration/apis/index.js | 1 + .../apis/saved_objects/find.js | 16 ++ 54 files changed, 743 insertions(+), 585 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsclient._constructor_.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkcreate.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkget.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsclient.create.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsclient.delete.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsclient.errors.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsclient.find.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsclient.get.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsclient.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsclient.update.md create mode 100644 src/core/server/saved_objects/migrations/kibana/__mocks__/kibana_migrator.ts delete mode 100644 src/core/server/saved_objects/service/lib/cache_index_patterns.test.ts delete mode 100644 src/core/server/saved_objects/service/lib/cache_index_patterns.ts create mode 100644 src/core/server/saved_objects/service/lib/scoped_client_provider.mock.ts diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md index a4fa3f17d0d94..866755e78648a 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md @@ -9,5 +9,5 @@ Search for objects Signature: ```typescript -find: (options: Pick) => Promise>; +find: (options: Pick) => Promise>; ``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md index 00a71d25cea38..1d0d942a24c0a 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md @@ -20,7 +20,7 @@ export declare class SavedObjectsClient | [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) | | (objects?: {
id: string;
type: string;
}[]) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>> | Returns an array of objects by id | | [create](./kibana-plugin-public.savedobjectsclient.create.md) | | <T extends SavedObjectAttributes>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>> | Persists an object | | [delete](./kibana-plugin-public.savedobjectsclient.delete.md) | | (type: string, id: string) => Promise<{}> | Deletes an object | -| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "perPage" | "sortField" | "fields" | "searchFields" | "hasReference" | "defaultSearchOperator">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | +| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "searchFields" | "defaultSearchOperator" | "hasReference" | "sortField" | "page" | "perPage" | "fields">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | | [get](./kibana-plugin-public.savedobjectsclient.get.md) | | <T extends SavedObjectAttributes>(type: string, id: string) => Promise<SimpleSavedObject<T>> | Fetches a single object | ## Methods diff --git a/docs/development/core/server/kibana-plugin-server.callapioptions.wrap401errors.md b/docs/development/core/server/kibana-plugin-server.callapioptions.wrap401errors.md index 4436a07d0b28b..296d769533950 100644 --- a/docs/development/core/server/kibana-plugin-server.callapioptions.wrap401errors.md +++ b/docs/development/core/server/kibana-plugin-server.callapioptions.wrap401errors.md @@ -9,5 +9,5 @@ Indicates whether `401 Unauthorized` errors returned from the Elasticsearch API Signature: ```typescript -wrap401Errors: boolean; +wrap401Errors?: boolean; ``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 8a259f6870628..c085d4cdf0d42 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -20,6 +20,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ClusterClient](./kibana-plugin-server.clusterclient.md) | Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)).See [ClusterClient](./kibana-plugin-server.clusterclient.md). | | [ElasticsearchErrorHelpers](./kibana-plugin-server.elasticsearcherrorhelpers.md) | Helpers for working with errors returned from the Elasticsearch service.Since the internal data of errors are subject to change, consumers of the Elasticsearch service should always use these helpers to classify errors instead of checking error internals such as body.error.header[WWW-Authenticate] | | [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | Kibana specific abstraction for an incoming request. | +| [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) | | | [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | | | [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md) | Serves the same purpose as "normal" ClusterClient but exposes additional callAsCurrentUser method that doesn't use credentials of the Kibana internal user (as callAsInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API.See [ScopedClusterClient](./kibana-plugin-server.scopedclusterclient.md). | @@ -77,7 +78,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginManifest](./kibana-plugin-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. | | [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) | | | [PluginsServiceStart](./kibana-plugin-server.pluginsservicestart.md) | | -| [RequestHandlerContext](./kibana-plugin-server.requesthandlercontext.md) | Plugin specific context passed to a route handler. | +| [RequestHandlerContext](./kibana-plugin-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients: - [savedObjects.client](./kibana-plugin-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [elasticsearch.dataClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request | | [RouteConfig](./kibana-plugin-server.routeconfig.md) | Route specific configuration. | | [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) | Additional route options. | | [SavedObject](./kibana-plugin-server.savedobject.md) | | @@ -159,6 +160,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [RouteMethod](./kibana-plugin-server.routemethod.md) | The set of common HTTP methods supported by Kibana routing. | | [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-server.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | -| [SavedObjectsClientContract](./kibana-plugin-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.\#\#\# 503s from missing indexUnlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's action.auto_create_index setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated.See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | +| [SavedObjectsClientContract](./kibana-plugin-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.\#\#\# 503s from missing indexUnlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's action.auto_create_index setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated.See [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | | [SavedObjectsClientWrapperFactory](./kibana-plugin-server.savedobjectsclientwrapperfactory.md) | Describes the factory used to create instances of Saved Objects Client Wrappers. | diff --git a/docs/development/core/server/kibana-plugin-server.requesthandlercontext.core.md b/docs/development/core/server/kibana-plugin-server.requesthandlercontext.core.md index d06b3b9ea637c..e1ea358320b9a 100644 --- a/docs/development/core/server/kibana-plugin-server.requesthandlercontext.core.md +++ b/docs/development/core/server/kibana-plugin-server.requesthandlercontext.core.md @@ -8,6 +8,9 @@ ```typescript core: { + savedObjects: { + client: SavedObjectsClientContract; + }; elasticsearch: { dataClient: IScopedClusterClient; adminClient: IScopedClusterClient; diff --git a/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md b/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md index 85c7989c6e863..37a40f98adef3 100644 --- a/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md +++ b/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md @@ -6,6 +6,8 @@ Plugin specific context passed to a route handler. +Provides the following clients: - [savedObjects.client](./kibana-plugin-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [elasticsearch.dataClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.adminClient](./kibana-plugin-server.scopedclusterclient.md) - Elasticsearch admin client which uses the credentials of the incoming request + Signature: ```typescript @@ -16,5 +18,5 @@ export interface RequestHandlerContext | Property | Type | Description | | --- | --- | --- | -| [core](./kibana-plugin-server.requesthandlercontext.core.md) | {
elasticsearch: {
dataClient: IScopedClusterClient;
adminClient: IScopedClusterClient;
};
} | | +| [core](./kibana-plugin-server.requesthandlercontext.core.md) | {
savedObjects: {
client: SavedObjectsClientContract;
};
elasticsearch: {
dataClient: IScopedClusterClient;
adminClient: IScopedClusterClient;
};
} | | diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient._constructor_.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient._constructor_.md new file mode 100644 index 0000000000000..0bcca3ec57b54 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient._constructor_.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) > [(constructor)](./kibana-plugin-server.savedobjectsclient._constructor_.md) + +## SavedObjectsClient.(constructor) + +Constructs a new instance of the `SavedObjectsClient` class + +Signature: + +```typescript +constructor(repository: SavedObjectsRepository); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| repository | SavedObjectsRepository | | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkcreate.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkcreate.md new file mode 100644 index 0000000000000..1081a91f92762 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkcreate.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) > [bulkCreate](./kibana-plugin-server.savedobjectsclient.bulkcreate.md) + +## SavedObjectsClient.bulkCreate() method + +Persists multiple documents batched together as a single request + +Signature: + +```typescript +bulkCreate(objects: Array>, options?: SavedObjectsCreateOptions): Promise>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| objects | Array<SavedObjectsBulkCreateObject<T>> | | +| options | SavedObjectsCreateOptions | | + +Returns: + +`Promise>` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkget.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkget.md new file mode 100644 index 0000000000000..6fbeadd4ce67c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.bulkget.md @@ -0,0 +1,29 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) > [bulkGet](./kibana-plugin-server.savedobjectsclient.bulkget.md) + +## SavedObjectsClient.bulkGet() method + +Returns an array of objects by id + +Signature: + +```typescript +bulkGet(objects?: SavedObjectsBulkGetObject[], options?: SavedObjectsBaseOptions): Promise>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| objects | SavedObjectsBulkGetObject[] | an array of ids, or an array of objects containing id, type and optionally fields | +| options | SavedObjectsBaseOptions | | + +Returns: + +`Promise>` + +## Example + +bulkGet(\[ { id: 'one', type: 'config' }, { id: 'foo', type: 'index-pattern' } \]) + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.create.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.create.md new file mode 100644 index 0000000000000..68b97ccdf2aef --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.create.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) > [create](./kibana-plugin-server.savedobjectsclient.create.md) + +## SavedObjectsClient.create() method + +Persists a SavedObject + +Signature: + +```typescript +create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| attributes | T | | +| options | SavedObjectsCreateOptions | | + +Returns: + +`Promise>` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.delete.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.delete.md new file mode 100644 index 0000000000000..657f56d591e77 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.delete.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) > [delete](./kibana-plugin-server.savedobjectsclient.delete.md) + +## SavedObjectsClient.delete() method + +Deletes a SavedObject + +Signature: + +```typescript +delete(type: string, id: string, options?: SavedObjectsBaseOptions): Promise<{}>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| id | string | | +| options | SavedObjectsBaseOptions | | + +Returns: + +`Promise<{}>` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.errors.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.errors.md new file mode 100644 index 0000000000000..f08440485c63c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.errors.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) > [errors](./kibana-plugin-server.savedobjectsclient.errors.md) + +## SavedObjectsClient.errors property + +Signature: + +```typescript +static errors: typeof SavedObjectsErrorHelpers; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.find.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.find.md new file mode 100644 index 0000000000000..a590cc4c4b663 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.find.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) > [find](./kibana-plugin-server.savedobjectsclient.find.md) + +## SavedObjectsClient.find() method + +Find all SavedObjects matching the search query + +Signature: + +```typescript +find(options: SavedObjectsFindOptions): Promise>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| options | SavedObjectsFindOptions | | + +Returns: + +`Promise>` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.get.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.get.md new file mode 100644 index 0000000000000..bde16a134f5b5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.get.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) > [get](./kibana-plugin-server.savedobjectsclient.get.md) + +## SavedObjectsClient.get() method + +Retrieves a single object + +Signature: + +```typescript +get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | The type of SavedObject to retrieve | +| id | string | The ID of the SavedObject to retrieve | +| options | SavedObjectsBaseOptions | | + +Returns: + +`Promise>` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.md new file mode 100644 index 0000000000000..0081c729fe10a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.md @@ -0,0 +1,38 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) + +## SavedObjectsClient class + + +Signature: + +```typescript +export declare class SavedObjectsClient +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(repository)](./kibana-plugin-server.savedobjectsclient._constructor_.md) | | Constructs a new instance of the SavedObjectsClient class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [errors](./kibana-plugin-server.savedobjectsclient.errors.md) | | typeof SavedObjectsErrorHelpers | | +| [errors](./kibana-plugin-server.savedobjectsclient.errors.md) | static | typeof SavedObjectsErrorHelpers | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [bulkCreate(objects, options)](./kibana-plugin-server.savedobjectsclient.bulkcreate.md) | | Persists multiple documents batched together as a single request | +| [bulkGet(objects, options)](./kibana-plugin-server.savedobjectsclient.bulkget.md) | | Returns an array of objects by id | +| [create(type, attributes, options)](./kibana-plugin-server.savedobjectsclient.create.md) | | Persists a SavedObject | +| [delete(type, id, options)](./kibana-plugin-server.savedobjectsclient.delete.md) | | Deletes a SavedObject | +| [find(options)](./kibana-plugin-server.savedobjectsclient.find.md) | | Find all SavedObjects matching the search query | +| [get(type, id, options)](./kibana-plugin-server.savedobjectsclient.get.md) | | Retrieves a single object | +| [update(type, id, attributes, options)](./kibana-plugin-server.savedobjectsclient.update.md) | | Updates an SavedObject | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclient.update.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.update.md new file mode 100644 index 0000000000000..16454c98bb55b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclient.update.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) > [update](./kibana-plugin-server.savedobjectsclient.update.md) + +## SavedObjectsClient.update() method + +Updates an SavedObject + +Signature: + +```typescript +update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| id | string | | +| attributes | Partial<T> | | +| options | SavedObjectsUpdateOptions | | + +Returns: + +`Promise>` + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsclientcontract.md b/docs/development/core/server/kibana-plugin-server.savedobjectsclientcontract.md index ae948b090146b..bc5b11dd21b78 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsclientcontract.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsclientcontract.md @@ -34,7 +34,7 @@ From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The obje Unlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's `action.auto_create_index` setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated. -See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) +See [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) Signature: diff --git a/packages/kbn-es-query/src/kuery/ast/ast.d.ts b/packages/kbn-es-query/src/kuery/ast/ast.d.ts index 448ef0e9cca75..06f4940e8ed3b 100644 --- a/packages/kbn-es-query/src/kuery/ast/ast.d.ts +++ b/packages/kbn-es-query/src/kuery/ast/ast.d.ts @@ -37,6 +37,6 @@ export function fromKueryExpression( parseOptions?: KueryParseOptions ): KueryNode; -export function toElasticsearchQuery(node: KueryNode, indexPattern: any): JsonObject; +export function toElasticsearchQuery(node: KueryNode, indexPattern?: any): JsonObject; export function doesKueryExpressionHaveLuceneSyntaxError(expression: string): boolean; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 911b87e2bedf2..0db6e74c7e804 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -768,7 +768,7 @@ export class SavedObjectsClient { }[]) => Promise>; create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; delete: (type: string, id: string) => Promise<{}>; - find: (options: Pick) => Promise>; + find: (options: Pick) => Promise>; get: (type: string, id: string) => Promise>; update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; } diff --git a/src/core/server/elasticsearch/api_types.ts b/src/core/server/elasticsearch/api_types.ts index 8444f97b0c7f0..5f2b61eb4935f 100644 --- a/src/core/server/elasticsearch/api_types.ts +++ b/src/core/server/elasticsearch/api_types.ts @@ -158,7 +158,7 @@ export interface CallAPIOptions { * header that could have been returned by the API itself. If API didn't specify that * then `Basic realm="Authorization Required"` is used as `WWW-Authenticate`. */ - wrap401Errors: boolean; + wrap401Errors?: boolean; /** * A signal object that allows you to abort the request via an AbortController object. */ diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index 1dfa8e3c66d10..00629b811b28f 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -233,9 +233,6 @@ describe('http service', () => { await kbnTestServer.request.get(root, '/new-platform/').expect(200); - // called twice by elasticsearch service in http route handler context provider - expect(clusterClientMock).toBeCalledTimes(2); - // admin client contains authHeaders for BWC with legacy platform. const [adminClient, dataClient] = clusterClientMock.mock.calls; const [, , adminClientHeaders] = adminClient; @@ -259,9 +256,6 @@ describe('http service', () => { .set('Authorization', authorizationHeader) .expect(200); - // called twice by elasticsearch service in http route handler context provider - expect(clusterClientMock).toBeCalledTimes(2); - const [adminClient, dataClient] = clusterClientMock.mock.calls; const [, , adminClientHeaders] = adminClient; expect(adminClientHeaders).toEqual({ authorization: authorizationHeader }); diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 0941153fee87d..d92c92841bb48 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -48,6 +48,7 @@ import { InternalHttpServiceSetup, HttpServiceSetup } from './http'; import { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId } from './plugins'; import { ContextSetup } from './context'; import { SavedObjectsServiceStart } from './saved_objects'; +import { SavedObjectsClientContract } from './saved_objects/types'; export { bootstrap } from './bootstrap'; export { ConfigPath, ConfigService, EnvironmentMode, PackageInfo } from './config'; @@ -180,10 +181,22 @@ export { LegacyServiceSetupDeps, LegacyServiceStartDeps } from './legacy'; /** * Plugin specific context passed to a route handler. + * + * Provides the following clients: + * - {@link SavedObjectsClient | savedObjects.client} - Saved Objects client + * which uses the credentials of the incoming request + * - {@link ScopedClusterClient | elasticsearch.dataClient} - Elasticsearch + * data client which uses the credentials of the incoming request + * - {@link ScopedClusterClient | elasticsearch.adminClient} - Elasticsearch + * admin client which uses the credentials of the incoming request + * * @public */ export interface RequestHandlerContext { core: { + savedObjects: { + client: SavedObjectsClientContract; + }; elasticsearch: { dataClient: IScopedClusterClient; adminClient: IScopedClusterClient; diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index 9b64d7c811aa7..bdb1499e065ca 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -44,8 +44,12 @@ import { HttpServiceStart, BasePathProxyServer } from '../http'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { DiscoveredPlugin, DiscoveredPluginInternal } from '../plugins'; import { PluginsServiceSetup, PluginsServiceStart } from '../plugins/plugins_service'; -import { SavedObjectsServiceStart } from 'src/core/server/saved_objects/saved_objects_service'; +import { + SavedObjectsServiceStart, + SavedObjectsServiceSetup, +} from 'src/core/server/saved_objects/saved_objects_service'; import { KibanaMigrator } from '../saved_objects/migrations'; +import { ISavedObjectsClientProvider } from '../saved_objects'; import { httpServiceMock } from '../http/http_service.mock'; const MockKbnServer: jest.Mock = KbnServer as any; @@ -59,6 +63,7 @@ let setupDeps: { elasticsearch: InternalElasticsearchServiceSetup; http: any; plugins: PluginsServiceSetup; + savedObjects: SavedObjectsServiceSetup; }; plugins: Record; }; @@ -99,6 +104,9 @@ beforeEach(() => { internal: new Map([['plugin-id', {} as DiscoveredPluginInternal]]), }, }, + savedObjects: { + clientProvider: {} as ISavedObjectsClientProvider, + }, }, plugins: { 'plugin-id': 'plugin-value' }, }; @@ -110,6 +118,7 @@ beforeEach(() => { }, savedObjects: { migrator: {} as KibanaMigrator, + clientProvider: {} as ISavedObjectsClientProvider, }, plugins: { contracts: new Map() }, }, diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 52e999f5e03a7..edd913fe16a63 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -275,6 +275,7 @@ export class LegacyService implements CoreService { kibanaMigrator: startDeps.core.savedObjects.migrator, uiPlugins: setupDeps.core.plugins.uiPlugins, elasticsearch: setupDeps.core.elasticsearch, + savedObjectsClientProvider: startDeps.core.savedObjects.clientProvider, }, logger: this.coreContext.logger, }, diff --git a/src/core/server/saved_objects/migrations/kibana/__mocks__/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/__mocks__/kibana_migrator.ts new file mode 100644 index 0000000000000..d9f745810412c --- /dev/null +++ b/src/core/server/saved_objects/migrations/kibana/__mocks__/kibana_migrator.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { mockKibanaMigrator } from '../kibana_migrator.mock'; + +export const mockKibanaMigratorInstance = mockKibanaMigrator.create(); + +const mockConstructor = jest.fn().mockImplementation(() => mockKibanaMigratorInstance); + +export const KibanaMigrator = mockConstructor; diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts index ca732f4f15028..c1021018538d5 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts @@ -17,16 +17,30 @@ * under the License. */ -import { KibanaMigrator, mergeProperties } from './kibana_migrator'; +import { KibanaMigrator } from './kibana_migrator'; import { buildActiveMappings } from '../core'; import { SavedObjectsMapping } from '../../mappings'; +const { mergeProperties } = jest.requireActual('./kibana_migrator'); + +const defaultSavedObjectMappings = [ + { + pluginId: 'testplugin', + properties: { + testtype: { + properties: { + name: { type: 'keyword' }, + }, + }, + }, + }, +]; const createMigrator = ( { savedObjectMappings, }: { savedObjectMappings: SavedObjectsMapping[]; - } = { savedObjectMappings: [] } + } = { savedObjectMappings: defaultSavedObjectMappings } ) => { const mockMigrator: jest.Mocked> = { runMigrations: jest.fn(), diff --git a/src/core/server/saved_objects/saved_objects_service.mock.ts b/src/core/server/saved_objects/saved_objects_service.mock.ts index 5561031d820ec..0a021ee97e26a 100644 --- a/src/core/server/saved_objects/saved_objects_service.mock.ts +++ b/src/core/server/saved_objects/saved_objects_service.mock.ts @@ -19,11 +19,13 @@ import { SavedObjectsService, SavedObjectsServiceStart } from './saved_objects_service'; import { mockKibanaMigrator } from './migrations/kibana/kibana_migrator.mock'; +import { savedObjectsClientProviderMock } from './service/lib/scoped_client_provider.mock'; type SavedObjectsServiceContract = PublicMethodsOf; const createStartContractMock = () => { const startContract: jest.Mocked = { + clientProvider: savedObjectsClientProviderMock.create(), migrator: mockKibanaMigrator.create(), }; @@ -37,7 +39,7 @@ const createsavedObjectsServiceMock = () => { stop: jest.fn(), }; - mocked.setup.mockResolvedValue({}); + mocked.setup.mockResolvedValue({ clientProvider: savedObjectsClientProviderMock.create() }); mocked.start.mockResolvedValue(createStartContractMock()); mocked.stop.mockResolvedValue(); return mocked; diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index ffcfa247f756f..07bb4342c754a 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -21,11 +21,13 @@ jest.mock('./migrations/kibana/kibana_migrator'); import { SavedObjectsService, SavedObjectsSetupDeps } from './saved_objects_service'; import { mockCoreContext } from '../core_context.mock'; -import { KibanaMigrator } from './migrations/kibana/kibana_migrator'; +// @ts-ignore Typescript doesn't know about the jest mock +import { KibanaMigrator, mockKibanaMigratorInstance } from './migrations/kibana/kibana_migrator'; import { of } from 'rxjs'; import * as legacyElasticsearch from 'elasticsearch'; import { Env } from '../config'; import { configServiceMock } from '../mocks'; +import { SavedObjectsClientProvider } from '.'; afterEach(() => { jest.clearAllMocks(); @@ -49,7 +51,7 @@ describe('SavedObjectsService', () => { const soService = new SavedObjectsService(coreContext); const coreSetup = ({ elasticsearch: { adminClient$: of(clusterClient) }, - legacy: { uiExports: {}, pluginExtendedConfig: {} }, + legacy: { uiExports: { savedObjectMappings: [] }, pluginExtendedConfig: {} }, } as unknown) as SavedObjectsSetupDeps; await soService.setup(coreSetup); @@ -58,6 +60,18 @@ describe('SavedObjectsService', () => { 'success' ); }); + + it('resolves with clientProvider', async () => { + const coreContext = mockCoreContext.create(); + const soService = new SavedObjectsService(coreContext); + const coreSetup = ({ + elasticsearch: { adminClient$: of({ callAsInternalUser: jest.fn() }) }, + legacy: { uiExports: {}, pluginExtendedConfig: {} }, + } as unknown) as SavedObjectsSetupDeps; + + const savedObjectsSetup = await soService.setup(coreSetup); + expect(savedObjectsSetup.clientProvider).toBeInstanceOf(SavedObjectsClientProvider); + }); }); describe('#start()', () => { @@ -72,9 +86,8 @@ describe('SavedObjectsService', () => { } as unknown) as SavedObjectsSetupDeps; await soService.setup(coreSetup); - const migrator = (KibanaMigrator as jest.Mock).mock.instances[0]; await soService.start({}); - expect(migrator.runMigrations).toHaveBeenCalledWith(true); + expect(mockKibanaMigratorInstance.runMigrations).toHaveBeenCalledWith(true); }); it('skips KibanaMigrator migrations when migrations.skip=true', async () => { @@ -87,9 +100,8 @@ describe('SavedObjectsService', () => { } as unknown) as SavedObjectsSetupDeps; await soService.setup(coreSetup); - const migrator = (KibanaMigrator as jest.Mock).mock.instances[0]; await soService.start({}); - expect(migrator.runMigrations).toHaveBeenCalledWith(true); + expect(mockKibanaMigratorInstance.runMigrations).toHaveBeenCalledWith(true); }); it('resolves with KibanaMigrator after waiting for migrations to complete', async () => { @@ -102,12 +114,12 @@ describe('SavedObjectsService', () => { } as unknown) as SavedObjectsSetupDeps; await soService.setup(coreSetup); - const migrator = (KibanaMigrator as jest.Mock).mock.instances[0]; - expect(migrator.runMigrations).toHaveBeenCalledTimes(0); + expect(mockKibanaMigratorInstance.runMigrations).toHaveBeenCalledTimes(0); + const startContract = await soService.start({}); - expect(startContract.migrator).toBeInstanceOf(KibanaMigrator); - expect(migrator.runMigrations).toHaveBeenCalledWith(false); - expect(migrator.runMigrations).toHaveBeenCalledTimes(1); + expect(startContract.migrator).toBe(mockKibanaMigratorInstance); + expect(mockKibanaMigratorInstance.runMigrations).toHaveBeenCalledWith(false); + expect(mockKibanaMigratorInstance.runMigrations).toHaveBeenCalledTimes(1); }); }); }); diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index ebcea8dc3b275..5ccb02414d043 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -19,6 +19,15 @@ import { CoreService } from 'src/core/types'; import { first } from 'rxjs/operators'; +import { + SavedObjectsClient, + SavedObjectsSchema, + SavedObjectsRepository, + SavedObjectsSerializer, + SavedObjectsClientProvider, + ISavedObjectsClientProvider, +} from './'; +import { getRootPropertiesObjects } from './mappings'; import { KibanaMigrator, IKibanaMigrator } from './migrations'; import { CoreContext } from '../core_context'; import { LegacyServiceSetup } from '../legacy/legacy_service'; @@ -26,19 +35,22 @@ import { ElasticsearchServiceSetup } from '../elasticsearch'; import { KibanaConfigType } from '../kibana_config'; import { retryCallCluster } from '../elasticsearch/retry_call_cluster'; import { SavedObjectsConfigType } from './saved_objects_config'; +import { KibanaRequest } from '../http'; import { Logger } from '..'; /** * @public */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface SavedObjectsServiceSetup {} +export interface SavedObjectsServiceSetup { + clientProvider: ISavedObjectsClientProvider; +} /** * @public */ export interface SavedObjectsServiceStart { migrator: IKibanaMigrator; + clientProvider: ISavedObjectsClientProvider; } /** @internal */ @@ -54,13 +66,14 @@ export interface SavedObjectsStartDeps {} export class SavedObjectsService implements CoreService { private migrator: KibanaMigrator | undefined; - logger: Logger; + private logger: Logger; + private clientProvider: ISavedObjectsClientProvider | undefined; constructor(private readonly coreContext: CoreContext) { this.logger = coreContext.logger.get('savedobjects-service'); } - public async setup(coreSetup: SavedObjectsSetupDeps) { + public async setup(coreSetup: SavedObjectsSetupDeps): Promise { this.logger.debug('Setting up SavedObjects service'); const { @@ -82,7 +95,7 @@ export class SavedObjectsService .pipe(first()) .toPromise(); - this.migrator = new KibanaMigrator({ + const migrator = (this.migrator = new KibanaMigrator({ savedObjectSchemas, savedObjectMappings, savedObjectMigrations, @@ -93,12 +106,41 @@ export class SavedObjectsService savedObjectsConfig, kibanaConfig, callCluster: retryCallCluster(adminClient.callAsInternalUser), + })); + + const mappings = this.migrator.getActiveMappings(); + const allTypes = Object.keys(getRootPropertiesObjects(mappings)); + const schema = new SavedObjectsSchema(savedObjectSchemas); + const serializer = new SavedObjectsSerializer(schema); + const visibleTypes = allTypes.filter(type => !schema.isHiddenType(type)); + + this.clientProvider = new SavedObjectsClientProvider({ + defaultClientFactory({ request }) { + const repository = new SavedObjectsRepository({ + index: kibanaConfig.index, + config: coreSetup.legacy.pluginExtendedConfig, + migrator, + mappings, + schema, + serializer, + allowedTypes: visibleTypes, + callCluster: retryCallCluster(adminClient.asScoped(request).callAsCurrentUser), + }); + + return new SavedObjectsClient(repository); + }, }); - return ({} as any) as Promise; + return { + clientProvider: this.clientProvider, + }; } public async start(core: SavedObjectsStartDeps): Promise { + if (!this.clientProvider) { + throw new Error('#setup() needs to be run first'); + } + this.logger.debug('Starting SavedObjects service'); /** @@ -119,7 +161,10 @@ export class SavedObjectsService const skipMigrations = cliArgs.optimize || savedObjectsConfig.skip; await this.migrator!.runMigrations(skipMigrations); - return { migrator: this.migrator! }; + return { + migrator: this.migrator!, + clientProvider: this.clientProvider, + }; } public async stop() {} diff --git a/src/core/server/saved_objects/service/index.ts b/src/core/server/saved_objects/service/index.ts index dbf35ff4e134d..15f46711fc94b 100644 --- a/src/core/server/saved_objects/service/index.ts +++ b/src/core/server/saved_objects/service/index.ts @@ -18,7 +18,7 @@ */ import { Readable } from 'stream'; -import { ScopedSavedObjectsClientProvider } from './lib'; +import { SavedObjectsClientProvider } from './lib'; import { SavedObjectsClient } from './saved_objects_client'; import { SavedObjectsExportOptions } from '../export'; import { SavedObjectsImportOptions, SavedObjectsImportResponse } from '../import'; @@ -31,10 +31,10 @@ import { SavedObjectsResolveImportErrorsOptions } from '../import/types'; */ export interface SavedObjectsLegacyService { // ATTENTION: these types are incomplete - addScopedSavedObjectsClientWrapperFactory: ScopedSavedObjectsClientProvider< + addScopedSavedObjectsClientWrapperFactory: SavedObjectsClientProvider< Request >['addClientWrapperFactory']; - getScopedSavedObjectsClient: ScopedSavedObjectsClientProvider['getClient']; + getScopedSavedObjectsClient: SavedObjectsClientProvider['getClient']; SavedObjectsClient: typeof SavedObjectsClient; types: string[]; schema: SavedObjectsSchema; @@ -51,12 +51,12 @@ export interface SavedObjectsLegacyService { export { SavedObjectsRepository, - ScopedSavedObjectsClientProvider, + SavedObjectsClientProvider, + ISavedObjectsClientProvider, SavedObjectsClientProviderOptions, SavedObjectsClientWrapperFactory, SavedObjectsClientWrapperOptions, SavedObjectsErrorHelpers, - SavedObjectsCacheIndexPatterns, } from './lib'; export * from './saved_objects_client'; diff --git a/src/core/server/saved_objects/service/lib/cache_index_patterns.test.ts b/src/core/server/saved_objects/service/lib/cache_index_patterns.test.ts deleted file mode 100644 index e3aeca42d1cf0..0000000000000 --- a/src/core/server/saved_objects/service/lib/cache_index_patterns.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { SavedObjectsCacheIndexPatterns } from './cache_index_patterns'; - -const mockGetFieldsForWildcard = jest.fn(); -const mockIndexPatternsService: jest.Mock = jest.fn().mockImplementation(() => ({ - getFieldsForWildcard: mockGetFieldsForWildcard, - getFieldsForTimePattern: jest.fn(), -})); - -describe('SavedObjectsRepository', () => { - let cacheIndexPatterns: SavedObjectsCacheIndexPatterns; - - const fields = [ - { - aggregatable: true, - name: 'config.type', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'foo.type', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'bar.type', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'baz.type', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'dashboard.otherField', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'hiddenType.someField', - searchable: true, - type: 'string', - }, - ]; - - beforeEach(() => { - cacheIndexPatterns = new SavedObjectsCacheIndexPatterns(); - jest.clearAllMocks(); - }); - - it('setIndexPatterns should return an error object when indexPatternsService is undefined', async () => { - try { - await cacheIndexPatterns.setIndexPatterns('test-index'); - } catch (error) { - expect(error.message).toMatch('indexPatternsService is not defined'); - } - }); - - it('setIndexPatterns should return an error object if getFieldsForWildcard is not defined', async () => { - mockGetFieldsForWildcard.mockImplementation(() => { - throw new Error('something happen'); - }); - try { - cacheIndexPatterns.setIndexPatternsService(new mockIndexPatternsService()); - await cacheIndexPatterns.setIndexPatterns('test-index'); - } catch (error) { - expect(error.message).toMatch('Index Pattern Error - something happen'); - } - }); - - it('setIndexPatterns should return empty array when getFieldsForWildcard is returning null or undefined', async () => { - mockGetFieldsForWildcard.mockImplementation(() => null); - cacheIndexPatterns.setIndexPatternsService(new mockIndexPatternsService()); - await cacheIndexPatterns.setIndexPatterns('test-index'); - expect(cacheIndexPatterns.getIndexPatterns()).toEqual(undefined); - }); - - it('setIndexPatterns should return index pattern when getFieldsForWildcard is returning fields', async () => { - mockGetFieldsForWildcard.mockImplementation(() => fields); - cacheIndexPatterns.setIndexPatternsService(new mockIndexPatternsService()); - await cacheIndexPatterns.setIndexPatterns('test-index'); - expect(cacheIndexPatterns.getIndexPatterns()).toEqual({ fields, title: 'test-index' }); - }); -}); diff --git a/src/core/server/saved_objects/service/lib/cache_index_patterns.ts b/src/core/server/saved_objects/service/lib/cache_index_patterns.ts deleted file mode 100644 index e96cf996f504c..0000000000000 --- a/src/core/server/saved_objects/service/lib/cache_index_patterns.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { FieldDescriptor } from 'src/legacy/server/index_patterns/service/index_patterns_service'; -import { IndexPatternsService } from 'src/legacy/server/index_patterns'; - -export interface SavedObjectsIndexPatternField { - name: string; - type: string; - aggregatable: boolean; - searchable: boolean; -} - -export interface SavedObjectsIndexPattern { - fields: SavedObjectsIndexPatternField[]; - title: string; -} - -export class SavedObjectsCacheIndexPatterns { - private _indexPatterns: SavedObjectsIndexPattern | undefined = undefined; - private _indexPatternsService: IndexPatternsService | undefined = undefined; - - public setIndexPatternsService(indexPatternsService: IndexPatternsService) { - this._indexPatternsService = indexPatternsService; - } - - public getIndexPatternsService() { - return this._indexPatternsService; - } - - public getIndexPatterns(): SavedObjectsIndexPattern | undefined { - return this._indexPatterns; - } - - public async setIndexPatterns(index: string) { - await this._getIndexPattern(index); - } - - private async _getIndexPattern(index: string) { - try { - if (this._indexPatternsService == null) { - throw new TypeError('indexPatternsService is not defined'); - } - const fieldsDescriptor: FieldDescriptor[] = await this._indexPatternsService.getFieldsForWildcard( - { - pattern: index, - } - ); - - this._indexPatterns = - fieldsDescriptor && Array.isArray(fieldsDescriptor) && fieldsDescriptor.length > 0 - ? { - fields: fieldsDescriptor.map(field => ({ - aggregatable: field.aggregatable, - name: field.name, - searchable: field.searchable, - type: field.type, - })), - title: index, - } - : undefined; - } catch (err) { - throw new Error(`Index Pattern Error - ${err.message}`); - } - } -} diff --git a/src/core/server/saved_objects/service/lib/filter_utils.test.ts b/src/core/server/saved_objects/service/lib/filter_utils.test.ts index 73a0804512ed1..80a6a96aeaf2b 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.test.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.test.ts @@ -19,66 +19,51 @@ import { fromKueryExpression } from '@kbn/es-query'; -import { - validateFilterKueryNode, - getSavedObjectTypeIndexPatterns, - validateConvertFilterToKueryNode, -} from './filter_utils'; -import { SavedObjectsIndexPattern } from './cache_index_patterns'; +import { validateFilterKueryNode, validateConvertFilterToKueryNode } from './filter_utils'; -const mockIndexPatterns: SavedObjectsIndexPattern = { - fields: [ - { - name: 'updatedAt', +const mockMappings = { + properties: { + updatedAt: { type: 'date', - aggregatable: true, - searchable: true, }, - { - name: 'foo.title', - type: 'text', - aggregatable: true, - searchable: true, - }, - { - name: 'foo.description', - type: 'text', - aggregatable: true, - searchable: true, - }, - { - name: 'foo.bytes', - type: 'number', - aggregatable: true, - searchable: true, - }, - { - name: 'bar.foo', - type: 'text', - aggregatable: true, - searchable: true, + foo: { + properties: { + title: { + type: 'text', + }, + description: { + type: 'text', + }, + bytes: { + type: 'number', + }, + }, }, - { - name: 'bar.description', - type: 'text', - aggregatable: true, - searchable: true, + bar: { + properties: { + foo: { + type: 'text', + }, + description: { + type: 'text', + }, + }, }, - { - name: 'hiddentype.description', - type: 'text', - aggregatable: true, - searchable: true, + hiddenType: { + properties: { + description: { + type: 'text', + }, + }, }, - ], - title: 'mock', + }, }; describe('Filter Utils', () => { describe('#validateConvertFilterToKueryNode', () => { test('Validate a simple filter', () => { expect( - validateConvertFilterToKueryNode(['foo'], 'foo.attributes.title: "best"', mockIndexPatterns) + validateConvertFilterToKueryNode(['foo'], 'foo.attributes.title: "best"', mockMappings) ).toEqual(fromKueryExpression('foo.title: "best"')); }); test('Assemble filter kuery node saved object attributes with one saved object type', () => { @@ -86,7 +71,7 @@ describe('Filter Utils', () => { validateConvertFilterToKueryNode( ['foo'], 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)', - mockIndexPatterns + mockMappings ) ).toEqual( fromKueryExpression( @@ -100,7 +85,7 @@ describe('Filter Utils', () => { validateConvertFilterToKueryNode( ['foo', 'bar'], 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)', - mockIndexPatterns + mockMappings ) ).toEqual( fromKueryExpression( @@ -114,7 +99,7 @@ describe('Filter Utils', () => { validateConvertFilterToKueryNode( ['foo', 'bar'], '(bar.updatedAt: 5678654567 OR foo.updatedAt: 5678654567) and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or bar.attributes.description :*)', - mockIndexPatterns + mockMappings ) ).toEqual( fromKueryExpression( @@ -128,7 +113,7 @@ describe('Filter Utils', () => { validateConvertFilterToKueryNode( ['foo', 'bar'], 'updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)', - mockIndexPatterns + mockMappings ); }).toThrowErrorMatchingInlineSnapshot( `"This key 'updatedAt' need to be wrapped by a saved object type like foo,bar: Bad Request"` @@ -137,7 +122,7 @@ describe('Filter Utils', () => { test('Lets make sure that we are throwing an exception if we are using hiddentype with types', () => { expect(() => { - validateConvertFilterToKueryNode([], 'hiddentype.title: "title"', mockIndexPatterns); + validateConvertFilterToKueryNode([], 'hiddentype.title: "title"', mockMappings); }).toThrowErrorMatchingInlineSnapshot(`"This type hiddentype is not allowed: Bad Request"`); }); }); @@ -149,7 +134,7 @@ describe('Filter Utils', () => { 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' ), ['foo'], - getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns) + mockMappings ); expect(validationObject).toEqual([ @@ -204,7 +189,7 @@ describe('Filter Utils', () => { 'updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' ), ['foo'], - getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns) + mockMappings ); expect(validationObject).toEqual([ @@ -259,7 +244,7 @@ describe('Filter Utils', () => { 'foo.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.description :*)' ), ['foo'], - getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns) + mockMappings ); expect(validationObject).toEqual([ @@ -316,7 +301,7 @@ describe('Filter Utils', () => { 'bar.updatedAt: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' ), ['foo'], - getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns) + mockMappings ); expect(validationObject).toEqual([ @@ -371,7 +356,7 @@ describe('Filter Utils', () => { 'foo.updatedAt33: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.header: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' ), ['foo'], - getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns) + mockMappings ); expect(validationObject).toEqual([ @@ -421,37 +406,4 @@ describe('Filter Utils', () => { ]); }); }); - - describe('#getSavedObjectTypeIndexPatterns', () => { - test('Get index patterns related to your type', () => { - const indexPatternsFilterByType = getSavedObjectTypeIndexPatterns(['foo'], mockIndexPatterns); - - expect(indexPatternsFilterByType).toEqual([ - { - name: 'updatedAt', - type: 'date', - aggregatable: true, - searchable: true, - }, - { - name: 'foo.title', - type: 'text', - aggregatable: true, - searchable: true, - }, - { - name: 'foo.description', - type: 'text', - aggregatable: true, - searchable: true, - }, - { - name: 'foo.bytes', - type: 'number', - aggregatable: true, - searchable: true, - }, - ]); - }); - }); }); diff --git a/src/core/server/saved_objects/service/lib/filter_utils.ts b/src/core/server/saved_objects/service/lib/filter_utils.ts index 2397971e66966..64abf268cacd6 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.ts @@ -19,23 +19,21 @@ import { fromKueryExpression, KueryNode, nodeTypes } from '@kbn/es-query'; import { get, set } from 'lodash'; - -import { SavedObjectsIndexPattern, SavedObjectsIndexPatternField } from './cache_index_patterns'; import { SavedObjectsErrorHelpers } from './errors'; +import { IndexMapping } from '../../mappings'; export const validateConvertFilterToKueryNode = ( - types: string[], + allowedTypes: string[], filter: string, - indexPattern: SavedObjectsIndexPattern | undefined + indexMapping: IndexMapping ): KueryNode => { - if (filter && filter.length > 0 && indexPattern) { + if (filter && filter.length > 0 && indexMapping) { const filterKueryNode = fromKueryExpression(filter); - const typeIndexPatterns = getSavedObjectTypeIndexPatterns(types, indexPattern); const validationFilterKuery = validateFilterKueryNode( filterKueryNode, - types, - typeIndexPatterns, + allowedTypes, + indexMapping, filterKueryNode.type === 'function' && ['is', 'range'].includes(filterKueryNode.function) ); @@ -60,7 +58,7 @@ export const validateConvertFilterToKueryNode = ( path.length === 0 ? filterKueryNode : get(filterKueryNode, path); if (item.isSavedObjectAttr) { existingKueryNode.arguments[0].value = existingKueryNode.arguments[0].value.split('.')[1]; - const itemType = types.filter(t => t === item.type); + const itemType = allowedTypes.filter(t => t === item.type); if (itemType.length === 1) { set( filterKueryNode, @@ -84,18 +82,6 @@ export const validateConvertFilterToKueryNode = ( return null; }; -export const getSavedObjectTypeIndexPatterns = ( - types: string[], - indexPattern: SavedObjectsIndexPattern | undefined -): SavedObjectsIndexPatternField[] => { - return indexPattern != null - ? indexPattern.fields.filter( - ip => - !ip.name.includes('.') || (ip.name.includes('.') && types.includes(ip.name.split('.')[0])) - ) - : []; -}; - interface ValidateFilterKueryNode { astPath: string; error: string; @@ -107,7 +93,7 @@ interface ValidateFilterKueryNode { export const validateFilterKueryNode = ( astFilter: KueryNode, types: string[], - typeIndexPatterns: SavedObjectsIndexPatternField[], + indexMapping: IndexMapping, storeValue: boolean = false, path: string = 'arguments' ): ValidateFilterKueryNode[] => { @@ -119,7 +105,7 @@ export const validateFilterKueryNode = ( ...validateFilterKueryNode( ast, types, - typeIndexPatterns, + indexMapping, ast.type === 'function' && ['is', 'range'].includes(ast.function), `${myPath}.arguments` ), @@ -131,8 +117,8 @@ export const validateFilterKueryNode = ( ...kueryNode, { astPath: splitPath.slice(0, splitPath.length - 1).join('.'), - error: hasFilterKeyError(ast.value, types, typeIndexPatterns), - isSavedObjectAttr: isSavedObjectAttr(ast.value, typeIndexPatterns), + error: hasFilterKeyError(ast.value, types, indexMapping), + isSavedObjectAttr: isSavedObjectAttr(ast.value, indexMapping), key: ast.value, type: getType(ast.value), }, @@ -144,47 +130,55 @@ export const validateFilterKueryNode = ( const getType = (key: string) => (key.includes('.') ? key.split('.')[0] : null); -export const isSavedObjectAttr = ( - key: string, - typeIndexPatterns: SavedObjectsIndexPatternField[] -) => { - const splitKey = key.split('.'); - if (splitKey.length === 1 && typeIndexPatterns.some(tip => tip.name === splitKey[0])) { +/** + * Is this filter key referring to a a top-level SavedObject attribute such as + * `updated_at` or `references`. + * + * @param key + * @param indexMapping + */ +export const isSavedObjectAttr = (key: string, indexMapping: IndexMapping) => { + const keySplit = key.split('.'); + if (keySplit.length === 1 && fieldDefined(indexMapping, keySplit[0])) { return true; - } else if (splitKey.length > 1 && typeIndexPatterns.some(tip => tip.name === splitKey[1])) { + } else if (keySplit.length === 2 && fieldDefined(indexMapping, keySplit[1])) { return true; + } else { + return false; } - return false; }; export const hasFilterKeyError = ( key: string, types: string[], - typeIndexPatterns: SavedObjectsIndexPatternField[] + indexMapping: IndexMapping ): string | null => { if (!key.includes('.')) { return `This key '${key}' need to be wrapped by a saved object type like ${types.join()}`; } else if (key.includes('.')) { const keySplit = key.split('.'); + if (keySplit.length <= 1 || !types.includes(keySplit[0])) { return `This type ${keySplit[0]} is not allowed`; } if ( - (keySplit.length === 2 && typeIndexPatterns.some(tip => tip.name === key)) || - (keySplit.length > 2 && types.includes(keySplit[0]) && keySplit[1] !== 'attributes') + (keySplit.length === 2 && fieldDefined(indexMapping, key)) || + (keySplit.length > 2 && keySplit[1] !== 'attributes') ) { return `This key '${key}' does NOT match the filter proposition SavedObjectType.attributes.key`; } if ( - (keySplit.length === 2 && !typeIndexPatterns.some(tip => tip.name === keySplit[1])) || + (keySplit.length === 2 && !fieldDefined(indexMapping, keySplit[1])) || (keySplit.length > 2 && - !typeIndexPatterns.some( - tip => - tip.name === [...keySplit.slice(0, 1), ...keySplit.slice(2, keySplit.length)].join('.') - )) + !fieldDefined(indexMapping, keySplit[0] + '.' + keySplit.slice(2, keySplit.length))) ) { return `This key '${key}' does NOT exist in ${types.join()} saved object index patterns`; } } return null; }; + +const fieldDefined = (indexMappings: IndexMapping, key: string) => { + const mappingKey = 'properties.' + key.split('.').join('.properties.'); + return get(indexMappings, mappingKey) != null; +}; diff --git a/src/core/server/saved_objects/service/lib/index.ts b/src/core/server/saved_objects/service/lib/index.ts index be78fdde76210..4bc159e17ec0f 100644 --- a/src/core/server/saved_objects/service/lib/index.ts +++ b/src/core/server/saved_objects/service/lib/index.ts @@ -21,10 +21,9 @@ export { SavedObjectsRepository, SavedObjectsRepositoryOptions } from './reposit export { SavedObjectsClientWrapperFactory, SavedObjectsClientWrapperOptions, - ScopedSavedObjectsClientProvider, + ISavedObjectsClientProvider, + SavedObjectsClientProvider, SavedObjectsClientProviderOptions, } from './scoped_client_provider'; export { SavedObjectsErrorHelpers } from './errors'; - -export { SavedObjectsCacheIndexPatterns } from './cache_index_patterns'; diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index dcabb90c0a13d..7e89b8f3c0820 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -273,10 +273,6 @@ describe('SavedObjectsRepository', () => { savedObjectsRepository = new SavedObjectsRepository({ index: '.kibana-test', - cacheIndexPatterns: { - setIndexPatterns: jest.fn(), - getIndexPatterns: () => undefined, - }, mappings, callCluster: callAdminCluster, migrator, @@ -290,8 +286,6 @@ describe('SavedObjectsRepository', () => { getSearchDslNS.getSearchDsl.mockReset(); }); - afterEach(() => { }); - describe('#create', () => { beforeEach(() => { callAdminCluster.mockImplementation((method, params) => ({ @@ -302,9 +296,7 @@ describe('SavedObjectsRepository', () => { }); it('waits until migrations are complete before proceeding', async () => { - migrator.runMigrations = jest.fn(async () => - expect(callAdminCluster).not.toHaveBeenCalled() - ); + migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled()); await expect( savedObjectsRepository.create( @@ -557,9 +549,7 @@ describe('SavedObjectsRepository', () => { describe('#bulkCreate', () => { it('waits until migrations are complete before proceeding', async () => { - migrator.runMigrations = jest.fn(async () => - expect(callAdminCluster).not.toHaveBeenCalled() - ); + migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled()); callAdminCluster.mockReturnValue({ items: [ { create: { type: 'config', id: 'config:one', _primary_term: 1, _seq_no: 1 } }, @@ -998,14 +988,12 @@ describe('SavedObjectsRepository', () => { expect(onBeforeWrite).toHaveBeenCalledTimes(1); }); - it('should return objects in the same order regardless of type', () => { }); + it('should return objects in the same order regardless of type', () => {}); }); describe('#delete', () => { it('waits until migrations are complete before proceeding', async () => { - migrator.runMigrations = jest.fn(async () => - expect(callAdminCluster).not.toHaveBeenCalled() - ); + migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled()); callAdminCluster.mockReturnValue({ result: 'deleted' }); await expect( savedObjectsRepository.delete('index-pattern', 'logstash-*', { @@ -1119,9 +1107,7 @@ describe('SavedObjectsRepository', () => { describe('#find', () => { it('waits until migrations are complete before proceeding', async () => { - migrator.runMigrations = jest.fn(async () => - expect(callAdminCluster).not.toHaveBeenCalled() - ); + migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled()); callAdminCluster.mockReturnValue(noNamespaceSearchResults); await expect(savedObjectsRepository.find({ type: 'foo' })).resolves.toBeDefined(); @@ -1159,13 +1145,6 @@ describe('SavedObjectsRepository', () => { } }); - it('requires index pattern to be defined if filter is defined', async () => { - callAdminCluster.mockReturnValue(noNamespaceSearchResults); - expect(savedObjectsRepository.find({ type: 'foo', filter: 'foo.type: hello' })) - .rejects - .toThrowErrorMatchingInlineSnapshot('"options.filter is missing index pattern to work correctly: Bad Request"'); - }); - it('passes mappings, schema, search, defaultSearchOperator, searchFields, type, sortField, sortOrder and hasReference to getSearchDsl', async () => { callAdminCluster.mockReturnValue(namespacedSearchResults); @@ -1190,6 +1169,75 @@ describe('SavedObjectsRepository', () => { expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, schema, relevantOpts); }); + it('accepts KQL filter and passes keuryNode to getSearchDsl', async () => { + callAdminCluster.mockReturnValue(namespacedSearchResults); + const findOpts = { + namespace: 'foo-namespace', + search: 'foo*', + searchFields: ['foo'], + type: ['dashboard'], + sortField: 'name', + sortOrder: 'desc', + defaultSearchOperator: 'AND', + hasReference: { + type: 'foo', + id: '1', + }, + indexPattern: undefined, + filter: 'dashboard.attributes.otherField: *', + }; + + await savedObjectsRepository.find(findOpts); + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledTimes(1); + const { kueryNode } = getSearchDslNS.getSearchDsl.mock.calls[0][2]; + expect(kueryNode).toMatchInlineSnapshot(` + Object { + "arguments": Array [ + Object { + "type": "literal", + "value": "dashboard.otherField", + }, + Object { + "type": "wildcard", + "value": "@kuery-wildcard@", + }, + Object { + "type": "literal", + "value": false, + }, + ], + "function": "is", + "type": "function", + } + `); + }); + + it('KQL filter syntax errors rejects with bad request', async () => { + callAdminCluster.mockReturnValue(namespacedSearchResults); + const findOpts = { + namespace: 'foo-namespace', + search: 'foo*', + searchFields: ['foo'], + type: ['dashboard'], + sortField: 'name', + sortOrder: 'desc', + defaultSearchOperator: 'AND', + hasReference: { + type: 'foo', + id: '1', + }, + indexPattern: undefined, + filter: 'dashboard.attributes.otherField:<', + }; + + await expect(savedObjectsRepository.find(findOpts)).rejects.toMatchInlineSnapshot(` + [Error: KQLSyntaxError: Expected "(", value, whitespace but "<" found. + dashboard.attributes.otherField:< + --------------------------------^: Bad Request] + `); + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledTimes(0); + }); + it('merges output of getSearchDsl into es request body', async () => { callAdminCluster.mockReturnValue(noNamespaceSearchResults); getSearchDslNS.getSearchDsl.mockReturnValue({ query: 1, aggregations: 2 }); @@ -1329,9 +1377,7 @@ describe('SavedObjectsRepository', () => { }; it('waits until migrations are complete before proceeding', async () => { - migrator.runMigrations = jest.fn(async () => - expect(callAdminCluster).not.toHaveBeenCalled() - ); + migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled()); callAdminCluster.mockResolvedValue(noNamespaceResult); await expect( @@ -1422,9 +1468,7 @@ describe('SavedObjectsRepository', () => { describe('#bulkGet', () => { it('waits until migrations are complete before proceeding', async () => { - migrator.runMigrations = jest.fn(async () => - expect(callAdminCluster).not.toHaveBeenCalled() - ); + migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled()); callAdminCluster.mockReturnValue({ docs: [] }); await expect( @@ -1676,9 +1720,7 @@ describe('SavedObjectsRepository', () => { }); it('waits until migrations are complete before proceeding', async () => { - migrator.runMigrations = jest.fn(async () => - expect(callAdminCluster).not.toHaveBeenCalled() - ); + migrator.runMigrations = jest.fn(async () => expect(callAdminCluster).not.toHaveBeenCalled()); await expect( savedObjectsRepository.update('index-pattern', 'logstash-*', attributes, { @@ -1745,11 +1787,7 @@ describe('SavedObjectsRepository', () => { }); it('does not pass references if omitted', async () => { - await savedObjectsRepository.update( - type, - id, - { title: 'Testing' } - ); + await savedObjectsRepository.update(type, id, { title: 'Testing' }); expect(callAdminCluster).toHaveBeenCalledTimes(1); expect(callAdminCluster).not.toHaveBeenCalledWith( @@ -1758,19 +1796,14 @@ describe('SavedObjectsRepository', () => { body: { doc: expect.objectContaining({ references: [], - }) - } + }), + }, }) ); }); it('passes references if they are provided', async () => { - await savedObjectsRepository.update( - type, - id, - { title: 'Testing' }, - { references: ['foo'] } - ); + await savedObjectsRepository.update(type, id, { title: 'Testing' }, { references: ['foo'] }); expect(callAdminCluster).toHaveBeenCalledTimes(1); expect(callAdminCluster).toHaveBeenCalledWith( @@ -1779,19 +1812,14 @@ describe('SavedObjectsRepository', () => { body: { doc: expect.objectContaining({ references: ['foo'], - }) - } + }), + }, }) ); }); it('passes empty references array if empty references array is provided', async () => { - await savedObjectsRepository.update( - type, - id, - { title: 'Testing' }, - { references: [] } - ); + await savedObjectsRepository.update(type, id, { title: 'Testing' }, { references: [] }); expect(callAdminCluster).toHaveBeenCalledTimes(1); expect(callAdminCluster).toHaveBeenCalledWith( @@ -1800,8 +1828,8 @@ describe('SavedObjectsRepository', () => { body: { doc: expect.objectContaining({ references: [], - }) - } + }), + }, }) ); }); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index cc5b9804769dd..b4723f35b1efc 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -25,7 +25,6 @@ import { getSearchDsl } from './search_dsl'; import { includedFields } from './included_fields'; import { decorateEsError } from './decorate_es_error'; import { SavedObjectsErrorHelpers } from './errors'; -import { SavedObjectsCacheIndexPatterns } from './cache_index_patterns'; import { decodeRequestVersion, encodeVersion, encodeHitVersion } from '../../version'; import { SavedObjectsSchema } from '../../schema'; import { KibanaMigrator } from '../../migrations'; @@ -77,7 +76,6 @@ export interface SavedObjectsRepositoryOptions { serializer: SavedObjectsSerializer; migrator: KibanaMigrator; allowedTypes: string[]; - cacheIndexPatterns: SavedObjectsCacheIndexPatterns; onBeforeWrite?: (...args: Parameters) => Promise; } @@ -95,13 +93,11 @@ export class SavedObjectsRepository { private _onBeforeWrite: (...args: Parameters) => Promise; private _unwrappedCallCluster: CallCluster; private _serializer: SavedObjectsSerializer; - private _cacheIndexPatterns: SavedObjectsCacheIndexPatterns; constructor(options: SavedObjectsRepositoryOptions) { const { index, config, - cacheIndexPatterns, mappings, callCluster, schema, @@ -123,7 +119,6 @@ export class SavedObjectsRepository { this._config = config; this._mappings = mappings; this._schema = schema; - this._cacheIndexPatterns = cacheIndexPatterns; if (allowedTypes.length === 0) { throw new Error('Empty or missing types for saved object repository!'); } @@ -133,9 +128,6 @@ export class SavedObjectsRepository { this._unwrappedCallCluster = async (...args: Parameters) => { await migrator.runMigrations(); - if (this._cacheIndexPatterns.getIndexPatterns() == null) { - await this._cacheIndexPatterns.setIndexPatterns(index); - } return callCluster(...args); }; this._schema = schema; @@ -441,21 +433,20 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createBadRequestError('options.fields must be an array'); } - if (filter && filter !== '' && this._cacheIndexPatterns.getIndexPatterns() == null) { - throw SavedObjectsErrorHelpers.createBadRequestError( - 'options.filter is missing index pattern to work correctly' - ); + let kueryNode; + try { + kueryNode = + filter && filter !== '' + ? validateConvertFilterToKueryNode(allowedTypes, filter, this._mappings) + : null; + } catch (e) { + if (e.name === 'KQLSyntaxError') { + throw SavedObjectsErrorHelpers.createBadRequestError('KQLSyntaxError: ' + e.message); + } else { + throw e; + } } - const kueryNode = - filter && filter !== '' - ? validateConvertFilterToKueryNode( - allowedTypes, - filter, - this._cacheIndexPatterns.getIndexPatterns() - ) - : null; - const esOptions = { index: this.getIndicesForTypes(allowedTypes), size: perPage, @@ -474,7 +465,6 @@ export class SavedObjectsRepository { sortOrder, namespace, hasReference, - indexPattern: kueryNode != null ? this._cacheIndexPatterns.getIndexPatterns() : undefined, kueryNode, }), }, diff --git a/src/core/server/saved_objects/service/lib/scoped_client_provider.mock.ts b/src/core/server/saved_objects/service/lib/scoped_client_provider.mock.ts new file mode 100644 index 0000000000000..5de234f4f93be --- /dev/null +++ b/src/core/server/saved_objects/service/lib/scoped_client_provider.mock.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 { ISavedObjectsClientProvider } from './scoped_client_provider'; + +const create = (): jest.Mocked => ({ + addClientWrapperFactory: jest.fn(), + getClient: jest.fn(), + setClientFactory: jest.fn(), +}); + +export const savedObjectsClientProviderMock = { + create, +}; diff --git a/src/core/server/saved_objects/service/lib/scoped_client_provider.test.js b/src/core/server/saved_objects/service/lib/scoped_client_provider.test.js index bce76bc7841e6..eb210b6843de0 100644 --- a/src/core/server/saved_objects/service/lib/scoped_client_provider.test.js +++ b/src/core/server/saved_objects/service/lib/scoped_client_provider.test.js @@ -17,14 +17,14 @@ * under the License. */ -import { ScopedSavedObjectsClientProvider } from './scoped_client_provider'; +import { SavedObjectsClientProvider } from './scoped_client_provider'; test(`uses default client factory when one isn't set`, () => { const returnValue = Symbol(); const defaultClientFactoryMock = jest.fn().mockReturnValue(returnValue); const request = Symbol(); - const clientProvider = new ScopedSavedObjectsClientProvider({ + const clientProvider = new SavedObjectsClientProvider({ defaultClientFactory: defaultClientFactoryMock, }); const result = clientProvider.getClient(request); @@ -42,7 +42,7 @@ test(`uses custom client factory when one is set`, () => { const returnValue = Symbol(); const customClientFactoryMock = jest.fn().mockReturnValue(returnValue); - const clientProvider = new ScopedSavedObjectsClientProvider({ + const clientProvider = new SavedObjectsClientProvider({ defaultClientFactory: defaultClientFactoryMock, }); clientProvider.setClientFactory(customClientFactoryMock); @@ -57,7 +57,7 @@ test(`uses custom client factory when one is set`, () => { }); test(`throws error when more than one scoped saved objects client factory is set`, () => { - const clientProvider = new ScopedSavedObjectsClientProvider({}); + const clientProvider = new SavedObjectsClientProvider({}); clientProvider.setClientFactory(() => {}); expect(() => { clientProvider.setClientFactory(() => {}); @@ -66,7 +66,7 @@ test(`throws error when more than one scoped saved objects client factory is set test(`throws error when registering a wrapper with a duplicate id`, () => { const defaultClientFactoryMock = jest.fn(); - const clientProvider = new ScopedSavedObjectsClientProvider({ + const clientProvider = new SavedObjectsClientProvider({ defaultClientFactory: defaultClientFactoryMock, }); const firstClientWrapperFactoryMock = jest.fn(); @@ -81,7 +81,7 @@ test(`throws error when registering a wrapper with a duplicate id`, () => { test(`invokes and uses wrappers in specified order`, () => { const defaultClient = Symbol(); const defaultClientFactoryMock = jest.fn().mockReturnValue(defaultClient); - const clientProvider = new ScopedSavedObjectsClientProvider({ + const clientProvider = new SavedObjectsClientProvider({ defaultClientFactory: defaultClientFactoryMock, }); const firstWrappedClient = Symbol('first client'); @@ -108,7 +108,7 @@ test(`invokes and uses wrappers in specified order`, () => { test(`does not invoke or use excluded wrappers`, () => { const defaultClient = Symbol(); const defaultClientFactoryMock = jest.fn().mockReturnValue(defaultClient); - const clientProvider = new ScopedSavedObjectsClientProvider({ + const clientProvider = new SavedObjectsClientProvider({ defaultClientFactory: defaultClientFactoryMock, }); const firstWrappedClient = Symbol('first client'); @@ -135,7 +135,7 @@ test(`does not invoke or use excluded wrappers`, () => { test(`allows all wrappers to be excluded`, () => { const defaultClient = Symbol(); const defaultClientFactoryMock = jest.fn().mockReturnValue(defaultClient); - const clientProvider = new ScopedSavedObjectsClientProvider({ + const clientProvider = new SavedObjectsClientProvider({ defaultClientFactory: defaultClientFactoryMock, }); const firstWrappedClient = Symbol('first client'); diff --git a/src/core/server/saved_objects/service/lib/scoped_client_provider.ts b/src/core/server/saved_objects/service/lib/scoped_client_provider.ts index 0e93f9c443f14..ad1ceb60cdb86 100644 --- a/src/core/server/saved_objects/service/lib/scoped_client_provider.ts +++ b/src/core/server/saved_objects/service/lib/scoped_client_provider.ts @@ -55,9 +55,24 @@ export interface SavedObjectsClientProviderOptions { } /** - * Provider for the Scoped Saved Object Client. + * @public + * See {@link SavedObjectsClientProvider} + */ +export type ISavedObjectsClientProvider = Pick< + SavedObjectsClientProvider, + keyof SavedObjectsClientProvider +>; + +/** + * Provider for the Scoped Saved Objects Client. + * + * @internalRemarks Because `getClient` is synchronous the Client Provider does + * not support creating factories that react to new ES clients emitted from + * elasticsearch.adminClient$. The Client Provider therefore doesn't support + * configuration changes to the Elasticsearch client. TODO: revisit once we've + * closed https://github.com/elastic/kibana/pull/45796 */ -export class ScopedSavedObjectsClientProvider { +export class SavedObjectsClientProvider { private readonly _wrapperFactories = new PriorityCollection<{ id: string; factory: SavedObjectsClientWrapperFactory; diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts index 75b3058029227..e585953109d2c 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts @@ -18,7 +18,6 @@ */ import { schemaMock } from '../../../schema/schema.mock'; -import { SavedObjectsIndexPattern } from '../cache_index_patterns'; import { getQueryParams } from './query_params'; const SCHEMA = schemaMock.create(); @@ -62,41 +61,6 @@ const MAPPINGS = { }, }, }; -const INDEX_PATTERN: SavedObjectsIndexPattern = { - fields: [ - { - aggregatable: true, - name: 'type', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'pending.title', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'saved.title', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'saved.obj.key1', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'global.name', - searchable: true, - type: 'string', - }, - ], - title: 'test', -}; // create a type clause to be used within the "should", if a namespace is specified // the clause will ensure the namespace matches; otherwise, the clause will ensure @@ -1005,7 +969,6 @@ describe('searchDsl/queryParams', () => { { type: 'literal', value: false }, ], }, - indexPattern: INDEX_PATTERN, }) ).toEqual({ query: { @@ -1121,7 +1084,6 @@ describe('searchDsl/queryParams', () => { }, ], }, - indexPattern: INDEX_PATTERN, }) ).toEqual({ query: { @@ -1240,7 +1202,6 @@ describe('searchDsl/queryParams', () => { { type: 'literal', value: false }, ], }, - indexPattern: INDEX_PATTERN, }) ).toEqual({ query: { diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts index 125b0c40af9e4..bee35b899d83c 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts @@ -20,7 +20,6 @@ import { toElasticsearchQuery, KueryNode } from '@kbn/es-query'; import { getRootPropertiesObjects, IndexMapping } from '../../../mappings'; import { SavedObjectsSchema } from '../../../schema'; -import { SavedObjectsIndexPattern } from '../cache_index_patterns'; /** * Gets the types based on the type. Uses mappings to support @@ -93,7 +92,6 @@ interface QueryParams { defaultSearchOperator?: string; hasReference?: HasReferenceQueryParams; kueryNode?: KueryNode; - indexPattern?: SavedObjectsIndexPattern; } /** @@ -109,12 +107,11 @@ export function getQueryParams({ defaultSearchOperator, hasReference, kueryNode, - indexPattern, }: QueryParams) { const types = getTypes(mappings, type); const bool: any = { filter: [ - ...(kueryNode != null ? [toElasticsearchQuery(kueryNode, indexPattern)] : []), + ...(kueryNode != null ? [toElasticsearchQuery(kueryNode)] : []), { bool: { must: hasReference diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts index 68f6060702505..868ca51a76eab 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts @@ -24,7 +24,6 @@ import { IndexMapping } from '../../../mappings'; import { SavedObjectsSchema } from '../../../schema'; import { getQueryParams } from './query_params'; import { getSortingParams } from './sorting_params'; -import { SavedObjectsIndexPattern } from '../cache_index_patterns'; interface GetSearchDslOptions { type: string | string[]; @@ -39,7 +38,6 @@ interface GetSearchDslOptions { id: string; }; kueryNode?: KueryNode; - indexPattern?: SavedObjectsIndexPattern; } export function getSearchDsl( @@ -57,7 +55,6 @@ export function getSearchDsl( namespace, hasReference, kueryNode, - indexPattern, } = options; if (!type) { @@ -79,7 +76,6 @@ export function getSearchDsl( defaultSearchOperator, hasReference, kueryNode, - indexPattern, }), ...getSortingParams(mappings, type, sortField, sortOrder), }; diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index 634f5ba007397..b83d72f9ad46d 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -119,7 +119,7 @@ export interface SavedObjectsUpdateResponse saved_objects: Array>; } -// @internal (undocumented) +// @public (undocumented) export class SavedObjectsClient { // Warning: (ae-forgotten-export) The symbol "SavedObjectsRepository" needs to be exported by the entry point index.d.ts constructor(repository: SavedObjectsRepository); @@ -1180,8 +1182,6 @@ export class SavedObjectsClient { update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; } -// Warning: (ae-incompatible-release-tags) The symbol "SavedObjectsClientContract" is marked as @public, but its signature references "SavedObjectsClient" which is marked as @internal -// // @public export type SavedObjectsClientContract = Pick; @@ -1407,14 +1407,14 @@ export interface SavedObjectsImportUnsupportedTypeError { // @internal @deprecated (undocumented) export interface SavedObjectsLegacyService { - // Warning: (ae-forgotten-export) The symbol "ScopedSavedObjectsClientProvider" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "SavedObjectsClientProvider" needs to be exported by the entry point index.d.ts // // (undocumented) - addScopedSavedObjectsClientWrapperFactory: ScopedSavedObjectsClientProvider['addClientWrapperFactory']; + addScopedSavedObjectsClientWrapperFactory: SavedObjectsClientProvider['addClientWrapperFactory']; // (undocumented) getSavedObjectsRepository(...rest: any[]): any; // (undocumented) - getScopedSavedObjectsClient: ScopedSavedObjectsClientProvider['getClient']; + getScopedSavedObjectsClient: SavedObjectsClientProvider['getClient']; // (undocumented) importExport: { objectLimit: number; diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 81d30a367261d..455bb97d47b57 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -21,7 +21,7 @@ import { take } from 'rxjs/operators'; import { Type } from '@kbn/config-schema'; import { ConfigService, Env, Config, ConfigPath } from './config'; -import { ElasticsearchService } from './elasticsearch'; +import { ElasticsearchService, ElasticsearchServiceSetup } from './elasticsearch'; import { HttpService, InternalHttpServiceSetup } from './http'; import { LegacyService } from './legacy'; import { Logger, LoggerFactory } from './logging'; @@ -36,7 +36,8 @@ import { config as kibanaConfig } from './kibana_config'; import { config as savedObjectsConfig } from './saved_objects'; import { mapToObject } from '../utils/'; import { ContextService } from './context'; -import { InternalCoreSetup } from './index'; +import { SavedObjectsServiceSetup } from './saved_objects/saved_objects_service'; +import { RequestHandlerContext } from '.'; const coreId = Symbol('core'); @@ -94,7 +95,6 @@ export class Server { http: httpSetup, }; - this.registerCoreContext(coreSetup); const pluginsSetup = await this.plugins.setup(coreSetup); const legacySetup = await this.legacy.setup({ @@ -102,11 +102,13 @@ export class Server { plugins: mapToObject(pluginsSetup.contracts), }); - await this.savedObjects.setup({ + const savedObjectsSetup = await this.savedObjects.setup({ elasticsearch: elasticsearchServiceSetup, legacy: legacySetup, }); + this.registerCoreContext({ ...coreSetup, savedObjects: savedObjectsSetup }); + return coreSetup; } @@ -147,17 +149,30 @@ export class Server { ); } - private registerCoreContext(coreSetup: InternalCoreSetup) { - coreSetup.http.registerRouteHandlerContext(coreId, 'core', async (context, req) => { - const adminClient = await coreSetup.elasticsearch.adminClient$.pipe(take(1)).toPromise(); - const dataClient = await coreSetup.elasticsearch.dataClient$.pipe(take(1)).toPromise(); - return { - elasticsearch: { - adminClient: adminClient.asScoped(req), - dataClient: dataClient.asScoped(req), - }, - }; - }); + private registerCoreContext(coreSetup: { + http: InternalHttpServiceSetup; + elasticsearch: ElasticsearchServiceSetup; + savedObjects: SavedObjectsServiceSetup; + }) { + coreSetup.http.registerRouteHandlerContext( + coreId, + 'core', + async (context, req): Promise => { + const adminClient = await coreSetup.elasticsearch.adminClient$.pipe(take(1)).toPromise(); + const dataClient = await coreSetup.elasticsearch.dataClient$.pipe(take(1)).toPromise(); + return { + savedObjects: { + // Note: the client provider doesn't support new ES clients + // emitted from adminClient$ + client: coreSetup.savedObjects.clientProvider.getClient(req), + }, + elasticsearch: { + adminClient: adminClient.asScoped(req), + dataClient: dataClient.asScoped(req), + }, + }; + } + ); } public async setupConfigSchemas() { diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.js b/src/legacy/server/saved_objects/saved_objects_mixin.js index 0de0f34e28a84..0583dea333c7d 100644 --- a/src/legacy/server/saved_objects/saved_objects_mixin.js +++ b/src/legacy/server/saved_objects/saved_objects_mixin.js @@ -19,15 +19,11 @@ // Disable lint errors for imports from src/core/server/saved_objects until SavedObjects migration is complete /* eslint-disable @kbn/eslint/no-restricted-paths */ - -import { first } from 'rxjs/operators'; import { SavedObjectsSchema } from '../../../core/server/saved_objects/schema'; import { SavedObjectsSerializer } from '../../../core/server/saved_objects/serialization'; import { SavedObjectsClient, SavedObjectsRepository, - ScopedSavedObjectsClientProvider, - SavedObjectsCacheIndexPatterns, getSortedObjectsForExport, importSavedObjects, resolveImportErrors, @@ -65,7 +61,6 @@ export async function savedObjectsMixin(kbnServer, server) { const schema = new SavedObjectsSchema(kbnServer.uiExports.savedObjectSchemas); const visibleTypes = allTypes.filter(type => !schema.isHiddenType(type)); const importableAndExportableTypes = getImportableAndExportableTypes({ kbnServer, visibleTypes }); - const cacheIndexPatterns = new SavedObjectsCacheIndexPatterns(); server.decorate('server', 'kibanaMigrator', migrator); server.decorate( @@ -104,15 +99,6 @@ export async function savedObjectsMixin(kbnServer, server) { const serializer = new SavedObjectsSerializer(schema); - if (cacheIndexPatterns.getIndexPatternsService() == null) { - const adminClient = await server.newPlatform.__internals.elasticsearch.adminClient$ - .pipe(first()) - .toPromise(); - cacheIndexPatterns.setIndexPatternsService( - server.indexPatternsServiceFactory({ callCluster: adminClient.callAsInternalUser }) - ); - } - const createRepository = (callCluster, extraTypes = []) => { if (typeof callCluster !== 'function') { throw new TypeError('Repository requires a "callCluster" function to be provided.'); @@ -131,7 +117,6 @@ export async function savedObjectsMixin(kbnServer, server) { return new SavedObjectsRepository({ index: config.get('kibana.index'), config, - cacheIndexPatterns, migrator, mappings, schema, @@ -141,17 +126,7 @@ export async function savedObjectsMixin(kbnServer, server) { }); }; - const provider = new ScopedSavedObjectsClientProvider({ - index: server.config().get('kibana.index'), - mappings, - defaultClientFactory({ request }) { - const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin'); - const callCluster = (...args) => callWithRequest(request, ...args); - const repository = createRepository(callCluster); - - return new SavedObjectsClient(repository); - }, - }); + const provider = kbnServer.newPlatform.__internals.savedObjectsClientProvider; const service = { types: visibleTypes, diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.test.js b/src/legacy/server/saved_objects/saved_objects_mixin.test.js index fe21b0c529b3f..45ac31ab7fc61 100644 --- a/src/legacy/server/saved_objects/saved_objects_mixin.test.js +++ b/src/legacy/server/saved_objects/saved_objects_mixin.test.js @@ -20,6 +20,8 @@ import { savedObjectsMixin } from './saved_objects_mixin'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { mockKibanaMigrator } from '../../../core/server/saved_objects/migrations/kibana/kibana_migrator.mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { savedObjectsClientProviderMock } from '../../../core/server/saved_objects/service/lib/scoped_client_provider.mock'; const savedObjectMappings = [ { @@ -75,6 +77,7 @@ describe('Saved Objects Mixin', () => { }); beforeEach(() => { + const clientProvider = savedObjectsClientProviderMock.create(); mockServer = { log: jest.fn(), route: jest.fn(), @@ -115,7 +118,7 @@ describe('Saved Objects Mixin', () => { }; mockKbnServer = { newPlatform: { - __internals: { kibanaMigrator: migrator }, + __internals: { kibanaMigrator: migrator, savedObjectsClientProvider: clientProvider }, }, server: mockServer, ready: () => {}, @@ -290,9 +293,8 @@ describe('Saved Objects Mixin', () => { }); describe('get client', () => { - it('should return a valid client object', () => { - const client = service.getScopedSavedObjectsClient(); - expect(client).toBeDefined(); + it('should have a method to get the client', () => { + expect(service).toHaveProperty('getScopedSavedObjectsClient'); }); it('should have a method to set the client factory', () => { @@ -314,21 +316,6 @@ describe('Saved Objects Mixin', () => { service.addScopedSavedObjectsClientWrapperFactory({}); }).not.toThrowError(); }); - - it('should call underlining callCluster', async () => { - mockCallCluster.mockImplementation(method => { - if (method === 'indices.get') { - return { status: 404 }; - } else if (method === 'indices.getAlias') { - return { status: 404 }; - } else if (method === 'cat.templates') { - return []; - } - }); - const client = await service.getScopedSavedObjectsClient(); - await client.create('testtype'); - expect(mockCallCluster).toHaveBeenCalled(); - }); }); describe('#getSavedObjectsClient', () => { diff --git a/src/plugins/data/server/search/routes.test.ts b/src/plugins/data/server/search/routes.test.ts index 0923b52565097..ebdcf48f608b9 100644 --- a/src/plugins/data/server/search/routes.test.ts +++ b/src/plugins/data/server/search/routes.test.ts @@ -19,7 +19,7 @@ import { httpServiceMock, httpServerMock } from '../../../../../src/core/server/mocks'; import { registerSearchRoute } from './routes'; -import { IRouter, ScopedClusterClient } from 'kibana/server'; +import { IRouter, ScopedClusterClient, RequestHandlerContext } from 'kibana/server'; describe('Search service', () => { let routerMock: jest.Mocked; @@ -56,7 +56,7 @@ describe('Search service', () => { registerSearchRoute(routerMock); const handler = routerMock.post.mock.calls[0][1]; - await handler(mockContext, mockRequest, mockResponse); + await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); expect(mockSearch).toBeCalled(); expect(mockSearch.mock.calls[0][0]).toStrictEqual(mockBody); @@ -88,7 +88,7 @@ describe('Search service', () => { registerSearchRoute(routerMock); const handler = routerMock.post.mock.calls[0][1]; - await handler(mockContext, mockRequest, mockResponse); + await handler((mockContext as unknown) as RequestHandlerContext, mockRequest, mockResponse); expect(mockSearch).toBeCalled(); expect(mockSearch.mock.calls[0][0]).toStrictEqual(mockBody); diff --git a/src/plugins/testbed/server/index.ts b/src/plugins/testbed/server/index.ts index f9f6b3cf97e91..4dd22d3dce1ef 100644 --- a/src/plugins/testbed/server/index.ts +++ b/src/plugins/testbed/server/index.ts @@ -43,10 +43,21 @@ class Plugin { ); const router = core.http.createRouter(); - router.get({ path: '/ping', validate: false }, async (context, req, res) => { - const response = await context.core.elasticsearch.adminClient.callAsInternalUser('ping'); - return res.ok({ body: `Pong: ${response}` }); - }); + router.get( + { path: '/requestcontext/elasticsearch', validate: false }, + async (context, req, res) => { + const response = await context.core.elasticsearch.adminClient.callAsInternalUser('ping'); + return res.ok({ body: `Elasticsearch: ${response}` }); + } + ); + + router.get( + { path: '/requestcontext/savedobjectsclient', validate: false }, + async (context, req, res) => { + const response = await context.core.savedObjects.client.find({ type: 'TYPE' }); + return res.ok({ body: `SavedObjects client: ${JSON.stringify(response)}` }); + } + ); return { data$: this.initializerContext.config.create().pipe( diff --git a/test/api_integration/apis/core/index.js b/test/api_integration/apis/core/index.js index d12a4a9d37b39..d617b2ad07351 100644 --- a/test/api_integration/apis/core/index.js +++ b/test/api_integration/apis/core/index.js @@ -20,11 +20,17 @@ export default function ({ getService }) { const supertest = getService('supertest'); - describe('core', () => { - it('provides access to request context', async () => ( + describe('core request context', () => { + it('provides access to elasticsearch', async () => ( await supertest - .get('/testbed/ping') - .expect(200, 'Pong: true') + .get('/requestcontext/elasticsearch') + .expect(200, 'Elasticsearch: true') + )); + + it('provides access to SavedObjects client', async () => ( + await supertest + .get('/requestcontext/savedobjectsclient') + .expect(200, 'SavedObjects client: {"page":1,"per_page":20,"total":0,"saved_objects":[]}') )); }); } diff --git a/test/api_integration/apis/index.js b/test/api_integration/apis/index.js index 09d1896da5c80..9f2672959390c 100644 --- a/test/api_integration/apis/index.js +++ b/test/api_integration/apis/index.js @@ -19,6 +19,7 @@ export default function ({ loadTestFile }) { describe('apis', () => { + loadTestFile(require.resolve('./core')); loadTestFile(require.resolve('./elasticsearch')); loadTestFile(require.resolve('./general')); loadTestFile(require.resolve('./home')); diff --git a/test/api_integration/apis/saved_objects/find.js b/test/api_integration/apis/saved_objects/find.js index a41df24ea7a41..a1c84a766a116 100644 --- a/test/api_integration/apis/saved_objects/find.js +++ b/test/api_integration/apis/saved_objects/find.js @@ -165,6 +165,22 @@ export default function ({ getService }) { }); }) )); + + it('KQL syntax error should return 400 with Bad Request', async () => ( + await supertest + .get('/api/saved_objects/_find?type=dashboard&filter=dashboard.attributes.title:foo { + console.log('body', JSON.stringify(resp.body)); + expect(resp.body).to.eql({ + error: 'Bad Request', + message: 'KQLSyntaxError: Expected AND, OR, end of input, ' + + 'whitespace but \"<\" found.\ndashboard.attributes.title:foo' + + ' Date: Wed, 16 Oct 2019 12:05:25 +0200 Subject: [PATCH 22/48] Adding @elastic/epm to CODEOWNERS (#48237) --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4c06d2aee017b..012e49690fd15 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -29,6 +29,7 @@ # Logs & Metrics UI /x-pack/legacy/plugins/infra/ @elastic/logs-metrics-ui +/x-pack/legacy/plugins/integrations_manager/ @elastic/epm # Machine Learning /x-pack/legacy/plugins/ml/ @elastic/ml-ui From 7df981fbf8f81e1df205082a883a74ca99839e22 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Wed, 16 Oct 2019 12:48:34 +0200 Subject: [PATCH 23/48] Ignore missing references on saved object exports (#47685) * add saved object export details in ndjson response Signed-off-by: pgayvallet * update core doc Signed-off-by: pgayvallet * exclude export details for space copy Signed-off-by: pgayvallet * fixing tests Signed-off-by: pgayvallet * display warning instead of success if export contains missing refs Signed-off-by: pgayvallet * nits/typo Signed-off-by: pgayvallet * properly updates api integration tests Signed-off-by: pgayvallet * fix typings Signed-off-by: pgayvallet * add test on objects_table component Signed-off-by: pgayvallet * remove added translations from jp/cn bundles Signed-off-by: pgayvallet * restoring line feeds Signed-off-by: pgayvallet * improve doc and user alert message Signed-off-by: pgayvallet * restoring line feeds on server.api.md Signed-off-by: pgayvallet * warning test label Signed-off-by: pgayvallet --- docs/api/saved-objects/export.asciidoc | 48 ++- .../core/server/kibana-plugin-server.md | 1 + ...jectsexportoptions.excludeexportdetails.md | 13 + ...ectsexportoptions.includereferencesdeep.md | 2 +- ...plugin-server.savedobjectsexportoptions.md | 3 +- ...bjectsexportresultdetails.exportedcount.md | 13 + ...-server.savedobjectsexportresultdetails.md | 22 ++ ...ectsexportresultdetails.missingrefcount.md | 13 + ...tsexportresultdetails.missingreferences.md | 16 + src/core/server/index.ts | 1 + .../get_sorted_objects_for_export.test.ts | 294 +++++++++------ .../export/get_sorted_objects_for_export.ts | 49 ++- src/core/server/saved_objects/export/index.ts | 1 + .../inject_nested_depdendencies.test.ts | 356 ++++++++++-------- .../export/inject_nested_depdendencies.ts | 30 +- src/core/server/saved_objects/index.ts | 6 +- src/core/server/server.api.md | 11 + .../__jest__/objects_table.test.js | 80 +++- .../components/objects_table/objects_table.js | 62 +-- .../__jest__/extract_export_details.test.ts | 96 +++++ .../objects/lib/extract_export_details.ts | 51 +++ .../management/sections/objects/lib/index.js | 1 + ...e_saved_objects_stream_from_ndjson.test.ts | 26 ++ ...create_saved_objects_stream_from_ndjson.ts | 8 +- .../saved_objects/routes/export.test.ts | 1 + .../server/saved_objects/routes/export.ts | 5 +- .../apis/saved_objects/export.js | 321 +++++++++------- .../lib/copy_to_spaces/copy_to_spaces.test.ts | 1 + .../lib/copy_to_spaces/copy_to_spaces.ts | 1 + .../resolve_copy_conflicts.test.ts | 1 + .../copy_to_spaces/resolve_copy_conflicts.ts | 1 + .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - .../common/suites/export.ts | 3 + 34 files changed, 1085 insertions(+), 456 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.excludeexportdetails.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.exportedcount.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.missingrefcount.md create mode 100644 docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.missingreferences.md create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/extract_export_details.test.ts create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/lib/extract_export_details.ts diff --git a/docs/api/saved-objects/export.asciidoc b/docs/api/saved-objects/export.asciidoc index d06756b109a90..e6c0adf2128ba 100644 --- a/docs/api/saved-objects/export.asciidoc +++ b/docs/api/saved-objects/export.asciidoc @@ -23,12 +23,29 @@ experimental[] Retrieve a set of saved objects that you want to import into {kib `includeReferencesDeep`:: (Optional, boolean) Includes all of the referenced objects in the exported objects. +`excludeExportDetails`:: + (Optional, boolean) Do not add export details entry at the end of the stream. + TIP: You must include `type` or `objects` in the request body. [[saved-objects-api-export-request-response-body]] ==== Response body -The format of the response body includes newline delimited JSON. +The format of the response body is newline delimited JSON. Each exported object is exported as a valid JSON record and separated by the newline character '\n'. + +When `excludeExportDetails=false` (the default) we append an export result details record at the end of the file after all the saved object records. The export result details object has the following format: + +[source,json] +-------------------------------------------------- +{ + "exportedCount": 27, + "missingRefCount": 2, + "missingReferences": [ + { "id": "an-id", "type": "visualisation"}, + { "id": "another-id", "type": "index-pattern"} + ] +} +-------------------------------------------------- [[export-objects-api-create-request-codes]] ==== Response code @@ -50,6 +67,18 @@ POST api/saved_objects/_export -------------------------------------------------- // KIBANA +Export all index pattern saved objects and exclude the export summary from the stream: + +[source,js] +-------------------------------------------------- +POST api/saved_objects/_export +{ + "type": "index-pattern", + "excludeExportDetails": true +} +-------------------------------------------------- +// KIBANA + Export a specific saved object: [source,js] @@ -65,3 +94,20 @@ POST api/saved_objects/_export } -------------------------------------------------- // KIBANA + +Export a specific saved object and it's related objects : + +[source,js] +-------------------------------------------------- +POST api/saved_objects/_export +{ + "objects": [ + { + "type": "dashboard", + "id": "be3733a0-9efe-11e7-acb3-3dab96693fab" + } + ], + "includeReferencesDeep": true +} +-------------------------------------------------- +// KIBANA diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index c085d4cdf0d42..b2d5491d01a4b 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -92,6 +92,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsClientWrapperOptions](./kibana-plugin-server.savedobjectsclientwrapperoptions.md) | Options passed to each SavedObjectsClientWrapperFactory to aid in creating the wrapper instance. | | [SavedObjectsCreateOptions](./kibana-plugin-server.savedobjectscreateoptions.md) | | | [SavedObjectsExportOptions](./kibana-plugin-server.savedobjectsexportoptions.md) | Options controlling the export operation. | +| [SavedObjectsExportResultDetails](./kibana-plugin-server.savedobjectsexportresultdetails.md) | Structure of the export result details entry | | [SavedObjectsFindOptions](./kibana-plugin-server.savedobjectsfindoptions.md) | | | [SavedObjectsFindResponse](./kibana-plugin-server.savedobjectsfindresponse.md) | Return type of the Saved Objects find() method.\*Note\*: this type is different between the Public and Server Saved Objects clients. | | [SavedObjectsImportConflictError](./kibana-plugin-server.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. | diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.excludeexportdetails.md b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.excludeexportdetails.md new file mode 100644 index 0000000000000..bffc809689834 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.excludeexportdetails.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsExportOptions](./kibana-plugin-server.savedobjectsexportoptions.md) > [excludeExportDetails](./kibana-plugin-server.savedobjectsexportoptions.excludeexportdetails.md) + +## SavedObjectsExportOptions.excludeExportDetails property + +flag to not append [export details](./kibana-plugin-server.savedobjectsexportresultdetails.md) to the end of the export stream. + +Signature: + +```typescript +excludeExportDetails?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.includereferencesdeep.md b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.includereferencesdeep.md index d721fc260eaf8..0cd7688e04184 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.includereferencesdeep.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.includereferencesdeep.md @@ -4,7 +4,7 @@ ## SavedObjectsExportOptions.includeReferencesDeep property -flag to also include all related saved objects in the export response. +flag to also include all related saved objects in the export stream. Signature: diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.md b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.md index 0f1bd94d01552..d312d7d4b3499 100644 --- a/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.md +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsexportoptions.md @@ -16,8 +16,9 @@ export interface SavedObjectsExportOptions | Property | Type | Description | | --- | --- | --- | +| [excludeExportDetails](./kibana-plugin-server.savedobjectsexportoptions.excludeexportdetails.md) | boolean | flag to not append [export details](./kibana-plugin-server.savedobjectsexportresultdetails.md) to the end of the export stream. | | [exportSizeLimit](./kibana-plugin-server.savedobjectsexportoptions.exportsizelimit.md) | number | the maximum number of objects to export. | -| [includeReferencesDeep](./kibana-plugin-server.savedobjectsexportoptions.includereferencesdeep.md) | boolean | flag to also include all related saved objects in the export response. | +| [includeReferencesDeep](./kibana-plugin-server.savedobjectsexportoptions.includereferencesdeep.md) | boolean | flag to also include all related saved objects in the export stream. | | [namespace](./kibana-plugin-server.savedobjectsexportoptions.namespace.md) | string | optional namespace to override the namespace used by the savedObjectsClient. | | [objects](./kibana-plugin-server.savedobjectsexportoptions.objects.md) | Array<{
id: string;
type: string;
}> | optional array of objects to export. | | [savedObjectsClient](./kibana-plugin-server.savedobjectsexportoptions.savedobjectsclient.md) | SavedObjectsClientContract | an instance of the SavedObjectsClient. | diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.exportedcount.md b/docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.exportedcount.md new file mode 100644 index 0000000000000..c2e588dd3c121 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.exportedcount.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsExportResultDetails](./kibana-plugin-server.savedobjectsexportresultdetails.md) > [exportedCount](./kibana-plugin-server.savedobjectsexportresultdetails.exportedcount.md) + +## SavedObjectsExportResultDetails.exportedCount property + +number of successfully exported objects + +Signature: + +```typescript +exportedCount: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.md b/docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.md new file mode 100644 index 0000000000000..fb3af350d21ea --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsExportResultDetails](./kibana-plugin-server.savedobjectsexportresultdetails.md) + +## SavedObjectsExportResultDetails interface + +Structure of the export result details entry + +Signature: + +```typescript +export interface SavedObjectsExportResultDetails +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [exportedCount](./kibana-plugin-server.savedobjectsexportresultdetails.exportedcount.md) | number | number of successfully exported objects | +| [missingRefCount](./kibana-plugin-server.savedobjectsexportresultdetails.missingrefcount.md) | number | number of missing references | +| [missingReferences](./kibana-plugin-server.savedobjectsexportresultdetails.missingreferences.md) | Array<{
id: string;
type: string;
}> | missing references details | + diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.missingrefcount.md b/docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.missingrefcount.md new file mode 100644 index 0000000000000..5b51199ea4780 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.missingrefcount.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsExportResultDetails](./kibana-plugin-server.savedobjectsexportresultdetails.md) > [missingRefCount](./kibana-plugin-server.savedobjectsexportresultdetails.missingrefcount.md) + +## SavedObjectsExportResultDetails.missingRefCount property + +number of missing references + +Signature: + +```typescript +missingRefCount: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.missingreferences.md b/docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.missingreferences.md new file mode 100644 index 0000000000000..1602bfb6e6cb6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.savedobjectsexportresultdetails.missingreferences.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsExportResultDetails](./kibana-plugin-server.savedobjectsexportresultdetails.md) > [missingReferences](./kibana-plugin-server.savedobjectsexportresultdetails.missingreferences.md) + +## SavedObjectsExportResultDetails.missingReferences property + +missing references details + +Signature: + +```typescript +missingReferences: Array<{ + id: string; + type: string; + }>; +``` diff --git a/src/core/server/index.ts b/src/core/server/index.ts index d92c92841bb48..fe4d0a05c4fb9 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -144,6 +144,7 @@ export { SavedObjectsCreateOptions, SavedObjectsErrorHelpers, SavedObjectsExportOptions, + SavedObjectsExportResultDetails, SavedObjectsFindResponse, SavedObjectsImportConflictError, SavedObjectsImportError, diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts index df3bbe7c455e4..1a2a843ebb2b8 100644 --- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts +++ b/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts @@ -74,27 +74,32 @@ describe('getSortedObjectsForExport()', () => { const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.find).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -122,6 +127,65 @@ describe('getSortedObjectsForExport()', () => { `); }); + test('exclude export details if option is specified', async () => { + savedObjectsClient.find.mockResolvedValueOnce({ + total: 2, + saved_objects: [ + { + id: '2', + type: 'search', + attributes: {}, + references: [ + { + name: 'name', + type: 'index-pattern', + id: '1', + }, + ], + }, + { + id: '1', + type: 'index-pattern', + attributes: {}, + references: [], + }, + ], + per_page: 1, + page: 0, + }); + const exportStream = await getSortedObjectsForExport({ + savedObjectsClient, + exportSizeLimit: 500, + types: ['index-pattern', 'search'], + excludeExportDetails: true, + }); + + const response = await readStreamToCompletion(exportStream); + + expect(response).toMatchInlineSnapshot(` + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + ] + `); + }); + test('exports selected types with search string when present', async () => { savedObjectsClient.find.mockResolvedValueOnce({ total: 2, @@ -158,27 +222,32 @@ describe('getSortedObjectsForExport()', () => { const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.find).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -242,27 +311,32 @@ describe('getSortedObjectsForExport()', () => { const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.find).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -365,27 +439,32 @@ describe('getSortedObjectsForExport()', () => { }); const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -456,27 +535,32 @@ describe('getSortedObjectsForExport()', () => { }); const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts index eca8fc0405300..e1a705a36db75 100644 --- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts +++ b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts @@ -20,7 +20,7 @@ import Boom from 'boom'; import { createListStream } from '../../../../legacy/utils/streams'; import { SavedObjectsClientContract } from '../types'; -import { injectNestedDependencies } from './inject_nested_depdendencies'; +import { fetchNestedDependencies } from './inject_nested_depdendencies'; import { sortObjects } from './sort_objects'; /** @@ -43,12 +43,32 @@ export interface SavedObjectsExportOptions { savedObjectsClient: SavedObjectsClientContract; /** the maximum number of objects to export. */ exportSizeLimit: number; - /** flag to also include all related saved objects in the export response. */ + /** flag to also include all related saved objects in the export stream. */ includeReferencesDeep?: boolean; + /** flag to not append {@link SavedObjectsExportResultDetails | export details} to the end of the export stream. */ + excludeExportDetails?: boolean; /** optional namespace to override the namespace used by the savedObjectsClient. */ namespace?: string; } +/** + * Structure of the export result details entry + * @public + */ +export interface SavedObjectsExportResultDetails { + /** number of successfully exported objects */ + exportedCount: number; + /** number of missing references */ + missingRefCount: number; + /** missing references details */ + missingReferences: Array<{ + /** the missing reference id. */ + id: string; + /** the missing reference type. */ + type: string; + }>; +} + async function fetchObjectsToExport({ objects, types, @@ -106,9 +126,10 @@ export async function getSortedObjectsForExport({ savedObjectsClient, exportSizeLimit, includeReferencesDeep = false, + excludeExportDetails = false, namespace, }: SavedObjectsExportOptions) { - const objectsToExport = await fetchObjectsToExport({ + const rootObjects = await fetchObjectsToExport({ types, objects, search, @@ -116,12 +137,18 @@ export async function getSortedObjectsForExport({ exportSizeLimit, namespace, }); - - const exportedObjects = sortObjects( - includeReferencesDeep - ? await injectNestedDependencies(objectsToExport, savedObjectsClient, namespace) - : objectsToExport - ); - - return createListStream(exportedObjects); + let exportedObjects = [...rootObjects]; + let missingReferences: SavedObjectsExportResultDetails['missingReferences'] = []; + if (includeReferencesDeep) { + const fetchResult = await fetchNestedDependencies(rootObjects, savedObjectsClient, namespace); + exportedObjects = fetchResult.objects; + missingReferences = fetchResult.missingRefs; + } + exportedObjects = sortObjects(exportedObjects); + const exportDetails: SavedObjectsExportResultDetails = { + exportedCount: exportedObjects.length, + missingRefCount: missingReferences.length, + missingReferences, + }; + return createListStream([...exportedObjects, ...(excludeExportDetails ? [] : [exportDetails])]); } diff --git a/src/core/server/saved_objects/export/index.ts b/src/core/server/saved_objects/export/index.ts index d994df2af627c..7533b8e500039 100644 --- a/src/core/server/saved_objects/export/index.ts +++ b/src/core/server/saved_objects/export/index.ts @@ -20,4 +20,5 @@ export { getSortedObjectsForExport, SavedObjectsExportOptions, + SavedObjectsExportResultDetails, } from './get_sorted_objects_for_export'; diff --git a/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts b/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts index 4613553fbd301..89d555e06a634 100644 --- a/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts +++ b/src/core/server/saved_objects/export/inject_nested_depdendencies.test.ts @@ -18,10 +18,7 @@ */ import { SavedObject } from '../types'; -import { - getObjectReferencesToFetch, - injectNestedDependencies, -} from './inject_nested_depdendencies'; +import { getObjectReferencesToFetch, fetchNestedDependencies } from './inject_nested_depdendencies'; describe('getObjectReferencesToFetch()', () => { test('works with no saved objects', () => { @@ -110,7 +107,7 @@ describe('getObjectReferencesToFetch()', () => { }); }); -describe('injectNestedDependencies', () => { +describe('fetchNestedDependencies', () => { const savedObjectsClient = { errors: {} as any, find: jest.fn(), @@ -135,16 +132,19 @@ describe('injectNestedDependencies', () => { references: [], }, ]; - const result = await injectNestedDependencies(savedObjects, savedObjectsClient); + const result = await fetchNestedDependencies(savedObjects, savedObjectsClient); expect(result).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - ] + Object { + "missingRefs": Array [], + "objects": Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + ], + } `); }); @@ -169,28 +169,31 @@ describe('injectNestedDependencies', () => { ], }, ]; - const result = await injectNestedDependencies(savedObjects, savedObjectsClient); + const result = await fetchNestedDependencies(savedObjects, savedObjectsClient); expect(result).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "ref_0", - "type": "index-pattern", - }, - ], - "type": "search", - }, - ] + Object { + "missingRefs": Array [], + "objects": Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "ref_0", + "type": "index-pattern", + }, + ], + "type": "search", + }, + ], + } `); }); @@ -219,28 +222,31 @@ describe('injectNestedDependencies', () => { }, ], }); - const result = await injectNestedDependencies(savedObjects, savedObjectsClient); + const result = await fetchNestedDependencies(savedObjects, savedObjectsClient); expect(result).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "ref_0", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - ] + Object { + "missingRefs": Array [], + "objects": Array [ + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "ref_0", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + ], + } `); expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` [MockFunction] { @@ -337,69 +343,72 @@ describe('injectNestedDependencies', () => { }, ], }); - const result = await injectNestedDependencies(savedObjects, savedObjectsClient); + const result = await fetchNestedDependencies(savedObjects, savedObjectsClient); expect(result).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "5", - "references": Array [ - Object { - "id": "4", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "3", - "name": "panel_1", - "type": "visualization", - }, - ], - "type": "dashboard", - }, - Object { - "attributes": Object {}, - "id": "4", - "references": Array [ - Object { - "id": "2", - "name": "ref_0", - "type": "search", - }, - ], - "type": "visualization", - }, - Object { - "attributes": Object {}, - "id": "3", - "references": Array [ - Object { - "id": "1", - "name": "ref_0", - "type": "index-pattern", - }, - ], - "type": "visualization", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "ref_0", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - ] + Object { + "missingRefs": Array [], + "objects": Array [ + Object { + "attributes": Object {}, + "id": "5", + "references": Array [ + Object { + "id": "4", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "3", + "name": "panel_1", + "type": "visualization", + }, + ], + "type": "dashboard", + }, + Object { + "attributes": Object {}, + "id": "4", + "references": Array [ + Object { + "id": "2", + "name": "ref_0", + "type": "search", + }, + ], + "type": "visualization", + }, + Object { + "attributes": Object {}, + "id": "3", + "references": Array [ + Object { + "id": "1", + "name": "ref_0", + "type": "index-pattern", + }, + ], + "type": "visualization", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "ref_0", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + ], + } `); expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` [MockFunction] { @@ -449,10 +458,10 @@ describe('injectNestedDependencies', () => { `); }); - test('throws error when bulkGet returns an error', async () => { + test('returns list of missing references', async () => { const savedObjects = [ { - id: '2', + id: '1', type: 'search', attributes: {}, references: [ @@ -461,6 +470,11 @@ describe('injectNestedDependencies', () => { type: 'index-pattern', id: '1', }, + { + name: 'ref_1', + type: 'index-pattern', + id: '2', + }, ], }, ]; @@ -474,11 +488,50 @@ describe('injectNestedDependencies', () => { message: 'Not found', }, }, + { + id: '2', + type: 'index-pattern', + attributes: {}, + references: [], + }, ], }); - await expect( - injectNestedDependencies(savedObjects, savedObjectsClient) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"Bad Request"`); + const result = await fetchNestedDependencies(savedObjects, savedObjectsClient); + expect(result).toMatchInlineSnapshot(` + Object { + "missingRefs": Array [ + Object { + "id": "1", + "type": "index-pattern", + }, + ], + "objects": Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [ + Object { + "id": "1", + "name": "ref_0", + "type": "index-pattern", + }, + Object { + "id": "2", + "name": "ref_1", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [], + "type": "index-pattern", + }, + ], + } + `); }); test(`doesn't deal with circular dependencies`, async () => { @@ -512,34 +565,37 @@ describe('injectNestedDependencies', () => { }, ], }); - const result = await injectNestedDependencies(savedObjects, savedObjectsClient); + const result = await fetchNestedDependencies(savedObjects, savedObjectsClient); expect(result).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "ref_0", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "attributes": Object {}, - "id": "1", - "references": Array [ - Object { - "id": "2", - "name": "ref_0", - "type": "search", - }, - ], - "type": "index-pattern", - }, - ] + Object { + "missingRefs": Array [], + "objects": Array [ + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "ref_0", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "attributes": Object {}, + "id": "1", + "references": Array [ + Object { + "id": "2", + "name": "ref_0", + "type": "search", + }, + ], + "type": "index-pattern", + }, + ], + } `); expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` [MockFunction] { diff --git a/src/core/server/saved_objects/export/inject_nested_depdendencies.ts b/src/core/server/saved_objects/export/inject_nested_depdendencies.ts index 279b06f955571..d00650926e57a 100644 --- a/src/core/server/saved_objects/export/inject_nested_depdendencies.ts +++ b/src/core/server/saved_objects/export/inject_nested_depdendencies.ts @@ -17,47 +17,43 @@ * under the License. */ -import Boom from 'boom'; import { SavedObject, SavedObjectsClientContract } from '../types'; export function getObjectReferencesToFetch(savedObjectsMap: Map) { const objectsToFetch = new Map(); for (const savedObject of savedObjectsMap.values()) { - for (const { type, id } of savedObject.references || []) { - if (!savedObjectsMap.has(`${type}:${id}`)) { - objectsToFetch.set(`${type}:${id}`, { type, id }); + for (const ref of savedObject.references || []) { + if (!savedObjectsMap.has(objKey(ref))) { + objectsToFetch.set(objKey(ref), { type: ref.type, id: ref.id }); } } } return [...objectsToFetch.values()]; } -export async function injectNestedDependencies( +export async function fetchNestedDependencies( savedObjects: SavedObject[], savedObjectsClient: SavedObjectsClientContract, namespace?: string ) { const savedObjectsMap = new Map(); for (const savedObject of savedObjects) { - savedObjectsMap.set(`${savedObject.type}:${savedObject.id}`, savedObject); + savedObjectsMap.set(objKey(savedObject), savedObject); } let objectsToFetch = getObjectReferencesToFetch(savedObjectsMap); while (objectsToFetch.length > 0) { const bulkGetResponse = await savedObjectsClient.bulkGet(objectsToFetch, { namespace }); - // Check for errors - const erroredObjects = bulkGetResponse.saved_objects.filter(obj => !!obj.error); - if (erroredObjects.length) { - const err = Boom.badRequest(); - err.output.payload.attributes = { - objects: erroredObjects, - }; - throw err; - } // Push to array result for (const savedObject of bulkGetResponse.saved_objects) { - savedObjectsMap.set(`${savedObject.type}:${savedObject.id}`, savedObject); + savedObjectsMap.set(objKey(savedObject), savedObject); } objectsToFetch = getObjectReferencesToFetch(savedObjectsMap); } - return [...savedObjectsMap.values()]; + const allObjects = [...savedObjectsMap.values()]; + return { + objects: allObjects.filter(obj => !obj.error), + missingRefs: allObjects.filter(obj => !!obj.error).map(obj => ({ type: obj.type, id: obj.id })), + }; } + +const objKey = (obj: { type: string; id: string }) => `${obj.type}:${obj.id}`; diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts index 674f8df33ee37..76c62e0841bff 100644 --- a/src/core/server/saved_objects/index.ts +++ b/src/core/server/saved_objects/index.ts @@ -25,7 +25,11 @@ export { SavedObjectsManagement } from './management'; export * from './import'; -export { getSortedObjectsForExport, SavedObjectsExportOptions } from './export'; +export { + getSortedObjectsForExport, + SavedObjectsExportOptions, + SavedObjectsExportResultDetails, +} from './export'; export { SavedObjectsSerializer, RawDoc as SavedObjectsRawDoc } from './serialization'; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 46e5d2b6ab6c6..604a0916c028f 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1263,6 +1263,7 @@ export class SavedObjectsErrorHelpers { // @public export interface SavedObjectsExportOptions { + excludeExportDetails?: boolean; exportSizeLimit: number; includeReferencesDeep?: boolean; namespace?: string; @@ -1275,6 +1276,16 @@ export interface SavedObjectsExportOptions { types?: string[]; } +// @public +export interface SavedObjectsExportResultDetails { + exportedCount: number; + missingRefCount: number; + missingReferences: Array<{ + id: string; + type: string; + }>; +} + // @public (undocumented) export interface SavedObjectsFindOptions extends SavedObjectsBaseOptions { // (undocumented) diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js index 7cf3f935ec4b5..39a9f7cd98a57 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/__jest__/objects_table.test.js @@ -25,6 +25,7 @@ import { ObjectsTable, POSSIBLE_TYPES } from '../objects_table'; import { Flyout } from '../components/flyout/'; import { Relationships } from '../components/relationships/'; import { findObjects } from '../../../lib'; +import { extractExportDetails } from '../../../lib/extract_export_details'; jest.mock('ui/kfetch', () => ({ kfetch: jest.fn() })); @@ -49,6 +50,10 @@ jest.mock('../../../lib/fetch_export_by_type_and_search', () => ({ fetchExportByTypeAndSearch: jest.fn(), })); +jest.mock('../../../lib/extract_export_details', () => ({ + extractExportDetails: jest.fn(), +})); + jest.mock('../../../lib/get_saved_object_counts', () => ({ getSavedObjectCounts: jest.fn().mockImplementation(() => { return { @@ -190,12 +195,14 @@ beforeEach(() => { let addDangerMock; let addSuccessMock; +let addWarningMock; describe('ObjectsTable', () => { beforeEach(() => { defaultProps.savedObjectsClient.find.mockClear(); + extractExportDetails.mockReset(); // mock _.debounce to fire immediately with no internal timer - require('lodash').debounce = function (func) { + require('lodash').debounce = func => { function debounced(...args) { return func.apply(this, args); } @@ -203,9 +210,11 @@ describe('ObjectsTable', () => { }; addDangerMock = jest.fn(); addSuccessMock = jest.fn(); + addWarningMock = jest.fn(); require('ui/notify').toastNotifications = { addDanger: addDangerMock, addSuccess: addSuccessMock, + addWarning: addWarningMock, }; }); @@ -280,6 +289,55 @@ describe('ObjectsTable', () => { }); }); + it('should display a warning is export contains missing references', async () => { + const mockSelectedSavedObjects = [ + { id: '1', type: 'index-pattern' }, + { id: '3', type: 'dashboard' }, + ]; + + const mockSavedObjects = mockSelectedSavedObjects.map(obj => ({ + _id: obj.id, + _type: obj._type, + _source: {}, + })); + + const mockSavedObjectsClient = { + ...defaultProps.savedObjectsClient, + bulkGet: jest.fn().mockImplementation(() => ({ + savedObjects: mockSavedObjects, + })), + }; + + const { fetchExportObjects } = require('../../../lib/fetch_export_objects'); + extractExportDetails.mockImplementation(() => ({ + exportedCount: 2, + missingRefCount: 1, + missingReferences: [{ id: '7', type: 'visualisation' }], + })); + + const component = shallowWithI18nProvider( + + ); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + // Set some as selected + component.instance().onSelectionChanged(mockSelectedSavedObjects); + + await component.instance().onExport(true); + + expect(fetchExportObjects).toHaveBeenCalledWith(mockSelectedSavedObjects, true); + expect(addWarningMock).toHaveBeenCalledWith({ + title: + 'Your file is downloading in the background. ' + + 'Some related objects could not be found. ' + + 'Please see the last line in the exported file for a list of missing objects.', + }); + }); + it('should allow the user to choose when exporting all', async () => { const component = shallowWithI18nProvider(); @@ -295,7 +353,9 @@ describe('ObjectsTable', () => { }); it('should export all', async () => { - const { fetchExportByTypeAndSearch } = require('../../../lib/fetch_export_by_type_and_search'); + const { + fetchExportByTypeAndSearch, + } = require('../../../lib/fetch_export_by_type_and_search'); const { saveAs } = require('@elastic/filesaver'); const component = shallowWithI18nProvider(); @@ -312,20 +372,20 @@ describe('ObjectsTable', () => { expect(fetchExportByTypeAndSearch).toHaveBeenCalledWith(POSSIBLE_TYPES, undefined, true); expect(saveAs).toHaveBeenCalledWith(blob, 'export.ndjson'); - expect(addSuccessMock).toHaveBeenCalledWith({ title: 'Your file is downloading in the background' }); + expect(addSuccessMock).toHaveBeenCalledWith({ + title: 'Your file is downloading in the background', + }); }); it('should export all, accounting for the current search criteria', async () => { - const { fetchExportByTypeAndSearch } = require('../../../lib/fetch_export_by_type_and_search'); + const { + fetchExportByTypeAndSearch, + } = require('../../../lib/fetch_export_by_type_and_search'); const { saveAs } = require('@elastic/filesaver'); - const component = shallowWithI18nProvider( - - ); + const component = shallowWithI18nProvider(); component.instance().onQueryChange({ - query: Query.parse('test') + query: Query.parse('test'), }); // Ensure all promises resolve diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js index 52871c360309c..188762f165b24 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/objects_table/objects_table.js @@ -64,6 +64,7 @@ import { fetchExportByTypeAndSearch, findObjects, } from '../../lib'; +import { extractExportDetails } from '../../lib/extract_export_details'; export const POSSIBLE_TYPES = chrome.getInjected('importAndExportableTypes'); @@ -296,32 +297,31 @@ export class ObjectsTable extends Component { } saveAs(blob, 'export.ndjson'); - toastNotifications.addSuccess({ - title: i18n.translate('kbn.management.objects.objectsTable.export.successNotification', { - defaultMessage: 'Your file is downloading in the background', - }), - }); + + const exportDetails = await extractExportDetails(blob); + this.showExportSuccessMessage(exportDetails); }; onExportAll = async () => { const { exportAllSelectedOptions, isIncludeReferencesDeepChecked, activeQuery } = this.state; const { queryText } = parseQuery(activeQuery); - const exportTypes = Object.entries(exportAllSelectedOptions).reduce( - (accum, [id, selected]) => { - if (selected) { - accum.push(id); - } - return accum; - }, - [] - ); + const exportTypes = Object.entries(exportAllSelectedOptions).reduce((accum, [id, selected]) => { + if (selected) { + accum.push(id); + } + return accum; + }, []); let blob; try { - blob = await fetchExportByTypeAndSearch(exportTypes, queryText ? `${queryText}*` : undefined, isIncludeReferencesDeepChecked); + blob = await fetchExportByTypeAndSearch( + exportTypes, + queryText ? `${queryText}*` : undefined, + isIncludeReferencesDeepChecked + ); } catch (e) { toastNotifications.addDanger({ - title: i18n.translate('kbn.management.objects.objectsTable.exportAll.dangerNotification', { + title: i18n.translate('kbn.management.objects.objectsTable.export.dangerNotification', { defaultMessage: 'Unable to generate export', }), }); @@ -329,14 +329,34 @@ export class ObjectsTable extends Component { } saveAs(blob, 'export.ndjson'); - toastNotifications.addSuccess({ - title: i18n.translate('kbn.management.objects.objectsTable.exportAll.successNotification', { - defaultMessage: 'Your file is downloading in the background', - }), - }); + + const exportDetails = await extractExportDetails(blob); + this.showExportSuccessMessage(exportDetails); this.setState({ isShowingExportAllOptionsModal: false }); }; + showExportSuccessMessage = exportDetails => { + if (exportDetails && exportDetails.missingReferences.length > 0) { + toastNotifications.addWarning({ + title: i18n.translate( + 'kbn.management.objects.objectsTable.export.successWithMissingRefsNotification', + { + defaultMessage: + 'Your file is downloading in the background. ' + + 'Some related objects could not be found. ' + + 'Please see the last line in the exported file for a list of missing objects.', + } + ), + }); + } else { + toastNotifications.addSuccess({ + title: i18n.translate('kbn.management.objects.objectsTable.export.successNotification', { + defaultMessage: 'Your file is downloading in the background', + }), + }); + } + }; + finishImport = () => { this.hideImportFlyout(); this.fetchSavedObjects(); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/extract_export_details.test.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/extract_export_details.test.ts new file mode 100644 index 0000000000000..a6ed2e36839f4 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/extract_export_details.test.ts @@ -0,0 +1,96 @@ +/* + * 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 { extractExportDetails, SavedObjectsExportResultDetails } from '../extract_export_details'; + +describe('extractExportDetails', () => { + const objLine = (id: string, type: string) => { + return JSON.stringify({ attributes: {}, id, references: [], type }) + '\n'; + }; + const detailsLine = ( + exported: number, + missingRefs: SavedObjectsExportResultDetails['missingReferences'] = [] + ) => { + return ( + JSON.stringify({ + exportedCount: exported, + missingRefCount: missingRefs.length, + missingReferences: missingRefs, + }) + '\n' + ); + }; + + it('should extract the export details from the export blob', async () => { + const exportData = new Blob( + [ + [ + objLine('1', 'index-pattern'), + objLine('2', 'index-pattern'), + objLine('3', 'index-pattern'), + detailsLine(3), + ].join(''), + ], + { type: 'application/ndjson', endings: 'transparent' } + ); + const result = await extractExportDetails(exportData); + expect(result).not.toBeUndefined(); + expect(result).toEqual({ + exportedCount: 3, + missingRefCount: 0, + missingReferences: [], + }); + }); + + it('should properly extract the missing references', async () => { + const exportData = new Blob( + [ + [ + objLine('1', 'index-pattern'), + detailsLine(1, [{ id: '2', type: 'index-pattern' }, { id: '3', type: 'index-pattern' }]), + ].join(''), + ], + { + type: 'application/ndjson', + endings: 'transparent', + } + ); + const result = await extractExportDetails(exportData); + expect(result).not.toBeUndefined(); + expect(result).toEqual({ + exportedCount: 1, + missingRefCount: 2, + missingReferences: [{ id: '2', type: 'index-pattern' }, { id: '3', type: 'index-pattern' }], + }); + }); + + it('should return undefined when the export does not contain details', async () => { + const exportData = new Blob( + [ + [ + objLine('1', 'index-pattern'), + objLine('2', 'index-pattern'), + objLine('3', 'index-pattern'), + ].join(''), + ], + { type: 'application/ndjson', endings: 'transparent' } + ); + const result = await extractExportDetails(exportData); + expect(result).toBeUndefined(); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/extract_export_details.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/extract_export_details.ts new file mode 100644 index 0000000000000..fdd72aece06bc --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/extract_export_details.ts @@ -0,0 +1,51 @@ +/* + * 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 async function extractExportDetails( + blob: Blob +): Promise { + const reader = new FileReader(); + const content = await new Promise((resolve, reject) => { + reader.addEventListener('loadend', e => { + resolve((e as any).target.result); + }); + reader.addEventListener('error', e => { + reject(e); + }); + reader.readAsText(blob, 'utf-8'); + }); + const lines = content.split('\n').filter(l => l.length > 0); + const maybeDetails = JSON.parse(lines[lines.length - 1]); + if (isExportDetails(maybeDetails)) { + return maybeDetails; + } +} + +export interface SavedObjectsExportResultDetails { + exportedCount: number; + missingRefCount: number; + missingReferences: Array<{ + id: string; + type: string; + }>; +} + +function isExportDetails(object: any): object is SavedObjectsExportResultDetails { + return 'exportedCount' in object && 'missingRefCount' in object && 'missingReferences' in object; +} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.js index 245812867f1de..b6c8d25568446 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/index.js @@ -32,3 +32,4 @@ export * from './log_legacy_import'; export * from './process_import_response'; export * from './get_default_title'; export * from './find_objects'; +export * from './extract_export_details'; diff --git a/src/legacy/server/saved_objects/lib/create_saved_objects_stream_from_ndjson.test.ts b/src/legacy/server/saved_objects/lib/create_saved_objects_stream_from_ndjson.test.ts index 2eab73137fff0..342063fefaec6 100644 --- a/src/legacy/server/saved_objects/lib/create_saved_objects_stream_from_ndjson.test.ts +++ b/src/legacy/server/saved_objects/lib/create_saved_objects_stream_from_ndjson.test.ts @@ -77,4 +77,30 @@ describe('createSavedObjectsStreamFromNdJson', () => { }, ]); }); + + it('filters the export details entry from the stream', async () => { + const savedObjectsStream = createSavedObjectsStreamFromNdJson( + new Readable({ + read() { + this.push('{"id": "foo", "type": "foo-type"}\n'); + this.push('{"id": "bar", "type": "bar-type"}\n'); + this.push('{"exportedCount": 2, "missingRefCount": 0, "missingReferences": []}\n'); + this.push(null); + }, + }) + ); + + const result = await readStreamToCompletion(savedObjectsStream); + + expect(result).toEqual([ + { + id: 'foo', + type: 'foo-type', + }, + { + id: 'bar', + type: 'bar-type', + }, + ]); + }); }); diff --git a/src/legacy/server/saved_objects/lib/create_saved_objects_stream_from_ndjson.ts b/src/legacy/server/saved_objects/lib/create_saved_objects_stream_from_ndjson.ts index 10047284f5c96..b96514054db56 100644 --- a/src/legacy/server/saved_objects/lib/create_saved_objects_stream_from_ndjson.ts +++ b/src/legacy/server/saved_objects/lib/create_saved_objects_stream_from_ndjson.ts @@ -17,7 +17,7 @@ * under the License. */ import { Readable } from 'stream'; -import { SavedObject } from 'src/core/server'; +import { SavedObject, SavedObjectsExportResultDetails } from 'src/core/server'; import { createSplitStream, createMapStream, createFilterStream } from '../../../utils/streams'; export function createSavedObjectsStreamFromNdJson(ndJsonStream: Readable) { @@ -30,5 +30,9 @@ export function createSavedObjectsStreamFromNdJson(ndJsonStream: Readable) { } }) ) - .pipe(createFilterStream(obj => !!obj)); + .pipe( + createFilterStream( + obj => !!obj && !(obj as SavedObjectsExportResultDetails).exportedCount + ) + ); } diff --git a/src/legacy/server/saved_objects/routes/export.test.ts b/src/legacy/server/saved_objects/routes/export.test.ts index 491e3a9067611..1b7e0dfa65db5 100644 --- a/src/legacy/server/saved_objects/routes/export.test.ts +++ b/src/legacy/server/saved_objects/routes/export.test.ts @@ -157,6 +157,7 @@ describe('POST /api/saved_objects/_export', () => { "calls": Array [ Array [ Object { + "excludeExportDetails": false, "exportSizeLimit": 10000, "includeReferencesDeep": true, "objects": undefined, diff --git a/src/legacy/server/saved_objects/routes/export.ts b/src/legacy/server/saved_objects/routes/export.ts index fc120030a873c..ce4aed4b78c2a 100644 --- a/src/legacy/server/saved_objects/routes/export.ts +++ b/src/legacy/server/saved_objects/routes/export.ts @@ -28,7 +28,7 @@ import { } from '../../../utils/streams'; // Disable lint errors for imports from src/core/server/saved_objects until SavedObjects migration is complete // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getSortedObjectsForExport } from '../../../../core/server/saved_objects/export'; +import { getSortedObjectsForExport } from '../../../../core/server/saved_objects'; import { Prerequisites } from './types'; interface ExportRequest extends Hapi.Request { @@ -43,6 +43,7 @@ interface ExportRequest extends Hapi.Request { }>; search?: string; includeReferencesDeep: boolean; + excludeExportDetails: boolean; }; } @@ -73,6 +74,7 @@ export const createExportRoute = ( .optional(), search: Joi.string().optional(), includeReferencesDeep: Joi.boolean().default(false), + excludeExportDetails: Joi.boolean().default(false), }) .xor('type', 'objects') .nand('search', 'objects') @@ -87,6 +89,7 @@ export const createExportRoute = ( objects: request.payload.objects, exportSizeLimit: server.config().get('savedObjects.maxImportExportSize'), includeReferencesDeep: request.payload.includeReferencesDeep, + excludeExportDetails: request.payload.excludeExportDetails, }); const docsToExport: string[] = await createPromiseFromStreams([ diff --git a/test/api_integration/apis/saved_objects/export.js b/test/api_integration/apis/saved_objects/export.js index e39749aa48159..9ab7a09309952 100644 --- a/test/api_integration/apis/saved_objects/export.js +++ b/test/api_integration/apis/saved_objects/export.js @@ -37,7 +37,30 @@ export default function ({ getService }) { type: ['index-pattern', 'search', 'visualization', 'dashboard'], }) .expect(200) - .then((resp) => { + .then(resp => { + const objects = resp.text.split('\n').map(JSON.parse); + expect(objects).to.have.length(4); + expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab'); + expect(objects[0]).to.have.property('type', 'index-pattern'); + expect(objects[1]).to.have.property('id', 'dd7caf20-9efd-11e7-acb3-3dab96693fab'); + expect(objects[1]).to.have.property('type', 'visualization'); + expect(objects[2]).to.have.property('id', 'be3733a0-9efe-11e7-acb3-3dab96693fab'); + expect(objects[2]).to.have.property('type', 'dashboard'); + expect(objects[3]).to.have.property('exportedCount', 3); + expect(objects[3]).to.have.property('missingRefCount', 0); + expect(objects[3].missingReferences).to.have.length(0); + }); + }); + + it('should exclude the export details if asked', async () => { + await supertest + .post('/api/saved_objects/_export') + .send({ + type: ['index-pattern', 'search', 'visualization', 'dashboard'], + excludeExportDetails: true, + }) + .expect(200) + .then(resp => { const objects = resp.text.split('\n').map(JSON.parse); expect(objects).to.have.length(3); expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab'); @@ -62,15 +85,18 @@ export default function ({ getService }) { ], }) .expect(200) - .then((resp) => { + .then(resp => { const objects = resp.text.split('\n').map(JSON.parse); - expect(objects).to.have.length(3); + expect(objects).to.have.length(4); expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab'); expect(objects[0]).to.have.property('type', 'index-pattern'); expect(objects[1]).to.have.property('id', 'dd7caf20-9efd-11e7-acb3-3dab96693fab'); expect(objects[1]).to.have.property('type', 'visualization'); expect(objects[2]).to.have.property('id', 'be3733a0-9efe-11e7-acb3-3dab96693fab'); expect(objects[2]).to.have.property('type', 'dashboard'); + expect(objects[3]).to.have.property('exportedCount', 3); + expect(objects[3]).to.have.property('missingRefCount', 0); + expect(objects[3].missingReferences).to.have.length(0); }); }); @@ -82,15 +108,18 @@ export default function ({ getService }) { type: ['dashboard'], }) .expect(200) - .then((resp) => { + .then(resp => { const objects = resp.text.split('\n').map(JSON.parse); - expect(objects).to.have.length(3); + expect(objects).to.have.length(4); expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab'); expect(objects[0]).to.have.property('type', 'index-pattern'); expect(objects[1]).to.have.property('id', 'dd7caf20-9efd-11e7-acb3-3dab96693fab'); expect(objects[1]).to.have.property('type', 'visualization'); expect(objects[2]).to.have.property('id', 'be3733a0-9efe-11e7-acb3-3dab96693fab'); expect(objects[2]).to.have.property('type', 'dashboard'); + expect(objects[3]).to.have.property('exportedCount', 3); + expect(objects[3]).to.have.property('missingRefCount', 0); + expect(objects[3].missingReferences).to.have.length(0); }); }); @@ -100,18 +129,21 @@ export default function ({ getService }) { .send({ includeReferencesDeep: true, type: ['dashboard'], - search: 'Requests*' + search: 'Requests*', }) .expect(200) - .then((resp) => { + .then(resp => { const objects = resp.text.split('\n').map(JSON.parse); - expect(objects).to.have.length(3); + expect(objects).to.have.length(4); expect(objects[0]).to.have.property('id', '91200a00-9efd-11e7-acb3-3dab96693fab'); expect(objects[0]).to.have.property('type', 'index-pattern'); expect(objects[1]).to.have.property('id', 'dd7caf20-9efd-11e7-acb3-3dab96693fab'); expect(objects[1]).to.have.property('type', 'visualization'); expect(objects[2]).to.have.property('id', 'be3733a0-9efe-11e7-acb3-3dab96693fab'); expect(objects[2]).to.have.property('type', 'dashboard'); + expect(objects[3]).to.have.property('exportedCount', 3); + expect(objects[3]).to.have.property('missingRefCount', 0); + expect(objects[3].missingReferences).to.have.length(0); }); }); @@ -127,7 +159,7 @@ export default function ({ getService }) { ], }) .expect(400) - .then((resp) => { + .then(resp => { expect(resp.body).to.eql({ statusCode: 400, error: 'Bad Request', @@ -159,12 +191,13 @@ export default function ({ getService }) { expect(resp.body).to.eql({ statusCode: 400, error: 'Bad Request', - message: 'child "type" fails because ["type" at position 0 fails because ' + + message: + 'child "type" fails because ["type" at position 0 fails because ' + '["0" must be one of [config, dashboard, index-pattern, query, search, url, visualization]]]', validation: { source: 'payload', keys: ['type.0'], - } + }, }); }); }); @@ -178,12 +211,12 @@ export default function ({ getService }) { await supertest .post('/api/saved_objects/_export') .expect(400) - .then((resp) => { + .then(resp => { expect(resp.body).to.eql({ statusCode: 400, error: 'Bad Request', message: '"value" must be an object', - validation: { source: 'payload', keys: [ 'value' ] }, + validation: { source: 'payload', keys: ['value'] }, }); }); }); @@ -193,47 +226,55 @@ export default function ({ getService }) { .post('/api/saved_objects/_export') .send({ type: 'dashboard', + excludeExportDetails: true, }) .expect(200) - .then((resp) => { - expect(resp.headers['content-disposition']).to.eql('attachment; filename="export.ndjson"'); + .then(resp => { + expect(resp.headers['content-disposition']).to.eql( + 'attachment; filename="export.ndjson"' + ); expect(resp.headers['content-type']).to.eql('application/ndjson'); const objects = resp.text.split('\n').map(JSON.parse); - expect(objects).to.eql([{ - attributes: { - description: '', - hits: 0, - kibanaSavedObjectMeta: { - searchSourceJSON: objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON, - }, - optionsJSON: objects[0].attributes.optionsJSON, - panelsJSON: objects[0].attributes.panelsJSON, - refreshInterval: { - display: 'Off', - pause: false, - value: 0, + expect(objects).to.eql([ + { + attributes: { + description: '', + hits: 0, + kibanaSavedObjectMeta: { + searchSourceJSON: + objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON, + }, + optionsJSON: objects[0].attributes.optionsJSON, + panelsJSON: objects[0].attributes.panelsJSON, + refreshInterval: { + display: 'Off', + pause: false, + value: 0, + }, + timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700', + timeRestore: true, + timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700', + title: 'Requests', + version: 1, }, - timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700', - timeRestore: true, - timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700', - title: 'Requests', - version: 1, + id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', + migrationVersion: objects[0].migrationVersion, + references: [ + { + id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', + name: 'panel_0', + type: 'visualization', + }, + ], + type: 'dashboard', + updated_at: '2017-09-21T18:57:40.826Z', + version: objects[0].version, }, - id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', - migrationVersion: objects[0].migrationVersion, - references: [ - { - id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - name: 'panel_0', - type: 'visualization', - }, - ], - type: 'dashboard', - updated_at: '2017-09-21T18:57:40.826Z', - version: objects[0].version, - }]); + ]); expect(objects[0].migrationVersion).to.be.ok(); - expect(() => JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)).not.to.throwError(); + expect(() => + JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON) + ).not.to.throwError(); expect(() => JSON.parse(objects[0].attributes.optionsJSON)).not.to.throwError(); expect(() => JSON.parse(objects[0].attributes.panelsJSON)).not.to.throwError(); }); @@ -244,47 +285,55 @@ export default function ({ getService }) { .post('/api/saved_objects/_export') .send({ type: ['dashboard'], + excludeExportDetails: true, }) .expect(200) - .then((resp) => { - expect(resp.headers['content-disposition']).to.eql('attachment; filename="export.ndjson"'); + .then(resp => { + expect(resp.headers['content-disposition']).to.eql( + 'attachment; filename="export.ndjson"' + ); expect(resp.headers['content-type']).to.eql('application/ndjson'); const objects = resp.text.split('\n').map(JSON.parse); - expect(objects).to.eql([{ - attributes: { - description: '', - hits: 0, - kibanaSavedObjectMeta: { - searchSourceJSON: objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON, - }, - optionsJSON: objects[0].attributes.optionsJSON, - panelsJSON: objects[0].attributes.panelsJSON, - refreshInterval: { - display: 'Off', - pause: false, - value: 0, + expect(objects).to.eql([ + { + attributes: { + description: '', + hits: 0, + kibanaSavedObjectMeta: { + searchSourceJSON: + objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON, + }, + optionsJSON: objects[0].attributes.optionsJSON, + panelsJSON: objects[0].attributes.panelsJSON, + refreshInterval: { + display: 'Off', + pause: false, + value: 0, + }, + timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700', + timeRestore: true, + timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700', + title: 'Requests', + version: 1, }, - timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700', - timeRestore: true, - timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700', - title: 'Requests', - version: 1, + id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', + migrationVersion: objects[0].migrationVersion, + references: [ + { + id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', + name: 'panel_0', + type: 'visualization', + }, + ], + type: 'dashboard', + updated_at: '2017-09-21T18:57:40.826Z', + version: objects[0].version, }, - id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', - migrationVersion: objects[0].migrationVersion, - references: [ - { - id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - name: 'panel_0', - type: 'visualization', - }, - ], - type: 'dashboard', - updated_at: '2017-09-21T18:57:40.826Z', - version: objects[0].version, - }]); + ]); expect(objects[0].migrationVersion).to.be.ok(); - expect(() => JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)).not.to.throwError(); + expect(() => + JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON) + ).not.to.throwError(); expect(() => JSON.parse(objects[0].attributes.optionsJSON)).not.to.throwError(); expect(() => JSON.parse(objects[0].attributes.panelsJSON)).not.to.throwError(); }); @@ -300,47 +349,55 @@ export default function ({ getService }) { id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', }, ], + excludeExportDetails: true, }) .expect(200) - .then((resp) => { - expect(resp.headers['content-disposition']).to.eql('attachment; filename="export.ndjson"'); + .then(resp => { + expect(resp.headers['content-disposition']).to.eql( + 'attachment; filename="export.ndjson"' + ); expect(resp.headers['content-type']).to.eql('application/ndjson'); const objects = resp.text.split('\n').map(JSON.parse); - expect(objects).to.eql([{ - attributes: { - description: '', - hits: 0, - kibanaSavedObjectMeta: { - searchSourceJSON: objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON, - }, - optionsJSON: objects[0].attributes.optionsJSON, - panelsJSON: objects[0].attributes.panelsJSON, - refreshInterval: { - display: 'Off', - pause: false, - value: 0, + expect(objects).to.eql([ + { + attributes: { + description: '', + hits: 0, + kibanaSavedObjectMeta: { + searchSourceJSON: + objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON, + }, + optionsJSON: objects[0].attributes.optionsJSON, + panelsJSON: objects[0].attributes.panelsJSON, + refreshInterval: { + display: 'Off', + pause: false, + value: 0, + }, + timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700', + timeRestore: true, + timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700', + title: 'Requests', + version: 1, }, - timeFrom: 'Wed Sep 16 2015 22:52:17 GMT-0700', - timeRestore: true, - timeTo: 'Fri Sep 18 2015 12:24:38 GMT-0700', - title: 'Requests', - version: 1, + id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', + migrationVersion: objects[0].migrationVersion, + references: [ + { + id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', + name: 'panel_0', + type: 'visualization', + }, + ], + type: 'dashboard', + updated_at: '2017-09-21T18:57:40.826Z', + version: objects[0].version, }, - id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', - migrationVersion: objects[0].migrationVersion, - references: [ - { - id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab', - name: 'panel_0', - type: 'visualization', - }, - ], - type: 'dashboard', - updated_at: '2017-09-21T18:57:40.826Z', - version: objects[0].version, - }]); + ]); expect(objects[0].migrationVersion).to.be.ok(); - expect(() => JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON)).not.to.throwError(); + expect(() => + JSON.parse(objects[0].attributes.kibanaSavedObjectMeta.searchSourceJSON) + ).not.to.throwError(); expect(() => JSON.parse(objects[0].attributes.optionsJSON)).not.to.throwError(); expect(() => JSON.parse(objects[0].attributes.panelsJSON)).not.to.throwError(); }); @@ -357,14 +414,15 @@ export default function ({ getService }) { id: 'be3733a0-9efe-11e7-acb3-3dab96693fab', }, ], + excludeExportDetails: true, }) .expect(400) - .then((resp) => { + .then(resp => { expect(resp.body).to.eql({ statusCode: 400, error: 'Bad Request', message: '"value" contains a conflict between exclusive peers [type, objects]', - validation: { source: 'payload', keys: [ 'value' ] }, + validation: { source: 'payload', keys: ['value'] }, }); }); }); @@ -382,14 +440,12 @@ export default function ({ getService }) { }, }) .expect(200) - .then((resp) => { + .then(resp => { customVisId = resp.body.id; }); }); after(async () => { - await supertest - .delete(`/api/saved_objects/visualization/${customVisId}`) - .expect(200); + await supertest.delete(`/api/saved_objects/visualization/${customVisId}`).expect(200); await esArchiver.unload('saved_objects/10k'); }); @@ -398,13 +454,14 @@ export default function ({ getService }) { .post('/api/saved_objects/_export') .send({ type: ['dashboard', 'visualization', 'search', 'index-pattern'], + excludeExportDetails: true, }) .expect(400) - .then((resp) => { + .then(resp => { expect(resp.body).to.eql({ statusCode: 400, error: 'Bad Request', - message: `Can't export more than 10000 objects` + message: `Can't export more than 10000 objects`, }); }); }); @@ -412,22 +469,24 @@ export default function ({ getService }) { }); describe('without kibana index', () => { - before(async () => ( - // just in case the kibana server has recreated it - await es.indices.delete({ - index: '.kibana', - ignore: [404], - }) - )); + before( + async () => + // just in case the kibana server has recreated it + await es.indices.delete({ + index: '.kibana', + ignore: [404], + }) + ); it('should return empty response', async () => { await supertest .post('/api/saved_objects/_export') .send({ type: ['index-pattern', 'search', 'visualization', 'dashboard'], + excludeExportDetails: true, }) .expect(200) - .then((resp) => { + .then(resp => { expect(resp.text).to.eql(''); }); }); diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts index 10e427a29e442..f01a28847bbdf 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts @@ -138,6 +138,7 @@ describe('copySavedObjectsToSpaces', () => { Array [ Array [ Object { + "excludeExportDetails": true, "exportSizeLimit": 1000, "includeReferencesDeep": true, "namespace": "sourceSpace", diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts index 608d57d873687..76c3037e672ad 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.ts @@ -36,6 +36,7 @@ export function copySavedObjectsToSpacesFactory( const objectStream = await importExport.getSortedObjectsForExport({ namespace: spaceIdToNamespace(sourceSpaceId), includeReferencesDeep: options.includeReferences, + excludeExportDetails: true, objects: options.objects, savedObjectsClient, types: eligibleTypes, diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts index fbafb18699081..97b7480ea4af8 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts @@ -158,6 +158,7 @@ describe('resolveCopySavedObjectsToSpacesConflicts', () => { Array [ Array [ Object { + "excludeExportDetails": true, "exportSizeLimit": 1000, "includeReferencesDeep": true, "namespace": "sourceSpace", diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts index d7c602c28b253..22ceeb9dd4dfa 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.ts @@ -31,6 +31,7 @@ export function resolveCopySavedObjectsToSpacesConflictsFactory( const objectStream = await importExport.getSortedObjectsForExport({ namespace: spaceIdToNamespace(sourceSpaceId), includeReferencesDeep: options.includeReferences, + excludeExportDetails: true, objects: options.objects, savedObjectsClient, types: eligibleTypes, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 59ac561065fa0..3919a864b8d69 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1884,8 +1884,6 @@ "kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModalTitle": "保存されたオブジェクトの削除", "kbn.management.objects.objectsTable.export.dangerNotification": "エクスポートを生成できません", "kbn.management.objects.objectsTable.export.successNotification": "ファイルはバックグラウンドでダウンロード中です", - "kbn.management.objects.objectsTable.exportAll.dangerNotification": "エクスポートを生成できません", - "kbn.management.objects.objectsTable.exportAll.successNotification": "ファイルはバックグラウンドでダウンロード中です", "kbn.management.objects.objectsTable.exportObjectsConfirmModal.cancelButtonLabel": "キャンセル", "kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportAllButtonLabel": "すべてエクスポート:", "kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportOptionsLabel": "オプション", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index cf40c5cb6e973..f53a630b7d135 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1885,8 +1885,6 @@ "kbn.management.objects.objectsTable.deleteSavedObjectsConfirmModalTitle": "删除已保存对象", "kbn.management.objects.objectsTable.export.dangerNotification": "无法生成报告", "kbn.management.objects.objectsTable.export.successNotification": "您的文件正在后台下载", - "kbn.management.objects.objectsTable.exportAll.dangerNotification": "无法生成报告", - "kbn.management.objects.objectsTable.exportAll.successNotification": "您的文件正在后台下载", "kbn.management.objects.objectsTable.exportObjectsConfirmModal.cancelButtonLabel": "取消", "kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportAllButtonLabel": "全部导出", "kbn.management.objects.objectsTable.exportObjectsConfirmModal.exportOptionsLabel": "选项", diff --git a/x-pack/test/saved_object_api_integration/common/suites/export.ts b/x-pack/test/saved_object_api_integration/common/suites/export.ts index d7d1a99e63e02..114a1fe53ccd6 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/export.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/export.ts @@ -104,6 +104,7 @@ export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest Date: Wed, 16 Oct 2019 14:18:34 +0200 Subject: [PATCH 24/48] UI settings move to NP (#47590) * add tests for logWithMetadata in LP * allow passing metadata to log in NP & LP * move ui_settings_client to NP * add ui_settings config * add ui_settings_service * switch to NP logging * export types * bootstrap uiSettings service in NP * pass NP uiSettings to LP * move ui_settings mock to NP * add test for mixin and switch to NP logger * make UiSettingsClient.getDefaults sync as it is * ui_settings_client uses private fields * ui_settings_client uses private methods * keep uiSettings config validation in NP only * update mocks * core context should know it is mocked * add tests for ui_settings_service * remove unused code from ui_settings_mixin test * improve types in ui_settings_mixin test * gen docs * test moved to NP * set pkg version in tests explicitly * update mocks in tests * UiSettingsServiceSetup --> InternalUiSettingsServiceSetup * add links to types * address eli comment * regen docs * remove unused types --- .eslintrc.js | 3 + ...ana-plugin-server.iuisettingsclient.get.md | 13 ++ ...-plugin-server.iuisettingsclient.getall.md | 13 ++ ...in-server.iuisettingsclient.getdefaults.md | 13 ++ ...erver.iuisettingsclient.getuserprovided.md | 16 ++ ...n-server.iuisettingsclient.isoverridden.md | 13 ++ .../kibana-plugin-server.iuisettingsclient.md | 28 +++ ...-plugin-server.iuisettingsclient.remove.md | 13 ++ ...gin-server.iuisettingsclient.removemany.md | 13 ++ ...ana-plugin-server.iuisettingsclient.set.md | 13 ++ ...plugin-server.iuisettingsclient.setmany.md | 13 ++ .../core/server/kibana-plugin-server.md | 3 + ...plugin-server.uisettingsparams.category.md | 13 ++ ...gin-server.uisettingsparams.description.md | 13 ++ .../kibana-plugin-server.uisettingsparams.md | 28 +++ ...ana-plugin-server.uisettingsparams.name.md | 13 ++ ...in-server.uisettingsparams.optionlabels.md | 13 ++ ...-plugin-server.uisettingsparams.options.md | 13 ++ ...plugin-server.uisettingsparams.readonly.md | 13 ++ ...ver.uisettingsparams.requirespagereload.md | 13 ++ ...ana-plugin-server.uisettingsparams.type.md | 13 ++ ...na-plugin-server.uisettingsparams.value.md | 13 ++ .../kibana-plugin-server.uisettingstype.md | 13 ++ src/core/server/core_context.mock.ts | 2 +- src/core/server/http/http_config.ts | 3 + src/core/server/http/http_service.mock.ts | 1 + src/core/server/http/http_service.ts | 4 + src/core/server/http/types.ts | 10 ++ src/core/server/index.ts | 10 ++ src/core/server/legacy/legacy_service.test.ts | 47 ++--- src/core/server/legacy/legacy_service.ts | 1 + src/core/server/mocks.ts | 1 + src/core/server/server.api.md | 40 +++++ src/core/server/server.ts | 10 ++ .../ui_settings/create_objects_client_stub.ts | 2 +- .../create_or_upgrade_saved_config.test.ts | 46 ++--- .../create_or_upgrade_saved_config.ts | 21 +-- .../get_upgradeable_config.ts | 2 +- .../create_or_upgrade_saved_config/index.ts | 0 .../create_or_upgrade.test.ts | 15 +- .../is_config_version_upgradeable.test.ts | 12 +- .../is_config_version_upgradeable.ts | 0 src/core/server/ui_settings/index.ts | 32 ++++ .../ui_settings/ui_settings_client.test.ts} | 33 ++-- .../server/ui_settings/ui_settings_client.ts} | 170 +++++++++--------- .../server/ui_settings/ui_settings_config.ts | 32 ++++ .../ui_settings/ui_settings_service.mock.ts | 19 +- .../ui_settings_service.test.mock.ts | 24 +++ .../ui_settings/ui_settings_service.test.ts | 109 +++++++++++ .../server/ui_settings/ui_settings_service.ts | 140 +++++++++++++++ src/legacy/server/config/schema.js | 4 +- .../server/config/transform_deprecations.js | 1 - .../config/transform_deprecations.test.js | 18 -- .../default_route_provider.test.ts | 4 +- .../http/setup_default_route_provider.ts | 2 +- src/legacy/server/kbn_server.d.ts | 9 +- .../mixin/field_formats_mixin.ts | 2 +- .../ui_settings_mixin.test.ts | 59 +++--- .../routes/integration_tests/lib/servers.ts | 3 +- .../ui/ui_settings/ui_settings_mixin.js | 15 +- .../ui_settings_service_factory.ts | 29 +-- .../ui_settings_service_for_request.ts | 17 +- .../server/lib/helpers/setup_request.test.ts | 6 +- .../legacy/plugins/ml/common/types/modules.ts | 3 +- 64 files changed, 968 insertions(+), 302 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.iuisettingsclient.get.md create mode 100644 docs/development/core/server/kibana-plugin-server.iuisettingsclient.getall.md create mode 100644 docs/development/core/server/kibana-plugin-server.iuisettingsclient.getdefaults.md create mode 100644 docs/development/core/server/kibana-plugin-server.iuisettingsclient.getuserprovided.md create mode 100644 docs/development/core/server/kibana-plugin-server.iuisettingsclient.isoverridden.md create mode 100644 docs/development/core/server/kibana-plugin-server.iuisettingsclient.md create mode 100644 docs/development/core/server/kibana-plugin-server.iuisettingsclient.remove.md create mode 100644 docs/development/core/server/kibana-plugin-server.iuisettingsclient.removemany.md create mode 100644 docs/development/core/server/kibana-plugin-server.iuisettingsclient.set.md create mode 100644 docs/development/core/server/kibana-plugin-server.iuisettingsclient.setmany.md create mode 100644 docs/development/core/server/kibana-plugin-server.uisettingsparams.category.md create mode 100644 docs/development/core/server/kibana-plugin-server.uisettingsparams.description.md create mode 100644 docs/development/core/server/kibana-plugin-server.uisettingsparams.md create mode 100644 docs/development/core/server/kibana-plugin-server.uisettingsparams.name.md create mode 100644 docs/development/core/server/kibana-plugin-server.uisettingsparams.optionlabels.md create mode 100644 docs/development/core/server/kibana-plugin-server.uisettingsparams.options.md create mode 100644 docs/development/core/server/kibana-plugin-server.uisettingsparams.readonly.md create mode 100644 docs/development/core/server/kibana-plugin-server.uisettingsparams.requirespagereload.md create mode 100644 docs/development/core/server/kibana-plugin-server.uisettingsparams.type.md create mode 100644 docs/development/core/server/kibana-plugin-server.uisettingsparams.value.md create mode 100644 docs/development/core/server/kibana-plugin-server.uisettingstype.md rename src/{legacy/ui => core/server}/ui_settings/create_objects_client_stub.ts (96%) rename src/{legacy/ui => core/server}/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts (87%) rename src/{legacy/ui => core/server}/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts (82%) rename src/{legacy/ui => core/server}/ui_settings/create_or_upgrade_saved_config/get_upgradeable_config.ts (95%) rename src/{legacy/ui => core/server}/ui_settings/create_or_upgrade_saved_config/index.ts (100%) rename src/{legacy/ui => core/server}/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts (95%) rename src/{legacy/ui => core/server}/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.test.ts (86%) rename src/{legacy/ui => core/server}/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.ts (100%) create mode 100644 src/core/server/ui_settings/index.ts rename src/{legacy/ui/ui_settings/ui_settings_service.test.ts => core/server/ui_settings/ui_settings_client.test.ts} (94%) rename src/{legacy/ui/ui_settings/ui_settings_service.ts => core/server/ui_settings/ui_settings_client.ts} (63%) create mode 100644 src/core/server/ui_settings/ui_settings_config.ts rename src/{legacy/ui => core/server}/ui_settings/ui_settings_service.mock.ts (71%) create mode 100644 src/core/server/ui_settings/ui_settings_service.test.mock.ts create mode 100644 src/core/server/ui_settings/ui_settings_service.test.ts create mode 100644 src/core/server/ui_settings/ui_settings_service.ts diff --git a/.eslintrc.js b/.eslintrc.js index 56950a70970d4..12bdd11fc8528 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -150,6 +150,9 @@ module.exports = { '!src/core/server/index.ts', '!src/core/server/mocks.ts', '!src/core/server/types.ts', + // for absolute imports until fixed in + // https://github.com/elastic/kibana/issues/36096 + '!src/core/server/types', '!src/core/server/*.test.mocks.ts', 'src/plugins/**/public/**/*', diff --git a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.get.md b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.get.md new file mode 100644 index 0000000000000..0ec3ac45c6cb5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.get.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IUiSettingsClient](./kibana-plugin-server.iuisettingsclient.md) > [get](./kibana-plugin-server.iuisettingsclient.get.md) + +## IUiSettingsClient.get property + +Retrieves uiSettings values set by the user with fallbacks to default values if not specified. + +Signature: + +```typescript +get: (key: string) => Promise; +``` diff --git a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.getall.md b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.getall.md new file mode 100644 index 0000000000000..d6765a5e5407e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.getall.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IUiSettingsClient](./kibana-plugin-server.iuisettingsclient.md) > [getAll](./kibana-plugin-server.iuisettingsclient.getall.md) + +## IUiSettingsClient.getAll property + +Retrieves a set of all uiSettings values set by the user with fallbacks to default values if not specified. + +Signature: + +```typescript +getAll: () => Promise>; +``` diff --git a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.getdefaults.md b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.getdefaults.md new file mode 100644 index 0000000000000..29faa6d945b43 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.getdefaults.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IUiSettingsClient](./kibana-plugin-server.iuisettingsclient.md) > [getDefaults](./kibana-plugin-server.iuisettingsclient.getdefaults.md) + +## IUiSettingsClient.getDefaults property + +Returns uiSettings default values [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) + +Signature: + +```typescript +getDefaults: () => Record; +``` diff --git a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.getuserprovided.md b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.getuserprovided.md new file mode 100644 index 0000000000000..9a449b64ed5d0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.getuserprovided.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IUiSettingsClient](./kibana-plugin-server.iuisettingsclient.md) > [getUserProvided](./kibana-plugin-server.iuisettingsclient.getuserprovided.md) + +## IUiSettingsClient.getUserProvided property + +Retrieves a set of all uiSettings values set by the user. + +Signature: + +```typescript +getUserProvided: () => Promise>; +``` diff --git a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.isoverridden.md b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.isoverridden.md new file mode 100644 index 0000000000000..a53655763a79b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.isoverridden.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IUiSettingsClient](./kibana-plugin-server.iuisettingsclient.md) > [isOverridden](./kibana-plugin-server.iuisettingsclient.isoverridden.md) + +## IUiSettingsClient.isOverridden property + +Shows whether the uiSettings value set by the user. + +Signature: + +```typescript +isOverridden: (key: string) => boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.md b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.md new file mode 100644 index 0000000000000..142f33d27c385 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IUiSettingsClient](./kibana-plugin-server.iuisettingsclient.md) + +## IUiSettingsClient interface + +Service that provides access to the UiSettings stored in elasticsearch. + +Signature: + +```typescript +export interface IUiSettingsClient +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [get](./kibana-plugin-server.iuisettingsclient.get.md) | <T extends SavedObjectAttribute = any>(key: string) => Promise<T> | Retrieves uiSettings values set by the user with fallbacks to default values if not specified. | +| [getAll](./kibana-plugin-server.iuisettingsclient.getall.md) | <T extends SavedObjectAttribute = any>() => Promise<Record<string, T>> | Retrieves a set of all uiSettings values set by the user with fallbacks to default values if not specified. | +| [getDefaults](./kibana-plugin-server.iuisettingsclient.getdefaults.md) | () => Record<string, UiSettingsParams> | Returns uiSettings default values [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) | +| [getUserProvided](./kibana-plugin-server.iuisettingsclient.getuserprovided.md) | <T extends SavedObjectAttribute = any>() => Promise<Record<string, {
userValue?: T;
isOverridden?: boolean;
}>> | Retrieves a set of all uiSettings values set by the user. | +| [isOverridden](./kibana-plugin-server.iuisettingsclient.isoverridden.md) | (key: string) => boolean | Shows whether the uiSettings value set by the user. | +| [remove](./kibana-plugin-server.iuisettingsclient.remove.md) | (key: string) => Promise<void> | Removes uiSettings value by key. | +| [removeMany](./kibana-plugin-server.iuisettingsclient.removemany.md) | (keys: string[]) => Promise<void> | Removes multiple uiSettings values by keys. | +| [set](./kibana-plugin-server.iuisettingsclient.set.md) | <T extends SavedObjectAttribute = any>(key: string, value: T) => Promise<void> | Writes uiSettings value and marks it as set by the user. | +| [setMany](./kibana-plugin-server.iuisettingsclient.setmany.md) | <T extends SavedObjectAttribute = any>(changes: Record<string, T>) => Promise<void> | Writes multiple uiSettings values and marks them as set by the user. | + diff --git a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.remove.md b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.remove.md new file mode 100644 index 0000000000000..fa15b11fd76e4 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.remove.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IUiSettingsClient](./kibana-plugin-server.iuisettingsclient.md) > [remove](./kibana-plugin-server.iuisettingsclient.remove.md) + +## IUiSettingsClient.remove property + +Removes uiSettings value by key. + +Signature: + +```typescript +remove: (key: string) => Promise; +``` diff --git a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.removemany.md b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.removemany.md new file mode 100644 index 0000000000000..ef5f994707aae --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.removemany.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IUiSettingsClient](./kibana-plugin-server.iuisettingsclient.md) > [removeMany](./kibana-plugin-server.iuisettingsclient.removemany.md) + +## IUiSettingsClient.removeMany property + +Removes multiple uiSettings values by keys. + +Signature: + +```typescript +removeMany: (keys: string[]) => Promise; +``` diff --git a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.set.md b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.set.md new file mode 100644 index 0000000000000..bc67d05b3f0ee --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.set.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IUiSettingsClient](./kibana-plugin-server.iuisettingsclient.md) > [set](./kibana-plugin-server.iuisettingsclient.set.md) + +## IUiSettingsClient.set property + +Writes uiSettings value and marks it as set by the user. + +Signature: + +```typescript +set: (key: string, value: T) => Promise; +``` diff --git a/docs/development/core/server/kibana-plugin-server.iuisettingsclient.setmany.md b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.setmany.md new file mode 100644 index 0000000000000..ec2c24951f0ec --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.iuisettingsclient.setmany.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IUiSettingsClient](./kibana-plugin-server.iuisettingsclient.md) > [setMany](./kibana-plugin-server.iuisettingsclient.setmany.md) + +## IUiSettingsClient.setMany property + +Writes multiple uiSettings values and marks them as set by the user. + +Signature: + +```typescript +setMany: (changes: Record) => Promise; +``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index b2d5491d01a4b..7b302838995c1 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -63,6 +63,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [IKibanaSocket](./kibana-plugin-server.ikibanasocket.md) | A tiny abstraction for TCP socket. | | [IndexSettingsDeprecationInfo](./kibana-plugin-server.indexsettingsdeprecationinfo.md) | | | [IRouter](./kibana-plugin-server.irouter.md) | Registers route handlers for specified resource path and method. See [RouteConfig](./kibana-plugin-server.routeconfig.md) and [RequestHandler](./kibana-plugin-server.requesthandler.md) for more information about arguments to route registrations. | +| [IUiSettingsClient](./kibana-plugin-server.iuisettingsclient.md) | Service that provides access to the UiSettings stored in elasticsearch. | | [KibanaRequestRoute](./kibana-plugin-server.kibanarequestroute.md) | Request specific route information exposed to a handler. | | [LegacyRequest](./kibana-plugin-server.legacyrequest.md) | | | [LegacyServiceSetupDeps](./kibana-plugin-server.legacyservicesetupdeps.md) | | @@ -112,6 +113,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SessionStorage](./kibana-plugin-server.sessionstorage.md) | Provides an interface to store and retrieve data across requests. | | [SessionStorageCookieOptions](./kibana-plugin-server.sessionstoragecookieoptions.md) | Configuration used to create HTTP session storage based on top of cookie mechanism. | | [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) | SessionStorage factory to bind one to an incoming request | +| [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) | UiSettings parameters defined by the plugins. | ## Variables @@ -163,4 +165,5 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectAttributeSingle](./kibana-plugin-server.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-server.savedobjectattribute.md) | | [SavedObjectsClientContract](./kibana-plugin-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.\#\#\# 503s from missing indexUnlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's action.auto_create_index setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated.See [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | | [SavedObjectsClientWrapperFactory](./kibana-plugin-server.savedobjectsclientwrapperfactory.md) | Describes the factory used to create instances of Saved Objects Client Wrappers. | +| [UiSettingsType](./kibana-plugin-server.uisettingstype.md) | UI element type to represent the settings. | diff --git a/docs/development/core/server/kibana-plugin-server.uisettingsparams.category.md b/docs/development/core/server/kibana-plugin-server.uisettingsparams.category.md new file mode 100644 index 0000000000000..47aedbfbf2810 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.uisettingsparams.category.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) > [category](./kibana-plugin-server.uisettingsparams.category.md) + +## UiSettingsParams.category property + +used to group the configured setting in the UI + +Signature: + +```typescript +category: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.uisettingsparams.description.md b/docs/development/core/server/kibana-plugin-server.uisettingsparams.description.md new file mode 100644 index 0000000000000..8d8887285ae2e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.uisettingsparams.description.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) > [description](./kibana-plugin-server.uisettingsparams.description.md) + +## UiSettingsParams.description property + +description provided to a user in UI + +Signature: + +```typescript +description: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.uisettingsparams.md b/docs/development/core/server/kibana-plugin-server.uisettingsparams.md new file mode 100644 index 0000000000000..275111c05eff9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.uisettingsparams.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) + +## UiSettingsParams interface + +UiSettings parameters defined by the plugins. + +Signature: + +```typescript +export interface UiSettingsParams +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [category](./kibana-plugin-server.uisettingsparams.category.md) | string[] | used to group the configured setting in the UI | +| [description](./kibana-plugin-server.uisettingsparams.description.md) | string | description provided to a user in UI | +| [name](./kibana-plugin-server.uisettingsparams.name.md) | string | title in the UI | +| [optionLabels](./kibana-plugin-server.uisettingsparams.optionlabels.md) | Record<string, string> | text labels for 'select' type UI element | +| [options](./kibana-plugin-server.uisettingsparams.options.md) | string[] | a range of valid values | +| [readonly](./kibana-plugin-server.uisettingsparams.readonly.md) | boolean | a flag indicating that value cannot be changed | +| [requiresPageReload](./kibana-plugin-server.uisettingsparams.requirespagereload.md) | boolean | a flag indicating whether new value applying requires page reloading | +| [type](./kibana-plugin-server.uisettingsparams.type.md) | UiSettingsType | defines a type of UI element [UiSettingsType](./kibana-plugin-server.uisettingstype.md) | +| [value](./kibana-plugin-server.uisettingsparams.value.md) | SavedObjectAttribute | default value to fall back to if a user doesn't provide any | + diff --git a/docs/development/core/server/kibana-plugin-server.uisettingsparams.name.md b/docs/development/core/server/kibana-plugin-server.uisettingsparams.name.md new file mode 100644 index 0000000000000..2b414eefffed2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.uisettingsparams.name.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) > [name](./kibana-plugin-server.uisettingsparams.name.md) + +## UiSettingsParams.name property + +title in the UI + +Signature: + +```typescript +name: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.uisettingsparams.optionlabels.md b/docs/development/core/server/kibana-plugin-server.uisettingsparams.optionlabels.md new file mode 100644 index 0000000000000..cb0e196fdcacc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.uisettingsparams.optionlabels.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) > [optionLabels](./kibana-plugin-server.uisettingsparams.optionlabels.md) + +## UiSettingsParams.optionLabels property + +text labels for 'select' type UI element + +Signature: + +```typescript +optionLabels?: Record; +``` diff --git a/docs/development/core/server/kibana-plugin-server.uisettingsparams.options.md b/docs/development/core/server/kibana-plugin-server.uisettingsparams.options.md new file mode 100644 index 0000000000000..71eecdfabc4a0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.uisettingsparams.options.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) > [options](./kibana-plugin-server.uisettingsparams.options.md) + +## UiSettingsParams.options property + +a range of valid values + +Signature: + +```typescript +options?: string[]; +``` diff --git a/docs/development/core/server/kibana-plugin-server.uisettingsparams.readonly.md b/docs/development/core/server/kibana-plugin-server.uisettingsparams.readonly.md new file mode 100644 index 0000000000000..faec4d6eadbcc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.uisettingsparams.readonly.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) > [readonly](./kibana-plugin-server.uisettingsparams.readonly.md) + +## UiSettingsParams.readonly property + +a flag indicating that value cannot be changed + +Signature: + +```typescript +readonly?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.uisettingsparams.requirespagereload.md b/docs/development/core/server/kibana-plugin-server.uisettingsparams.requirespagereload.md new file mode 100644 index 0000000000000..224b3695224b9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.uisettingsparams.requirespagereload.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) > [requiresPageReload](./kibana-plugin-server.uisettingsparams.requirespagereload.md) + +## UiSettingsParams.requiresPageReload property + +a flag indicating whether new value applying requires page reloading + +Signature: + +```typescript +requiresPageReload?: boolean; +``` diff --git a/docs/development/core/server/kibana-plugin-server.uisettingsparams.type.md b/docs/development/core/server/kibana-plugin-server.uisettingsparams.type.md new file mode 100644 index 0000000000000..ccf2d67b2dffb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.uisettingsparams.type.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) > [type](./kibana-plugin-server.uisettingsparams.type.md) + +## UiSettingsParams.type property + +defines a type of UI element [UiSettingsType](./kibana-plugin-server.uisettingstype.md) + +Signature: + +```typescript +type?: UiSettingsType; +``` diff --git a/docs/development/core/server/kibana-plugin-server.uisettingsparams.value.md b/docs/development/core/server/kibana-plugin-server.uisettingsparams.value.md new file mode 100644 index 0000000000000..455756899ecfc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.uisettingsparams.value.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [UiSettingsParams](./kibana-plugin-server.uisettingsparams.md) > [value](./kibana-plugin-server.uisettingsparams.value.md) + +## UiSettingsParams.value property + +default value to fall back to if a user doesn't provide any + +Signature: + +```typescript +value: SavedObjectAttribute; +``` diff --git a/docs/development/core/server/kibana-plugin-server.uisettingstype.md b/docs/development/core/server/kibana-plugin-server.uisettingstype.md new file mode 100644 index 0000000000000..789d4d5788468 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.uisettingstype.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [UiSettingsType](./kibana-plugin-server.uisettingstype.md) + +## UiSettingsType type + +UI element type to represent the settings. + +Signature: + +```typescript +export declare type UiSettingsType = 'json' | 'markdown' | 'number' | 'select' | 'boolean' | 'string'; +``` diff --git a/src/core/server/core_context.mock.ts b/src/core/server/core_context.mock.ts index e8c0a0a4830bf..d287348e19079 100644 --- a/src/core/server/core_context.mock.ts +++ b/src/core/server/core_context.mock.ts @@ -32,7 +32,7 @@ function create({ env?: Env; logger?: jest.Mocked; configService?: jest.Mocked; -} = {}): CoreContext { +} = {}): DeeplyMockedKeys { return { coreId: Symbol(), env, logger, configService }; } diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index a42d38fd4cb70..c4a61aaf83ac7 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -38,6 +38,7 @@ export const config = { validate: match(validBasePathRegex, "must start with a slash, don't end with one"), }) ), + defaultRoute: schema.maybe(schema.string()), cors: schema.conditional( schema.contextRef('dev'), true, @@ -106,6 +107,7 @@ export class HttpConfig { public basePath?: string; public rewriteBasePath: boolean; public publicDir: string; + public defaultRoute?: string; public ssl: SslConfig; /** @@ -123,5 +125,6 @@ export class HttpConfig { this.rewriteBasePath = rawConfig.rewriteBasePath; this.publicDir = env.staticFilesDir; this.ssl = new SslConfig(rawConfig.ssl || {}); + this.defaultRoute = rawConfig.defaultRoute; } } diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index 40a140f154a6e..00c9aedc42cfb 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -68,6 +68,7 @@ const createSetupContractMock = () => { getAuthHeaders: jest.fn(), }, isTlsEnabled: false, + config: {}, }; setupContract.createCookieSessionStorageFactory.mockResolvedValue( sessionStorageMock.createFactory() diff --git a/src/core/server/http/http_service.ts b/src/core/server/http/http_service.ts index c720d2fea4fc6..caebd768c70e5 100644 --- a/src/core/server/http/http_service.ts +++ b/src/core/server/http/http_service.ts @@ -106,6 +106,10 @@ export class HttpService implements CoreService ) => this.requestHandlerContext!.registerContext(pluginOpaqueId, contextName, provider), + + config: { + defaultRoute: config.defaultRoute, + }, }; return contract; diff --git a/src/core/server/http/types.ts b/src/core/server/http/types.ts index d75028ca12d66..6f5cb02fd8cba 100644 --- a/src/core/server/http/types.ts +++ b/src/core/server/http/types.ts @@ -231,6 +231,16 @@ export interface InternalHttpServiceSetup contextName: T, provider: RequestHandlerContextProvider ) => RequestHandlerContextContainer; + config: { + /** + * @internalRemarks + * Deprecated part of the server config, provided until + * https://github.com/elastic/kibana/issues/40255 + * + * @deprecated + * */ + defaultRoute?: string; + }; } /** @public */ diff --git a/src/core/server/index.ts b/src/core/server/index.ts index fe4d0a05c4fb9..90e5746b2766d 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -48,6 +48,8 @@ import { InternalHttpServiceSetup, HttpServiceSetup } from './http'; import { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId } from './plugins'; import { ContextSetup } from './context'; import { SavedObjectsServiceStart } from './saved_objects'; + +import { InternalUiSettingsServiceSetup } from './ui_settings'; import { SavedObjectsClientContract } from './saved_objects/types'; export { bootstrap } from './bootstrap'; @@ -164,6 +166,13 @@ export { SavedObjectsUpdateResponse, } from './saved_objects'; +export { + IUiSettingsClient, + UiSettingsParams, + InternalUiSettingsServiceSetup, + UiSettingsType, +} from './ui_settings'; + export { RecursiveReadonly } from '../utils'; export { @@ -231,6 +240,7 @@ export interface InternalCoreSetup { context: ContextSetup; http: InternalHttpServiceSetup; elasticsearch: InternalElasticsearchServiceSetup; + uiSettings: InternalUiSettingsServiceSetup; } /** diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index bdb1499e065ca..590bd192e3ded 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -30,52 +30,33 @@ jest.mock('./plugins/find_legacy_plugin_specs.ts', () => ({ }), })); -import { LegacyService } from '.'; +import { LegacyService, LegacyServiceSetupDeps, LegacyServiceStartDeps } from '.'; // @ts-ignore: implicit any for JS file import MockClusterManager from '../../../cli/cluster/cluster_manager'; import KbnServer from '../../../legacy/server/kbn_server'; import { Config, Env, ObjectToConfigAdapter } from '../config'; -import { ContextSetup } from '../context'; import { contextServiceMock } from '../context/context_service.mock'; import { getEnvOptions } from '../config/__mocks__/env'; import { configServiceMock } from '../config/config_service.mock'; -import { InternalElasticsearchServiceSetup } from '../elasticsearch'; -import { HttpServiceStart, BasePathProxyServer } from '../http'; + +import { BasePathProxyServer } from '../http'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { DiscoveredPlugin, DiscoveredPluginInternal } from '../plugins'; -import { PluginsServiceSetup, PluginsServiceStart } from '../plugins/plugins_service'; -import { - SavedObjectsServiceStart, - SavedObjectsServiceSetup, -} from 'src/core/server/saved_objects/saved_objects_service'; + import { KibanaMigrator } from '../saved_objects/migrations'; import { ISavedObjectsClientProvider } from '../saved_objects'; import { httpServiceMock } from '../http/http_service.mock'; +import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; const MockKbnServer: jest.Mock = KbnServer as any; let coreId: symbol; let env: Env; let config$: BehaviorSubject; -let setupDeps: { - core: { - context: ContextSetup; - elasticsearch: InternalElasticsearchServiceSetup; - http: any; - plugins: PluginsServiceSetup; - savedObjects: SavedObjectsServiceSetup; - }; - plugins: Record; -}; - -let startDeps: { - core: { - http: HttpServiceStart; - savedObjects: SavedObjectsServiceStart; - plugins: PluginsServiceStart; - }; - plugins: Record; -}; + +let setupDeps: LegacyServiceSetupDeps; + +let startDeps: LegacyServiceStartDeps; const logger = loggingServiceMock.create(); let configService: ReturnType; @@ -91,12 +72,14 @@ beforeEach(() => { core: { context: contextServiceMock.createSetupContract(), elasticsearch: { legacy: {} } as any, + uiSettings: uiSettingsServiceMock.createSetup(), http: { ...httpServiceMock.createSetupContract(), auth: { getAuthHeaders: () => undefined, - }, + } as any, }, + plugins: { contracts: new Map([['plugin-id', 'plugin-value']]), uiPlugins: { @@ -104,18 +87,12 @@ beforeEach(() => { internal: new Map([['plugin-id', {} as DiscoveredPluginInternal]]), }, }, - savedObjects: { - clientProvider: {} as ISavedObjectsClientProvider, - }, }, plugins: { 'plugin-id': 'plugin-value' }, }; startDeps = { core: { - http: { - isListening: () => true, - }, savedObjects: { migrator: {} as KibanaMigrator, clientProvider: {} as ISavedObjectsClientProvider, diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index edd913fe16a63..dd3ee3db89153 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -275,6 +275,7 @@ export class LegacyService implements CoreService { kibanaMigrator: startDeps.core.savedObjects.migrator, uiPlugins: setupDeps.core.plugins.uiPlugins, elasticsearch: setupDeps.core.elasticsearch, + uiSettings: setupDeps.core.uiSettings, savedObjectsClientProvider: startDeps.core.savedObjects.clientProvider, }, logger: this.coreContext.logger, diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index c3d524c77402c..fb703c6c35008 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -30,6 +30,7 @@ export { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service. export { httpServiceMock } from './http/http_service.mock'; export { loggingServiceMock } from './logging/logging_service.mock'; export { SavedObjectsClientMock } from './saved_objects/service/saved_objects_client.mock'; +export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; export function pluginInitializerContextConfigMock(config: T) { const mock: jest.Mocked['config']> = { diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 604a0916c028f..a329fda5d8593 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -722,6 +722,8 @@ export interface InternalCoreSetup { // // (undocumented) http: InternalHttpServiceSetup; + // (undocumented) + uiSettings: InternalUiSettingsServiceSetup; } // @internal (undocumented) @@ -732,6 +734,12 @@ export interface InternalCoreStart { savedObjects: SavedObjectsServiceStart; } +// @internal (undocumented) +export interface InternalUiSettingsServiceSetup { + asScopedToClient(savedObjectsClient: SavedObjectsClientContract): IUiSettingsClient; + setDefaults(values: Record): void; +} + // @public export interface IRouter { delete:

(route: RouteConfig, handler: RequestHandler) => void; @@ -751,6 +759,22 @@ export type IsAuthenticated = (request: KibanaRequest | LegacyRequest) => boolea // @public export type IScopedClusterClient = Pick; +// @public +export interface IUiSettingsClient { + get: (key: string) => Promise; + getAll: () => Promise>; + getDefaults: () => Record; + getUserProvided: () => Promise>; + isOverridden: (key: string) => boolean; + remove: (key: string) => Promise; + removeMany: (keys: string[]) => Promise; + set: (key: string, value: T) => Promise; + setMany: (changes: Record) => Promise; +} + // @public export class KibanaRequest { // @internal (undocumented) @@ -1556,6 +1580,22 @@ export interface SessionStorageFactory { asScoped: (request: KibanaRequest) => SessionStorage; } +// @public +export interface UiSettingsParams { + category: string[]; + description: string; + name: string; + optionLabels?: Record; + options?: string[]; + readonly?: boolean; + requiresPageReload?: boolean; + type?: UiSettingsType; + value: SavedObjectAttribute; +} + +// @public +export type UiSettingsType = 'json' | 'markdown' | 'number' | 'select' | 'boolean' | 'string'; + // Warnings were encountered during analysis: // diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 455bb97d47b57..3e4f5c2e5a813 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -25,6 +25,7 @@ import { ElasticsearchService, ElasticsearchServiceSetup } from './elasticsearch import { HttpService, InternalHttpServiceSetup } from './http'; import { LegacyService } from './legacy'; import { Logger, LoggerFactory } from './logging'; +import { UiSettingsService } from './ui_settings'; import { PluginsService, config as pluginsConfig } from './plugins'; import { SavedObjectsService } from '../server/saved_objects'; @@ -34,6 +35,7 @@ import { config as loggingConfig } from './logging'; import { config as devConfig } from './dev'; import { config as kibanaConfig } from './kibana_config'; import { config as savedObjectsConfig } from './saved_objects'; +import { config as uiSettingsConfig } from './ui_settings'; import { mapToObject } from '../utils/'; import { ContextService } from './context'; import { SavedObjectsServiceSetup } from './saved_objects/saved_objects_service'; @@ -50,6 +52,7 @@ export class Server { private readonly log: Logger; private readonly plugins: PluginsService; private readonly savedObjects: SavedObjectsService; + private readonly uiSettings: UiSettingsService; constructor( readonly config$: Observable, @@ -66,6 +69,7 @@ export class Server { this.legacy = new LegacyService(core); this.elasticsearch = new ElasticsearchService(core); this.savedObjects = new SavedObjectsService(core); + this.uiSettings = new UiSettingsService(core); } public async setup() { @@ -89,10 +93,15 @@ export class Server { http: httpSetup, }); + const uiSettingsSetup = await this.uiSettings.setup({ + http: httpSetup, + }); + const coreSetup = { context: contextServiceSetup, elasticsearch: elasticsearchServiceSetup, http: httpSetup, + uiSettings: uiSettingsSetup, }; const pluginsSetup = await this.plugins.setup(coreSetup); @@ -184,6 +193,7 @@ export class Server { [devConfig.path, devConfig.schema], [kibanaConfig.path, kibanaConfig.schema], [savedObjectsConfig.path, savedObjectsConfig.schema], + [uiSettingsConfig.path, uiSettingsConfig.schema], ]; for (const [path, schema] of schemas) { diff --git a/src/legacy/ui/ui_settings/create_objects_client_stub.ts b/src/core/server/ui_settings/create_objects_client_stub.ts similarity index 96% rename from src/legacy/ui/ui_settings/create_objects_client_stub.ts rename to src/core/server/ui_settings/create_objects_client_stub.ts index ad19b5c8bc7cf..d52ec58fa7e37 100644 --- a/src/legacy/ui/ui_settings/create_objects_client_stub.ts +++ b/src/core/server/ui_settings/create_objects_client_stub.ts @@ -18,7 +18,7 @@ */ import sinon from 'sinon'; -import { SavedObjectsClient } from '../../../../src/core/server'; +import { SavedObjectsClient } from '../saved_objects'; export const savedObjectsClientErrors = SavedObjectsClient.errors; diff --git a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts similarity index 87% rename from src/legacy/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts rename to src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts index 654c0fbb66c8b..5f7e915365873 100644 --- a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.test.ts @@ -18,14 +18,13 @@ */ import sinon from 'sinon'; -import expect from '@kbn/expect'; import Chance from 'chance'; +import { loggingServiceMock } from '../../logging/logging_service.mock'; import * as getUpgradeableConfigNS from './get_upgradeable_config'; import { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; const chance = new Chance(); - describe('uiSettings/createOrUpgradeSavedConfig', function() { const sandbox = sinon.createSandbox(); afterEach(() => sandbox.restore()); @@ -35,7 +34,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { const buildNum = chance.integer({ min: 1000, max: 5000 }); function setup() { - const logWithMetadata = sinon.stub(); + const logger = loggingServiceMock.create(); const getUpgradeableConfig = sandbox.stub(getUpgradeableConfigNS, 'getUpgradeableConfig'); const savedObjectsClient = { create: sinon.stub().callsFake(async (type, attributes, options = {}) => ({ @@ -50,7 +49,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { savedObjectsClient, version, buildNum, - logWithMetadata, + log: logger.get(), ...options, }); @@ -62,7 +61,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { return { buildNum, - logWithMetadata, + logger, run, version, savedObjectsClient, @@ -126,7 +125,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { }); it('should log a message for upgrades', async () => { - const { getUpgradeableConfig, logWithMetadata, run } = setup(); + const { getUpgradeableConfig, logger, run } = setup(); getUpgradeableConfig.resolves({ id: prevVersion, @@ -136,20 +135,21 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { }); await run(); - sinon.assert.calledOnce(logWithMetadata); - sinon.assert.calledWithExactly( - logWithMetadata, - ['plugin', 'elasticsearch'], - sinon.match('Upgrade'), - sinon.match({ - prevVersion, - newVersion: version, - }) - ); + expect(loggingServiceMock.collect(logger).debug).toMatchInlineSnapshot(` + Array [ + Array [ + "Upgrade config from 4.0.0 to 4.0.1", + Object { + "newVersion": "4.0.1", + "prevVersion": "4.0.0", + }, + ], + ] + `); }); it('does not log when upgrade fails', async () => { - const { getUpgradeableConfig, logWithMetadata, run, savedObjectsClient } = setup(); + const { getUpgradeableConfig, logger, run, savedObjectsClient } = setup(); getUpgradeableConfig.resolves({ id: prevVersion, @@ -166,10 +166,10 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { await run(); throw new Error('Expected run() to throw an error'); } catch (error) { - expect(error.message).to.be('foo'); + expect(error.message).toBe('foo'); } - sinon.assert.notCalled(logWithMetadata); + expect(loggingServiceMock.collect(logger).debug).toHaveLength(0); }); }); @@ -198,7 +198,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { }); const result = await run({ onWriteError: () => 123 }); - expect(result).to.be(123); + expect(result).toBe(123); }); it('rejects with the error from onWriteError() if it rejects', async () => { @@ -214,7 +214,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { }); throw new Error('expected run() to reject'); } catch (error) { - expect(error.message).to.be('foo bar'); + expect(error.message).toBe('foo bar'); } }); @@ -233,7 +233,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { }); throw new Error('expected run() to reject'); } catch (error) { - expect(error.message).to.be('foo bar'); + expect(error.message).toBe('foo bar'); } }); @@ -250,7 +250,7 @@ describe('uiSettings/createOrUpgradeSavedConfig', function() { }); throw new Error('expected run() to reject'); } catch (error) { - expect(error.message).to.be('foo'); + expect(error.message).toBe('foo'); } }); }); diff --git a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts similarity index 82% rename from src/legacy/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts rename to src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts index 0dc3d5f50e97e..1655297adb6c9 100644 --- a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/create_or_upgrade_saved_config.ts @@ -18,8 +18,9 @@ */ import { defaults } from 'lodash'; -import { SavedObjectsClientContract, SavedObjectAttribute } from 'src/core/server'; -import { Legacy } from 'kibana'; + +import { SavedObjectsClientContract, SavedObjectAttribute } from '../../saved_objects/types'; +import { Logger } from '../../logging'; import { getUpgradeableConfig } from './get_upgradeable_config'; @@ -27,7 +28,7 @@ interface Options { savedObjectsClient: SavedObjectsClientContract; version: string; buildNum: number; - logWithMetadata: Legacy.Server['logWithMetadata']; + log: Logger; onWriteError?: ( error: Error, attributes: Record @@ -36,7 +37,7 @@ interface Options { export async function createOrUpgradeSavedConfig( options: Options ): Promise | undefined> { - const { savedObjectsClient, version, buildNum, logWithMetadata, onWriteError } = options; + const { savedObjectsClient, version, buildNum, log, onWriteError } = options; // try to find an older config we can upgrade const upgradeableConfig = await getUpgradeableConfig({ @@ -59,13 +60,9 @@ export async function createOrUpgradeSavedConfig { let savedObjectsClient: SavedObjectsClientContract; let kbnServer: KbnServer; @@ -88,7 +89,7 @@ describe('createOrUpgradeSavedConfig()', () => { savedObjectsClient, version: '5.4.0', buildNum: 54099, - logWithMetadata: sinon.stub(), + log: logger, }); const config540 = await savedObjectsClient.get('config', '5.4.0'); @@ -114,7 +115,7 @@ describe('createOrUpgradeSavedConfig()', () => { savedObjectsClient, version: '5.4.1', buildNum: 54199, - logWithMetadata: sinon.stub(), + log: logger, }); const config541 = await savedObjectsClient.get('config', '5.4.1'); @@ -140,7 +141,7 @@ describe('createOrUpgradeSavedConfig()', () => { savedObjectsClient, version: '7.0.0-rc1', buildNum: 70010, - logWithMetadata: sinon.stub(), + log: logger, }); const config700rc1 = await savedObjectsClient.get('config', '7.0.0-rc1'); @@ -167,7 +168,7 @@ describe('createOrUpgradeSavedConfig()', () => { savedObjectsClient, version: '7.0.0', buildNum: 70099, - logWithMetadata: sinon.stub(), + log: logger, }); const config700 = await savedObjectsClient.get('config', '7.0.0'); @@ -195,7 +196,7 @@ describe('createOrUpgradeSavedConfig()', () => { savedObjectsClient, version: '6.2.3-rc1', buildNum: 62310, - logWithMetadata: sinon.stub(), + log: logger, }); const config623rc1 = await savedObjectsClient.get('config', '6.2.3-rc1'); diff --git a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.test.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.test.ts similarity index 86% rename from src/legacy/ui/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.test.ts rename to src/core/server/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.test.ts index 6bb2cb3b87850..073a6961fdec4 100644 --- a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.test.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.test.ts @@ -20,8 +20,6 @@ import expect from '@kbn/expect'; import { isConfigVersionUpgradeable } from './is_config_version_upgradeable'; -// @ts-ignore -import { pkg } from '../../../utils'; describe('savedObjects/health_check/isConfigVersionUpgradeable', function() { function isUpgradeableTest(savedVersion: string, kibanaVersion: string, expected: boolean) { @@ -30,10 +28,10 @@ describe('savedObjects/health_check/isConfigVersionUpgradeable', function() { }); } - isUpgradeableTest('1.0.0-beta1', pkg.version, false); - isUpgradeableTest('1.0.0-beta256', pkg.version, false); + isUpgradeableTest('1.0.0-beta1', '7.4.0', false); + isUpgradeableTest('1.0.0-beta256', '7.4.0', false); isUpgradeableTest('10.100.1000-beta256', '10.100.1000-beta257', false); - isUpgradeableTest(pkg.version, pkg.version, false); + isUpgradeableTest('7.4.0', '7.4.0', false); isUpgradeableTest('4.0.0-RC1', '4.0.0-RC2', true); isUpgradeableTest('10.100.1000-rc256', '10.100.1000-RC257', true); isUpgradeableTest('4.0.0-rc2', '4.0.0-rc1', false); @@ -48,6 +46,6 @@ describe('savedObjects/health_check/isConfigVersionUpgradeable', function() { isUpgradeableTest('4.1.0-rc1-SNAPSHOT', '4.1.0-rc1', false); isUpgradeableTest('5.0.0-alpha11', '5.0.0', false); isUpgradeableTest('50.0.10-rc150-SNAPSHOT', '50.0.9', false); - isUpgradeableTest(undefined as any, pkg.version, false); - isUpgradeableTest('@@version', pkg.version, false); + isUpgradeableTest(undefined as any, '7.4.0', false); + isUpgradeableTest('@@version', '7.4.0', false); }); diff --git a/src/legacy/ui/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.ts similarity index 100% rename from src/legacy/ui/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.ts rename to src/core/server/ui_settings/create_or_upgrade_saved_config/is_config_version_upgradeable.ts diff --git a/src/core/server/ui_settings/index.ts b/src/core/server/ui_settings/index.ts new file mode 100644 index 0000000000000..edd0bfc4f3a89 --- /dev/null +++ b/src/core/server/ui_settings/index.ts @@ -0,0 +1,32 @@ +/* + * 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 { + IUiSettingsClient, + UiSettingsClient, + UiSettingsServiceOptions, +} from './ui_settings_client'; + +export { config } from './ui_settings_config'; +export { + UiSettingsParams, + UiSettingsService, + InternalUiSettingsServiceSetup, + UiSettingsType, +} from './ui_settings_service'; diff --git a/src/legacy/ui/ui_settings/ui_settings_service.test.ts b/src/core/server/ui_settings/ui_settings_client.test.ts similarity index 94% rename from src/legacy/ui/ui_settings/ui_settings_service.test.ts rename to src/core/server/ui_settings/ui_settings_client.test.ts index f37076b27ad6f..59c13fbebee70 100644 --- a/src/legacy/ui/ui_settings/ui_settings_service.test.ts +++ b/src/core/server/ui_settings/ui_settings_client.test.ts @@ -21,17 +21,20 @@ import expect from '@kbn/expect'; import Chance from 'chance'; import sinon from 'sinon'; -import { UiSettingsService } from './ui_settings_service'; +import { loggingServiceMock } from '../logging/logging_service.mock'; + +import { UiSettingsClient } from './ui_settings_client'; import * as createOrUpgradeSavedConfigNS from './create_or_upgrade_saved_config/create_or_upgrade_saved_config'; import { createObjectsClientStub, savedObjectsClientErrors } from './create_objects_client_stub'; +const logger = loggingServiceMock.create().get(); + const TYPE = 'config'; const ID = 'kibana-version'; const BUILD_NUM = 1234; const chance = new Chance(); interface SetupOptions { - getDefaults?: () => Record; defaults?: Record; esDocSource?: Record; overrides?: Record; @@ -41,17 +44,18 @@ describe('ui settings', () => { const sandbox = sinon.createSandbox(); function setup(options: SetupOptions = {}) { - const { getDefaults, defaults = {}, overrides = {}, esDocSource = {} } = options; + const { defaults = {}, overrides = {}, esDocSource = {} } = options; const savedObjectsClient = createObjectsClientStub(esDocSource); - const uiSettings = new UiSettingsService({ + const uiSettings = new UiSettingsClient({ type: TYPE, id: ID, buildNum: BUILD_NUM, - getDefaults: getDefaults || (() => defaults), + defaults, savedObjectsClient, overrides, + log: logger, }); const createOrUpgradeSavedConfig = sandbox.stub( @@ -239,25 +243,10 @@ describe('ui settings', () => { }); describe('#getDefaults()', () => { - it('returns a promise for the defaults', async () => { - const { uiSettings } = setup(); - const promise = uiSettings.getDefaults(); - expect(promise).to.be.a(Promise); - expect(await promise).to.eql({}); - }); - }); - - describe('getDefaults() argument', () => { - it('casts sync `getDefaults()` to promise', () => { - const getDefaults = () => ({ key: { value: chance.word() } }); - const { uiSettings } = setup({ getDefaults }); - expect(uiSettings.getDefaults()).to.be.a(Promise); - }); - - it('returns the defaults returned by getDefaults() argument', async () => { + it('returns the defaults passed to the constructor', () => { const value = chance.word(); const { uiSettings } = setup({ defaults: { key: { value } } }); - expect(await uiSettings.getDefaults()).to.eql({ + expect(uiSettings.getDefaults()).to.eql({ key: { value }, }); }); diff --git a/src/legacy/ui/ui_settings/ui_settings_service.ts b/src/core/server/ui_settings/ui_settings_client.ts similarity index 63% rename from src/legacy/ui/ui_settings/ui_settings_service.ts rename to src/core/server/ui_settings/ui_settings_client.ts index 57312140b16b3..c495d1b4c4567 100644 --- a/src/legacy/ui/ui_settings/ui_settings_service.ts +++ b/src/core/server/ui_settings/ui_settings_client.ts @@ -16,12 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import { Legacy } from 'kibana'; import { defaultsDeep } from 'lodash'; import Boom from 'boom'; -import { SavedObjectsClientContract, SavedObjectAttribute } from 'src/core/server'; +import { SavedObjectsClientContract, SavedObjectAttribute } from '../saved_objects/types'; +import { Logger } from '../logging'; import { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config'; +import { UiSettingsParams } from './ui_settings_service'; export interface UiSettingsServiceOptions { type: string; @@ -29,8 +30,8 @@ export interface UiSettingsServiceOptions { buildNum: number; savedObjectsClient: SavedObjectsClientContract; overrides?: Record; - getDefaults?: () => Record; - logWithMetadata?: Legacy.Server['logWithMetadata']; + defaults?: Record; + log: Logger; } interface ReadOptions { @@ -38,82 +39,87 @@ interface ReadOptions { autoCreateOrUpgradeIfMissing?: boolean; } -interface UserProvidedValue { - userValue?: SavedObjectAttribute; +interface UserProvidedValue { + userValue?: T; isOverridden?: boolean; } type UiSettingsRawValue = UiSettingsParams & UserProvidedValue; -type UserProvided = Record; +type UserProvided = Record>; type UiSettingsRaw = Record; -type UiSettingsType = 'json' | 'markdown' | 'number' | 'select' | 'boolean' | 'string'; - -interface UiSettingsParams { - name: string; - value: SavedObjectAttribute; - description: string; - category: string[]; - options?: string[]; - optionLabels?: Record; - requiresPageReload?: boolean; - readonly?: boolean; - type?: UiSettingsType; -} - +/** + * Service that provides access to the UiSettings stored in elasticsearch. + * + * @public + */ export interface IUiSettingsClient { - getDefaults: () => Promise>; + /** + * Returns uiSettings default values {@link UiSettingsParams} + */ + getDefaults: () => Record; + /** + * Retrieves uiSettings values set by the user with fallbacks to default values if not specified. + */ get: (key: string) => Promise; + /** + * Retrieves a set of all uiSettings values set by the user with fallbacks to default values if not specified. + */ getAll: () => Promise>; - getUserProvided: () => Promise; + /** + * Retrieves a set of all uiSettings values set by the user. + */ + getUserProvided: () => Promise< + Record + >; + /** + * Writes multiple uiSettings values and marks them as set by the user. + */ setMany: (changes: Record) => Promise; + /** + * Writes uiSettings value and marks it as set by the user. + */ set: (key: string, value: T) => Promise; + /** + * Removes uiSettings value by key. + */ remove: (key: string) => Promise; + /** + * Removes multiple uiSettings values by keys. + */ removeMany: (keys: string[]) => Promise; + /** + * Shows whether the uiSettings value set by the user. + */ isOverridden: (key: string) => boolean; } -/** - * Service that provides access to the UiSettings stored in elasticsearch. - * @class UiSettingsService - */ -export class UiSettingsService implements IUiSettingsClient { - private readonly _type: UiSettingsServiceOptions['type']; - private readonly _id: UiSettingsServiceOptions['id']; - private readonly _buildNum: UiSettingsServiceOptions['buildNum']; - private readonly _savedObjectsClient: UiSettingsServiceOptions['savedObjectsClient']; - private readonly _overrides: NonNullable; - private readonly _getDefaults: NonNullable; - private readonly _logWithMetadata: NonNullable; + +export class UiSettingsClient implements IUiSettingsClient { + private readonly type: UiSettingsServiceOptions['type']; + private readonly id: UiSettingsServiceOptions['id']; + private readonly buildNum: UiSettingsServiceOptions['buildNum']; + private readonly savedObjectsClient: UiSettingsServiceOptions['savedObjectsClient']; + private readonly overrides: NonNullable; + private readonly defaults: NonNullable; + private readonly log: Logger; constructor(options: UiSettingsServiceOptions) { - const { - type, - id, - buildNum, - savedObjectsClient, - // we use a function for getDefaults() so that defaults can be different in - // different scenarios, and so they can change over time - getDefaults = () => ({}), - // function that accepts log messages in the same format as server.logWithMetadata - logWithMetadata = () => {}, - overrides = {}, - } = options; - - this._type = type; - this._id = id; - this._buildNum = buildNum; - this._savedObjectsClient = savedObjectsClient; - this._getDefaults = getDefaults; - this._overrides = overrides; - this._logWithMetadata = logWithMetadata; + const { type, id, buildNum, savedObjectsClient, log, defaults = {}, overrides = {} } = options; + + this.type = type; + this.id = id; + this.buildNum = buildNum; + this.savedObjectsClient = savedObjectsClient; + this.defaults = defaults; + this.overrides = overrides; + this.log = log; } - async getDefaults() { - return await this._getDefaults(); + getDefaults() { + return this.defaults; } - // returns a Promise for the value of the requested setting async get(key: string): Promise { const all = await this.getAll(); return all[key]; @@ -135,14 +141,16 @@ export class UiSettingsService implements IUiSettingsClient { // NOTE: should be a private method async getRaw(): Promise { const userProvided = await this.getUserProvided(); - return defaultsDeep(userProvided, await this.getDefaults()); + return defaultsDeep(userProvided, this.defaults); } - async getUserProvided(options: ReadOptions = {}): Promise { + async getUserProvided( + options: ReadOptions = {} + ): Promise> { const userProvided: UserProvided = {}; // write the userValue for each key stored in the saved object that is not overridden - for (const [key, userValue] of Object.entries(await this._read(options))) { + for (const [key, userValue] of Object.entries(await this.read(options))) { if (userValue !== null && !this.isOverridden(key)) { userProvided[key] = { userValue, @@ -152,7 +160,7 @@ export class UiSettingsService implements IUiSettingsClient { // write all overridden keys, dropping the userValue is override is null and // adding keys for overrides that are not in saved object - for (const [key, userValue] of Object.entries(this._overrides)) { + for (const [key, userValue] of Object.entries(this.overrides)) { userProvided[key] = userValue === null ? { isOverridden: true } : { isOverridden: true, userValue }; } @@ -161,7 +169,7 @@ export class UiSettingsService implements IUiSettingsClient { } async setMany(changes: Record) { - await this._write({ changes }); + await this.write({ changes }); } async set(key: string, value: T) { @@ -181,7 +189,7 @@ export class UiSettingsService implements IUiSettingsClient { } isOverridden(key: string) { - return this._overrides.hasOwnProperty(key); + return this.overrides.hasOwnProperty(key); } // NOTE: should be private method @@ -191,7 +199,7 @@ export class UiSettingsService implements IUiSettingsClient { } } - private async _write({ + private async write({ changes, autoCreateOrUpgradeIfMissing = true, }: { @@ -203,28 +211,28 @@ export class UiSettingsService implements IUiSettingsClient { } try { - await this._savedObjectsClient.update(this._type, this._id, changes); + await this.savedObjectsClient.update(this.type, this.id, changes); } catch (error) { - const { isNotFoundError } = this._savedObjectsClient.errors; + const { isNotFoundError } = this.savedObjectsClient.errors; if (!isNotFoundError(error) || !autoCreateOrUpgradeIfMissing) { throw error; } await createOrUpgradeSavedConfig({ - savedObjectsClient: this._savedObjectsClient, - version: this._id, - buildNum: this._buildNum, - logWithMetadata: this._logWithMetadata, + savedObjectsClient: this.savedObjectsClient, + version: this.id, + buildNum: this.buildNum, + log: this.log, }); - await this._write({ + await this.write({ changes, autoCreateOrUpgradeIfMissing: false, }); } } - private async _read({ + private async read({ ignore401Errors = false, autoCreateOrUpgradeIfMissing = true, }: ReadOptions = {}): Promise> { @@ -233,18 +241,18 @@ export class UiSettingsService implements IUiSettingsClient { isNotFoundError, isForbiddenError, isNotAuthorizedError, - } = this._savedObjectsClient.errors; + } = this.savedObjectsClient.errors; try { - const resp = await this._savedObjectsClient.get(this._type, this._id); + const resp = await this.savedObjectsClient.get(this.type, this.id); return resp.attributes; } catch (error) { if (isNotFoundError(error) && autoCreateOrUpgradeIfMissing) { const failedUpgradeAttributes = await createOrUpgradeSavedConfig({ - savedObjectsClient: this._savedObjectsClient, - version: this._id, - buildNum: this._buildNum, - logWithMetadata: this._logWithMetadata, + savedObjectsClient: this.savedObjectsClient, + version: this.id, + buildNum: this.buildNum, + log: this.log, onWriteError(writeError, attributes) { if (isConflictError(writeError)) { // trigger `!failedUpgradeAttributes` check below, since another @@ -262,7 +270,7 @@ export class UiSettingsService implements IUiSettingsClient { }); if (!failedUpgradeAttributes) { - return await this._read({ + return await this.read({ ignore401Errors, autoCreateOrUpgradeIfMissing: false, }); @@ -284,7 +292,7 @@ export class UiSettingsService implements IUiSettingsClient { isForbiddenError, isEsUnavailableError, isNotAuthorizedError, - } = this._savedObjectsClient.errors; + } = this.savedObjectsClient.errors; return ( isForbiddenError(error) || diff --git a/src/core/server/ui_settings/ui_settings_config.ts b/src/core/server/ui_settings/ui_settings_config.ts new file mode 100644 index 0000000000000..702286f953ef1 --- /dev/null +++ b/src/core/server/ui_settings/ui_settings_config.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export type UiSettingsConfigType = TypeOf; + +export const config = { + path: 'uiSettings', + schema: schema.object({ + overrides: schema.object({}, { allowUnknowns: true }), + // Deprecation is implemented in LP. + // We define schema here not to fail on the validation step. + enabled: schema.maybe(schema.boolean()), + }), +}; diff --git a/src/legacy/ui/ui_settings/ui_settings_service.mock.ts b/src/core/server/ui_settings/ui_settings_service.mock.ts similarity index 71% rename from src/legacy/ui/ui_settings/ui_settings_service.mock.ts rename to src/core/server/ui_settings/ui_settings_service.mock.ts index 7c1a17ebd447c..2127faf0d2029 100644 --- a/src/legacy/ui/ui_settings/ui_settings_service.mock.ts +++ b/src/core/server/ui_settings/ui_settings_service.mock.ts @@ -17,9 +17,10 @@ * under the License. */ -import { IUiSettingsClient } from './ui_settings_service'; +import { IUiSettingsClient } from './ui_settings_client'; +import { InternalUiSettingsServiceSetup } from './ui_settings_service'; -const createServiceMock = () => { +const createClientMock = () => { const mocked: jest.Mocked = { getDefaults: jest.fn(), get: jest.fn(), @@ -35,6 +36,18 @@ const createServiceMock = () => { return mocked; }; +const createSetupMock = () => { + const mocked: jest.Mocked = { + setDefaults: jest.fn(), + asScopedToClient: jest.fn(), + }; + + mocked.asScopedToClient.mockReturnValue(createClientMock()); + + return mocked; +}; + export const uiSettingsServiceMock = { - create: createServiceMock, + createSetup: createSetupMock, + createClient: createClientMock, }; diff --git a/src/core/server/ui_settings/ui_settings_service.test.mock.ts b/src/core/server/ui_settings/ui_settings_service.test.mock.ts new file mode 100644 index 0000000000000..586ad3049ed6a --- /dev/null +++ b/src/core/server/ui_settings/ui_settings_service.test.mock.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const MockUiSettingsClientConstructor = jest.fn(); + +jest.doMock('./ui_settings_client', () => ({ + UiSettingsClient: MockUiSettingsClientConstructor, +})); diff --git a/src/core/server/ui_settings/ui_settings_service.test.ts b/src/core/server/ui_settings/ui_settings_service.test.ts new file mode 100644 index 0000000000000..832d61bdb4137 --- /dev/null +++ b/src/core/server/ui_settings/ui_settings_service.test.ts @@ -0,0 +1,109 @@ +/* + * 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 { BehaviorSubject } from 'rxjs'; + +import { MockUiSettingsClientConstructor } from './ui_settings_service.test.mock'; + +import { UiSettingsService } from './ui_settings_service'; +import { httpServiceMock } from '../http/http_service.mock'; +import { loggingServiceMock } from '../logging/logging_service.mock'; +import { SavedObjectsClientMock } from '../mocks'; +import { mockCoreContext } from '../core_context.mock'; + +const overrides = { + overrideBaz: 'baz', +}; + +const defaults = { + foo: { + name: 'foo', + value: 'bar', + category: [], + description: '', + }, +}; + +const coreContext = mockCoreContext.create(); +coreContext.configService.atPath.mockReturnValue(new BehaviorSubject({ overrides })); +const httpSetup = httpServiceMock.createSetupContract(); +const setupDeps = { http: httpSetup }; +const savedObjectsClient = SavedObjectsClientMock.create(); + +afterEach(() => { + MockUiSettingsClientConstructor.mockClear(); +}); + +describe('uiSettings', () => { + describe('#setup', () => { + describe('#asScopedToClient', () => { + it('passes overrides to UiSettingsClient', async () => { + const service = new UiSettingsService(coreContext); + const setup = await service.setup(setupDeps); + setup.asScopedToClient(savedObjectsClient); + expect(MockUiSettingsClientConstructor).toBeCalledTimes(1); + expect(MockUiSettingsClientConstructor.mock.calls[0][0].overrides).toBe(overrides); + expect(MockUiSettingsClientConstructor.mock.calls[0][0].overrides).toEqual(overrides); + }); + + it('passes overrides with deprecated "server.defaultRoute"', async () => { + const service = new UiSettingsService(coreContext); + const httpSetupWithDefaultRoute = httpServiceMock.createSetupContract(); + httpSetupWithDefaultRoute.config.defaultRoute = '/defaultRoute'; + const setup = await service.setup({ http: httpSetupWithDefaultRoute }); + setup.asScopedToClient(savedObjectsClient); + + expect(MockUiSettingsClientConstructor.mock.calls[0][0].overrides).toEqual({ + ...overrides, + defaultRoute: '/defaultRoute', + }); + + expect(loggingServiceMock.collect(coreContext.logger).warn).toMatchInlineSnapshot(` + Array [ + Array [ + "Config key \\"server.defaultRoute\\" is deprecated. It has been replaced with \\"uiSettings.overrides.defaultRoute\\"", + ], + ] + `); + }); + + it('passes a copy of set defaults to UiSettingsClient', async () => { + const service = new UiSettingsService(coreContext); + const setup = await service.setup(setupDeps); + + setup.setDefaults(defaults); + setup.asScopedToClient(savedObjectsClient); + expect(MockUiSettingsClientConstructor).toBeCalledTimes(1); + + expect(MockUiSettingsClientConstructor.mock.calls[0][0].defaults).toEqual(defaults); + expect(MockUiSettingsClientConstructor.mock.calls[0][0].defaults).not.toBe(defaults); + }); + }); + + describe('#setDefaults', () => { + it('throws if set defaults for the same key twice', async () => { + const service = new UiSettingsService(coreContext); + const setup = await service.setup(setupDeps); + setup.setDefaults(defaults); + expect(() => setup.setDefaults(defaults)).toThrowErrorMatchingInlineSnapshot( + `"uiSettings defaults for key [foo] has been already set"` + ); + }); + }); + }); +}); diff --git a/src/core/server/ui_settings/ui_settings_service.ts b/src/core/server/ui_settings/ui_settings_service.ts new file mode 100644 index 0000000000000..746fa514c5d4b --- /dev/null +++ b/src/core/server/ui_settings/ui_settings_service.ts @@ -0,0 +1,140 @@ +/* + * 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 { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { CoreService } from '../../types'; +import { CoreContext } from '../core_context'; +import { Logger } from '../logging'; + +import { SavedObjectsClientContract, SavedObjectAttribute } from '../saved_objects/types'; +import { InternalHttpServiceSetup } from '../http'; +import { UiSettingsConfigType } from './ui_settings_config'; +import { IUiSettingsClient, UiSettingsClient } from './ui_settings_client'; +import { mapToObject } from '../../utils/'; + +interface SetupDeps { + http: InternalHttpServiceSetup; +} + +/** + * UI element type to represent the settings. + * @public + * */ +export type UiSettingsType = 'json' | 'markdown' | 'number' | 'select' | 'boolean' | 'string'; + +/** + * UiSettings parameters defined by the plugins. + * @public + * */ +export interface UiSettingsParams { + /** title in the UI */ + name: string; + /** default value to fall back to if a user doesn't provide any */ + value: SavedObjectAttribute; + /** description provided to a user in UI */ + description: string; + /** used to group the configured setting in the UI */ + category: string[]; + /** a range of valid values */ + options?: string[]; + /** text labels for 'select' type UI element */ + optionLabels?: Record; + /** a flag indicating whether new value applying requires page reloading */ + requiresPageReload?: boolean; + /** a flag indicating that value cannot be changed */ + readonly?: boolean; + /** defines a type of UI element {@link UiSettingsType} */ + type?: UiSettingsType; +} + +/** @internal */ +export interface InternalUiSettingsServiceSetup { + /** + * Sets the parameters with default values for the uiSettings. + * @param values + */ + setDefaults(values: Record): void; + /** + * Creates uiSettings client with provided *scoped* saved objects client {@link IUiSettingsClient} + * @param values + */ + asScopedToClient(savedObjectsClient: SavedObjectsClientContract): IUiSettingsClient; +} + +/** @internal */ +export class UiSettingsService implements CoreService { + private readonly log: Logger; + private readonly config$: Observable; + private readonly uiSettingsDefaults = new Map(); + + constructor(private readonly coreContext: CoreContext) { + this.log = coreContext.logger.get('ui-settings-service'); + this.config$ = coreContext.configService.atPath('uiSettings'); + } + + public async setup(deps: SetupDeps): Promise { + this.log.debug('Setting up ui settings service'); + const overrides = await this.getOverrides(deps); + const { version, buildNum } = this.coreContext.env.packageInfo; + + return { + setDefaults: this.setDefaults.bind(this), + asScopedToClient: (savedObjectsClient: SavedObjectsClientContract) => { + return new UiSettingsClient({ + type: 'config', + id: version, + buildNum, + savedObjectsClient, + defaults: mapToObject(this.uiSettingsDefaults), + overrides, + log: this.log, + }); + }, + }; + } + + public async start() {} + + public async stop() {} + + private setDefaults(values: Record = {}) { + Object.entries(values).forEach(([key, value]) => { + if (this.uiSettingsDefaults.has(key)) { + throw new Error(`uiSettings defaults for key [${key}] has been already set`); + } + this.uiSettingsDefaults.set(key, value); + }); + } + + private async getOverrides(deps: SetupDeps) { + const config = await this.config$.pipe(first()).toPromise(); + const overrides: Record = config.overrides; + // manually implemented deprecation until New platform Config service + // supports them https://github.com/elastic/kibana/issues/40255 + if (typeof deps.http.config.defaultRoute !== 'undefined') { + overrides.defaultRoute = deps.http.config.defaultRoute; + this.log.warn( + 'Config key "server.defaultRoute" is deprecated. It has been replaced with "uiSettings.overrides.defaultRoute"' + ); + } + + return overrides; + } +} diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index 1d74fc63bcc0f..3f9b897730f51 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -108,9 +108,7 @@ export default () => Joi.object({ ssl: HANDLED_IN_NEW_PLATFORM, }).default(), - uiSettings: Joi.object().keys({ - overrides: Joi.object().unknown(true).default() - }).default(), + uiSettings: HANDLED_IN_NEW_PLATFORM, logging: Joi.object().keys({ silent: Joi.boolean().default(false), diff --git a/src/legacy/server/config/transform_deprecations.js b/src/legacy/server/config/transform_deprecations.js index 8be880074f9fd..7cac17a88fe64 100644 --- a/src/legacy/server/config/transform_deprecations.js +++ b/src/legacy/server/config/transform_deprecations.js @@ -95,7 +95,6 @@ const cspRules = (settings, log) => { const deprecations = [ //server - rename('server.defaultRoute', 'uiSettings.overrides.defaultRoute'), unused('server.xsrf.token'), unused('uiSettings.enabled'), rename('optimize.lazy', 'optimize.watch'), diff --git a/src/legacy/server/config/transform_deprecations.test.js b/src/legacy/server/config/transform_deprecations.test.js index 4094443ac0006..f8cf38efc8bd8 100644 --- a/src/legacy/server/config/transform_deprecations.test.js +++ b/src/legacy/server/config/transform_deprecations.test.js @@ -62,24 +62,6 @@ describe('server/config', function () { }); }); - describe('server.defaultRoute', () => { - it('renames to uiSettings.overrides.defaultRoute when specified', () => { - const settings = { - server: { - defaultRoute: '/app/foo', - }, - }; - - expect(transformDeprecations(settings)).toEqual({ - uiSettings: { - overrides: { - defaultRoute: '/app/foo' - } - } - }); - }); - }); - describe('csp.rules', () => { describe('with nonce source', () => { it('logs a warning', () => { diff --git a/src/legacy/server/http/integration_tests/default_route_provider.test.ts b/src/legacy/server/http/integration_tests/default_route_provider.test.ts index fe8c464965132..d74be851a3535 100644 --- a/src/legacy/server/http/integration_tests/default_route_provider.test.ts +++ b/src/legacy/server/http/integration_tests/default_route_provider.test.ts @@ -45,11 +45,11 @@ describe('default route provider', () => { throw Error(`unsupported ui setting: ${key}`); }, getDefaults: () => { - return Promise.resolve({ + return { defaultRoute: { value: '/app/kibana', }, - }); + }; }, }; }); diff --git a/src/legacy/server/http/setup_default_route_provider.ts b/src/legacy/server/http/setup_default_route_provider.ts index 07ff61015a187..f627cf30a3cff 100644 --- a/src/legacy/server/http/setup_default_route_provider.ts +++ b/src/legacy/server/http/setup_default_route_provider.ts @@ -40,7 +40,7 @@ export function setupDefaultRouteProvider(server: Legacy.Server) { `Ignoring configured default route of '${defaultRoute}', as it is malformed.` ); - const fallbackRoute = (await uiSettings.getDefaults()).defaultRoute.value; + const fallbackRoute = uiSettings.getDefaults().defaultRoute.value; const qualifiedFallbackRoute = `${request.getBasePath()}${fallbackRoute}`; return qualifiedFallbackRoute; diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index f5771c6b86d9a..e7f2f4c85435f 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -28,6 +28,7 @@ import { LoggerFactory, SavedObjectsClientContract, SavedObjectsLegacyService, + IUiSettingsClient, PackageInfo, } from '../../core/server'; @@ -41,7 +42,6 @@ import { CallClusterWithRequest, ElasticsearchPlugin } from '../core_plugins/ela import { CapabilitiesModifier } from './capabilities'; import { IndexPatternsServiceFactory } from './index_patterns'; import { Capabilities } from '../../core/public'; -import { IUiSettingsClient } from '../../legacy/ui/ui_settings/ui_settings_service'; import { UiSettingsServiceFactoryOptions } from '../../legacy/ui/ui_settings/ui_settings_service_factory'; export interface KibanaConfig { @@ -104,6 +104,13 @@ type KbnMixinFunc = (kbnServer: KbnServer, server: Server, config: any) => Promi // eslint-disable-next-line import/no-default-export export default class KbnServer { public readonly newPlatform: { + __internals: { + hapiServer: LegacyServiceSetupDeps['core']['http']['server']; + uiPlugins: LegacyServiceSetupDeps['core']['plugins']['uiPlugins']; + elasticsearch: LegacyServiceSetupDeps['core']['elasticsearch']; + uiSettings: LegacyServiceSetupDeps['core']['uiSettings']; + kibanaMigrator: LegacyServiceStartDeps['core']['savedObjects']['migrator']; + }; env: { mode: Readonly; packageInfo: Readonly; diff --git a/src/legacy/ui/field_formats/mixin/field_formats_mixin.ts b/src/legacy/ui/field_formats/mixin/field_formats_mixin.ts index 3720677ec17d2..f9a5c25878322 100644 --- a/src/legacy/ui/field_formats/mixin/field_formats_mixin.ts +++ b/src/legacy/ui/field_formats/mixin/field_formats_mixin.ts @@ -28,7 +28,7 @@ export function fieldFormatsMixin(kbnServer: any, server: Legacy.Server) { // for use outside of the request context, for special cases server.decorate('server', 'fieldFormatServiceFactory', async function(uiSettings) { const uiConfigs = await uiSettings.getAll(); - const uiSettingDefaults = await uiSettings.getDefaults(); + const uiSettingDefaults = uiSettings.getDefaults(); Object.keys(uiSettingDefaults).forEach(key => { if (has(uiConfigs, key) && uiSettingDefaults[key].type === 'json') { uiConfigs[key] = JSON.parse(uiConfigs[key]); diff --git a/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts b/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts index f43c6436d1c33..9f553b37935d7 100644 --- a/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts +++ b/src/legacy/ui/ui_settings/integration_tests/ui_settings_mixin.test.ts @@ -20,9 +20,7 @@ import sinon from 'sinon'; import expect from '@kbn/expect'; -// @ts-ignore -import { Config } from '../../../server/config'; - +import { SavedObjectsClientMock } from '../../../../core/server/mocks'; import * as uiSettingsServiceFactoryNS from '../ui_settings_service_factory'; import * as getUiSettingsServiceForRequestNS from '../ui_settings_service_for_request'; // @ts-ignore @@ -33,18 +31,16 @@ interface Decorators { request: { [name: string]: any }; } +const uiSettingDefaults = { + application: { + defaultProperty1: 'value1', + }, +}; + describe('uiSettingsMixin()', () => { const sandbox = sinon.createSandbox(); function setup() { - const config = Config.withDefaultSchema({ - uiSettings: { - overrides: { - foo: 'bar', - }, - }, - }); - // maps of decorations passed to `server.decorate()` const decorations: Decorators = { server: {}, @@ -55,7 +51,6 @@ describe('uiSettingsMixin()', () => { const server = { log: sinon.stub(), route: sinon.stub(), - config: () => config, addMemoizedFactoryToRequest(name: string, factory: (...args: any[]) => any) { this.decorate('request', name, function(this: typeof server) { return factory(this); @@ -73,12 +68,18 @@ describe('uiSettingsMixin()', () => { const kbnServer = { server, - config, - uiExports: { addConsumer: sinon.stub() }, + uiExports: { uiSettingDefaults }, ready: sinon.stub().returns(readyPromise), + newPlatform: { + __internals: { + uiSettings: { + setDefaults: sinon.stub(), + }, + }, + }, }; - uiSettingsMixin(kbnServer, server, config); + uiSettingsMixin(kbnServer, server); return { kbnServer, @@ -90,6 +91,15 @@ describe('uiSettingsMixin()', () => { afterEach(() => sandbox.restore()); + it('passes uiSettingsDefaults to the new platform', () => { + const { kbnServer } = setup(); + sinon.assert.calledOnce(kbnServer.newPlatform.__internals.uiSettings.setDefaults); + sinon.assert.calledWithExactly( + kbnServer.newPlatform.__internals.uiSettings.setDefaults, + uiSettingDefaults + ); + }); + describe('server.uiSettingsServiceFactory()', () => { it('decorates server with "uiSettingsServiceFactory"', () => { const { decorations } = setup(); @@ -116,18 +126,16 @@ describe('uiSettingsMixin()', () => { uiSettingsServiceFactoryNS, 'uiSettingsServiceFactory' ); + sinon.assert.notCalled(uiSettingsServiceFactoryStub); + + const savedObjectsClient = SavedObjectsClientMock.create(); decorations.server.uiSettingsServiceFactory({ - foo: 'bar', + savedObjectsClient, }); sinon.assert.calledOnce(uiSettingsServiceFactoryStub); sinon.assert.calledWithExactly(uiSettingsServiceFactoryStub, server as any, { - // @ts-ignore foo doesn't exist on Hapi.Server - foo: 'bar', - overrides: { - foo: 'bar', - }, - getDefaults: sinon.match.func, + savedObjectsClient, }); }); }); @@ -161,12 +169,7 @@ describe('uiSettingsMixin()', () => { sinon.assert.notCalled(getUiSettingsServiceForRequestStub); const request = {}; decorations.request.getUiSettingsService.call(request); - sinon.assert.calledWith(getUiSettingsServiceForRequestStub, server as any, request as any, { - overrides: { - foo: 'bar', - }, - getDefaults: sinon.match.func, - }); + sinon.assert.calledWith(getUiSettingsServiceForRequestStub, server as any, request as any); }); }); diff --git a/src/legacy/ui/ui_settings/routes/integration_tests/lib/servers.ts b/src/legacy/ui/ui_settings/routes/integration_tests/lib/servers.ts index b076a2a86e166..ae0ef1c91411e 100644 --- a/src/legacy/ui/ui_settings/routes/integration_tests/lib/servers.ts +++ b/src/legacy/ui/ui_settings/routes/integration_tests/lib/servers.ts @@ -18,12 +18,11 @@ */ import { UnwrapPromise } from '@kbn/utility-types'; -import { SavedObjectsClientContract } from 'src/core/server'; +import { SavedObjectsClientContract, IUiSettingsClient } from 'src/core/server'; import KbnServer from '../../../../../server/kbn_server'; import { createTestServers } from '../../../../../../test_utils/kbn_server'; import { CallCluster } from '../../../../../../legacy/core_plugins/elasticsearch'; -import { IUiSettingsClient } from '../../../ui_settings_service'; let kbnServer: KbnServer; let servers: ReturnType; diff --git a/src/legacy/ui/ui_settings/ui_settings_mixin.js b/src/legacy/ui/ui_settings/ui_settings_mixin.js index 3683ba5dd265e..8c7ef25c6f8d7 100644 --- a/src/legacy/ui/ui_settings/ui_settings_mixin.js +++ b/src/legacy/ui/ui_settings/ui_settings_mixin.js @@ -42,22 +42,15 @@ export function uiSettingsMixin(kbnServer, server) { acc[currentKey] = updatedDefaultSetting; return acc; }, {}); - const getDefaults = () => mergedUiSettingDefaults; - const overrides = kbnServer.config.get('uiSettings.overrides'); + + kbnServer.newPlatform.__internals.uiSettings.setDefaults(mergedUiSettingDefaults); server.decorate('server', 'uiSettingsServiceFactory', (options = {}) => { - return uiSettingsServiceFactory(server, { - getDefaults, - overrides, - ...options - }); + return uiSettingsServiceFactory(server, options); }); server.addMemoizedFactoryToRequest('getUiSettingsService', request => { - return getUiSettingsServiceForRequest(server, request, { - getDefaults, - overrides, - }); + return getUiSettingsServiceForRequest(server, request); }); server.decorate('server', 'uiSettings', () => { diff --git a/src/legacy/ui/ui_settings/ui_settings_service_factory.ts b/src/legacy/ui/ui_settings/ui_settings_service_factory.ts index 9e1384494161c..ab4eb75e4b703 100644 --- a/src/legacy/ui/ui_settings/ui_settings_service_factory.ts +++ b/src/legacy/ui/ui_settings/ui_settings_service_factory.ts @@ -17,18 +17,13 @@ * under the License. */ import { Legacy } from 'kibana'; -import { - IUiSettingsClient, - UiSettingsService, - UiSettingsServiceOptions, -} from './ui_settings_service'; +import { IUiSettingsClient, SavedObjectsClientContract } from 'src/core/server'; -export type UiSettingsServiceFactoryOptions = Pick< - UiSettingsServiceOptions, - 'savedObjectsClient' | 'getDefaults' | 'overrides' ->; +export interface UiSettingsServiceFactoryOptions { + savedObjectsClient: SavedObjectsClientContract; +} /** - * Create an instance of UiSettingsService that will use the + * Create an instance of UiSettingsClient that will use the * passed `savedObjectsClient` to communicate with elasticsearch * * @return {IUiSettingsClient} @@ -37,17 +32,5 @@ export function uiSettingsServiceFactory( server: Legacy.Server, options: UiSettingsServiceFactoryOptions ): IUiSettingsClient { - const config = server.config(); - - const { savedObjectsClient, getDefaults, overrides } = options; - - return new UiSettingsService({ - type: 'config', - id: config.get('pkg.version'), - buildNum: config.get('pkg.buildNum'), - savedObjectsClient, - getDefaults, - overrides, - logWithMetadata: server.logWithMetadata, - }); + return server.newPlatform.__internals.uiSettings.asScopedToClient(options.savedObjectsClient); } diff --git a/src/legacy/ui/ui_settings/ui_settings_service_for_request.ts b/src/legacy/ui/ui_settings/ui_settings_service_for_request.ts index e265ad5f1e115..057fc64c9ebd7 100644 --- a/src/legacy/ui/ui_settings/ui_settings_service_for_request.ts +++ b/src/legacy/ui/ui_settings/ui_settings_service_for_request.ts @@ -18,10 +18,9 @@ */ import { Legacy } from 'kibana'; +import { IUiSettingsClient } from 'src/core/server'; import { uiSettingsServiceFactory } from './ui_settings_service_factory'; -import { IUiSettingsClient, UiSettingsServiceOptions } from './ui_settings_service'; -type Options = Pick; /** * Get/create an instance of UiSettingsService bound to a specific request. * Each call is cached (keyed on the request object itself) and subsequent @@ -36,16 +35,8 @@ type Options = Pick; */ export function getUiSettingsServiceForRequest( server: Legacy.Server, - request: Legacy.Request, - options: Options + request: Legacy.Request ): IUiSettingsClient { - const { getDefaults, overrides } = options; - - const uiSettingsService = uiSettingsServiceFactory(server, { - getDefaults, - overrides, - savedObjectsClient: request.getSavedObjectsClient(), - }); - - return uiSettingsService; + const savedObjectsClient = request.getSavedObjectsClient(); + return uiSettingsServiceFactory(server, { savedObjectsClient }); } diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts index 6edac00b8a1ab..cfc33b4fad7ea 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts @@ -6,7 +6,7 @@ import { Legacy } from 'kibana'; import { setupRequest } from './setup_request'; -import { uiSettingsServiceMock } from '../../../../../../../src/legacy/ui/ui_settings/ui_settings_service.mock'; +import { uiSettingsServiceMock } from 'src/core/server/mocks'; function getMockRequest() { const callWithRequestSpy = jest.fn(); @@ -125,7 +125,7 @@ describe('setupRequest', () => { it('should set `ignore_throttled=true` if `includeFrozen=false`', async () => { const { mockRequest, callWithRequestSpy } = getMockRequest(); - const uiSettingsService = uiSettingsServiceMock.create(); + const uiSettingsService = uiSettingsServiceMock.createClient(); // mock includeFrozen to return false uiSettingsService.get.mockResolvedValue(false); mockRequest.getUiSettingsService = () => uiSettingsService; @@ -138,7 +138,7 @@ describe('setupRequest', () => { it('should set `ignore_throttled=false` if `includeFrozen=true`', async () => { const { mockRequest, callWithRequestSpy } = getMockRequest(); - const uiSettingsService = uiSettingsServiceMock.create(); + const uiSettingsService = uiSettingsServiceMock.createClient(); // mock includeFrozen to return true uiSettingsService.get.mockResolvedValue(true); mockRequest.getUiSettingsService = () => uiSettingsService; diff --git a/x-pack/legacy/plugins/ml/common/types/modules.ts b/x-pack/legacy/plugins/ml/common/types/modules.ts index cb012c3641f3b..18879304d7c7f 100644 --- a/x-pack/legacy/plugins/ml/common/types/modules.ts +++ b/x-pack/legacy/plugins/ml/common/types/modules.ts @@ -3,9 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { SavedObjectAttributes } from 'src/core/server/types'; import { Datafeed, Job } from '../../public/jobs/new_job_new/common/job_creator/configs'; -import { SavedObjectAttributes } from '../../../../../../target/types/core/server'; export interface ModuleJob { id: string; From 323d71ea44df0ba8dc9e7d367ff8f2d6407cfbd6 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 16 Oct 2019 13:32:54 +0100 Subject: [PATCH 25/48] [Task Manager] Fixes error when we claim new tasks beyond capacity (#48384) Fixes an issue where we would try and claim new tasks even when there are no available workers --- .../plugins/task_manager/task_manager.test.ts | 27 ++++++++- .../plugins/task_manager/task_manager.ts | 58 ++++++++++++++----- 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/x-pack/legacy/plugins/task_manager/task_manager.test.ts b/x-pack/legacy/plugins/task_manager/task_manager.test.ts index 25e68a91fe680..ce6bafa5ddd63 100644 --- a/x-pack/legacy/plugins/task_manager/task_manager.test.ts +++ b/x-pack/legacy/plugins/task_manager/task_manager.test.ts @@ -6,7 +6,7 @@ import _ from 'lodash'; import sinon from 'sinon'; -import { TaskManager } from './task_manager'; +import { TaskManager, claimAvailableTasks } from './task_manager'; import { SavedObjectsClientMock } from 'src/core/server/mocks'; import { SavedObjectsSerializer, SavedObjectsSchema } from 'src/core/server'; import { mockLogger } from './test_utils'; @@ -66,6 +66,7 @@ describe('TaskManager', () => { const promise = client.schedule(task); client.start(); await promise; + expect(savedObjectsClient.create).toHaveBeenCalled(); }); @@ -164,4 +165,28 @@ describe('TaskManager', () => { /Cannot add middleware after the task manager is initialized/i ); }); + + describe('claimAvailableTasks', () => { + test('should claim Available Tasks when there are available workers', () => { + const logger = mockLogger(); + const claim = jest.fn(() => Promise.resolve({ docs: [], claimedTasks: 0 })); + + const availableWorkers = 1; + + claimAvailableTasks(claim, availableWorkers, logger); + + expect(claim).toHaveBeenCalledTimes(1); + }); + + test('shouldnt claim Available Tasks when there are no available workers', () => { + const logger = mockLogger(); + const claim = jest.fn(() => Promise.resolve({ docs: [], claimedTasks: 0 })); + + const availableWorkers = 0; + + claimAvailableTasks(claim, availableWorkers, logger); + + expect(claim).not.toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/legacy/plugins/task_manager/task_manager.ts b/x-pack/legacy/plugins/task_manager/task_manager.ts index 7d2794fa33bcc..11cfc2ff8dafa 100644 --- a/x-pack/legacy/plugins/task_manager/task_manager.ts +++ b/x-pack/legacy/plugins/task_manager/task_manager.ts @@ -21,7 +21,13 @@ import { import { TaskPoller } from './task_poller'; import { TaskPool } from './task_pool'; import { TaskManagerRunner } from './task_runner'; -import { FetchOpts, FetchResult, TaskStore } from './task_store'; +import { + FetchOpts, + FetchResult, + TaskStore, + OwnershipClaimingOpts, + ClaimOwnershipResult, +} from './task_store'; export interface TaskManagerOpts { logger: Logger; @@ -103,7 +109,17 @@ export class TaskManager { const poller = new TaskPoller({ logger: this.logger, pollInterval: opts.config.get('xpack.task_manager.poll_interval'), - work: (): Promise => fillPool(pool.run, () => this.claimAvailableTasks(), createRunner), + work: (): Promise => + fillPool( + pool.run, + () => + claimAvailableTasks( + this.store.claimAvailableTasks.bind(this.store), + this.pool.availableWorkers, + this.logger + ), + createRunner + ), }); this.pool = pool; @@ -135,20 +151,6 @@ export class TaskManager { startPoller(); } - private async claimAvailableTasks() { - const { docs, claimedTasks } = await this.store.claimAvailableTasks({ - size: this.pool.availableWorkers, - claimOwnershipUntil: intervalFromNow('30s')!, - }); - - if (docs.length !== claimedTasks) { - this.logger.warn( - `[Task Ownership error]: (${claimedTasks}) tasks were claimed by Kibana, but (${docs.length}) tasks were fetched` - ); - } - return docs; - } - private async waitUntilStarted() { if (!this.isStarted) { await new Promise(resolve => { @@ -247,3 +249,27 @@ export class TaskManager { } } } + +export async function claimAvailableTasks( + claim: (opts: OwnershipClaimingOpts) => Promise, + availableWorkers: number, + logger: Logger +) { + if (availableWorkers > 0) { + const { docs, claimedTasks } = await claim({ + size: availableWorkers, + claimOwnershipUntil: intervalFromNow('30s')!, + }); + + if (docs.length !== claimedTasks) { + logger.warn( + `[Task Ownership error]: (${claimedTasks}) tasks were claimed by Kibana, but (${docs.length}) tasks were fetched` + ); + } + return docs; + } + logger.info( + `[Task Ownership]: Task Manager has skipped Claiming Ownership of available tasks at it has ran out Available Workers. If this happens often, consider adjusting the "xpack.task_manager.max_workers" configuration.` + ); + return []; +} From 09d99584df5c60898e3edd42ddcfdb8f01f888ba Mon Sep 17 00:00:00 2001 From: Michail Yasonik Date: Wed, 16 Oct 2019 18:13:34 +0530 Subject: [PATCH 26/48] Improves Canvas controls accessibility (#48005) --- .../auto_refresh_controls.tsx | 118 +++++++++++++----- .../control_settings/control_settings.scss | 4 + .../control_settings/kiosk_controls.tsx | 92 +++++++++----- 3 files changed, 146 insertions(+), 68 deletions(-) diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/auto_refresh_controls.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/auto_refresh_controls.tsx index fde0e2b4012c1..97d8920d50dd3 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/auto_refresh_controls.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/auto_refresh_controls.tsx @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, ReactNode } from 'react'; +import React, { ReactNode } from 'react'; import PropTypes from 'prop-types'; import { EuiFlexGroup, - EuiFlexGrid, EuiFlexItem, EuiLink, EuiSpacer, @@ -16,10 +15,11 @@ import { EuiDescriptionList, EuiDescriptionListTitle, EuiDescriptionListDescription, - EuiFormLabel, + EuiTitle, EuiText, EuiButtonIcon, EuiToolTip, + htmlIdGenerator, } from '@elastic/eui'; import { timeDuration } from '../../../lib/time_duration'; import { RefreshControl } from '../refresh_control'; @@ -37,26 +37,36 @@ interface Props { } interface ListGroupProps { + 'aria-labelledby': string; + className: string; children: ReactNode; } interface RefreshItemProps { duration: number; label: string; + descriptionId: string; } -const ListGroup = ({ children }: ListGroupProps) => ( -

    {[children]}
+const ListGroup = ({ children, ...rest }: ListGroupProps) => ( +
    + {[children]} +
); +const generateId = htmlIdGenerator(); + export const AutoRefreshControls = ({ refreshInterval, setRefresh, disableInterval }: Props) => { - const RefreshItem = ({ duration, label }: RefreshItemProps) => ( + const RefreshItem = ({ duration, label, descriptionId }: RefreshItemProps) => (
  • - setRefresh(duration)}>{label} + setRefresh(duration)} aria-describedby={descriptionId}> + {label} +
  • ); const interval = timeDuration(refreshInterval); + const intervalTitleId = generateId(); return ( @@ -67,11 +77,9 @@ export const AutoRefreshControls = ({ refreshInterval, setRefresh, disableInterv {strings.getRefreshListTitle()} {refreshInterval > 0 ? ( - - {timeStrings.getCycleTimeText(interval.length, interval.format)} - + <>{timeStrings.getCycleTimeText(interval.length, interval.format)} ) : ( - {strings.getRefreshListDurationManualText()} + <>{strings.getRefreshListDurationManualText()} )} @@ -98,31 +106,73 @@ export const AutoRefreshControls = ({ refreshInterval, setRefresh, disableInterv - {strings.getIntervalFormLabelText()} + +

    {strings.getIntervalFormLabelText()}

    +
    - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/control_settings.scss b/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/control_settings.scss index beaead8b99fc3..3d217dd1fc180 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/control_settings.scss +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/control_settings.scss @@ -1,3 +1,7 @@ .canvasControlSettings__popover { width: 600px; } + +.canvasControlSettings__list { + columns: 2; +} diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/kiosk_controls.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/kiosk_controls.tsx index a98cdddc21e19..9e6f0a91c6120 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/kiosk_controls.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/kiosk_controls.tsx @@ -10,15 +10,15 @@ import { EuiDescriptionList, EuiDescriptionListDescription, EuiDescriptionListTitle, - EuiFormLabel, + EuiTitle, EuiHorizontalRule, EuiLink, EuiSpacer, EuiSwitch, EuiText, - EuiFlexGrid, EuiFlexItem, EuiFlexGroup, + htmlIdGenerator, } from '@elastic/eui'; import { timeDuration } from '../../../lib/time_duration'; import { CustomInterval } from './custom_interval'; @@ -36,31 +36,40 @@ interface Props { } interface ListGroupProps { + 'aria-labelledby'?: string; + className: string; children: ReactNode; } - interface RefreshItemProps { duration: number; label: string; + descriptionId: string; } -const ListGroup = ({ children }: ListGroupProps) => ( -
      {[children]}
    +const ListGroup = ({ children, ...rest }: ListGroupProps) => ( +
      + {[children]} +
    ); +const generateId = htmlIdGenerator(); + export const KioskControls = ({ autoplayEnabled, autoplayInterval, onSetEnabled, onSetInterval, }: Props) => { - const RefreshItem = ({ duration, label }: RefreshItemProps) => ( + const RefreshItem = ({ duration, label, descriptionId }: RefreshItemProps) => (
  • - onSetInterval(duration)}>{label} + onSetInterval(duration)} aria-describedby={descriptionId}> + {label} +
  • ); const interval = timeDuration(autoplayInterval); + const intervalTitleId = generateId(); return ( @@ -68,40 +77,55 @@ export const KioskControls = ({ {strings.getTitle()} - {timeStrings.getCycleTimeText(interval.length, interval.format)} + {timeStrings.getCycleTimeText(interval.length, interval.format)} -
    - onSetEnabled(ev.target.checked)} - /> - -
    + onSetEnabled(ev.target.checked)} + /> + - {strings.getCycleFormLabel()} + +

    {strings.getCycleFormLabel()}

    +
    - - - - - - - - - - - - - - - - - + + + + + + + + From a9515bd9320f80542431faa86e74148af86b2250 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Wed, 16 Oct 2019 09:12:42 -0400 Subject: [PATCH 27/48] [ML] DF Analytics fix: show MSE and rSquared in expanded row only for regression jobs (#48390) * Only show MSE and rSquared in expanded row for regression jobs * use isRegressionAnalysis check --- .../components/analytics_list/expanded_row.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx index ab8ef81c593a2..67bad23adacf6 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx @@ -20,6 +20,7 @@ import { ExpandedRowJsonPane } from './expanded_row_json_pane'; import { ProgressBar } from './progress_bar'; import { getDependentVar, getValuesFromResponse, loadEvalData, Eval } from '../../../../common'; import { isCompletedAnalyticsJob } from './common'; +import { isRegressionAnalysis } from '../../../../common/analytics'; // import { ExpandedRowMessagesPane } from './expanded_row_messages_pane'; function getItemDescription(value: any) { @@ -60,6 +61,7 @@ export const ExpandedRow: FC = ({ item }) => { const index = idx(item, _ => _.config.dest.index) as string; const dependentVariable = getDependentVar(item.config.analysis); const jobIsCompleted = isCompletedAnalyticsJob(item.stats); + const isRegressionJob = isRegressionAnalysis(item.config.analysis); const loadData = async () => { setIsLoadingGeneralization(true); @@ -113,7 +115,7 @@ export const ExpandedRow: FC = ({ item }) => { }; useEffect(() => { - if (jobIsCompleted) { + if (jobIsCompleted && isRegressionJob) { loadData(); } }, [jobIsCompleted]); @@ -162,7 +164,7 @@ export const ExpandedRow: FC = ({ item }) => { position: 'right', }; - if (jobIsCompleted) { + if (jobIsCompleted && isRegressionJob) { stats.items.push( { title: 'generalization mean squared error', From 03a1ee15a7118cd1d4cd3adcb21ceba4498d7d41 Mon Sep 17 00:00:00 2001 From: ffknob Date: Wed, 16 Oct 2019 10:18:29 -0300 Subject: [PATCH 28/48] [APM] Use EUI to render "Local variables" for the stack trace (#48208) --- .../shared/Stacktrace/Variables.tsx | 61 ++++++------------- 1 file changed, 19 insertions(+), 42 deletions(-) diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/Variables.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/Variables.tsx index dfc9c95c9143a..34c46f84c76b9 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/Variables.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/Variables.tsx @@ -5,18 +5,12 @@ */ import theme from '@elastic/eui/dist/eui_theme_light.json'; +import styled from 'styled-components'; +import { EuiAccordion } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import styled from 'styled-components'; +import { borderRadius, px, unit, units } from '../../../style/variables'; import { IStackframe } from '../../../../typings/es_schemas/raw/fields/Stackframe'; -import { - borderRadius, - fontFamily, - px, - unit, - units -} from '../../../style/variables'; -import { Ellipsis } from '../Icons'; import { DottedKeyValueTable } from '../DottedKeyValueTable'; const VariablesContainer = styled.div` @@ -24,17 +18,6 @@ const VariablesContainer = styled.div` border-top: 1px solid ${theme.euiColorLightShade}; border-radius: 0 0 ${borderRadius} ${borderRadius}; padding: ${px(units.half)} ${px(unit)}; - font-family: ${fontFamily}; -`; - -const VariablesToggle = styled.a` - display: block; - cursor: pointer; - user-select: none; -`; - -const VariablesTableContainer = styled.div` - padding: ${px(units.plus)} ${px(unit)} 0; `; interface Props { @@ -42,34 +25,28 @@ interface Props { } export class Variables extends React.Component { - public state = { - isVisible: false - }; - - public onClick = () => { - this.setState(() => ({ isVisible: !this.state.isVisible })); - }; - public render() { if (!this.props.vars) { return null; } return ( - - - {' '} - {i18n.translate( - 'xpack.apm.stacktraceTab.localVariablesToogleButtonLabel', - { defaultMessage: 'Local variables' } - )} - - {this.state.isVisible && ( - - - - )} - + + + + + + + + + ); } } From 5b165466e60c8885143fb9f675e83b9950e143b6 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 16 Oct 2019 14:35:55 +0100 Subject: [PATCH 29/48] [ML] Removing old angular directives (#48382) * [ML] Removing old angular directives * reverts small change * typescriptifying access denied page * changing access denied text * updating translations --- .../ml/public/access_denied/index.html | 46 ----- .../plugins/ml/public/access_denied/index.js | 25 --- .../plugins/ml/public/access_denied/index.tsx | 39 +++++ .../plugins/ml/public/access_denied/page.tsx | 63 +++++++ x-pack/legacy/plugins/ml/public/app.js | 1 - .../data_recognizer_directive.js | 17 -- .../components/data_recognizer/index.ts | 2 - .../field_title_bar_directive.js | 44 ----- .../components/field_title_bar/index.js | 4 - .../field_type_icon_directive.js | 46 ----- .../components/field_type_icon/index.js | 3 - .../full_time_range_selector_directive.js | 58 ------- .../messagebar/__tests__/messagebar.js | 29 ---- .../public/components/messagebar/_index.scss | 1 - .../components/messagebar/_messagebar.scss | 35 ---- .../index.js => messagebar/index.ts} | 4 +- .../components/messagebar/messagebar.html | 7 - .../components/messagebar/messagebar.js | 26 --- .../messagebar/messagebar_service.d.ts | 7 - .../messagebar/messagebar_service.js | 58 ------- .../components/navigation_menu/index.ts | 2 +- ...navigation_menu_react_wrapper_directive.js | 29 ---- .../index.js => validate_job/index.ts} | 2 +- .../validate_job/validate_job_directive.js | 26 --- .../validate_job/validate_job_view.js | 4 +- .../pages/analytics_exploration/page.tsx | 2 +- .../pages/analytics_management/page.tsx | 2 +- .../datavisualizer/datavisualizer_selector.js | 2 +- .../file_based/file_datavisualizer.js | 2 +- .../datavisualizer/index_based/page.tsx | 164 +++++++++--------- .../datavisualizer/index_based/route.ts | 2 +- .../plugins/ml/public/explorer/explorer.js | 2 +- x-pack/legacy/plugins/ml/public/index.scss | 1 - .../edit_job_flyout/edit_job_flyout.js | 2 +- .../group_selector/group_selector.js | 2 +- .../public/jobs/jobs_list/components/utils.js | 2 +- .../plugins/ml/public/jobs/jobs_list/jobs.js | 2 +- .../estimate_bucket_span.ts | 2 +- .../multi_metric_view/metric_selection.tsx | 2 +- .../metric_selection_summary.tsx | 2 +- .../population_view/metric_selection.tsx | 2 +- .../metric_selection_summary.tsx | 2 +- .../single_metric_view/metric_selection.tsx | 2 +- .../metric_selection_summary.tsx | 2 +- .../components/validation_step/validation.tsx | 2 +- .../ml/public/overview/overview_page.tsx | 2 +- .../ml/public/services/calendar_service.js | 2 +- .../plugins/ml/public/services/job_service.js | 2 +- .../settings/calendars/edit/new_calendar.js | 2 +- .../calendars/edit/new_calendar.test.js | 2 +- .../settings/calendars/list/calendars_list.js | 2 +- .../calendars/list/calendars_list.test.js | 2 +- .../filter_lists/edit/edit_filter_list.js | 2 +- .../edit/edit_filter_list.test.js | 2 +- .../filter_lists/list/filter_lists.js | 2 +- .../filter_lists/list/filter_lists.test.js | 2 +- .../plugins/ml/public/settings/settings.js | 2 +- .../ml/public/settings/settings.test.js | 2 +- .../forecasting_modal_directive.js | 27 --- .../timeseriesexplorer/timeseriesexplorer.js | 2 +- .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - 62 files changed, 222 insertions(+), 620 deletions(-) delete mode 100644 x-pack/legacy/plugins/ml/public/access_denied/index.html delete mode 100644 x-pack/legacy/plugins/ml/public/access_denied/index.js create mode 100644 x-pack/legacy/plugins/ml/public/access_denied/index.tsx create mode 100644 x-pack/legacy/plugins/ml/public/access_denied/page.tsx delete mode 100644 x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer_directive.js delete mode 100644 x-pack/legacy/plugins/ml/public/components/field_title_bar/field_title_bar_directive.js delete mode 100644 x-pack/legacy/plugins/ml/public/components/field_type_icon/field_type_icon_directive.js delete mode 100644 x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector_directive.js delete mode 100644 x-pack/legacy/plugins/ml/public/components/messagebar/__tests__/messagebar.js delete mode 100644 x-pack/legacy/plugins/ml/public/components/messagebar/_index.scss delete mode 100644 x-pack/legacy/plugins/ml/public/components/messagebar/_messagebar.scss rename x-pack/legacy/plugins/ml/public/components/{validate_job/index.js => messagebar/index.ts} (80%) delete mode 100644 x-pack/legacy/plugins/ml/public/components/messagebar/messagebar.html delete mode 100644 x-pack/legacy/plugins/ml/public/components/messagebar/messagebar.js delete mode 100644 x-pack/legacy/plugins/ml/public/components/navigation_menu/navigation_menu_react_wrapper_directive.js rename x-pack/legacy/plugins/ml/public/components/{messagebar/index.js => validate_job/index.ts} (82%) delete mode 100644 x-pack/legacy/plugins/ml/public/components/validate_job/validate_job_directive.js delete mode 100644 x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/forecasting_modal_directive.js diff --git a/x-pack/legacy/plugins/ml/public/access_denied/index.html b/x-pack/legacy/plugins/ml/public/access_denied/index.html deleted file mode 100644 index e085e089b2728..0000000000000 --- a/x-pack/legacy/plugins/ml/public/access_denied/index.html +++ /dev/null @@ -1,46 +0,0 @@ - -
    -
    -
    -
    - - -
    -
    -

    -
    -
    - -
    - - -
    -
    diff --git a/x-pack/legacy/plugins/ml/public/access_denied/index.js b/x-pack/legacy/plugins/ml/public/access_denied/index.js deleted file mode 100644 index 52b813917e9be..0000000000000 --- a/x-pack/legacy/plugins/ml/public/access_denied/index.js +++ /dev/null @@ -1,25 +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 uiRoutes from 'ui/routes'; -import uiChrome from 'ui/chrome'; -import template from './index.html'; - -uiRoutes.when('/access-denied', { - template, - controllerAs: 'accessDenied', - controller($window, kbnUrl, kbnBaseUrl) { - this.goToKibana = () => { - $window.location.href = uiChrome.getBasePath() + kbnBaseUrl; - }; - - this.retry = () => { - return kbnUrl.redirect('/jobs'); - }; - } -}); diff --git a/x-pack/legacy/plugins/ml/public/access_denied/index.tsx b/x-pack/legacy/plugins/ml/public/access_denied/index.tsx new file mode 100644 index 0000000000000..883754896487e --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/access_denied/index.tsx @@ -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 React from 'react'; +import ReactDOM from 'react-dom'; +import uiRoutes from 'ui/routes'; +import { I18nContext } from 'ui/i18n'; +// @ts-ignore +import { uiModules } from 'ui/modules'; +import { AccessDeniedPage } from './page'; + +const module = uiModules.get('apps/ml', ['react']); + +const template = ``; + +uiRoutes.when('/access-denied', { + template, +}); + +module.directive('accessDenied', function() { + return { + scope: {}, + restrict: 'E', + link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { + ReactDOM.render( + {React.createElement(AccessDeniedPage)}, + element[0] + ); + + element.on('$destroy', () => { + ReactDOM.unmountComponentAtNode(element[0]); + scope.$destroy(); + }); + }, + }; +}); diff --git a/x-pack/legacy/plugins/ml/public/access_denied/page.tsx b/x-pack/legacy/plugins/ml/public/access_denied/page.tsx new file mode 100644 index 0000000000000..1c908e114cbeb --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/access_denied/page.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + EuiCallOut, + EuiPage, + EuiPageBody, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { NavigationMenu } from '../components/navigation_menu'; + +export const AccessDeniedPage = () => ( + + + + + + + +

    + +

    +
    +
    +
    + + + + +

    + +

    +
    +
    +
    +
    +
    +
    +); diff --git a/x-pack/legacy/plugins/ml/public/app.js b/x-pack/legacy/plugins/ml/public/app.js index a97150eaa3d2c..5208f9b24bc27 100644 --- a/x-pack/legacy/plugins/ml/public/app.js +++ b/x-pack/legacy/plugins/ml/public/app.js @@ -16,7 +16,6 @@ import 'plugins/ml/access_denied'; import 'plugins/ml/jobs'; import 'plugins/ml/overview'; import 'plugins/ml/services/calendar_service'; -import 'plugins/ml/components/messagebar'; import 'plugins/ml/data_frame_analytics'; import 'plugins/ml/datavisualizer'; import 'plugins/ml/explorer'; diff --git a/x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer_directive.js b/x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer_directive.js deleted file mode 100644 index 81186fb9af812..0000000000000 --- a/x-pack/legacy/plugins/ml/public/components/data_recognizer/data_recognizer_directive.js +++ /dev/null @@ -1,17 +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 'ngreact'; -import { DataRecognizer } from './data_recognizer'; - -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); -module.directive('mlDataRecognizer', function (reactDirective) { - return reactDirective(DataRecognizer, undefined, { restrict: 'AE' }); -}); diff --git a/x-pack/legacy/plugins/ml/public/components/data_recognizer/index.ts b/x-pack/legacy/plugins/ml/public/components/data_recognizer/index.ts index bb5a2ac4e479f..cc66fdac85af7 100644 --- a/x-pack/legacy/plugins/ml/public/components/data_recognizer/index.ts +++ b/x-pack/legacy/plugins/ml/public/components/data_recognizer/index.ts @@ -4,6 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './data_recognizer_directive'; - export { DataRecognizer } from './data_recognizer'; diff --git a/x-pack/legacy/plugins/ml/public/components/field_title_bar/field_title_bar_directive.js b/x-pack/legacy/plugins/ml/public/components/field_title_bar/field_title_bar_directive.js deleted file mode 100644 index 96ea2e7e27c37..0000000000000 --- a/x-pack/legacy/plugins/ml/public/components/field_title_bar/field_title_bar_directive.js +++ /dev/null @@ -1,44 +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 ReactDOM from 'react-dom'; - -import { FieldTitleBar } from './field_title_bar'; -import { I18nContext } from 'ui/i18n'; - -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); - -module.directive('mlFieldTitleBar', function () { - return { - restrict: 'E', - replace: false, - scope: { - card: '=' - }, - link: function (scope, element) { - scope.$watch('card', updateComponent); - - updateComponent(); - - function updateComponent() { - const props = { - card: scope.card - }; - - ReactDOM.render( - - {React.createElement(FieldTitleBar, props)} - , - element[0] - ); - } - } - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/components/field_title_bar/index.js b/x-pack/legacy/plugins/ml/public/components/field_title_bar/index.js index f04b029bd7c74..e969d15d3abfa 100644 --- a/x-pack/legacy/plugins/ml/public/components/field_title_bar/index.js +++ b/x-pack/legacy/plugins/ml/public/components/field_title_bar/index.js @@ -4,8 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ - - -import './field_title_bar_directive'; - export { FieldTitleBar } from './field_title_bar'; diff --git a/x-pack/legacy/plugins/ml/public/components/field_type_icon/field_type_icon_directive.js b/x-pack/legacy/plugins/ml/public/components/field_type_icon/field_type_icon_directive.js deleted file mode 100644 index 2be9129df6f7c..0000000000000 --- a/x-pack/legacy/plugins/ml/public/components/field_type_icon/field_type_icon_directive.js +++ /dev/null @@ -1,46 +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 ReactDOM from 'react-dom'; - -import { FieldTypeIcon } from './field_type_icon.js'; -import { I18nContext } from 'ui/i18n'; - -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); - -module.directive('mlFieldTypeIcon', function () { - return { - restrict: 'E', - replace: false, - scope: { - tooltipEnabled: '=', - type: '=' - }, - link: function (scope, element) { - scope.$watch('type', updateComponent); - - updateComponent(); - - function updateComponent() { - const props = { - tooltipEnabled: scope.tooltipEnabled, - type: scope.type - }; - - ReactDOM.render( - - {React.createElement(FieldTypeIcon, props)} - , - element[0] - ); - } - } - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/components/field_type_icon/index.js b/x-pack/legacy/plugins/ml/public/components/field_type_icon/index.js index 93f2f659b52d5..c9c8fc3e5e751 100644 --- a/x-pack/legacy/plugins/ml/public/components/field_type_icon/index.js +++ b/x-pack/legacy/plugins/ml/public/components/field_type_icon/index.js @@ -5,7 +5,4 @@ */ - -import './field_type_icon_directive'; - export { FieldTypeIcon } from './field_type_icon'; diff --git a/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector_directive.js b/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector_directive.js deleted file mode 100644 index 5c78d376317ac..0000000000000 --- a/x-pack/legacy/plugins/ml/public/components/full_time_range_selector/full_time_range_selector_directive.js +++ /dev/null @@ -1,58 +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 ReactDOM from 'react-dom'; - -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import { I18nContext } from 'ui/i18n'; - -import { FullTimeRangeSelector } from './index'; - -// Angular directive wrapper for the 'Use full time range' button. -module.directive('mlFullTimeRangeSelector', function () { - return { - restrict: 'E', - replace: true, - scope: { - indexPattern: '=', - disabled: '=', - query: '=' - }, - link: (scope, element) => { - - function renderComponent() { - const props = { - indexPattern: scope.indexPattern, - query: scope.query, - disabled: scope.disabled - }; - - ReactDOM.render( - - {React.createElement(FullTimeRangeSelector, props)} - , - element[0] - ); - } - - renderComponent(); - - // As the directive is only used in the job wizards and the data visualizer, - // it is safe to only watch the disabled property. - scope.$watch('disabled', renderComponent); - - element.on('$destroy', () => { - scope.$destroy(); - }); - - } - - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/components/messagebar/__tests__/messagebar.js b/x-pack/legacy/plugins/ml/public/components/messagebar/__tests__/messagebar.js deleted file mode 100644 index 5cbcdd9b8afc2..0000000000000 --- a/x-pack/legacy/plugins/ml/public/components/messagebar/__tests__/messagebar.js +++ /dev/null @@ -1,29 +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 ngMock from 'ng_mock'; -import expect from '@kbn/expect'; - -describe('ML - Message Bar Controller', () => { - beforeEach(() => { - ngMock.module('kibana'); - }); - - it('Initialize Message Bar Controller', (done) => { - ngMock.inject(function ($rootScope, $controller) { - const scope = $rootScope.$new(); - - expect(() => { - $controller('MlMessageBarController', { $scope: scope }); - }).to.not.throwError(); - - expect(scope.messages).to.eql([]); - done(); - }); - }); -}); diff --git a/x-pack/legacy/plugins/ml/public/components/messagebar/_index.scss b/x-pack/legacy/plugins/ml/public/components/messagebar/_index.scss deleted file mode 100644 index 55e87d2640465..0000000000000 --- a/x-pack/legacy/plugins/ml/public/components/messagebar/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'messagebar'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/public/components/messagebar/_messagebar.scss b/x-pack/legacy/plugins/ml/public/components/messagebar/_messagebar.scss deleted file mode 100644 index a4d48ff64ad6b..0000000000000 --- a/x-pack/legacy/plugins/ml/public/components/messagebar/_messagebar.scss +++ /dev/null @@ -1,35 +0,0 @@ - -ml-message-bar { - font-size: $euiFontSizeS; - - // SASSTODO: Needs proper selector - div { - padding: $euiSizeXS; - } - - .ml-message { - border-bottom: 1px solid $euiColorEmptyShade; - color: $euiColorEmptyShade; - margin: 0px; - border-radius: $euiBorderRadius; - padding: $euiSizeXS $euiSizeS; - - // SASSTODO: Needs proper selector - a { - color: $euiColorEmptyShade; - float: right; - } - } - - // SASSTODO: Needs proper variables - .ml-message-info { - background-color: #858585; - } - .ml-message-warning { - background-color: #FF7800; - } - .ml-message-error { - background-color: #e74c3c; - } - -} diff --git a/x-pack/legacy/plugins/ml/public/components/validate_job/index.js b/x-pack/legacy/plugins/ml/public/components/messagebar/index.ts similarity index 80% rename from x-pack/legacy/plugins/ml/public/components/validate_job/index.js rename to x-pack/legacy/plugins/ml/public/components/messagebar/index.ts index 07e6725e33518..35130d28a890d 100644 --- a/x-pack/legacy/plugins/ml/public/components/validate_job/index.js +++ b/x-pack/legacy/plugins/ml/public/components/messagebar/index.ts @@ -4,6 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ - - -import './validate_job_directive'; +export { mlMessageBarService } from './messagebar_service'; diff --git a/x-pack/legacy/plugins/ml/public/components/messagebar/messagebar.html b/x-pack/legacy/plugins/ml/public/components/messagebar/messagebar.html deleted file mode 100644 index ef06d3e4c48fe..0000000000000 --- a/x-pack/legacy/plugins/ml/public/components/messagebar/messagebar.html +++ /dev/null @@ -1,7 +0,0 @@ -
    -
    - {{msg.text}} x -
    -
    \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/public/components/messagebar/messagebar.js b/x-pack/legacy/plugins/ml/public/components/messagebar/messagebar.js deleted file mode 100644 index 293b0bc507b29..0000000000000 --- a/x-pack/legacy/plugins/ml/public/components/messagebar/messagebar.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - - - -import template from './messagebar.html'; - -import { mlMessageBarService } from 'plugins/ml/components/messagebar/messagebar_service'; - -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); - -module - .controller('MlMessageBarController', function ($scope) { - $scope.messages = mlMessageBarService.getMessages(); - $scope.removeMessage = mlMessageBarService.removeMessage; - }) - .directive('mlMessageBar', function () { - return { - restrict: 'AE', - template - }; - }); diff --git a/x-pack/legacy/plugins/ml/public/components/messagebar/messagebar_service.d.ts b/x-pack/legacy/plugins/ml/public/components/messagebar/messagebar_service.d.ts index 747ee928ac0c1..29a537a7ca8d8 100644 --- a/x-pack/legacy/plugins/ml/public/components/messagebar/messagebar_service.d.ts +++ b/x-pack/legacy/plugins/ml/public/components/messagebar/messagebar_service.d.ts @@ -5,13 +5,6 @@ */ declare interface MlMessageBarService { - getMessages(): any[]; - addMessage(msg: any): void; - removeMessage(index: number): void; - clear(): void; - info(text: any): void; - warning(text: any): void; - error(text: any, resp?: any): void; notify: { error(text: any, resp?: any): void; }; diff --git a/x-pack/legacy/plugins/ml/public/components/messagebar/messagebar_service.js b/x-pack/legacy/plugins/ml/public/components/messagebar/messagebar_service.js index 79412c1d2d72f..a373ed71772f0 100644 --- a/x-pack/legacy/plugins/ml/public/components/messagebar/messagebar_service.js +++ b/x-pack/legacy/plugins/ml/public/components/messagebar/messagebar_service.js @@ -9,57 +9,6 @@ import { toastNotifications } from 'ui/notify'; import { MLRequestFailure } from '../../util/ml_error'; import { i18n } from '@kbn/i18n'; -const messages = []; - - -const MSG_STYLE = { INFO: 'ml-message-info', WARNING: 'ml-message-warning', ERROR: 'ml-message-error' }; - -function getMessages() { - return messages; -} - -function addMessage(msg) { - if (messages.find(m => (m.text === msg.text && m.style === msg.style)) === undefined) { - messages.push(msg); - } -} - -function removeMessage(index) { - messages.splice(index, 1); -} - -function clear() { - messages.length = 0; -} - -function info(text) { - addMessage({ text, style: MSG_STYLE.INFO }); -} - -function warning(text) { - addMessage({ text, style: MSG_STYLE.WARNING }); -} - -function error(text, resp) { - text = `${text} ${expandErrorMessageObj(resp)}`; - addMessage({ text, style: MSG_STYLE.ERROR }); -} - -function expandErrorMessageObj(resp) { - let txt = ''; - if (resp !== undefined && typeof resp === 'object') { - try { - const respObj = JSON.parse(resp.response); - if (typeof respObj === 'object' && respObj.error !== undefined) { - txt = respObj.error.reason; - } - } catch(e) { - txt = resp.message; - } - } - return txt; -} - function errorNotify(text, resp) { let err = null; if (typeof text === 'object' && text.response !== undefined) { @@ -78,13 +27,6 @@ function errorNotify(text, resp) { } export const mlMessageBarService = { - getMessages, - addMessage, - removeMessage, - clear, - info, - warning, - error, notify: { error: errorNotify } diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/index.ts b/x-pack/legacy/plugins/ml/public/components/navigation_menu/index.ts index 55fe88ec90869..60baa8a9559e9 100644 --- a/x-pack/legacy/plugins/ml/public/components/navigation_menu/index.ts +++ b/x-pack/legacy/plugins/ml/public/components/navigation_menu/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './navigation_menu_react_wrapper_directive'; +export { NavigationMenu } from './navigation_menu'; diff --git a/x-pack/legacy/plugins/ml/public/components/navigation_menu/navigation_menu_react_wrapper_directive.js b/x-pack/legacy/plugins/ml/public/components/navigation_menu/navigation_menu_react_wrapper_directive.js deleted file mode 100644 index 543f04b6a4125..0000000000000 --- a/x-pack/legacy/plugins/ml/public/components/navigation_menu/navigation_menu_react_wrapper_directive.js +++ /dev/null @@ -1,29 +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 ReactDOM from 'react-dom'; - -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); - -import { NavigationMenu } from './navigation_menu'; - -module.directive('mlNavMenu', function () { - return { - restrict: 'E', - transclude: true, - link: function (scope, element, attrs) { - ReactDOM.render(, element[0]); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - } - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/components/messagebar/index.js b/x-pack/legacy/plugins/ml/public/components/validate_job/index.ts similarity index 82% rename from x-pack/legacy/plugins/ml/public/components/messagebar/index.js rename to x-pack/legacy/plugins/ml/public/components/validate_job/index.ts index 6dc4892b37a2b..fed452502cc89 100644 --- a/x-pack/legacy/plugins/ml/public/components/messagebar/index.js +++ b/x-pack/legacy/plugins/ml/public/components/validate_job/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './messagebar'; +export { ValidateJob } from './validate_job_view'; diff --git a/x-pack/legacy/plugins/ml/public/components/validate_job/validate_job_directive.js b/x-pack/legacy/plugins/ml/public/components/validate_job/validate_job_directive.js deleted file mode 100644 index 29455f3949157..0000000000000 --- a/x-pack/legacy/plugins/ml/public/components/validate_job/validate_job_directive.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - - - - -import 'ngreact'; - -import { wrapInI18nContext } from 'ui/i18n'; -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import { ValidateJob } from './validate_job_view'; -import { mlJobService } from 'plugins/ml/services/job_service'; - -module.directive('mlValidateJob', function (reactDirective) { - return reactDirective( - wrapInI18nContext(ValidateJob), - undefined, - { restrict: 'E' }, - { mlJobService } - ); -}); diff --git a/x-pack/legacy/plugins/ml/public/components/validate_job/validate_job_view.js b/x-pack/legacy/plugins/ml/public/components/validate_job/validate_job_view.js index 88e85cc5e530f..42ade2d9f9360 100644 --- a/x-pack/legacy/plugins/ml/public/components/validate_job/validate_job_view.js +++ b/x-pack/legacy/plugins/ml/public/components/validate_job/validate_job_view.js @@ -167,7 +167,7 @@ Modal.propType = { title: PropTypes.string }; -class ValidateJob extends Component { +export class ValidateJob extends Component { constructor(props) { super(props); this.state = getDefaultState(); @@ -329,5 +329,3 @@ ValidateJob.propTypes = { setIsValid: PropTypes.func, idFilterList: PropTypes.array, }; - -export { ValidateJob }; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/page.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/page.tsx index ad12404419133..0bbc313704174 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/page.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_exploration/page.tsx @@ -20,7 +20,7 @@ import { EuiTitle, } from '@elastic/eui'; -import { NavigationMenu } from '../../../components/navigation_menu/navigation_menu'; +import { NavigationMenu } from '../../../components/navigation_menu'; import { Exploration } from './components/exploration'; import { RegressionExploration } from './components/regression_exploration'; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/page.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/page.tsx index c846a196dafab..fcff4aa06b6bb 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/page.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/page.tsx @@ -23,7 +23,7 @@ import { EuiTitle, } from '@elastic/eui'; -import { NavigationMenu } from '../../../components/navigation_menu/navigation_menu'; +import { NavigationMenu } from '../../../components/navigation_menu'; import { CreateAnalyticsButton } from './components/create_analytics_button'; import { DataFrameAnalyticsList } from './components/analytics_list'; import { RefreshAnalyticsListButton } from './components/refresh_analytics_list_button'; diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/datavisualizer_selector.js b/x-pack/legacy/plugins/ml/public/datavisualizer/datavisualizer_selector.js index 65cc2533ed4fb..3bd825aee15ef 100644 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/datavisualizer_selector.js +++ b/x-pack/legacy/plugins/ml/public/datavisualizer/datavisualizer_selector.js @@ -24,7 +24,7 @@ import { isFullLicense } from '../license/check_license'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; import { timefilter } from 'ui/timefilter'; -import { NavigationMenu } from '../components/navigation_menu/navigation_menu'; +import { NavigationMenu } from '../components/navigation_menu'; function startTrialDescription() { return ( diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/file_datavisualizer.js b/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/file_datavisualizer.js index 241d80a2fcb0b..588c0b1ac2843 100644 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/file_datavisualizer.js +++ b/x-pack/legacy/plugins/ml/public/datavisualizer/file_based/file_datavisualizer.js @@ -8,7 +8,7 @@ import React, { Fragment } from 'react'; import { timefilter } from 'ui/timefilter'; -import { NavigationMenu } from '../../components/navigation_menu/navigation_menu'; +import { NavigationMenu } from '../../components/navigation_menu'; import { FileDataVisualizerView } from './components/file_datavisualizer_view'; diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/page.tsx b/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/page.tsx index e9ee556caaa1c..9123858eef4e5 100644 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/page.tsx +++ b/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/page.tsx @@ -26,6 +26,7 @@ import { EuiTitle, } from '@elastic/eui'; +import { NavigationMenu } from '../../components/navigation_menu'; import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; import { ML_JOB_FIELD_TYPES } from '../../../common/constants/field_types'; import { SEARCH_QUERY_LANGUAGE } from '../../../common/constants/search'; @@ -591,87 +592,90 @@ export const Page: FC = () => { } return ( - - - - - -

    {currentIndexPattern.title}

    -
    -
    - {currentIndexPattern.timeFieldName !== undefined && ( - - + + + + + + + +

    {currentIndexPattern.title}

    +
    - )} -
    - - - - - - - - - {totalMetricFieldCount > 0 && ( - - - - - )} - - - - - {showActionsPanel === true && ( - - - + {currentIndexPattern.timeFieldName !== undefined && ( + + + )} - - -
    -
    +
    + + + + + + + + + {totalMetricFieldCount > 0 && ( + + + + + )} + + + + + {showActionsPanel === true && ( + + + + )} + + +
    +
    + ); }; diff --git a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/route.ts b/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/route.ts index ff08591ac24d0..66edcbd945f77 100644 --- a/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/route.ts +++ b/x-pack/legacy/plugins/ml/public/datavisualizer/index_based/route.ts @@ -15,7 +15,7 @@ import { loadCurrentIndexPattern, loadCurrentSavedSearch } from '../../util/inde import { checkMlNodesAvailable } from '../../ml_nodes_check'; import { getDataVisualizerBreadcrumbs } from './breadcrumbs'; -const template = ``; +const template = ``; uiRoutes.when('/jobs/new_job/datavisualizer', { template, diff --git a/x-pack/legacy/plugins/ml/public/explorer/explorer.js b/x-pack/legacy/plugins/ml/public/explorer/explorer.js index 70a5c9c505acc..c9a2de3969280 100644 --- a/x-pack/legacy/plugins/ml/public/explorer/explorer.js +++ b/x-pack/legacy/plugins/ml/public/explorer/explorer.js @@ -43,7 +43,7 @@ import { InfluencersList } from '../components/influencers_list'; import { ALLOW_CELL_RANGE_SELECTION, dragSelect$, explorer$ } from './explorer_dashboard_service'; import { mlResultsService } from 'plugins/ml/services/results_service'; import { LoadingIndicator } from '../components/loading_indicator/loading_indicator'; -import { NavigationMenu } from '../components/navigation_menu/navigation_menu'; +import { NavigationMenu } from '../components/navigation_menu'; import { CheckboxShowCharts, showCharts$ } from '../components/controls/checkbox_showcharts'; import { JobSelector } from '../components/job_selector'; import { SelectInterval, interval$ } from '../components/controls/select_interval/select_interval'; diff --git a/x-pack/legacy/plugins/ml/public/index.scss b/x-pack/legacy/plugins/ml/public/index.scss index af616ff54222f..97f0e037e2648 100644 --- a/x-pack/legacy/plugins/ml/public/index.scss +++ b/x-pack/legacy/plugins/ml/public/index.scss @@ -41,7 +41,6 @@ @import 'components/items_grid/index'; @import 'components/job_selector/index'; @import 'components/loading_indicator/index'; // SASSTODO: This component should be replaced with EuiLoadingSpinner - @import 'components/messagebar/index'; @import 'components/navigation_menu/index'; @import 'components/rule_editor/index'; // SASSTODO: This file overwrites EUI directly @import 'components/stats_bar/index'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js index 56b59eb949ee8..acc5a469204f9 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js @@ -26,7 +26,7 @@ import { JobDetails, Detectors, Datafeed, CustomUrls } from './tabs'; import { saveJob } from './edit_utils'; import { loadFullJob } from '../utils'; import { validateModelMemoryLimit, validateGroupNames, isValidCustomUrls } from '../validate_job'; -import { mlMessageBarService } from '../../../../components/messagebar/messagebar_service'; +import { mlMessageBarService } from '../../../../components/messagebar'; import { toastNotifications } from 'ui/notify'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js index dea9c27e1a90e..d104d2e02d07b 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js @@ -28,7 +28,7 @@ import { cloneDeep } from 'lodash'; import { ml } from '../../../../../services/ml_api_service'; import { GroupList } from './group_list'; import { NewGroupInput } from './new_group_input'; -import { mlMessageBarService } from '../../../../../components/messagebar/messagebar_service'; +import { mlMessageBarService } from '../../../../../components/messagebar'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; function createSelectedGroups(jobs, groups) { diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/utils.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/utils.js index 484b7ad18f586..3344d2d4ad4a5 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/utils.js +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/utils.js @@ -6,7 +6,7 @@ import { each } from 'lodash'; import { toastNotifications } from 'ui/notify'; -import { mlMessageBarService } from 'plugins/ml/components/messagebar/messagebar_service'; +import { mlMessageBarService } from 'plugins/ml/components/messagebar'; import rison from 'rison-node'; import chrome from 'ui/chrome'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/jobs.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/jobs.js index 5bdf9c1944e43..188048d2d2f05 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/jobs.js +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/jobs.js @@ -6,7 +6,7 @@ import React, { Fragment } from 'react'; -import { NavigationMenu } from '../../components/navigation_menu/navigation_menu'; +import { NavigationMenu } from '../../components/navigation_menu'; import { JobsListView } from './components/jobs_list_view'; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts index f60c9c3fb8ca8..f86baaccb84d3 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts @@ -15,7 +15,7 @@ import { } from '../../../../../common/job_creator'; import { ml, BucketSpanEstimatorData } from '../../../../../../../services/ml_api_service'; import { useKibanaContext } from '../../../../../../../contexts/kibana'; -import { mlMessageBarService } from '../../../../../../../components/messagebar/messagebar_service'; +import { mlMessageBarService } from '../../../../../../../components/messagebar'; export enum ESTIMATE_STATUS { NOT_RUNNING, diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx index e42bb76373e62..f913b5a5720d6 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx @@ -15,7 +15,7 @@ import { AggFieldPair } from '../../../../../../../../common/types/fields'; import { getChartSettings, defaultChartSettings } from '../../../charts/common/settings'; import { MetricSelector } from './metric_selector'; import { ChartGrid } from './chart_grid'; -import { mlMessageBarService } from '../../../../../../../components/messagebar/messagebar_service'; +import { mlMessageBarService } from '../../../../../../../components/messagebar'; interface Props { setIsValid: (na: boolean) => void; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx index 44b8fd0255d3d..f39a316440e74 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx @@ -12,7 +12,7 @@ import { Results, ModelItem, Anomaly } from '../../../../../common/results_loade import { LineChartData } from '../../../../../common/chart_loader'; import { getChartSettings, defaultChartSettings } from '../../../charts/common/settings'; import { ChartGrid } from './chart_grid'; -import { mlMessageBarService } from '../../../../../../../components/messagebar/messagebar_service'; +import { mlMessageBarService } from '../../../../../../../components/messagebar'; export const MultiMetricDetectorsSummary: FC = () => { const { jobCreator: jc, chartLoader, resultsLoader, chartInterval } = useContext( diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection.tsx index 00948902c0407..87d3e87d65536 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection.tsx @@ -17,7 +17,7 @@ import { getChartSettings, defaultChartSettings } from '../../../charts/common/s import { MetricSelector } from './metric_selector'; import { SplitFieldSelector } from '../split_field'; import { ChartGrid } from './chart_grid'; -import { mlMessageBarService } from '../../../../../../../components/messagebar/messagebar_service'; +import { mlMessageBarService } from '../../../../../../../components/messagebar'; interface Props { setIsValid: (na: boolean) => void; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx index 5b7d16e2bd7c9..b13f8e3a73a10 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx @@ -15,7 +15,7 @@ import { LineChartData } from '../../../../../common/chart_loader'; import { Field, AggFieldPair } from '../../../../../../../../common/types/fields'; import { getChartSettings, defaultChartSettings } from '../../../charts/common/settings'; import { ChartGrid } from './chart_grid'; -import { mlMessageBarService } from '../../../../../../../components/messagebar/messagebar_service'; +import { mlMessageBarService } from '../../../../../../../components/messagebar'; type DetectorFieldValues = Record; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx index 5a6e08cb2948a..45872faae4c09 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx @@ -13,7 +13,7 @@ import { newJobCapsService } from '../../../../../../../services/new_job_capabil import { AggFieldPair } from '../../../../../../../../common/types/fields'; import { AnomalyChart, CHART_TYPE } from '../../../charts/anomaly_chart'; import { getChartSettings } from '../../../charts/common/settings'; -import { mlMessageBarService } from '../../../../../../../components/messagebar/messagebar_service'; +import { mlMessageBarService } from '../../../../../../../components/messagebar'; interface Props { setIsValid: (na: boolean) => void; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx index 8124f20a33da8..85fb5890307ba 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx @@ -11,7 +11,7 @@ import { Results, ModelItem, Anomaly } from '../../../../../common/results_loade import { LineChartData } from '../../../../../common/chart_loader'; import { AnomalyChart, CHART_TYPE } from '../../../charts/anomaly_chart'; import { getChartSettings } from '../../../charts/common/settings'; -import { mlMessageBarService } from '../../../../../../../components/messagebar/messagebar_service'; +import { mlMessageBarService } from '../../../../../../../components/messagebar'; const DTR_IDX = 0; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/validation_step/validation.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/validation_step/validation.tsx index ac7e89ecf95b7..b60e225cdff4d 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/validation_step/validation.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/validation_step/validation.tsx @@ -9,7 +9,7 @@ import { WizardNav } from '../wizard_nav'; import { WIZARD_STEPS, StepProps } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; import { mlJobService } from '../../../../../services/job_service'; -import { ValidateJob } from '../../../../../components/validate_job/validate_job_view'; +import { ValidateJob } from '../../../../../components/validate_job'; import { JOB_TYPE } from '../../../common/job_creator/util/constants'; const idFilterList = [ diff --git a/x-pack/legacy/plugins/ml/public/overview/overview_page.tsx b/x-pack/legacy/plugins/ml/public/overview/overview_page.tsx index bb9e45ae84a41..39c3205d54eda 100644 --- a/x-pack/legacy/plugins/ml/public/overview/overview_page.tsx +++ b/x-pack/legacy/plugins/ml/public/overview/overview_page.tsx @@ -6,7 +6,7 @@ import React, { Fragment, FC } from 'react'; import { EuiFlexGroup, EuiPage, EuiPageBody } from '@elastic/eui'; -import { NavigationMenu } from '../components/navigation_menu/navigation_menu'; +import { NavigationMenu } from '../components/navigation_menu'; import { OverviewSideBar } from './components/sidebar'; import { OverviewContent } from './components/content'; diff --git a/x-pack/legacy/plugins/ml/public/services/calendar_service.js b/x-pack/legacy/plugins/ml/public/services/calendar_service.js index 84f5bdb3c8e18..09e3001a0f7f5 100644 --- a/x-pack/legacy/plugins/ml/public/services/calendar_service.js +++ b/x-pack/legacy/plugins/ml/public/services/calendar_service.js @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { ml } from 'plugins/ml/services/ml_api_service'; import { mlJobService } from 'plugins/ml/services/job_service'; -import { mlMessageBarService } from 'plugins/ml/components/messagebar/messagebar_service'; +import { mlMessageBarService } from 'plugins/ml/components/messagebar'; diff --git a/x-pack/legacy/plugins/ml/public/services/job_service.js b/x-pack/legacy/plugins/ml/public/services/job_service.js index 845481d41da77..bea6631a6a2c4 100644 --- a/x-pack/legacy/plugins/ml/public/services/job_service.js +++ b/x-pack/legacy/plugins/ml/public/services/job_service.js @@ -13,7 +13,7 @@ import { i18n } from '@kbn/i18n'; import { ml } from './ml_api_service'; -import { mlMessageBarService } from '../components/messagebar/messagebar_service'; +import { mlMessageBarService } from '../components/messagebar'; import { isWebUrl } from '../util/url_utils'; import { ML_DATA_PREVIEW_COUNT } from '../../common/util/job_utils'; import { parseInterval } from '../../common/util/parse_interval'; diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_calendar.js b/x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_calendar.js index 1351a06c21f2c..12c8339c52d71 100644 --- a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_calendar.js +++ b/x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_calendar.js @@ -21,7 +21,7 @@ import { import chrome from 'ui/chrome'; import { toastNotifications } from 'ui/notify'; -import { NavigationMenu } from '../../../components/navigation_menu/navigation_menu'; +import { NavigationMenu } from '../../../components/navigation_menu'; import { getCalendarSettingsData, validateCalendarId } from './utils'; import { CalendarForm } from './calendar_form/'; diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_calendar.test.js b/x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_calendar.test.js index bc2bddab7a6cb..a7547b9e43c73 100644 --- a/x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_calendar.test.js +++ b/x-pack/legacy/plugins/ml/public/settings/calendars/edit/new_calendar.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../../../components/navigation_menu/navigation_menu', () => ({ +jest.mock('../../../components/navigation_menu', () => ({ NavigationMenu: () =>
    })); jest.mock('../../../privilege/check_privilege', () => ({ diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/calendars_list.js b/x-pack/legacy/plugins/ml/public/settings/calendars/list/calendars_list.js index 0b5657b62ebf3..b7ad2c36f3b43 100644 --- a/x-pack/legacy/plugins/ml/public/settings/calendars/list/calendars_list.js +++ b/x-pack/legacy/plugins/ml/public/settings/calendars/list/calendars_list.js @@ -17,7 +17,7 @@ import { EUI_MODAL_CONFIRM_BUTTON, } from '@elastic/eui'; -import { NavigationMenu } from '../../../components/navigation_menu/navigation_menu'; +import { NavigationMenu } from '../../../components/navigation_menu'; import { CalendarsListHeader } from './header'; import { CalendarsListTable } from './table/'; import { ml } from '../../../services/ml_api_service'; diff --git a/x-pack/legacy/plugins/ml/public/settings/calendars/list/calendars_list.test.js b/x-pack/legacy/plugins/ml/public/settings/calendars/list/calendars_list.test.js index 84211597f313c..d32b0ae9a4d5f 100644 --- a/x-pack/legacy/plugins/ml/public/settings/calendars/list/calendars_list.test.js +++ b/x-pack/legacy/plugins/ml/public/settings/calendars/list/calendars_list.test.js @@ -10,7 +10,7 @@ import { ml } from '../../../services/ml_api_service'; import { CalendarsList } from './calendars_list'; -jest.mock('../../../components/navigation_menu/navigation_menu', () => ({ +jest.mock('../../../components/navigation_menu', () => ({ NavigationMenu: () =>
    })); jest.mock('../../../privilege/check_privilege', () => ({ diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/edit_filter_list.js b/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/edit_filter_list.js index 2a27dd0fc9774..aed789304186e 100644 --- a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/edit_filter_list.js +++ b/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/edit_filter_list.js @@ -31,7 +31,7 @@ import { toastNotifications } from 'ui/notify'; import { EditFilterListHeader } from './header'; import { EditFilterListToolbar } from './toolbar'; import { ItemsGrid } from '../../../components/items_grid'; -import { NavigationMenu } from '../../../components/navigation_menu/navigation_menu'; +import { NavigationMenu } from '../../../components/navigation_menu'; import { isValidFilterListId, saveFilterList diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/edit_filter_list.test.js b/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/edit_filter_list.test.js index 035913d5dc465..d2030a3dc944e 100644 --- a/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/edit_filter_list.test.js +++ b/x-pack/legacy/plugins/ml/public/settings/filter_lists/edit/edit_filter_list.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../../../components/navigation_menu/navigation_menu', () => ({ +jest.mock('../../../components/navigation_menu', () => ({ NavigationMenu: () =>
    })); diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/filter_lists.js b/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/filter_lists.js index 8ea2bb8929913..c6b177bcceaef 100644 --- a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/filter_lists.js +++ b/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/filter_lists.js @@ -21,7 +21,7 @@ import { injectI18n } from '@kbn/i18n/react'; import { toastNotifications } from 'ui/notify'; -import { NavigationMenu } from '../../../components/navigation_menu/navigation_menu'; +import { NavigationMenu } from '../../../components/navigation_menu'; import { FilterListsHeader } from './header'; import { FilterListsTable } from './table'; diff --git a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/filter_lists.test.js b/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/filter_lists.test.js index 3c6a564ec92b4..ddf832565a8be 100644 --- a/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/filter_lists.test.js +++ b/x-pack/legacy/plugins/ml/public/settings/filter_lists/list/filter_lists.test.js @@ -9,7 +9,7 @@ import React from 'react'; import { FilterLists } from './filter_lists'; -jest.mock('../../../components/navigation_menu/navigation_menu', () => ({ +jest.mock('../../../components/navigation_menu', () => ({ NavigationMenu: () =>
    })); jest.mock('../../../privilege/check_privilege', () => ({ diff --git a/x-pack/legacy/plugins/ml/public/settings/settings.js b/x-pack/legacy/plugins/ml/public/settings/settings.js index e8d2533af7f4b..f04283563797f 100644 --- a/x-pack/legacy/plugins/ml/public/settings/settings.js +++ b/x-pack/legacy/plugins/ml/public/settings/settings.js @@ -23,7 +23,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { useUiChromeContext } from '../contexts/ui/use_ui_chrome_context'; -import { NavigationMenu } from '../components/navigation_menu/navigation_menu'; +import { NavigationMenu } from '../components/navigation_menu'; export function Settings({ canGetFilters, diff --git a/x-pack/legacy/plugins/ml/public/settings/settings.test.js b/x-pack/legacy/plugins/ml/public/settings/settings.test.js index 0fe0a4ab50be4..8efe558fda961 100644 --- a/x-pack/legacy/plugins/ml/public/settings/settings.test.js +++ b/x-pack/legacy/plugins/ml/public/settings/settings.test.js @@ -10,7 +10,7 @@ import React from 'react'; import { Settings } from './settings'; jest.mock('../contexts/ui/use_ui_chrome_context'); -jest.mock('../components/navigation_menu/navigation_menu', () => ({ +jest.mock('../components/navigation_menu', () => ({ NavigationMenu: () =>
    , })); diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/forecasting_modal_directive.js b/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/forecasting_modal_directive.js deleted file mode 100644 index cf101256724e9..0000000000000 --- a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/components/forecasting_modal/forecasting_modal_directive.js +++ /dev/null @@ -1,27 +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 'ngreact'; - -import { wrapInI18nContext } from 'ui/i18n'; -import { timefilter } from 'ui/timefilter'; -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import { ForecastingModal } from './forecasting_modal'; - -module.directive('mlForecastingModal', function ($injector) { - const reactDirective = $injector.get('reactDirective'); - return reactDirective( - wrapInI18nContext(ForecastingModal), - // reactDirective service requires for react component to have propTypes, but injectI18n doesn't copy propTypes from wrapped component. - // That's why we pass propTypes directly to reactDirective service. - Object.keys(ForecastingModal.WrappedComponent.propTypes || {}), - { restrict: 'E' }, - { timefilter } - ); -}); diff --git a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.js b/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.js index 5afb5ae64385d..cea2dc37bfad3 100644 --- a/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/legacy/plugins/ml/public/timeseriesexplorer/timeseriesexplorer.js @@ -51,7 +51,7 @@ import { EntityControl } from './components/entity_control'; import { ForecastingModal } from './components/forecasting_modal/forecasting_modal'; import { JobSelector } from '../components/job_selector'; import { LoadingIndicator } from '../components/loading_indicator/loading_indicator'; -import { NavigationMenu } from '../components/navigation_menu/navigation_menu'; +import { NavigationMenu } from '../components/navigation_menu'; import { severity$, SelectSeverity } from '../components/controls/select_severity/select_severity'; import { interval$, SelectInterval } from '../components/controls/select_interval/select_interval'; import { TimeseriesChart } from './components/timeseries_chart/timeseries_chart'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3919a864b8d69..bb26a1cf2b1ed 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5774,10 +5774,6 @@ "xpack.maps.tooltip.toolsControl.cancelDrawButtonLabel": "キャンセル", "xpack.maps.xyztmssource.attributionLink": "属性テキストにはリンクが必要です", "xpack.maps.xyztmssource.attributionText": "属性 URL にはテキストが必要です", - "xpack.ml.accessDenied.backToKibanaHomeButtonLabel": "Kibana ホームに戻る", - "xpack.ml.accessDenied.noGrantedPrivilegesDescription": "{kibanaUserParam} と {machineLearningUserParam} ロールの権限が必要です。{br}これらのロールはシステム管理者がユーザー管理ページで設定します。", - "xpack.ml.accessDenied.noPermissionToAccessMLLabel": "機械学習へのアクセスにはパーミッションが必要です", - "xpack.ml.accessDenied.retryButtonLabel": "再試行", "xpack.ml.annotationsTable.actionsColumnName": "アクション", "xpack.ml.annotationsTable.annotationColumnName": "注釈", "xpack.ml.annotationsTable.annotationsNotCreatedTitle": "このジョブには注釈が作成されていません", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f53a630b7d135..7854d7b37ef9a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5776,10 +5776,6 @@ "xpack.maps.tooltip.toolsControl.cancelDrawButtonLabel": "取消", "xpack.maps.xyztmssource.attributionLink": "属性文本必须附带链接", "xpack.maps.xyztmssource.attributionText": "属性 url 必须附带文本", - "xpack.ml.accessDenied.backToKibanaHomeButtonLabel": "返回 Kibana 主页", - "xpack.ml.accessDenied.noGrantedPrivilegesDescription": "您必须具有 {kibanaUserParam} 和 {machineLearningUserParam} 角色授予的权限。{br}您的系统管理员可以在“管理用户”页面上设置这些角色。", - "xpack.ml.accessDenied.noPermissionToAccessMLLabel": "您需要具备访问 Machine Learning 的权限", - "xpack.ml.accessDenied.retryButtonLabel": "重试", "xpack.ml.annotationsTable.actionsColumnName": "操作", "xpack.ml.annotationsTable.annotationColumnName": "注释", "xpack.ml.annotationsTable.annotationsNotCreatedTitle": "没有为此作业创建注释", From 9f11d0d6e806b42cc6e509011156424681a4d99b Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Wed, 16 Oct 2019 17:15:10 +0300 Subject: [PATCH 30/48] [Vis: Default editor] Add unit tests for metric_agg and extended_bounds, fix types (#47815) * Add unit tests for metric_agg and extended_bounds * Fix types * Code refactoring, create agg_utils and tests * Remove unused translations * Fix typos * Update snapshots --- src/legacy/ui/public/agg_types/agg_utils.ts | 39 --- .../editors/default/components/agg.test.tsx | 3 +- .../default/components/agg_common_props.ts | 11 +- .../extended_bounds.test.tsx.snap | 44 ++++ .../__snapshots__/metric_agg.test.tsx.snap | 35 +++ .../default/controls/agg_control_props.tsx | 5 +- .../controls/agg_control_react_wrapper.tsx | 34 --- .../default/controls/agg_utils.test.tsx | 230 ++++++++++++++++++ .../vis/editors/default/controls/agg_utils.ts | 132 ++++++++++ .../default/controls/extended_bounds.test.tsx | 139 +++++++++++ .../default/controls/extended_bounds.tsx | 11 +- .../default/controls/metric_agg.test.tsx | 148 +++++++++++ .../editors/default/controls/metric_agg.tsx | 75 ++---- .../default/controls/number_interval.tsx | 15 +- .../vis/editors/default/controls/order_by.tsx | 74 ++---- .../default/controls/radius_ratio_option.tsx | 2 +- .../default/controls/rows_or_columns.tsx | 2 +- .../public/vis/editors/default/schemas.d.ts | 3 +- .../public/vis/editors/default/vis_options.js | 2 +- .../translations/translations/ja-JP.json | 6 +- .../translations/translations/zh-CN.json | 6 +- 21 files changed, 800 insertions(+), 216 deletions(-) delete mode 100644 src/legacy/ui/public/agg_types/agg_utils.ts create mode 100644 src/legacy/ui/public/vis/editors/default/controls/__snapshots__/extended_bounds.test.tsx.snap create mode 100644 src/legacy/ui/public/vis/editors/default/controls/__snapshots__/metric_agg.test.tsx.snap delete mode 100644 src/legacy/ui/public/vis/editors/default/controls/agg_control_react_wrapper.tsx create mode 100644 src/legacy/ui/public/vis/editors/default/controls/agg_utils.test.tsx create mode 100644 src/legacy/ui/public/vis/editors/default/controls/agg_utils.ts create mode 100644 src/legacy/ui/public/vis/editors/default/controls/extended_bounds.test.tsx create mode 100644 src/legacy/ui/public/vis/editors/default/controls/metric_agg.test.tsx diff --git a/src/legacy/ui/public/agg_types/agg_utils.ts b/src/legacy/ui/public/agg_types/agg_utils.ts deleted file mode 100644 index d7f4ec961ded6..0000000000000 --- a/src/legacy/ui/public/agg_types/agg_utils.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { i18n } from '@kbn/i18n'; -import { AggConfig } from './agg_config'; - -function safeMakeLabel(agg: AggConfig) { - try { - return agg.makeLabel(); - } catch (e) { - return i18n.translate('common.ui.aggTypes.aggNotValidLabel', { - defaultMessage: '- agg not valid -', - }); - } -} - -function isCompatibleAggregation(aggFilter: string[]) { - return (agg: AggConfig) => { - return !aggFilter.includes(`!${agg.type.name}`); - }; -} - -export { safeMakeLabel, isCompatibleAggregation }; diff --git a/src/legacy/ui/public/vis/editors/default/components/agg.test.tsx b/src/legacy/ui/public/vis/editors/default/components/agg.test.tsx index b87eb3f5fb303..5dfb14156deee 100644 --- a/src/legacy/ui/public/vis/editors/default/components/agg.test.tsx +++ b/src/legacy/ui/public/vis/editors/default/components/agg.test.tsx @@ -26,7 +26,6 @@ import { act } from 'react-dom/test-utils'; import { DefaultEditorAggParams } from './agg_params'; import { IndexPattern } from 'ui/index_patterns'; import { AggType } from 'ui/agg_types'; -import { Schema } from 'ui/vis/editors/default/schemas'; jest.mock('./agg_params', () => ({ DefaultEditorAggParams: () => null, @@ -158,7 +157,7 @@ describe('DefaultEditorAgg component', () => { it('should add schema component', () => { defaultProps.agg.schema = { editorComponent: () =>
    , - } as Schema; + } as any; const comp = mount(); expect(comp.find('.schemaComponent').exists()).toBeTruthy(); diff --git a/src/legacy/ui/public/vis/editors/default/components/agg_common_props.ts b/src/legacy/ui/public/vis/editors/default/components/agg_common_props.ts index 1a057f0c21702..232eaba76f3a1 100644 --- a/src/legacy/ui/public/vis/editors/default/components/agg_common_props.ts +++ b/src/legacy/ui/public/vis/editors/default/components/agg_common_props.ts @@ -22,10 +22,13 @@ import { AggConfig, VisState, VisParams } from '../../..'; import { AggParams } from '../agg_params'; import { AggGroupNames } from '../agg_groups'; -export type OnAggParamsChange = ( - params: AggParams | VisParams, - paramName: string, - value: unknown +export type OnAggParamsChange = < + Params extends AggParams | VisParams, + ParamName extends keyof Params +>( + params: Params, + paramName: ParamName, + value: Params[ParamName] ) => void; export interface DefaultEditorAggCommonProps { diff --git a/src/legacy/ui/public/vis/editors/default/controls/__snapshots__/extended_bounds.test.tsx.snap b/src/legacy/ui/public/vis/editors/default/controls/__snapshots__/extended_bounds.test.tsx.snap new file mode 100644 index 0000000000000..37783dc01492d --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/controls/__snapshots__/extended_bounds.test.tsx.snap @@ -0,0 +1,44 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ExtendedBoundsParamEditor should be rendered with default set of props 1`] = ` + + + + + + + + + + +`; diff --git a/src/legacy/ui/public/vis/editors/default/controls/__snapshots__/metric_agg.test.tsx.snap b/src/legacy/ui/public/vis/editors/default/controls/__snapshots__/metric_agg.test.tsx.snap new file mode 100644 index 0000000000000..a176295260c44 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/controls/__snapshots__/metric_agg.test.tsx.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MetricAggParamEditor should be rendered with default set of props 1`] = ` + + + +`; diff --git a/src/legacy/ui/public/vis/editors/default/controls/agg_control_props.tsx b/src/legacy/ui/public/vis/editors/default/controls/agg_control_props.tsx index 91294dffe71de..f215cf755886d 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/agg_control_props.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/agg_control_props.tsx @@ -19,9 +19,10 @@ import { VisParams } from '../../..'; import { AggParams } from '../agg_params'; +import { OnAggParamsChange } from '../components/agg_common_props'; -export interface AggControlProps { +export interface AggControlProps { aggParams: AggParams; editorStateParams: VisParams; - setValue(params: AggParams, paramName: string, value: T): void; + setValue: OnAggParamsChange; } diff --git a/src/legacy/ui/public/vis/editors/default/controls/agg_control_react_wrapper.tsx b/src/legacy/ui/public/vis/editors/default/controls/agg_control_react_wrapper.tsx deleted file mode 100644 index 17c5b319d424a..0000000000000 --- a/src/legacy/ui/public/vis/editors/default/controls/agg_control_react_wrapper.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { AggControlProps } from './agg_control_props'; - -interface AggControlReactWrapperProps extends AggControlProps { - component: React.FunctionComponent>; -} - -function AggControlReactWrapper({ - component: Component, - ...rest -}: AggControlReactWrapperProps) { - return ; -} - -export { AggControlReactWrapper }; diff --git a/src/legacy/ui/public/vis/editors/default/controls/agg_utils.test.tsx b/src/legacy/ui/public/vis/editors/default/controls/agg_utils.test.tsx new file mode 100644 index 0000000000000..9a96cc2221bd4 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/controls/agg_utils.test.tsx @@ -0,0 +1,230 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { FunctionComponent } from 'react'; +import { mount, ReactWrapper } from 'enzyme'; + +import { AggConfig } from 'ui/agg_types'; +import { + safeMakeLabel, + useAvailableOptions, + useFallbackMetric, + useValidation, + CUSTOM_METRIC, +} from './agg_utils'; + +type Callback = () => void; + +let testComp: ReactWrapper; + +const TestHook: FunctionComponent<{ callback: Callback }> = ({ callback }) => { + callback(); + return null; +}; + +const testHook = (callback: Callback) => { + testComp = mount(); +}; + +const metricAggs = [ + { + id: '2', + type: { name: 'count' }, + makeLabel() { + return 'count'; + }, + }, + { + id: '3', + type: { name: 'avg' }, + makeLabel() { + return 'avg'; + }, + }, +] as AggConfig[]; + +const incompatibleAggs = [ + { + id: '2', + type: { name: 'top_hits' }, + makeLabel() { + return 'top_hits'; + }, + }, + { + id: '3', + type: { name: 'percentiles' }, + makeLabel() { + return 'percentiles'; + }, + }, +] as AggConfig[]; +const aggFilter = ['!top_hits', '!percentiles']; + +describe('Aggregations utils', () => { + describe('useFallbackMetric', () => { + let setValue: jest.Mock; + beforeEach(() => { + setValue = jest.fn(); + }); + + describe('should not call setValue', () => { + test('if there are no metricAggs', () => { + testHook(() => { + useFallbackMetric(setValue, aggFilter); + }); + + expect(setValue).not.toBeCalled(); + }); + + test('if there is no value', () => { + testHook(() => { + useFallbackMetric(setValue, aggFilter, metricAggs); + }); + + expect(setValue).not.toBeCalled(); + }); + + test('if value is "custom" metric', () => { + testHook(() => { + useFallbackMetric(setValue, aggFilter, metricAggs, 'custom'); + }); + + expect(setValue).not.toBeCalled(); + }); + + test('if value is selected metric is still available', () => { + testHook(() => { + useFallbackMetric(setValue, aggFilter, metricAggs, '2'); + }); + + expect(setValue).not.toBeCalled(); + }); + }); + + describe('should set up a new value if selected metric was removed', () => { + test('called with undefined', () => { + testHook(() => { + useFallbackMetric(setValue, aggFilter, metricAggs, '7'); + }); + + expect(setValue).toBeCalledWith(undefined); + }); + + test('called with fallback value', () => { + testHook(() => { + useFallbackMetric(setValue, aggFilter, metricAggs, '7', '_key'); + }); + + expect(setValue).toBeCalledWith('_key'); + }); + }); + }); + + describe('useAvailableOptions', () => { + test('should create an array with the only custom metric', () => { + let options; + + testHook(() => { + options = useAvailableOptions(aggFilter); + }); + + expect(options).toEqual([CUSTOM_METRIC]); + }); + + test('should include default options', () => { + const DEFAULT_OPTIONS = [{ text: '', value: '', hidden: true }]; + let options; + + testHook(() => { + options = useAvailableOptions(aggFilter, [], DEFAULT_OPTIONS); + }); + + expect(options).toEqual([CUSTOM_METRIC, ...DEFAULT_OPTIONS]); + }); + + test('should create an array with enabled metrics in appropriate format', () => { + let options; + + testHook(() => { + options = useAvailableOptions(aggFilter, metricAggs); + }); + + expect(options).toEqual([ + { text: expect.any(String), value: '2', disabled: false }, + { text: expect.any(String), value: '3', disabled: false }, + CUSTOM_METRIC, + ]); + }); + + test('should create an array with disabled metrics in appropriate format', () => { + let options; + + testHook(() => { + options = useAvailableOptions(aggFilter, incompatibleAggs); + }); + + expect(options).toEqual([ + { text: expect.any(String), value: '2', disabled: true }, + { text: expect.any(String), value: '3', disabled: true }, + CUSTOM_METRIC, + ]); + }); + }); + + describe('useValidation', () => { + let setValidity: jest.Mock; + beforeEach(() => { + setValidity = jest.fn(); + }); + + test('should call setValidity', () => { + testHook(() => { + useValidation(setValidity, false); + }); + + expect(setValidity).toBeCalledWith(false); + }); + + test('should call setValidity with true on component unmount', () => { + testHook(() => { + useValidation(setValidity, false); + }); + + testComp.unmount(); + + expect(setValidity).lastCalledWith(true); + expect(setValidity).toBeCalledTimes(2); + }); + }); + + describe('safeMakeLabel', () => { + test('should make agg label', () => { + const label = safeMakeLabel(metricAggs[0]); + + expect(label).toBe('count'); + }); + + test('should not fail and return a safety string if makeLabel func is not exist', () => { + const label = safeMakeLabel({} as AggConfig); + + expect(label).toEqual(expect.any(String)); + }); + }); +}); diff --git a/src/legacy/ui/public/vis/editors/default/controls/agg_utils.ts b/src/legacy/ui/public/vis/editors/default/controls/agg_utils.ts new file mode 100644 index 0000000000000..6491ef2e46054 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/controls/agg_utils.ts @@ -0,0 +1,132 @@ +/* + * 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 { useEffect, useCallback, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { AggConfig } from 'ui/agg_types'; + +type AggFilter = string[]; + +const DEFAULT_METRIC = 'custom'; +const CUSTOM_METRIC = { + text: i18n.translate('common.ui.aggTypes.customMetricLabel', { + defaultMessage: 'Custom metric', + }), + value: DEFAULT_METRIC, +}; + +function useCompatibleAggCallback(aggFilter: AggFilter) { + return useCallback(isCompatibleAggregation(aggFilter), [aggFilter]); +} + +/** + * the effect is used to set up a default metric aggregation in case, + * when previously selected metric has been removed + */ +function useFallbackMetric( + setValue: (value?: string) => void, + aggFilter: AggFilter, + metricAggs?: AggConfig[], + value?: string, + fallbackValue?: string +) { + const isCompatibleAgg = useCompatibleAggCallback(aggFilter); + + useEffect(() => { + if (metricAggs && value && value !== DEFAULT_METRIC) { + // ensure that metric is set to a valid agg + const respAgg = metricAggs + .filter(isCompatibleAgg) + .find(aggregation => aggregation.id === value); + + if (!respAgg) { + setValue(fallbackValue); + } + } + }, [setValue, isCompatibleAgg, metricAggs, value, fallbackValue]); +} + +/** + * this makes an array of available options in appropriate format for EuiSelect, + * calculates if an option is disabled + */ +function useAvailableOptions( + aggFilter: AggFilter, + metricAggs: AggConfig[] = [], + defaultOptions: Array<{ text: string; value: string }> = [] +) { + const isCompatibleAgg = useCompatibleAggCallback(aggFilter); + + const options = useMemo( + () => [ + ...metricAggs.map(respAgg => ({ + text: i18n.translate('common.ui.aggTypes.definiteMetricLabel', { + defaultMessage: 'Metric: {metric}', + values: { + metric: safeMakeLabel(respAgg), + }, + }), + value: respAgg.id, + disabled: !isCompatibleAgg(respAgg), + })), + CUSTOM_METRIC, + ...defaultOptions, + ], + [metricAggs, defaultOptions, isCompatibleAgg] + ); + + return options; +} + +/** + * the effect is used to set up the editor form validity + * and reset it if a param has been removed + */ +function useValidation(setValidity: (isValid: boolean) => void, isValid: boolean) { + useEffect(() => { + setValidity(isValid); + + return () => setValidity(true); + }, [isValid]); +} + +function safeMakeLabel(agg: AggConfig): string { + try { + return agg.makeLabel(); + } catch (e) { + return i18n.translate('common.ui.aggTypes.aggNotValidLabel', { + defaultMessage: '- agg not valid -', + }); + } +} + +function isCompatibleAggregation(aggFilter: string[]) { + return (agg: AggConfig) => { + return !aggFilter.includes(`!${agg.type.name}`); + }; +} + +export { + CUSTOM_METRIC, + safeMakeLabel, + isCompatibleAggregation, + useAvailableOptions, + useFallbackMetric, + useValidation, +}; diff --git a/src/legacy/ui/public/vis/editors/default/controls/extended_bounds.test.tsx b/src/legacy/ui/public/vis/editors/default/controls/extended_bounds.test.tsx new file mode 100644 index 0000000000000..9ceab07f76158 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/controls/extended_bounds.test.tsx @@ -0,0 +1,139 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { mount, shallow } from 'enzyme'; + +import { ExtendedBoundsParamEditor, Bounds } from './extended_bounds'; +import { AggParamEditorProps } from '..'; + +describe('ExtendedBoundsParamEditor', () => { + let defaultProps: Partial>; + + beforeEach(() => { + defaultProps = { + setValue: jest.fn(), + setValidity: jest.fn(), + }; + }); + + test('should be rendered with default set of props', () => { + const comp = shallow( + )} /> + ); + + expect(comp).toMatchSnapshot(); + }); + + describe('validation', () => { + test('should not show validation if "showValidation" param is falsy', () => { + const comp = mount( + )} + showValidation={false} + /> + ); + + expect(comp.children().props().isInvalid).toBeFalsy(); + expect(defaultProps.setValidity).toBeCalledWith(false); + }); + + test('should change its validity due to passed props and show error if it is invalid', () => { + const comp = mount( + )} + showValidation={true} + /> + ); + + expect(comp.children().props().error).toEqual(expect.any(String)); + expect(comp.children().props().isInvalid).toBeTruthy(); + expect(defaultProps.setValidity).toBeCalledWith(false); + + // set valid bounds + comp.setProps({ value: { min: 0, max: 10 } }); + + expect(comp.props().error).toBeUndefined(); + expect(comp.children().props().isInvalid).toBeFalsy(); + expect(defaultProps.setValidity).toBeCalledWith(true); + + // set invalid bounds - min > max + comp.setProps({ value: { min: 10, max: 2 } }); + + expect(comp.children().props().error).toEqual(expect.any(String)); + expect(comp.children().props().isInvalid).toBeTruthy(); + expect(defaultProps.setValidity).toBeCalledWith(false); + }); + + test('should set valid state after removing from the DOM tree', () => { + const comp = mount( + )} + showValidation={true} + /> + ); + + expect(defaultProps.setValidity).toBeCalledWith(false); + + comp.unmount(); + + expect(defaultProps.setValidity).lastCalledWith(true); + expect(defaultProps.setValidity).toBeCalledTimes(2); + }); + }); + + describe('handle changes', () => { + test('should set numeric "min" or an empty string on change event', () => { + const comp = mount( + )} + showValidation={true} + /> + ); + + const minBound = comp.find('input').first(); + minBound.simulate('change', { target: { value: '2' } }); + + expect(defaultProps.setValue).lastCalledWith({ min: 2 }); + + minBound.simulate('change', { target: { value: '' } }); + + expect(defaultProps.setValue).lastCalledWith({ min: '' }); + }); + + test('should set numeric "max" or an empty string on change event', () => { + const comp = mount( + )} + value={{ min: 10, max: '' }} + showValidation={true} + /> + ); + + const maxBound = comp.find('input').last(); + maxBound.simulate('change', { target: { value: '30' } }); + + expect(defaultProps.setValue).toBeCalledWith({ min: 10, max: 30 }); + + maxBound.simulate('change', { target: { value: '' } }); + + expect(defaultProps.setValue).lastCalledWith({ min: 10, max: '' }); + }); + }); +}); diff --git a/src/legacy/ui/public/vis/editors/default/controls/extended_bounds.tsx b/src/legacy/ui/public/vis/editors/default/controls/extended_bounds.tsx index dba8c2f871352..c933261cb45e6 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/extended_bounds.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/extended_bounds.tsx @@ -17,14 +17,15 @@ * under the License. */ -import React, { useEffect, ChangeEvent } from 'react'; +import React, { ChangeEvent } from 'react'; import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isUndefined } from 'lodash'; +import { useValidation } from './agg_utils'; import { AggParamEditorProps } from '..'; -interface Bounds { +export interface Bounds { min: number | ''; max: number | ''; } @@ -61,11 +62,7 @@ function ExtendedBoundsParamEditor({ }); } - useEffect(() => { - setValidity(isValid); - - return () => setValidity(true); - }, [isValid]); + useValidation(setValidity, isValid); const handleChange = (ev: ChangeEvent, name: string) => { setValue({ diff --git a/src/legacy/ui/public/vis/editors/default/controls/metric_agg.test.tsx b/src/legacy/ui/public/vis/editors/default/controls/metric_agg.test.tsx new file mode 100644 index 0000000000000..24c506ca31738 --- /dev/null +++ b/src/legacy/ui/public/vis/editors/default/controls/metric_agg.test.tsx @@ -0,0 +1,148 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { mount, shallow } from 'enzyme'; + +import { AggConfig } from 'ui/agg_types'; +import { DEFAULT_OPTIONS, aggFilter, MetricAggParamEditor } from './metric_agg'; +import { AggParamEditorProps } from '..'; + +jest.mock('./agg_utils', () => ({ + useAvailableOptions: jest.fn((aggFilterArray, filteredMetrics, defaultOptions) => [ + ...filteredMetrics.map(({ id, type }: { id: string; type: { name: string } }) => ({ + text: type.name, + value: id, + })), + ...defaultOptions, + ]), + useFallbackMetric: jest.fn(), + useValidation: jest.fn(), +})); + +import { useAvailableOptions, useFallbackMetric, useValidation } from './agg_utils'; + +const agg = { + id: '1', + type: { name: 'cumulative_sum' }, + makeLabel() { + return 'cumulative_sum'; + }, +} as AggConfig; + +const metricAggs = [ + agg, + { + id: '2', + type: { name: 'count' }, + makeLabel() { + return 'count'; + }, + }, + { + id: '3', + type: { name: 'avg' }, + makeLabel() { + return 'avg'; + }, + }, + { + id: '4', + type: { name: 'max' }, + makeLabel() { + return 'max'; + }, + }, +] as AggConfig[]; + +describe('MetricAggParamEditor', () => { + let defaultProps: Partial>; + + beforeEach(() => { + defaultProps = { + agg, + showValidation: false, + setValue: jest.fn(), + setValidity: jest.fn(), + }; + }); + + test('should be rendered with default set of props', () => { + const comp = shallow( + )} /> + ); + + expect(comp).toMatchSnapshot(); + }); + + test('should call custom hooks', () => { + shallow( + )} value="custom" /> + ); + + expect(useFallbackMetric).toHaveBeenCalledWith(defaultProps.setValue, aggFilter, [], 'custom'); + expect(useValidation).toHaveBeenCalledWith(defaultProps.setValidity, true); + expect(useAvailableOptions).toHaveBeenCalledWith(aggFilter, [], DEFAULT_OPTIONS); + }); + + test('should filter self aggregation from available options', () => { + const comp = shallow( + )} + value="custom" + metricAggs={[agg]} + /> + ); + + expect(comp.find('EuiSelect').props()).toHaveProperty('options', [...DEFAULT_OPTIONS]); + expect(useFallbackMetric).toHaveBeenCalledWith( + defaultProps.setValue, + aggFilter, + [agg], + 'custom' + ); + }); + + test('should be valid/invalid if value is defined/undefined', () => { + const comp = mount( + )} value="custom" /> + ); + + expect(comp.children().props()).toHaveProperty('isInvalid', false); + expect(useValidation).lastCalledWith(defaultProps.setValidity, true); + + comp.setProps({ value: undefined, showValidation: true }); + + expect(comp.children().props()).toHaveProperty('isInvalid', true); + expect(useValidation).lastCalledWith(defaultProps.setValidity, false); + }); + + test('should set new value into the model on change', () => { + const comp = mount( + )} + value="custom" + metricAggs={metricAggs} + /> + ); + + comp.find('select').simulate('change', { target: { value: '2' } }); + expect(defaultProps.setValue).lastCalledWith('2'); + }); +}); diff --git a/src/legacy/ui/public/vis/editors/default/controls/metric_agg.tsx b/src/legacy/ui/public/vis/editors/default/controls/metric_agg.tsx index 82cf9af097805..9d25a02606ed2 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/metric_agg.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/metric_agg.tsx @@ -17,15 +17,15 @@ * under the License. */ -import React, { useEffect } from 'react'; +import React, { useMemo, useCallback } from 'react'; import { EuiFormRow, EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { safeMakeLabel, isCompatibleAggregation } from '../../../../agg_types/agg_utils'; +import { useAvailableOptions, useFallbackMetric, useValidation } from './agg_utils'; import { AggParamEditorProps } from '..'; const aggFilter = ['!top_hits', '!percentiles', '!percentile_ranks', '!median', '!std_dev']; -const isCompatibleAgg = isCompatibleAggregation(aggFilter); const EMPTY_VALUE = 'EMPTY_VALUE'; +const DEFAULT_OPTIONS = [{ text: '', value: EMPTY_VALUE, hidden: true }]; function MetricAggParamEditor({ agg, @@ -34,71 +34,32 @@ function MetricAggParamEditor({ setValue, setValidity, setTouched, - metricAggs, + metricAggs = [], }: AggParamEditorProps) { const label = i18n.translate('common.ui.aggTypes.metricLabel', { defaultMessage: 'Metric', }); const isValid = !!value; - useEffect(() => { - setValidity(isValid); - }, [isValid]); + useValidation(setValidity, isValid); + useFallbackMetric(setValue, aggFilter, metricAggs, value); - useEffect(() => { - if (metricAggs && value && value !== 'custom') { - // ensure that metricAgg is set to a valid agg - const respAgg = metricAggs - .filter(isCompatibleAgg) - .find(aggregation => aggregation.id === value); - - if (!respAgg) { - setValue(); - } - } - }, [metricAggs]); - - const options = metricAggs - ? metricAggs - .filter(respAgg => respAgg.type.name !== agg.type.name) - .map(respAgg => ({ - text: i18n.translate('common.ui.aggTypes.definiteMetricLabel', { - defaultMessage: 'Metric: {safeMakeLabel}', - values: { - safeMakeLabel: safeMakeLabel(respAgg), - }, - }), - value: respAgg.id, - disabled: !isCompatibleAgg(respAgg), - })) - : []; - - options.push({ - text: i18n.translate('common.ui.aggTypes.customMetricLabel', { - defaultMessage: 'Custom metric', - }), - value: 'custom', - disabled: false, - }); - - if (!value) { - options.unshift({ text: '', value: EMPTY_VALUE, disabled: false }); - } + const filteredMetrics = useMemo( + () => metricAggs.filter(respAgg => respAgg.type.name !== agg.type.name), + [metricAggs, agg.type.name] + ); + const options = useAvailableOptions(aggFilter, filteredMetrics, DEFAULT_OPTIONS); + const onChange = useCallback(ev => setValue(ev.target.value), [setValue]); return ( - + setValue(ev.target.value)} - fullWidth={true} - compressed - isInvalid={showValidation ? !isValid : false} + onChange={onChange} + isInvalid={showValidation && !isValid} onBlur={setTouched} data-test-subj={`visEditorSubAggMetric${agg.id}`} /> @@ -106,4 +67,4 @@ function MetricAggParamEditor({ ); } -export { MetricAggParamEditor }; +export { DEFAULT_OPTIONS, aggFilter, MetricAggParamEditor }; diff --git a/src/legacy/ui/public/vis/editors/default/controls/number_interval.tsx b/src/legacy/ui/public/vis/editors/default/controls/number_interval.tsx index 33f92c337648c..1084d6e8212e2 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/number_interval.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/number_interval.tsx @@ -18,7 +18,7 @@ */ import { get } from 'lodash'; -import React, { useEffect } from 'react'; +import React, { useEffect, useCallback } from 'react'; import { EuiFieldNumber, EuiFormRow, EuiIconTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -62,17 +62,18 @@ function NumberIntervalParamEditor({ setValidity(isValid); }, [isValid]); - const onChange = (event: React.ChangeEvent) => { - const numberValue = parseFloat(event.target.value); - setValue(isNaN(numberValue) ? undefined : numberValue); - }; + const onChange = useCallback( + ({ target }: React.ChangeEvent) => + setValue(isNaN(target.valueAsNumber) ? undefined : target.valueAsNumber), + [setValue] + ); return ( { - setValidity(isValid); - }, [isValid]); + useValidation(setValidity, isValid); useEffect(() => { // setup the initial value of orderBy if (!value) { - let respAgg = { id: '_key' }; + let respAgg = { id: DEFAULT_VALUE }; if (metricAggs) { respAgg = metricAggs.filter(isCompatibleAgg)[0] || respAgg; @@ -70,61 +82,19 @@ function OrderByParamEditor({ } }, []); - useEffect(() => { - if (metricAggs && value && value !== 'custom') { - // ensure that orderBy is set to a valid agg - const respAgg = metricAggs - .filter(isCompatibleAgg) - .find(aggregation => aggregation.id === value); - - if (!respAgg) { - setValue('_key'); - } - } - }, [metricAggs]); - - const defaultOptions = [ - { - text: i18n.translate('common.ui.aggTypes.orderAgg.customMetricLabel', { - defaultMessage: 'Custom metric', - }), - value: 'custom', - }, - { - text: i18n.translate('common.ui.aggTypes.orderAgg.alphabeticalLabel', { - defaultMessage: 'Alphabetical', - }), - value: '_key', - }, - ]; + useFallbackMetric(setValue, aggFilter, metricAggs, value, DEFAULT_VALUE); - const options = metricAggs - ? metricAggs.map(respAgg => ({ - text: i18n.translate('common.ui.aggTypes.orderAgg.metricLabel', { - defaultMessage: 'Metric: {metric}', - values: { - metric: safeMakeLabel(respAgg), - }, - }), - value: respAgg.id, - disabled: !isCompatibleAgg(respAgg), - })) - : []; + const options = useAvailableOptions(aggFilter, metricAggs, DEFAULT_OPTIONS); return ( - + setValue(ev.target.value)} fullWidth={true} compressed - isInvalid={showValidation ? !isValid : false} + isInvalid={showValidation && !isValid} onBlur={setTouched} data-test-subj={`visEditorOrderBy${agg.id}`} /> diff --git a/src/legacy/ui/public/vis/editors/default/controls/radius_ratio_option.tsx b/src/legacy/ui/public/vis/editors/default/controls/radius_ratio_option.tsx index 595ab6e0faf46..d9e0abc0cce59 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/radius_ratio_option.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/radius_ratio_option.tsx @@ -26,7 +26,7 @@ import { AggControlProps } from './agg_control_props'; const DEFAULT_VALUE = 50; const PARAM_NAME = 'radiusRatio'; -function RadiusRatioOptionControl({ editorStateParams, setValue }: AggControlProps) { +function RadiusRatioOptionControl({ editorStateParams, setValue }: AggControlProps) { const label = ( <> ) { +function RowsOrColumnsControl({ aggParams, setValue }: AggControlProps) { const idSelected = `visEditorSplitBy__${aggParams.row}`; const options = [ { diff --git a/src/legacy/ui/public/vis/editors/default/schemas.d.ts b/src/legacy/ui/public/vis/editors/default/schemas.d.ts index 905a7e8bc11b6..236421f30fb09 100644 --- a/src/legacy/ui/public/vis/editors/default/schemas.d.ts +++ b/src/legacy/ui/public/vis/editors/default/schemas.d.ts @@ -19,6 +19,7 @@ import { AggParam } from '../../../agg_types'; import { AggGroupNames } from './agg_groups'; +import { AggControlProps } from './controls/agg_control_props'; export interface Schema { aggFilter: string | string[]; @@ -32,5 +33,5 @@ export interface Schema { defaults: unknown; hideCustomLabel?: boolean; mustBeFirst?: boolean; - editorComponent?: any; + editorComponent?: React.ComponentType; } diff --git a/src/legacy/ui/public/vis/editors/default/vis_options.js b/src/legacy/ui/public/vis/editors/default/vis_options.js index 0562a3ed0ca29..6425a03ed9a74 100644 --- a/src/legacy/ui/public/vis/editors/default/vis_options.js +++ b/src/legacy/ui/public/vis/editors/default/vis_options.js @@ -20,7 +20,7 @@ import { wrapInI18nContext } from 'ui/i18n'; import { uiModules } from '../../../modules'; import { VisOptionsReactWrapper } from './vis_options_react_wrapper'; -import { safeMakeLabel } from 'ui/agg_types/agg_utils'; +import { safeMakeLabel } from './controls/agg_utils'; /** * This directive sort of "transcludes" in whatever template you pass in via the `editor` attribute. diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index bb26a1cf2b1ed..005f44a3842de 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -123,7 +123,7 @@ "common.ui.aggTypes.dateRanges.addRangeButtonLabel": "範囲を追加", "common.ui.aggTypes.dateRanges.fromColumnLabel": "From", "common.ui.aggTypes.dateRanges.toColumnLabel": "To", - "common.ui.aggTypes.definiteMetricLabel": "メトリック: {safeMakeLabel}", + "common.ui.aggTypes.definiteMetricLabel": "メトリック: {metric}", "common.ui.aggTypes.dropPartialBucketsLabel": "不完全なバケットをドロップ", "common.ui.aggTypes.dropPartialBucketsTooltip": "時間範囲外にわたるバケットを削除してヒストグラムが不完全なバケットで開始・終了しないようにします。", "common.ui.aggTypes.extendedBounds.errorMessage": "最低値は最大値以下でなければなりません。", @@ -228,8 +228,6 @@ "common.ui.aggTypes.onlyRequestDataAroundMapExtentLabel": "マップ範囲のデータのみリクエストしてください", "common.ui.aggTypes.onlyRequestDataAroundMapExtentTooltip": "geo_bounding_box フィルター集約を適用して、襟付きのマップビューボックスにサブジェクトエリアを絞ります", "common.ui.aggTypes.orderAgg.alphabeticalLabel": "アルファベット順", - "common.ui.aggTypes.orderAgg.customMetricLabel": "カスタムメトリック", - "common.ui.aggTypes.orderAgg.metricLabel": "メトリック: {metric}", "common.ui.aggTypes.orderAgg.orderByLabel": "並び順", "common.ui.aggTypes.orderLabel": "順序", "common.ui.aggTypes.otherBucket.groupValuesLabel": "他の値を別のバケットにまとめる", @@ -10508,4 +10506,4 @@ "xpack.fileUpload.fileParser.errorReadingFile": "ファイルの読み込み中にエラーが発生しました", "xpack.fileUpload.fileParser.noFileProvided": "エラー、ファイルが提供されていません" } -} \ No newline at end of file +} diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 7854d7b37ef9a..1d90ed2928fd7 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -123,7 +123,7 @@ "common.ui.aggTypes.dateRanges.addRangeButtonLabel": "添加范围", "common.ui.aggTypes.dateRanges.fromColumnLabel": "从", "common.ui.aggTypes.dateRanges.toColumnLabel": "到", - "common.ui.aggTypes.definiteMetricLabel": "指标:{safeMakeLabel}", + "common.ui.aggTypes.definiteMetricLabel": "指标:{metric}", "common.ui.aggTypes.dropPartialBucketsLabel": "丢弃部分存储桶", "common.ui.aggTypes.dropPartialBucketsTooltip": "移除超出时间范围的存储桶,以便直方图不以不完整的存储桶开始和结束。", "common.ui.aggTypes.extendedBounds.errorMessage": "最小值应小于或等于最大值。", @@ -228,8 +228,6 @@ "common.ui.aggTypes.onlyRequestDataAroundMapExtentLabel": "仅请求地图范围的数据", "common.ui.aggTypes.onlyRequestDataAroundMapExtentTooltip": "应用 geo_bounding_box 筛选聚合以使用领口将主题区域缩小到地图视图框", "common.ui.aggTypes.orderAgg.alphabeticalLabel": "按字母顺序", - "common.ui.aggTypes.orderAgg.customMetricLabel": "定制指标", - "common.ui.aggTypes.orderAgg.metricLabel": "指标:{metric}", "common.ui.aggTypes.orderAgg.orderByLabel": "排序依据", "common.ui.aggTypes.orderLabel": "顺序", "common.ui.aggTypes.otherBucket.groupValuesLabel": "在单独的存储桶中对其他值分组", @@ -10664,4 +10662,4 @@ "xpack.fileUpload.fileParser.errorReadingFile": "读取文件时出错", "xpack.fileUpload.fileParser.noFileProvided": "错误,未提供任何文件" } -} \ No newline at end of file +} From 316219484ff177a59f094421c8819875f325abdc Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Wed, 16 Oct 2019 15:20:45 +0100 Subject: [PATCH 31/48] [Logs UI] Add "Analyze in ML" buttons (#48268) --- .../logs/log_analysis/log_analysis_jobs.tsx | 9 +- .../logs/analysis/page_results_content.tsx | 4 +- .../sections/analyze_in_ml_button.tsx | 101 ++++++++++++++++++ .../sections/anomalies/expanded_row.tsx | 12 ++- .../analysis/sections/anomalies/index.tsx | 19 ++-- .../analysis/sections/anomalies/table.tsx | 4 +- .../public/utils/use_kibana_injected_var.ts | 1 - 7 files changed, 135 insertions(+), 15 deletions(-) create mode 100644 x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/analyze_in_ml_button.tsx diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx index 83d4760259d9b..aa66febc5b7c4 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx @@ -8,7 +8,7 @@ import createContainer from 'constate-latest'; import { useMemo, useCallback, useEffect } from 'react'; import { callGetMlModuleAPI } from './api/ml_get_module'; -import { bucketSpan } from '../../../../common/log_analysis'; +import { bucketSpan, getJobId } from '../../../../common/log_analysis'; import { useTrackedPromise } from '../../../utils/use_tracked_promise'; import { callJobsSummaryAPI } from './api/ml_get_jobs_summary_api'; import { callSetupMlModuleAPI, SetupMlModuleResponsePayload } from './api/ml_setup_module_api'; @@ -138,6 +138,12 @@ export const useLogAnalysisJobs = ({ fetchModuleDefinition(); }, [fetchModuleDefinition]); + const jobIds = useMemo(() => { + return { + 'log-entry-rate': getJobId(spaceId, sourceId, 'log-entry-rate'), + }; + }, [sourceId, spaceId]); + return { fetchJobStatus, isLoadingSetupStatus, @@ -149,6 +155,7 @@ export const useLogAnalysisJobs = ({ viewSetupForReconfiguration, viewSetupForUpdate, viewResults, + jobIds, }; }; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx index 33037aac9afe5..e846d4e9e4ac5 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx @@ -19,9 +19,9 @@ import numeral from '@elastic/numeral'; import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment'; import React, { useCallback, useContext, useMemo, useState } from 'react'; +import euiStyled from '../../../../../../common/eui_styled_components'; import { TimeRange } from '../../../../common/http_api/shared/time_range'; import { bucketSpan } from '../../../../common/log_analysis'; -import euiStyled from '../../../../../../common/eui_styled_components'; import { LoadingPage } from '../../../components/loading_page'; import { LogAnalysisJobs, @@ -134,6 +134,7 @@ export const AnalysisResultsContent = ({ setupStatus, viewSetupForReconfiguration, viewSetupForUpdate, + jobIds, } = useContext(LogAnalysisJobs.Context); useInterval(() => { @@ -214,6 +215,7 @@ export const AnalysisResultsContent = ({ setTimeRange={handleChartTimeRangeChange} setupStatus={setupStatus} timeRange={queryTimeRange} + jobId={jobIds['log-entry-rate']} /> diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/analyze_in_ml_button.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/analyze_in_ml_button.tsx new file mode 100644 index 0000000000000..852ce20e37d41 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/analyze_in_ml_button.tsx @@ -0,0 +1,101 @@ +/* + * 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 url from 'url'; +import { EuiButton } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import chrome from 'ui/chrome'; +import { QueryString } from 'ui/utils/query_string'; +import { encode } from 'rison-node'; +import { TimeRange } from '../../../../../common/http_api/shared/time_range'; + +export const AnalyzeInMlButton: React.FunctionComponent<{ + jobId: string; + partition?: string; + timeRange: TimeRange; +}> = ({ jobId, partition, timeRange }) => { + const pathname = chrome.addBasePath('/app/ml'); + const buttonLabel = ( + + ); + return partition ? ( + + {buttonLabel} + + ) : ( + + {buttonLabel} + + ); +}; + +const getOverallAnomalyExplorerLink = (pathname: string, jobId: string, timeRange: TimeRange) => { + const { from, to } = convertTimeRangeToParams(timeRange); + + const _g = encode({ + ml: { + jobIds: [jobId], + }, + time: { + from, + to, + }, + }); + + const hash = `/explorer?${QueryString.encode({ _g })}`; + + return url.format({ + pathname, + hash, + }); +}; + +const getPartitionSpecificSingleMetricViewerLink = ( + pathname: string, + jobId: string, + partition: string, + timeRange: TimeRange +) => { + const { from, to } = convertTimeRangeToParams(timeRange); + + const _g = encode({ + ml: { + jobIds: [jobId], + }, + time: { + from, + to, + mode: 'absolute', + }, + }); + + const _a = encode({ + mlTimeSeriesExplorer: { + entities: { 'event.dataset': partition }, + }, + }); + + const hash = `/timeseriesexplorer?${QueryString.encode({ _g, _a })}`; + + return url.format({ + pathname, + hash, + }); +}; + +const convertTimeRangeToParams = (timeRange: TimeRange): { from: string; to: string } => { + return { + from: new Date(timeRange.startTime).toISOString(), + to: new Date(timeRange.endTime).toISOString(), + }; +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/expanded_row.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/expanded_row.tsx index a6f2ca71068c2..43cfbb306717d 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/expanded_row.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/expanded_row.tsx @@ -7,7 +7,7 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import numeral from '@elastic/numeral'; -import { EuiFlexGroup, EuiFlexItem, EuiStat } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiSpacer } from '@elastic/eui'; import { AnomaliesChart } from './chart'; import { GetLogEntryRateSuccessResponsePayload } from '../../../../../../common/http_api/log_analysis/results/log_entry_rate'; import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; @@ -17,6 +17,7 @@ import { formatAnomalyScore, getTotalNumberOfLogEntriesForPartition, } from '../helpers/data_formatters'; +import { AnalyzeInMlButton } from '../analyze_in_ml_button'; export const AnomaliesTableExpandedRow: React.FunctionComponent<{ partitionId: string; @@ -24,7 +25,8 @@ export const AnomaliesTableExpandedRow: React.FunctionComponent<{ results: GetLogEntryRateSuccessResponsePayload['data']; setTimeRange: (timeRange: TimeRange) => void; timeRange: TimeRange; -}> = ({ results, timeRange, setTimeRange, topAnomalyScore, partitionId }) => { + jobId: string; +}> = ({ results, timeRange, setTimeRange, topAnomalyScore, partitionId, jobId }) => { const logEntryRateSeries = useMemo( () => results && results.histogramBuckets @@ -83,6 +85,12 @@ export const AnomaliesTableExpandedRow: React.FunctionComponent<{ )} reverse /> + + + + + + ); diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/index.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/index.tsx index 1bba8995f0c76..5aa5891d7981d 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/index.tsx @@ -5,7 +5,6 @@ */ import { - EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, @@ -16,7 +15,6 @@ import { } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import React, { useMemo } from 'react'; import euiStyled from '../../../../../../../../common/eui_styled_components'; @@ -32,6 +30,7 @@ import { import { AnomaliesChart } from './chart'; import { AnomaliesTable } from './table'; import { LogAnalysisJobProblemIndicator } from '../../../../../components/logging/log_analysis_job_status'; +import { AnalyzeInMlButton } from '../analyze_in_ml_button'; export const AnomaliesResults: React.FunctionComponent<{ isLoading: boolean; @@ -42,6 +41,7 @@ export const AnomaliesResults: React.FunctionComponent<{ timeRange: TimeRange; viewSetupForReconfiguration: () => void; viewSetupForUpdate: () => void; + jobId: string; }> = ({ isLoading, jobStatus, @@ -51,6 +51,7 @@ export const AnomaliesResults: React.FunctionComponent<{ timeRange, viewSetupForReconfiguration, viewSetupForUpdate, + jobId, }) => { const title = i18n.translate('xpack.infra.logs.analysis.anomaliesSectionTitle', { defaultMessage: 'Anomalies', @@ -105,12 +106,7 @@ export const AnomaliesResults: React.FunctionComponent<{ - - - + @@ -193,7 +189,12 @@ export const AnomaliesResults: React.FunctionComponent<{ - + )} diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/table.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/table.tsx index 2a8ac44d09083..88ffcae6e9dea 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/table.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/table.tsx @@ -52,7 +52,8 @@ export const AnomaliesTable: React.FunctionComponent<{ results: GetLogEntryRateSuccessResponsePayload['data']; setTimeRange: (timeRange: TimeRange) => void; timeRange: TimeRange; -}> = ({ results, timeRange, setTimeRange }) => { + jobId: string; +}> = ({ results, timeRange, setTimeRange, jobId }) => { const tableItems: TableItem[] = useMemo(() => { return Object.entries(getTopAnomalyScoresByPartition(results)).map(([key, value]) => { return { @@ -112,6 +113,7 @@ export const AnomaliesTable: React.FunctionComponent<{ topAnomalyScore={item.topAnomalyScore} setTimeRange={setTimeRange} timeRange={timeRange} + jobId={jobId} /> ), }; diff --git a/x-pack/legacy/plugins/infra/public/utils/use_kibana_injected_var.ts b/x-pack/legacy/plugins/infra/public/utils/use_kibana_injected_var.ts index bcd36f72bef07..4afcb4fd88430 100644 --- a/x-pack/legacy/plugins/infra/public/utils/use_kibana_injected_var.ts +++ b/x-pack/legacy/plugins/infra/public/utils/use_kibana_injected_var.ts @@ -14,6 +14,5 @@ import { npSetup } from 'ui/new_platform'; */ export const useKibanaInjectedVar = (name: string, defaultValue?: unknown) => { const injectedMetadata = npSetup.core.injectedMetadata; - return injectedMetadata.getInjectedVar(name, defaultValue); }; From c7abd93c0111e50f89f9c23f9b863eb7eaf257b2 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 16 Oct 2019 07:27:28 -0700 Subject: [PATCH 32/48] [ML] Fix analytics starting state. (#48373) - Fixes the missing starting state for analytics jobs. - Fixes checks if a analytics job is running, for example fixes an issue where the Delete-button was available for a running job in the analytics job list. --- .../analytics_list/action_delete.tsx | 7 ++----- .../components/analytics_list/actions.tsx | 2 +- .../analytics_list/analytics_list.tsx | 8 +------- .../components/analytics_list/columns.tsx | 11 +++++++---- .../components/analytics_list/common.test.ts | 18 ++++++++++++++---- .../components/analytics_list/common.ts | 18 ++++++++++++++---- .../analytics_service/delete_analytics.ts | 10 +++------- .../analytics_service/stop_analytics.ts | 4 ++-- .../analytics_panel/analytics_stats_bar.tsx | 14 ++++++-------- 9 files changed, 50 insertions(+), 42 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx index e0e45eddfe171..75841b52521bd 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx @@ -21,17 +21,14 @@ import { createPermissionFailureMessage, } from '../../../../../privilege/check_privilege'; -import { DataFrameAnalyticsListRow, DATA_FRAME_TASK_STATE } from './common'; +import { isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common'; interface DeleteActionProps { item: DataFrameAnalyticsListRow; } export const DeleteAction: FC = ({ item }) => { - const disabled = - item.stats.state === DATA_FRAME_TASK_STATE.STARTED || - item.stats.state === DATA_FRAME_TASK_STATE.ANALYZING || - item.stats.state === DATA_FRAME_TASK_STATE.REINDEXING; + const disabled = isDataFrameAnalyticsRunning(item.stats.state); const canDeleteDataFrameAnalytics: boolean = checkPermission('canDeleteDataFrameAnalytics'); diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx index 59496f00d8fe4..7ad86c5d380ca 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx @@ -56,7 +56,7 @@ export const getActions = () => { AnalyticsViewAction, { render: (item: DataFrameAnalyticsListRow) => { - if (!isDataFrameAnalyticsRunning(item.stats)) { + if (!isDataFrameAnalyticsRunning(item.stats.state)) { return ; } diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 45f53691eab8f..8cc1719ffea8c 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -8,12 +8,7 @@ import React, { Fragment, FC, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { - // EuiBadge, - EuiButtonEmpty, - EuiCallOut, - EuiEmptyPrompt, -} from '@elastic/eui'; +import { EuiButtonEmpty, EuiCallOut, EuiEmptyPrompt } from '@elastic/eui'; import { DataFrameAnalyticsId, useRefreshAnalyticsList } from '../../../../common'; import { checkPermission } from '../../../../../privilege/check_privilege'; @@ -24,7 +19,6 @@ import { DataFrameAnalyticsListRow, ItemIdToExpandedRowMap, DATA_FRAME_TASK_STATE, - // DATA_FRAME_MODE, Query, Clause, } from './common'; diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx index 07adff6c3e415..19d7b4db36c9b 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/columns.tsx @@ -20,10 +20,12 @@ import { import { getAnalysisType, DataFrameAnalyticsId } from '../../../../common'; import { getDataFrameAnalyticsProgress, + isDataFrameAnalyticsFailed, + isDataFrameAnalyticsRunning, + isDataFrameAnalyticsStopped, DataFrameAnalyticsListColumn, DataFrameAnalyticsListRow, DataFrameAnalyticsStats, - DATA_FRAME_TASK_STATE, } from './common'; import { getActions } from './actions'; @@ -32,6 +34,7 @@ enum TASK_STATE_COLOR { failed = 'danger', reindexing = 'primary', started = 'primary', + starting = 'primary', stopped = 'hollow', } @@ -41,7 +44,7 @@ export const getTaskStateBadge = ( ) => { const color = TASK_STATE_COLOR[state]; - if (state === DATA_FRAME_TASK_STATE.FAILED && reason !== undefined) { + if (isDataFrameAnalyticsFailed(state) && reason !== undefined) { return ( @@ -91,10 +94,10 @@ export const progressColumn = { {!isBatchTransform && ( - {item.stats.state === DATA_FRAME_TASK_STATE.STARTED && ( + {isDataFrameAnalyticsRunning(item.stats.state) && ( )} - {item.stats.state === DATA_FRAME_TASK_STATE.STOPPED && ( + {isDataFrameAnalyticsStopped(item.stats.state) && ( )} diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/common.test.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/common.test.ts index 3da57a24cfcdf..30f87ad8a375b 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/common.test.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/common.test.ts @@ -11,10 +11,12 @@ import { isCompletedAnalyticsJob, isDataFrameAnalyticsRunning, isDataFrameAnalyticsStats, + DataFrameAnalyticsStats, + DATA_FRAME_TASK_STATE, } from './common'; -const completedJob = StatsMock.data_frame_analytics[0]; -const runningJob = StatsMock.data_frame_analytics[1]; +const completedJob = StatsMock.data_frame_analytics[0] as DataFrameAnalyticsStats; +const runningJob = StatsMock.data_frame_analytics[1] as DataFrameAnalyticsStats; describe('Data Frame Analytics: common utils', () => { test('isCompletedAnalyticsJob()', () => { @@ -23,8 +25,16 @@ describe('Data Frame Analytics: common utils', () => { }); test('isDataFrameAnalyticsRunning()', () => { - expect(isDataFrameAnalyticsRunning(completedJob)).toBe(false); - expect(isDataFrameAnalyticsRunning(runningJob)).toBe(true); + expect(isDataFrameAnalyticsRunning(completedJob.state)).toBe(false); + expect(isDataFrameAnalyticsRunning(runningJob.state)).toBe(true); + runningJob.state = DATA_FRAME_TASK_STATE.STARTED; + expect(isDataFrameAnalyticsRunning(runningJob.state)).toBe(true); + runningJob.state = DATA_FRAME_TASK_STATE.STARTING; + expect(isDataFrameAnalyticsRunning(runningJob.state)).toBe(true); + runningJob.state = DATA_FRAME_TASK_STATE.REINDEXING; + expect(isDataFrameAnalyticsRunning(runningJob.state)).toBe(true); + runningJob.state = DATA_FRAME_TASK_STATE.FAILED; + expect(isDataFrameAnalyticsRunning(runningJob.state)).toBe(false); }); test('isDataFrameAnalyticsStats()', () => { diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts index fa2f6719bcb8e..99d1889b265ed 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts @@ -12,6 +12,7 @@ export enum DATA_FRAME_TASK_STATE { FAILED = 'failed', REINDEXING = 'reindexing', STARTED = 'started', + STARTING = 'starting', STOPPED = 'stopped', } @@ -54,14 +55,23 @@ export interface DataFrameAnalyticsStats { state: DATA_FRAME_TASK_STATE; } -export function isDataFrameAnalyticsRunning(stats: DataFrameAnalyticsStats) { +export function isDataFrameAnalyticsFailed(state: DATA_FRAME_TASK_STATE) { + return state === DATA_FRAME_TASK_STATE.FAILED; +} + +export function isDataFrameAnalyticsRunning(state: DATA_FRAME_TASK_STATE) { return ( - stats.state === DATA_FRAME_TASK_STATE.ANALYZING || - stats.state === DATA_FRAME_TASK_STATE.STARTED || - stats.state === DATA_FRAME_TASK_STATE.REINDEXING + state === DATA_FRAME_TASK_STATE.ANALYZING || + state === DATA_FRAME_TASK_STATE.REINDEXING || + state === DATA_FRAME_TASK_STATE.STARTED || + state === DATA_FRAME_TASK_STATE.STARTING ); } +export function isDataFrameAnalyticsStopped(state: DATA_FRAME_TASK_STATE) { + return state === DATA_FRAME_TASK_STATE.STOPPED; +} + export function isDataFrameAnalyticsStats(arg: any): arg is DataFrameAnalyticsStats { return ( typeof arg === 'object' && diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts index dde4f8efc899c..fb366b517f0b7 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts @@ -11,18 +11,14 @@ import { ml } from '../../../../../services/ml_api_service'; import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../../common'; import { - DATA_FRAME_TASK_STATE, + isDataFrameAnalyticsFailed, DataFrameAnalyticsListRow, } from '../../components/analytics_list/common'; export const deleteAnalytics = async (d: DataFrameAnalyticsListRow) => { try { - if (d.stats.state === DATA_FRAME_TASK_STATE.FAILED) { - await ml.dataFrameAnalytics.stopDataFrameAnalytics( - d.config.id, - d.stats.state === DATA_FRAME_TASK_STATE.FAILED, - true - ); + if (isDataFrameAnalyticsFailed(d.stats.state)) { + await ml.dataFrameAnalytics.stopDataFrameAnalytics(d.config.id, true, true); } await ml.dataFrameAnalytics.deleteDataFrameAnalytics(d.config.id); toastNotifications.addSuccess( diff --git a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts index 44286c8b1f7dd..84d1835c6e1e3 100644 --- a/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts +++ b/x-pack/legacy/plugins/ml/public/data_frame_analytics/pages/analytics_management/services/analytics_service/stop_analytics.ts @@ -11,7 +11,7 @@ import { ml } from '../../../../../services/ml_api_service'; import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../../common'; import { - DATA_FRAME_TASK_STATE, + isDataFrameAnalyticsFailed, DataFrameAnalyticsListRow, } from '../../components/analytics_list/common'; @@ -19,7 +19,7 @@ export const stopAnalytics = async (d: DataFrameAnalyticsListRow) => { try { await ml.dataFrameAnalytics.stopDataFrameAnalytics( d.config.id, - d.stats.state === DATA_FRAME_TASK_STATE.FAILED, + isDataFrameAnalyticsFailed(d.stats.state), true ); toastNotifications.addSuccess( diff --git a/x-pack/legacy/plugins/ml/public/overview/components/analytics_panel/analytics_stats_bar.tsx b/x-pack/legacy/plugins/ml/public/overview/components/analytics_panel/analytics_stats_bar.tsx index 63d363e2f5a4d..19a907ff8e899 100644 --- a/x-pack/legacy/plugins/ml/public/overview/components/analytics_panel/analytics_stats_bar.tsx +++ b/x-pack/legacy/plugins/ml/public/overview/components/analytics_panel/analytics_stats_bar.tsx @@ -8,8 +8,10 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { StatsBar, AnalyticStatsBarStats } from '../../../components/stats_bar'; import { + isDataFrameAnalyticsFailed, + isDataFrameAnalyticsRunning, + isDataFrameAnalyticsStopped, DataFrameAnalyticsListRow, - DATA_FRAME_TASK_STATE, } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; function getAnalyticsStats(analyticsList: any[]) { @@ -53,15 +55,11 @@ function getAnalyticsStats(analyticsList: any[]) { let stoppedJobs = 0; analyticsList.forEach(job => { - if (job.stats.state === DATA_FRAME_TASK_STATE.FAILED) { + if (isDataFrameAnalyticsFailed(job.stats.state)) { failedJobs++; - } else if ( - job.stats.state === DATA_FRAME_TASK_STATE.STARTED || - job.stats.state === DATA_FRAME_TASK_STATE.ANALYZING || - job.stats.state === DATA_FRAME_TASK_STATE.REINDEXING - ) { + } else if (isDataFrameAnalyticsRunning(job.stats.state)) { startedJobs++; - } else if (job.stats.state === DATA_FRAME_TASK_STATE.STOPPED) { + } else if (isDataFrameAnalyticsStopped(job.stats.state)) { stoppedJobs++; } }); From 52f2758167f5284912c397e86e6b715ef8e0a55b Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 16 Oct 2019 15:28:38 +0100 Subject: [PATCH 33/48] prevent Kibana plugin from completing before the Kibana UUID has been obtained (#47741) * fix(kibana-uuid): await kibana uuid management before enabaling routes and plugins * feat(kibana-uuid): use Kibana uuid to identify the Task Manager instance * fix(task-manager): Fixed unused import * feat(kibana-uuid): fail Task Manager startup if no uuid is available --- src/legacy/core_plugins/kibana/index.js | 4 +-- .../plugins/task_manager/task_manager.test.ts | 26 +++++++++++++++++++ .../plugins/task_manager/task_manager.ts | 19 ++++++++------ 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 8b12f71660844..7a47b5d54316f 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -329,9 +329,9 @@ export default function (kibana) { } }, - init: function (server) { + init: async function (server) { // uuid - manageUuid(server); + await manageUuid(server); // routes searchApi(server); scriptsApi(server); diff --git a/x-pack/legacy/plugins/task_manager/task_manager.test.ts b/x-pack/legacy/plugins/task_manager/task_manager.test.ts index ce6bafa5ddd63..2bcfdb2f4d4e3 100644 --- a/x-pack/legacy/plugins/task_manager/task_manager.test.ts +++ b/x-pack/legacy/plugins/task_manager/task_manager.test.ts @@ -25,6 +25,9 @@ describe('TaskManager', () => { poll_interval: 6000000, }, }, + server: { + uuid: 'some-uuid', + }, }; const config = { get: (path: string) => _.get(defaultConfig, path), @@ -43,6 +46,29 @@ describe('TaskManager', () => { afterEach(() => clock.restore()); + test('throws if no valid UUID is available', async () => { + expect(() => { + const configWithoutServerUUID = { + xpack: { + task_manager: { + max_workers: 10, + index: 'foo', + max_attempts: 9, + poll_interval: 6000000, + }, + }, + }; + new TaskManager({ + ...taskManagerOpts, + config: { + get: (path: string) => _.get(configWithoutServerUUID, path), + }, + }); + }).toThrowErrorMatchingInlineSnapshot( + `"TaskManager is unable to start as Kibana has no valid UUID assigned to it."` + ); + }); + test('allows and queues scheduling tasks before starting', async () => { const client = new TaskManager(taskManagerOpts); client.registerTaskDefinitions({ diff --git a/x-pack/legacy/plugins/task_manager/task_manager.ts b/x-pack/legacy/plugins/task_manager/task_manager.ts index 11cfc2ff8dafa..41b08cda98f21 100644 --- a/x-pack/legacy/plugins/task_manager/task_manager.ts +++ b/x-pack/legacy/plugins/task_manager/task_manager.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import uuid from 'uuid'; import { SavedObjectsClientContract, SavedObjectsSerializer } from 'src/core/server'; import { Logger } from './types'; import { fillPool } from './lib/fill_pool'; @@ -37,12 +36,6 @@ export interface TaskManagerOpts { serializer: SavedObjectsSerializer; } -function generateTaskManagerUUID(logger: Logger): string { - const taskManagerUUID = uuid.v4(); - logger.info(`Initialising Task Manager with UUID: ${taskManagerUUID}`); - return taskManagerUUID; -} - /* * The TaskManager is the public interface into the task manager system. This glues together * all of the disparate modules in one integration point. The task manager operates in two different ways: @@ -82,6 +75,16 @@ export class TaskManager { this.definitions = {}; this.logger = opts.logger; + const taskManagerId = opts.config.get('server.uuid'); + if (!taskManagerId) { + this.logger.error( + `TaskManager is unable to start as there the Kibana UUID is invalid (value of the "server.uuid" configuration is ${taskManagerId})` + ); + throw new Error(`TaskManager is unable to start as Kibana has no valid UUID assigned to it.`); + } else { + this.logger.info(`TaskManager is identified by the Kibana UUID: ${taskManagerId}`); + } + /* Kibana UUID needs to be pulled live (not cached), as it takes a long time * to initialize, and can change after startup */ const store = new TaskStore({ @@ -91,7 +94,7 @@ export class TaskManager { index: opts.config.get('xpack.task_manager.index'), maxAttempts: opts.config.get('xpack.task_manager.max_attempts'), definitions: this.definitions, - taskManagerId: generateTaskManagerUUID(this.logger), + taskManagerId: `kibana:${taskManagerId}`, }); const pool = new TaskPool({ From 0a88fdc9446e6659f2b2dead55ab51e953963763 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Wed, 16 Oct 2019 11:14:11 -0400 Subject: [PATCH 34/48] fix miss spelling (#48405) --- .../legacy/plugins/siem/public/components/timeline/timeline.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx index fdf8381685118..efab2ac52a0b6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx @@ -116,7 +116,7 @@ export const Timeline = React.memo( indexPattern, browserFields, filters: [], - kqlQuery: { query: kqlQueryExpression, language: 'keury' }, + kqlQuery: { query: kqlQueryExpression, language: 'kuery' }, kqlMode, start, end, From 694845a96d3b3ab08e1ebeee0b491dd951df6546 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 16 Oct 2019 11:18:43 -0400 Subject: [PATCH 35/48] Spaces - fix heading levels (#48393) --- .../section_panel/__snapshots__/section_panel.test.tsx.snap | 4 ++-- .../management/edit_space/section_panel/section_panel.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/section_panel/__snapshots__/section_panel.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/section_panel/__snapshots__/section_panel.test.tsx.snap index 43149d7b3911f..43c5e05725414 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/section_panel/__snapshots__/section_panel.test.tsx.snap +++ b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/section_panel/__snapshots__/section_panel.test.tsx.snap @@ -13,7 +13,7 @@ exports[`it renders without blowing up 1`] = ` -

    +

    Elasticsearch -

    +
    { -

    +

    {this.props.iconType && ( { )} {this.props.title} -

    +
    {this.props.collapsible && ( From d26275e888ca78d853f4f4bad146af34a6aecda5 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Wed, 16 Oct 2019 11:19:24 -0400 Subject: [PATCH 36/48] Security - fixing heading levels (#48397) * adjust heading levels * fix heading levels on account screen --- .../components/change_password/change_password.tsx | 2 +- .../account/components/personal_info/personal_info.tsx | 4 ++-- .../management/roles_grid/components/roles_grid_page.tsx | 4 ++-- .../management/users_grid/components/users_list_page.tsx | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/x-pack/legacy/plugins/security/public/views/account/components/change_password/change_password.tsx b/x-pack/legacy/plugins/security/public/views/account/components/change_password/change_password.tsx index 08a6eaaf5102f..63abb4539470d 100644 --- a/x-pack/legacy/plugins/security/public/views/account/components/change_password/change_password.tsx +++ b/x-pack/legacy/plugins/security/public/views/account/components/change_password/change_password.tsx @@ -35,7 +35,7 @@ export class ChangePassword extends Component { return ( {changePasswordTitle}} + title={

    {changePasswordTitle}

    } description={

    { +

    -

    + } description={ { -

    +

    -

    +

    diff --git a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.tsx b/x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.tsx index 664c3cea285d5..651b073cb114e 100644 --- a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.tsx @@ -63,12 +63,12 @@ class UsersListPageUI extends Component { +

    -

    + } body={

    @@ -209,12 +209,12 @@ class UsersListPageUI extends Component { -

    +

    -

    +
    From f82c2e939f08616fd62d6172059878febe67b802 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Wed, 16 Oct 2019 10:34:20 -0500 Subject: [PATCH 37/48] Improve Migration guide with examples and links (#46661) --- src/core/MIGRATION.md | 124 ++++++++++++--- src/core/MIGRATION_EXAMPLES.md | 267 +++++++++++++++++++++++++++++++++ 2 files changed, 367 insertions(+), 24 deletions(-) create mode 100644 src/core/MIGRATION_EXAMPLES.md diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 01bcf1aa6f215..09158295fde07 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -25,6 +25,7 @@ * [How is "common" code shared on both the client and server?](#how-is-common-code-shared-on-both-the-client-and-server) * [When does code go into a plugin, core, or packages?](#when-does-code-go-into-a-plugin-core-or-packages) * [How do I build my shim for New Platform services?](#how-do-i-build-my-shim-for-new-platform-services) + * aka, where did everything move to? * [How to](#how-to) * [Configure plugin](#configure-plugin) * [Mock new platform services in tests](#mock-new-platform-services-in-tests) @@ -296,6 +297,14 @@ Once those things are finished for any given plugin, it can officially be switch Legacy server-side plugins access functionality from core and other plugins at runtime via function arguments, which is similar to how they must be architected to use the new plugin system. This greatly simplifies the plan of action for migrating server-side plugins. +Here is the high-level for migrating a server-side plugin: +- De-couple from hapi.js server and request objects +- Introduce a new plugin definition shim +- Replace legacy services in shim with new platform services +- Finally, move to the new plugin system + +These steps (except for the last one) do not have to be completed strictly in order, and some can be done in parallel or as part of the same change. In general, we recommend that larger plugins approach this more methodically, doing each step in a separate change. This makes each individual change less risk and more focused. This approach may not make sense for smaller plugins. For instance, it may be simpler to switch to New Platform services when you introduce your Plugin class, rather than shimming it with the legacy service. + ### De-couple from hapi.js server and request objects Most integrations with core and other plugins occur through the hapi.js `server` and `request` objects, and neither of these things are exposed through the new platform, so tackle this problem first. @@ -340,6 +349,8 @@ export default (kibana) => { This example legacy plugin uses hapi's `server` object directly inside of its `init` function, which is something we can address in a later step. What we need to address in this step is when we pass the raw `server` and `request` objects into our custom `search` function. +Our goal in this step is to make sure we're not integrating with other plugins via functions on `server.plugins.*` or on the `request` object. You should begin by finding all of the integration points where you make these calls, and put them behind a "facade" abstraction that can hide the details of where these APIs come from. This allows you to easily switch out how you access these APIs without having to change all of the code that may use them. + Instead, we identify which functionality we actually need from those objects and craft custom new interfaces for them, taking care not to leak hapi.js implementation details into their design. ```ts @@ -440,7 +451,7 @@ We now move this logic into a new plugin definition, which is based off of the c import { ElasticsearchPlugin } from '../elasticsearch'; interface CoreSetup { - elasticsearch: ElasticsearchPlugin // note: we know elasticsearch will move to core + elasticsearch: ElasticsearchPlugin // note: Elasticsearch is in Core in NP, rather than a plugin } interface FooSetup { @@ -457,11 +468,14 @@ export class Plugin { public setup(core: CoreSetup, plugins: PluginsSetup) { const serverFacade: ServerFacade = { plugins: { + // We're still using the legacy Elasticsearch here, but we're now accessing it + // the same way a NP plugin would, via core. Later, we'll swap this out for the + // actual New Platform service. elasticsearch: core.elasticsearch } } - // HTTP functionality from core + // HTTP functionality from legacy platform, accessed in the NP convention, just like Elasticsearch above. core.http.route({ // note: we know routes will be created on core.http path: '/api/demo_plugin/search', method: 'POST', @@ -518,6 +532,8 @@ export default (kibana) => { This introduces a layer between the legacy plugin system with hapi.js and the logic you want to move to the new plugin system. The functionality exposed through that layer is still provided from the legacy world and in some cases is still technically powered directly by hapi, but building this layer forced you to identify the remaining touch points into the legacy world and it provides you with control when you start migrating to new platform-backed services. +> Need help constructing your shim? There are some common APIs that are already present in the New Platform. In these cases, it may make more sense to simply use the New Platform service rather than crafting your own shim. Refer to the _[How do I build my shim for New Platform services?](#how-do-i-build-my-shim-for-new-platform-services)_ section for a table of legacy to new platform service translations to identify these. Note that while some APIs have simply _moved_ others are completely different. Take care when choosing how much refactoring to do in a single change. + ### Switch to new platform services At this point, your legacy server-side plugin is described in the shape and conventions of the new plugin system, and all of the touch points with the legacy world and hapi.js have been isolated to the shims in the legacy plugin definition. @@ -594,10 +610,16 @@ At this point, your legacy server-side plugin logic is no longer coupled to lega With both shims converted, you are now ready to complete your migration to the new platform. -Many plugins will copy and paste all of their plugin code into a new plugin directory and then delete their legacy shims. +Many plugins will copy and paste all of their plugin code into a new plugin directory in either `src/plugins` for OSS or `x-pack/plugins` for commerical code and then delete their legacy shims. It's at this point that you'll want to make sure to create your `kibana.json` file if it does not already exist. With the previous steps resolved, this final step should be easy, but the exact process may vary plugin by plugin, so when you're at this point talk to the platform team to figure out the exact changes you need. +Other plugins may want to move subsystems over individually. For instance, you can move routes over to the New Platform in groups rather than all at once. Other examples that could be broken up: +- Configuration schema ([see example](./MIGRATION_EXAMPLES.md#declaring-config-schema)) +- HTTP route registration +- Polling mechanisms (eg. job worker) + +In general, we recommend moving all at once by ensuring you're not depending on any legacy code before you move over. ## Browser-side plan of action @@ -841,6 +863,11 @@ Many plugins at this point will copy over their plugin definition class & the co With the previous steps resolved, this final step should be easy, but the exact process may vary plugin by plugin, so when you're at this point talk to the platform team to figure out the exact changes you need. +Other plugins may want to move subsystems over individually. Examples of pieces that could be broken up: +- Registration logic (eg. viz types, embeddables, chrome nav controls) +- Application mounting +- Polling mechanisms (eg. job worker) + #### Bonus: Tips for complex migration scenarios For a few plugins, some of these steps (such as angular removal) could be a months-long process. In those cases, it may be helpful from an organizational perspective to maintain a clear separation of code that is and isn't "ready" for the new platform. @@ -1021,6 +1048,8 @@ Many of the utilities you're using to build your plugins are available in the Ne #### Client-side +TODO: add links to API docs on items in "New Platform" column. + ##### Core services In client code, `core` can be imported in legacy plugins via the `ui/new_platform` module. @@ -1028,21 +1057,22 @@ In client code, `core` can be imported in legacy plugins via the `ui/new_platfor import { npStart: { core } } from 'ui/new_platform'; ``` -| Legacy Platform | New Platform | Notes | -|-------------------------------------------------------|--------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------| -| `chrome.addBasePath` | `core.http.basePath.prepend` | | -| `chrome.breadcrumbs.set` | `core.chrome.setBreadcrumbs` | | -| `chrome.getUiSettingsClient` | `core.uiSettings` | | -| `chrome.helpExtension.set` | `core.chrome.setHelpExtension` | | -| `chrome.setVisible` | `core.chrome.setVisible` | | -| `chrome.getInjected` | -- | Not available, we'd like to hear about your usecase. | -| `chrome.setRootTemplate` / `chrome.setRootController` | -- | Use application mounting via `core.application.register` (coming soon). | -| `import { recentlyAccessed } from 'ui/persisted_log'` | `core.chrome.recentlyAccessed` | | -| `ui/documentation_links` | `core.docLinks` | | -| `ui/kfetch` | `core.http` | API is nearly identical | -| `ui/metadata` | `core.injectedMetadata` | May be removed in the future. If you have a necessary usecase, please let us know. | -| `ui/notify` | `core.notifications` | Currently only supports toast messages. Banners coming soon. | -| `ui/routes` | -- | There is no global routing mechanism. Each app [configures its own routing](/rfcs/text/0004_application_service_mounting.md#complete-example). | +| Legacy Platform | New Platform | Notes | +|-------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------| +| `chrome.addBasePath` | [`core.http.basePath.prepend`](/docs/development/core/public/kibana-plugin-public.httpservicebase.basepath.md) | | +| `chrome.breadcrumbs.set` | [`core.chrome.setBreadcrumbs`](/docs/development/core/public/kibana-plugin-public.chromestart.setbreadcrumbs.md) | | +| `chrome.getUiSettingsClient` | [`core.uiSettings`](/docs/development/core/public/kibana-plugin-public.uisettingsclient.md) | | +| `chrome.helpExtension.set` | [`core.chrome.setHelpExtension`](/docs/development/core/public/kibana-plugin-public.chromestart.sethelpextension.md) | | +| `chrome.setVisible` | [`core.chrome.setIsVisible`](/docs/development/core/public/kibana-plugin-public.chromestart.setisvisible.md) | | +| `chrome.getInjected` | -- | Not implemented yet, see [#41990](https://github.com/elastic/kibana/issues/41990) | +| `chrome.setRootTemplate` / `chrome.setRootController` | -- | Use application mounting via `core.application.register` (not available to legacy plugins at this time). | +| `import { recentlyAccessed } from 'ui/persisted_log'` | [`core.chrome.recentlyAccessed`](/docs/development/core/public/kibana-plugin-public.chromerecentlyaccessed.md) | | +| `ui/capabilities` | [`core.application.capabilities`](/docs/development/core/public/kibana-plugin-public.capabilities.md) | | +| `ui/documentation_links` | [`core.docLinks`](/docs/development/core/public/kibana-plugin-public.doclinksstart.md) | | +| `ui/kfetch` | [`core.http`](/docs/development/core/public/kibana-plugin-public.httpservicebase.md) | API is nearly identical | +| `ui/notify` | [`core.notifications`](/docs/development/core/public/kibana-plugin-public.notificationsstart.md) and [`core.overlays`](/docs/development/core/public/kibana-plugin-public.overlaystart.md) | Toast messages are in `notifications`, banners are in `overlays`. May be combined later. | +| `ui/routes` | -- | There is no global routing mechanism. Each app [configures its own routing](/rfcs/text/0004_application_service_mounting.md#complete-example). | +| `ui/saved_objects` | [`core.savedObjects`](/docs/development/core/public/kibana-plugin-public.savedobjectsstart.md) | Client API is the same | _See also: [Public's CoreStart API Docs](/docs/development/core/public/kibana-plugin-public.corestart.md)_ @@ -1081,15 +1111,61 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy'; ##### Core services In server code, `core` can be accessed from either `server.newPlatform` or `kbnServer.newPlatform`. There are not currently very many services available on the server-side: -| Legacy Platform | New Platform | Notes | -|----------------------------------------------------|-----------------------------------|----------------------------------------------------| -| `request.getBasePath()` | `core.http.basePath.get` | | -| `server.plugins.elasticsearch.getCluster('data')` | `core.elasticsearch.dataClient$` | Handlers will also include a pre-configured client | -| `server.plugins.elasticsearch.getCluster('admin')` | `core.elasticsearch.adminClient$` | Handlers will also include a pre-configured client | -| `request.getBasePath()` | `core.http.basePath` | | +| Legacy Platform | New Platform | Notes | +|----------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------| +| `server.config()` | [`initializerContext.config.create()`](/docs/development/core/server/kibana-plugin-server.plugininitializercontext.config.md) | Must also define schema. See _[how to configure plugin](#configure-plugin)_ | +| `server.route` | [`core.http.createRouter`](/docs/development/core/server/kibana-plugin-server.httpservicesetup.createrouter.md) | | +| `request.getBasePath()` | [`core.http.basePath.get`](/docs/development/core/server/kibana-plugin-server.httpservicesetup.basepath.md) | | +| `server.plugins.elasticsearch.getCluster('data')` | [`core.elasticsearch.dataClient$`](/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient_.md) | Handlers will also include a pre-configured client | +| `server.plugins.elasticsearch.getCluster('admin')` | [`core.elasticsearch.adminClient$`](/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient_.md) | Handlers will also include a pre-configured client | _See also: [Server's CoreSetup API Docs](/docs/development/core/server/kibana-plugin-server.coresetup.md)_ +#### UI Exports + +The legacy platform uses a set of "uiExports" to inject modules from one plugin into other plugins. This mechansim is not necessary in the New Platform because all plugins are executed on the page at once (though only one application) is rendered at a time. + +This table shows where these uiExports have moved to in the New Platform. In most cases, if a uiExport you need is not yet available in the New Platform, you may leave in your legacy plugin for the time being and continue to migrate the rest of your app to the New Platform. + +| Legacy Platform | New Platform | Notes | +|------------------------------|---------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------| +| `aliases` | | | +| `app` | [`core.application.register`](/docs/development/core/public/kibana-plugin-public.applicationsetup.register.md) | | +| `canvas` | | Should be an API on the canvas plugin. | +| `chromeNavControls` | [`core.chrome.navControls.register{Left,Right}`](/docs/development/core/public/kibana-plugin-public.chromenavcontrols.md) | | +| `contextMenuActions` | | Should be an API on the devTools plugin. | +| `devTools` | | | +| `docViews` | | | +| `embeddableActions` | | Should be an API on the embeddables plugin. | +| `embeddableFactories` | | Should be an API on the embeddables plugin. | +| `fieldFormatEditors` | | | +| `fieldFormats` | | | +| `hacks` | n/a | Just run the code in your plugin's `start` method. | +| `home` | | Should be an API on the home plugin. | +| `indexManagement` | | Should be an API on the indexManagement plugin. | +| `injectDefaultVars` | n/a | Plugins will only be able to "whitelist" config values for the frontend. See [#41990](https://github.com/elastic/kibana/issues/41990) | +| `inspectorViews` | | Should be an API on the data (?) plugin. | +| `interpreter` | | Should be an API on the interpreter plugin. | +| `links` | n/a | Not necessary, just register your app via `core.application.register` | +| `managementSections` | [`plugins.management.sections.register`](/rfcs/text/0006_management_section_service.md) | API finalized, implementation in progress. | +| `mappings` | | Part of SavedObjects, see [#33587](https://github.com/elastic/kibana/issues/33587) | +| `migrations` | | Part of SavedObjects, see [#33587](https://github.com/elastic/kibana/issues/33587) | +| `navbarExtensions` | n/a | Deprecated | +| `savedObjectSchemas` | | Part of SavedObjects, see [#33587](https://github.com/elastic/kibana/issues/33587) | +| `savedObjectsManagement` | | Part of SavedObjects, see [#33587](https://github.com/elastic/kibana/issues/33587) | +| `savedObjectTypes` | | Part of SavedObjects, see [#33587](https://github.com/elastic/kibana/issues/33587) | +| `search` | | | +| `shareContextMenuExtensions` | | | +| `styleSheetPaths` | | | +| `taskDefinitions` | | Should be an API on the taskManager plugin. | +| `uiCapabilities` | [`core.application.register`](/docs/development/core/public/kibana-plugin-public.applicationsetup.register.md) | | +| `uiSettingDefaults` | | Most likely part of server-side UiSettingsService. | +| `validations` | | Part of SavedObjects, see [#33587](https://github.com/elastic/kibana/issues/33587) | +| `visEditorTypes` | | | +| `visTypeEnhancers` | | | +| `visTypes` | | | +| `visualize` | | | + ## How to ### Configure plugin diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md new file mode 100644 index 0000000000000..79ae8953df0ea --- /dev/null +++ b/src/core/MIGRATION_EXAMPLES.md @@ -0,0 +1,267 @@ +# Migration Examples + +This document is a list of examples of how to migrate plugin code from legacy +APIs to their New Platform equivalents. + +## Configuration + +### Declaring config schema + +Declaring the schema of your configuration fields is similar to the Legacy Platform but uses the `@kbn/config-schema` package instead of Joi. This package has full TypeScript support, but may be missing some features you need. Let the Platform team know by opening an issue and we'll add what you're missing. + +```ts +// Legacy config schema +import Joi from 'joi'; + +new kibana.Plugin({ + config() { + return Joi.object({ + enabled: Joi.boolean().default(true), + defaultAppId: Joi.string().default('home'), + index: Joi.string().default('.kibana'), + disableWelcomeScreen: Joi.boolean().default(false), + autocompleteTerminateAfter: Joi.number().integer().min(1).default(100000), + }) + } +}); + +// New Platform equivalent +import { schema, TypeOf } from '@kbn/config-schema'; + +export const config = { + schema: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + defaultAppId: schema.string({ defaultValue: true }), + index: schema.string({ defaultValue: '.kibana' }), + disableWelcomeScreen: schema.boolean({ defaultValue: false }), + autocompleteTerminateAfter: schema.duration({ min: 1, defaultValue: 100000 }), + }) +}; + +// @kbn/config-schema is written in TypeScript, so you can use your schema +// definition to create a type to use in your plugin code. +export type MyPluginConfig = TypeOf; +``` + +### Using New Platform config from a Legacy plugin + +During the migration process, you'll want to migrate your schema to the new +format. However, legacy plugins cannot directly get access to New Platform's +config service due to the way that config is tied to the `kibana.json` file +(which does not exist for legacy plugins). + +There is a workaround though: +- Create a New Platform plugin that contains your plugin's config schema in the new format +- Expose the config from the New Platform plugin in its setup contract +- Read the config from the setup contract in your legacy plugin + +#### Create a New Platform plugin + +For example, if wanted to move the legacy `timelion` plugin's configuration to +the New Platform, we could create a NP plugin with the same name in +`src/plugins/timelion` with the following files: + +```json5 +// src/plugins/timelion/kibana.json +{ + "id": "timelion", + "server": true +} +``` + +```ts +// src/plugins/timelion/server/index.ts +import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginInitializerContext } from 'src/core/server'; +import { TimelionPlugin } from './plugin'; + +export const config = { + schema: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }); +} + +export const plugin = (initContext: PluginInitializerContext) => new TimelionPlugin(initContext); + +export type TimelionConfig = TypeOf; +export { TimelionSetup } from './plugin'; +``` + +```ts +// src/plugins/timelion/server/plugin.ts +import { PluginInitializerContext, Plugin, CoreSetup } from '../../core/server'; +import { TimelionConfig } from '.'; + +export class TimelionPlugin implements Plugin { + constructor(private readonly initContext: PluginInitializerContext) {} + + public setup(core: CoreSetup) { + return { + __legacy: { + config$: this.initContext.config.create(), + }, + }; + } + + public start() {} + public stop() {} +} + +export interface TimelionSetup { + /** @deprecated */ + __legacy: { + config$: Observable; + }; +} +``` + +With the New Platform plugin in place, you can then read this `config$` +Observable from your legacy plugin: + +```ts +import { take } from 'rxjs/operators'; + +new kibana.Plugin({ + async init(server) { + const { config$ } = server.newPlatform.setup.plugins.timelion; + const currentConfig = await config$.pipe(take(1)).toPromise(); + } +}); +``` + +## HTTP Routes + +In the legacy platform, plugins have direct access to the Hapi `server` object +which gives full access to all of Hapi's API. In the New Platform, plugins have +access to the +[HttpServiceSetup](/docs/development/core/server/kibana-plugin-server.httpservicesetup.md) +interface, which is exposed via the +[CoreSetup](/docs/development/core/server/kibana-plugin-server.coresetup.md) +object injected into the `setup` method of server-side plugins. + +This interface has a different API with slightly different behaviors. +- All input (body, query parameters, and URL parameters) must be validated using + the `@kbn/config-schema` package. If no validation schema is provided, these + values will be empty objects. +- All exceptions thrown by handlers result in 500 errors. If you need a specific + HTTP error code, catch any exceptions in your handler and construct the + appropriate response using the provided response factory. While you can + continue using the `boom` module internally in your plugin, the framework does + not have native support for converting Boom exceptions into HTTP responses. + +### Route Registration + +```ts +// Legacy route registration +import Joi from 'joi'; + +new kibana.Plugin({ + init(server) { + server.route({ + path: '/api/my-plugin/my-route', + method: 'POST', + options: { + validate: { + payload: Joi.object({ + field1: Joi.string().required(), + }), + } + }, + handler(req, h) { + return { message: `Received field1: ${req.payload.field1}` }; + } + }); + } +}); + + +// New Platform equivalent +import { schema } from '@kbn/config-schema'; + +class Plugin { + public setup(core) { + const router = core.http.createRouter(); + router.post( + { + path: '/api/my-plugin/my-route', + validate: { + body: schema.object({ + field1: schema.string(), + }), + } + }, + (context, req, res) => { + return res.ok({ + body: { + message: `Received field1: ${req.body.field1}` + } + }); + } + ) + } +} +``` + +### Accessing Services + +Services in the Legacy Platform were typically available via methods on either +`server.plugins.*`, `server.*`, or `req.*`. In the New Platform, all services +are available via the `context` argument to the route handler. The type of this +argument is the +[RequestHandlerContext](/docs/development/core/server/kibana-plugin-server.requesthandlercontext.md). +The APIs available here will include all Core services and any services +registered by plugins this plugin depends on. + +```ts +new kibana.Plugin({ + init(server) { + const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); + + server.route({ + path: '/api/my-plugin/my-route', + method: 'POST', + async handler(req, h) { + const results = await callWithRequest(req, 'search', query); + return { results }; + } + }); + } +}); + +class Plugin { + public setup(core) { + const router = core.http.createRouter(); + router.post( + { + path: '/api/my-plugin/my-route', + }, + async (context, req, res) => { + const results = await context.elasticsearch.dataClient.callAsCurrentUser('search', query); + return res.ok({ + body: { results } + }); + } + ) + } +} +``` + +## Chrome + +In the Legacy Platform, the `ui/chrome` import contained APIs for a very wide +range of features. In the New Platform, some of these APIs have changed or moved +elsewhere. + +| Legacy Platform | New Platform | Notes | +|-------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `chrome.addBasePath` | [`core.http.basePath.prepend`](/docs/development/core/public/kibana-plugin-public.httpservicebase.basepath.md) | | +| `chrome.breadcrumbs.set` | [`core.chrome.setBreadcrumbs`](/docs/development/core/public/kibana-plugin-public.chromestart.setbreadcrumbs.md) | | +| `chrome.getUiSettingsClient` | [`core.uiSettings`](/docs/development/core/public/kibana-plugin-public.uisettingsclient.md) | | +| `chrome.helpExtension.set` | [`core.chrome.setHelpExtension`](/docs/development/core/public/kibana-plugin-public.chromestart.sethelpextension.md) | | +| `chrome.setVisible` | [`core.chrome.setIsVisible`](/docs/development/core/public/kibana-plugin-public.chromestart.setisvisible.md) | | +| `chrome.getInjected` | [`core.injectedMetadata.getInjected`](/docs/development/core/public/kibana-plugin-public.coresetup.injectedmetadata.md) (temporary) | A temporary API is available to read injected vars provided by legacy plugins. This will be removed after [#41990](https://github.com/elastic/kibana/issues/41990) is completed. | +| `chrome.setRootTemplate` / `chrome.setRootController` | -- | Use application mounting via `core.application.register` (not currently avaiable to legacy plugins). | + +In most cases, the most convenient way to access these APIs will be via the +[AppMountContext](/docs/development/core/public/kibana-plugin-public.appmountcontext.md) +object passed to your application when your app is mounted on the page. From ae00c15e8dfa9052d8b1e3689176f9df41cbe1d7 Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Wed, 16 Oct 2019 17:26:05 +0100 Subject: [PATCH 38/48] [ML] Fixes module datafeed configs to use indices plus SIEM job desc edit (#48417) --- .../modules/apache_ecs/ml/datafeed_low_request_rate_ecs.json | 2 +- .../apache_ecs/ml/datafeed_source_ip_request_rate_ecs.json | 2 +- .../apache_ecs/ml/datafeed_source_ip_url_count_ecs.json | 2 +- .../modules/apache_ecs/ml/datafeed_status_code_rate_ecs.json | 2 +- .../modules/apache_ecs/ml/datafeed_visitor_rate_ecs.json | 2 +- .../apm_transaction/ml/datafeed_high_mean_response_time.json | 2 +- .../ml/datafeed_docker_high_count_process_events_ecs.json | 2 +- .../ml/datafeed_docker_rare_process_activity_ecs.json | 2 +- .../ml/datafeed_hosts_high_count_process_events_ecs.json | 2 +- .../ml/datafeed_hosts_rare_process_activity_ecs.json | 4 ++-- .../modules/logs_ui_analysis/ml/datafeed_log_entry_rate.json | 2 +- .../ml/datafeed_high_mean_cpu_iowait_ecs.json | 2 +- .../ml/datafeed_max_disk_utilization_ecs.json | 2 +- .../ml/datafeed_metricbeat_outages_ecs.json | 2 +- .../modules/nginx_ecs/ml/datafeed_low_request_rate_ecs.json | 2 +- .../nginx_ecs/ml/datafeed_source_ip_request_rate_ecs.json | 2 +- .../nginx_ecs/ml/datafeed_source_ip_url_count_ecs.json | 2 +- .../modules/nginx_ecs/ml/datafeed_status_code_rate_ecs.json | 2 +- .../modules/nginx_ecs/ml/datafeed_visitor_rate_ecs.json | 2 +- .../ml/datafeed_high_sum_total_sales.json | 2 +- .../sample_data_weblogs/ml/datafeed_low_request_rate.json | 2 +- .../sample_data_weblogs/ml/datafeed_response_code_rates.json | 2 +- .../modules/sample_data_weblogs/ml/datafeed_url_scanning.json | 2 +- .../ml/datafeed_rare_process_by_host_linux_ecs.json | 2 +- .../ml/datafeed_suspicious_login_activity_ecs.json | 2 +- .../ml/datafeed_suspicious_login_activity_ecs.json | 2 +- .../ml/datafeed_rare_process_by_host_windows_ecs.json | 2 +- .../ml/windows_rare_user_type10_remote_login.json | 4 ++-- 28 files changed, 30 insertions(+), 30 deletions(-) diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_low_request_rate_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_low_request_rate_ecs.json index f91e102a3f1c1..98d4feb550c31 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_low_request_rate_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_low_request_rate_ecs.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": [ + "indices": [ "INDEX_PATTERN_NAME" ], "query": { diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_request_rate_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_request_rate_ecs.json index 824d6a934d865..5ea53cf35ff4b 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_request_rate_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_request_rate_ecs.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": [ + "indices": [ "INDEX_PATTERN_NAME" ], "query": { diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_url_count_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_url_count_ecs.json index 824d6a934d865..5ea53cf35ff4b 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_url_count_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_source_ip_url_count_ecs.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": [ + "indices": [ "INDEX_PATTERN_NAME" ], "query": { diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_status_code_rate_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_status_code_rate_ecs.json index 824d6a934d865..5ea53cf35ff4b 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_status_code_rate_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_status_code_rate_ecs.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": [ + "indices": [ "INDEX_PATTERN_NAME" ], "query": { diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_visitor_rate_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_visitor_rate_ecs.json index 0a98563f98817..7e5f70e8c8b0b 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_visitor_rate_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apache_ecs/ml/datafeed_visitor_rate_ecs.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": [ + "indices": [ "INDEX_PATTERN_NAME" ], "query": { diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_response_time.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_response_time.json index 2f22f2c9be3c8..dc37d05d18111 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_response_time.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_response_time.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": [ + "indices": [ "INDEX_PATTERN_NAME" ], "query": { diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_high_count_process_events_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_high_count_process_events_ecs.json index 483e4dab68333..7370aed5321ac 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_high_count_process_events_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_high_count_process_events_ecs.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": [ + "indices": [ "INDEX_PATTERN_NAME" ], "query": { diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_rare_process_activity_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_rare_process_activity_ecs.json index 483e4dab68333..7370aed5321ac 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_rare_process_activity_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_docker_ecs/ml/datafeed_docker_rare_process_activity_ecs.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": [ + "indices": [ "INDEX_PATTERN_NAME" ], "query": { diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_high_count_process_events_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_high_count_process_events_ecs.json index 30d43b331b839..9c04257fb8904 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_high_count_process_events_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_high_count_process_events_ecs.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": [ + "indices": [ "INDEX_PATTERN_NAME" ], "query": { diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_rare_process_activity_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_rare_process_activity_ecs.json index dea861b70305e..9c04257fb8904 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_rare_process_activity_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/auditbeat_process_hosts_ecs/ml/datafeed_hosts_rare_process_activity_ecs.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": [ + "indices": [ "INDEX_PATTERN_NAME" ], "query": { @@ -10,7 +10,7 @@ ], "must": { "exists": { "field": "auditd.data.syscall" } - }, + }, "must_not": { "exists": { "field": "container.runtime" } } diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/datafeed_log_entry_rate.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/datafeed_log_entry_rate.json index 073e72a188616..2ece259e2bb45 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/datafeed_log_entry_rate.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/logs_ui_analysis/ml/datafeed_log_entry_rate.json @@ -1,4 +1,4 @@ { "job_id": "JOB_ID", - "indexes": ["INDEX_PATTERN_NAME"] + "indices": ["INDEX_PATTERN_NAME"] } diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_high_mean_cpu_iowait_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_high_mean_cpu_iowait_ecs.json index fd164e218ee22..fe87160142cff 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_high_mean_cpu_iowait_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_high_mean_cpu_iowait_ecs.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": [ + "indices": [ "INDEX_PATTERN_NAME" ], "query": { diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_max_disk_utilization_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_max_disk_utilization_ecs.json index 0b1a6099d6794..6ccbfe94c220c 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_max_disk_utilization_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_max_disk_utilization_ecs.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": [ + "indices": [ "INDEX_PATTERN_NAME" ], "query": { diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_metricbeat_outages_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_metricbeat_outages_ecs.json index 35974310eadb2..d6f33127dfc08 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_metricbeat_outages_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/metricbeat_system_ecs/ml/datafeed_metricbeat_outages_ecs.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": [ + "indices": [ "INDEX_PATTERN_NAME" ], "query": { diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_low_request_rate_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_low_request_rate_ecs.json index d3333928299ea..92f7663f42653 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_low_request_rate_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_low_request_rate_ecs.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": [ + "indices": [ "INDEX_PATTERN_NAME" ], "query": { diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_request_rate_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_request_rate_ecs.json index bccf1bd8de6d5..682844b1bc5c7 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_request_rate_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_request_rate_ecs.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": [ + "indices": [ "INDEX_PATTERN_NAME" ], "query": { diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_url_count_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_url_count_ecs.json index bccf1bd8de6d5..682844b1bc5c7 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_url_count_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_source_ip_url_count_ecs.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": [ + "indices": [ "INDEX_PATTERN_NAME" ], "query": { diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_status_code_rate_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_status_code_rate_ecs.json index bccf1bd8de6d5..682844b1bc5c7 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_status_code_rate_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_status_code_rate_ecs.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": [ + "indices": [ "INDEX_PATTERN_NAME" ], "query": { diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_visitor_rate_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_visitor_rate_ecs.json index e3faf85461938..7027d3e8902bc 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_visitor_rate_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/nginx_ecs/ml/datafeed_visitor_rate_ecs.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": [ + "indices": [ "INDEX_PATTERN_NAME" ], "query": { diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/datafeed_high_sum_total_sales.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/datafeed_high_sum_total_sales.json index 193239995c6d3..0a955a766bd53 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/datafeed_high_sum_total_sales.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_ecommerce/ml/datafeed_high_sum_total_sales.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": ["INDEX_PATTERN_NAME"], + "indices": ["INDEX_PATTERN_NAME"], "query": { "bool": { "filter": [{ "term": { "_index": "kibana_sample_data_ecommerce" } }] diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json index e2682e2c15008..843a7d1651dc8 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": ["INDEX_PATTERN_NAME"], + "indices": ["INDEX_PATTERN_NAME"], "query": { "bool": { "filter": [{ "term": { "_index": "kibana_sample_data_logs" } }] diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_response_code_rates.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_response_code_rates.json index 3afbfdefc31e8..3a0f67daa392a 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_response_code_rates.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_response_code_rates.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": ["INDEX_PATTERN_NAME"], + "indices": ["INDEX_PATTERN_NAME"], "query": { "bool": { "filter": [{ "term": { "_index": "kibana_sample_data_logs" } }] diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_url_scanning.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_url_scanning.json index 3afbfdefc31e8..3a0f67daa392a 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_url_scanning.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_url_scanning.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": ["INDEX_PATTERN_NAME"], + "indices": ["INDEX_PATTERN_NAME"], "query": { "bool": { "filter": [{ "term": { "_index": "kibana_sample_data_logs" } }] diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_rare_process_by_host_linux_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_rare_process_by_host_linux_ecs.json index 9de27f5d213f2..93a5646a7bf01 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_rare_process_by_host_linux_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_rare_process_by_host_linux_ecs.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": [ + "indices": [ "INDEX_PATTERN_NAME" ], "max_empty_searches": 10, diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_suspicious_login_activity_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_suspicious_login_activity_ecs.json index e92ba08378fab..a177abfd0f116 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_suspicious_login_activity_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_suspicious_login_activity_ecs.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": [ + "indices": [ "INDEX_PATTERN_NAME" ], "max_empty_searches": 10, diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/datafeed_suspicious_login_activity_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/datafeed_suspicious_login_activity_ecs.json index 75e7148b4db1a..386b9fab25667 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/datafeed_suspicious_login_activity_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/datafeed_suspicious_login_activity_ecs.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": [ + "indices": [ "INDEX_PATTERN_NAME" ], "max_empty_searches": 10, diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_rare_process_by_host_windows_ecs.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_rare_process_by_host_windows_ecs.json index 81519bf6001e3..6daa5881575ab 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_rare_process_by_host_windows_ecs.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_rare_process_by_host_windows_ecs.json @@ -1,6 +1,6 @@ { "job_id": "JOB_ID", - "indexes": [ + "indices": [ "INDEX_PATTERN_NAME" ], "max_empty_searches": 10, diff --git a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/windows_rare_user_type10_remote_login.json b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/windows_rare_user_type10_remote_login.json index b9992a9e01182..ee009e465ec23 100644 --- a/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/windows_rare_user_type10_remote_login.json +++ b/x-pack/legacy/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat_auth/ml/windows_rare_user_type10_remote_login.json @@ -1,6 +1,6 @@ { "job_type": "anomaly_detector", - "description": "SIEM Winlogbeat Auth: Unusual terminal services users can indicate account takeover or credentialed access (beta)", + "description": "SIEM Winlogbeat Auth: Unusual RDP (remote desktop protocol) user logins can indicate account takeover or credentialed access (beta)", "groups": [ "siem", "winlogbeat", @@ -49,4 +49,4 @@ } ] } -} \ No newline at end of file +} From ee2ea1fbe7d4d3edaa9cdc9c262b7f984df00a04 Mon Sep 17 00:00:00 2001 From: cachedout Date: Wed, 16 Oct 2019 16:32:21 +0000 Subject: [PATCH 39/48] Standarize on 'JVM Heap' phrase for heap metrics (#48093) * Standarize on 'JVM Heap' for heap metrics * Update tests for JVM Heap label * Update another test for JVM heap label change --- .../public/components/elasticsearch/cluster_status/index.js | 2 +- .../monitoring/public/components/elasticsearch/nodes/nodes.js | 2 +- .../test/functional/apps/monitoring/elasticsearch/indices.js | 2 +- x-pack/test/functional/apps/monitoring/elasticsearch/nodes.js | 4 ++-- .../test/functional/apps/monitoring/elasticsearch/overview.js | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js index b9fb2c0b9f962..5c730215f93c9 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js +++ b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js @@ -40,7 +40,7 @@ export function ClusterStatus({ stats }) { }, { label: i18n.translate('xpack.monitoring.elasticsearch.clusterStatus.memoryLabel', { - defaultMessage: 'Memory' + defaultMessage: 'JVM Heap' }), value: formatMetric(memUsed, 'byte') + ' / ' + formatMetric(memMax, 'byte'), 'data-test-subj': 'memory' diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js index 6bc4dd509f0b8..72a74964fd35e 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js +++ b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js @@ -206,7 +206,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => { cols.push({ name: i18n.translate('xpack.monitoring.elasticsearch.nodes.jvmMemoryColumnTitle', { - defaultMessage: '{javaVirtualMachine} Memory', + defaultMessage: '{javaVirtualMachine} Heap', values: { javaVirtualMachine: 'JVM' } diff --git a/x-pack/test/functional/apps/monitoring/elasticsearch/indices.js b/x-pack/test/functional/apps/monitoring/elasticsearch/indices.js index ff809b95a834e..1ed7c15a9ecf1 100644 --- a/x-pack/test/functional/apps/monitoring/elasticsearch/indices.js +++ b/x-pack/test/functional/apps/monitoring/elasticsearch/indices.js @@ -34,7 +34,7 @@ export default function ({ getService, getPageObjects }) { expect(await esClusterSummaryStatus.getContent()).to.eql({ nodesCount: 'Nodes\n1', indicesCount: 'Indices\n19', - memory: 'Memory\n267.7 MB / 676.8 MB', + memory: 'JVM Heap\n267.7 MB / 676.8 MB', totalShards: 'Total shards\n46', unassignedShards: 'Unassigned shards\n23', documentCount: 'Documents\n4,535', diff --git a/x-pack/test/functional/apps/monitoring/elasticsearch/nodes.js b/x-pack/test/functional/apps/monitoring/elasticsearch/nodes.js index 86f47775e50cd..7ad09e034e13b 100644 --- a/x-pack/test/functional/apps/monitoring/elasticsearch/nodes.js +++ b/x-pack/test/functional/apps/monitoring/elasticsearch/nodes.js @@ -38,7 +38,7 @@ export default function ({ getService, getPageObjects }) { expect(await esClusterSummaryStatus.getContent()).to.eql({ nodesCount: 'Nodes\n2', indicesCount: 'Indices\n20', - memory: 'Memory\n696.6 MB / 1.3 GB', + memory: 'JVM Heap\n696.6 MB / 1.3 GB', totalShards: 'Total shards\n79', unassignedShards: 'Unassigned shards\n7', documentCount: 'Documents\n25,758', @@ -213,7 +213,7 @@ export default function ({ getService, getPageObjects }) { expect(await esClusterSummaryStatus.getContent()).to.eql({ nodesCount: 'Nodes\n3', indicesCount: 'Indices\n20', - memory: 'Memory\n575.3 MB / 2.0 GB', + memory: 'JVM Heap\n575.3 MB / 2.0 GB', totalShards: 'Total shards\n80', unassignedShards: 'Unassigned shards\n5', documentCount: 'Documents\n25,927', diff --git a/x-pack/test/functional/apps/monitoring/elasticsearch/overview.js b/x-pack/test/functional/apps/monitoring/elasticsearch/overview.js index d86127b3e6fb8..a0e91e2336e08 100644 --- a/x-pack/test/functional/apps/monitoring/elasticsearch/overview.js +++ b/x-pack/test/functional/apps/monitoring/elasticsearch/overview.js @@ -34,7 +34,7 @@ export default function ({ getService, getPageObjects }) { expect(await esClusterSummaryStatus.getContent()).to.eql({ nodesCount: 'Nodes\n3', indicesCount: 'Indices\n20', - memory: 'Memory\n575.3 MB / 2.0 GB', + memory: 'JVM Heap\n575.3 MB / 2.0 GB', totalShards: 'Total shards\n80', unassignedShards: 'Unassigned shards\n5', documentCount: 'Documents\n25,927', From b267704c4992f44592f96827dea101b6153f32be Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Wed, 16 Oct 2019 10:00:07 -0700 Subject: [PATCH 40/48] Print the name of the plugin in an exception thrown (#48297) --- x-pack/plugins/features/server/feature_registry.test.ts | 4 +++- x-pack/plugins/features/server/feature_registry.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/features/server/feature_registry.test.ts b/x-pack/plugins/features/server/feature_registry.test.ts index 1e2814021f0f6..66460a811009e 100644 --- a/x-pack/plugins/features/server/feature_registry.test.ts +++ b/x-pack/plugins/features/server/feature_registry.test.ts @@ -494,6 +494,8 @@ describe('FeatureRegistry', () => { featureRegistry.getAll(); expect(() => { featureRegistry.register(feature2); - }).toThrowErrorMatchingInlineSnapshot(`"Features are locked, can't register new features"`); + }).toThrowErrorMatchingInlineSnapshot( + `"Features are locked, can't register new features. Attempt to register test-feature-2 failed."` + ); }); }); diff --git a/x-pack/plugins/features/server/feature_registry.ts b/x-pack/plugins/features/server/feature_registry.ts index c430811f2ead5..bec0ab1ed0bf7 100644 --- a/x-pack/plugins/features/server/feature_registry.ts +++ b/x-pack/plugins/features/server/feature_registry.ts @@ -15,7 +15,9 @@ export class FeatureRegistry { public register(feature: FeatureWithAllOrReadPrivileges) { if (this.locked) { - throw new Error(`Features are locked, can't register new features`); + throw new Error( + `Features are locked, can't register new features. Attempt to register ${feature.id} failed.` + ); } validateFeature(feature); From b03dfdf68ae18971080f943b368df0841d1014e7 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Wed, 16 Oct 2019 12:13:50 -0500 Subject: [PATCH 41/48] Allow LP plugins to access NP plugin context providers (#47639) --- src/core/public/core_system.test.ts | 16 +++++++++++++ src/core/public/core_system.ts | 11 ++++++--- src/core/public/legacy/legacy_service.mock.ts | 4 +++- ...dex.test.mocks.ts => server.test.mocks.ts} | 7 ++++++ src/core/server/server.test.ts | 22 ++++++++++++++++- src/core/server/server.ts | 11 ++++++--- test/plugin_functional/config.js | 6 ++++- .../plugins/core_legacy_compat/package.json | 17 ------------- .../plugins/core_legacy_compat/tsconfig.json | 14 ----------- .../plugins/core_plugin_legacy/index.ts | 12 ++++++++++ .../public/application.tsx | 0 .../public/index.ts | 0 test/plugin_functional/services/index.js | 24 +++++++++++++++++++ .../index.ts => services/supertest.js} | 20 ++++++---------- .../core_plugins/legacy_plugins.js | 22 +++++++++++------ 15 files changed, 126 insertions(+), 60 deletions(-) rename src/core/server/{index.test.mocks.ts => server.test.mocks.ts} (89%) delete mode 100644 test/plugin_functional/plugins/core_legacy_compat/package.json delete mode 100644 test/plugin_functional/plugins/core_legacy_compat/tsconfig.json rename test/plugin_functional/plugins/{core_legacy_compat => core_plugin_legacy}/public/application.tsx (100%) rename test/plugin_functional/plugins/{core_legacy_compat => core_plugin_legacy}/public/index.ts (100%) create mode 100644 test/plugin_functional/services/index.js rename test/plugin_functional/{plugins/core_legacy_compat/index.ts => services/supertest.js} (66%) diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index 460f2b3839aec..ec6f020a3490b 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -168,6 +168,22 @@ describe('#setup()', () => { expect(MockContextService.setup).toHaveBeenCalledTimes(1); }); + it('injects legacy dependency to context#setup()', async () => { + const pluginA = Symbol(); + const pluginB = Symbol(); + const pluginDependencies = new Map([[pluginA, []], [pluginB, [pluginA]]]); + MockPluginsService.getOpaqueIds.mockReturnValue(pluginDependencies); + await setupCore(); + + expect(MockContextService.setup).toHaveBeenCalledWith({ + pluginDependencies: new Map([ + [pluginA, []], + [pluginB, [pluginA]], + [MockLegacyPlatformService.legacyId, [pluginA, pluginB]], + ]), + }); + }); + it('calls injectedMetadata#setup()', async () => { await setupCore(); expect(MockInjectedMetadataService.setup).toHaveBeenCalledTimes(1); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 7104b9e10c741..71e9c39df7aae 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -161,9 +161,14 @@ export class CoreSystem { const pluginDependencies = this.plugins.getOpaqueIds(); const context = this.context.setup({ - // We inject a fake "legacy plugin" with no dependencies so that legacy plugins can register context providers - // that will only be available to other legacy plugins and will not leak into New Platform plugins. - pluginDependencies: new Map([...pluginDependencies, [this.legacy.legacyId, []]]), + // We inject a fake "legacy plugin" with dependencies on every plugin so that legacy plugins: + // 1) Can access context from any NP plugin + // 2) Can register context providers that will only be available to other legacy plugins and will not leak into + // New Platform plugins. + pluginDependencies: new Map([ + ...pluginDependencies, + [this.legacy.legacyId, [...pluginDependencies.keys()]], + ]), }); const application = this.application.setup({ context }); diff --git a/src/core/public/legacy/legacy_service.mock.ts b/src/core/public/legacy/legacy_service.mock.ts index bf5d4f67059cb..0c8d9682185d5 100644 --- a/src/core/public/legacy/legacy_service.mock.ts +++ b/src/core/public/legacy/legacy_service.mock.ts @@ -18,9 +18,11 @@ */ import { LegacyPlatformService } from './legacy_service'; -type LegacyPlatformServiceContract = PublicMethodsOf; +// Use Required to get only public properties +type LegacyPlatformServiceContract = Required; const createMock = () => { const mocked: jest.Mocked = { + legacyId: Symbol(), setup: jest.fn(), start: jest.fn(), stop: jest.fn(), diff --git a/src/core/server/index.test.mocks.ts b/src/core/server/server.test.mocks.ts similarity index 89% rename from src/core/server/index.test.mocks.ts rename to src/core/server/server.test.mocks.ts index 12cba7b29fc78..f8eb5e32f4c5a 100644 --- a/src/core/server/index.test.mocks.ts +++ b/src/core/server/server.test.mocks.ts @@ -36,6 +36,7 @@ jest.doMock('./elasticsearch/elasticsearch_service', () => ({ })); export const mockLegacyService = { + legacyId: Symbol(), setup: jest.fn().mockReturnValue({ uiExports: {} }), start: jest.fn(), stop: jest.fn(), @@ -55,3 +56,9 @@ export const mockSavedObjectsService = savedObjectsServiceMock.create(); jest.doMock('./saved_objects/saved_objects_service', () => ({ SavedObjectsService: jest.fn(() => mockSavedObjectsService), })); + +import { contextServiceMock } from './context/context_service.mock'; +export const mockContextService = contextServiceMock.create(); +jest.doMock('./context/context_service', () => ({ + ContextService: jest.fn(() => mockContextService), +})); diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 7319e4126fc07..aee6461580654 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -24,7 +24,8 @@ import { mockPluginsService, mockConfigService, mockSavedObjectsService, -} from './index.test.mocks'; + mockContextService, +} from './server.test.mocks'; import { BehaviorSubject } from 'rxjs'; import { Env, Config, ObjectToConfigAdapter } from './config'; @@ -64,6 +65,25 @@ test('sets up services on "setup"', async () => { expect(mockSavedObjectsService.setup).toHaveBeenCalledTimes(1); }); +test('injects legacy dependency to context#setup()', async () => { + const server = new Server(config$, env, logger); + + const pluginA = Symbol(); + const pluginB = Symbol(); + const pluginDependencies = new Map([[pluginA, []], [pluginB, [pluginA]]]); + mockPluginsService.discover.mockResolvedValue(pluginDependencies); + + await server.setup(); + + expect(mockContextService.setup).toHaveBeenCalledWith({ + pluginDependencies: new Map([ + [pluginA, []], + [pluginB, [pluginA]], + [mockLegacyService.legacyId, [pluginA, pluginB]], + ]), + }); +}); + test('runs services on "start"', async () => { const server = new Server(config$, env, logger); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 3e4f5c2e5a813..ffe910a87385a 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -78,9 +78,14 @@ export class Server { // Discover any plugins before continuing. This allows other systems to utilize the plugin dependency graph. const pluginDependencies = await this.plugins.discover(); const contextServiceSetup = this.context.setup({ - // We inject a fake "legacy plugin" with no dependencies so that legacy plugins can register context providers - // that will only be available to other legacy plugins and will not leak into New Platform plugins. - pluginDependencies: new Map([...pluginDependencies, [this.legacy.legacyId, []]]), + // We inject a fake "legacy plugin" with dependencies on every plugin so that legacy plugins: + // 1) Can access context from any NP plugin + // 2) Can register context providers that will only be available to other legacy plugins and will not leak into + // New Platform plugins. + pluginDependencies: new Map([ + ...pluginDependencies, + [this.legacy.legacyId, [...pluginDependencies.keys()]], + ]), }); const httpSetup = await this.http.setup({ diff --git a/test/plugin_functional/config.js b/test/plugin_functional/config.js index 7d4ece98249f8..e5ad767349358 100644 --- a/test/plugin_functional/config.js +++ b/test/plugin_functional/config.js @@ -19,6 +19,7 @@ import path from 'path'; import fs from 'fs'; +import { services } from './services'; export default async function ({ readConfigFile }) { const functionalConfig = await readConfigFile(require.resolve('../functional/config')); @@ -48,7 +49,10 @@ export default async function ({ readConfigFile }) { require.resolve('./test_suites/core_plugins'), ], - services: functionalConfig.get('services'), + services: { + ...functionalConfig.get('services'), + ...services, + }, pageObjects: functionalConfig.get('pageObjects'), servers: functionalConfig.get('servers'), esTestCluster: functionalConfig.get('esTestCluster'), diff --git a/test/plugin_functional/plugins/core_legacy_compat/package.json b/test/plugin_functional/plugins/core_legacy_compat/package.json deleted file mode 100644 index b42847356b9c2..0000000000000 --- a/test/plugin_functional/plugins/core_legacy_compat/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "core_legacy_compat", - "version": "1.0.0", - "main": "target/test/plugin_functional/plugins/core_legacy_compat", - "kibana": { - "version": "kibana", - "templateVersion": "1.0.0" - }, - "license": "Apache-2.0", - "scripts": { - "kbn": "node ../../../../scripts/kbn.js", - "build": "rm -rf './target' && tsc" - }, - "devDependencies": { - "typescript": "3.5.3" - } -} diff --git a/test/plugin_functional/plugins/core_legacy_compat/tsconfig.json b/test/plugin_functional/plugins/core_legacy_compat/tsconfig.json deleted file mode 100644 index 4a564ee1e5578..0000000000000 --- a/test/plugin_functional/plugins/core_legacy_compat/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../../../../tsconfig.json", - "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true - }, - "include": [ - "index.ts", - "public/**/*.ts", - "public/**/*.tsx", - "../../../../typings/**/*" - ], - "exclude": [] -} diff --git a/test/plugin_functional/plugins/core_plugin_legacy/index.ts b/test/plugin_functional/plugins/core_plugin_legacy/index.ts index b9cd4a7e8bb34..d8fcb534a0bb3 100644 --- a/test/plugin_functional/plugins/core_plugin_legacy/index.ts +++ b/test/plugin_functional/plugins/core_plugin_legacy/index.ts @@ -24,6 +24,13 @@ export default function(kibana: any) { return new kibana.Plugin({ id: 'core_plugin_legacy', require: ['kibana'], + uiExports: { + app: { + title: 'Core Legacy Compat', + description: 'This is a sample plugin to test core to legacy compatibility', + main: 'plugins/core_plugin_legacy/index', + }, + }, init(server: KbnServer) { const { http } = server.newPlatform.setup.core; const router = http.createRouter(); @@ -32,6 +39,11 @@ export default function(kibana: any) { const response = await context.core.elasticsearch.adminClient.callAsInternalUser('ping'); return res.ok({ body: `Pong in legacy via new platform: ${response}` }); }); + + router.get({ path: '/api/np-context-in-legacy', validate: false }, (context, req, res) => { + const contexts = Object.keys(context); + return res.ok({ body: { contexts } }); + }); }, }); } diff --git a/test/plugin_functional/plugins/core_legacy_compat/public/application.tsx b/test/plugin_functional/plugins/core_plugin_legacy/public/application.tsx similarity index 100% rename from test/plugin_functional/plugins/core_legacy_compat/public/application.tsx rename to test/plugin_functional/plugins/core_plugin_legacy/public/application.tsx diff --git a/test/plugin_functional/plugins/core_legacy_compat/public/index.ts b/test/plugin_functional/plugins/core_plugin_legacy/public/index.ts similarity index 100% rename from test/plugin_functional/plugins/core_legacy_compat/public/index.ts rename to test/plugin_functional/plugins/core_plugin_legacy/public/index.ts diff --git a/test/plugin_functional/services/index.js b/test/plugin_functional/services/index.js new file mode 100644 index 0000000000000..bf02587772f4b --- /dev/null +++ b/test/plugin_functional/services/index.js @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { KibanaSupertestProvider } from './supertest'; + +export const services = { + supertest: KibanaSupertestProvider, +}; diff --git a/test/plugin_functional/plugins/core_legacy_compat/index.ts b/test/plugin_functional/services/supertest.js similarity index 66% rename from test/plugin_functional/plugins/core_legacy_compat/index.ts rename to test/plugin_functional/services/supertest.js index dbec480816882..390f89acaa775 100644 --- a/test/plugin_functional/plugins/core_legacy_compat/index.ts +++ b/test/plugin_functional/services/supertest.js @@ -17,19 +17,13 @@ * under the License. */ -import { Server } from 'hapi'; -// eslint-disable-next-line import/no-default-export -export default function(kibana: any) { - return new kibana.Plugin({ - uiExports: { - app: { - title: 'Core Legacy Compat', - description: 'This is a sample plugin to test core to legacy compatibility', - main: 'plugins/core_legacy_compat/index', - }, - }, +import { format as formatUrl } from 'url'; - init(server: Server) {}, - }); +import supertestAsPromised from 'supertest-as-promised'; + +export function KibanaSupertestProvider({ getService }) { + const config = getService('config'); + const kibanaServerUrl = formatUrl(config.get('servers.kibana')); + return supertestAsPromised(kibanaServerUrl); } diff --git a/test/plugin_functional/test_suites/core_plugins/legacy_plugins.js b/test/plugin_functional/test_suites/core_plugins/legacy_plugins.js index 4e421f4523750..c6edf803c9938 100644 --- a/test/plugin_functional/test_suites/core_plugins/legacy_plugins.js +++ b/test/plugin_functional/test_suites/core_plugins/legacy_plugins.js @@ -21,21 +21,29 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common']); - const browser = getService('browser'); const testSubjects = getService('testSubjects'); + const supertest = getService('supertest'); describe('legacy plugins', function describeIndexTests() { - it('have access to New Platform HTTP service', async () => { - const url = `${PageObjects.common.getHostPort()}/api/np-http-in-legacy`; - await browser.get(url); + describe('http', () => { + it('has access to New Platform HTTP service', async () => { + await supertest + .get('/api/np-http-in-legacy') + .expect(200) + .expect('Pong in legacy via new platform: true'); + }); - const pageSource = await browser.execute('return window.document.body.textContent;'); - expect(pageSource).to.equal('Pong in legacy via new platform: true'); + it('has access to New Platform HTTP context providers', async () => { + await supertest + .get('/api/np-context-in-legacy') + .expect(200) + .expect(JSON.stringify({ contexts: ['core', 'search', 'pluginA'] })); + }); }); describe('application service compatibility layer', function describeIndexTests() { it('can render legacy apps', async () => { - await PageObjects.common.navigateToApp('core_legacy_compat'); + await PageObjects.common.navigateToApp('core_plugin_legacy'); expect(await testSubjects.exists('coreLegacyCompatH1')).to.be(true); }); }); From 77247773b9cdcfc2257713f1df2a49e1c31d7066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Wed, 16 Oct 2019 19:38:42 +0200 Subject: [PATCH 42/48] [APM] Remove `type` from agent configuration (#48404) --- x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts | 5 ++++- .../agent_configuration/create_or_update_configuration.ts | 5 ++--- .../settings/agent_configuration/mark_applied_by_agent.ts | 1 - 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts index 03cfa7e0c8a83..79fcfe95c0552 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts @@ -17,6 +17,9 @@ import { cloneDeep, has, isString, set } from 'lodash'; import { OBSERVER_VERSION_MAJOR } from '../../../common/elasticsearch_fieldnames'; import { StringMap } from '../../../typings/common'; +// `type` was deprecated in 7.0 +export type APMIndexDocumentParams = Omit, 'type'>; + function getApmIndices(config: Legacy.KibanaConfig) { return [ config.get('apm_oss.errorIndices'), @@ -118,7 +121,7 @@ export function getESClient(req: Legacy.Request) { AggregationSearchResponseWithTotalHitsAsObject >; }, - index: (params: IndexDocumentParams) => { + index: (params: APMIndexDocumentParams) => { return cluster.callWithRequest(req, 'index', params); }, delete: (params: IndicesDeleteParams) => { diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts index 6450040098cd4..9f0d8e2f1c718 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts @@ -5,9 +5,9 @@ */ import hash from 'object-hash'; -import { IndexDocumentParams } from 'elasticsearch'; import { Setup } from '../../helpers/setup_request'; import { AgentConfiguration } from './configuration_types'; +import { APMIndexDocumentParams } from '../../helpers/es_client'; export async function createOrUpdateConfiguration({ configurationId, @@ -23,8 +23,7 @@ export async function createOrUpdateConfiguration({ }) { const { client, config } = setup; - const params: IndexDocumentParams = { - type: '_doc', + const params: APMIndexDocumentParams = { refresh: true, index: config.get('apm_oss.apmAgentConfigurationIndex'), body: { diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts index ea2b15e6985d5..867045142cea0 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts @@ -19,7 +19,6 @@ export async function markAppliedByAgent({ const { client, config } = setup; const params = { - type: '_doc', index: config.get('apm_oss.apmAgentConfigurationIndex'), id, // by specifying the `id` elasticsearch will do an "upsert" body: { From 853ddcd09c8c0b1eaf7e6aa1b17726d4bce95840 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Wed, 16 Oct 2019 10:43:46 -0700 Subject: [PATCH 43/48] [Reporting/Screenshots] Add step to skip telemetry (#48312) --- .../common/lib/screenshots/index.ts | 5 ++++ .../common/lib/screenshots/skip_telemetry.ts | 29 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/skip_telemetry.ts diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts index bcf6c63b08dd8..9713adc76a5fa 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts @@ -25,6 +25,7 @@ import { waitForElementsToBeInDOM } from './wait_for_dom_elements'; import { getTimeRange } from './get_time_range'; import { getElementPositionAndAttributes } from './get_element_position_data'; import { getScreenshots } from './get_screenshots'; +import { skipTelemetry } from './skip_telemetry'; // NOTE: Typescript does not throw an error if this interface has errors! interface ScreenshotResults { @@ -56,6 +57,10 @@ export function screenshotsObservableFactory(server: KbnServer) { (browser: HeadlessBrowser) => openUrl(browser, url, conditionalHeaders, logger), browser => browser ), + mergeMap( + (browser: HeadlessBrowser) => skipTelemetry(browser, logger), + browser => browser + ), mergeMap( (browser: HeadlessBrowser) => { logger.debug( diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/skip_telemetry.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/skip_telemetry.ts new file mode 100644 index 0000000000000..367354032a843 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/skip_telemetry.ts @@ -0,0 +1,29 @@ +/* + * 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 { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver'; +import { LevelLogger } from '../../../../server/lib'; + +const LAST_REPORT_STORAGE_KEY = 'xpack.data'; + +export async function skipTelemetry(browser: HeadlessBrowser, logger: LevelLogger) { + const storageData = await browser.evaluate({ + fn: storageKey => { + // set something + const optOutJSON = JSON.stringify({ lastReport: Date.now() }); + localStorage.setItem(storageKey, optOutJSON); + + // get it + const session = localStorage.getItem(storageKey); + + // return it + return session; + }, + args: [LAST_REPORT_STORAGE_KEY], + }); + + logger.debug(`added data to localStorage to skip telmetry: ${storageData}`); +} From df53c806ba2bd282fe417030c57c670690e8edbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Wed, 16 Oct 2019 21:20:14 +0200 Subject: [PATCH 44/48] Add 7.5 to .backportrc.json --- .backportrc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.backportrc.json b/.backportrc.json index 30b18b7561eaa..af064451595c8 100644 --- a/.backportrc.json +++ b/.backportrc.json @@ -1,5 +1,5 @@ { "upstream": "elastic/kibana", - "branches": [{ "name": "7.x", "checked": true }, "7.4", "7.3", "7.2", "7.1", "7.0", "6.8", "6.7", "6.6", "6.5", "6.4", "6.3", "6.2", "6.1", "6.0", "5.6"], + "branches": [{ "name": "7.x", "checked": true }, "7.5", "7.4", "7.3", "7.2", "7.1", "7.0", "6.8", "6.7", "6.6", "6.5", "6.4", "6.3", "6.2", "6.1", "6.0", "5.6"], "labels": ["backport"] } From 53e1c95c7696d4155a76cbea09f35668286fc67c Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Wed, 16 Oct 2019 21:24:12 +0200 Subject: [PATCH 45/48] Move doc, doc_viewer, context to discover folder (#47436) * Move doc folder to discover folder * Move doc_viewer folder to discover folder * Move context folder to discover folder * Move context scss import to discover folder * Introduce angular folder, start migration --- .../public/context/components/action_bar/_index.scss | 1 - .../core_plugins/kibana/public/discover/_index.scss | 8 +++++++- .../directives/__snapshots__/no_results.test.js.snap | 0 .../discover/{ => angular}/directives/_histogram.scss | 0 .../public/discover/{ => angular}/directives/_index.scss | 0 .../discover/{ => angular}/directives/_no_results.scss | 0 .../discover/{ => angular}/directives/histogram.tsx | 0 .../public/discover/{ => angular}/directives/index.js | 2 +- .../discover/{ => angular}/directives/no_results.js | 0 .../discover/{ => angular}/directives/no_results.test.js | 0 .../discover/{ => angular}/directives/uninitialized.tsx | 0 .../{ => angular}/directives/unsupported_index_pattern.js | 0 .../public/discover/{controllers => angular}/discover.js | 0 .../{controllers => angular}/get_painless_error.ts | 0 .../kibana/public/{ => discover}/context/NOTES.md | 4 ++-- .../kibana/public/{ => discover}/context/_index.scss | 0 .../public/{ => discover}/context/api/__tests__/_stubs.js | 0 .../public/{ => discover}/context/api/__tests__/anchor.js | 0 .../{ => discover}/context/api/__tests__/predecessors.js | 0 .../{ => discover}/context/api/__tests__/successors.js | 0 .../kibana/public/{ => discover}/context/api/anchor.js | 0 .../kibana/public/{ => discover}/context/api/context.ts | 2 +- .../context/api/utils/__tests__/date_conversion.test.ts | 0 .../context/api/utils/__tests__/sorting.test.ts | 0 .../{ => discover}/context/api/utils/date_conversion.ts | 0 .../context/api/utils/fetch_hits_in_interval.ts | 0 .../context/api/utils/generate_intervals.ts | 0 .../context/api/utils/get_es_query_search_after.ts | 0 .../{ => discover}/context/api/utils/get_es_query_sort.ts | 0 .../public/{ => discover}/context/api/utils/sorting.ts | 0 .../kibana/public/{ => discover}/context/app.html | 0 .../kibana/public/{ => discover}/context/app.js | 2 +- .../context/components/action_bar/_action_bar.scss | 0 .../discover/context/components/action_bar/_index.scss | 1 + .../context/components/action_bar/action_bar.test.tsx | 0 .../context/components/action_bar/action_bar.tsx | 0 .../context/components/action_bar/action_bar_directive.ts | 0 .../context/components/action_bar/action_bar_warning.tsx | 0 .../{ => discover}/context/components/action_bar/index.ts | 0 .../kibana/public/{ => discover}/context/index.html | 0 .../kibana/public/{ => discover}/context/index.js | 2 +- .../kibana/public/{ => discover}/context/query/actions.js | 2 +- .../public/{ => discover}/context/query/constants.js | 0 .../kibana/public/{ => discover}/context/query/index.js | 0 .../kibana/public/{ => discover}/context/query/state.js | 0 .../context/query_parameters/__tests__/_utils.js | 0 .../query_parameters/__tests__/action_add_filter.js | 0 .../__tests__/action_set_predecessor_count.js | 0 .../__tests__/action_set_query_parameters.js | 0 .../__tests__/action_set_successor_count.js | 0 .../{ => discover}/context/query_parameters/actions.js | 0 .../{ => discover}/context/query_parameters/constants.ts | 0 .../{ => discover}/context/query_parameters/index.js | 0 .../{ => discover}/context/query_parameters/state.ts | 0 .../kibana/public/{ => discover}/doc/doc.test.tsx | 0 .../core_plugins/kibana/public/{ => discover}/doc/doc.tsx | 2 +- .../kibana/public/{ => discover}/doc/doc_directive.ts | 0 .../kibana/public/{ => discover}/doc/index.html | 0 .../kibana/public/{ => discover}/doc/index.ts | 0 .../public/{ => discover}/doc/use_es_doc_search.test.tsx | 0 .../kibana/public/{ => discover}/doc/use_es_doc_search.ts | 2 +- .../public/discover/doc_table/components/table_row.js | 2 +- .../doc_viewer/__snapshots__/doc_viewer.test.tsx.snap | 0 .../__snapshots__/doc_viewer_render_tab.test.tsx.snap | 0 .../public/{ => discover}/doc_viewer/_doc_viewer.scss | 0 .../kibana/public/discover/doc_viewer/_index.scss | 1 + .../public/{ => discover}/doc_viewer/doc_viewer.test.tsx | 0 .../public/{ => discover}/doc_viewer/doc_viewer.tsx | 0 .../{ => discover}/doc_viewer/doc_viewer_directive.ts | 0 .../{ => discover}/doc_viewer/doc_viewer_render_error.tsx | 2 +- .../doc_viewer/doc_viewer_render_tab.test.tsx | 0 .../{ => discover}/doc_viewer/doc_viewer_render_tab.tsx | 0 .../public/{ => discover}/doc_viewer/doc_viewer_tab.tsx | 0 .../{doc_viewer/index.js => discover/doc_viewer/index.ts} | 2 ++ src/legacy/core_plugins/kibana/public/discover/index.js | 7 +++++-- .../core_plugins/kibana/public/doc_viewer/_index.scss | 1 - src/legacy/core_plugins/kibana/public/index.scss | 6 ------ src/legacy/core_plugins/kibana/public/kibana.js | 2 -- src/legacy/ui/public/registry/doc_views_helpers.tsx | 2 +- 79 files changed, 28 insertions(+), 25 deletions(-) delete mode 100644 src/legacy/core_plugins/kibana/public/context/components/action_bar/_index.scss rename src/legacy/core_plugins/kibana/public/discover/{ => angular}/directives/__snapshots__/no_results.test.js.snap (100%) rename src/legacy/core_plugins/kibana/public/discover/{ => angular}/directives/_histogram.scss (100%) rename src/legacy/core_plugins/kibana/public/discover/{ => angular}/directives/_index.scss (100%) rename src/legacy/core_plugins/kibana/public/discover/{ => angular}/directives/_no_results.scss (100%) rename src/legacy/core_plugins/kibana/public/discover/{ => angular}/directives/histogram.tsx (100%) rename src/legacy/core_plugins/kibana/public/discover/{ => angular}/directives/index.js (96%) rename src/legacy/core_plugins/kibana/public/discover/{ => angular}/directives/no_results.js (100%) rename src/legacy/core_plugins/kibana/public/discover/{ => angular}/directives/no_results.test.js (100%) rename src/legacy/core_plugins/kibana/public/discover/{ => angular}/directives/uninitialized.tsx (100%) rename src/legacy/core_plugins/kibana/public/discover/{ => angular}/directives/unsupported_index_pattern.js (100%) rename src/legacy/core_plugins/kibana/public/discover/{controllers => angular}/discover.js (100%) rename src/legacy/core_plugins/kibana/public/discover/{controllers => angular}/get_painless_error.ts (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/NOTES.md (97%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/_index.scss (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/api/__tests__/_stubs.js (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/api/__tests__/anchor.js (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/api/__tests__/predecessors.js (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/api/__tests__/successors.js (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/api/anchor.js (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/api/context.ts (98%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/api/utils/__tests__/date_conversion.test.ts (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/api/utils/__tests__/sorting.test.ts (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/api/utils/date_conversion.ts (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/api/utils/fetch_hits_in_interval.ts (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/api/utils/generate_intervals.ts (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/api/utils/get_es_query_search_after.ts (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/api/utils/get_es_query_sort.ts (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/api/utils/sorting.ts (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/app.html (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/app.js (99%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/components/action_bar/_action_bar.scss (100%) create mode 100644 src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/_index.scss rename src/legacy/core_plugins/kibana/public/{ => discover}/context/components/action_bar/action_bar.test.tsx (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/components/action_bar/action_bar.tsx (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/components/action_bar/action_bar_directive.ts (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/components/action_bar/action_bar_warning.tsx (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/components/action_bar/index.ts (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/index.html (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/index.js (98%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/query/actions.js (98%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/query/constants.js (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/query/index.js (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/query/state.js (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/query_parameters/__tests__/_utils.js (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/query_parameters/__tests__/action_add_filter.js (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/query_parameters/__tests__/action_set_predecessor_count.js (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/query_parameters/__tests__/action_set_query_parameters.js (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/query_parameters/__tests__/action_set_successor_count.js (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/query_parameters/actions.js (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/query_parameters/constants.ts (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/query_parameters/index.js (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/context/query_parameters/state.ts (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/doc/doc.test.tsx (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/doc/doc.tsx (98%) rename src/legacy/core_plugins/kibana/public/{ => discover}/doc/doc_directive.ts (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/doc/index.html (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/doc/index.ts (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/doc/use_es_doc_search.test.tsx (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/doc/use_es_doc_search.ts (97%) rename src/legacy/core_plugins/kibana/public/{ => discover}/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/doc_viewer/__snapshots__/doc_viewer_render_tab.test.tsx.snap (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/doc_viewer/_doc_viewer.scss (100%) create mode 100644 src/legacy/core_plugins/kibana/public/discover/doc_viewer/_index.scss rename src/legacy/core_plugins/kibana/public/{ => discover}/doc_viewer/doc_viewer.test.tsx (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/doc_viewer/doc_viewer.tsx (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/doc_viewer/doc_viewer_directive.ts (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/doc_viewer/doc_viewer_render_error.tsx (94%) rename src/legacy/core_plugins/kibana/public/{ => discover}/doc_viewer/doc_viewer_render_tab.test.tsx (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/doc_viewer/doc_viewer_render_tab.tsx (100%) rename src/legacy/core_plugins/kibana/public/{ => discover}/doc_viewer/doc_viewer_tab.tsx (100%) rename src/legacy/core_plugins/kibana/public/{doc_viewer/index.js => discover/doc_viewer/index.ts} (96%) delete mode 100644 src/legacy/core_plugins/kibana/public/doc_viewer/_index.scss diff --git a/src/legacy/core_plugins/kibana/public/context/components/action_bar/_index.scss b/src/legacy/core_plugins/kibana/public/context/components/action_bar/_index.scss deleted file mode 100644 index 1f54ecea5e1cb..0000000000000 --- a/src/legacy/core_plugins/kibana/public/context/components/action_bar/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './action_bar'; diff --git a/src/legacy/core_plugins/kibana/public/discover/_index.scss b/src/legacy/core_plugins/kibana/public/discover/_index.scss index 57a6b89c37722..0b0bd12cb268b 100644 --- a/src/legacy/core_plugins/kibana/public/discover/_index.scss +++ b/src/legacy/core_plugins/kibana/public/discover/_index.scss @@ -10,7 +10,7 @@ @import 'components/fetch_error/index'; @import 'components/field_chooser/index'; -@import 'directives/index'; +@import 'angular/directives/index'; @import 'doc_table/index'; @import 'hacks'; @@ -18,3 +18,9 @@ @import 'discover'; @import 'embeddable/index'; + +// Doc Viewer +@import 'doc_viewer/index'; + +// Context styles +@import 'context/index'; diff --git a/src/legacy/core_plugins/kibana/public/discover/directives/__snapshots__/no_results.test.js.snap b/src/legacy/core_plugins/kibana/public/discover/angular/directives/__snapshots__/no_results.test.js.snap similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/directives/__snapshots__/no_results.test.js.snap rename to src/legacy/core_plugins/kibana/public/discover/angular/directives/__snapshots__/no_results.test.js.snap diff --git a/src/legacy/core_plugins/kibana/public/discover/directives/_histogram.scss b/src/legacy/core_plugins/kibana/public/discover/angular/directives/_histogram.scss similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/directives/_histogram.scss rename to src/legacy/core_plugins/kibana/public/discover/angular/directives/_histogram.scss diff --git a/src/legacy/core_plugins/kibana/public/discover/directives/_index.scss b/src/legacy/core_plugins/kibana/public/discover/angular/directives/_index.scss similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/directives/_index.scss rename to src/legacy/core_plugins/kibana/public/discover/angular/directives/_index.scss diff --git a/src/legacy/core_plugins/kibana/public/discover/directives/_no_results.scss b/src/legacy/core_plugins/kibana/public/discover/angular/directives/_no_results.scss similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/directives/_no_results.scss rename to src/legacy/core_plugins/kibana/public/discover/angular/directives/_no_results.scss diff --git a/src/legacy/core_plugins/kibana/public/discover/directives/histogram.tsx b/src/legacy/core_plugins/kibana/public/discover/angular/directives/histogram.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/directives/histogram.tsx rename to src/legacy/core_plugins/kibana/public/discover/angular/directives/histogram.tsx diff --git a/src/legacy/core_plugins/kibana/public/discover/directives/index.js b/src/legacy/core_plugins/kibana/public/discover/angular/directives/index.js similarity index 96% rename from src/legacy/core_plugins/kibana/public/discover/directives/index.js rename to src/legacy/core_plugins/kibana/public/discover/angular/directives/index.js index dc834c02759e0..27918bd704f5a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/directives/index.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/directives/index.js @@ -20,7 +20,7 @@ import 'ngreact'; import { wrapInI18nContext } from 'ui/i18n'; import { uiModules } from 'ui/modules'; -import '../../../../../ui/public/render_complete/directive'; +import '../../../../../../ui/public/render_complete/directive'; import { DiscoverNoResults } from './no_results'; import { DiscoverUninitialized } from './uninitialized'; diff --git a/src/legacy/core_plugins/kibana/public/discover/directives/no_results.js b/src/legacy/core_plugins/kibana/public/discover/angular/directives/no_results.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/directives/no_results.js rename to src/legacy/core_plugins/kibana/public/discover/angular/directives/no_results.js diff --git a/src/legacy/core_plugins/kibana/public/discover/directives/no_results.test.js b/src/legacy/core_plugins/kibana/public/discover/angular/directives/no_results.test.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/directives/no_results.test.js rename to src/legacy/core_plugins/kibana/public/discover/angular/directives/no_results.test.js diff --git a/src/legacy/core_plugins/kibana/public/discover/directives/uninitialized.tsx b/src/legacy/core_plugins/kibana/public/discover/angular/directives/uninitialized.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/directives/uninitialized.tsx rename to src/legacy/core_plugins/kibana/public/discover/angular/directives/uninitialized.tsx diff --git a/src/legacy/core_plugins/kibana/public/discover/directives/unsupported_index_pattern.js b/src/legacy/core_plugins/kibana/public/discover/angular/directives/unsupported_index_pattern.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/directives/unsupported_index_pattern.js rename to src/legacy/core_plugins/kibana/public/discover/angular/directives/unsupported_index_pattern.js diff --git a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js b/src/legacy/core_plugins/kibana/public/discover/angular/discover.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/controllers/discover.js rename to src/legacy/core_plugins/kibana/public/discover/angular/discover.js diff --git a/src/legacy/core_plugins/kibana/public/discover/controllers/get_painless_error.ts b/src/legacy/core_plugins/kibana/public/discover/angular/get_painless_error.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/controllers/get_painless_error.ts rename to src/legacy/core_plugins/kibana/public/discover/angular/get_painless_error.ts diff --git a/src/legacy/core_plugins/kibana/public/context/NOTES.md b/src/legacy/core_plugins/kibana/public/discover/context/NOTES.md similarity index 97% rename from src/legacy/core_plugins/kibana/public/context/NOTES.md rename to src/legacy/core_plugins/kibana/public/discover/context/NOTES.md index 445080c215998..7aaa251348961 100644 --- a/src/legacy/core_plugins/kibana/public/context/NOTES.md +++ b/src/legacy/core_plugins/kibana/public/discover/context/NOTES.md @@ -29,11 +29,11 @@ employed in some cases, e.g. when dealing with the isolate scope bindings in **Loose Coupling**: An attempt was made to couple the parts that make up this app as loosely as possible. This means using pure functions whenever possible and isolating the angular directives diligently. To that end, the app has been -implemented as the independent `ContextApp` directive in [app.js](./app.js). It +implemented as the independent `ContextApp` directive in [app.js](app.js). It does not access the Kibana `AppState` directly but communicates only via its directive properties. The binding of these attributes to the state and thereby to the route is performed by the `CreateAppRouteController`in -[index.js](./index.js). Similarly, the `SizePicker` directive only communicates +[index.js](index.js). Similarly, the `SizePicker` directive only communicates with its parent via the passed properties. diff --git a/src/legacy/core_plugins/kibana/public/context/_index.scss b/src/legacy/core_plugins/kibana/public/discover/context/_index.scss similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/_index.scss rename to src/legacy/core_plugins/kibana/public/discover/context/_index.scss diff --git a/src/legacy/core_plugins/kibana/public/context/api/__tests__/_stubs.js b/src/legacy/core_plugins/kibana/public/discover/context/api/__tests__/_stubs.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/api/__tests__/_stubs.js rename to src/legacy/core_plugins/kibana/public/discover/context/api/__tests__/_stubs.js diff --git a/src/legacy/core_plugins/kibana/public/context/api/__tests__/anchor.js b/src/legacy/core_plugins/kibana/public/discover/context/api/__tests__/anchor.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/api/__tests__/anchor.js rename to src/legacy/core_plugins/kibana/public/discover/context/api/__tests__/anchor.js diff --git a/src/legacy/core_plugins/kibana/public/context/api/__tests__/predecessors.js b/src/legacy/core_plugins/kibana/public/discover/context/api/__tests__/predecessors.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/api/__tests__/predecessors.js rename to src/legacy/core_plugins/kibana/public/discover/context/api/__tests__/predecessors.js diff --git a/src/legacy/core_plugins/kibana/public/context/api/__tests__/successors.js b/src/legacy/core_plugins/kibana/public/discover/context/api/__tests__/successors.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/api/__tests__/successors.js rename to src/legacy/core_plugins/kibana/public/discover/context/api/__tests__/successors.js diff --git a/src/legacy/core_plugins/kibana/public/context/api/anchor.js b/src/legacy/core_plugins/kibana/public/discover/context/api/anchor.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/api/anchor.js rename to src/legacy/core_plugins/kibana/public/discover/context/api/anchor.js diff --git a/src/legacy/core_plugins/kibana/public/context/api/context.ts b/src/legacy/core_plugins/kibana/public/discover/context/api/context.ts similarity index 98% rename from src/legacy/core_plugins/kibana/public/context/api/context.ts rename to src/legacy/core_plugins/kibana/public/discover/context/api/context.ts index baecf8a673521..39c7421d3b912 100644 --- a/src/legacy/core_plugins/kibana/public/context/api/context.ts +++ b/src/legacy/core_plugins/kibana/public/discover/context/api/context.ts @@ -18,7 +18,7 @@ */ // @ts-ignore -import { SearchSourceProvider, SearchSource } from 'ui/courier'; +import { SearchSourceProvider } from 'ui/courier'; import { IPrivate } from 'ui/private'; import { Filter } from '@kbn/es-query'; import { IndexPatterns, IndexPattern } from 'ui/index_patterns'; diff --git a/src/legacy/core_plugins/kibana/public/context/api/utils/__tests__/date_conversion.test.ts b/src/legacy/core_plugins/kibana/public/discover/context/api/utils/__tests__/date_conversion.test.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/api/utils/__tests__/date_conversion.test.ts rename to src/legacy/core_plugins/kibana/public/discover/context/api/utils/__tests__/date_conversion.test.ts diff --git a/src/legacy/core_plugins/kibana/public/context/api/utils/__tests__/sorting.test.ts b/src/legacy/core_plugins/kibana/public/discover/context/api/utils/__tests__/sorting.test.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/api/utils/__tests__/sorting.test.ts rename to src/legacy/core_plugins/kibana/public/discover/context/api/utils/__tests__/sorting.test.ts diff --git a/src/legacy/core_plugins/kibana/public/context/api/utils/date_conversion.ts b/src/legacy/core_plugins/kibana/public/discover/context/api/utils/date_conversion.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/api/utils/date_conversion.ts rename to src/legacy/core_plugins/kibana/public/discover/context/api/utils/date_conversion.ts diff --git a/src/legacy/core_plugins/kibana/public/context/api/utils/fetch_hits_in_interval.ts b/src/legacy/core_plugins/kibana/public/discover/context/api/utils/fetch_hits_in_interval.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/api/utils/fetch_hits_in_interval.ts rename to src/legacy/core_plugins/kibana/public/discover/context/api/utils/fetch_hits_in_interval.ts diff --git a/src/legacy/core_plugins/kibana/public/context/api/utils/generate_intervals.ts b/src/legacy/core_plugins/kibana/public/discover/context/api/utils/generate_intervals.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/api/utils/generate_intervals.ts rename to src/legacy/core_plugins/kibana/public/discover/context/api/utils/generate_intervals.ts diff --git a/src/legacy/core_plugins/kibana/public/context/api/utils/get_es_query_search_after.ts b/src/legacy/core_plugins/kibana/public/discover/context/api/utils/get_es_query_search_after.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/api/utils/get_es_query_search_after.ts rename to src/legacy/core_plugins/kibana/public/discover/context/api/utils/get_es_query_search_after.ts diff --git a/src/legacy/core_plugins/kibana/public/context/api/utils/get_es_query_sort.ts b/src/legacy/core_plugins/kibana/public/discover/context/api/utils/get_es_query_sort.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/api/utils/get_es_query_sort.ts rename to src/legacy/core_plugins/kibana/public/discover/context/api/utils/get_es_query_sort.ts diff --git a/src/legacy/core_plugins/kibana/public/context/api/utils/sorting.ts b/src/legacy/core_plugins/kibana/public/discover/context/api/utils/sorting.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/api/utils/sorting.ts rename to src/legacy/core_plugins/kibana/public/discover/context/api/utils/sorting.ts diff --git a/src/legacy/core_plugins/kibana/public/context/app.html b/src/legacy/core_plugins/kibana/public/discover/context/app.html similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/app.html rename to src/legacy/core_plugins/kibana/public/discover/context/app.html diff --git a/src/legacy/core_plugins/kibana/public/context/app.js b/src/legacy/core_plugins/kibana/public/discover/context/app.js similarity index 99% rename from src/legacy/core_plugins/kibana/public/context/app.js rename to src/legacy/core_plugins/kibana/public/discover/context/app.js index 6b10d80078f80..7754f743632cb 100644 --- a/src/legacy/core_plugins/kibana/public/context/app.js +++ b/src/legacy/core_plugins/kibana/public/discover/context/app.js @@ -38,7 +38,7 @@ import { import { timefilter } from 'ui/timefilter'; // load directives -import '../../../data/public/legacy'; +import '../../../../data/public/legacy'; const module = uiModules.get('apps/context', [ 'elasticsearch', diff --git a/src/legacy/core_plugins/kibana/public/context/components/action_bar/_action_bar.scss b/src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/_action_bar.scss similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/components/action_bar/_action_bar.scss rename to src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/_action_bar.scss diff --git a/src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/_index.scss b/src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/_index.scss new file mode 100644 index 0000000000000..d54e2caffc122 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/_index.scss @@ -0,0 +1 @@ +@import 'action_bar'; diff --git a/src/legacy/core_plugins/kibana/public/context/components/action_bar/action_bar.test.tsx b/src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/action_bar.test.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/components/action_bar/action_bar.test.tsx rename to src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/action_bar.test.tsx diff --git a/src/legacy/core_plugins/kibana/public/context/components/action_bar/action_bar.tsx b/src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/action_bar.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/components/action_bar/action_bar.tsx rename to src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/action_bar.tsx diff --git a/src/legacy/core_plugins/kibana/public/context/components/action_bar/action_bar_directive.ts b/src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/action_bar_directive.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/components/action_bar/action_bar_directive.ts rename to src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/action_bar_directive.ts diff --git a/src/legacy/core_plugins/kibana/public/context/components/action_bar/action_bar_warning.tsx b/src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/action_bar_warning.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/components/action_bar/action_bar_warning.tsx rename to src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/action_bar_warning.tsx diff --git a/src/legacy/core_plugins/kibana/public/context/components/action_bar/index.ts b/src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/components/action_bar/index.ts rename to src/legacy/core_plugins/kibana/public/discover/context/components/action_bar/index.ts diff --git a/src/legacy/core_plugins/kibana/public/context/index.html b/src/legacy/core_plugins/kibana/public/discover/context/index.html similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/index.html rename to src/legacy/core_plugins/kibana/public/discover/context/index.html diff --git a/src/legacy/core_plugins/kibana/public/context/index.js b/src/legacy/core_plugins/kibana/public/discover/context/index.js similarity index 98% rename from src/legacy/core_plugins/kibana/public/context/index.js rename to src/legacy/core_plugins/kibana/public/discover/context/index.js index 71044d36d1aa5..902bee2badb7c 100644 --- a/src/legacy/core_plugins/kibana/public/context/index.js +++ b/src/legacy/core_plugins/kibana/public/discover/context/index.js @@ -25,7 +25,7 @@ import { i18n } from '@kbn/i18n'; import './app'; import contextAppRouteTemplate from './index.html'; -import { getRootBreadcrumbs } from '../discover/breadcrumbs'; +import { getRootBreadcrumbs } from '../breadcrumbs'; import { npStart } from 'ui/new_platform'; import { subscribeWithScope } from 'ui/utils/subscribe_with_scope'; diff --git a/src/legacy/core_plugins/kibana/public/context/query/actions.js b/src/legacy/core_plugins/kibana/public/discover/context/query/actions.js similarity index 98% rename from src/legacy/core_plugins/kibana/public/context/query/actions.js rename to src/legacy/core_plugins/kibana/public/discover/context/query/actions.js index 72624210fcb49..c55dcc374fa5a 100644 --- a/src/legacy/core_plugins/kibana/public/context/query/actions.js +++ b/src/legacy/core_plugins/kibana/public/discover/context/query/actions.js @@ -26,7 +26,7 @@ import { fetchAnchorProvider } from '../api/anchor'; import { fetchContextProvider } from '../api/context'; import { QueryParameterActionsProvider } from '../query_parameters'; import { FAILURE_REASONS, LOADING_STATUS } from './constants'; -import { MarkdownSimple } from '../../../../kibana_react/public'; +import { MarkdownSimple } from '../../../../../kibana_react/public'; export function QueryActionsProvider(Private, Promise) { const fetchAnchor = Private(fetchAnchorProvider); diff --git a/src/legacy/core_plugins/kibana/public/context/query/constants.js b/src/legacy/core_plugins/kibana/public/discover/context/query/constants.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/query/constants.js rename to src/legacy/core_plugins/kibana/public/discover/context/query/constants.js diff --git a/src/legacy/core_plugins/kibana/public/context/query/index.js b/src/legacy/core_plugins/kibana/public/discover/context/query/index.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/query/index.js rename to src/legacy/core_plugins/kibana/public/discover/context/query/index.js diff --git a/src/legacy/core_plugins/kibana/public/context/query/state.js b/src/legacy/core_plugins/kibana/public/discover/context/query/state.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/query/state.js rename to src/legacy/core_plugins/kibana/public/discover/context/query/state.js diff --git a/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/_utils.js b/src/legacy/core_plugins/kibana/public/discover/context/query_parameters/__tests__/_utils.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/_utils.js rename to src/legacy/core_plugins/kibana/public/discover/context/query_parameters/__tests__/_utils.js diff --git a/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_add_filter.js b/src/legacy/core_plugins/kibana/public/discover/context/query_parameters/__tests__/action_add_filter.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_add_filter.js rename to src/legacy/core_plugins/kibana/public/discover/context/query_parameters/__tests__/action_add_filter.js diff --git a/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_predecessor_count.js b/src/legacy/core_plugins/kibana/public/discover/context/query_parameters/__tests__/action_set_predecessor_count.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_predecessor_count.js rename to src/legacy/core_plugins/kibana/public/discover/context/query_parameters/__tests__/action_set_predecessor_count.js diff --git a/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_query_parameters.js b/src/legacy/core_plugins/kibana/public/discover/context/query_parameters/__tests__/action_set_query_parameters.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_query_parameters.js rename to src/legacy/core_plugins/kibana/public/discover/context/query_parameters/__tests__/action_set_query_parameters.js diff --git a/src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_successor_count.js b/src/legacy/core_plugins/kibana/public/discover/context/query_parameters/__tests__/action_set_successor_count.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_successor_count.js rename to src/legacy/core_plugins/kibana/public/discover/context/query_parameters/__tests__/action_set_successor_count.js diff --git a/src/legacy/core_plugins/kibana/public/context/query_parameters/actions.js b/src/legacy/core_plugins/kibana/public/discover/context/query_parameters/actions.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/query_parameters/actions.js rename to src/legacy/core_plugins/kibana/public/discover/context/query_parameters/actions.js diff --git a/src/legacy/core_plugins/kibana/public/context/query_parameters/constants.ts b/src/legacy/core_plugins/kibana/public/discover/context/query_parameters/constants.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/query_parameters/constants.ts rename to src/legacy/core_plugins/kibana/public/discover/context/query_parameters/constants.ts diff --git a/src/legacy/core_plugins/kibana/public/context/query_parameters/index.js b/src/legacy/core_plugins/kibana/public/discover/context/query_parameters/index.js similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/query_parameters/index.js rename to src/legacy/core_plugins/kibana/public/discover/context/query_parameters/index.js diff --git a/src/legacy/core_plugins/kibana/public/context/query_parameters/state.ts b/src/legacy/core_plugins/kibana/public/discover/context/query_parameters/state.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/context/query_parameters/state.ts rename to src/legacy/core_plugins/kibana/public/discover/context/query_parameters/state.ts diff --git a/src/legacy/core_plugins/kibana/public/doc/doc.test.tsx b/src/legacy/core_plugins/kibana/public/discover/doc/doc.test.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/doc/doc.test.tsx rename to src/legacy/core_plugins/kibana/public/discover/doc/doc.test.tsx diff --git a/src/legacy/core_plugins/kibana/public/doc/doc.tsx b/src/legacy/core_plugins/kibana/public/discover/doc/doc.tsx similarity index 98% rename from src/legacy/core_plugins/kibana/public/doc/doc.tsx rename to src/legacy/core_plugins/kibana/public/discover/doc/doc.tsx index 3b972d88d329f..1f2d2fc532b57 100644 --- a/src/legacy/core_plugins/kibana/public/doc/doc.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/doc/doc.tsx @@ -22,7 +22,7 @@ import { EuiCallOut, EuiLink, EuiLoadingSpinner, EuiPageContent } from '@elastic import { IndexPatterns } from 'ui/index_patterns'; import { metadata } from 'ui/metadata'; import { ElasticSearchHit } from 'ui/registry/doc_views_types'; -import { DocViewer } from '../doc_viewer/doc_viewer'; +import { DocViewer } from '../doc_viewer'; import { ElasticRequestState, useEsDocSearch } from './use_es_doc_search'; export interface ElasticSearchResult { diff --git a/src/legacy/core_plugins/kibana/public/doc/doc_directive.ts b/src/legacy/core_plugins/kibana/public/discover/doc/doc_directive.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/doc/doc_directive.ts rename to src/legacy/core_plugins/kibana/public/discover/doc/doc_directive.ts diff --git a/src/legacy/core_plugins/kibana/public/doc/index.html b/src/legacy/core_plugins/kibana/public/discover/doc/index.html similarity index 100% rename from src/legacy/core_plugins/kibana/public/doc/index.html rename to src/legacy/core_plugins/kibana/public/discover/doc/index.html diff --git a/src/legacy/core_plugins/kibana/public/doc/index.ts b/src/legacy/core_plugins/kibana/public/discover/doc/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/doc/index.ts rename to src/legacy/core_plugins/kibana/public/discover/doc/index.ts diff --git a/src/legacy/core_plugins/kibana/public/doc/use_es_doc_search.test.tsx b/src/legacy/core_plugins/kibana/public/discover/doc/use_es_doc_search.test.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/doc/use_es_doc_search.test.tsx rename to src/legacy/core_plugins/kibana/public/discover/doc/use_es_doc_search.test.tsx diff --git a/src/legacy/core_plugins/kibana/public/doc/use_es_doc_search.ts b/src/legacy/core_plugins/kibana/public/discover/doc/use_es_doc_search.ts similarity index 97% rename from src/legacy/core_plugins/kibana/public/doc/use_es_doc_search.ts rename to src/legacy/core_plugins/kibana/public/discover/doc/use_es_doc_search.ts index acbfbf5f7aedd..d1a01dadb72be 100644 --- a/src/legacy/core_plugins/kibana/public/doc/use_es_doc_search.ts +++ b/src/legacy/core_plugins/kibana/public/discover/doc/use_es_doc_search.ts @@ -19,7 +19,7 @@ import { useEffect, useState } from 'react'; import { ElasticSearchHit } from 'ui/registry/doc_views_types'; import { DocProps } from './doc'; -import { IndexPattern } from '../../../data/public/index_patterns'; +import { IndexPattern } from '../../../../data/public/index_patterns'; export enum ElasticRequestState { Loading, diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_row.js b/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_row.js index a0a69969b49ff..8e4105bac827e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_row.js +++ b/src/legacy/core_plugins/kibana/public/discover/doc_table/components/table_row.js @@ -20,7 +20,7 @@ import _ from 'lodash'; import $ from 'jquery'; import rison from 'rison-node'; -import 'plugins/kibana/doc_viewer'; +import '../../doc_viewer'; import { noWhiteSpace } from '../../../../common/utils/no_white_space'; import openRowHtml from './table_row/open.html'; import detailsHtml from './table_row/details.html'; diff --git a/src/legacy/core_plugins/kibana/public/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap b/src/legacy/core_plugins/kibana/public/discover/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/kibana/public/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap rename to src/legacy/core_plugins/kibana/public/discover/doc_viewer/__snapshots__/doc_viewer.test.tsx.snap diff --git a/src/legacy/core_plugins/kibana/public/doc_viewer/__snapshots__/doc_viewer_render_tab.test.tsx.snap b/src/legacy/core_plugins/kibana/public/discover/doc_viewer/__snapshots__/doc_viewer_render_tab.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/kibana/public/doc_viewer/__snapshots__/doc_viewer_render_tab.test.tsx.snap rename to src/legacy/core_plugins/kibana/public/discover/doc_viewer/__snapshots__/doc_viewer_render_tab.test.tsx.snap diff --git a/src/legacy/core_plugins/kibana/public/doc_viewer/_doc_viewer.scss b/src/legacy/core_plugins/kibana/public/discover/doc_viewer/_doc_viewer.scss similarity index 100% rename from src/legacy/core_plugins/kibana/public/doc_viewer/_doc_viewer.scss rename to src/legacy/core_plugins/kibana/public/discover/doc_viewer/_doc_viewer.scss diff --git a/src/legacy/core_plugins/kibana/public/discover/doc_viewer/_index.scss b/src/legacy/core_plugins/kibana/public/discover/doc_viewer/_index.scss new file mode 100644 index 0000000000000..aaf925f435d81 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/doc_viewer/_index.scss @@ -0,0 +1 @@ +@import 'doc_viewer'; diff --git a/src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer.test.tsx b/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer.test.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer.test.tsx rename to src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer.test.tsx diff --git a/src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer.tsx b/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer.tsx rename to src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer.tsx diff --git a/src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer_directive.ts b/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_directive.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer_directive.ts rename to src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_directive.ts diff --git a/src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer_render_error.tsx b/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_render_error.tsx similarity index 94% rename from src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer_render_error.tsx rename to src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_render_error.tsx index b81610c5569a4..80b9cb5110db7 100644 --- a/src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer_render_error.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_render_error.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { EuiCallOut, EuiCodeBlock } from '@elastic/eui'; // @ts-ignore -import { formatMsg, formatStack } from '../../../../ui/public/notify/lib'; +import { formatMsg, formatStack } from 'ui/notify/lib/index'; interface Props { error: Error | string | null; diff --git a/src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer_render_tab.test.tsx b/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_render_tab.test.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer_render_tab.test.tsx rename to src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_render_tab.test.tsx diff --git a/src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer_render_tab.tsx b/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_render_tab.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer_render_tab.tsx rename to src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_render_tab.tsx diff --git a/src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer_tab.tsx b/src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_tab.tsx similarity index 100% rename from src/legacy/core_plugins/kibana/public/doc_viewer/doc_viewer_tab.tsx rename to src/legacy/core_plugins/kibana/public/discover/doc_viewer/doc_viewer_tab.tsx diff --git a/src/legacy/core_plugins/kibana/public/doc_viewer/index.js b/src/legacy/core_plugins/kibana/public/discover/doc_viewer/index.ts similarity index 96% rename from src/legacy/core_plugins/kibana/public/doc_viewer/index.js rename to src/legacy/core_plugins/kibana/public/discover/doc_viewer/index.ts index 0771de0f2d599..8ce94f24128df 100644 --- a/src/legacy/core_plugins/kibana/public/doc_viewer/index.js +++ b/src/legacy/core_plugins/kibana/public/discover/doc_viewer/index.ts @@ -18,3 +18,5 @@ */ import './doc_viewer_directive'; + +export * from './doc_viewer'; diff --git a/src/legacy/core_plugins/kibana/public/discover/index.js b/src/legacy/core_plugins/kibana/public/discover/index.js index def832107322d..e509936306275 100644 --- a/src/legacy/core_plugins/kibana/public/discover/index.js +++ b/src/legacy/core_plugins/kibana/public/discover/index.js @@ -19,12 +19,15 @@ import './saved_searches/saved_searches'; import { i18n } from '@kbn/i18n'; -import './directives'; + +import './angular/directives'; import 'ui/collapsible_sidebar'; import './components/field_chooser/field_chooser'; -import './controllers/discover'; +import './angular/discover'; import './doc_table/components/table_row'; import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; +import './doc'; +import './context'; FeatureCatalogueRegistryProvider.register(() => { return { diff --git a/src/legacy/core_plugins/kibana/public/doc_viewer/_index.scss b/src/legacy/core_plugins/kibana/public/doc_viewer/_index.scss deleted file mode 100644 index c8fe67fd3bae4..0000000000000 --- a/src/legacy/core_plugins/kibana/public/doc_viewer/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './doc_viewer'; diff --git a/src/legacy/core_plugins/kibana/public/index.scss b/src/legacy/core_plugins/kibana/public/index.scss index 7a47ca5e8eb1c..7a6a3ca1d01d0 100644 --- a/src/legacy/core_plugins/kibana/public/index.scss +++ b/src/legacy/core_plugins/kibana/public/index.scss @@ -9,9 +9,6 @@ // Public UI styles @import 'src/legacy/ui/public/index'; -// Context styles -@import './context/index'; - // Dev tools styles @import './dev_tools/index'; @@ -30,9 +27,6 @@ // Management styles @import './management/index'; -// Doc Viewer -@import './doc_viewer/index'; - // Dashboard styles // MUST STAY AT THE BOTTOM BECAUSE OF DARK THEME IMPORTS @import './dashboard/index'; diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index 9a96bf26aede6..6c809e84c8c84 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -52,9 +52,7 @@ import './discover'; import './visualize'; import './dashboard'; import './management'; -import './doc'; import './dev_tools'; -import './context'; import 'ui/vislib'; import 'ui/agg_response'; import 'ui/agg_types'; diff --git a/src/legacy/ui/public/registry/doc_views_helpers.tsx b/src/legacy/ui/public/registry/doc_views_helpers.tsx index 02f276c48124b..1ff00713b10ef 100644 --- a/src/legacy/ui/public/registry/doc_views_helpers.tsx +++ b/src/legacy/ui/public/registry/doc_views_helpers.tsx @@ -26,7 +26,7 @@ import { AngularController, AngularDirective, } from './doc_views_types'; -import { DocViewerError } from '../../../core_plugins/kibana/public/doc_viewer/doc_viewer_render_error'; +import { DocViewerError } from '../../../core_plugins/kibana/public/discover/doc_viewer/doc_viewer_render_error'; /** * Compiles and injects the give angular template into the given dom node From 47fef4eaca00875442482eac3d506bccba3ee74f Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Wed, 16 Oct 2019 21:36:31 +0200 Subject: [PATCH 46/48] filter uiPlugins dependencies to only include ui deps (#47879) Signed-off-by: pgayvallet --- .../server/plugins/plugins_system.test.ts | 204 ++++++++++-------- src/core/server/plugins/plugins_system.ts | 31 +-- 2 files changed, 130 insertions(+), 105 deletions(-) diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts index 100d043a762f8..7599ff0378caf 100644 --- a/src/core/server/plugins/plugins_system.test.ts +++ b/src/core/server/plugins/plugins_system.test.ts @@ -43,7 +43,8 @@ function createPlugin( required = [], optional = [], server = true, - }: { required?: string[]; optional?: string[]; server?: boolean } = {} + ui = true, + }: { required?: string[]; optional?: string[]; server?: boolean; ui?: boolean } = {} ) { return new PluginWrapper({ path: 'some-path', @@ -55,7 +56,7 @@ function createPlugin( requiredPlugins: required, optionalPlugins: optional, server, - ui: true, + ui, }, opaqueId: Symbol(id), initializerContext: { logger } as any, @@ -147,13 +148,13 @@ test('`setupPlugins` ignores missing optional dependency', async () => { pluginsSystem.addPlugin(plugin); expect([...(await pluginsSystem.setupPlugins(setupDeps))]).toMatchInlineSnapshot(` -Array [ - Array [ - "some-id", - "test", - ], -] -`); + Array [ + Array [ + "some-id", + "test", + ], + ] + `); }); test('correctly orders plugins and returns exposed values for "setup" and "start"', async () => { @@ -221,29 +222,29 @@ test('correctly orders plugins and returns exposed values for "setup" and "start ); expect([...(await pluginsSystem.setupPlugins(setupDeps))]).toMatchInlineSnapshot(` -Array [ - Array [ - "order-0", - "added-as-1", - ], - Array [ - "order-1", - "added-as-3", - ], - Array [ - "order-2", - "added-as-2", - ], - Array [ - "order-3", - "added-as-4", - ], - Array [ - "order-4", - "added-as-0", - ], -] -`); + Array [ + Array [ + "order-0", + "added-as-1", + ], + Array [ + "order-1", + "added-as-3", + ], + Array [ + "order-2", + "added-as-2", + ], + Array [ + "order-3", + "added-as-4", + ], + Array [ + "order-4", + "added-as-0", + ], + ] + `); for (const [plugin, deps] of plugins) { expect(mockCreatePluginSetupContext).toHaveBeenCalledWith(coreContext, setupDeps, plugin); @@ -253,29 +254,29 @@ Array [ const startDeps = {}; expect([...(await pluginsSystem.startPlugins(startDeps))]).toMatchInlineSnapshot(` -Array [ - Array [ - "order-0", - "started-as-1", - ], - Array [ - "order-1", - "started-as-3", - ], - Array [ - "order-2", - "started-as-2", - ], - Array [ - "order-3", - "started-as-4", - ], - Array [ - "order-4", - "started-as-0", - ], -] -`); + Array [ + Array [ + "order-0", + "started-as-1", + ], + Array [ + "order-1", + "started-as-3", + ], + Array [ + "order-2", + "started-as-2", + ], + Array [ + "order-3", + "started-as-4", + ], + Array [ + "order-4", + "started-as-0", + ], + ] + `); for (const [plugin, deps] of plugins) { expect(mockCreatePluginStartContext).toHaveBeenCalledWith(coreContext, startDeps, plugin); @@ -296,17 +297,17 @@ test('`setupPlugins` only setups plugins that have server side', async () => { }); expect([...(await pluginsSystem.setupPlugins(setupDeps))]).toMatchInlineSnapshot(` -Array [ - Array [ - "order-1", - "added-as-2", - ], - Array [ - "order-0", - "added-as-0", - ], -] -`); + Array [ + Array [ + "order-1", + "added-as-2", + ], + Array [ + "order-0", + "added-as-0", + ], + ] + `); expect(mockCreatePluginSetupContext).toHaveBeenCalledWith( coreContext, @@ -327,11 +328,11 @@ Array [ test('`uiPlugins` returns empty Maps before plugins are added', async () => { expect(pluginsSystem.uiPlugins()).toMatchInlineSnapshot(` -Object { - "internal": Map {}, - "public": Map {}, -} -`); + Object { + "internal": Map {}, + "public": Map {}, + } + `); }); test('`uiPlugins` returns ordered Maps of all plugin manifests', async () => { @@ -354,14 +355,37 @@ test('`uiPlugins` returns ordered Maps of all plugin manifests', async () => { }); expect([...pluginsSystem.uiPlugins().internal.keys()]).toMatchInlineSnapshot(` -Array [ - "order-0", - "order-1", - "order-2", - "order-3", - "order-4", -] -`); + Array [ + "order-0", + "order-1", + "order-2", + "order-3", + "order-4", + ] + `); +}); + +test('`uiPlugins` returns only ui plugin dependencies', async () => { + const plugins = [ + createPlugin('ui-plugin', { + required: ['req-ui', 'req-no-ui'], + optional: ['opt-ui', 'opt-no-ui'], + ui: true, + server: false, + }), + createPlugin('req-ui', { ui: true, server: false }), + createPlugin('req-no-ui', { ui: false, server: true }), + createPlugin('opt-ui', { ui: true, server: false }), + createPlugin('opt-no-ui', { ui: false, server: true }), + ]; + + plugins.forEach(plugin => { + pluginsSystem.addPlugin(plugin); + }); + + const plugin = pluginsSystem.uiPlugins().internal.get('ui-plugin')!; + expect(plugin.requiredPlugins).toEqual(['req-ui']); + expect(plugin.optionalPlugins).toEqual(['opt-ui']); }); test('can start without plugins', async () => { @@ -386,15 +410,15 @@ test('`startPlugins` only starts plugins that were setup', async () => { await pluginsSystem.setupPlugins(setupDeps); const result = await pluginsSystem.startPlugins({}); expect([...result]).toMatchInlineSnapshot(` -Array [ - Array [ - "order-1", - "started-as-2", - ], - Array [ - "order-0", - "started-as-0", - ], -] -`); + Array [ + Array [ + "order-1", + "started-as-2", + ], + Array [ + "order-0", + "started-as-0", + ], + ] + `); }); diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index 1f797525ba14f..266a68b32703e 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -161,22 +161,23 @@ export class PluginsSystem { * Get a Map of all discovered UI plugins in topological order. */ public uiPlugins() { + const uiPluginNames = [...this.getTopologicallySortedPluginNames().keys()].filter( + pluginName => this.plugins.get(pluginName)!.includesUiPlugin + ); const internal = new Map( - [...this.getTopologicallySortedPluginNames().keys()] - .filter(pluginName => this.plugins.get(pluginName)!.includesUiPlugin) - .map(pluginName => { - const plugin = this.plugins.get(pluginName)!; - return [ - pluginName, - { - id: pluginName, - path: plugin.path, - configPath: plugin.manifest.configPath, - requiredPlugins: plugin.manifest.requiredPlugins, - optionalPlugins: plugin.manifest.optionalPlugins, - }, - ] as [PluginName, DiscoveredPluginInternal]; - }) + uiPluginNames.map(pluginName => { + const plugin = this.plugins.get(pluginName)!; + return [ + pluginName, + { + id: pluginName, + path: plugin.path, + configPath: plugin.manifest.configPath, + requiredPlugins: plugin.manifest.requiredPlugins.filter(p => uiPluginNames.includes(p)), + optionalPlugins: plugin.manifest.optionalPlugins.filter(p => uiPluginNames.includes(p)), + }, + ] as [PluginName, DiscoveredPluginInternal]; + }) ); const publicPlugins = new Map( From 1e3f14395c5e720c0e3a21370387e80d27b8b653 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Wed, 16 Oct 2019 15:10:40 -0500 Subject: [PATCH 47/48] [DOCS] API intro (#47164) * [DOCS] API intro * Logstash configuration management * Reformatting * Comments from Josh * Commets from Gail * Fixed broken things --- docs/api/dashboard-api.asciidoc | 17 ++ docs/api/dashboard-import.asciidoc | 14 -- .../export-dashboard.asciidoc} | 6 +- .../import-dashboard.asciidoc} | 2 +- docs/api/features.asciidoc | 218 +++++++++++++++++- docs/api/features/get.asciidoc | 216 ----------------- ...logstash-configuration-management.asciidoc | 25 +- ...eate.asciidoc => create-logstash.asciidoc} | 3 +- ...lete.asciidoc => delete-pipeline.asciidoc} | 4 +- .../{list.asciidoc => list-pipeline.asciidoc} | 3 +- ...ve.asciidoc => retrieve-pipeline.asciidoc} | 5 +- docs/api/role-management.asciidoc | 15 +- docs/api/role-management/delete.asciidoc | 2 +- docs/api/role-management/get-all.asciidoc | 2 +- docs/api/role-management/get.asciidoc | 2 +- docs/api/role-management/put.asciidoc | 4 +- docs/api/saved-objects.asciidoc | 31 ++- docs/api/saved-objects/bulk_create.asciidoc | 2 +- docs/api/saved-objects/bulk_get.asciidoc | 2 +- docs/api/saved-objects/create.asciidoc | 6 +- docs/api/saved-objects/delete.asciidoc | 2 +- docs/api/saved-objects/export.asciidoc | 2 +- docs/api/saved-objects/import.asciidoc | 2 +- docs/api/saved-objects/update.asciidoc | 2 +- docs/api/spaces-management.asciidoc | 24 +- docs/api/spaces-management/delete.asciidoc | 2 +- docs/api/spaces-management/get.asciidoc | 2 +- docs/api/spaces-management/get_all.asciidoc | 2 +- docs/api/spaces-management/post.asciidoc | 2 +- docs/api/spaces-management/put.asciidoc | 2 +- ...olve_copy_saved_objects_conflicts.asciidoc | 2 +- docs/api/upgrade-assistant.asciidoc | 13 +- docs/api/url-shortening.asciidoc | 54 ++++- docs/api/url_shortening/shorten_url.asciidoc | 55 ----- docs/api/using-api.asciidoc | 46 ++++ docs/dev-tools/console/console.asciidoc | 2 + docs/user/api.asciidoc | 17 +- 37 files changed, 433 insertions(+), 377 deletions(-) create mode 100644 docs/api/dashboard-api.asciidoc delete mode 100644 docs/api/dashboard-import.asciidoc rename docs/api/{dashboard-import/export.asciidoc => dashboard/export-dashboard.asciidoc} (85%) rename docs/api/{dashboard-import/import.asciidoc => dashboard/import-dashboard.asciidoc} (99%) delete mode 100644 docs/api/features/get.asciidoc rename docs/api/logstash-configuration-management/{create.asciidoc => create-logstash.asciidoc} (96%) rename docs/api/logstash-configuration-management/{delete.asciidoc => delete-pipeline.asciidoc} (97%) rename docs/api/logstash-configuration-management/{list.asciidoc => list-pipeline.asciidoc} (84%) rename docs/api/logstash-configuration-management/{retrieve.asciidoc => retrieve-pipeline.asciidoc} (85%) delete mode 100644 docs/api/url_shortening/shorten_url.asciidoc create mode 100644 docs/api/using-api.asciidoc diff --git a/docs/api/dashboard-api.asciidoc b/docs/api/dashboard-api.asciidoc new file mode 100644 index 0000000000000..50c2abc975763 --- /dev/null +++ b/docs/api/dashboard-api.asciidoc @@ -0,0 +1,17 @@ +[[dashboard-api]] +== Import and export dashboard APIs + +Import and export dashboards with the corresponding saved objects, such as visualizations, saved +searches, and index patterns. + +WARNING: Do not write documents directly to the `.kibana` index. When you write directly +to the `.kibana` index, the data becomes corrupted and permanently breaks future {kib} versions. + +The following import and export dashboard APIs are available: + +* <> to import dashboards and corresponding saved objects + +* <> to export dashboards and corresponding saved objects + +include::dashboard/import-dashboard.asciidoc[] +include::dashboard/export-dashboard.asciidoc[] \ No newline at end of file diff --git a/docs/api/dashboard-import.asciidoc b/docs/api/dashboard-import.asciidoc deleted file mode 100644 index 19eb6f2a446b5..0000000000000 --- a/docs/api/dashboard-import.asciidoc +++ /dev/null @@ -1,14 +0,0 @@ -[[dashboard-import-api]] -== Dashboard import and export APIs - -Import and export dashboards with the corresponding saved objects, such as visualizations, saved -searches, and index patterns. - -WARNING: Do not write documents directly to the `.kibana` index. When you write directly -to the `.kibana` index, the data becomes corrupted and permanently breaks future {kib} versions. - -* <> -* <> - -include::dashboard-import/import.asciidoc[] -include::dashboard-import/export.asciidoc[] diff --git a/docs/api/dashboard-import/export.asciidoc b/docs/api/dashboard/export-dashboard.asciidoc similarity index 85% rename from docs/api/dashboard-import/export.asciidoc rename to docs/api/dashboard/export-dashboard.asciidoc index 13a0ffe8febeb..7858b69d44c79 100644 --- a/docs/api/dashboard-import/export.asciidoc +++ b/docs/api/dashboard/export-dashboard.asciidoc @@ -1,7 +1,7 @@ [[dashboard-api-export]] === Export dashboard API ++++ -Dashboard export +Export dashboard ++++ experimental[] Export dashboards and corresponding saved objects. @@ -21,7 +21,7 @@ experimental[] Export dashboards and corresponding saved objects. ==== Response body `objects`:: - (array) A top level property that includes the saved objects. The order of the objects is not guaranteed. Use the exact response body as the request body for the corresponding <>. + (array) A top level property that includes the saved objects. The order of the objects is not guaranteed. Use the exact response body as the request body for the corresponding <>. [[dashboard-api-export-codes]] ==== Response code @@ -39,4 +39,4 @@ GET api/kibana/dashboards/export?dashboard=942dcef0-b2cd-11e8-ad8e-85441f0c2e5c -------------------------------------------------- // KIBANA -<1> In this example, `942dcef0-b2cd-11e8-ad8e-85441f0c2e5c` is the dashboard ID. +<1> The dashboard ID is `942dcef0-b2cd-11e8-ad8e-85441f0c2e5c`. \ No newline at end of file diff --git a/docs/api/dashboard-import/import.asciidoc b/docs/api/dashboard/import-dashboard.asciidoc similarity index 99% rename from docs/api/dashboard-import/import.asciidoc rename to docs/api/dashboard/import-dashboard.asciidoc index 9ff84abb51c8e..0c6ea2bcf5933 100644 --- a/docs/api/dashboard-import/import.asciidoc +++ b/docs/api/dashboard/import-dashboard.asciidoc @@ -1,4 +1,4 @@ -[[dashboard-import-api-import]] +[[dashboard-import-api]] === Import dashboard API ++++ Import dashboard diff --git a/docs/api/features.asciidoc b/docs/api/features.asciidoc index 798f2be3d7212..7928f7f34aad7 100644 --- a/docs/api/features.asciidoc +++ b/docs/api/features.asciidoc @@ -1,9 +1,217 @@ [role="xpack"] -[[features-api]] -== Features API +[[features-api-get]] +== Get features API -View information about the available features in {kib}. Features are used by spaces and security to refine and secure access to {kib}. +experimental[] Retrieves all {kib} features. Features are used by spaces and security to refine and secure access to {kib}. -* <> +[float] +[[features-api-get-request]] +=== Request -include::features/get.asciidoc[] +`GET /api/features` + +[float] +[[features-api-get-codes]] +=== Response code + +`200`:: + Indicates a successful call. + +[float] +[[features-api-get-example]] +=== Example + +The API returns the following: + +[source,js] +-------------------------------------------------- + { + "id": "discover", + "name": "Discover", + "icon": "discoverApp", + "navLinkId": "kibana:discover", + "app": [ + "kibana" + ], + "catalogue": [ + "discover" + ], + "privileges": { + "all": { + "savedObject": { + "all": [ + "search", + "url" + ], + "read": [ + "config", + "index-pattern" + ] + }, + "ui": [ + "show", + "createShortUrl", + "save" + ] + }, + "read": { + "savedObject": { + "all": [], + "read": [ + "config", + "index-pattern", + "search", + "url" + ] + }, + "ui": [ + "show" + ] + } + } + }, + { + "id": "visualize", + "name": "Visualize", + "icon": "visualizeApp", + "navLinkId": "kibana:visualize", + "app": [ + "kibana" + ], + "catalogue": [ + "visualize" + ], + "privileges": { + "all": { + "savedObject": { + "all": [ + "visualization", + "url" + ], + "read": [ + "config", + "index-pattern", + "search" + ] + }, + "ui": [ + "show", + "createShortUrl", + "delete", + "save" + ] + }, + "read": { + "savedObject": { + "all": [], + "read": [ + "config", + "index-pattern", + "search", + "visualization" + ] + }, + "ui": [ + "show" + ] + } + } + }, + { + "id": "dashboard", + "name": "Dashboard", + "icon": "dashboardApp", + "navLinkId": "kibana:dashboard", + "app": [ + "kibana" + ], + "catalogue": [ + "dashboard" + ], + "privileges": { + "all": { + "savedObject": { + "all": [ + "dashboard", + "url" + ], + "read": [ + "config", + "index-pattern", + "search", + "visualization", + "timelion-sheet", + "canvas-workpad" + ] + }, + "ui": [ + "createNew", + "show", + "showWriteControls" + ] + }, + "read": { + "savedObject": { + "all": [], + "read": [ + "config", + "index-pattern", + "search", + "visualization", + "timelion-sheet", + "canvas-workpad", + "dashboard" + ] + }, + "ui": [ + "show" + ] + } + } + }, + { + "id": "dev_tools", + "name": "Dev Tools", + "icon": "devToolsApp", + "navLinkId": "kibana:dev_tools", + "app": [ + "kibana" + ], + "catalogue": [ + "console", + "searchprofiler", + "grokdebugger" + ], + "privileges": { + "all": { + "api": [ + "console" + ], + "savedObject": { + "all": [], + "read": [ + "config" + ] + }, + "ui": [ + "show" + ] + }, + "read": { + "api": [ + "console" + ], + "savedObject": { + "all": [], + "read": [ + "config" + ] + }, + "ui": [ + "show" + ] + } + }, + "privilegesTooltip": "User should also be granted the appropriate Elasticsearch cluster and index privileges" + }, +-------------------------------------------------- diff --git a/docs/api/features/get.asciidoc b/docs/api/features/get.asciidoc deleted file mode 100644 index fc2c0446d82a8..0000000000000 --- a/docs/api/features/get.asciidoc +++ /dev/null @@ -1,216 +0,0 @@ -[[features-api-get]] -=== Get features API -++++ -Get features -++++ - -experimental[] Retrieves all {kib} features. - -[[features-api-get-request]] -==== Request - -`GET /api/features` - -[[features-api-get-codes]] -==== Response code - -`200`:: - Indicates a successful call. - -[[features-api-get-example]] -==== Example - -The API returns the following: - -[source,js] --------------------------------------------------- - { - "id": "discover", - "name": "Discover", - "icon": "discoverApp", - "navLinkId": "kibana:discover", - "app": [ - "kibana" - ], - "catalogue": [ - "discover" - ], - "privileges": { - "all": { - "savedObject": { - "all": [ - "search", - "url" - ], - "read": [ - "config", - "index-pattern" - ] - }, - "ui": [ - "show", - "createShortUrl", - "save" - ] - }, - "read": { - "savedObject": { - "all": [], - "read": [ - "config", - "index-pattern", - "search", - "url" - ] - }, - "ui": [ - "show" - ] - } - } - }, - { - "id": "visualize", - "name": "Visualize", - "icon": "visualizeApp", - "navLinkId": "kibana:visualize", - "app": [ - "kibana" - ], - "catalogue": [ - "visualize" - ], - "privileges": { - "all": { - "savedObject": { - "all": [ - "visualization", - "url" - ], - "read": [ - "config", - "index-pattern", - "search" - ] - }, - "ui": [ - "show", - "createShortUrl", - "delete", - "save" - ] - }, - "read": { - "savedObject": { - "all": [], - "read": [ - "config", - "index-pattern", - "search", - "visualization" - ] - }, - "ui": [ - "show" - ] - } - } - }, - { - "id": "dashboard", - "name": "Dashboard", - "icon": "dashboardApp", - "navLinkId": "kibana:dashboard", - "app": [ - "kibana" - ], - "catalogue": [ - "dashboard" - ], - "privileges": { - "all": { - "savedObject": { - "all": [ - "dashboard", - "url" - ], - "read": [ - "config", - "index-pattern", - "search", - "visualization", - "timelion-sheet", - "canvas-workpad" - ] - }, - "ui": [ - "createNew", - "show", - "showWriteControls" - ] - }, - "read": { - "savedObject": { - "all": [], - "read": [ - "config", - "index-pattern", - "search", - "visualization", - "timelion-sheet", - "canvas-workpad", - "dashboard" - ] - }, - "ui": [ - "show" - ] - } - } - }, - { - "id": "dev_tools", - "name": "Dev Tools", - "icon": "devToolsApp", - "navLinkId": "kibana:dev_tools", - "app": [ - "kibana" - ], - "catalogue": [ - "console", - "searchprofiler", - "grokdebugger" - ], - "privileges": { - "all": { - "api": [ - "console" - ], - "savedObject": { - "all": [], - "read": [ - "config" - ] - }, - "ui": [ - "show" - ] - }, - "read": { - "api": [ - "console" - ], - "savedObject": { - "all": [], - "read": [ - "config" - ] - }, - "ui": [ - "show" - ] - } - }, - "privilegesTooltip": "User should also be granted the appropriate Elasticsearch cluster and index privileges" - }, --------------------------------------------------- diff --git a/docs/api/logstash-configuration-management.asciidoc b/docs/api/logstash-configuration-management.asciidoc index e53218ec439aa..fbb45095c214b 100644 --- a/docs/api/logstash-configuration-management.asciidoc +++ b/docs/api/logstash-configuration-management.asciidoc @@ -6,12 +6,19 @@ Programmatically integrate with the Logstash configuration management feature. WARNING: Do not directly access the `.logstash` index. The structure of the `.logstash` index is subject to change, which could cause your integration to break. Instead, use the Logstash configuration management APIs. -* <> -* <> -* <> -* <> - -include::logstash-configuration-management/create.asciidoc[] -include::logstash-configuration-management/retrieve.asciidoc[] -include::logstash-configuration-management/delete.asciidoc[] -include::logstash-configuration-management/list.asciidoc[] \ No newline at end of file +The following Logstash configuration management APIs are available: + +* <> to delete a centrally-managed Logstash pipeline + +* <> to list all centrally-managed Logstash pipelines + +* <> to create a centrally-managed Logstash pipeline, or update an existing pipeline + +* <> to retrieve a centrally-managed Logstash pipeline + +include::logstash-configuration-management/delete-pipeline.asciidoc[] +include::logstash-configuration-management/list-pipeline.asciidoc[] +include::logstash-configuration-management/create-logstash.asciidoc[] +include::logstash-configuration-management/retrieve-pipeline.asciidoc[] + + diff --git a/docs/api/logstash-configuration-management/create.asciidoc b/docs/api/logstash-configuration-management/create-logstash.asciidoc similarity index 96% rename from docs/api/logstash-configuration-management/create.asciidoc rename to docs/api/logstash-configuration-management/create-logstash.asciidoc index d6fbfd4947bab..38e0ee12a0ebf 100644 --- a/docs/api/logstash-configuration-management/create.asciidoc +++ b/docs/api/logstash-configuration-management/create-logstash.asciidoc @@ -1,8 +1,7 @@ -[role="xpack"] [[logstash-configuration-management-api-create]] === Create Logstash pipeline API ++++ -Create pipeline +Create Logstash pipeline ++++ experimental[] Create a centrally-managed Logstash pipeline, or update an existing pipeline. diff --git a/docs/api/logstash-configuration-management/delete.asciidoc b/docs/api/logstash-configuration-management/delete-pipeline.asciidoc similarity index 97% rename from docs/api/logstash-configuration-management/delete.asciidoc rename to docs/api/logstash-configuration-management/delete-pipeline.asciidoc index c5aadb495ee15..15d44034b46fe 100644 --- a/docs/api/logstash-configuration-management/delete.asciidoc +++ b/docs/api/logstash-configuration-management/delete-pipeline.asciidoc @@ -1,4 +1,3 @@ -[role="xpack"] [[logstash-configuration-management-api-delete]] === Delete Logstash pipeline API ++++ @@ -31,4 +30,5 @@ experimental[] Delete a centrally-managed Logstash pipeline. -------------------------------------------------- DELETE api/logstash/pipeline/hello-world -------------------------------------------------- -// KIBANA \ No newline at end of file +// KIBANA + diff --git a/docs/api/logstash-configuration-management/list.asciidoc b/docs/api/logstash-configuration-management/list-pipeline.asciidoc similarity index 84% rename from docs/api/logstash-configuration-management/list.asciidoc rename to docs/api/logstash-configuration-management/list-pipeline.asciidoc index a11199d7b8dbc..7140c35d89853 100644 --- a/docs/api/logstash-configuration-management/list.asciidoc +++ b/docs/api/logstash-configuration-management/list-pipeline.asciidoc @@ -1,4 +1,3 @@ -[role="xpack"] [[logstash-configuration-management-api-list]] === List Logstash pipeline API ++++ @@ -36,4 +35,4 @@ The API returns the following: } -------------------------------------------------- -<1> The `username` property may or may not be present, depending on if security was enabled when the pipeline was created or last updated. +<1> The `username` property appears when security is enabled, and depends on when the pipeline was created or last updated. \ No newline at end of file diff --git a/docs/api/logstash-configuration-management/retrieve.asciidoc b/docs/api/logstash-configuration-management/retrieve-pipeline.asciidoc similarity index 85% rename from docs/api/logstash-configuration-management/retrieve.asciidoc rename to docs/api/logstash-configuration-management/retrieve-pipeline.asciidoc index 3bc5f13c56ac1..93a1ec3aa1da5 100644 --- a/docs/api/logstash-configuration-management/retrieve.asciidoc +++ b/docs/api/logstash-configuration-management/retrieve-pipeline.asciidoc @@ -1,11 +1,10 @@ -[role="xpack"] [[logstash-configuration-management-api-retrieve]] === Retrieve pipeline API ++++ Retrieve pipeline ++++ -experimental[] Retrieves a centrally-managed Logstash pipeline. +experimental[] Retrieve a centrally-managed Logstash pipeline. [[logstash-configuration-management-api-retrieve-request]] ==== Request @@ -34,4 +33,4 @@ The API returns the following: "queue.type": "persistent" } } --------------------------------------------------- +-------------------------------------------------- \ No newline at end of file diff --git a/docs/api/role-management.asciidoc b/docs/api/role-management.asciidoc index 622d26571cd6e..482d1a9b3cdd3 100644 --- a/docs/api/role-management.asciidoc +++ b/docs/api/role-management.asciidoc @@ -1,15 +1,20 @@ [role="xpack"] [[role-management-api]] -== Kibana role management APIs +== {kib} role management APIs Manage the roles that grant <>. WARNING: Do not use the {ref}/security-api.html#security-role-apis[{es} role management APIs] to manage {kib} roles. -* <> -* <> -* <> -* <> +The following {kib} role management APIs are available: + +* <> to create a new {kib} role, or update the attributes of an existing role + +* <> to retrieve all {kib} roles + +* <> to retrieve a specific role + +* <> to delete a {kib} role include::role-management/put.asciidoc[] include::role-management/get.asciidoc[] diff --git a/docs/api/role-management/delete.asciidoc b/docs/api/role-management/delete.asciidoc index eb8ab183576fd..acf2e4a3e3f1f 100644 --- a/docs/api/role-management/delete.asciidoc +++ b/docs/api/role-management/delete.asciidoc @@ -4,7 +4,7 @@ Delete role ++++ -Deletes a {kib} role. +Delete a {kib} role. experimental["The underlying mechanism of enforcing role-based access control is stable, but the APIs for managing the roles are experimental."] diff --git a/docs/api/role-management/get-all.asciidoc b/docs/api/role-management/get-all.asciidoc index b318fecb3b4e1..4a3dbd7734d3a 100644 --- a/docs/api/role-management/get-all.asciidoc +++ b/docs/api/role-management/get-all.asciidoc @@ -4,7 +4,7 @@ Get all roles ++++ -Retrieves all {kib} roles. +Retrieve all {kib} roles. experimental["The underlying mechanism of enforcing role-based access control is stable, but the APIs for managing the roles are experimental."] diff --git a/docs/api/role-management/get.asciidoc b/docs/api/role-management/get.asciidoc index 34d7c256bd564..44423b01abe5b 100644 --- a/docs/api/role-management/get.asciidoc +++ b/docs/api/role-management/get.asciidoc @@ -4,7 +4,7 @@ Get specific role ++++ -Retrieves a specific role. +Retrieve a specific role. experimental["The underlying mechanism of enforcing role-based access control is stable, but the APIs for managing the roles are experimental."] diff --git a/docs/api/role-management/put.asciidoc b/docs/api/role-management/put.asciidoc index af7544f8ecb46..67ec15892afe4 100644 --- a/docs/api/role-management/put.asciidoc +++ b/docs/api/role-management/put.asciidoc @@ -4,7 +4,7 @@ Create or update role ++++ -Creates a new {kib} role, or updates the attributes of an existing role. {kib} roles are stored in the +Create a new {kib} role, or update the attributes of an existing role. {kib} roles are stored in the {es} native realm. experimental["The underlying mechanism of enforcing role-based access control is stable, but the APIs for managing the roles are experimental."] @@ -40,7 +40,7 @@ To use the create or update role API, you must have the `manage_security` cluste `feature` ::: (object) Contains privileges for specific features. When the `feature` privileges are specified, you are unable to use the `base` section. - To retrieve a list of available features, use the <>. + To retrieve a list of available features, use the <>. `spaces` ::: (list) The spaces to apply the privileges to. diff --git a/docs/api/saved-objects.asciidoc b/docs/api/saved-objects.asciidoc index 6c7e015f9f81c..a4e9fa32f8a5c 100644 --- a/docs/api/saved-objects.asciidoc +++ b/docs/api/saved-objects.asciidoc @@ -6,16 +6,27 @@ Manage {kib} saved objects, including dashboards, visualizations, index patterns WARNING: Do not write documents directly to the `.kibana` index. When you write directly to the `.kibana` index, the data becomes corrupted and permanently breaks future {kib} versions. -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> +The following saved objects APIs are available: + +* <> to retrieve a single {kib} saved object by ID + +* <> to retrieve multiple {kib} saved objects by ID + +* <> to retrieve a paginated set of {kib} saved objects by various conditions + +* <> to create {kib} saved objects + +* <> to create multiple {kib} saved objects + +* <> to update the attributes for existing {kib} saved objects + +* <> to remove {kib} saved objects + +* <> to retrieve sets of saved objects that you want to import into {kib} + +* <> to create sets of {kib} saved objects from a file created by the export API + +* <> to resolve errors from the import API include::saved-objects/get.asciidoc[] include::saved-objects/bulk_get.asciidoc[] diff --git a/docs/api/saved-objects/bulk_create.asciidoc b/docs/api/saved-objects/bulk_create.asciidoc index de66af78f5bd2..ca8cc0f287015 100644 --- a/docs/api/saved-objects/bulk_create.asciidoc +++ b/docs/api/saved-objects/bulk_create.asciidoc @@ -1,7 +1,7 @@ [[saved-objects-api-bulk-create]] === Bulk create saved objects API ++++ -Bulk create objects +Bulk create saved objects ++++ experimental[] Create multiple {kib} saved objects. diff --git a/docs/api/saved-objects/bulk_get.asciidoc b/docs/api/saved-objects/bulk_get.asciidoc index 454572254e5c5..4f2cbcb980f82 100644 --- a/docs/api/saved-objects/bulk_get.asciidoc +++ b/docs/api/saved-objects/bulk_get.asciidoc @@ -4,7 +4,7 @@ Bulk get objects ++++ -experimental[] Retrieves multiple {kib} saved objects by ID. +experimental[] Retrieve multiple {kib} saved objects by ID. [[saved-objects-api-bulk-get-request]] ==== Request diff --git a/docs/api/saved-objects/create.asciidoc b/docs/api/saved-objects/create.asciidoc index 1969d9b9ffbd8..fecc3f3732f2a 100644 --- a/docs/api/saved-objects/create.asciidoc +++ b/docs/api/saved-objects/create.asciidoc @@ -1,10 +1,10 @@ [[saved-objects-api-create]] -=== Create saved object API +=== Create saved objects API ++++ -Create object +Create saved objects ++++ -experimental[] Create a {kib} saved object. +experimental[] Create {kib} saved objects. [[saved-objects-api-create-request]] ==== Request diff --git a/docs/api/saved-objects/delete.asciidoc b/docs/api/saved-objects/delete.asciidoc index 84381e3946adc..4a96cf554f784 100644 --- a/docs/api/saved-objects/delete.asciidoc +++ b/docs/api/saved-objects/delete.asciidoc @@ -4,7 +4,7 @@ Delete object ++++ -experimental[] Remove a {kib} saved object. +experimental[] Remove {kib} saved objects. WARNING: Once you delete a saved object, _it cannot be recovered_. diff --git a/docs/api/saved-objects/export.asciidoc b/docs/api/saved-objects/export.asciidoc index e6c0adf2128ba..ee56e6bad75c8 100644 --- a/docs/api/saved-objects/export.asciidoc +++ b/docs/api/saved-objects/export.asciidoc @@ -4,7 +4,7 @@ Export objects ++++ -experimental[] Retrieve a set of saved objects that you want to import into {kib}. +experimental[] Retrieve sets of saved objects that you want to import into {kib}. [[saved-objects-api-export-request]] ==== Request diff --git a/docs/api/saved-objects/import.asciidoc b/docs/api/saved-objects/import.asciidoc index af384718051c0..0331f23284352 100644 --- a/docs/api/saved-objects/import.asciidoc +++ b/docs/api/saved-objects/import.asciidoc @@ -4,7 +4,7 @@ Import objects ++++ -experimental[] Create a set of {kib} saved objects from a file created by the export API. +experimental[] Create sets of {kib} saved objects from a file created by the export API. [[saved-objects-api-import-request]] ==== Request diff --git a/docs/api/saved-objects/update.asciidoc b/docs/api/saved-objects/update.asciidoc index a1e7be37e1c4a..5c4bb98d09228 100644 --- a/docs/api/saved-objects/update.asciidoc +++ b/docs/api/saved-objects/update.asciidoc @@ -4,7 +4,7 @@ Update object ++++ -experimental[] Update the attributes for an existing {kib} saved object. +experimental[] Update the attributes for existing {kib} saved objects. [[saved-objects-api-update-request]] ==== Request diff --git a/docs/api/spaces-management.asciidoc b/docs/api/spaces-management.asciidoc index bb949667963b7..2e3b9abec9120 100644 --- a/docs/api/spaces-management.asciidoc +++ b/docs/api/spaces-management.asciidoc @@ -1,16 +1,24 @@ [role="xpack"] [[spaces-api]] -== Kibana Spaces APIs +== {kib} spaces APIs Manage your {kib} spaces. -* <> -* <> -* <> -* <> -* <> -* <> -* <> +The following {kib} spaces APIs are available: + +* <> to create a {kib} space + +* <> to update an existing {kib} space + +* <> to retrieve a specified {kib} space + +* <> to retrieve all {kib} spaces + +* <> to delete a {kib} space + +* <> to copy saved objects between spaces + +* <> to overwrite saved objects returned as errors from the copy saved objects to space API include::spaces-management/post.asciidoc[] include::spaces-management/put.asciidoc[] diff --git a/docs/api/spaces-management/delete.asciidoc b/docs/api/spaces-management/delete.asciidoc index 64b90f3d49dad..c66307ea3070f 100644 --- a/docs/api/spaces-management/delete.asciidoc +++ b/docs/api/spaces-management/delete.asciidoc @@ -4,7 +4,7 @@ Delete space ++++ -Deletes a {kib} space. +Delete a {kib} space. experimental["The underlying Spaces concepts are stable, but the APIs for managing Spaces are experimental."] diff --git a/docs/api/spaces-management/get.asciidoc b/docs/api/spaces-management/get.asciidoc index 569117e866d2a..49119d7602b20 100644 --- a/docs/api/spaces-management/get.asciidoc +++ b/docs/api/spaces-management/get.asciidoc @@ -4,7 +4,7 @@ Get space ++++ -Retrieves a specified {kib} space. +Retrieve a specified {kib} space. experimental["The underlying Spaces concepts are stable, but the APIs for managing Spaces are experimental."] diff --git a/docs/api/spaces-management/get_all.asciidoc b/docs/api/spaces-management/get_all.asciidoc index 6f683b864e145..4ae67a0dcca8b 100644 --- a/docs/api/spaces-management/get_all.asciidoc +++ b/docs/api/spaces-management/get_all.asciidoc @@ -4,7 +4,7 @@ Get all spaces ++++ -Retrieves all {kib} spaces. +Retrieve all {kib} spaces. experimental["The underlying Spaces concepts are stable, but the APIs for managing Spaces are experimental."] diff --git a/docs/api/spaces-management/post.asciidoc b/docs/api/spaces-management/post.asciidoc index ea8311ea8213f..d8c194d8dc2b5 100644 --- a/docs/api/spaces-management/post.asciidoc +++ b/docs/api/spaces-management/post.asciidoc @@ -4,7 +4,7 @@ Create space ++++ -Creates a {kib} space. +Create a {kib} space. experimental["The underlying Spaces concepts are stable, but the APIs for managing Spaces are experimental."] diff --git a/docs/api/spaces-management/put.asciidoc b/docs/api/spaces-management/put.asciidoc index 113fdbce6a90a..586818707c76f 100644 --- a/docs/api/spaces-management/put.asciidoc +++ b/docs/api/spaces-management/put.asciidoc @@ -4,7 +4,7 @@ Update space ++++ -Updates an existing {kib} space. +Update an existing {kib} space. experimental["The underlying Spaces concepts are stable, but the APIs for managing Spaces are experimental."] diff --git a/docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc b/docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc index bb82c07e765cb..7b52125599c05 100644 --- a/docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc +++ b/docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc @@ -5,7 +5,7 @@ Resolve copy to space conflicts ++++ -Overwrites specific saved objects that were returned as errors from the <>. +Overwrite specific saved objects that were returned as errors from the <>. experimental["The underlying Spaces concepts are stable, but the APIs for managing Spaces are experimental."] diff --git a/docs/api/upgrade-assistant.asciidoc b/docs/api/upgrade-assistant.asciidoc index 3cb256c337175..b524307c0f273 100644 --- a/docs/api/upgrade-assistant.asciidoc +++ b/docs/api/upgrade-assistant.asciidoc @@ -4,10 +4,15 @@ Check the upgrade status of your Elasticsearch cluster and reindex indices that were created in the previous major version. The assistant helps you prepare for the next major version of Elasticsearch. -* <> -* <> -* <> -* <> +The following upgrade assistant APIs are available: + +* <> to check the status of your cluster + +* <> to start a new reindex or resume a paused reindex + +* <> to check the status of the reindex operation + +* <> to cancel reindexes that are waiting for the Elasticsearch reindex task to complete include::upgrade-assistant/status.asciidoc[] include::upgrade-assistant/reindexing.asciidoc[] diff --git a/docs/api/url-shortening.asciidoc b/docs/api/url-shortening.asciidoc index e98210e92d94a..8bc701a3d5d12 100644 --- a/docs/api/url-shortening.asciidoc +++ b/docs/api/url-shortening.asciidoc @@ -1,11 +1,57 @@ [[url-shortening-api]] -== URL shortening API +=== Shorten URL API +++++ +Shorten URL +++++ -{kib} URLs contain the state of the application, which makes them long and cumbersome. +experimental[] Convert a {kib} URL into a token. {kib} URLs contain the state of the application, which makes them long and cumbersome. Internet Explorer has URL length restrictions, and some wiki and markup parsers don't do well with the full-length version of the {kib} URL. Short URLs are designed to make sharing {kib} URLs easier. -* <> +[[url-shortening-api-request]] +==== Request -include::url_shortening/shorten_url.asciidoc[] +`POST /api/shorten_url` + +[[url-shortening-api-request-body]] +==== Request body + +`url`:: + (Required, string) The {kib} URL that you want to shorten, Relative to `/app/kibana`. + +[[url-shortening-api-response-body]] +==== Response body + +urlId:: A top level property that contains the shortened URL token for the provided request body. + +[[url-shortening-api-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[url-shortening-api-example]] +==== Example + +[source,js] +-------------------------------------------------- +POST api/shorten_url +{ + "url": "/app/kibana#/dashboard?_g=()&_a=(description:'',filters:!(),fullScreenMode:!f,options:(hidePanelTitles:!f,useMargins:!t),panels:!((embeddableConfig:(),gridData:(h:15,i:'1',w:24,x:0,y:0),id:'8f4d0c00-4c86-11e8-b3d7-01146121b73d',panelIndex:'1',type:visualization,version:'7.0.0-alpha1')),query:(language:lucene,query:''),timeRestore:!f,title:'New%20Dashboard',viewMode:edit)" +} +-------------------------------------------------- +// KIBANA + +The API returns the following result: + +[source,js] +-------------------------------------------------- +{ + "urlId": "f73b295ff92718b26bc94edac766d8e3" +} +-------------------------------------------------- + +For easy sharing, construct the shortened {kib} URL: + +`http://localhost:5601/goto/f73b295ff92718b26bc94edac766d8e3` diff --git a/docs/api/url_shortening/shorten_url.asciidoc b/docs/api/url_shortening/shorten_url.asciidoc deleted file mode 100644 index 0e04a32efe35e..0000000000000 --- a/docs/api/url_shortening/shorten_url.asciidoc +++ /dev/null @@ -1,55 +0,0 @@ -[[shorten-url-api]] -=== Shorten URL API -++++ -Shorten URL -++++ - -experimental[] Convert a {kib} URL into a token. - -[[url-shortening-api-request]] -==== Request - -`POST /api/shorten_url` - -[[url-shortening-api-request-body]] -==== Request body - -`url`:: - (Required, string) The {kib} URL that you want to shorten, Relative to `/app/kibana`. - -[[url-shortening-api-response-body]] -==== Response body - -urlId:: A top level property that contains the shortened URL token for the provided request body. - -[[url-shortening-api-codes]] -==== Response code - -`200`:: - Indicates a successful call. - -[[url-shortening-api-example]] -==== Example - -[source,js] --------------------------------------------------- -POST api/shorten_url -{ - "url": "/app/kibana#/dashboard?_g=()&_a=(description:'',filters:!(),fullScreenMode:!f,options:(hidePanelTitles:!f,useMargins:!t),panels:!((embeddableConfig:(),gridData:(h:15,i:'1',w:24,x:0,y:0),id:'8f4d0c00-4c86-11e8-b3d7-01146121b73d',panelIndex:'1',type:visualization,version:'7.0.0-alpha1')),query:(language:lucene,query:''),timeRestore:!f,title:'New%20Dashboard',viewMode:edit)" -} --------------------------------------------------- -// KIBANA - -The API returns the following result: - -[source,js] --------------------------------------------------- -{ - "urlId": "f73b295ff92718b26bc94edac766d8e3" -} --------------------------------------------------- - -For easy sharing, construct the shortened {kib} URL: - -`http://localhost:5601/goto/f73b295ff92718b26bc94edac766d8e3` - diff --git a/docs/api/using-api.asciidoc b/docs/api/using-api.asciidoc new file mode 100644 index 0000000000000..82071af463636 --- /dev/null +++ b/docs/api/using-api.asciidoc @@ -0,0 +1,46 @@ +[[using-api]] +== Using the APIs + +Interact with the {kib} APIs through the `curl` command and HTTP and HTTPs protocols. + +It is recommended that you use HTTPs on port 5601 because it is more secure. + +NOTE: The {kib} Console supports only Elasticsearch APIs. You are unable to interact with the {kib} APIs with the Console and must use `curl` or another HTTP tool instead. For more information, refer to <>. + +[float] +[[api-authentication]] +=== Authentication +{kib} supports token-based authentication with the same username and password that you use to log into the {kib} Console. + +[float] +[[api-calls]] +=== API calls +API calls are stateless. Each request that you make happens in isolation from other calls and must include all of the necessary information for {kib} to fulfill the request. API requests return JSON output, which is a format that is machine-readable and works well for automation. + +Calls to the API endpoints require different operations. To interact with the {kib} APIs, use the following operations: + +* *GET* - Fetches the information. + +* *POST* - Adds new information. + +* *PUT* - Updates the existing information. + +* *DELETE* - Removes the information. + +For example, the following `curl` command exports a dashboard: + +[source,sh] +-- +curl -X POST -u $USER:$PASSWORD "localhost:5601/api/kibana/dashboards/export?dashboard=942dcef0-b2cd-11e8-ad8e-85441f0c2e5c" +-- + +The following {kib} APIs are available: + +* <> +* <> +* <> +* <> +* <> +* <> +* <> +* <> diff --git a/docs/dev-tools/console/console.asciidoc b/docs/dev-tools/console/console.asciidoc index a678e7189abec..26620688499af 100644 --- a/docs/dev-tools/console/console.asciidoc +++ b/docs/dev-tools/console/console.asciidoc @@ -12,6 +12,8 @@ To get started, go to *Dev Tools > Console*. [role="screenshot"] image::dev-tools/console/images/console.png["Console"] +NOTE: You are unable to interact with the REST API of {kib} with the Console. + [float] [[console-api]] === Write requests diff --git a/docs/user/api.asciidoc b/docs/user/api.asciidoc index 8ad8b71c789f4..20f1fc89367f2 100644 --- a/docs/user/api.asciidoc +++ b/docs/user/api.asciidoc @@ -8,7 +8,7 @@ integration with {kib}, or automating certain aspects of configuring and deploying {kib}. Each API is experimental and can include breaking changes in any version of -{kib}, or might have been entirely removed from {kib}. +{kib}, or might be entirely removed from {kib}. //// Each API has one of the following labels: @@ -29,25 +29,14 @@ entirely. If a label is missing from an API, it is considered `experimental`. //// -NOTE: You cannot access the APIs via the Console in {kib}. - -[float] -== APIs -* <> -* <> -* <> -* <> -* <> -* <> -* <> -* <> -- +include::{kib-repo-dir}/api/using-api.asciidoc[] include::{kib-repo-dir}/api/features.asciidoc[] include::{kib-repo-dir}/api/spaces-management.asciidoc[] include::{kib-repo-dir}/api/role-management.asciidoc[] include::{kib-repo-dir}/api/saved-objects.asciidoc[] -include::{kib-repo-dir}/api/dashboard-import.asciidoc[] +include::{kib-repo-dir}/api/dashboard-api.asciidoc[] include::{kib-repo-dir}/api/logstash-configuration-management.asciidoc[] include::{kib-repo-dir}/api/url-shortening.asciidoc[] include::{kib-repo-dir}/api/upgrade-assistant.asciidoc[] From 2335902ffa89a722e58493f9dbf149c3c1e47858 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Wed, 16 Oct 2019 17:52:30 -0400 Subject: [PATCH 48/48] [SIEM Remove GraphiQL (#48135) * remove GraphIql in production * fix api intergation on CI --- .../lib/framework/kibana_framework_adapter.ts | 29 +++++++++++-------- .../apis/siem/feature_controls.ts | 18 ++++++++++-- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts index 6616087d0ea66..ea6607325bef7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/framework/kibana_framework_adapter.ts @@ -5,6 +5,7 @@ */ import { GenericParams } from 'elasticsearch'; +import { EnvironmentMode } from 'kibana/public'; import { GraphQLSchema } from 'graphql'; import { Legacy } from 'kibana'; @@ -28,9 +29,11 @@ interface CallWithRequestParams extends GenericParams { export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { public version: string; + public envMode: EnvironmentMode; constructor(private server: Legacy.Server) { this.version = server.config().get('pkg.version'); + this.envMode = server.newPlatform.env.mode; } public async callWithRequest( @@ -90,19 +93,21 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter { plugin: graphqlHapi, }); - this.server.register({ - options: { - graphiqlOptions: { - endpointURL: routePath, - passHeader: `'kbn-version': '${this.version}'`, - }, - path: `${routePath}/graphiql`, - route: { - tags: ['access:siem'], + if (!this.envMode.prod) { + this.server.register({ + options: { + graphiqlOptions: { + endpointURL: routePath, + passHeader: `'kbn-version': '${this.version}'`, + }, + path: `${routePath}/graphiql`, + route: { + tags: ['access:siem'], + }, }, - }, - plugin: graphiqlHapi, - }); + plugin: graphiqlHapi, + }); + } } public getIndexPatternsService( diff --git a/x-pack/test/api_integration/apis/siem/feature_controls.ts b/x-pack/test/api_integration/apis/siem/feature_controls.ts index 836c35386b332..9bb36810021af 100644 --- a/x-pack/test/api_integration/apis/siem/feature_controls.ts +++ b/x-pack/test/api_integration/apis/siem/feature_controls.ts @@ -20,6 +20,7 @@ const introspectionQuery = gql` `; export default function({ getService }: FtrProviderContext) { + const config = getService('config'); const supertest = getService('supertestWithoutAuth'); const security: SecurityService = getService('security'); const spaces: SpacesService = getService('spaces'); @@ -82,6 +83,11 @@ export default function({ getService }: FtrProviderContext) { }; describe('feature controls', () => { + let isProd = false; + before(() => { + const kbnConfig = config.get('servers.kibana'); + isProd = kbnConfig.hostname === 'localhost' && kbnConfig.port === 5620 ? false : true; + }); it(`APIs can't be accessed by user with no privileges`, async () => { const username = 'logstash_read'; const roleName = 'logstash_read'; @@ -130,7 +136,11 @@ export default function({ getService }: FtrProviderContext) { expectGraphQLResponse(graphQLResult); const graphQLIResult = await executeGraphIQLRequest(username, password); - expectGraphIQLResponse(graphQLIResult); + if (!isProd) { + expectGraphIQLResponse(graphQLIResult); + } else { + expectGraphIQL404(graphQLIResult); + } } finally { await security.role.delete(roleName); await security.user.delete(username); @@ -225,7 +235,11 @@ export default function({ getService }: FtrProviderContext) { expectGraphQLResponse(graphQLResult); const graphQLIResult = await executeGraphIQLRequest(username, password, space1Id); - expectGraphIQLResponse(graphQLIResult); + if (!isProd) { + expectGraphIQLResponse(graphQLIResult); + } else { + expectGraphIQL404(graphQLIResult); + } }); it(`user_1 can't access APIs in space_2`, async () => {