From 4f535c6621ae6b5ec8df3027d7524e16a8c84ead Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Mon, 9 Aug 2021 18:42:55 -0700 Subject: [PATCH 001/104] [DOCS] Updates file data visualizer details (#107609) --- docs/setup/connect-to-elasticsearch.asciidoc | 26 ++++++++++++++------ docs/user/ml/index.asciidoc | 2 +- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index 880c98902983f..25af236845abe 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -46,18 +46,30 @@ image::images/add-data-fleet.png[Add data using Fleet] [[upload-data-kibana]] === Upload a file -experimental[] If your data is in a CSV, JSON, or log file, you can upload it using the {file-data-viz}. You can upload a file up to 100 MB. This value is configurable up to 1 GB in -<>. To upload a file with geospatial data, -refer to <>. +If you have a log file or delimited CSV, TSV, or JSON file, you can upload it, +view its fields and metrics, and optionally import it into {es}. -[role="screenshot"] -image::images/add-data-fv.png[File Data Visualizer] +NOTE: This feature is not intended for use as part of a repeated production +process, but rather for the initial exploration of your data. + +You can upload a file up to 100 MB. This value is configurable up to 1 GB in +<>. To upload a file with geospatial +data, refer to <>. +[role="screenshot"] +image::images/add-data-fv.png[Uploading a file in {kib}] +The {stack-security-features} provide roles and privileges that control which +users can upload files. To upload a file in {kib} and import it into an {es} +index, you'll need: -NOTE: This feature is not intended for use as part of a -repeated production process, but rather for the initial exploration of your data. +* `all` {kib} privileges for *Discover* +* `manage_pipeline` or `manage_ingest_pipelines` cluster privilege +* `create`, `create_index`, `manage`, and `read` index privileges for the index +You can manage your roles, privileges, and spaces in **{stack-manage-app}** in +{kib}. For more information, refer to +<>. [discrete] === Additional options for loading your data diff --git a/docs/user/ml/index.asciidoc b/docs/user/ml/index.asciidoc index a05ff1eeec4a6..842d7cb054f32 100644 --- a/docs/user/ml/index.asciidoc +++ b/docs/user/ml/index.asciidoc @@ -17,7 +17,7 @@ if your data is stored in {es} and contains a time field, you can use the [role="screenshot"] image::user/ml/images/ml-data-visualizer-sample.jpg[{data-viz} for sample flight data] -experimental[] You can also upload a CSV, NDJSON, or log file. The *{data-viz}* +You can also upload a CSV, NDJSON, or log file. The *{data-viz}* identifies the file format and field mappings. You can then optionally import that data into an {es} index. To change the default file size limit, see <>. From 6b29ca1ce8b1de4d8a8433391b0bb778434dae8b Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Mon, 9 Aug 2021 23:00:58 -0400 Subject: [PATCH 002/104] [Security Solution][Detection Page] Status filter refactor (#107249) --- .../alerts_histogram_panel/index.test.tsx | 63 +++++++++++++- .../alerts_filter_group/index.tsx | 35 ++++++-- .../components/alerts_table/index.test.tsx | 2 - .../components/alerts_table/index.tsx | 27 +----- .../detection_engine/detection_engine.tsx | 85 ++++++++++++++++--- .../detection_engine/rules/details/index.tsx | 85 +++++++++++++++++-- 6 files changed, 242 insertions(+), 55 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx index 0d6793eb2b886..484cd66575005 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { waitFor, act } from '@testing-library/react'; import { mount } from 'enzyme'; -import { esQuery } from '../../../../../../../../src/plugins/data/public'; +import { esQuery, Filter } from '../../../../../../../../src/plugins/data/public'; import { TestProviders } from '../../../../common/mock'; import { SecurityPageName } from '../../../../app/types'; @@ -78,6 +78,11 @@ describe('AlertsHistogramPanel', () => { updateDateRange: jest.fn(), }; + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + it('renders correctly', () => { const wrapper = mount( @@ -157,7 +162,7 @@ describe('AlertsHistogramPanel', () => { combinedQueries: '{"bool":{"must":[],"filter":[{"match_all":{}},{"exists":{"field":"process.name"}}],"should":[],"must_not":[]}}', }; - mount( + const wrapper = mount( @@ -180,6 +185,60 @@ describe('AlertsHistogramPanel', () => { ], ]); }); + wrapper.unmount(); + }); + }); + + describe('Filters', () => { + it('filters props is valid, alerts query include filter', async () => { + const mockGetAlertsHistogramQuery = jest.spyOn(helpers, 'getAlertsHistogramQuery'); + const statusFilter: Filter = { + meta: { + alias: null, + disabled: false, + key: 'signal.status', + negate: false, + params: { + query: 'open', + }, + type: 'phrase', + }, + query: { + term: { + 'signal.status': 'open', + }, + }, + }; + + const props = { + ...defaultProps, + query: { query: '', language: 'kql' }, + filters: [statusFilter], + }; + const wrapper = mount( + + + + ); + + await waitFor(() => { + expect(mockGetAlertsHistogramQuery.mock.calls[1]).toEqual([ + 'signal.rule.name', + '2020-07-07T08:20:18.966Z', + '2020-07-08T08:20:18.966Z', + [ + { + bool: { + filter: [{ term: { 'signal.status': 'open' } }], + must: [], + must_not: [], + should: [], + }, + }, + ], + ]); + }); + wrapper.unmount(); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.tsx index db918951b8555..82f58b5b1c723 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_filter_group/index.tsx @@ -6,7 +6,9 @@ */ import { EuiFilterButton, EuiFilterGroup } from '@elastic/eui'; +import { rgba } from 'polished'; import React, { useCallback, useState } from 'react'; +import styled from 'styled-components'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; import * as i18n from '../translations'; @@ -14,6 +16,17 @@ export const FILTER_OPEN: Status = 'open'; export const FILTER_CLOSED: Status = 'closed'; export const FILTER_IN_PROGRESS: Status = 'in-progress'; +const StatusFilterButton = styled(EuiFilterButton)<{ isActive: boolean }>` + background: ${({ isActive, theme }) => (isActive ? theme.eui.euiColorPrimary : '')}; +`; + +const StatusFilterGroup = styled(EuiFilterGroup)` + background: ${({ theme }) => rgba(theme.eui.euiColorPrimary, 0.2)}; + .euiButtonEmpty--ghost:enabled:focus { + background-color: ${({ theme }) => theme.eui.euiColorPrimary}; + } +`; + interface Props { onFilterGroupChanged: (filterGroup: Status) => void; } @@ -37,33 +50,39 @@ const AlertsTableFilterGroupComponent: React.FC = ({ onFilterGroupChanged }, [setFilterGroup, onFilterGroupChanged]); return ( - - + {i18n.OPEN_ALERTS} - + - {i18n.IN_PROGRESS_ALERTS} - + - {i18n.CLOSED_ALERTS} - - + + ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx index be11aecfe47dd..dba7915460ada 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx @@ -35,9 +35,7 @@ describe('AlertsTableComponent', () => { isSelectAllChecked={false} clearSelected={jest.fn()} setEventsLoading={jest.fn()} - clearEventsLoading={jest.fn()} setEventsDeleted={jest.fn()} - clearEventsDeleted={jest.fn()} showBuildingBlockAlerts={false} onShowBuildingBlockAlertsChanged={jest.fn()} showOnlyThreatIndicatorAlerts={false} diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index a1f2025c6c0d5..40b6a21ea3e68 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -31,7 +31,6 @@ import { alertsDefaultModelRuleRegistry, buildAlertStatusFilterRuleRegistry, } from './default_config'; -import { FILTER_OPEN, AlertsTableFilterGroup } from './alerts_filter_group'; import { AlertsUtilityBar } from './alerts_utility_bar'; import * as i18nCommon from '../../../common/translations'; import * as i18n from './translations'; @@ -68,13 +67,12 @@ interface OwnProps { showOnlyThreatIndicatorAlerts: boolean; timelineId: TimelineIdLiteral; to: string; + filterGroup?: Status; } type AlertsTableComponentProps = OwnProps & PropsFromRedux; export const AlertsTableComponent: React.FC = ({ - clearEventsDeleted, - clearEventsLoading, clearSelected, defaultFilters, from, @@ -95,10 +93,10 @@ export const AlertsTableComponent: React.FC = ({ showOnlyThreatIndicatorAlerts, timelineId, to, + filterGroup = 'open', }) => { const dispatch = useDispatch(); const [showClearSelectionAction, setShowClearSelectionAction] = useState(false); - const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); const { browserFields, indexPattern: indexPatterns, @@ -216,17 +214,6 @@ export const AlertsTableComponent: React.FC = ({ } }, [dispatch, isSelectAllChecked, timelineId]); - // Callback for when open/closed filter changes - const onFilterGroupChangedCallback = useCallback( - (newFilterGroup: Status) => { - clearEventsLoading!({ id: timelineId }); - clearEventsDeleted!({ id: timelineId }); - clearSelected!({ id: timelineId }); - setFilterGroup(newFilterGroup); - }, - [clearEventsLoading, clearEventsDeleted, clearSelected, setFilterGroup, timelineId] - ); - // Callback for clearing entire selection from utility bar const clearSelectionCallback = useCallback(() => { clearSelected!({ id: timelineId }); @@ -372,11 +359,6 @@ export const AlertsTableComponent: React.FC = ({ ); }, [dispatch, defaultTimelineModel, filterManager, tGridEnabled, timelineId]); - const headerFilterGroup = useMemo( - () => , - [onFilterGroupChangedCallback] - ); - if (loading || indexPatternsLoading || isEmpty(selectedPatterns)) { return ( @@ -393,7 +375,6 @@ export const AlertsTableComponent: React.FC = ({ defaultModel={defaultTimelineModel} end={to} currentFilter={filterGroup} - headerFilterGroup={headerFilterGroup} id={timelineId} onRuleChange={onRuleChange} renderCellValue={RenderCellValue} @@ -438,8 +419,6 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ eventIds: string[]; isLoading: boolean; }) => dispatch(timelineActions.setEventsLoading({ id, eventIds, isLoading })), - clearEventsLoading: ({ id }: { id: string }) => - dispatch(timelineActions.clearEventsLoading({ id })), setEventsDeleted: ({ id, eventIds, @@ -449,8 +428,6 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ eventIds: string[]; isDeleted: boolean; }) => dispatch(timelineActions.setEventsDeleted({ id, eventIds, isDeleted })), - clearEventsDeleted: ({ id }: { id: string }) => - dispatch(timelineActions.clearEventsDeleted({ id })), }); const connector = connect(makeMapStateToProps, mapDispatchToProps); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index f7e4ebd4daf1e..323ef93133e24 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -5,11 +5,19 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiWindowEvent } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiWindowEvent, + EuiHorizontalRule, +} from '@elastic/eui'; import styled from 'styled-components'; import { noop } from 'lodash/fp'; import React, { useCallback, useMemo, useRef, useState } from 'react'; -import { useDispatch } from 'react-redux'; +import { connect, ConnectedProps, useDispatch } from 'react-redux'; +import { Dispatch } from 'redux'; +import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { isTab } from '../../../../../timelines/public'; import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector'; @@ -44,9 +52,11 @@ import { resetKeyboardFocus, showGlobalFilters, } from '../../../timelines/components/timeline/helpers'; -import { timelineSelectors } from '../../../timelines/store/timeline'; +import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { + buildAlertStatusFilter, + buildAlertStatusFilterRuleRegistry, buildShowBuildingBlockFilter, buildShowBuildingBlockFilterRuleRegistry, buildThreatMatchFilter, @@ -58,6 +68,10 @@ import { MissingPrivilegesCallOut } from '../../components/callouts/missing_priv import { useKibana } from '../../../common/lib/kibana'; import { AlertsCountPanel } from '../../components/alerts_kpis/alerts_count_panel'; import { CHART_HEIGHT } from '../../components/alerts_kpis/common/config'; +import { + AlertsTableFilterGroup, + FILTER_OPEN, +} from '../../components/alerts_table/alerts_filter_group'; /** * Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space. @@ -68,7 +82,13 @@ const StyledFullHeightContainer = styled.div` flex: 1 1 auto; `; -const DetectionEnginePageComponent = () => { +type DetectionEngineComponentProps = PropsFromRedux; + +const DetectionEnginePageComponent: React.FC = ({ + clearEventsDeleted, + clearEventsLoading, + clearSelected, +}) => { const dispatch = useDispatch(); const containerElement = useRef(null); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); @@ -108,6 +128,7 @@ const DetectionEnginePageComponent = () => { const [showOnlyThreatIndicatorAlerts, setShowOnlyThreatIndicatorAlerts] = useState(false); const loading = userInfoLoading || listsConfigLoading; const { navigateToUrl } = useKibana().services.application; + const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); const updateDateRangeCallback = useCallback( ({ x }) => { @@ -134,23 +155,51 @@ const DetectionEnginePageComponent = () => { [formatUrl, navigateToUrl] ); + // Callback for when open/closed filter changes + const onFilterGroupChangedCallback = useCallback( + (newFilterGroup: Status) => { + const timelineId = TimelineId.detectionsPage; + clearEventsLoading!({ id: timelineId }); + clearEventsDeleted!({ id: timelineId }); + clearSelected!({ id: timelineId }); + setFilterGroup(newFilterGroup); + }, + [clearEventsLoading, clearEventsDeleted, clearSelected, setFilterGroup] + ); + const alertsHistogramDefaultFilters = useMemo( () => [ ...filters, ...(ruleRegistryEnabled - ? buildShowBuildingBlockFilterRuleRegistry(showBuildingBlockAlerts) // TODO: Once we are past experimental phase this code should be removed - : buildShowBuildingBlockFilter(showBuildingBlockAlerts)), + ? [ + // TODO: Once we are past experimental phase this code should be removed + ...buildShowBuildingBlockFilterRuleRegistry(showBuildingBlockAlerts), + ...buildAlertStatusFilterRuleRegistry(filterGroup), + ] + : [ + ...buildShowBuildingBlockFilter(showBuildingBlockAlerts), + ...buildAlertStatusFilter(filterGroup), + ]), ...buildThreatMatchFilter(showOnlyThreatIndicatorAlerts), ], - [filters, ruleRegistryEnabled, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts] + [ + filters, + ruleRegistryEnabled, + showBuildingBlockAlerts, + showOnlyThreatIndicatorAlerts, + filterGroup, + ] ); // AlertsTable manages global filters itself, so not including `filters` const alertsTableDefaultFilters = useMemo( () => [ ...(ruleRegistryEnabled - ? buildShowBuildingBlockFilterRuleRegistry(showBuildingBlockAlerts) // TODO: Once we are past experimental phase this code should be removed - : buildShowBuildingBlockFilter(showBuildingBlockAlerts)), + ? [ + // TODO: Once we are past experimental phase this code should be removed + ...buildShowBuildingBlockFilterRuleRegistry(showBuildingBlockAlerts), + ] + : [...buildShowBuildingBlockFilter(showBuildingBlockAlerts)]), ...buildThreatMatchFilter(showOnlyThreatIndicatorAlerts), ], [ruleRegistryEnabled, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts] @@ -254,6 +303,9 @@ const DetectionEnginePageComponent = () => { {i18n.BUTTON_MANAGE_RULES} + + + { showOnlyThreatIndicatorAlerts={showOnlyThreatIndicatorAlerts} onShowOnlyThreatIndicatorAlertsChanged={onShowOnlyThreatIndicatorAlertsCallback} to={to} + filterGroup={filterGroup} /> @@ -304,4 +357,16 @@ const DetectionEnginePageComponent = () => { ); }; -export const DetectionEnginePage = React.memo(DetectionEnginePageComponent); +const mapDispatchToProps = (dispatch: Dispatch) => ({ + clearSelected: ({ id }: { id: string }) => dispatch(timelineActions.clearSelected({ id })), + clearEventsLoading: ({ id }: { id: string }) => + dispatch(timelineActions.clearEventsLoading({ id })), + clearEventsDeleted: ({ id }: { id: string }) => + dispatch(timelineActions.clearEventsDeleted({ id })), +}); + +const connector = connect(null, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const DetectionEnginePage = connector(React.memo(DetectionEnginePageComponent)); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 233189a3e8be9..230eaeb10939d 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -23,7 +23,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { noop } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useParams } from 'react-router-dom'; -import { useDispatch } from 'react-redux'; +import { connect, ConnectedProps, useDispatch } from 'react-redux'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; import { @@ -31,6 +31,7 @@ import { ExceptionListIdentifiers, } from '@kbn/securitysolution-io-ts-list-types'; +import { Dispatch } from 'redux'; import { isTab } from '../../../../../../../timelines/public'; import { useDeepEqualSelector, @@ -63,6 +64,8 @@ import { StepDefineRule } from '../../../../components/rules/step_define_rule'; import { StepScheduleRule } from '../../../../components/rules/step_schedule_rule'; import { buildAlertsRuleIdFilter, + buildAlertStatusFilter, + buildAlertStatusFilterRuleRegistry, buildShowBuildingBlockFilter, buildShowBuildingBlockFilterRuleRegistry, buildThreatMatchFilter, @@ -98,7 +101,7 @@ import { resetKeyboardFocus, showGlobalFilters, } from '../../../../../timelines/components/timeline/helpers'; -import { timelineSelectors } from '../../../../../timelines/store/timeline'; +import { timelineActions, timelineSelectors } from '../../../../../timelines/store/timeline'; import { timelineDefaults } from '../../../../../timelines/store/timeline/defaults'; import { useSourcererScope } from '../../../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../../../common/store/sourcerer/model'; @@ -118,6 +121,11 @@ import { MissingPrivilegesCallOut } from '../../../../components/callouts/missin import { useRuleWithFallback } from '../../../../containers/detection_engine/rules/use_rule_with_fallback'; import { BadgeOptions } from '../../../../../common/components/header_page/types'; import { AlertsStackByField } from '../../../../components/alerts_kpis/common/types'; +import { Status } from '../../../../../../common/detection_engine/schemas/common/schemas'; +import { + AlertsTableFilterGroup, + FILTER_OPEN, +} from '../../../../components/alerts_table/alerts_filter_group'; /** * Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space. @@ -155,7 +163,13 @@ const ruleDetailTabs = [ }, ]; -const RuleDetailsPageComponent = () => { +type DetectionEngineComponentProps = PropsFromRedux; + +const RuleDetailsPageComponent: React.FC = ({ + clearEventsDeleted, + clearEventsLoading, + clearSelected, +}) => { const { navigateToApp } = useKibana().services.application; const dispatch = useDispatch(); const containerElement = useRef(null); @@ -226,6 +240,7 @@ const RuleDetailsPageComponent = () => { const mlCapabilities = useMlCapabilities(); const { formatUrl } = useFormatUrl(SecurityPageName.rules); const { globalFullScreen } = useGlobalFullScreen(); + const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); // TODO: Once we are past experimental phase this code should be removed const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled'); @@ -315,6 +330,18 @@ const RuleDetailsPageComponent = () => { [rule, ruleLoading] ); + // Callback for when open/closed filter changes + const onFilterGroupChangedCallback = useCallback( + (newFilterGroup: Status) => { + const timelineId = TimelineId.detectionsPage; + clearEventsLoading!({ id: timelineId }); + clearEventsDeleted!({ id: timelineId }); + clearSelected!({ id: timelineId }); + setFilterGroup(newFilterGroup); + }, + [clearEventsLoading, clearEventsDeleted, clearSelected, setFilterGroup] + ); + // Set showBuildingBlockAlerts if rule is a Building Block Rule otherwise we won't show alerts useEffect(() => { setShowBuildingBlockAlerts(rule?.building_block_type != null); @@ -324,11 +351,38 @@ const RuleDetailsPageComponent = () => { () => [ ...buildAlertsRuleIdFilter(ruleId), ...(ruleRegistryEnabled - ? buildShowBuildingBlockFilterRuleRegistry(showBuildingBlockAlerts) // TODO: Once we are past experimental phase this code should be removed - : buildShowBuildingBlockFilter(showBuildingBlockAlerts)), + ? [ + ...buildShowBuildingBlockFilterRuleRegistry(showBuildingBlockAlerts), // TODO: Once we are past experimental phase this code should be removed + ...buildAlertStatusFilterRuleRegistry(filterGroup), + ] + : [ + ...buildShowBuildingBlockFilter(showBuildingBlockAlerts), + ...buildAlertStatusFilter(filterGroup), + ]), ...buildThreatMatchFilter(showOnlyThreatIndicatorAlerts), ], - [ruleId, ruleRegistryEnabled, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts] + [ + ruleId, + ruleRegistryEnabled, + showBuildingBlockAlerts, + showOnlyThreatIndicatorAlerts, + filterGroup, + ] + ); + + const alertsTableDefaultFilters = useMemo( + () => [ + ...buildAlertsRuleIdFilter(ruleId), + ...filters, + ...(ruleRegistryEnabled + ? [ + // TODO: Once we are past experimental phase this code should be removed + ...buildShowBuildingBlockFilterRuleRegistry(showBuildingBlockAlerts), + ] + : [...buildShowBuildingBlockFilter(showBuildingBlockAlerts)]), + ...buildThreatMatchFilter(showOnlyThreatIndicatorAlerts), + ], + [ruleId, filters, ruleRegistryEnabled, showBuildingBlockAlerts, showOnlyThreatIndicatorAlerts] ); const alertMergedFilters = useMemo(() => [...alertDefaultFilters, ...filters], [ @@ -705,6 +759,8 @@ const RuleDetailsPageComponent = () => { {ruleDetailTab === RuleDetailTabs.alerts && ( <> + + { {ruleId != null && ( { ); }; +const mapDispatchToProps = (dispatch: Dispatch) => ({ + clearSelected: ({ id }: { id: string }) => dispatch(timelineActions.clearSelected({ id })), + clearEventsLoading: ({ id }: { id: string }) => + dispatch(timelineActions.clearEventsLoading({ id })), + clearEventsDeleted: ({ id }: { id: string }) => + dispatch(timelineActions.clearEventsDeleted({ id })), +}); + +const connector = connect(null, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + RuleDetailsPageComponent.displayName = 'RuleDetailsPageComponent'; -export const RuleDetailsPage = React.memo(RuleDetailsPageComponent); +export const RuleDetailsPage = connector(React.memo(RuleDetailsPageComponent)); RuleDetailsPage.displayName = 'RuleDetailsPage'; From 3d6aa9f44df06eed78e563ffda175bd517e45114 Mon Sep 17 00:00:00 2001 From: Jason Rhodes Date: Tue, 10 Aug 2021 00:09:26 -0400 Subject: [PATCH 003/104] [Stack Monitoring] Adds optional debug logging for Stack Monitoring (#107711) --- .../plugins/monitoring/server/config.test.ts | 5 +- x-pack/plugins/monitoring/server/config.ts | 2 + .../plugins/monitoring/server/debug_logger.ts | 85 +++++++++++++++++++ x-pack/plugins/monitoring/server/plugin.ts | 47 +++++----- .../plugins/monitoring/server/routes/index.ts | 15 +++- 5 files changed, 127 insertions(+), 27 deletions(-) create mode 100644 x-pack/plugins/monitoring/server/debug_logger.ts diff --git a/x-pack/plugins/monitoring/server/config.test.ts b/x-pack/plugins/monitoring/server/config.test.ts index 45b3e07200680..9a5699189241f 100644 --- a/x-pack/plugins/monitoring/server/config.test.ts +++ b/x-pack/plugins/monitoring/server/config.test.ts @@ -7,8 +7,7 @@ import fs from 'fs'; import { when } from 'jest-when'; - -import { createConfig, configSchema } from './config'; +import { configSchema, createConfig } from './config'; const MOCKED_PATHS = [ '/proc/self/cgroup', @@ -71,6 +70,8 @@ describe('config schema', () => { "enabled": false, }, }, + "debug_log_path": "", + "debug_mode": false, "elasticsearch": Object { "apiVersion": "master", "customHeaders": Object {}, diff --git a/x-pack/plugins/monitoring/server/config.ts b/x-pack/plugins/monitoring/server/config.ts index 8c411fb5c28a8..98fd02b03539c 100644 --- a/x-pack/plugins/monitoring/server/config.ts +++ b/x-pack/plugins/monitoring/server/config.ts @@ -25,6 +25,8 @@ export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), ui: schema.object({ enabled: schema.boolean({ defaultValue: true }), + debug_mode: schema.boolean({ defaultValue: false }), + debug_log_path: schema.string({ defaultValue: '' }), ccs: schema.object({ enabled: schema.boolean({ defaultValue: true }), }), diff --git a/x-pack/plugins/monitoring/server/debug_logger.ts b/x-pack/plugins/monitoring/server/debug_logger.ts new file mode 100644 index 0000000000000..0add1f12f0304 --- /dev/null +++ b/x-pack/plugins/monitoring/server/debug_logger.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import fs from 'fs'; +import { MonitoringConfig } from './config'; +import { RouteDependencies } from './types'; + +export function decorateDebugServer( + _server: any, + config: MonitoringConfig, + logger: RouteDependencies['logger'] +) { + // bail if the proper config value is not set (extra protection) + if (!config.ui.debug_mode) { + return _server; + } + + // create a debug logger that will either write to file (if debug_log_path exists) or log out via logger + const debugLog = createDebugLogger({ path: config.ui.debug_log_path, logger }); + + return { + // maintain the rest of _server untouched + ..._server, + // TODO: replace any + route: (options: any) => { + const apiPath = options.path; + return _server.route({ + ...options, + // TODO: replace any + handler: async (req: any) => { + const { elasticsearch: cached } = req.server.plugins; + const apiRequestHeaders = req.headers; + req.server.plugins.elasticsearch = { + ...req.server.plugins.elasticsearch, + getCluster: (name: string) => { + const cluster = cached.getCluster(name); + return { + ...cluster, + // TODO: better types? + callWithRequest: async (_req: typeof req, type: string, params: any) => { + const result = await cluster.callWithRequest(_req, type, params); + + // log everything about this request -> query -> result + debugLog({ + api_path: apiPath, + referer_url: apiRequestHeaders.referer, + query: { + params, + result, + }, + }); + + return result; + }, + }; + }, + }; + return options.handler(req); + }, + }); + }, + }; +} + +function createDebugLogger({ + path, + logger, +}: { + path: string; + logger: RouteDependencies['logger']; +}) { + if (path.length > 0) { + const stream = fs.createWriteStream('./stack_monitoring_debug_log.ndjson', { flags: 'a' }); + return function logToFile(line: any) { + stream.write(JSON.stringify(line)); + }; + } else { + return function logToStdOut(line: any) { + logger.info(JSON.stringify(line)); + }; + } +} diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index aed48b7391529..6bfa052a6fe8f 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -6,53 +6,52 @@ */ import Boom from '@hapi/boom'; -import { i18n } from '@kbn/i18n'; -import { has, get } from 'lodash'; import { TypeOf } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; import { - Logger, - PluginInitializerContext, - KibanaRequest, - KibanaResponseFactory, CoreSetup, - ICustomClusterClient, CoreStart, CustomHttpResponseOptions, - ResponseError, + ICustomClusterClient, + KibanaRequest, + KibanaResponseFactory, + Logger, Plugin, + PluginInitializerContext, + ResponseError, SharedGlobalConfig, } from 'kibana/server'; +import { get, has } from 'lodash'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { - LOGGING_TAG, + ALERTS, KIBANA_MONITORING_LOGGING_TAG, KIBANA_STATS_TYPE_MONITORING, - ALERTS, + LOGGING_TAG, SAVED_OBJECT_TELEMETRY, } from '../common/constants'; -import { MonitoringConfig, createConfig, configSchema } from './config'; -import { requireUIRoutes } from './routes'; +import { AlertsFactory } from './alerts'; +import { configSchema, createConfig, MonitoringConfig } from './config'; +import { instantiateClient } from './es_client/instantiate_client'; import { initBulkUploader } from './kibana_monitoring'; -import { initInfraSource } from './lib/logs/init_infra_source'; import { registerCollectors } from './kibana_monitoring/collectors'; -import { registerMonitoringTelemetryCollection } from './telemetry_collection'; +import { initInfraSource } from './lib/logs/init_infra_source'; import { LicenseService } from './license_service'; -import { AlertsFactory } from './alerts'; +import { requireUIRoutes } from './routes'; +import { EndpointTypes, Globals } from './static_globals'; +import { registerMonitoringTelemetryCollection } from './telemetry_collection'; import { + IBulkUploader, + LegacyRequest, + LegacyShimDependencies, MonitoringCore, MonitoringLicenseService, MonitoringPluginSetup, - LegacyShimDependencies, - IBulkUploader, PluginsSetup, PluginsStart, - LegacyRequest, RequestHandlerContextMonitoringPlugin, } from './types'; -import { Globals, EndpointTypes } from './static_globals'; -import { instantiateClient } from './es_client/instantiate_client'; - // This is used to test the version of kibana const snapshotRegex = /-snapshot/i; @@ -192,7 +191,11 @@ export class MonitoringPlugin plugins ); - requireUIRoutes(this.monitoringCore, { + if (config.ui.debug_mode) { + this.log.info('MONITORING DEBUG MODE: ON'); + } + + requireUIRoutes(this.monitoringCore, config, { cluster, router, licenseService: this.licenseService, diff --git a/x-pack/plugins/monitoring/server/routes/index.ts b/x-pack/plugins/monitoring/server/routes/index.ts index 0f65fde1b2966..05a8de96b4c07 100644 --- a/x-pack/plugins/monitoring/server/routes/index.ts +++ b/x-pack/plugins/monitoring/server/routes/index.ts @@ -4,14 +4,23 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - /* eslint import/namespace: ['error', { allowComputed: true }]*/ + +import { MonitoringConfig } from '../config'; +import { decorateDebugServer } from '../debug_logger'; +import { RouteDependencies } from '../types'; // @ts-ignore import * as uiRoutes from './api/v1/ui'; // namespace import -import { RouteDependencies } from '../types'; -export function requireUIRoutes(server: any, npRoute: RouteDependencies) { +export function requireUIRoutes( + _server: any, + config: MonitoringConfig, + npRoute: RouteDependencies +) { const routes = Object.keys(uiRoutes); + const server = config.ui.debug_mode + ? decorateDebugServer(_server, config, npRoute.logger) + : _server; routes.forEach((route) => { const registerRoute = uiRoutes[route]; // computed reference to module objects imported via namespace From 0ebe3657c1c5598e14827707acfd5c6bb9ec6318 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Tue, 10 Aug 2021 09:08:40 +0300 Subject: [PATCH 004/104] [Canvas] Date format argument refactor. (#106496) * Refactored date_format argument. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../arguments/date_format/{index.ts => index.tsx} | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) rename x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/{index.ts => index.tsx} (72%) diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.tsx similarity index 72% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.tsx index 986b39f8ea2e6..ef5eb18b44590 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.tsx @@ -5,9 +5,10 @@ * 2.0. */ -import { compose, withProps } from 'recompose'; +import React from 'react'; import moment from 'moment'; -import { DateFormatArgInput as Component, Props as ComponentProps } from './date_format'; +import { Assign } from '@kbn/utility-types'; +import { DateFormatArgInput, Props as ComponentProps } from './date_format'; import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component'; import { ArgumentFactory } from '../../../../types/arguments'; import { ArgumentStrings } from '../../../../i18n'; @@ -16,6 +17,10 @@ import { SetupInitializer } from '../../../plugin'; const { DateFormat: strings } = ArgumentStrings; +const getDateFormatArgInput = (defaultDateFormats: ComponentProps['dateFormats']) => ( + props: Assign +) => ; + export const dateFormatInitializer: SetupInitializer> = ( core, plugins @@ -35,12 +40,10 @@ export const dateFormatInitializer: SetupInitializer(withProps({ dateFormats }))(Component); - return () => ({ name: 'dateFormat', displayName: strings.getDisplayName(), help: strings.getHelp(), - simpleTemplate: templateFromReactComponent(DateFormatArgInput), + simpleTemplate: templateFromReactComponent(getDateFormatArgInput(dateFormats)), }); }; From 6fafe876275b7d68c0748f366aa96780c275e27f Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Tue, 10 Aug 2021 09:09:36 +0300 Subject: [PATCH 005/104] [Canvas] Number format argument refactor. (#106502) * Refactored number format argument. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../number_format/{index.ts => index.tsx} | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) rename x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/{index.ts => index.tsx} (77%) diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.tsx similarity index 77% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.tsx index 7c7d573bcd76c..e1d86ba3ff9d2 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import { compose, withProps } from 'recompose'; -import { NumberFormatArgInput as Component, Props as ComponentProps } from './number_format'; +import React from 'react'; +import { Assign } from '@kbn/utility-types'; +import { NumberFormatArgInput, Props as ComponentProps } from './number_format'; import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component'; import { ArgumentFactory } from '../../../../types/arguments'; import { ArgumentStrings } from '../../../../i18n'; @@ -15,6 +16,10 @@ import { FORMATS_UI_SETTINGS } from '../../../../../../../src/plugins/field_form const { NumberFormat: strings } = ArgumentStrings; +const getNumberFormatArgInput = (defaultNumberFormats: ComponentProps['numberFormats']) => ( + props: Assign +) => ; + export const numberFormatInitializer: SetupInitializer> = ( core, plugins @@ -35,14 +40,10 @@ export const numberFormatInitializer: SetupInitializer(withProps({ numberFormats }))( - Component - ); - return () => ({ name: 'numberFormat', displayName: strings.getDisplayName(), help: strings.getHelp(), - simpleTemplate: templateFromReactComponent(NumberFormatArgInput), + simpleTemplate: templateFromReactComponent(getNumberFormatArgInput(numberFormats)), }); }; From 7366df94546d17d89213bc1c688132e5056e0242 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Tue, 10 Aug 2021 09:15:04 +0300 Subject: [PATCH 006/104] [Canvas] `ArgTemplateForm` refactor. (#106637) * Refactored `ArgTemplateForm`. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/arg_form/arg_template_form.js | 95 ------------------- .../components/arg_form/arg_template_form.tsx | 89 +++++++++++++++++ 2 files changed, 89 insertions(+), 95 deletions(-) delete mode 100644 x-pack/plugins/canvas/public/components/arg_form/arg_template_form.js create mode 100644 x-pack/plugins/canvas/public/components/arg_form/arg_template_form.tsx diff --git a/x-pack/plugins/canvas/public/components/arg_form/arg_template_form.js b/x-pack/plugins/canvas/public/components/arg_form/arg_template_form.js deleted file mode 100644 index 4b968c9dd4ee0..0000000000000 --- a/x-pack/plugins/canvas/public/components/arg_form/arg_template_form.js +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { compose, withPropsOnChange, withProps } from 'recompose'; -import { RenderToDom } from '../render_to_dom'; -import { ExpressionFormHandlers } from '../../../common/lib/expression_form_handlers'; - -class ArgTemplateFormComponent extends React.Component { - static propTypes = { - template: PropTypes.func, - argumentProps: PropTypes.shape({ - valueMissing: PropTypes.bool, - label: PropTypes.string, - setLabel: PropTypes.func.isRequired, - expand: PropTypes.bool, - setExpand: PropTypes.func, - onValueRemove: PropTypes.func, - resetErrorState: PropTypes.func.isRequired, - renderError: PropTypes.func.isRequired, - }), - handlers: PropTypes.object.isRequired, - error: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]).isRequired, - errorTemplate: PropTypes.oneOfType([PropTypes.element, PropTypes.func]).isRequired, - }; - - UNSAFE_componentWillUpdate(prevProps) { - //see if error state changed - if (this.props.error !== prevProps.error) { - this.props.handlers.destroy(); - } - } - componentDidUpdate() { - if (this.props.error) { - return this._renderErrorTemplate(); - } - this._renderTemplate(this._domNode); - } - - componentWillUnmount() { - this.props.handlers.destroy(); - } - - _domNode = null; - - _renderTemplate = (domNode) => { - const { template, argumentProps, handlers } = this.props; - if (template) { - return template(domNode, argumentProps, handlers); - } - }; - - _renderErrorTemplate = () => { - const { errorTemplate, argumentProps } = this.props; - return React.createElement(errorTemplate, argumentProps); - }; - - render() { - const { template, error } = this.props; - - if (error) { - return this._renderErrorTemplate(); - } - - if (!template) { - return null; - } - - return ( - { - this._domNode = domNode; - this._renderTemplate(domNode); - }} - /> - ); - } -} - -export const ArgTemplateForm = compose( - withPropsOnChange( - () => false, - () => ({ - expressionFormHandlers: new ExpressionFormHandlers(), - }) - ), - withProps(({ handlers, expressionFormHandlers }) => ({ - handlers: Object.assign(expressionFormHandlers, handlers), - })) -)(ArgTemplateFormComponent); diff --git a/x-pack/plugins/canvas/public/components/arg_form/arg_template_form.tsx b/x-pack/plugins/canvas/public/components/arg_form/arg_template_form.tsx new file mode 100644 index 0000000000000..1d7227d65e536 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/arg_form/arg_template_form.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useEffect, useCallback, useRef } from 'react'; +import usePrevious from 'react-use/lib/usePrevious'; +import { RenderToDom } from '../render_to_dom'; +import { ExpressionFormHandlers } from '../../../common/lib/expression_form_handlers'; + +interface ArgTemplateFormProps { + template?: ( + domNode: HTMLElement, + config: ArgTemplateFormProps['argumentProps'], + handlers: ArgTemplateFormProps['handlers'] + ) => void; + argumentProps: { + valueMissing?: boolean; + label?: string; + setLabel: (label: string) => void; + expand?: boolean; + setExpand?: (expand: boolean) => void; + onValueRemove?: (argName: string, argIndex: string) => void; + resetErrorState: () => void; + renderError: () => void; + }; + handlers?: { [key: string]: (...args: any[]) => any }; + error?: unknown; + errorTemplate: React.FunctionComponent; +} + +const mergeWithFormHandlers = (handlers: ArgTemplateFormProps['handlers']) => + Object.assign(new ExpressionFormHandlers(), handlers); + +export const ArgTemplateForm: React.FunctionComponent = ({ + template, + argumentProps, + handlers, + error, + errorTemplate, +}) => { + const [updatedHandlers, setHandlers] = useState(mergeWithFormHandlers(handlers)); + const previousError = usePrevious(error); + const domNodeRef = useRef(); + const renderTemplate = useCallback( + (domNode) => template && template(domNode, argumentProps, updatedHandlers), + [template, argumentProps, updatedHandlers] + ); + + const renderErrorTemplate = useCallback(() => React.createElement(errorTemplate, argumentProps), [ + errorTemplate, + argumentProps, + ]); + + useEffect(() => { + setHandlers(mergeWithFormHandlers(handlers)); + }, [handlers]); + + useEffect(() => { + if (previousError !== error) { + updatedHandlers.destroy(); + } + }, [previousError, error, updatedHandlers]); + + useEffect(() => { + if (!error) { + renderTemplate(domNodeRef.current); + } + }, [error, renderTemplate, domNodeRef]); + + if (error) { + return renderErrorTemplate(); + } + + if (!template) { + return null; + } + + return ( + { + domNodeRef.current = domNode; + renderTemplate(domNode); + }} + /> + ); +}; From 5480c4d0f4a6ecb04a0ba0e55b9370b850a29cf5 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 10 Aug 2021 09:53:40 +0300 Subject: [PATCH 007/104] [Execution context] Add nested context support (#107523) * Add nested context support * remove execution context service on the client side ExecutionContextContaier is not compatible with SerializableState, so I had to fall back to passing context as POJO. With this change, using a service looks like overhead. * update docs * fix test * address comments from Josh * put export back * update docs * remove outdated export * use input.title for unsaved vis Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...-core-public.corestart.executioncontext.md | 13 - .../kibana-plugin-core-public.corestart.md | 1 - ...lic.executioncontextservicestart.create.md | 19 - ...ore-public.executioncontextservicestart.md | 25 - ...in-core-public.httpfetchoptions.context.md | 2 +- ...ana-plugin-core-public.httpfetchoptions.md | 2 +- ...-core-public.iexecutioncontextcontainer.md | 20 - ...lic.iexecutioncontextcontainer.toheader.md | 11 - ...ublic.iexecutioncontextcontainer.tojson.md | 11 - ...blic.kibanaexecutioncontext.description.md | 13 - ...n-core-public.kibanaexecutioncontext.id.md | 13 - ...ugin-core-public.kibanaexecutioncontext.md | 23 +- ...core-public.kibanaexecutioncontext.name.md | 13 - ...core-public.kibanaexecutioncontext.type.md | 13 - ...-core-public.kibanaexecutioncontext.url.md | 13 - .../core/public/kibana-plugin-core-public.md | 4 +- ...n-core-server.executioncontextsetup.get.md | 17 - ...lugin-core-server.executioncontextsetup.md | 3 +- ...n-core-server.executioncontextsetup.set.md | 24 - ...erver.executioncontextsetup.withcontext.md | 25 + ...erver.iexecutioncontextcontainer.tojson.md | 4 +- ...rver.kibanaexecutioncontext.description.md | 13 - ...n-core-server.kibanaexecutioncontext.id.md | 13 - ...ugin-core-server.kibanaexecutioncontext.md | 23 +- ...core-server.kibanaexecutioncontext.name.md | 13 - ...core-server.kibanaexecutioncontext.type.md | 13 - ...-core-server.kibanaexecutioncontext.url.md | 13 - ...ore-server.kibanaserverexecutioncontext.md | 19 - ....kibanaserverexecutioncontext.requestid.md | 11 - .../core/server/kibana-plugin-core-server.md | 3 +- ...ugins-embeddable-public.embeddableinput.md | 1 + ...ic.executioncontext.getexecutioncontext.md | 2 +- ...ins-expressions-public.executioncontext.md | 2 +- ...expressionloaderparams.executioncontext.md | 2 +- ...ressions-public.iexpressionloaderparams.md | 2 +- ...er.executioncontext.getexecutioncontext.md | 2 +- ...ins-expressions-server.executioncontext.md | 2 +- src/core/public/core_system.test.mocks.ts | 9 - src/core/public/core_system.test.ts | 19 - src/core/public/core_system.ts | 7 - .../execution_context_container.test.ts | 71 +++ .../execution_context_service.mock.ts | 37 -- .../execution_context_service.ts | 39 -- src/core/public/execution_context/index.ts | 4 +- src/core/public/http/fetch.test.ts | 14 +- src/core/public/http/fetch.ts | 3 +- src/core/public/http/types.ts | 4 +- src/core/public/index.ts | 5 - src/core/public/mocks.ts | 3 - src/core/public/plugins/plugin_context.ts | 1 - .../public/plugins/plugins_service.test.ts | 2 - src/core/public/public.api.md | 32 +- .../elasticsearch/client/cluster_client.ts | 3 +- .../elasticsearch/client/configure_client.ts | 5 +- .../elasticsearch/elasticsearch_service.ts | 2 +- .../execution_context_container.test.ts | 101 ++-- .../execution_context_container.ts | 27 +- .../execution_context_service.mock.ts | 7 +- .../execution_context_service.test.ts | 446 +++++++++++++++--- .../execution_context_service.ts | 85 ++-- src/core/server/execution_context/index.ts | 1 - .../integration_tests/tracing.test.ts | 138 ++++-- src/core/server/http/http_server.ts | 7 +- src/core/server/index.ts | 6 +- src/core/server/plugins/plugin_context.ts | 4 +- src/core/server/server.api.md | 24 +- src/core/types/execution_context.ts | 14 +- .../embeddable/dashboard_container.tsx | 5 +- .../hooks/use_dashboard_app_state.ts | 7 + .../lib/build_dashboard_container.ts | 4 + .../lib/convert_dashboard_state.ts | 4 + .../application/lib/diff_dashboard_state.ts | 2 +- src/plugins/dashboard/public/types.ts | 6 +- .../data/public/search/expressions/esaggs.ts | 2 +- .../data/server/search/routes/bsearch.ts | 33 +- src/plugins/embeddable/common/types.ts | 4 +- src/plugins/embeddable/public/public.api.md | 2 + src/plugins/embeddable/server/server.api.md | 1 + .../expressions/common/execution/types.ts | 4 +- .../common/service/expressions_services.ts | 4 +- src/plugins/expressions/public/public.api.md | 6 +- src/plugins/expressions/public/types/index.ts | 4 +- src/plugins/expressions/server/server.api.md | 4 +- .../public/request_handler.ts | 4 +- .../public/embeddable/visualize_embeddable.ts | 17 +- .../visualize_embeddable_factory.tsx | 5 +- src/plugins/visualizations/public/mocks.ts | 1 - src/plugins/visualizations/public/plugin.ts | 2 - .../core_plugins/execution_context.ts | 4 +- .../public/embeddable/embeddable.test.tsx | 18 - .../lens/public/embeddable/embeddable.tsx | 11 +- .../public/embeddable/embeddable_factory.ts | 10 +- .../public/embeddable/expression_wrapper.tsx | 4 +- x-pack/plugins/lens/public/plugin.ts | 1 - 94 files changed, 887 insertions(+), 805 deletions(-) delete mode 100644 docs/development/core/public/kibana-plugin-core-public.corestart.executioncontext.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.executioncontextservicestart.create.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.executioncontextservicestart.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.toheader.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.tojson.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.description.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.id.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.name.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.type.md delete mode 100644 docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.url.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.get.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.set.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.withcontext.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.description.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.id.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.name.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.type.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.url.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.kibanaserverexecutioncontext.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.kibanaserverexecutioncontext.requestid.md delete mode 100644 src/core/public/execution_context/execution_context_service.mock.ts delete mode 100644 src/core/public/execution_context/execution_context_service.ts diff --git a/docs/development/core/public/kibana-plugin-core-public.corestart.executioncontext.md b/docs/development/core/public/kibana-plugin-core-public.corestart.executioncontext.md deleted file mode 100644 index 66c5f3efa2d84..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.corestart.executioncontext.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [CoreStart](./kibana-plugin-core-public.corestart.md) > [executionContext](./kibana-plugin-core-public.corestart.executioncontext.md) - -## CoreStart.executionContext property - -[ExecutionContextServiceStart](./kibana-plugin-core-public.executioncontextservicestart.md) - -Signature: - -```typescript -executionContext: ExecutionContextServiceStart; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.corestart.md b/docs/development/core/public/kibana-plugin-core-public.corestart.md index df1929b1f20ab..6ad9adca53ef5 100644 --- a/docs/development/core/public/kibana-plugin-core-public.corestart.md +++ b/docs/development/core/public/kibana-plugin-core-public.corestart.md @@ -20,7 +20,6 @@ export interface CoreStart | [chrome](./kibana-plugin-core-public.corestart.chrome.md) | ChromeStart | [ChromeStart](./kibana-plugin-core-public.chromestart.md) | | [deprecations](./kibana-plugin-core-public.corestart.deprecations.md) | DeprecationsServiceStart | [DeprecationsServiceStart](./kibana-plugin-core-public.deprecationsservicestart.md) | | [docLinks](./kibana-plugin-core-public.corestart.doclinks.md) | DocLinksStart | [DocLinksStart](./kibana-plugin-core-public.doclinksstart.md) | -| [executionContext](./kibana-plugin-core-public.corestart.executioncontext.md) | ExecutionContextServiceStart | [ExecutionContextServiceStart](./kibana-plugin-core-public.executioncontextservicestart.md) | | [fatalErrors](./kibana-plugin-core-public.corestart.fatalerrors.md) | FatalErrorsStart | [FatalErrorsStart](./kibana-plugin-core-public.fatalerrorsstart.md) | | [http](./kibana-plugin-core-public.corestart.http.md) | HttpStart | [HttpStart](./kibana-plugin-core-public.httpstart.md) | | [i18n](./kibana-plugin-core-public.corestart.i18n.md) | I18nStart | [I18nStart](./kibana-plugin-core-public.i18nstart.md) | diff --git a/docs/development/core/public/kibana-plugin-core-public.executioncontextservicestart.create.md b/docs/development/core/public/kibana-plugin-core-public.executioncontextservicestart.create.md deleted file mode 100644 index b36f8ade848e5..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.executioncontextservicestart.create.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ExecutionContextServiceStart](./kibana-plugin-core-public.executioncontextservicestart.md) > [create](./kibana-plugin-core-public.executioncontextservicestart.create.md) - -## ExecutionContextServiceStart.create property - -Creates a context container carrying the meta-data of a runtime operation. Provided meta-data will be propagated to Kibana and Elasticsearch servers. - -```js -const context = executionContext.create(...); -http.fetch('/endpoint/', { context }); - -``` - -Signature: - -```typescript -create: (context: KibanaExecutionContext) => IExecutionContextContainer; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.executioncontextservicestart.md b/docs/development/core/public/kibana-plugin-core-public.executioncontextservicestart.md deleted file mode 100644 index d3eecf601ba9c..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.executioncontextservicestart.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ExecutionContextServiceStart](./kibana-plugin-core-public.executioncontextservicestart.md) - -## ExecutionContextServiceStart interface - - -Signature: - -```typescript -export interface ExecutionContextServiceStart -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [create](./kibana-plugin-core-public.executioncontextservicestart.create.md) | (context: KibanaExecutionContext) => IExecutionContextContainer | Creates a context container carrying the meta-data of a runtime operation. Provided meta-data will be propagated to Kibana and Elasticsearch servers. -```js -const context = executionContext.create(...); -http.fetch('/endpoint/', { context }); - -``` - | - diff --git a/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.context.md b/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.context.md index 6c6ce3171aaeb..09ab95a5135f6 100644 --- a/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.context.md +++ b/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.context.md @@ -7,5 +7,5 @@ Signature: ```typescript -context?: IExecutionContextContainer; +context?: KibanaExecutionContext; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.md b/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.md index 020a941189013..45a48372b4512 100644 --- a/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.httpfetchoptions.md @@ -18,7 +18,7 @@ export interface HttpFetchOptions extends HttpRequestInit | --- | --- | --- | | [asResponse](./kibana-plugin-core-public.httpfetchoptions.asresponse.md) | boolean | When true the return type of [HttpHandler](./kibana-plugin-core-public.httphandler.md) will be an [HttpResponse](./kibana-plugin-core-public.httpresponse.md) with detailed request and response information. When false, the return type will just be the parsed response body. Defaults to false. | | [asSystemRequest](./kibana-plugin-core-public.httpfetchoptions.assystemrequest.md) | boolean | Whether or not the request should include the "system request" header to differentiate an end user request from Kibana internal request. Can be read on the server-side using KibanaRequest\#isSystemRequest. Defaults to false. | -| [context](./kibana-plugin-core-public.httpfetchoptions.context.md) | IExecutionContextContainer | | +| [context](./kibana-plugin-core-public.httpfetchoptions.context.md) | KibanaExecutionContext | | | [headers](./kibana-plugin-core-public.httpfetchoptions.headers.md) | HttpHeadersInit | Headers to send with the request. See [HttpHeadersInit](./kibana-plugin-core-public.httpheadersinit.md). | | [prependBasePath](./kibana-plugin-core-public.httpfetchoptions.prependbasepath.md) | boolean | Whether or not the request should automatically prepend the basePath. Defaults to true. | | [query](./kibana-plugin-core-public.httpfetchoptions.query.md) | HttpFetchQuery | The query string for an HTTP request. See [HttpFetchQuery](./kibana-plugin-core-public.httpfetchquery.md). | diff --git a/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.md b/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.md deleted file mode 100644 index 96ca86cffbc5f..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IExecutionContextContainer](./kibana-plugin-core-public.iexecutioncontextcontainer.md) - -## IExecutionContextContainer interface - - -Signature: - -```typescript -export interface IExecutionContextContainer -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [toHeader](./kibana-plugin-core-public.iexecutioncontextcontainer.toheader.md) | () => Record<string, string> | | -| [toJSON](./kibana-plugin-core-public.iexecutioncontextcontainer.tojson.md) | () => Readonly<KibanaExecutionContext> | | - diff --git a/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.toheader.md b/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.toheader.md deleted file mode 100644 index 03132d24bcca5..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.toheader.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IExecutionContextContainer](./kibana-plugin-core-public.iexecutioncontextcontainer.md) > [toHeader](./kibana-plugin-core-public.iexecutioncontextcontainer.toheader.md) - -## IExecutionContextContainer.toHeader property - -Signature: - -```typescript -toHeader: () => Record; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.tojson.md b/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.tojson.md deleted file mode 100644 index 916148141c8f3..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.iexecutioncontextcontainer.tojson.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [IExecutionContextContainer](./kibana-plugin-core-public.iexecutioncontextcontainer.md) > [toJSON](./kibana-plugin-core-public.iexecutioncontextcontainer.tojson.md) - -## IExecutionContextContainer.toJSON property - -Signature: - -```typescript -toJSON: () => Readonly; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.description.md b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.description.md deleted file mode 100644 index ea8c543c6789e..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.description.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) > [description](./kibana-plugin-core-public.kibanaexecutioncontext.description.md) - -## KibanaExecutionContext.description property - -human readable description. For example, a vis title, action name - -Signature: - -```typescript -readonly description: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.id.md b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.id.md deleted file mode 100644 index d17f9cb8a7ff3..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.id.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) > [id](./kibana-plugin-core-public.kibanaexecutioncontext.id.md) - -## KibanaExecutionContext.id property - -unique value to identify the source - -Signature: - -```typescript -readonly id: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.md b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.md index 06154c814c4e7..8b758715a1975 100644 --- a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.md +++ b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.md @@ -2,22 +2,19 @@ [Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) -## KibanaExecutionContext interface +## KibanaExecutionContext type +Represents a meta-information about a Kibana entity initiating a search request. Signature: ```typescript -export interface KibanaExecutionContext +export declare type KibanaExecutionContext = { + readonly type: string; + readonly name: string; + readonly id: string; + readonly description: string; + readonly url?: string; + parent?: KibanaExecutionContext; +}; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [description](./kibana-plugin-core-public.kibanaexecutioncontext.description.md) | string | human readable description. For example, a vis title, action name | -| [id](./kibana-plugin-core-public.kibanaexecutioncontext.id.md) | string | unique value to identify the source | -| [name](./kibana-plugin-core-public.kibanaexecutioncontext.name.md) | string | public name of a user-facing feature | -| [type](./kibana-plugin-core-public.kibanaexecutioncontext.type.md) | string | Kibana application initated an operation. | -| [url](./kibana-plugin-core-public.kibanaexecutioncontext.url.md) | string | in browser - url to navigate to a current page, on server - endpoint path, for task: task SO url | - diff --git a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.name.md b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.name.md deleted file mode 100644 index 21dde32e21ce7..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.name.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) > [name](./kibana-plugin-core-public.kibanaexecutioncontext.name.md) - -## KibanaExecutionContext.name property - -public name of a user-facing feature - -Signature: - -```typescript -readonly name: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.type.md b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.type.md deleted file mode 100644 index 48009ebaaeaa5..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.type.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) > [type](./kibana-plugin-core-public.kibanaexecutioncontext.type.md) - -## KibanaExecutionContext.type property - -Kibana application initated an operation. - -Signature: - -```typescript -readonly type: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.url.md b/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.url.md deleted file mode 100644 index 47ad7604b473d..0000000000000 --- a/docs/development/core/public/kibana-plugin-core-public.kibanaexecutioncontext.url.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) > [url](./kibana-plugin-core-public.kibanaexecutioncontext.url.md) - -## KibanaExecutionContext.url property - -in browser - url to navigate to a current page, on server - endpoint path, for task: task SO url - -Signature: - -```typescript -readonly url?: string; -``` diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index d743508e046ea..e984fbb675e6d 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -63,7 +63,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [DocLinksStart](./kibana-plugin-core-public.doclinksstart.md) | | | [DomainDeprecationDetails](./kibana-plugin-core-public.domaindeprecationdetails.md) | | | [ErrorToastOptions](./kibana-plugin-core-public.errortoastoptions.md) | Options available for [IToasts](./kibana-plugin-core-public.itoasts.md) error APIs. | -| [ExecutionContextServiceStart](./kibana-plugin-core-public.executioncontextservicestart.md) | | | [FatalErrorInfo](./kibana-plugin-core-public.fatalerrorinfo.md) | Represents the message and stack of a fatal Error | | [FatalErrorsSetup](./kibana-plugin-core-public.fatalerrorssetup.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. | | [HttpFetchOptions](./kibana-plugin-core-public.httpfetchoptions.md) | All options that may be used with a [HttpHandler](./kibana-plugin-core-public.httphandler.md). | @@ -80,14 +79,12 @@ The plugin integrates with the core system via lifecycle events: `setup` | [I18nStart](./kibana-plugin-core-public.i18nstart.md) | I18nStart.Context is required by any localizable React component from @kbn/i18n and @elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. | | [IAnonymousPaths](./kibana-plugin-core-public.ianonymouspaths.md) | APIs for denoting paths as not requiring authentication | | [IBasePath](./kibana-plugin-core-public.ibasepath.md) | APIs for manipulating the basePath on URL segments. | -| [IExecutionContextContainer](./kibana-plugin-core-public.iexecutioncontextcontainer.md) | | | [IExternalUrl](./kibana-plugin-core-public.iexternalurl.md) | APIs for working with external URLs. | | [IExternalUrlPolicy](./kibana-plugin-core-public.iexternalurlpolicy.md) | A policy describing whether access to an external destination is allowed. | | [IHttpFetchError](./kibana-plugin-core-public.ihttpfetcherror.md) | | | [IHttpInterceptController](./kibana-plugin-core-public.ihttpinterceptcontroller.md) | Used to halt a request Promise chain in a [HttpInterceptor](./kibana-plugin-core-public.httpinterceptor.md). | | [IHttpResponseInterceptorOverrides](./kibana-plugin-core-public.ihttpresponseinterceptoroverrides.md) | Properties that can be returned by HttpInterceptor.request to override the response. | | [IUiSettingsClient](./kibana-plugin-core-public.iuisettingsclient.md) | Client-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. [IUiSettingsClient](./kibana-plugin-core-public.iuisettingsclient.md) | -| [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) | | | [NavigateToAppOptions](./kibana-plugin-core-public.navigatetoappoptions.md) | Options for the [navigateToApp API](./kibana-plugin-core-public.applicationstart.navigatetoapp.md) | | [NotificationsSetup](./kibana-plugin-core-public.notificationssetup.md) | | | [NotificationsStart](./kibana-plugin-core-public.notificationsstart.md) | | @@ -162,6 +159,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [FatalErrorsStart](./kibana-plugin-core-public.fatalerrorsstart.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. | | [HttpStart](./kibana-plugin-core-public.httpstart.md) | See [HttpSetup](./kibana-plugin-core-public.httpsetup.md) | | [IToasts](./kibana-plugin-core-public.itoasts.md) | Methods for adding and removing global toast messages. See [ToastsApi](./kibana-plugin-core-public.toastsapi.md). | +| [KibanaExecutionContext](./kibana-plugin-core-public.kibanaexecutioncontext.md) | Represents a meta-information about a Kibana entity initiating a search request. | | [MountPoint](./kibana-plugin-core-public.mountpoint.md) | A function that should mount DOM content inside the provided container element and return a handler to unmount it. | | [NavType](./kibana-plugin-core-public.navtype.md) | | | [PluginInitializer](./kibana-plugin-core-public.plugininitializer.md) | The plugin export at the root of a plugin's public directory should conform to this interface. | diff --git a/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.get.md b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.get.md deleted file mode 100644 index d152b9a0c5df2..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.get.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ExecutionContextSetup](./kibana-plugin-core-server.executioncontextsetup.md) > [get](./kibana-plugin-core-server.executioncontextsetup.get.md) - -## ExecutionContextSetup.get() method - -Retrieves an opearation meta-data for the current async context. - -Signature: - -```typescript -get(): IExecutionContextContainer | undefined; -``` -Returns: - -`IExecutionContextContainer | undefined` - diff --git a/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.md b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.md index 137df77769c8d..24591648ad953 100644 --- a/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.md @@ -15,6 +15,5 @@ export interface ExecutionContextSetup | Method | Description | | --- | --- | -| [get()](./kibana-plugin-core-server.executioncontextsetup.get.md) | Retrieves an opearation meta-data for the current async context. | -| [set(context)](./kibana-plugin-core-server.executioncontextsetup.set.md) | Stores the meta-data of a runtime operation. Data are carried over all async operations automatically. The sequential calls merge provided "context" object shallowly. | +| [withContext(context, fn)](./kibana-plugin-core-server.executioncontextsetup.withcontext.md) | Keeps track of execution context while the passed function is executed. Data are carried over all async operations spawned by the passed function. The nested calls stack the registered context on top of each other. | diff --git a/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.set.md b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.set.md deleted file mode 100644 index 4c8ba4d21b8c4..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.set.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ExecutionContextSetup](./kibana-plugin-core-server.executioncontextsetup.md) > [set](./kibana-plugin-core-server.executioncontextsetup.set.md) - -## ExecutionContextSetup.set() method - -Stores the meta-data of a runtime operation. Data are carried over all async operations automatically. The sequential calls merge provided "context" object shallowly. - -Signature: - -```typescript -set(context: Partial): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| context | Partial<KibanaServerExecutionContext> | | - -Returns: - -`void` - diff --git a/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.withcontext.md b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.withcontext.md new file mode 100644 index 0000000000000..87da071203018 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.executioncontextsetup.withcontext.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ExecutionContextSetup](./kibana-plugin-core-server.executioncontextsetup.md) > [withContext](./kibana-plugin-core-server.executioncontextsetup.withcontext.md) + +## ExecutionContextSetup.withContext() method + +Keeps track of execution context while the passed function is executed. Data are carried over all async operations spawned by the passed function. The nested calls stack the registered context on top of each other. + +Signature: + +```typescript +withContext(context: KibanaExecutionContext | undefined, fn: (...args: any[]) => R): R; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| context | KibanaExecutionContext | undefined | | +| fn | (...args: any[]) => R | | + +Returns: + +`R` + diff --git a/docs/development/core/server/kibana-plugin-core-server.iexecutioncontextcontainer.tojson.md b/docs/development/core/server/kibana-plugin-core-server.iexecutioncontextcontainer.tojson.md index f67aa88862fee..6b643f7f72c95 100644 --- a/docs/development/core/server/kibana-plugin-core-server.iexecutioncontextcontainer.tojson.md +++ b/docs/development/core/server/kibana-plugin-core-server.iexecutioncontextcontainer.tojson.md @@ -7,9 +7,9 @@ Signature: ```typescript -toJSON(): Readonly; +toJSON(): Readonly; ``` Returns: -`Readonly` +`Readonly` diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.description.md b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.description.md deleted file mode 100644 index 00c907b578cf3..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.description.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) > [description](./kibana-plugin-core-server.kibanaexecutioncontext.description.md) - -## KibanaExecutionContext.description property - -human readable description. For example, a vis title, action name - -Signature: - -```typescript -readonly description: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.id.md b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.id.md deleted file mode 100644 index 4ade96691fb14..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.id.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) > [id](./kibana-plugin-core-server.kibanaexecutioncontext.id.md) - -## KibanaExecutionContext.id property - -unique value to identify the source - -Signature: - -```typescript -readonly id: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.md b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.md index c21eb1110ed8e..db06f9b13f9f6 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.md @@ -2,22 +2,19 @@ [Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) -## KibanaExecutionContext interface +## KibanaExecutionContext type +Represents a meta-information about a Kibana entity initiating a search request. Signature: ```typescript -export interface KibanaExecutionContext +export declare type KibanaExecutionContext = { + readonly type: string; + readonly name: string; + readonly id: string; + readonly description: string; + readonly url?: string; + parent?: KibanaExecutionContext; +}; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [description](./kibana-plugin-core-server.kibanaexecutioncontext.description.md) | string | human readable description. For example, a vis title, action name | -| [id](./kibana-plugin-core-server.kibanaexecutioncontext.id.md) | string | unique value to identify the source | -| [name](./kibana-plugin-core-server.kibanaexecutioncontext.name.md) | string | public name of a user-facing feature | -| [type](./kibana-plugin-core-server.kibanaexecutioncontext.type.md) | string | Kibana application initated an operation. | -| [url](./kibana-plugin-core-server.kibanaexecutioncontext.url.md) | string | in browser - url to navigate to a current page, on server - endpoint path, for task: task SO url | - diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.name.md b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.name.md deleted file mode 100644 index 92f58c01bcc11..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.name.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) > [name](./kibana-plugin-core-server.kibanaexecutioncontext.name.md) - -## KibanaExecutionContext.name property - -public name of a user-facing feature - -Signature: - -```typescript -readonly name: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.type.md b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.type.md deleted file mode 100644 index 6941bb150efdd..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.type.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) > [type](./kibana-plugin-core-server.kibanaexecutioncontext.type.md) - -## KibanaExecutionContext.type property - -Kibana application initated an operation. - -Signature: - -```typescript -readonly type: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.url.md b/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.url.md deleted file mode 100644 index dee241cd79398..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaexecutioncontext.url.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) > [url](./kibana-plugin-core-server.kibanaexecutioncontext.url.md) - -## KibanaExecutionContext.url property - -in browser - url to navigate to a current page, on server - endpoint path, for task: task SO url - -Signature: - -```typescript -readonly url?: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaserverexecutioncontext.md b/docs/development/core/server/kibana-plugin-core-server.kibanaserverexecutioncontext.md deleted file mode 100644 index f309e4fd0006c..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaserverexecutioncontext.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaServerExecutionContext](./kibana-plugin-core-server.kibanaserverexecutioncontext.md) - -## KibanaServerExecutionContext interface - - -Signature: - -```typescript -export interface KibanaServerExecutionContext extends Partial -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [requestId](./kibana-plugin-core-server.kibanaserverexecutioncontext.requestid.md) | string | | - diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaserverexecutioncontext.requestid.md b/docs/development/core/server/kibana-plugin-core-server.kibanaserverexecutioncontext.requestid.md deleted file mode 100644 index dff3fd7f2e9ff..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaserverexecutioncontext.requestid.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaServerExecutionContext](./kibana-plugin-core-server.kibanaserverexecutioncontext.md) > [requestId](./kibana-plugin-core-server.kibanaserverexecutioncontext.requestid.md) - -## KibanaServerExecutionContext.requestId property - -Signature: - -```typescript -requestId: string; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index a3f925151af61..c459a48c1ca42 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -110,10 +110,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ISavedObjectsPointInTimeFinder](./kibana-plugin-core-server.isavedobjectspointintimefinder.md) | | | [IScopedClusterClient](./kibana-plugin-core-server.iscopedclusterclient.md) | Serves the same purpose as the normal [cluster client](./kibana-plugin-core-server.iclusterclient.md) but exposes an additional asCurrentUser method that doesn't use credentials of the Kibana internal user (as asInternalUser does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead. | | [IUiSettingsClient](./kibana-plugin-core-server.iuisettingsclient.md) | Server-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. | -| [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) | | | [KibanaRequestEvents](./kibana-plugin-core-server.kibanarequestevents.md) | Request events. | | [KibanaRequestRoute](./kibana-plugin-core-server.kibanarequestroute.md) | Request specific route information exposed to a handler. | -| [KibanaServerExecutionContext](./kibana-plugin-core-server.kibanaserverexecutioncontext.md) | | | [LegacyAPICaller](./kibana-plugin-core-server.legacyapicaller.md) | | | [LegacyCallAPIOptions](./kibana-plugin-core-server.legacycallapioptions.md) | The set of options that defines how API call should be made and result be processed. | | [LegacyElasticsearchError](./kibana-plugin-core-server.legacyelasticsearcherror.md) | @deprecated. The new elasticsearch client doesn't wrap errors anymore. 7.16 | @@ -279,6 +277,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ISavedObjectsImporter](./kibana-plugin-core-server.isavedobjectsimporter.md) | | | [ISavedObjectsRepository](./kibana-plugin-core-server.isavedobjectsrepository.md) | See [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) | | [ISavedObjectTypeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) | See [SavedObjectTypeRegistry](./kibana-plugin-core-server.savedobjecttyperegistry.md) for documentation. | +| [KibanaExecutionContext](./kibana-plugin-core-server.kibanaexecutioncontext.md) | Represents a meta-information about a Kibana entity initiating a search request. | | [KibanaRequestRouteOptions](./kibana-plugin-core-server.kibanarequestrouteoptions.md) | Route options: If 'GET' or 'OPTIONS' method, body options won't be returned. | | [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) | Creates an object containing request response payload, HTTP headers, error details, and other data transmitted to the client. | | [KnownHeaders](./kibana-plugin-core-server.knownheaders.md) | Set of well-known HTTP headers. | diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md index 07ede291e33d2..729cc23dac501 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md @@ -18,5 +18,6 @@ export declare type EmbeddableInput = { disableTriggers?: boolean; searchSessionId?: string; syncColors?: boolean; + executionContext?: KibanaExecutionContext; }; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getexecutioncontext.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getexecutioncontext.md index bfe1925a21f68..bc27adbed1d9a 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getexecutioncontext.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.getexecutioncontext.md @@ -9,5 +9,5 @@ Contains the meta-data about the source of the expression. Signature: ```typescript -getExecutionContext: () => IExecutionContextContainer | undefined; +getExecutionContext: () => KibanaExecutionContext | undefined; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md index 38165a1683316..1fd926f1a0c07 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md @@ -17,7 +17,7 @@ export interface ExecutionContextAbortSignal | Adds ability to abort current execution. | -| [getExecutionContext](./kibana-plugin-plugins-expressions-public.executioncontext.getexecutioncontext.md) | () => IExecutionContextContainer | undefined | Contains the meta-data about the source of the expression. | +| [getExecutionContext](./kibana-plugin-plugins-expressions-public.executioncontext.getexecutioncontext.md) | () => KibanaExecutionContext | undefined | Contains the meta-data about the source of the expression. | | [getKibanaRequest](./kibana-plugin-plugins-expressions-public.executioncontext.getkibanarequest.md) | () => KibanaRequest | Getter to retrieve the KibanaRequest object inside an expression function. Useful for functions which are running on the server and need to perform operations that are scoped to a specific user. | | [getSearchContext](./kibana-plugin-plugins-expressions-public.executioncontext.getsearchcontext.md) | () => ExecutionContextSearch | Get search context of the expression. | | [getSearchSessionId](./kibana-plugin-plugins-expressions-public.executioncontext.getsearchsessionid.md) | () => string | undefined | Search context in which expression should operate. | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.executioncontext.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.executioncontext.md index 504f5e2df7d6e..c133621424b5f 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.executioncontext.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.executioncontext.md @@ -7,5 +7,5 @@ Signature: ```typescript -executionContext?: IExecutionContextContainer; +executionContext?: KibanaExecutionContext; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md index a228628fece0f..69ecd229b5aa6 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md @@ -19,7 +19,7 @@ export interface IExpressionLoaderParams | [customRenderers](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.customrenderers.md) | [] | | | [debug](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.debug.md) | boolean | | | [disableCaching](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.disablecaching.md) | boolean | | -| [executionContext](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.executioncontext.md) | IExecutionContextContainer | | +| [executionContext](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.executioncontext.md) | KibanaExecutionContext | | | [hasCompatibleActions](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.hascompatibleactions.md) | ExpressionRenderHandlerParams['hasCompatibleActions'] | | | [inspectorAdapters](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.inspectoradapters.md) | Adapters | | | [onRenderError](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.onrendererror.md) | RenderErrorHandlerFnType | | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getexecutioncontext.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getexecutioncontext.md index b4ceb8f96d698..b692ee1611f97 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getexecutioncontext.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.getexecutioncontext.md @@ -9,5 +9,5 @@ Contains the meta-data about the source of the expression. Signature: ```typescript -getExecutionContext: () => IExecutionContextContainer | undefined; +getExecutionContext: () => KibanaExecutionContext | undefined; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md index 3b308ca46ab0a..5958853d10903 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md @@ -17,7 +17,7 @@ export interface ExecutionContextAbortSignal | Adds ability to abort current execution. | -| [getExecutionContext](./kibana-plugin-plugins-expressions-server.executioncontext.getexecutioncontext.md) | () => IExecutionContextContainer | undefined | Contains the meta-data about the source of the expression. | +| [getExecutionContext](./kibana-plugin-plugins-expressions-server.executioncontext.getexecutioncontext.md) | () => KibanaExecutionContext | undefined | Contains the meta-data about the source of the expression. | | [getKibanaRequest](./kibana-plugin-plugins-expressions-server.executioncontext.getkibanarequest.md) | () => KibanaRequest | Getter to retrieve the KibanaRequest object inside an expression function. Useful for functions which are running on the server and need to perform operations that are scoped to a specific user. | | [getSearchContext](./kibana-plugin-plugins-expressions-server.executioncontext.getsearchcontext.md) | () => ExecutionContextSearch | Get search context of the expression. | | [getSearchSessionId](./kibana-plugin-plugins-expressions-server.executioncontext.getsearchsessionid.md) | () => string | undefined | Search context in which expression should operate. | diff --git a/src/core/public/core_system.test.mocks.ts b/src/core/public/core_system.test.mocks.ts index c80c2e3f49775..afb8aec31cccd 100644 --- a/src/core/public/core_system.test.mocks.ts +++ b/src/core/public/core_system.test.mocks.ts @@ -19,7 +19,6 @@ import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; import { docLinksServiceMock } from './doc_links/doc_links_service.mock'; import { renderingServiceMock } from './rendering/rendering_service.mock'; import { integrationsServiceMock } from './integrations/integrations_service.mock'; -import { executionContextServiceMock } from './execution_context/execution_context_service.mock'; import { coreAppMock } from './core_app/core_app.mock'; export const MockInjectedMetadataService = injectedMetadataServiceMock.create(); @@ -112,14 +111,6 @@ jest.doMock('./integrations', () => ({ IntegrationsService: IntegrationsServiceConstructor, })); -export const MockExecutionContextService = executionContextServiceMock.create(); -export const ExecutionContextServiceConstructor = jest - .fn() - .mockImplementation(() => MockExecutionContextService); -jest.doMock('./execution_context', () => ({ - ExecutionContextService: ExecutionContextServiceConstructor, -})); - export const MockCoreApp = coreAppMock.create(); export const CoreAppConstructor = jest.fn().mockImplementation(() => MockCoreApp); jest.doMock('./core_app', () => ({ diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index efafb25da27ee..8ead0f50785bd 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -30,7 +30,6 @@ import { RenderingServiceConstructor, IntegrationsServiceConstructor, MockIntegrationsService, - MockExecutionContextService, CoreAppConstructor, MockCoreApp, } from './core_system.test.mocks'; @@ -183,11 +182,6 @@ describe('#setup()', () => { await setupCore(); expect(MockCoreApp.setup).toHaveBeenCalledTimes(1); }); - - it('calls executionContext.setup()', async () => { - await setupCore(); - expect(MockExecutionContextService.setup).toHaveBeenCalledTimes(1); - }); }); describe('#start()', () => { @@ -275,11 +269,6 @@ describe('#start()', () => { await startCore(); expect(MockCoreApp.start).toHaveBeenCalledTimes(1); }); - - it('calls executionContext.start()', async () => { - await startCore(); - expect(MockExecutionContextService.start).toHaveBeenCalledTimes(1); - }); }); describe('#stop()', () => { @@ -338,14 +327,6 @@ describe('#stop()', () => { expect(MockCoreApp.stop).toHaveBeenCalled(); }); - it('calls executionContext.stop()', () => { - const coreSystem = createCoreSystem(); - - expect(MockExecutionContextService.stop).not.toHaveBeenCalled(); - coreSystem.stop(); - expect(MockExecutionContextService.stop).toHaveBeenCalled(); - }); - it('clears the rootDomElement', async () => { const rootDomElement = document.createElement('div'); const coreSystem = createCoreSystem({ diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 43e7d443f5c00..e5dcd8f817a0a 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -29,7 +29,6 @@ import { SavedObjectsService } from './saved_objects'; import { IntegrationsService } from './integrations'; import { DeprecationsService } from './deprecations'; import { CoreApp } from './core_app'; -import { ExecutionContextService } from './execution_context'; import type { InternalApplicationSetup, InternalApplicationStart } from './application/types'; interface Params { @@ -84,7 +83,6 @@ export class CoreSystem { private readonly integrations: IntegrationsService; private readonly coreApp: CoreApp; private readonly deprecations: DeprecationsService; - private readonly executionContext: ExecutionContextService; private readonly rootDomElement: HTMLElement; private readonly coreContext: CoreContext; private fatalErrorsSetup: FatalErrorsSetup | null = null; @@ -120,7 +118,6 @@ export class CoreSystem { this.application = new ApplicationService(); this.integrations = new IntegrationsService(); this.deprecations = new DeprecationsService(); - this.executionContext = new ExecutionContextService(); this.plugins = new PluginsService(this.coreContext, injectedMetadata.uiPlugins); this.coreApp = new CoreApp(this.coreContext); @@ -140,7 +137,6 @@ export class CoreSystem { const http = this.http.setup({ injectedMetadata, fatalErrors: this.fatalErrorsSetup }); const uiSettings = this.uiSettings.setup({ http, injectedMetadata }); const notifications = this.notifications.setup({ uiSettings }); - this.executionContext.setup(); const application = this.application.setup({ http }); this.coreApp.setup({ application, http, injectedMetadata, notifications }); @@ -205,7 +201,6 @@ export class CoreSystem { notifications, }); const deprecations = this.deprecations.start({ http }); - const executionContext = this.executionContext.start(); this.coreApp.start({ application, docLinks, http, notifications, uiSettings }); @@ -222,7 +217,6 @@ export class CoreSystem { uiSettings, fatalErrors, deprecations, - executionContext, }; await this.plugins.start(core); @@ -266,7 +260,6 @@ export class CoreSystem { this.i18n.stop(); this.application.stop(); this.deprecations.stop(); - this.executionContext.stop(); this.rootDomElement.textContent = ''; } } diff --git a/src/core/public/execution_context/execution_context_container.test.ts b/src/core/public/execution_context/execution_context_container.test.ts index a4ee355ab40a4..5e4e34d102e5b 100644 --- a/src/core/public/execution_context/execution_context_container.test.ts +++ b/src/core/public/execution_context/execution_context_container.test.ts @@ -29,6 +29,30 @@ describe('KibanaExecutionContext', () => { `); }); + it('includes a parent context to string representation', () => { + const parentContext: KibanaExecutionContext = { + type: 'parent-type', + name: 'parent-name', + id: '41', + description: 'parent-descripton', + }; + + const context: KibanaExecutionContext = { + type: 'test-type', + name: 'test-name', + id: '42', + description: 'test-descripton', + parent: parentContext, + }; + + const value = new ExecutionContextContainer(context).toHeader(); + expect(value).toMatchInlineSnapshot(` + Object { + "x-kbn-context": "%7B%22type%22%3A%22test-type%22%2C%22name%22%3A%22test-name%22%2C%22id%22%3A%2242%22%2C%22description%22%3A%22test-descripton%22%2C%22parent%22%3A%7B%22type%22%3A%22parent-type%22%2C%22name%22%3A%22parent-name%22%2C%22id%22%3A%2241%22%2C%22description%22%3A%22parent-descripton%22%7D%7D", + } + `); + }); + it('trims a string representation of provided execution context if it is bigger max allowed size', () => { const context: KibanaExecutionContext = { type: 'test-type', @@ -65,4 +89,51 @@ describe('KibanaExecutionContext', () => { `); }); }); + describe('toJSON', () => { + it('returns JSON representation of the context', () => { + const context: KibanaExecutionContext = { + type: 'test-type', + name: 'test-name', + id: '42', + description: 'test-descripton', + }; + + const value = new ExecutionContextContainer(context).toJSON(); + expect(value).toEqual(context); + }); + + it('returns JSON representation when the parent context if provided', () => { + const parentAContext: KibanaExecutionContext = { + type: 'parent-a-type', + name: 'parent-a-name', + id: '40', + description: 'parent-a-descripton', + }; + + const parentBContext: KibanaExecutionContext = { + type: 'parent-b-type', + name: 'parent-b-name', + id: '41', + description: 'parent-b-descripton', + parent: parentAContext, + }; + + const context: KibanaExecutionContext = { + type: 'test-type', + name: 'test-name', + id: '42', + description: 'test-descripton', + parent: parentBContext, + }; + + const value = new ExecutionContextContainer(context).toJSON(); + expect(value).toEqual({ + ...context, + parent: { + ...parentBContext, + parent: parentAContext, + }, + }); + }); + }); }); diff --git a/src/core/public/execution_context/execution_context_service.mock.ts b/src/core/public/execution_context/execution_context_service.mock.ts deleted file mode 100644 index 071e61f17c25c..0000000000000 --- a/src/core/public/execution_context/execution_context_service.mock.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -import type { PublicMethodsOf } from '@kbn/utility-types'; -import type { Plugin } from 'src/core/public'; -import type { ExecutionContextServiceStart } from './execution_context_service'; -import type { ExecutionContextContainer } from './execution_context_container'; - -const createContainerMock = () => { - const mock: jest.Mocked> = { - toHeader: jest.fn(), - toJSON: jest.fn(), - }; - return mock; -}; -const createStartContractMock = () => { - const mock: jest.Mocked = { - create: jest.fn().mockReturnValue(createContainerMock()), - }; - return mock; -}; - -const createMock = (): jest.Mocked => ({ - setup: jest.fn(), - start: jest.fn(), - stop: jest.fn(), -}); - -export const executionContextServiceMock = { - create: createMock, - createStartContract: createStartContractMock, - createContainer: createContainerMock, -}; diff --git a/src/core/public/execution_context/execution_context_service.ts b/src/core/public/execution_context/execution_context_service.ts deleted file mode 100644 index 934e68d15be04..0000000000000 --- a/src/core/public/execution_context/execution_context_service.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -import type { CoreService, KibanaExecutionContext } from '../../types'; -import { - ExecutionContextContainer, - IExecutionContextContainer, -} from './execution_context_container'; - -/** - * @public - */ -export interface ExecutionContextServiceStart { - /** - * Creates a context container carrying the meta-data of a runtime operation. - * Provided meta-data will be propagated to Kibana and Elasticsearch servers. - * ```js - * const context = executionContext.create(...); - * http.fetch('/endpoint/', { context }); - * ``` - */ - create: (context: KibanaExecutionContext) => IExecutionContextContainer; -} - -export class ExecutionContextService implements CoreService { - setup() {} - start(): ExecutionContextServiceStart { - return { - create(context: KibanaExecutionContext) { - return new ExecutionContextContainer(context); - }, - }; - } - stop() {} -} diff --git a/src/core/public/execution_context/index.ts b/src/core/public/execution_context/index.ts index d0c8348d864e7..b15a967ac714a 100644 --- a/src/core/public/execution_context/index.ts +++ b/src/core/public/execution_context/index.ts @@ -7,6 +7,4 @@ */ export type { KibanaExecutionContext } from '../../types'; -export { ExecutionContextService } from './execution_context_service'; -export type { ExecutionContextServiceStart } from './execution_context_service'; -export type { IExecutionContextContainer } from './execution_context_container'; +export { ExecutionContextContainer } from './execution_context_container'; diff --git a/src/core/public/http/fetch.test.ts b/src/core/public/http/fetch.test.ts index 67ec816d08430..7e3cd9019b08d 100644 --- a/src/core/public/http/fetch.test.ts +++ b/src/core/public/http/fetch.test.ts @@ -15,7 +15,6 @@ import { first } from 'rxjs/operators'; import { Fetch } from './fetch'; import { BasePath } from './base_path'; import { HttpResponse, HttpFetchOptionsWithPath } from './types'; -import { executionContextServiceMock } from '../execution_context/execution_context_service.mock'; function delay(duration: number) { return new Promise((r) => setTimeout(r, duration)); @@ -230,14 +229,19 @@ describe('Fetch', () => { it('should inject context headers if provided', async () => { fetchMock.get('*', {}); - const executionContainerMock = executionContextServiceMock.createContainer(); - executionContainerMock.toHeader.mockReturnValueOnce({ 'x-kbn-context': 'value' }); + await fetchInstance.fetch('/my/path', { - context: executionContainerMock, + context: { + type: 'test-type', + name: 'test-name', + description: 'test-description', + id: '42', + }, }); expect(fetchMock.lastOptions()!.headers).toMatchObject({ - 'x-kbn-context': 'value', + 'x-kbn-context': + '%7B%22type%22%3A%22test-type%22%2C%22name%22%3A%22test-name%22%2C%22description%22%3A%22test-description%22%2C%22id%22%3A%2242%22%7D', }); }); diff --git a/src/core/public/http/fetch.ts b/src/core/public/http/fetch.ts index fb178a937e18a..372445b2b0902 100644 --- a/src/core/public/http/fetch.ts +++ b/src/core/public/http/fetch.ts @@ -22,6 +22,7 @@ import { HttpFetchError } from './http_fetch_error'; import { HttpInterceptController } from './http_intercept_controller'; import { interceptRequest, interceptResponse } from './intercept'; import { HttpInterceptHaltError } from './http_intercept_halt_error'; +import { ExecutionContextContainer } from '../execution_context'; interface Params { basePath: IBasePath; @@ -124,7 +125,7 @@ export class Fetch { 'Content-Type': 'application/json', ...options.headers, 'kbn-version': this.params.kibanaVersion, - ...options.context?.toHeader(), + ...(options.context ? new ExecutionContextContainer(options.context).toHeader() : {}), }), }; diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts index ccf68201bc207..3eb718b318f86 100644 --- a/src/core/public/http/types.ts +++ b/src/core/public/http/types.ts @@ -8,7 +8,7 @@ import { Observable } from 'rxjs'; import { MaybePromise } from '@kbn/utility-types'; -import type { IExecutionContextContainer } from '../execution_context'; +import type { KibanaExecutionContext } from '../execution_context'; /** @public */ export interface HttpSetup { @@ -272,7 +272,7 @@ export interface HttpFetchOptions extends HttpRequestInit { */ asResponse?: boolean; - context?: IExecutionContextContainer; + context?: KibanaExecutionContext; } /** diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 40304d27580ca..e6e6433291873 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -65,7 +65,6 @@ import { ApplicationSetup, Capabilities, ApplicationStart } from './application' import { DocLinksStart } from './doc_links'; import { SavedObjectsStart } from './saved_objects'; import { DeprecationsServiceStart } from './deprecations'; -import type { ExecutionContextServiceStart } from './execution_context'; export type { PackageInfo, @@ -186,8 +185,6 @@ export type { export type { DeprecationsServiceStart, ResolveDeprecationResponse } from './deprecations'; -export type { IExecutionContextContainer, ExecutionContextServiceStart } from './execution_context'; - export type { MountPoint, UnmountCallback, PublicUiSettingsParams } from './types'; export { URL_MAX_LENGTH } from './core_app'; @@ -276,8 +273,6 @@ export interface CoreStart { fatalErrors: FatalErrorsStart; /** {@link DeprecationsServiceStart} */ deprecations: DeprecationsServiceStart; - /** {@link ExecutionContextServiceStart} */ - executionContext: ExecutionContextServiceStart; /** * exposed temporarily until https://github.com/elastic/kibana/issues/41990 done * use *only* to retrieve config values. There is no way to set injected values diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index 63b94ea4ac4e3..bd7623beba651 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -25,7 +25,6 @@ import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; import { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; import { injectedMetadataServiceMock } from './injected_metadata/injected_metadata_service.mock'; import { deprecationsServiceMock } from './deprecations/deprecations_service.mock'; -import { executionContextServiceMock } from './execution_context/execution_context_service.mock'; export { chromeServiceMock } from './chrome/chrome_service.mock'; export { docLinksServiceMock } from './doc_links/doc_links_service.mock'; @@ -40,7 +39,6 @@ export { savedObjectsServiceMock } from './saved_objects/saved_objects_service.m export { scopedHistoryMock } from './application/scoped_history.mock'; export { applicationServiceMock } from './application/application_service.mock'; export { deprecationsServiceMock } from './deprecations/deprecations_service.mock'; -export { executionContextServiceMock } from './execution_context/execution_context_service.mock'; function createCoreSetupMock({ basePath = '', @@ -86,7 +84,6 @@ function createCoreStartMock({ basePath = '' } = {}) { getInjectedVar: injectedMetadataServiceMock.createStartContract().getInjectedVar, }, fatalErrors: fatalErrorsServiceMock.createStartContract(), - executionContext: executionContextServiceMock.createStartContract(), }; return mock; diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index be3cff54aca8e..49c895aa80fc4 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -140,6 +140,5 @@ export function createPluginStartContext< }, fatalErrors: deps.fatalErrors, deprecations: deps.deprecations, - executionContext: deps.executionContext, }; } diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index 3f23889c57de6..06c72823c7752 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -35,7 +35,6 @@ import { CoreSetup, CoreStart, PluginInitializerContext } from '..'; import { docLinksServiceMock } from '../doc_links/doc_links_service.mock'; import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; import { deprecationsServiceMock } from '../deprecations/deprecations_service.mock'; -import { executionContextServiceMock } from '../execution_context/execution_context_service.mock'; export let mockPluginInitializers: Map; @@ -105,7 +104,6 @@ describe('PluginsService', () => { savedObjects: savedObjectsServiceMock.createStartContract(), fatalErrors: fatalErrorsServiceMock.createStartContract(), deprecations: deprecationsServiceMock.createStartContract(), - executionContext: executionContextServiceMock.createStartContract(), }; mockStartContext = { ...mockStartDeps, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 80fd3927e05a3..9de10c3c88534 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -433,8 +433,6 @@ export interface CoreStart { // (undocumented) docLinks: DocLinksStart; // (undocumented) - executionContext: ExecutionContextServiceStart; - // (undocumented) fatalErrors: FatalErrorsStart; // (undocumented) http: HttpStart; @@ -717,11 +715,6 @@ export interface ErrorToastOptions extends ToastOptions { toastMessage?: string; } -// @public (undocumented) -export interface ExecutionContextServiceStart { - create: (context: KibanaExecutionContext) => IExecutionContextContainer; -} - // @public export interface FatalErrorInfo { // (undocumented) @@ -761,7 +754,7 @@ export interface HttpFetchOptions extends HttpRequestInit { asResponse?: boolean; asSystemRequest?: boolean; // (undocumented) - context?: IExecutionContextContainer; + context?: KibanaExecutionContext; headers?: HttpHeadersInit; prependBasePath?: boolean; query?: HttpFetchQuery; @@ -897,14 +890,6 @@ export interface IBasePath { readonly serverBasePath: string; } -// @public (undocumented) -export interface IExecutionContextContainer { - // (undocumented) - toHeader: () => Record; - // (undocumented) - toJSON: () => Readonly; -} - // @public export interface IExternalUrl { validateUrl(relativeOrAbsoluteUrl: string): URL | null; @@ -967,14 +952,15 @@ export interface IUiSettingsClient { set: (key: string, value: any) => Promise; } -// @public (undocumented) -export interface KibanaExecutionContext { - readonly description: string; - readonly id: string; - readonly name: string; +// @public +export type KibanaExecutionContext = { readonly type: string; + readonly name: string; + readonly id: string; + readonly description: string; readonly url?: string; -} + parent?: KibanaExecutionContext; +}; // @public export type MountPoint = (element: T) => UnmountCallback; @@ -1692,6 +1678,6 @@ export interface UserProvidedValues { // Warnings were encountered during analysis: // -// src/core/public/core_system.ts:172:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts +// src/core/public/core_system.ts:168:21 - (ae-forgotten-export) The symbol "InternalApplicationStart" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/server/elasticsearch/client/cluster_client.ts b/src/core/server/elasticsearch/client/cluster_client.ts index d164736cead07..f81b651843013 100644 --- a/src/core/server/elasticsearch/client/cluster_client.ts +++ b/src/core/server/elasticsearch/client/cluster_client.ts @@ -10,7 +10,6 @@ import { Client } from '@elastic/elasticsearch'; import { Logger } from '../../logging'; import { GetAuthHeaders, Headers, isKibanaRequest, isRealRequest } from '../../http'; import { ensureRawRequest, filterHeaders } from '../../http/router'; -import type { IExecutionContextContainer } from '../../execution_context'; import { ScopeableRequest } from '../types'; import { ElasticsearchClient } from './types'; import { configureClient } from './configure_client'; @@ -64,7 +63,7 @@ export class ClusterClient implements ICustomClusterClient { logger: Logger, type: string, private readonly getAuthHeaders: GetAuthHeaders = noop, - getExecutionContext: () => IExecutionContextContainer | undefined = noop + getExecutionContext: () => string | undefined = noop ) { this.asInternalUser = configureClient(config, { logger, type, getExecutionContext }); this.rootScopedClient = configureClient(config, { diff --git a/src/core/server/elasticsearch/client/configure_client.ts b/src/core/server/elasticsearch/client/configure_client.ts index f2953862c25e0..3c32dd2cfd4f4 100644 --- a/src/core/server/elasticsearch/client/configure_client.ts +++ b/src/core/server/elasticsearch/client/configure_client.ts @@ -14,7 +14,6 @@ import type { TransportRequestParams, TransportRequestOptions, } from '@elastic/elasticsearch/lib/Transport'; -import type { IExecutionContextContainer } from '../../execution_context'; import { Logger } from '../../logging'; import { parseClientOptions, ElasticsearchClientConfig } from './client_config'; @@ -31,14 +30,14 @@ export const configureClient = ( logger: Logger; type: string; scoped?: boolean; - getExecutionContext?: () => IExecutionContextContainer | undefined; + getExecutionContext?: () => string | undefined; } ): Client => { const clientOptions = parseClientOptions(config, scoped); class KibanaTransport extends Transport { request(params: TransportRequestParams, options?: TransportRequestOptions) { const opts = options || {}; - const opaqueId = getExecutionContext()?.toString(); + const opaqueId = getExecutionContext(); if (opaqueId && !opts.opaqueId) { // rewrites headers['x-opaque-id'] if it presents opts.opaqueId = opaqueId; diff --git a/src/core/server/elasticsearch/elasticsearch_service.ts b/src/core/server/elasticsearch/elasticsearch_service.ts index f983a8b77fe08..acd2204334c0e 100644 --- a/src/core/server/elasticsearch/elasticsearch_service.ts +++ b/src/core/server/elasticsearch/elasticsearch_service.ts @@ -149,7 +149,7 @@ export class ElasticsearchService this.coreContext.logger.get('elasticsearch'), type, this.getAuthHeaders, - () => this.executionContextClient?.get() + () => this.executionContextClient?.getAsHeader() ); } diff --git a/src/core/server/execution_context/execution_context_container.test.ts b/src/core/server/execution_context/execution_context_container.test.ts index e8408d37f40f4..41095815a6b44 100644 --- a/src/core/server/execution_context/execution_context_container.test.ts +++ b/src/core/server/execution_context/execution_context_container.test.ts @@ -5,7 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import type { KibanaServerExecutionContext } from './execution_context_service'; +import type { KibanaExecutionContext } from '../../types'; + import { ExecutionContextContainer, getParentContextFrom, @@ -14,52 +15,80 @@ import { } from './execution_context_container'; describe('KibanaExecutionContext', () => { + describe('constructor', () => { + it('allows context to define parent explicitly', () => { + const parentContext: KibanaExecutionContext = { + type: 'parent-type', + name: 'parent-name', + id: '44', + description: 'parent-descripton', + }; + const parentContainer = new ExecutionContextContainer(parentContext); + + const context: KibanaExecutionContext = { + type: 'test-type', + name: 'test-name', + id: '42', + description: 'test-descripton', + parent: { + type: 'custom-parent-type', + name: 'custom-parent-name', + id: '41', + description: 'custom-parent-descripton', + }, + }; + + const value = new ExecutionContextContainer(context, parentContainer).toJSON(); + expect(value).toEqual(context); + }); + }); + describe('toString', () => { it('returns a string representation of provided execution context', () => { - const context: KibanaServerExecutionContext = { + const context: KibanaExecutionContext = { type: 'test-type', name: 'test-name', id: '42', description: 'test-descripton', - requestId: '1234-5678', }; const value = new ExecutionContextContainer(context).toString(); - expect(value).toMatchInlineSnapshot(`"1234-5678;kibana:test-type:test-name:42"`); + expect(value).toBe('test-type:test-name:42'); }); - it('returns a limited representation if optional properties are omitted', () => { - const context: KibanaServerExecutionContext = { - requestId: '1234-5678', + it('includes a parent context to string representation', () => { + const parentContext: KibanaExecutionContext = { + type: 'parent-type', + name: 'parent-name', + id: '41', + description: 'parent-descripton', }; + const parentContainer = new ExecutionContextContainer(parentContext); - const value = new ExecutionContextContainer(context).toString(); - expect(value).toMatchInlineSnapshot(`"1234-5678"`); + const context: KibanaExecutionContext = { + type: 'test-type', + name: 'test-name', + id: '42', + description: 'test-descripton', + }; + + const value = new ExecutionContextContainer(context, parentContainer).toString(); + expect(value).toBe('parent-type:parent-name:41;test-type:test-name:42'); }); it('returns an escaped string representation of provided execution contextStringified', () => { - const context: KibanaServerExecutionContext = { + const context: KibanaExecutionContext = { id: 'Visualization☺漢字', type: 'test-type', name: 'test-name', - requestId: '1234-5678', + description: 'test-description', }; const value = new ExecutionContextContainer(context).toString(); - expect(value).toMatchInlineSnapshot( - `"1234-5678;kibana:test-type:test-name:Visualization%E2%98%BA%E6%BC%A2%E5%AD%97"` - ); + expect(value).toBe('test-type:test-name:Visualization%E2%98%BA%E6%BC%A2%E5%AD%97'); }); it('trims a string representation of provided execution context if it is bigger max allowed size', () => { - expect( - new Blob([ - new ExecutionContextContainer({ - requestId: '1234-5678'.repeat(1000), - }).toString(), - ]).size - ).toBeLessThanOrEqual(BAGGAGE_MAX_PER_NAME_VALUE_PAIRS); - expect( new Blob([ new ExecutionContextContainer({ @@ -67,7 +96,6 @@ describe('KibanaExecutionContext', () => { name: 'test-name', id: '42'.repeat(1000), description: 'test-descripton', - requestId: '1234-5678', }).toString(), ]).size ).toBeLessThanOrEqual(BAGGAGE_MAX_PER_NAME_VALUE_PAIRS); @@ -76,16 +104,35 @@ describe('KibanaExecutionContext', () => { describe('toJSON', () => { it('returns a context object', () => { - const context: KibanaServerExecutionContext = { + const context: KibanaExecutionContext = { type: 'test-type', name: 'test-name', id: '42', description: 'test-descripton', - requestId: '1234-5678', }; const value = new ExecutionContextContainer(context).toJSON(); - expect(value).toBe(context); + expect(value).toEqual(context); + }); + + it('returns a context object with registed parent object', () => { + const parentContext: KibanaExecutionContext = { + type: 'parent-type', + name: 'parent-name', + id: '41', + description: 'parent-descripton', + }; + const parentContainer = new ExecutionContextContainer(parentContext); + + const context: KibanaExecutionContext = { + type: 'test-type', + name: 'test-name', + id: '42', + description: 'test-descripton', + }; + + const value = new ExecutionContextContainer(context, parentContainer).toJSON(); + expect(value).toEqual({ ...context, parent: parentContext }); }); }); }); @@ -97,7 +144,7 @@ describe('getParentContextFrom', () => { expect(getParentContextFrom({ [BAGGAGE_HEADER]: header })).toEqual(ctx); }); - it('does not throw an exception if given not a valid value', () => { + it('does not throw an exception if given not a valid JSON object', () => { expect(getParentContextFrom({ [BAGGAGE_HEADER]: 'value' })).toBeUndefined(); expect(getParentContextFrom({ [BAGGAGE_HEADER]: '' })).toBeUndefined(); expect(getParentContextFrom({})).toBeUndefined(); diff --git a/src/core/server/execution_context/execution_context_container.ts b/src/core/server/execution_context/execution_context_container.ts index 6c4a5606df152..a81c409ab3e9e 100644 --- a/src/core/server/execution_context/execution_context_container.ts +++ b/src/core/server/execution_context/execution_context_container.ts @@ -5,7 +5,6 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import type { KibanaServerExecutionContext } from './execution_context_service'; import type { KibanaExecutionContext } from '../../types'; // Switch to the standard Baggage header. blocked by @@ -47,27 +46,23 @@ function enforceMaxLength(header: string): string { */ export interface IExecutionContextContainer { toString(): string; - toJSON(): Readonly; + toJSON(): Readonly; +} + +function stringify(ctx: KibanaExecutionContext): string { + const stringifiedCtx = `${ctx.type}:${ctx.name}:${encodeURIComponent(ctx.id)}`; + return ctx.parent ? `${stringify(ctx.parent)};${stringifiedCtx}` : stringifiedCtx; } export class ExecutionContextContainer implements IExecutionContextContainer { - readonly #context: Readonly; - constructor(context: Readonly) { - this.#context = context; + readonly #context: Readonly; + constructor(context: KibanaExecutionContext, parent?: IExecutionContextContainer) { + this.#context = { parent: parent?.toJSON(), ...context }; } toString(): string { - const ctx = this.#context; - const contextStringified = - ctx.type && ctx.id && ctx.name - ? // id may contain non-ASCII symbols - `kibana:${encodeURIComponent(ctx.type)}:${encodeURIComponent( - ctx.name - )}:${encodeURIComponent(ctx.id)}` - : ''; - const result = contextStringified ? `${ctx.requestId};${contextStringified}` : ctx.requestId; - return enforceMaxLength(result); + return enforceMaxLength(stringify(this.#context)); } - toJSON(): Readonly { + toJSON() { return this.#context; } } diff --git a/src/core/server/execution_context/execution_context_service.mock.ts b/src/core/server/execution_context/execution_context_service.mock.ts index 657805df273ca..2e31145f6c0ee 100644 --- a/src/core/server/execution_context/execution_context_service.mock.ts +++ b/src/core/server/execution_context/execution_context_service.mock.ts @@ -15,9 +15,11 @@ import type { const createExecutionContextMock = () => { const mock: jest.Mocked = { set: jest.fn(), - reset: jest.fn(), + setRequestId: jest.fn(), + withContext: jest.fn(), get: jest.fn(), getParentContextFrom: jest.fn(), + getAsHeader: jest.fn(), }; return mock; }; @@ -28,8 +30,7 @@ const createInternalSetupContractMock = () => { const createSetupContractMock = () => { const mock: jest.Mocked = { - set: jest.fn(), - get: jest.fn(), + withContext: jest.fn(), }; return mock; }; diff --git a/src/core/server/execution_context/execution_context_service.test.ts b/src/core/server/execution_context/execution_context_service.test.ts index 0c213429e1951..3abaa13d11103 100644 --- a/src/core/server/execution_context/execution_context_service.test.ts +++ b/src/core/server/execution_context/execution_context_service.test.ts @@ -25,97 +25,400 @@ describe('ExecutionContextService', () => { service = new ExecutionContextService(core).setup(); }); - it('sets and gets a value in async context', async () => { - const chainA = Promise.resolve().then(async () => { - service.set({ - requestId: '0000', + describe('set', () => { + it('sets and gets a value in async context', async () => { + const chainA = Promise.resolve().then(async () => { + service.set({ + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + }); + await delay(500); + return service.get(); + }); + + const chainB = Promise.resolve().then(async () => { + service.set({ + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + }); + await delay(100); + return service.get(); + }); + + expect( + await Promise.all([chainA, chainB]).then((results) => + results.map((result) => result?.toJSON()) + ) + ).toEqual([ + { + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + parent: undefined, + }, + + { + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + parent: undefined, + }, + ]); + }); + + it('a sequentual call rewrites the context', async () => { + const result = await Promise.resolve().then(async () => { + service.set({ + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + }); + service.set({ + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + }); + + return service.get(); + }); + + expect(result?.toJSON()).toEqual({ + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + parent: undefined, }); - await delay(500); - return service.get(); }); - const chainB = Promise.resolve().then(async () => { + it('emits context to the logs when "set" is called', async () => { service.set({ - requestId: '1111', + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', }); - await delay(100); - return service.get(); - }); - - expect( - await Promise.all([chainA, chainB]).then((results) => - results.map((result) => result?.toJSON()) - ) - ).toEqual([ - { - requestId: '0000', - }, - { - requestId: '1111', - }, - ]); + expect(loggingSystemMock.collect(core.logger).debug).toMatchInlineSnapshot(` + Array [ + Array [ + "set the execution context: {\\"type\\":\\"type-a\\",\\"name\\":\\"name-a\\",\\"id\\":\\"id-a\\",\\"description\\":\\"description-a\\"}", + ], + ] + `); + }); + + it('can be disabled', async () => { + const coreWithDisabledService = mockCoreContext.create(); + coreWithDisabledService.configService.atPath.mockReturnValue( + new BehaviorSubject({ enabled: false }) + ); + const disabledService = new ExecutionContextService(coreWithDisabledService).setup(); + const chainA = await Promise.resolve().then(async () => { + disabledService.set({ + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + }); + await delay(100); + return disabledService.get(); + }); + + expect(chainA).toBeUndefined(); + }); }); - it('sets and resets a value in async context', async () => { - const chainA = Promise.resolve().then(async () => { - service.set({ - requestId: '0000', + describe('withContext', () => { + it('sets and gets a value in async context', async () => { + const chainA = service.withContext( + { + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + }, + async () => { + await delay(10); + return service.get(); + } + ); + + const chainB = service.withContext( + { + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + }, + async () => { + await delay(50); + return service.get(); + } + ); + + expect( + await Promise.all([chainA, chainB]).then((results) => + results.map((result) => result?.toJSON()) + ) + ).toEqual([ + { + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + parent: undefined, + }, + { + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + parent: undefined, + }, + ]); + }); + + it('sets the context for a wrapped function only', () => { + service.withContext( + { + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + }, + async () => { + await delay(10); + return service.get(); + } + ); + + expect(service.get()).toBe(undefined); + }); + + it('a sequentual call does not affect orhers contexts', async () => { + const chainA = service.withContext( + { + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + }, + async () => { + await delay(50); + return service.get(); + } + ); + + const chainB = service.withContext( + { + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + }, + async () => { + await delay(10); + return service.get(); + } + ); + const result = await Promise.all([chainA, chainB]); + expect(result.map((r) => r?.toJSON())).toEqual([ + { + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + parent: undefined, + }, + { + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + parent: undefined, + }, + ]); + }); + + it('supports nested contexts', async () => { + const result = await service.withContext( + { + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + }, + async () => { + await delay(10); + return service.withContext( + { + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + }, + () => service.get() + ); + } + ); + + expect(result?.toJSON()).toEqual({ + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + parent: { + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + parent: undefined, + }, }); - await delay(500); - service.reset(); - return service.get(); }); - const chainB = Promise.resolve().then(async () => { + it('inherits a nested context configured by "set"', async () => { service.set({ - requestId: '1111', + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', }); - await delay(100); - return service.get(); - }); - - expect( - await Promise.all([chainA, chainB]).then((results) => - results.map((result) => result?.toJSON()) - ) - ).toEqual([ - undefined, - { - requestId: '1111', - }, - ]); - }); + const result = await service.withContext( + { + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + }, + async () => { + await delay(10); + return service.get(); + } + ); - it('emits context to the logs when "set" is called', async () => { - service.set({ - requestId: '0000', + expect(result?.toJSON()).toEqual({ + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + parent: { + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + parent: undefined, + }, + }); + }); + + it('do not swallow errors', () => { + const error = new Error('oops'); + const promise = service.withContext( + { + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + }, + async () => { + await delay(10); + throw error; + } + ); + + expect(promise).rejects.toBe(error); + }); + + it('emits context to the logs when "withContext" is called', async () => { + service.withContext( + { + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + }, + (i) => i + ); + expect(loggingSystemMock.collect(core.logger).debug).toMatchInlineSnapshot(` + Array [ + Array [ + "stored the execution context: {\\"type\\":\\"type-a\\",\\"name\\":\\"name-a\\",\\"id\\":\\"id-a\\",\\"description\\":\\"description-a\\"}", + ], + ] + `); + }); + + it('can be disabled', async () => { + const coreWithDisabledService = mockCoreContext.create(); + coreWithDisabledService.configService.atPath.mockReturnValue( + new BehaviorSubject({ enabled: false }) + ); + const disabledService = new ExecutionContextService(coreWithDisabledService).setup(); + const result = await disabledService.withContext( + { + type: 'type-b', + name: 'name-b', + id: 'id-b', + description: 'description-b', + }, + async () => { + await delay(10); + return service.get(); + } + ); + + expect(result).toBeUndefined(); }); - expect(loggingSystemMock.collect(core.logger).debug).toMatchInlineSnapshot(` - Array [ - Array [ - "stored the execution context: {\\"requestId\\":\\"0000\\"}", - ], - ] - `); }); - }); - describe('config', () => { - it('can be disabled', async () => { - const core = mockCoreContext.create(); - core.configService.atPath.mockReturnValue(new BehaviorSubject({ enabled: false })); - const service = new ExecutionContextService(core).setup(); - const chainA = await Promise.resolve().then(async () => { + describe('getAsHeader', () => { + it('returns request id if no context provided', async () => { + service.setRequestId('1234'); + + expect(service.getAsHeader()).toBe('1234'); + }); + + it('returns request id and registered context', async () => { + service.setRequestId('1234'); service.set({ - requestId: '0000', + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', }); - await delay(100); - return service.get(); + + expect(service.getAsHeader()).toBe('1234;kibana:type-a:name-a:id-a'); }); - expect(chainA).toBeUndefined(); + it('can be disabled', async () => { + const coreWithDisabledService = mockCoreContext.create(); + coreWithDisabledService.configService.atPath.mockReturnValue( + new BehaviorSubject({ enabled: false }) + ); + const disabledService = new ExecutionContextService(coreWithDisabledService).setup(); + disabledService.setRequestId('1234'); + disabledService.set({ + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', + }); + + expect(disabledService.getAsHeader()).toBeUndefined(); + }); }); + }); + describe('config', () => { it('reacts to config changes', async () => { const core = mockCoreContext.create(); const config$ = new BehaviorSubject({ enabled: false }); @@ -124,7 +427,10 @@ describe('ExecutionContextService', () => { function exec() { return Promise.resolve().then(async () => { service.set({ - requestId: '0000', + type: 'type-a', + name: 'name-a', + id: 'id-a', + description: 'description-a', }); await delay(100); return service.get(); diff --git a/src/core/server/execution_context/execution_context_service.ts b/src/core/server/execution_context/execution_context_service.ts index b187283e27e34..5a8d104cebcd9 100644 --- a/src/core/server/execution_context/execution_context_service.ts +++ b/src/core/server/execution_context/execution_context_service.ts @@ -19,21 +19,26 @@ import { getParentContextFrom, } from './execution_context_container'; -/** - * @public - */ -export interface KibanaServerExecutionContext extends Partial { - requestId: string; -} - /** * @internal */ export interface IExecutionContext { getParentContextFrom(headers: Record): KibanaExecutionContext | undefined; - set(context: Partial): void; - reset(): void; + setRequestId(requestId: string): void; + set(context: KibanaExecutionContext): void; + /** + * The sole purpose of this imperative internal API is to be used by the http service. + * The event-based nature of Hapi server doesn't allow us to wrap a request handler with "withContext". + * Since all the Hapi event lifecycle will lose the execution context. + * Nodejs docs also recommend using AsyncLocalStorage.run() over AsyncLocalStorage.enterWith(). + * https://nodejs.org/api/async_context.html#async_context_asynclocalstorage_enterwith_store + */ get(): IExecutionContextContainer | undefined; + withContext(context: KibanaExecutionContext | undefined, fn: (...args: any[]) => R): R; + /** + * returns serialized representation to send as a header + **/ + getAsHeader(): string | undefined; } /** @@ -51,15 +56,11 @@ export type InternalExecutionContextStart = IExecutionContext; */ export interface ExecutionContextSetup { /** - * Stores the meta-data of a runtime operation. - * Data are carried over all async operations automatically. - * The sequential calls merge provided "context" object shallowly. - **/ - set(context: Partial): void; - /** - * Retrieves an opearation meta-data for the current async context. + * Keeps track of execution context while the passed function is executed. + * Data are carried over all async operations spawned by the passed function. + * The nested calls stack the registered context on top of each other. **/ - get(): IExecutionContextContainer | undefined; + withContext(context: KibanaExecutionContext | undefined, fn: (...args: any[]) => R): R; } /** @@ -70,13 +71,15 @@ export type ExecutionContextStart = ExecutionContextSetup; export class ExecutionContextService implements CoreService { private readonly log: Logger; - private readonly asyncLocalStorage: AsyncLocalStorage; + private readonly contextStore: AsyncLocalStorage; + private readonly requestIdStore: AsyncLocalStorage<{ requestId: string }>; private enabled = false; private configSubscription?: Subscription; constructor(private readonly coreContext: CoreContext) { this.log = coreContext.logger.get('execution_context'); - this.asyncLocalStorage = new AsyncLocalStorage(); + this.contextStore = new AsyncLocalStorage(); + this.requestIdStore = new AsyncLocalStorage<{ requestId: string }>(); } setup(): InternalExecutionContextSetup { @@ -89,8 +92,10 @@ export class ExecutionContextService return { getParentContextFrom, set: this.set.bind(this), - reset: this.reset.bind(this), + withContext: this.withContext.bind(this), + setRequestId: this.setRequestId.bind(this), get: this.get.bind(this), + getAsHeader: this.getAsHeader.bind(this), }; } @@ -98,8 +103,10 @@ export class ExecutionContextService return { getParentContextFrom, set: this.set.bind(this), - reset: this.reset.bind(this), + setRequestId: this.setRequestId.bind(this), + withContext: this.withContext.bind(this), get: this.get.bind(this), + getAsHeader: this.getAsHeader.bind(this), }; } @@ -111,25 +118,43 @@ export class ExecutionContextService } } - private set(context: KibanaServerExecutionContext) { + private set(context: KibanaExecutionContext) { if (!this.enabled) return; - const prevValue = this.asyncLocalStorage.getStore(); - // merges context objects shallowly. repeats the deafult logic of apm.setCustomContext(ctx) - const contextContainer = new ExecutionContextContainer({ ...prevValue?.toJSON(), ...context }); + const contextContainer = new ExecutionContextContainer(context); // we have to use enterWith since Hapi lifecycle model is built on event emitters. // therefore if we wrapped request handler in asyncLocalStorage.run(), we would lose context in other lifecycles. - this.asyncLocalStorage.enterWith(contextContainer); + this.contextStore.enterWith(contextContainer); + this.log.debug(`set the execution context: ${JSON.stringify(contextContainer)}`); + } + + private withContext( + context: KibanaExecutionContext | undefined, + fn: (...args: any[]) => R + ): R { + if (!this.enabled || !context) { + return fn(); + } + const parent = this.contextStore.getStore(); + const contextContainer = new ExecutionContextContainer(context, parent); this.log.debug(`stored the execution context: ${JSON.stringify(contextContainer)}`); + + return this.contextStore.run(contextContainer, fn); } - private reset() { + private setRequestId(requestId: string) { if (!this.enabled) return; - // @ts-expect-error "undefined" is not supported in type definitions, which is wrong - this.asyncLocalStorage.enterWith(undefined); + this.requestIdStore.enterWith({ requestId }); } private get(): IExecutionContextContainer | undefined { if (!this.enabled) return; - return this.asyncLocalStorage.getStore(); + return this.contextStore.getStore(); + } + + private getAsHeader(): string | undefined { + if (!this.enabled) return; + const stringifiedCtx = this.contextStore.getStore()?.toString(); + const requestId = this.requestIdStore.getStore()?.requestId; + return stringifiedCtx ? `${requestId};kibana:${stringifiedCtx}` : requestId; } } diff --git a/src/core/server/execution_context/index.ts b/src/core/server/execution_context/index.ts index f8018c75995e7..d34eed5cc34f1 100644 --- a/src/core/server/execution_context/index.ts +++ b/src/core/server/execution_context/index.ts @@ -14,7 +14,6 @@ export type { ExecutionContextSetup, ExecutionContextStart, IExecutionContext, - KibanaServerExecutionContext, } from './execution_context_service'; export type { IExecutionContextContainer } from './execution_context_container'; export { config } from './execution_context_config'; diff --git a/src/core/server/execution_context/integration_tests/tracing.test.ts b/src/core/server/execution_context/integration_tests/tracing.test.ts index ade67d0dd2605..5451ee222d776 100644 --- a/src/core/server/execution_context/integration_tests/tracing.test.ts +++ b/src/core/server/execution_context/integration_tests/tracing.test.ts @@ -205,7 +205,10 @@ describe('trace', () => { executionContext.set(parentContext); const { headers } = await context.core.elasticsearch.client.asCurrentUser.ping(); return res.ok({ - body: { context: executionContext.get()?.toJSON(), header: headers?.['x-opaque-id'] }, + body: { + context: executionContext.get()?.toJSON(), + header: headers?.['x-opaque-id'], + }, }); }); @@ -237,7 +240,7 @@ describe('trace', () => { await root.start(); const response = await kbnTestServer.request.get(root, '/execution-context').expect(200); - expect(response.body).toEqual({ ...parentContext, requestId: expect.any(String) }); + expect(response.body).toEqual(parentContext); }); it('sets execution context for an async request handler', async () => { @@ -253,7 +256,7 @@ describe('trace', () => { await root.start(); const response = await kbnTestServer.request.get(root, '/execution-context').expect(200); - expect(response.body).toEqual({ ...parentContext, requestId: expect.any(String) }); + expect(response.body).toEqual(parentContext); }); it('execution context is uniq for sequential requests', async () => { @@ -261,8 +264,9 @@ describe('trace', () => { const { createRouter } = http; const router = createRouter(''); + let id = 42; router.get({ path: '/execution-context', validate: false }, async (context, req, res) => { - executionContext.set(parentContext); + executionContext.set({ ...parentContext, id: String(id++) }); await delay(100); return res.ok({ body: executionContext.get() }); }); @@ -271,9 +275,8 @@ describe('trace', () => { const responseA = await kbnTestServer.request.get(root, '/execution-context').expect(200); const responseB = await kbnTestServer.request.get(root, '/execution-context').expect(200); - expect(responseA.body).toEqual({ ...parentContext, requestId: expect.any(String) }); - expect(responseB.body).toEqual({ ...parentContext, requestId: expect.any(String) }); - expect(responseA.body.requestId).not.toBe(responseB.body.requestId); + expect(responseA.body).toEqual({ ...parentContext, id: '42' }); + expect(responseB.body).toEqual({ ...parentContext, id: '43' }); }); it('execution context is uniq for concurrent requests', async () => { @@ -283,7 +286,7 @@ describe('trace', () => { const router = createRouter(''); let id = 2; router.get({ path: '/execution-context', validate: false }, async (context, req, res) => { - executionContext.set(parentContext); + executionContext.set({ ...parentContext, id: String(id) }); await delay(id-- * 100); return res.ok({ body: executionContext.get() }); }); @@ -298,13 +301,10 @@ describe('trace', () => { responseB, responseC, ]); - expect(bodyA.requestId).toBeDefined(); - expect(bodyB.requestId).toBeDefined(); - expect(bodyC.requestId).toBeDefined(); - expect(bodyA.requestId).not.toBe(bodyB.requestId); - expect(bodyB.requestId).not.toBe(bodyC.requestId); - expect(bodyA.requestId).not.toBe(bodyC.requestId); + expect(bodyA.id).toBe('2'); + expect(bodyB.id).toBe('1'); + expect(bodyC.id).toBe('0'); }); it('execution context is uniq for concurrent requests when "x-opaque-id" provided', async () => { @@ -316,7 +316,8 @@ describe('trace', () => { router.get({ path: '/execution-context', validate: false }, async (context, req, res) => { executionContext.set(parentContext); await delay(id-- * 100); - return res.ok({ body: executionContext.get() }); + const { headers } = await context.core.elasticsearch.client.asCurrentUser.ping(); + return res.ok({ body: headers || {} }); }); await root.start(); @@ -335,9 +336,9 @@ describe('trace', () => { responseB, responseC, ]); - expect(bodyA.requestId).toBe('req-1'); - expect(bodyB.requestId).toBe('req-2'); - expect(bodyC.requestId).toBe('req-3'); + expect(bodyA['x-opaque-id']).toContain('req-1'); + expect(bodyB['x-opaque-id']).toContain('req-2'); + expect(bodyC['x-opaque-id']).toContain('req-3'); }); it('parses the parent context if present', async () => { @@ -355,7 +356,7 @@ describe('trace', () => { .set(new ExecutionContextContainer(parentContext).toHeader()) .expect(200); - expect(response.body).toEqual({ ...parentContext, requestId: expect.any(String) }); + expect(response.body).toEqual(parentContext); }); it('execution context is the same for all the lifecycle events', async () => { @@ -410,7 +411,7 @@ describe('trace', () => { .set(new ExecutionContextContainer(parentContext).toHeader()) .expect(200); - expect(response.body).toEqual({ ...parentContext, requestId: expect.any(String) }); + expect(response.body).toEqual(parentContext); expect(response.body).toEqual(onPreRoutingContext); expect(response.body).toEqual(onPreAuthContext); @@ -461,32 +462,26 @@ describe('trace', () => { expect(header).toContain('kibana:test-type:test-name:42'); }); - it('a repeat call overwrites the old context', async () => { - const { http, executionContext } = await root.setup(); + it('passes "x-opaque-id" if no execution context is registered', async () => { + const { http } = await root.setup(); const { createRouter } = http; const router = createRouter(''); - const newContext = { - type: 'new-type', - name: 'new-name', - id: '41', - description: 'new-description', - }; router.get({ path: '/execution-context', validate: false }, async (context, req, res) => { - executionContext.set(newContext); const { headers } = await context.core.elasticsearch.client.asCurrentUser.ping(); return res.ok({ body: headers || {} }); }); await root.start(); + const myOpaqueId = 'my-opaque-id'; const response = await kbnTestServer.request .get(root, '/execution-context') - .set(new ExecutionContextContainer(parentContext).toHeader()) + .set('x-opaque-id', myOpaqueId) .expect(200); const header = response.body['x-opaque-id']; - expect(header).toContain('kibana:new-type:new-name:41'); + expect(header).toBe(myOpaqueId); }); it('does not affect "x-opaque-id" set by user', async () => { @@ -531,14 +526,83 @@ describe('trace', () => { await root.start(); - const myOpaqueId = 'my-opaque-id'; - const response = await kbnTestServer.request - .get(root, '/execution-context') - .set('x-opaque-id', myOpaqueId) - .expect(200); + const response = await kbnTestServer.request.get(root, '/execution-context').expect(200); const header = response.body['x-opaque-id']; - expect(header).toBe('my-opaque-id;kibana:test-type:test-name:42'); + expect(header).toContain('kibana:test-type:test-name:42'); + }); + + describe('withContext', () => { + it('sets execution context for a nested function', async () => { + const { executionContext, http } = await root.setup(); + const { createRouter } = http; + + const router = createRouter(''); + router.get({ path: '/execution-context', validate: false }, async (context, req, res) => { + return executionContext.withContext(parentContext, () => + res.ok({ body: executionContext.get() }) + ); + }); + + await root.start(); + const response = await kbnTestServer.request.get(root, '/execution-context').expect(200); + expect(response.body).toEqual(parentContext); + }); + + it('set execution context inerits a parent if presented', async () => { + const { executionContext, http } = await root.setup(); + const { createRouter } = http; + + const router = createRouter(''); + const nestedContext = { + type: 'nested-type', + name: 'nested-name', + id: '43', + description: 'nested-description', + }; + router.get({ path: '/execution-context', validate: false }, async (context, req, res) => { + return executionContext.withContext(parentContext, async () => { + await delay(100); + return executionContext.withContext(nestedContext, async () => { + await delay(100); + return res.ok({ body: executionContext.get() }); + }); + }); + }); + + await root.start(); + const response = await kbnTestServer.request.get(root, '/execution-context').expect(200); + expect(response.body).toEqual({ ...nestedContext, parent: parentContext }); + }); + + it('extends the execution context passed from the client-side', async () => { + const { http, executionContext } = await root.setup(); + const { createRouter } = http; + + const router = createRouter(''); + const newContext = { + type: 'new-type', + name: 'new-name', + id: '41', + description: 'new-description', + }; + router.get({ path: '/execution-context', validate: false }, async (context, req, res) => { + const { headers } = await executionContext.withContext(newContext, () => + context.core.elasticsearch.client.asCurrentUser.ping() + ); + return res.ok({ body: headers || {} }); + }); + + await root.start(); + + const response = await kbnTestServer.request + .get(root, '/execution-context') + .set(new ExecutionContextContainer(parentContext).toHeader()) + .expect(200); + + const header = response.body['x-opaque-id']; + expect(header).toContain('kibana:test-type:test-name:42;new-type:new-name:41'); + }); }); }); }); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 85c035154a7a7..26725aff71b6c 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -337,10 +337,9 @@ export class HttpServer { const requestId = getRequestId(request, config.requestId); const parentContext = executionContext?.getParentContextFrom(request.headers); - executionContext?.set({ - ...parentContext, - requestId, - }); + if (parentContext) executionContext?.set(parentContext); + + executionContext?.setRequestId(requestId); request.app = { ...(request.app ?? {}), diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 191a3bd972f15..6ee95a09de303 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -84,11 +84,7 @@ export type { import type { ExecutionContextSetup, ExecutionContextStart } from './execution_context'; -export type { - IExecutionContextContainer, - KibanaServerExecutionContext, - KibanaExecutionContext, -} from './execution_context'; +export type { IExecutionContextContainer, KibanaExecutionContext } from './execution_context'; export { bootstrap } from './bootstrap'; export type { diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 29194b1e8fc62..b972c6078ca2b 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -156,7 +156,9 @@ export function createPluginSetupContext( elasticsearch: { legacy: deps.elasticsearch.legacy, }, - executionContext: deps.executionContext, + executionContext: { + withContext: deps.executionContext.withContext, + }, http: { createCookieSessionStorageFactory: deps.http.createCookieSessionStorageFactory, registerRouteHandlerContext: < diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 7f2ce38a5bdd4..50cb98939ec57 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1048,8 +1048,7 @@ export interface ErrorHttpResponseOptions { // @public (undocumented) export interface ExecutionContextSetup { - get(): IExecutionContextContainer | undefined; - set(context: Partial): void; + withContext(context: KibanaExecutionContext | undefined, fn: (...args: any[]) => R): R; } // @public (undocumented) @@ -1236,7 +1235,7 @@ export interface ICustomClusterClient extends IClusterClient { // @public (undocumented) export interface IExecutionContextContainer { // (undocumented) - toJSON(): Readonly; + toJSON(): Readonly; // (undocumented) toString(): string; } @@ -1357,14 +1356,15 @@ export interface IUiSettingsClient { setMany: (changes: Record) => Promise; } -// @public (undocumented) -export interface KibanaExecutionContext { - readonly description: string; - readonly id: string; - readonly name: string; +// @public +export type KibanaExecutionContext = { readonly type: string; + readonly name: string; + readonly id: string; + readonly description: string; readonly url?: string; -} + parent?: KibanaExecutionContext; +}; // @public export class KibanaRequest { @@ -1437,12 +1437,6 @@ export const kibanaResponseFactory: { noContent: (options?: HttpResponseOptions) => KibanaResponse; }; -// @public (undocumented) -export interface KibanaServerExecutionContext extends Partial { - // (undocumented) - requestId: string; -} - // Warning: (ae-forgotten-export) The symbol "KnownKeys" needs to be exported by the entry point index.d.ts // // @public diff --git a/src/core/types/execution_context.ts b/src/core/types/execution_context.ts index df17195d84a9a..8a2d657812da8 100644 --- a/src/core/types/execution_context.ts +++ b/src/core/types/execution_context.ts @@ -6,9 +6,13 @@ * Side Public License, v 1. */ -/** @public */ - -export interface KibanaExecutionContext { +/** + * @public + * Represents a meta-information about a Kibana entity initiating a search request. + */ +// use type to make it compatible with SerializableState +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type KibanaExecutionContext = { /** * Kibana application initated an operation. * */ @@ -21,4 +25,6 @@ export interface KibanaExecutionContext { readonly description: string; /** in browser - url to navigate to a current page, on server - endpoint path, for task: task SO url */ readonly url?: string; -} + /** a context that spawned the current context. */ + parent?: KibanaExecutionContext; +}; diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx index b6e2a7c4b8f02..953bd619c444b 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx @@ -11,7 +11,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; import uuid from 'uuid'; -import { CoreStart, IUiSettingsClient } from 'src/core/public'; +import { CoreStart, IUiSettingsClient, KibanaExecutionContext } from 'src/core/public'; import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { UiActionsStart } from '../../services/ui_actions'; @@ -68,6 +68,7 @@ export interface InheritedChildInput extends IndexSignature { id: string; searchSessionId?: string; syncColors?: boolean; + executionContext?: KibanaExecutionContext; } export type DashboardReactContextValue = KibanaReactContextValue; @@ -249,6 +250,7 @@ export class DashboardContainer extends Container { const { search: { session }, @@ -102,6 +105,7 @@ export const buildDashboardContainer = async ({ query: data.query, searchSessionId, savedDashboard, + executionContext, }); /** diff --git a/src/plugins/dashboard/public/application/lib/convert_dashboard_state.ts b/src/plugins/dashboard/public/application/lib/convert_dashboard_state.ts index ee2ec2bb14fe4..2e6290ec920c0 100644 --- a/src/plugins/dashboard/public/application/lib/convert_dashboard_state.ts +++ b/src/plugins/dashboard/public/application/lib/convert_dashboard_state.ts @@ -7,6 +7,7 @@ */ import _ from 'lodash'; +import type { KibanaExecutionContext } from 'src/core/public'; import { DashboardSavedObject } from '../../saved_dashboards'; import { getTagsFromSavedDashboard, migrateAppState } from '.'; import { EmbeddablePackageState, ViewMode } from '../../services/embeddable'; @@ -40,6 +41,7 @@ interface StateToDashboardContainerInputProps { query: DashboardBuildContext['query']; incomingEmbeddable?: EmbeddablePackageState; dashboardCapabilities: DashboardBuildContext['dashboardCapabilities']; + executionContext?: KibanaExecutionContext; } interface StateToRawDashboardStateProps { @@ -92,6 +94,7 @@ export const stateToDashboardContainerInput = ({ searchSessionId, savedDashboard, dashboardState, + executionContext, }: StateToDashboardContainerInputProps): DashboardContainerInput => { const { filterManager, timefilter: timefilterService } = queryService; const { timefilter } = timefilterService; @@ -125,6 +128,7 @@ export const stateToDashboardContainerInput = ({ timeRange: { ..._.cloneDeep(timefilter.getTime()), }, + executionContext, }; }; diff --git a/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts b/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts index b1abba44891fc..8f0c8acf81022 100644 --- a/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts +++ b/src/plugins/dashboard/public/application/lib/diff_dashboard_state.ts @@ -29,7 +29,7 @@ export const diffDashboardContainerInput = ( return commonDiffFilters( (originalInput as unknown) as DashboardDiffCommonFilters, (newInput as unknown) as DashboardDiffCommonFilters, - ['searchSessionId', 'lastReloadRequestTime'] + ['searchSessionId', 'lastReloadRequestTime', 'executionContext'] ); }; diff --git a/src/plugins/dashboard/public/types.ts b/src/plugins/dashboard/public/types.ts index 89c9adb572142..6dc068cf55f4d 100644 --- a/src/plugins/dashboard/public/types.ts +++ b/src/plugins/dashboard/public/types.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { +import type { AppMountParameters, CoreStart, SavedObjectsClientContract, @@ -14,8 +14,8 @@ import { ChromeStart, IUiSettingsClient, PluginInitializerContext, + KibanaExecutionContext, } from 'kibana/public'; - import { History } from 'history'; import { AnyAction, Dispatch } from 'redux'; import { BehaviorSubject, Subject } from 'rxjs'; @@ -86,6 +86,7 @@ export interface DashboardContainerInput extends ContainerInput { panels: { [panelId: string]: DashboardPanelState; }; + executionContext?: KibanaExecutionContext; } /** @@ -131,6 +132,7 @@ export type DashboardBuildContext = Pick< dispatchDashboardStateChange: Dispatch; $triggerDashboardRefresh: Subject<{ force?: boolean }>; $onDashboardStateChange: BehaviorSubject; + executionContext?: KibanaExecutionContext; }; export interface DashboardOptions { diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index c4a6ae3a516df..cf3de20fea50e 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -64,7 +64,7 @@ export function getFunctionDefinition({ timeFields: args.timeFields, timeRange: get(input, 'timeRange', undefined), getNow, - executionContext: getExecutionContext()?.toJSON(), + executionContext: getExecutionContext(), }) ) ); diff --git a/src/plugins/data/server/search/routes/bsearch.ts b/src/plugins/data/server/search/routes/bsearch.ts index e655a19e46f80..43853aa0ea939 100644 --- a/src/plugins/data/server/search/routes/bsearch.ts +++ b/src/plugins/data/server/search/routes/bsearch.ts @@ -33,23 +33,24 @@ export function registerBsearchRoute( onBatchItem: async ({ request: requestData, options }) => { const search = getScoped(request); const { executionContext, ...restOptions } = options || {}; - if (executionContext) executionContextService.set(executionContext); - return search - .search(requestData, restOptions) - .pipe( - first(), - catchError((err) => { - // Re-throw as object, to get attributes passed to the client - // eslint-disable-next-line no-throw-literal - throw { - message: err.message, - statusCode: err.statusCode, - attributes: err.errBody?.error, - }; - }) - ) - .toPromise(); + return executionContextService.withContext(executionContext, () => + search + .search(requestData, restOptions) + .pipe( + first(), + catchError((err) => { + // Re-throw as object, to get attributes passed to the client + // eslint-disable-next-line no-throw-literal + throw { + message: err.message, + statusCode: err.statusCode, + attributes: err.errBody?.error, + }; + }) + ) + .toPromise() + ); }, }; }); diff --git a/src/plugins/embeddable/common/types.ts b/src/plugins/embeddable/common/types.ts index a45700375672f..1cfc5073d6125 100644 --- a/src/plugins/embeddable/common/types.ts +++ b/src/plugins/embeddable/common/types.ts @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import type { KibanaExecutionContext } from 'src/core/public'; import { PersistableStateService, SerializableState } from '../../kibana_utils/common'; export enum ViewMode { @@ -49,6 +49,8 @@ export type EmbeddableInput = { * Flag whether colors should be synced with other panels */ syncColors?: boolean; + + executionContext?: KibanaExecutionContext; }; export interface PanelState { diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 54fe2a7b0bec2..33c9f2c8f9ff9 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -28,6 +28,7 @@ import { I18nStart as I18nStart_2 } from 'src/core/public'; import { IconType } from '@elastic/eui'; import { IncomingHttpHeaders } from 'http'; import { KibanaClient } from '@elastic/elasticsearch/api/kibana'; +import { KibanaExecutionContext as KibanaExecutionContext_2 } from 'src/core/public'; import { Location } from 'history'; import { LocationDescriptorObject } from 'history'; import { Logger } from '@kbn/logging'; @@ -421,6 +422,7 @@ export type EmbeddableInput = { disableTriggers?: boolean; searchSessionId?: string; syncColors?: boolean; + executionContext?: KibanaExecutionContext_2; }; // Warning: (ae-missing-release-tag) "EmbeddableInstanceConfiguration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/src/plugins/embeddable/server/server.api.md b/src/plugins/embeddable/server/server.api.md index f8f3dcb0aa0ba..4409a37d31621 100644 --- a/src/plugins/embeddable/server/server.api.md +++ b/src/plugins/embeddable/server/server.api.md @@ -6,6 +6,7 @@ import { CoreSetup } from 'kibana/server'; import { CoreStart } from 'kibana/server'; +import { KibanaExecutionContext } from 'src/core/public'; import { Plugin } from 'kibana/server'; // Warning: (ae-forgotten-export) The symbol "EmbeddableStateWithType" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/expressions/common/execution/types.ts b/src/plugins/expressions/common/execution/types.ts index 19537b3f164e7..2d164778605ae 100644 --- a/src/plugins/expressions/common/execution/types.ts +++ b/src/plugins/expressions/common/execution/types.ts @@ -8,7 +8,7 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import type { KibanaRequest } from 'src/core/server'; -import type { IExecutionContextContainer } from 'src/core/public'; +import type { KibanaExecutionContext } from 'src/core/public'; import { ExpressionType, SerializableState } from '../expression_types'; import { Adapters, RequestAdapter } from '../../../inspector/common'; @@ -67,7 +67,7 @@ export interface ExecutionContext< /** * Contains the meta-data about the source of the expression. */ - getExecutionContext: () => IExecutionContextContainer | undefined; + getExecutionContext: () => KibanaExecutionContext | undefined; } /** diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index cd52c8c3239de..68d868d61ad05 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -9,7 +9,7 @@ import { Observable } from 'rxjs'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import type { KibanaRequest } from 'src/core/server'; -import type { IExecutionContextContainer } from 'src/core/public'; +import type { KibanaExecutionContext } from 'src/core/public'; import { Executor } from '../executor'; import { AnyExpressionRenderDefinition, ExpressionRendererRegistry } from '../expression_renderers'; @@ -84,7 +84,7 @@ export interface ExpressionExecutionParams { inspectorAdapters?: Adapters; - executionContext?: IExecutionContextContainer; + executionContext?: KibanaExecutionContext; } /** diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 529d6653cb7f1..4af6b4f1e797e 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -9,7 +9,7 @@ import { CoreStart } from 'src/core/public'; import { Ensure } from '@kbn/utility-types'; import { EnvironmentMode } from '@kbn/config'; import { EventEmitter } from 'events'; -import { IExecutionContextContainer } from 'src/core/public'; +import { KibanaExecutionContext } from 'src/core/public'; import { KibanaRequest } from 'src/core/server'; import { Observable } from 'rxjs'; import { ObservableLike } from '@kbn/utility-types'; @@ -139,7 +139,7 @@ export type ExecutionContainer = StateContainer { abortSignal: AbortSignal; - getExecutionContext: () => IExecutionContextContainer | undefined; + getExecutionContext: () => KibanaExecutionContext | undefined; getKibanaRequest?: () => KibanaRequest; getSearchContext: () => ExecutionContextSearch; getSearchSessionId: () => string | undefined; @@ -908,7 +908,7 @@ export interface IExpressionLoaderParams { // (undocumented) disableCaching?: boolean; // (undocumented) - executionContext?: IExecutionContextContainer; + executionContext?: KibanaExecutionContext; // (undocumented) hasCompatibleActions?: ExpressionRenderHandlerParams['hasCompatibleActions']; // (undocumented) diff --git a/src/plugins/expressions/public/types/index.ts b/src/plugins/expressions/public/types/index.ts index 675ed7eeed7c3..ce1381ba8ea43 100644 --- a/src/plugins/expressions/public/types/index.ts +++ b/src/plugins/expressions/public/types/index.ts @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import type { IExecutionContextContainer } from 'src/core/public'; +import type { KibanaExecutionContext } from 'src/core/public'; import { Adapters } from '../../../inspector/public'; import { IInterpreterRenderHandlers, @@ -48,7 +48,7 @@ export interface IExpressionLoaderParams { renderMode?: RenderMode; syncColors?: boolean; hasCompatibleActions?: ExpressionRenderHandlerParams['hasCompatibleActions']; - executionContext?: IExecutionContextContainer; + executionContext?: KibanaExecutionContext; /** * The flag to toggle on emitting partial results. diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index 236c54db7140c..22280c1bd4b22 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -8,7 +8,7 @@ import { CoreSetup } from 'src/core/server'; import { CoreStart } from 'src/core/server'; import { Ensure } from '@kbn/utility-types'; import { EventEmitter } from 'events'; -import { IExecutionContextContainer } from 'src/core/public'; +import { KibanaExecutionContext } from 'src/core/public'; import { KibanaRequest } from 'src/core/server'; import { Observable } from 'rxjs'; import { ObservableLike } from '@kbn/utility-types'; @@ -137,7 +137,7 @@ export type ExecutionContainer = StateContainer { abortSignal: AbortSignal; - getExecutionContext: () => IExecutionContextContainer | undefined; + getExecutionContext: () => KibanaExecutionContext | undefined; getKibanaRequest?: () => KibanaRequest; getSearchContext: () => ExecutionContextSearch; getSearchSessionId: () => string | undefined; diff --git a/src/plugins/vis_type_timeseries/public/request_handler.ts b/src/plugins/vis_type_timeseries/public/request_handler.ts index 66dbdaaabcddb..0a110dd65d5e9 100644 --- a/src/plugins/vis_type_timeseries/public/request_handler.ts +++ b/src/plugins/vis_type_timeseries/public/request_handler.ts @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import type { IExecutionContextContainer } from 'src/core/public'; +import type { KibanaExecutionContext } from 'src/core/public'; import { getTimezone } from './application/lib/get_timezone'; import { getUISettings, getDataStart, getCoreStart } from './services'; import { ROUTES } from '../common/constants'; @@ -19,7 +19,7 @@ interface MetricsRequestHandlerParams { uiState: Record; visParams: TimeseriesVisParams; searchSessionId?: string; - executionContext?: IExecutionContextContainer; + executionContext?: KibanaExecutionContext; } export const metricsRequestHandler = async ({ diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index d7ea04daae1ae..b71542a8beeea 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -384,6 +384,15 @@ export class VisualizeEmbeddable }; private async updateHandler() { + const context = { + type: 'visualization', + name: this.vis.type.title, + id: this.vis.id ?? 'an_unsaved_vis', + description: this.vis.title || this.input.title || this.vis.type.name, + url: this.output.editUrl, + parent: this.parent?.getInput().executionContext, + }; + const expressionParams: IExpressionLoaderParams = { searchContext: { timeRange: this.timeRange, @@ -394,13 +403,7 @@ export class VisualizeEmbeddable syncColors: this.input.syncColors, uiState: this.vis.uiState, inspectorAdapters: this.inspectorAdapters, - executionContext: this.deps.start().core.executionContext.create({ - type: 'visualization', - name: this.vis.type.name, - id: this.vis.id ?? 'an_unsaved_vis', - description: this.vis.title ?? this.vis.type.title, - url: this.output.editUrl, - }), + executionContext: context, }; if (this.abortController) { this.abortController.abort(); diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx index fd22489eb7556..872132416352f 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx @@ -59,10 +59,7 @@ interface VisualizationAttributes extends SavedObjectAttributes { export interface VisualizeEmbeddableFactoryDeps { start: StartServicesGetter< - Pick< - VisualizationsStartDeps, - 'inspector' | 'embeddable' | 'savedObjectsClient' | 'executionContext' - > + Pick >; } diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts index 97800ef40e8ad..901593626a945 100644 --- a/src/plugins/visualizations/public/mocks.ts +++ b/src/plugins/visualizations/public/mocks.ts @@ -64,7 +64,6 @@ const createInstance = async () => { getAttributeService: jest.fn(), savedObjectsClient: coreMock.createStart().savedObjects.client, savedObjects: savedObjectsPluginMock.createStartContract(), - executionContext: coreMock.createStart().executionContext, }); return { diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index f5b2f0edd044e..ae97080b31fc5 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -54,7 +54,6 @@ import type { Plugin, ApplicationStart, SavedObjectsClientContract, - ExecutionContextServiceStart, } from '../../../core/public'; import type { UsageCollectionSetup } from '../../usage_collection/public'; import type { UiActionsStart } from '../../ui_actions/public'; @@ -104,7 +103,6 @@ export interface VisualizationsStartDeps { getAttributeService: EmbeddableStart['getAttributeService']; savedObjects: SavedObjectsStart; savedObjectsClient: SavedObjectsClientContract; - executionContext: ExecutionContextServiceStart; } /** diff --git a/test/plugin_functional/test_suites/core_plugins/execution_context.ts b/test/plugin_functional/test_suites/core_plugins/execution_context.ts index 93dc494ba5d6d..7dc9922dca51d 100644 --- a/test/plugin_functional/test_suites/core_plugins/execution_context.ts +++ b/test/plugin_functional/test_suites/core_plugins/execution_context.ts @@ -25,13 +25,13 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide await browser.execute(async () => { const coreStart = window._coreProvider.start.core; - const context = coreStart.executionContext.create({ + const context = { type: 'visualization', name: 'execution_context_app', // add a non-ASCII symbols to make sure it doesn't break the context propagation mechanism id: 'Visualization☺漢字', description: 'какое-то странное описание', - }); + }; const result = await coreStart.http.get('/execution_context/pass', { context, diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx index 5cf2abf7b8d0f..56abf499aac88 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx @@ -128,7 +128,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, { timeRange: { @@ -168,7 +167,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, { timeRange: { @@ -212,7 +210,6 @@ describe('embeddable', () => { }, errors: [{ shortMessage: '', longMessage: 'my validation error' }], }), - executionContext: coreMock.createStart().executionContext, }, {} as LensEmbeddableInput ); @@ -256,7 +253,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, {} as LensEmbeddableInput ); @@ -295,7 +291,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, { id: '123' } as LensEmbeddableInput ); @@ -337,7 +332,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, { id: '123' } as LensEmbeddableInput ); @@ -386,7 +380,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, { id: '123' } as LensEmbeddableInput ); @@ -433,7 +426,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, { id: '123' } as LensEmbeddableInput ); @@ -487,7 +479,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, input ); @@ -541,7 +532,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, input ); @@ -594,7 +584,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, input ); @@ -636,7 +625,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, { id: '123' } as LensEmbeddableInput ); @@ -678,7 +666,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, { id: '123' } as LensEmbeddableInput ); @@ -720,7 +707,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, { id: '123', timeRange, query, filters } as LensEmbeddableInput ); @@ -777,7 +763,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, ({ id: '123', onLoad } as unknown) as LensEmbeddableInput ); @@ -850,7 +835,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, ({ id: '123', onFilter } as unknown) as LensEmbeddableInput ); @@ -898,7 +882,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, ({ id: '123', onBrushEnd } as unknown) as LensEmbeddableInput ); @@ -946,7 +929,6 @@ describe('embeddable', () => { }, errors: undefined, }), - executionContext: coreMock.createStart().executionContext, }, ({ id: '123', onTableRowClick } as unknown) as LensEmbeddableInput ); diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index e26466be6f81b..4ecb86ac1069f 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -8,7 +8,6 @@ import { isEqual, uniqBy } from 'lodash'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import type { ExecutionContextServiceStart } from 'src/core/public'; import { ExecutionContextSearch, Filter, @@ -98,7 +97,6 @@ export interface LensEmbeddableDeps { getTriggerCompatibleActions?: UiActionsStart['getTriggerCompatibleActions']; capabilities: { canSaveVisualizations: boolean; canSaveDashboards: boolean }; usageCollection?: UsageCollectionSetup; - executionContext: ExecutionContextServiceStart; } export class Embeddable @@ -324,13 +322,16 @@ export class Embeddable if (this.input.onLoad) { this.input.onLoad(true); } - const executionContext = this.deps.executionContext.create({ + + const executionContext = { type: 'lens', name: this.savedVis.visualizationType ?? '', - description: this.savedVis.title ?? this.savedVis.description ?? '', id: this.id, + description: this.savedVis.title || this.input.title || '', url: this.output.editUrl, - }); + parent: this.input.executionContext, + }; + const input = this.getInput(); render( diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts index ba4e4df849488..4cc074b5e830c 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts @@ -5,12 +5,7 @@ * 2.0. */ -import type { - Capabilities, - HttpSetup, - SavedObjectReference, - ExecutionContextServiceStart, -} from 'kibana/public'; +import type { Capabilities, HttpSetup, SavedObjectReference } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { RecursiveReadonly } from '@kbn/utility-types'; import { Ast } from '@kbn/interpreter/target/common'; @@ -38,7 +33,6 @@ export interface LensEmbeddableStartServices { indexPatternService: IndexPatternsContract; uiActions?: UiActionsStart; usageCollection?: UsageCollectionSetup; - executionContext: ExecutionContextServiceStart; documentToExpression: ( doc: Document ) => Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }>; @@ -93,7 +87,6 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { indexPatternService, capabilities, usageCollection, - executionContext, } = await this.getStartServices(); const { Embeddable } = await import('../async_services'); @@ -113,7 +106,6 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { canSaveVisualizations: Boolean(capabilities.visualize.save), }, usageCollection, - executionContext, }, input, parent diff --git a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx index b61dba1623028..fc6fcee9428b0 100644 --- a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx +++ b/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx @@ -14,7 +14,7 @@ import { ReactExpressionRendererType, ReactExpressionRendererProps, } from 'src/plugins/expressions/public'; -import type { IExecutionContextContainer } from 'src/core/public'; +import type { KibanaExecutionContext } from 'src/core/public'; import { ExecutionContextSearch } from 'src/plugins/data/public'; import { DefaultInspectorAdapters, RenderMode } from 'src/plugins/expressions'; import classNames from 'classnames'; @@ -40,7 +40,7 @@ export interface ExpressionWrapperProps { className?: string; canEdit: boolean; onRuntimeError: () => void; - executionContext?: IExecutionContextContainer; + executionContext?: KibanaExecutionContext; } interface VisualizationErrorProps { diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index ae8aa268c0580..26608f9cc78be 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -177,7 +177,6 @@ export class LensPlugin { attributeService: await this.attributeService!(), capabilities: coreStart.application.capabilities, coreHttp: coreStart.http, - executionContext: coreStart.executionContext, timefilter: deps.data.query.timefilter.timefilter, expressionRenderer: deps.expressions.ReactExpressionRenderer, documentToExpression: this.editorFrameService!.documentToExpression, From 6008b5ae55998c8225ccbf15893b1448811ce82a Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 10 Aug 2021 10:19:40 +0300 Subject: [PATCH 008/104] [Lens] chore: remove dead code (#107882) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../indexpattern_datasource/datapanel.tsx | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 3f359f214fe40..0e53b0f3c8d44 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -347,23 +347,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ supportedFieldTypes.has(field.type) ); const sorted = allSupportedTypesFields.sort(sortFields); - let groupedFields; - // optimization before existingFields are synced - if (!hasSyncedExistingFields) { - groupedFields = { - ...defaultFieldGroups, - ...groupBy(sorted, (field) => { - if (field.type === 'document') { - return 'specialFields'; - } else if (field.meta) { - return 'metaFields'; - } else { - return 'emptyFields'; - } - }), - }; - } - groupedFields = { + const groupedFields = { ...defaultFieldGroups, ...groupBy(sorted, (field) => { if (field.type === 'document') { @@ -455,7 +439,6 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ return fieldGroupDefinitions; }, [ allFields, - hasSyncedExistingFields, fieldInfoUnavailable, filters.length, existenceFetchTimeout, From 04260d30a71c42ede2c534155e03e7682066149e Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Tue, 10 Aug 2021 09:54:08 +0200 Subject: [PATCH 009/104] [Security Solution] Display a correct label for memory and behavior protections (#107724) --- .../view/policy_forms/components/protection_switch.tsx | 7 ++++--- .../policy/view/policy_forms/protections/behavior.tsx | 10 +++++++++- .../policy/view/policy_forms/protections/malware.tsx | 10 +++++++++- .../policy/view/policy_forms/protections/memory.tsx | 10 +++++++++- .../view/policy_forms/protections/ransomware.tsx | 10 +++++++++- x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 7 files changed, 40 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_switch.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_switch.tsx index eaa5a66e4e375..85db83bf0ada6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_switch.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/protection_switch.tsx @@ -24,9 +24,11 @@ import { PolicyProtection, MacPolicyProtection, LinuxPolicyProtection } from '.. export const ProtectionSwitch = React.memo( ({ protection, + protectionLabel, osList, }: { protection: PolicyProtection; + protectionLabel?: string; osList: ImmutableArray>; }) => { const policyDetailsConfig = usePolicyDetailsSelector(policyConfig); @@ -93,10 +95,9 @@ export const ProtectionSwitch = React.memo( return ( { const OSes: Immutable = [OS.windows, OS.mac, OS.linux]; const protection = 'behavior_protection'; + const protectionLabel = i18n.translate( + 'xpack.securitySolution.endpoint.policy.protections.behavior', + { + defaultMessage: 'Behaviour protections', + } + ); return ( { })} supportedOss={[OperatingSystem.WINDOWS, OperatingSystem.MAC, OperatingSystem.LINUX]} dataTestSubj="behaviorProtectionsForm" - rightCorner={} + rightCorner={ + + } > diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx index 6374ba3bc4f5f..5056cd2feb101 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -26,6 +26,12 @@ import { ProtectionSwitch } from '../components/protection_switch'; export const MalwareProtections = React.memo(() => { const OSes: Immutable = [OS.windows, OS.mac, OS.linux]; const protection = 'malware'; + const protectionLabel = i18n.translate( + 'xpack.securitySolution.endpoint.policy.protections.malware', + { + defaultMessage: 'Malware protections', + } + ); const isPlatinumPlus = useLicense().isPlatinumPlus(); return ( @@ -35,7 +41,9 @@ export const MalwareProtections = React.memo(() => { })} supportedOss={[OperatingSystem.WINDOWS, OperatingSystem.MAC, OperatingSystem.LINUX]} dataTestSubj="malwareProtectionsForm" - rightCorner={} + rightCorner={ + + } > {isPlatinumPlus && } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx index 45d472a00f31e..f12449fccbe2b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx @@ -25,6 +25,12 @@ import { ProtectionSwitch } from '../components/protection_switch'; export const MemoryProtection = React.memo(() => { const OSes: Immutable = [OS.windows]; const protection = 'memory_protection'; + const protectionLabel = i18n.translate( + 'xpack.securitySolution.endpoint.policy.protections.memory', + { + defaultMessage: 'Memory protections', + } + ); return ( { })} supportedOss={[OperatingSystem.WINDOWS]} dataTestSubj="memoryProtectionsForm" - rightCorner={} + rightCorner={ + + } > diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx index 70f41015bc257..96635482f4b61 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx @@ -25,6 +25,12 @@ import { ProtectionSwitch } from '../components/protection_switch'; export const Ransomware = React.memo(() => { const OSes: Immutable = [OS.windows]; const protection = 'ransomware'; + const protectionLabel = i18n.translate( + 'xpack.securitySolution.endpoint.policy.protections.ransomware', + { + defaultMessage: 'Ransomware protections', + } + ); return ( { })} supportedOss={[OperatingSystem.WINDOWS]} dataTestSubj="ransomwareProtectionsForm" - rightCorner={} + rightCorner={ + + } > diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8decd2a76b9b4..14e1516ba1f15 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -21527,7 +21527,6 @@ "xpack.securitySolution.endpoint.policy.details.platinum": "プラチナ", "xpack.securitySolution.endpoint.policy.details.prevent": "防御", "xpack.securitySolution.endpoint.policy.details.protections": "保護", - "xpack.securitySolution.endpoint.policy.details.protectionsEnabled": "{protectionName}保護は{mode, select, true {有効です} false {無効です}}", "xpack.securitySolution.endpoint.policy.details.ransomware": "ランサムウェア", "xpack.securitySolution.endpoint.policy.details.save": "保存", "xpack.securitySolution.endpoint.policy.details.settings": "設定", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 90e5e37f3d7aa..40db95bd4fb7c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -22013,7 +22013,6 @@ "xpack.securitySolution.endpoint.policy.details.platinum": "白金级", "xpack.securitySolution.endpoint.policy.details.prevent": "防御", "xpack.securitySolution.endpoint.policy.details.protections": "防护", - "xpack.securitySolution.endpoint.policy.details.protectionsEnabled": "{protectionName}防护{mode, select, true {已启用} false {已禁用}}", "xpack.securitySolution.endpoint.policy.details.ransomware": "勒索软件", "xpack.securitySolution.endpoint.policy.details.save": "保存", "xpack.securitySolution.endpoint.policy.details.settings": "设置", From 87db81505deb30b9a71d645d845b2c1c0e10af3a Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Tue, 10 Aug 2021 10:17:38 +0200 Subject: [PATCH 010/104] [Discover] Apply histogram layout of new table to classic table layout (#107766) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../apps/main/components/chart/discover_chart.test.tsx | 1 - .../apps/main/components/chart/discover_chart.tsx | 7 +------ .../apps/main/components/layout/discover_layout.scss | 7 ------- .../apps/main/components/layout/discover_layout.tsx | 4 +--- 4 files changed, 2 insertions(+), 17 deletions(-) diff --git a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx index 7a13f18997b86..dc3c9ebbc75ca 100644 --- a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.test.tsx @@ -96,7 +96,6 @@ function getProps(timefield?: string) { }) as DataCharts$; return { - isLegacy: false, resetQuery: jest.fn(), savedSearch: savedSearchMock, savedSearchDataChart$: charts$, diff --git a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx index 165f708bf7083..7d761aa93b808 100644 --- a/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx +++ b/src/plugins/discover/public/application/apps/main/components/chart/discover_chart.tsx @@ -21,7 +21,6 @@ import { DiscoverServices } from '../../../../../build_services'; const TimechartHeaderMemoized = memo(TimechartHeader); const DiscoverHistogramMemoized = memo(DiscoverHistogram); export function DiscoverChart({ - isLegacy, resetQuery, savedSearch, savedSearchDataChart$, @@ -31,7 +30,6 @@ export function DiscoverChart({ stateContainer, timefield, }: { - isLegacy: boolean; resetQuery: () => void; savedSearch: SavedSearch; savedSearchDataChart$: DataCharts$; @@ -135,10 +133,7 @@ export function DiscoverChart({ })} className="dscTimechart" > -
+
uiSettings.get(DOC_TABLE_LEGACY), [uiSettings]); const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]); const resultState = useMemo( @@ -251,7 +250,6 @@ export function DiscoverLayout({ > Date: Tue, 10 Aug 2021 11:34:09 +0300 Subject: [PATCH 011/104] [Timelion] Fix behavior for points and bars in timelion (#107398) * Fix behavior for points and bars * Fix lint * Fix color for points * Some fixes * Fix lint Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../vis_type_timelion/common/vis_data.ts | 1 + .../public/components/series/area.tsx | 18 +++++++++++++++--- .../public/components/series/bar.tsx | 8 +++----- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/plugins/vis_type_timelion/common/vis_data.ts b/src/plugins/vis_type_timelion/common/vis_data.ts index e3041f43a8f19..bab6198be603e 100644 --- a/src/plugins/vis_type_timelion/common/vis_data.ts +++ b/src/plugins/vis_type_timelion/common/vis_data.ts @@ -30,4 +30,5 @@ export interface VisSeries { color?: string; data: Array>; stack: boolean; + _hide?: boolean; } diff --git a/src/plugins/vis_type_timelion/public/components/series/area.tsx b/src/plugins/vis_type_timelion/public/components/series/area.tsx index 589a488d3acad..73e16d97684d5 100644 --- a/src/plugins/vis_type_timelion/public/components/series/area.tsx +++ b/src/plugins/vis_type_timelion/public/components/series/area.tsx @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +// @ts-ignore +import chroma from 'chroma-js'; import React from 'react'; import { AreaSeries, ScaleType, CurveType, AreaSeriesStyle, PointShape } from '@elastic/charts'; import type { VisSeries } from '../../../common/vis_data'; @@ -19,6 +21,16 @@ interface AreaSeriesComponentProps { const isShowLines = (lines: VisSeries['lines'], points: VisSeries['points']) => lines?.show ? true : points?.show ? false : true; +const getPointFillColor = (points: VisSeries['points'], color: string | undefined) => { + const pointFillColor = points?.fillColor || points?.fill === undefined ? 'white' : color; + return ( + pointFillColor && + chroma(pointFillColor) + .alpha(points?.fill ?? 1) + .css() + ); +}; + const getAreaSeriesStyle = ({ color, lines, points }: AreaSeriesComponentProps['visData']) => ({ line: { @@ -33,8 +45,8 @@ const getAreaSeriesStyle = ({ color, lines, points }: AreaSeriesComponentProps[' visible: lines?.show ?? points?.show ?? true, }, point: { - fill: points?.fillColor ?? color, - opacity: points?.lineWidth !== undefined ? (points.fill || 1) * 10 : 10, + fill: getPointFillColor(points, color), + opacity: 1, radius: points?.radius ?? 3, stroke: color, strokeWidth: points?.lineWidth ?? 2, @@ -53,7 +65,7 @@ export const AreaSeriesComponent = ({ index, groupId, visData }: AreaSeriesCompo yScaleType={ScaleType.Linear} xAccessor={0} yAccessors={[1]} - data={visData.data} + data={visData._hide ? [] : visData.data} sortIndex={index} color={visData.color} stackAccessors={visData.stack ? [0] : undefined} diff --git a/src/plugins/vis_type_timelion/public/components/series/bar.tsx b/src/plugins/vis_type_timelion/public/components/series/bar.tsx index 6a97c8fea9690..0a26fb51c32c6 100644 --- a/src/plugins/vis_type_timelion/public/components/series/bar.tsx +++ b/src/plugins/vis_type_timelion/public/components/series/bar.tsx @@ -27,14 +27,12 @@ const getBarSeriesStyle = ({ color, bars }: BarSeriesComponentProps['visData']) return { rectBorder: { - stroke: color, - strokeWidth: Math.max(1, bars.lineWidth ? Math.ceil(bars.lineWidth / 2) : 1), - visible: true, + visible: false, }, rect: { fill: color, opacity, - widthPixel: 1, + widthPixel: Math.max(1, bars.lineWidth ?? 1), }, } as BarSeriesStyle; }; @@ -48,7 +46,7 @@ export const BarSeriesComponent = ({ index, groupId, visData }: BarSeriesCompone yScaleType={ScaleType.Linear} xAccessor={0} yAccessors={[1]} - data={visData.data} + data={visData._hide ? [] : visData.data} sortIndex={index} enableHistogramMode={false} color={visData.color} From 54b4296243c21f4ecfd25a05e0077840602ea1dc Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Tue, 10 Aug 2021 11:38:55 +0300 Subject: [PATCH 012/104] [Canvas] Datacolumn refactor (#106268) * Refactored Datacolumn. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../uis/arguments/datacolumn/index.js | 198 ++++++++---------- .../datacolumn/simple_math_function.js | 7 +- 2 files changed, 94 insertions(+), 111 deletions(-) diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/index.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/index.js index b6343f5ca7ca4..bc77f347530b7 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/index.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/index.js @@ -5,142 +5,128 @@ * 2.0. */ -import React, { Component } from 'react'; -import { compose, withPropsOnChange, withHandlers } from 'recompose'; +import React, { useState, useCallback, useEffect } from 'react'; import PropTypes from 'prop-types'; import { EuiSelect, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { sortBy } from 'lodash'; import { getType } from '@kbn/interpreter/common'; -import { createStatefulPropHoc } from '../../../../public/components/enhance/stateful_prop'; import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component'; import { ArgumentStrings } from '../../../../i18n'; import { SimpleMathFunction } from './simple_math_function'; import { getFormObject } from './get_form_object'; const { DataColumn: strings } = ArgumentStrings; + const maybeQuoteValue = (val) => (val.match(/\s/) ? `'${val}'` : val); +const valueNotSet = (val) => !val || val.length === 0; -// TODO: Garbage, we could make a much nicer math form that can handle way more. -class DatacolumnArgInput extends Component { - static propTypes = { - columns: PropTypes.array.isRequired, - onValueChange: PropTypes.func.isRequired, - mathValue: PropTypes.object.isRequired, - setMathFunction: PropTypes.func.isRequired, - typeInstance: PropTypes.object.isRequired, - renderError: PropTypes.func.isRequired, - argId: PropTypes.string.isRequired, - }; - - inputRefs = {}; - - render() { - const { - onValueChange, - columns, - mathValue, - setMathFunction, - renderError, - argId, - typeInstance, - } = this.props; - - if (mathValue.error) { - renderError(); - return null; - } - - const allowedTypes = typeInstance.options.allowedTypes || false; - const onlyShowMathFunctions = typeInstance.options.onlyMath || false; - const valueNotSet = (val) => !val || val.length === 0; - - const updateFunctionValue = () => { - const fn = this.inputRefs.fn.value; - const column = this.inputRefs.column.value; +const getMathValue = (argValue, columns) => { + if (getType(argValue) !== 'string') { + return { error: 'argValue is not a string type' }; + } + try { + const matchedCol = columns.find(({ name }) => argValue === name); + const val = matchedCol ? maybeQuoteValue(matchedCol.name) : argValue; + const mathValue = getFormObject(val); + return { ...mathValue, column: mathValue.column || '' }; + } catch (e) { + return { error: e.message }; + } +}; +// TODO: Garbage, we could make a much nicer math form that can handle way more. +const DatacolumnArgInput = ({ + onValueChange, + columns, + argValue, + renderError, + argId, + typeInstance, +}) => { + const [mathValue, setMathValue] = useState(getMathValue(argValue, columns)); + + useEffect(() => { + setMathValue(getMathValue(argValue, columns)); + }, [argValue, columns]); + + const allowedTypes = typeInstance.options.allowedTypes || false; + const onlyShowMathFunctions = typeInstance.options.onlyMath || false; + + const updateFunctionValue = useCallback( + (fn, column) => { // if setting size, auto-select the first column if no column is already set if (fn === 'size') { - const col = column || (columns[0] && columns[0].name); + const col = column || (columns[0] && columns[0].name) || ''; if (col) { return onValueChange(`${fn}(${maybeQuoteValue(col)})`); } } - // this.inputRefs.column is the column selection, if there is no value, do nothing + // if there is no column value, do nothing if (valueNotSet(column)) { - return setMathFunction(fn); + return setMathValue({ ...mathValue, fn }); } - // this.inputRefs.fn is the math function to use, if it's not set, just use the value input + // if fn is not set, just use the value input if (valueNotSet(fn)) { return onValueChange(column); } - // this.inputRefs.fn has a value, so use it as a math.js expression + // fn has a value, so use it as a math.js expression onValueChange(`${fn}(${maybeQuoteValue(column)})`); - }; - - const column = - columns.map((col) => col.name).find((colName) => colName === mathValue.column) || ''; - - const options = [{ value: '', text: 'select column', disabled: true }]; - - sortBy(columns, 'name').forEach((column) => { - if (allowedTypes && !allowedTypes.includes(column.type)) { - return; - } - options.push({ value: column.name, text: column.name }); - }); - - return ( - - - (this.inputRefs.fn = ref)} - onlymath={onlyShowMathFunctions} - onChange={updateFunctionValue} - /> - - - (this.inputRefs.column = ref)} - onChange={updateFunctionValue} - /> - - - ); + }, + [mathValue, onValueChange, columns] + ); + + const onChangeFn = useCallback( + ({ target: { value } }) => updateFunctionValue(value, mathValue.column), + [mathValue.column, updateFunctionValue] + ); + + const onChangeColumn = useCallback( + ({ target: { value } }) => updateFunctionValue(mathValue.fn, value), + [mathValue.fn, updateFunctionValue] + ); + + if (mathValue.error) { + renderError(); + return null; } -} -const EnhancedDatacolumnArgInput = compose( - withPropsOnChange(['argValue', 'columns'], ({ argValue, columns }) => ({ - mathValue: ((argValue) => { - if (getType(argValue) !== 'string') { - return { error: 'argValue is not a string type' }; - } - try { - const matchedCol = columns.find(({ name }) => argValue === name); - const val = matchedCol ? maybeQuoteValue(matchedCol.name) : argValue; - return getFormObject(val); - } catch (e) { - return { error: e.message }; - } - })(argValue), - })), - createStatefulPropHoc('mathValue', 'setMathValue'), - withHandlers({ - setMathFunction: ({ mathValue, setMathValue }) => (fn) => setMathValue({ ...mathValue, fn }), - }) -)(DatacolumnArgInput); - -EnhancedDatacolumnArgInput.propTypes = { - argValue: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, + const firstColumnOption = { value: '', text: 'select column', disabled: true }; + const options = sortBy(columns, 'name') + .filter((column) => !allowedTypes || allowedTypes.includes(column.type)) + .map(({ name }) => ({ value: name, text: name })); + + return ( + + + + + + + + + ); +}; + +DatacolumnArgInput.propTypes = { columns: PropTypes.array.isRequired, + onValueChange: PropTypes.func.isRequired, + typeInstance: PropTypes.object.isRequired, + renderError: PropTypes.func.isRequired, + argId: PropTypes.string.isRequired, + argValue: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, }; export const datacolumn = () => ({ @@ -148,5 +134,5 @@ export const datacolumn = () => ({ displayName: strings.getDisplayName(), help: strings.getHelp(), default: '""', - simpleTemplate: templateFromReactComponent(EnhancedDatacolumnArgInput), + simpleTemplate: templateFromReactComponent(DatacolumnArgInput), }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/simple_math_function.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/simple_math_function.js index c54b44d3cc5ba..5fd859a789d85 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/simple_math_function.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/simple_math_function.js @@ -12,7 +12,7 @@ import { ArgumentStrings } from '../../../../i18n'; const { DataColumn: strings } = ArgumentStrings; -export const SimpleMathFunction = ({ onChange, value, inputRef, onlymath }) => { +export const SimpleMathFunction = ({ onChange, value, onlymath }) => { const options = [ { text: strings.getOptionAverage(), value: 'mean' }, { text: strings.getOptionCount(), value: 'size' }, @@ -29,15 +29,12 @@ export const SimpleMathFunction = ({ onChange, value, inputRef, onlymath }) => { options.unshift({ text: strings.getOptionValue(), value: '' }); } - return ( - - ); + return ; }; SimpleMathFunction.propTypes = { onChange: PropTypes.func, value: PropTypes.string, - inputRef: PropTypes.func, onlymath: PropTypes.bool, }; From e2d88b71b5e120ba601245e679cd142b4af3b89c Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Tue, 10 Aug 2021 11:39:40 +0300 Subject: [PATCH 013/104] [Canvas] Arg form refactor. (#106551) * Refactored `ArgForm`. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/components/arg_form/arg_form.js | 240 +++++++++--------- .../public/components/arg_form/arg_label.js | 28 +- .../components/arg_form/pending_arg_value.js | 1 - 3 files changed, 131 insertions(+), 138 deletions(-) diff --git a/x-pack/plugins/canvas/public/components/arg_form/arg_form.js b/x-pack/plugins/canvas/public/components/arg_form/arg_form.js index 88cb529fcdb8d..0f22307c9cd1c 100644 --- a/x-pack/plugins/canvas/public/components/arg_form/arg_form.js +++ b/x-pack/plugins/canvas/public/components/arg_form/arg_form.js @@ -5,9 +5,8 @@ * 2.0. */ -import React, { PureComponent } from 'react'; +import React, { useRef, useEffect } from 'react'; import PropTypes from 'prop-types'; -import { compose, branch, renderComponent } from 'recompose'; import { ErrorBoundary } from '../enhance/error_boundary'; import { ArgSimpleForm } from './arg_simple_form'; import { ArgTemplateForm } from './arg_template_form'; @@ -16,132 +15,131 @@ import { AdvancedFailure } from './advanced_failure'; import { ArgLabel } from './arg_label'; import { PendingArgValue } from './pending_arg_value'; -const branches = [ - // rendered argType args should be resolved, but are not - branch(({ argTypeInstance, resolvedArgValue }) => { - const { argType } = argTypeInstance; +const isPending = (argTypeInstance, resolvedArgValue) => { + const { argType } = argTypeInstance; - // arg does not need to be resolved, no need to branch - if (!argType.resolveArgValue) { - return false; - } + // arg does not need to be resolved, no need to branch + if (!argType.resolveArgValue) { + return false; + } - // arg needs to be resolved, render pending if the value is not defined - return typeof resolvedArgValue === 'undefined'; - }, renderComponent(PendingArgValue)), -]; + // arg needs to be resolved, render pending if the value is not defined + return typeof resolvedArgValue === 'undefined'; +}; // This is what is being generated by render() from the Arg class. It is called in FunctionForm -class ArgFormComponent extends PureComponent { - componentDidMount() { - // keep track of whether or not the component is mounted, to prevent rogue setState calls - this._isMounted = true; - } - - componentWillUnmount() { - this._isMounted = false; +export const ArgForm = (props) => { + const { + argId, + argTypeInstance, + templateProps, + valueMissing, + label, + setLabel, + onValueRemove, + workpad, + assets, + renderError, + setRenderError, + resolvedArgValue, + } = props; + + const isMounted = useRef(); + + useEffect(() => { + isMounted.current = true; + return () => { + isMounted.current = false; + }; + }, []); + + if (isPending(argTypeInstance, resolvedArgValue)) { + return ; } - render() { - const { - argId, - argTypeInstance, - templateProps, - valueMissing, - label, - setLabel, - onValueRemove, - workpad, - assets, - renderError, - setRenderError, - resolvedArgValue, - } = this.props; - - return ( - - {({ error, resetErrorState }) => { - const { template, simpleTemplate } = argTypeInstance.argType; - const hasError = Boolean(error) || renderError; - - const argumentProps = { - ...templateProps, - resolvedArgValue, - defaultValue: argTypeInstance.default, - - renderError: () => { - // TODO: don't do this - // It's an ugly hack to avoid React's render cycle and ensure the error happens on the next tick - // This is important; Otherwise we end up updating state in the middle of a render cycle - Promise.resolve().then(() => { - // Provide templates with a renderError method, and wrap the error in a known error type - // to stop Kibana's window.error from being called - // see window_error_handler.js for details, - this._isMounted && setRenderError(true); - }); - }, - error: hasError, - setLabel: (label) => this._isMounted && setLabel(label), - resetErrorState: () => { - resetErrorState(); - this._isMounted && setRenderError(false); - }, - label, - workpad, - argId, - assets, - }; - - const expandableLabel = Boolean(hasError || template); - - const simpleArg = ( - + {({ error, resetErrorState }) => { + const { template, simpleTemplate } = argTypeInstance.argType; + const hasError = Boolean(error) || renderError; + + const argumentProps = { + ...templateProps, + resolvedArgValue, + defaultValue: argTypeInstance.default, + + renderError: () => { + // TODO: don't do this + // It's an ugly hack to avoid React's render cycle and ensure the error happens on the next tick + // This is important; Otherwise we end up updating state in the middle of a render cycle + Promise.resolve().then(() => { + // Provide templates with a renderError method, and wrap the error in a known error type + // to stop Kibana's window.error from being called + // see window_error_handler.js for details, + isMounted.current && setRenderError(true); + }); + }, + error: hasError, + setLabel: (label) => isMounted.current && setLabel(label), + resetErrorState: () => { + resetErrorState(); + isMounted.current && setRenderError(false); + }, + label, + workpad, + argId, + assets, + }; + + const expandableLabel = Boolean(hasError || template); + + const simpleArg = ( + + + + ); + + const extendedArg = ( +
+ +
+ ); + + return ( +
+ - - - ); - - const extendedArg = ( -
- -
- ); - - return ( -
- - {extendedArg} - -
- ); - }} - - ); - } -} + {extendedArg} +
+
+ ); + }} +
+ ); +}; -ArgFormComponent.propTypes = { +ArgForm.propTypes = { argId: PropTypes.string.isRequired, workpad: PropTypes.object.isRequired, argTypeInstance: PropTypes.shape({ @@ -161,5 +159,3 @@ ArgFormComponent.propTypes = { setRenderError: PropTypes.func.isRequired, resolvedArgValue: PropTypes.any, }; - -export const ArgForm = compose(...branches)(ArgFormComponent); diff --git a/x-pack/plugins/canvas/public/components/arg_form/arg_label.js b/x-pack/plugins/canvas/public/components/arg_form/arg_label.js index e199019bc27b6..58ad44b4cbf8a 100644 --- a/x-pack/plugins/canvas/public/components/arg_form/arg_label.js +++ b/x-pack/plugins/canvas/public/components/arg_form/arg_label.js @@ -30,21 +30,19 @@ export const ArgLabel = (props) => {
{children}
) : ( - simpleArg && ( - - - {label} - - - } - id={argId} - > - {simpleArg} - - ) + + + {label} + + + } + id={argId} + > + {simpleArg || children} + )}
); diff --git a/x-pack/plugins/canvas/public/components/arg_form/pending_arg_value.js b/x-pack/plugins/canvas/public/components/arg_form/pending_arg_value.js index f933230f39928..38b29c7d80091 100644 --- a/x-pack/plugins/canvas/public/components/arg_form/pending_arg_value.js +++ b/x-pack/plugins/canvas/public/components/arg_form/pending_arg_value.js @@ -47,7 +47,6 @@ export class PendingArgValue extends React.PureComponent { render() { const { label, argTypeInstance } = this.props; - return (
Date: Tue, 10 Aug 2021 10:48:03 +0200 Subject: [PATCH 014/104] [Lens][Accessibility] Take into account background color for non opaque colors (#107877) * :bug: Take into account also background color if opaque * :white_check_mark: Add tests for opaque colors --- .../public/shared_components/coloring/utils.test.ts | 11 +++++++++++ .../lens/public/shared_components/coloring/utils.ts | 7 ++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts b/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts index 3564785aa570e..97dc2e45c96dc 100644 --- a/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts +++ b/x-pack/plugins/lens/public/shared_components/coloring/utils.test.ts @@ -405,4 +405,15 @@ describe('getContrastColor', () => { expect(getContrastColor('#fff', true)).toBe('#000000'); expect(getContrastColor('#fff', false)).toBe('#000000'); }); + + it('should take into account background color if the primary color is opaque', () => { + expect(getContrastColor('rgba(0,0,0,0)', true)).toBe('#ffffff'); + expect(getContrastColor('rgba(0,0,0,0)', false)).toBe('#000000'); + expect(getContrastColor('#00000000', true)).toBe('#ffffff'); + expect(getContrastColor('#00000000', false)).toBe('#000000'); + expect(getContrastColor('#ffffff00', true)).toBe('#ffffff'); + expect(getContrastColor('#ffffff00', false)).toBe('#000000'); + expect(getContrastColor('rgba(255,255,255,0)', true)).toBe('#ffffff'); + expect(getContrastColor('rgba(255,255,255,0)', false)).toBe('#000000'); + }); }); diff --git a/x-pack/plugins/lens/public/shared_components/coloring/utils.ts b/x-pack/plugins/lens/public/shared_components/coloring/utils.ts index 8cd0a6cf49001..b2969565f5390 100644 --- a/x-pack/plugins/lens/public/shared_components/coloring/utils.ts +++ b/x-pack/plugins/lens/public/shared_components/coloring/utils.ts @@ -294,7 +294,12 @@ export function getColorStops( export function getContrastColor(color: string, isDarkTheme: boolean) { const darkColor = isDarkTheme ? euiDarkVars.euiColorInk : euiLightVars.euiColorInk; const lightColor = isDarkTheme ? euiDarkVars.euiColorGhost : euiLightVars.euiColorGhost; - return isColorDark(...chroma(color).rgb()) ? lightColor : darkColor; + const backgroundColor = isDarkTheme + ? euiDarkVars.euiPageBackgroundColor + : euiLightVars.euiPageBackgroundColor; + const finalColor = + chroma(color).alpha() < 1 ? chroma.blend(backgroundColor, color, 'overlay') : chroma(color); + return isColorDark(...finalColor.rgb()) ? lightColor : darkColor; } /** From 78e2bd2788fa4ba9dcfa12725c8b7b8ebae3671f Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 10 Aug 2021 11:56:15 +0200 Subject: [PATCH 015/104] [APM] Make rangeFrom/rangeTo required (#107717) --- .../src/create_router.test.tsx | 44 ++++++++++++ .../src/create_router.ts | 37 +++++++--- .../src/types/index.ts | 2 + .../backend_error_rate_chart.tsx | 7 +- .../backend_latency_chart.tsx | 7 +- .../backend_throughput_chart.tsx | 7 +- .../app/backend_detail_overview/index.tsx | 9 ++- .../app/error_group_details/index.tsx | 5 ++ .../service_dependencies_breakdown_chart.tsx | 5 +- .../service_inventory/service_list/index.tsx | 4 +- .../service_list/service_list.test.tsx | 5 +- .../service_map/Popover/service_contents.tsx | 20 +++++- .../public/components/routing/home/index.tsx | 24 +++++-- .../routing/service_detail/index.tsx | 32 ++++++--- .../public/components/shared/backend_link.tsx | 2 +- .../public/components/shared/service_link.tsx | 2 +- .../apm/public/hooks/use_comparison.ts | 12 +++- .../apm/public/hooks/use_time_range.test.ts | 68 +++++++++++++++++++ .../apm/public/hooks/use_time_range.ts | 39 +++++++++-- 19 files changed, 282 insertions(+), 49 deletions(-) create mode 100644 x-pack/plugins/apm/public/hooks/use_time_range.test.ts diff --git a/packages/kbn-typed-react-router-config/src/create_router.test.tsx b/packages/kbn-typed-react-router-config/src/create_router.test.tsx index 97b6d16e3214e..4de4b44196ddd 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.test.tsx +++ b/packages/kbn-typed-react-router-config/src/create_router.test.tsx @@ -27,6 +27,11 @@ describe('createRouter', () => { rangeTo: t.string, }), }), + defaults: { + query: { + rangeFrom: 'now-30m', + }, + }, children: [ { path: '/services', @@ -164,6 +169,20 @@ describe('createRouter', () => { router.getParams('/service-map', history.location, true); }).not.toThrowError(); }); + + it('applies defaults', () => { + history.push('/services?rangeTo=now&transactionType=request'); + + const topLevelParams = router.getParams('/', history.location); + + expect(topLevelParams).toEqual({ + path: {}, + query: { + rangeFrom: 'now-30m', + rangeTo: 'now', + }, + }); + }); }); describe('matchRoutes', () => { @@ -181,6 +200,19 @@ describe('createRouter', () => { router.matchRoutes('/traces', history.location); }).toThrowError('No matching route found for /traces'); }); + + it('applies defaults', () => { + history.push('/services?rangeTo=now&transactionType=request'); + + const matches = router.matchRoutes('/', history.location); + + expect(matches[1]?.match.params).toEqual({ + query: { + rangeFrom: 'now-30m', + rangeTo: 'now', + }, + }); + }); }); describe('link', () => { @@ -241,5 +273,17 @@ describe('createRouter', () => { } as any); }).toThrowError(); }); + + it('applies defaults', () => { + const href = router.link('/traces', { + // @ts-ignore + query: { + rangeTo: 'now', + aggregationType: 'avg', + }, + }); + + expect(href).toEqual('/traces?aggregationType=avg&rangeFrom=now-30m&rangeTo=now'); + }); }); }); diff --git a/packages/kbn-typed-react-router-config/src/create_router.ts b/packages/kbn-typed-react-router-config/src/create_router.ts index 4a8b89560d516..846808cb798f1 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.ts +++ b/packages/kbn-typed-react-router-config/src/create_router.ts @@ -76,10 +76,12 @@ export function createRouter(routes: TRoutes): Router(routes: TRoutes): Router { const params: { path?: Record; query?: Record } | undefined = args[0]; - const paramsWithDefaults = merge({ path: {}, query: {} }, params); + const paramsWithBuiltInDefaults = merge({ path: {}, query: {} }, params); path = path .split('/') .map((part) => { - return part.startsWith(':') ? paramsWithDefaults.path[part.split(':')[1]] : part; + return part.startsWith(':') ? paramsWithBuiltInDefaults.path[part.split(':')[1]] : part; }) .join('/'); @@ -125,15 +127,25 @@ export function createRouter(routes: TRoutes): Router { + return routesByReactRouterConfig.get(match.route)!; + }); + const validationType = mergeRt( ...(compact( - matches.map((match) => { - return routesByReactRouterConfig.get(match.route)?.params; + matchedRoutes.map((match) => { + return match.params; }) ) as [any, any]) ); - const validation = validationType.decode(paramsWithDefaults); + const paramsWithRouteDefaults = merge( + {}, + ...matchedRoutes.map((route) => route.defaults ?? {}), + paramsWithBuiltInDefaults + ); + + const validation = validationType.decode(paramsWithRouteDefaults); if (isLeft(validation)) { throw new Error(PathReporter.report(validation).join('\n')); @@ -141,7 +153,7 @@ export function createRouter(routes: TRoutes): Router(routes: TRoutes): Router { const matches = matchRoutes(...args); return matches.length - ? merge({ path: {}, query: {} }, ...matches.map((match) => match.match.params)) + ? merge( + { path: {}, query: {} }, + ...matches.map((match) => merge({}, match.route?.defaults ?? {}, match.match.params)) + ) : undefined; }, matchRoutes: (...args: any[]) => { diff --git a/packages/kbn-typed-react-router-config/src/types/index.ts b/packages/kbn-typed-react-router-config/src/types/index.ts index f1609d8709620..1d6a77d360007 100644 --- a/packages/kbn-typed-react-router-config/src/types/index.ts +++ b/packages/kbn-typed-react-router-config/src/types/index.ts @@ -56,6 +56,7 @@ interface PlainRoute { element: ReactElement; children?: PlainRoute[]; params?: t.Type; + defaults?: Record>; } interface ReadonlyPlainRoute { @@ -63,6 +64,7 @@ interface ReadonlyPlainRoute { readonly element: ReactElement; readonly children?: readonly ReadonlyPlainRoute[]; readonly params?: t.Type; + readonly defaults?: Record>; } export type Route = PlainRoute | ReadonlyPlainRoute; diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_error_rate_chart.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_error_rate_chart.tsx index 8dc47dc6549fc..bcdc4aae5ded1 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_error_rate_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_error_rate_chart.tsx @@ -15,6 +15,7 @@ import { useTimeRange } from '../../../hooks/use_time_range'; import { Coordinate, TimeSeries } from '../../../../typings/timeseries'; import { TimeseriesChart } from '../../shared/charts/timeseries_chart'; import { useTheme } from '../../../hooks/use_theme'; +import { useApmParams } from '../../../hooks/use_apm_params'; function yLabelFormat(y?: number | null) { return asPercent(y || 0, 1); @@ -25,7 +26,11 @@ export function BackendErrorRateChart({ height }: { height: number }) { const theme = useTheme(); - const { start, end } = useTimeRange(); + const { + query: { rangeFrom, rangeTo }, + } = useApmParams('/backends/:backendName/overview'); + + const { start, end } = useTimeRange({ rangeFrom, rangeTo }); const { urlParams: { kuery, environment }, diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_latency_chart.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_latency_chart.tsx index 0f855edfae770..472295e69c20a 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_latency_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_latency_chart.tsx @@ -19,13 +19,18 @@ import { getMaxY, getResponseTimeTickFormatter, } from '../../shared/charts/transaction_charts/helper'; +import { useApmParams } from '../../../hooks/use_apm_params'; export function BackendLatencyChart({ height }: { height: number }) { const { backendName } = useApmBackendContext(); const theme = useTheme(); - const { start, end } = useTimeRange(); + const { + query: { rangeFrom, rangeTo }, + } = useApmParams('/backends/:backendName/overview'); + + const { start, end } = useTimeRange({ rangeFrom, rangeTo }); const { urlParams: { kuery, environment }, diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_throughput_chart.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_throughput_chart.tsx index 0b962742051b4..6088d315866db 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_throughput_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_throughput_chart.tsx @@ -15,13 +15,18 @@ import { useTimeRange } from '../../../hooks/use_time_range'; import { Coordinate, TimeSeries } from '../../../../typings/timeseries'; import { TimeseriesChart } from '../../shared/charts/timeseries_chart'; import { useTheme } from '../../../hooks/use_theme'; +import { useApmParams } from '../../../hooks/use_apm_params'; export function BackendThroughputChart({ height }: { height: number }) { const { backendName } = useApmBackendContext(); const theme = useTheme(); - const { start, end } = useTimeRange(); + const { + query: { rangeFrom, rangeTo }, + } = useApmParams('/backends/:backendName/overview'); + + const { start, end } = useTimeRange({ rangeFrom, rangeTo }); const { urlParams: { kuery, environment }, diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx index cef0b4e410c38..fdcb6bdf38e42 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx @@ -27,7 +27,7 @@ import { BackendDetailTemplate } from '../../routing/templates/backend_detail_te export function BackendDetailOverview() { const { path: { backendName }, - query, + query: { rangeFrom, rangeTo }, } = useApmParams('/backends/:backendName/overview'); const apmRouter = useApmRouter(); @@ -35,13 +35,16 @@ export function BackendDetailOverview() { useBreadcrumb([ { title: BackendInventoryTitle, - href: apmRouter.link('/backends'), + href: apmRouter.link('/backends', { query: { rangeFrom, rangeTo } }), }, { title: backendName, href: apmRouter.link('/backends/:backendName/overview', { path: { backendName }, - query, + query: { + rangeFrom, + rangeTo, + }, }), }, ]); diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx index 3b7dea1e64060..cf4cc68865977 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/index.tsx @@ -101,6 +101,7 @@ export function ErrorGroupDetails() { const { path: { groupId }, + query: { rangeFrom, rangeTo }, } = useApmParams('/services/:serviceName/errors/:groupId'); useBreadcrumb({ @@ -110,6 +111,10 @@ export function ErrorGroupDetails() { serviceName, groupId, }, + query: { + rangeFrom, + rangeTo, + }, }), }); diff --git a/x-pack/plugins/apm/public/components/app/service_dependencies/service_dependencies_breakdown_chart.tsx b/x-pack/plugins/apm/public/components/app/service_dependencies/service_dependencies_breakdown_chart.tsx index a33b0db7c4baf..1ce6d54754719 100644 --- a/x-pack/plugins/apm/public/components/app/service_dependencies/service_dependencies_breakdown_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/service_dependencies/service_dependencies_breakdown_chart.tsx @@ -18,13 +18,14 @@ export function ServiceDependenciesBreakdownChart({ }: { height: number; }) { - const { start, end } = useTimeRange(); const { serviceName } = useApmServiceContext(); const { - query: { kuery, environment }, + query: { kuery, environment, rangeFrom, rangeTo }, } = useApmParams('/services/:serviceName/dependencies'); + const { start, end } = useTimeRange({ rangeFrom, rangeTo }); + const { data, status } = useFetcher( (callApmApi) => { return callApmApi({ diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx index c2ba67356fb6b..39e65f7eb15d1 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx @@ -13,6 +13,7 @@ import { EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { TypeOf } from '@kbn/typed-react-router-config'; import { orderBy } from 'lodash'; import React, { useMemo } from 'react'; import { ValuesType } from 'utility-types'; @@ -31,6 +32,7 @@ import { import { useApmParams } from '../../../../hooks/use_apm_params'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { unit } from '../../../../utils/style'; +import { ApmRoutes } from '../../../routing/apm_route_config'; import { EnvironmentBadge } from '../../../shared/EnvironmentBadge'; import { ITableColumn, ManagedTable } from '../../../shared/managed_table'; import { ServiceLink } from '../../../shared/service_link'; @@ -66,7 +68,7 @@ export function getServiceColumns({ showTransactionTypeColumn, comparisonData, }: { - query: Record; + query: TypeOf['query']; showTransactionTypeColumn: boolean; comparisonData?: ServicesDetailedStatisticsAPIResponse; }): Array> { diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/service_list.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/service_list.test.tsx index 70a6191a1d6b4..d6cf7bf018d0f 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/service_list.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/service_list.test.tsx @@ -61,7 +61,10 @@ describe('ServiceList', () => { environments: ['test'], }; const renderedColumns = getServiceColumns({ - query: {}, + query: { + rangeFrom: 'now-15m', + rangeTo: 'now', + }, showTransactionTypeColumn: false, }).map((c) => c.render!(service[c.field!], service)); diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/service_contents.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/service_contents.tsx index b486e5e19fb03..274e25b342d2a 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/service_contents.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/service_contents.tsx @@ -10,6 +10,7 @@ import { EuiButton, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; +import { useApmParams } from '../../../../hooks/use_apm_params'; import type { ContentsProps } from '.'; import { NodeStats } from '../../../../../common/service_map'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; @@ -17,14 +18,29 @@ import { useApmRouter } from '../../../../hooks/use_apm_router'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; import { AnomalyDetection } from './anomaly_detection'; import { StatsList } from './stats_list'; +import { useTimeRange } from '../../../../hooks/use_time_range'; export function ServiceContents({ onFocusClick, nodeData }: ContentsProps) { const apmRouter = useApmRouter(); const { - urlParams: { environment, start, end }, + urlParams: { environment }, } = useUrlParams(); + const { query } = useApmParams('/*'); + + if ( + !('rangeFrom' in query && 'rangeTo' in query) || + !query.rangeFrom || + !query.rangeTo + ) { + throw new Error('Expected rangeFrom and rangeTo to be set'); + } + + const { rangeFrom, rangeTo } = query; + + const { start, end } = useTimeRange({ rangeFrom, rangeTo }); + const serviceName = nodeData.id!; const { data = { transactionStats: {} } as NodeStats, status } = useFetcher( @@ -49,10 +65,12 @@ export function ServiceContents({ onFocusClick, nodeData }: ContentsProps) { const detailsUrl = apmRouter.link('/services/:serviceName', { path: { serviceName }, + query: { rangeFrom, rangeTo }, }); const focusUrl = apmRouter.link('/services/:serviceName/service-map', { path: { serviceName }, + query: { rangeFrom, rangeTo }, }); const { serviceAnomalyStats } = nodeData; diff --git a/x-pack/plugins/apm/public/components/routing/home/index.tsx b/x-pack/plugins/apm/public/components/routing/home/index.tsx index 10f88d3e8b4cc..1502038b378b2 100644 --- a/x-pack/plugins/apm/public/components/routing/home/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/home/index.tsx @@ -53,14 +53,24 @@ export const BackendInventoryTitle = i18n.translate( export const home = { path: '/', element: , - params: t.partial({ - query: t.partial({ - rangeFrom: t.string, - rangeTo: t.string, - environment: t.string, - kuery: t.string, - }), + params: t.type({ + query: t.intersection([ + t.partial({ + environment: t.string, + kuery: t.string, + }), + t.type({ + rangeFrom: t.string, + rangeTo: t.string, + }), + ]), }), + defaults: { + query: { + rangeFrom: 'now-15m', + rangeTo: 'now', + }, + }, children: [ page({ path: '/services', diff --git a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx index 5771d21b77a3a..9716dd01561e5 100644 --- a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx @@ -66,19 +66,29 @@ export const serviceDetail = { serviceName: t.string, }), }), - t.partial({ - query: t.partial({ - environment: t.string, - rangeFrom: t.string, - rangeTo: t.string, - comparisonEnabled: t.string, - comparisonType: t.string, - latencyAggregationType: t.string, - transactionType: t.string, - kuery: t.string, - }), + t.type({ + query: t.intersection([ + t.type({ + rangeFrom: t.string, + rangeTo: t.string, + }), + t.partial({ + environment: t.string, + comparisonEnabled: t.string, + comparisonType: t.string, + latencyAggregationType: t.string, + transactionType: t.string, + kuery: t.string, + }), + ]), }), ]), + defaults: { + query: { + rangeFrom: 'now-15m', + rangeTo: 'now', + }, + }, children: [ page({ path: '/overview', diff --git a/x-pack/plugins/apm/public/components/shared/backend_link.tsx b/x-pack/plugins/apm/public/components/shared/backend_link.tsx index 069631b9f17ac..84ce37391b369 100644 --- a/x-pack/plugins/apm/public/components/shared/backend_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/backend_link.tsx @@ -18,7 +18,7 @@ const StyledLink = euiStyled(EuiLink)`${truncate('100%')};`; interface BackendLinkProps { backendName: string; - query?: TypeOf['query']; + query: TypeOf['query']; subtype?: string; type?: string; } diff --git a/x-pack/plugins/apm/public/components/shared/service_link.tsx b/x-pack/plugins/apm/public/components/shared/service_link.tsx index d61f55fe53cf0..d79243315c773 100644 --- a/x-pack/plugins/apm/public/components/shared/service_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/service_link.tsx @@ -19,7 +19,7 @@ const StyledLink = euiStyled(EuiLink)`${truncate('100%')};`; interface ServiceLinkProps { agentName?: AgentName; - query?: TypeOf['query']; + query: TypeOf['query']; serviceName: string; } diff --git a/x-pack/plugins/apm/public/hooks/use_comparison.ts b/x-pack/plugins/apm/public/hooks/use_comparison.ts index b84942657dc9d..2875d973a5e18 100644 --- a/x-pack/plugins/apm/public/hooks/use_comparison.ts +++ b/x-pack/plugins/apm/public/hooks/use_comparison.ts @@ -10,6 +10,7 @@ import { getTimeRangeComparison, } from '../components/shared/time_comparison/get_time_range_comparison'; import { useUrlParams } from '../context/url_params_context/use_url_params'; +import { useApmParams } from './use_apm_params'; import { useTheme } from './use_theme'; import { useTimeRange } from './use_time_range'; @@ -17,7 +18,16 @@ export function useComparison() { const theme = useTheme(); const comparisonChartTheme = getComparisonChartTheme(theme); - const { start, end } = useTimeRange(); + const { query } = useApmParams('/*'); + + if (!('rangeFrom' in query && 'rangeTo' in query)) { + throw new Error('rangeFrom or rangeTo not defined in query'); + } + + const { start, end } = useTimeRange({ + rangeFrom: query.rangeFrom, + rangeTo: query.rangeTo, + }); const { urlParams: { comparisonType, comparisonEnabled }, diff --git a/x-pack/plugins/apm/public/hooks/use_time_range.test.ts b/x-pack/plugins/apm/public/hooks/use_time_range.test.ts new file mode 100644 index 0000000000000..dbdd7de171650 --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_time_range.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + act, + renderHook, + RenderHookResult, +} from '@testing-library/react-hooks'; +import { useTimeRange } from './use_time_range'; + +describe('useTimeRange', () => { + let hook: RenderHookResult< + Parameters[0], + ReturnType + >; + + beforeEach(() => { + Date.now = jest.fn(() => new Date(Date.UTC(2021, 0, 1, 12)).valueOf()); + + hook = renderHook( + (props) => { + const { rangeFrom, rangeTo } = props; + return useTimeRange({ rangeFrom, rangeTo }); + }, + { initialProps: { rangeFrom: 'now-15m', rangeTo: 'now' } } + ); + }); + + afterEach(() => {}); + + it('returns the parsed range on first render', () => { + expect(hook.result.current.start).toEqual('2021-01-01T11:45:00.000Z'); + expect(hook.result.current.end).toEqual('2021-01-01T12:00:00.000Z'); + }); + + it('only changes the parsed range when rangeFrom/rangeTo change', () => { + Date.now = jest.fn(() => new Date(Date.UTC(2021, 0, 1, 13)).valueOf()); + + hook.rerender({ rangeFrom: 'now-15m', rangeTo: 'now' }); + + expect(hook.result.current.start).toEqual('2021-01-01T11:45:00.000Z'); + expect(hook.result.current.end).toEqual('2021-01-01T12:00:00.000Z'); + + hook.rerender({ rangeFrom: 'now-30m', rangeTo: 'now' }); + + expect(hook.result.current.start).toEqual('2021-01-01T12:30:00.000Z'); + expect(hook.result.current.end).toEqual('2021-01-01T13:00:00.000Z'); + }); + + it('updates when refreshTimeRange is called', async () => { + Date.now = jest.fn(() => new Date(Date.UTC(2021, 0, 1, 13)).valueOf()); + + hook.rerender({ rangeFrom: 'now-15m', rangeTo: 'now' }); + + expect(hook.result.current.start).toEqual('2021-01-01T11:45:00.000Z'); + expect(hook.result.current.end).toEqual('2021-01-01T12:00:00.000Z'); + + act(() => { + hook.result.current.refreshTimeRange(); + }); + + expect(hook.result.current.start).toEqual('2021-01-01T12:45:00.000Z'); + expect(hook.result.current.end).toEqual('2021-01-01T13:00:00.000Z'); + }); +}); diff --git a/x-pack/plugins/apm/public/hooks/use_time_range.ts b/x-pack/plugins/apm/public/hooks/use_time_range.ts index 7afdfb7db6a04..8263767a402dd 100644 --- a/x-pack/plugins/apm/public/hooks/use_time_range.ts +++ b/x-pack/plugins/apm/public/hooks/use_time_range.ts @@ -5,19 +5,46 @@ * 2.0. */ -import { useUrlParams } from '../context/url_params_context/use_url_params'; +import { isEqual } from 'lodash'; +import { useCallback, useRef, useState } from 'react'; +import { getDateRange } from '../context/url_params_context/helpers'; -export function useTimeRange() { - const { - urlParams: { start, end }, - } = useUrlParams(); +export function useTimeRange({ + rangeFrom, + rangeTo, +}: { + rangeFrom: string; + rangeTo: string; +}) { + const rangeRef = useRef({ rangeFrom, rangeTo }); + + const [timeRangeId, setTimeRangeId] = useState(0); + + const stateRef = useRef(getDateRange({ state: {}, rangeFrom, rangeTo })); + + const updateParsedTime = useCallback(() => { + stateRef.current = getDateRange({ state: {}, rangeFrom, rangeTo }); + }, [rangeFrom, rangeTo]); + + if (!isEqual(rangeRef.current, { rangeFrom, rangeTo })) { + updateParsedTime(); + } + + const { start, end } = stateRef.current; + + const refreshTimeRange = useCallback(() => { + updateParsedTime(); + setTimeRangeId((id) => id + 1); + }, [setTimeRangeId, updateParsedTime]); if (!start || !end) { - throw new Error('Time range not set'); + throw new Error('start and/or end were unexpectedly not set'); } return { start, end, + refreshTimeRange, + timeRangeId, }; } From eca7146a9f60e6b42442065e372e4a1b92039176 Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Tue, 10 Aug 2021 12:31:05 +0200 Subject: [PATCH 016/104] [RAC] [Alerts] Add highlight to building block alerts (#107727) * [rac] [Alerts] Add highlight to building block alerts * Pull back 'additional filters' button to t-grid --- .../common/components/events_viewer/index.tsx | 5 +- .../alerts_table/alerts_utility_bar/index.tsx | 124 +++++++++------- .../components/alerts_table/index.tsx | 13 +- .../components/t_grid/body/helpers.test.tsx | 32 +++++ .../public/components/t_grid/body/helpers.tsx | 23 +++ .../public/components/t_grid/body/index.tsx | 135 ++++++++++-------- .../components/t_grid/integrated/index.tsx | 12 +- 7 files changed, 225 insertions(+), 119 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 8a8ebd18174be..fe6c7e85e175d 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -62,6 +62,7 @@ export interface OwnProps { renderCellValue: (props: CellValueElementProps) => React.ReactNode; rowRenderers: RowRenderer[]; utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; + additionalFilters?: React.ReactNode; } type Props = OwnProps & PropsFromRedux; @@ -98,6 +99,7 @@ const StatefulEventsViewerComponent: React.FC = ({ showCheckboxes, sort, utilityBar, + additionalFilters, // If truthy, the graph viewer (Resolver) is showing graphEventId, }) => { @@ -165,7 +167,7 @@ const StatefulEventsViewerComponent: React.FC = ({ setGlobalFullScreen, start, sort, - utilityBar, + additionalFilters, graphEventId, filterStatus: currentFilter, leadingControlColumns, @@ -291,6 +293,7 @@ export const StatefulEventsViewer = connector( prevProps.showCheckboxes === nextProps.showCheckboxes && prevProps.start === nextProps.start && prevProps.utilityBar === nextProps.utilityBar && + prevProps.additionalFilters === nextProps.additionalFilters && prevProps.graphEventId === nextProps.graphEventId ) ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx index 1ef79a64f831e..8a88c430b03e9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.tsx @@ -50,14 +50,12 @@ const UtilityBarFlexGroup = styled(EuiFlexGroup)` min-width: 175px; `; -const BuildingBlockContainer = styled(EuiFlexItem)` - background: repeating-linear-gradient( - 127deg, - rgba(245, 167, 0, 0.2), - rgba(245, 167, 0, 0.2) 1px, - rgba(245, 167, 0, 0.05) 2px, - rgba(245, 167, 0, 0.05) 10px - ); +const AdditionalFiltersItem = styled(EuiFlexItem)` + padding: ${({ theme }) => theme.eui.paddingSizes.s}; +`; + +const BuildingBlockContainer = styled(AdditionalFiltersItem)` + background: ${({ theme }) => theme.eui.euiColorHighlight}; `; const AlertsUtilityBarComponent: React.FC = ({ @@ -146,39 +144,6 @@ const AlertsUtilityBarComponent: React.FC = ({ ); - const UtilityBarAdditionalFiltersContent = (closePopover: () => void) => ( - - - ) => { - closePopover(); - onShowBuildingBlockAlertsChanged(e.target.checked); - }} - checked={showBuildingBlockAlerts} - color="text" - data-test-subj="showBuildingBlockAlertsCheckbox" - label={i18n.ADDITIONAL_FILTERS_ACTIONS_SHOW_BUILDING_BLOCK} - /> - - - ) => { - closePopover(); - onShowOnlyThreatIndicatorAlertsChanged(e.target.checked); - }} - checked={showOnlyThreatIndicatorAlerts} - color="text" - data-test-subj="showOnlyThreatIndicatorAlertsCheckbox" - label={i18n.ADDITIONAL_FILTERS_ACTIONS_SHOW_ONLY_THREAT_INDICATOR_ALERTS} - /> - - - ); - const handleSelectAllAlertsClick = useCallback(() => { if (!showClearSelection) { selectAll(); @@ -233,16 +198,13 @@ const AlertsUtilityBarComponent: React.FC = ({ )} - - {i18n.ADDITIONAL_FILTERS_ACTIONS} - + @@ -260,3 +222,63 @@ export const AlertsUtilityBar = React.memo( prevProps.showBuildingBlockAlerts === nextProps.showBuildingBlockAlerts && prevProps.showOnlyThreatIndicatorAlerts === nextProps.showOnlyThreatIndicatorAlerts ); + +export const AditionalFiltersAction = ({ + areEventsLoading, + onShowBuildingBlockAlertsChanged, + showBuildingBlockAlerts, + onShowOnlyThreatIndicatorAlertsChanged, + showOnlyThreatIndicatorAlerts, +}: { + areEventsLoading: boolean; + onShowBuildingBlockAlertsChanged: (showBuildingBlockAlerts: boolean) => void; + showBuildingBlockAlerts: boolean; + onShowOnlyThreatIndicatorAlertsChanged: (showOnlyThreatIndicatorAlerts: boolean) => void; + showOnlyThreatIndicatorAlerts: boolean; +}) => { + const UtilityBarAdditionalFiltersContent = (closePopover: () => void) => ( + + + ) => { + closePopover(); + onShowBuildingBlockAlertsChanged(e.target.checked); + }} + checked={showBuildingBlockAlerts} + color="text" + data-test-subj="showBuildingBlockAlertsCheckbox" + label={i18n.ADDITIONAL_FILTERS_ACTIONS_SHOW_BUILDING_BLOCK} + /> + + + ) => { + closePopover(); + onShowOnlyThreatIndicatorAlertsChanged(e.target.checked); + }} + checked={showOnlyThreatIndicatorAlerts} + color="text" + data-test-subj="showOnlyThreatIndicatorAlertsCheckbox" + label={i18n.ADDITIONAL_FILTERS_ACTIONS_SHOW_ONLY_THREAT_INDICATOR_ALERTS} + /> + + + ); + + return ( + + {i18n.ADDITIONAL_FILTERS_ACTIONS} + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 40b6a21ea3e68..d8d6424ef2a73 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -31,7 +31,7 @@ import { alertsDefaultModelRuleRegistry, buildAlertStatusFilterRuleRegistry, } from './default_config'; -import { AlertsUtilityBar } from './alerts_utility_bar'; +import { AditionalFiltersAction, AlertsUtilityBar } from './alerts_utility_bar'; import * as i18nCommon from '../../../common/translations'; import * as i18n from './translations'; import { @@ -313,6 +313,16 @@ export const AlertsTableComponent: React.FC = ({ ] ); + const additionalFiltersComponent = ( + 0} + onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChanged} + showBuildingBlockAlerts={showBuildingBlockAlerts} + onShowOnlyThreatIndicatorAlertsChanged={onShowOnlyThreatIndicatorAlertsChanged} + showOnlyThreatIndicatorAlerts={showOnlyThreatIndicatorAlerts} + /> + ); + const defaultFiltersMemo = useMemo(() => { // TODO: Once we are past experimental phase this code should be removed const alertStatusFilter = ruleRegistryEnabled @@ -382,6 +392,7 @@ export const AlertsTableComponent: React.FC = ({ scopeId={SourcererScopeName.detections} start={from} utilityBar={utilityBarCallback} + additionalFilters={additionalFiltersComponent} /> ); }; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx index fe9c5ea2bc332..2b58487fce53a 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx @@ -14,8 +14,12 @@ import { mapSortDirectionToDirection, mapSortingColumns, stringifyEvent, + addBuildingBlockStyle, } from './helpers'; +import { euiThemeVars } from '@kbn/ui-shared-deps/theme'; +import { mockDnsEvent } from '../../../mock'; + describe('helpers', () => { describe('stringifyEvent', () => { test('it omits __typename when it appears at arbitrary levels', () => { @@ -388,4 +392,32 @@ describe('helpers', () => { ).toBe(false); }); }); + + describe('addBuildingBlockStyle', () => { + const THEME = { eui: euiThemeVars, darkMode: false }; + + test('it calls `setCellProps` with background color when event is a building block', () => { + const mockedSetCellProps = jest.fn(); + const ecs = { + ...mockDnsEvent, + ...{ signal: { rule: { building_block_type: ['default'] } } }, + }; + + addBuildingBlockStyle(ecs, THEME, mockedSetCellProps); + + expect(mockedSetCellProps).toBeCalledWith({ + style: { + backgroundColor: euiThemeVars.euiColorHighlight, + }, + }); + }); + + test('it call `setCellProps` reseting the background color when event is not a building block', () => { + const mockedSetCellProps = jest.fn(); + + addBuildingBlockStyle(mockDnsEvent, THEME, mockedSetCellProps); + + expect(mockedSetCellProps).toBeCalledWith({ style: { backgroundColor: 'inherit' } }); + }); + }); }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx index fb50d5ebabb8c..790414314ecdd 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx @@ -7,6 +7,7 @@ import { isEmpty } from 'lodash/fp'; +import { EuiDataGridCellValueElementProps } from '@elastic/eui'; import type { Ecs } from '../../../../common/ecs'; import type { BrowserField, @@ -20,6 +21,8 @@ import type { TimelineEventsType, } from '../../../../common/types/timeline'; +import type { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any export const omitTypenameAndEmpty = (k: string, v: any): any | undefined => k !== '__typename' && v != null ? v : undefined; @@ -185,3 +188,23 @@ export const allowSorting = ({ return isAllowlistedNonBrowserField || isAggregatable; }; +export const addBuildingBlockStyle = ( + ecs: Ecs, + theme: EuiTheme, + setCellProps: EuiDataGridCellValueElementProps['setCellProps'] +) => { + if (isEventBuildingBlockType(ecs)) { + setCellProps({ + style: { + backgroundColor: `${theme.eui.euiColorHighlight}`, + }, + }); + } else { + // reset cell style + setCellProps({ + style: { + backgroundColor: 'inherit', + }, + }); + } +}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx index 00f9d513a1a16..cc94f901446a7 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx @@ -24,28 +24,34 @@ import React, { useEffect, useMemo, useState, + useContext, } from 'react'; + import { connect, ConnectedProps, useDispatch } from 'react-redux'; +import { ThemeContext } from 'styled-components'; import { TGridCellAction, - TimelineId, - TimelineTabs, BulkActionsProp, - SortColumnTimeline, -} from '../../../../common/types/timeline'; - -import type { CellValueElementProps, ColumnHeaderOptions, ControlColumnProps, RowRenderer, AlertStatus, + SortColumnTimeline, + TimelineId, + TimelineTabs, } from '../../../../common/types/timeline'; + import type { TimelineItem, TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; import { getActionsColumnWidth, getColumnHeaders } from './column_headers/helpers'; -import { getEventIdToDataMapping, mapSortDirectionToDirection, mapSortingColumns } from './helpers'; +import { + addBuildingBlockStyle, + getEventIdToDataMapping, + mapSortDirectionToDirection, + mapSortingColumns, +} from './helpers'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers'; import type { BrowserFields } from '../../../../common/search_strategy/index_fields'; @@ -58,6 +64,7 @@ import { RowAction } from './row_action'; import * as i18n from './translations'; import { AlertCount } from '../styles'; import { checkBoxControlColumn } from './control_columns'; +import type { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common'; const StatefulAlertStatusBulkActions = lazy( () => import('../toolbar/bulk_actions/alert_status_bulk_actions') @@ -121,6 +128,7 @@ const transformControlColumns = ({ onSelectPage, browserFields, sort, + theme, }: { actionColumnsWidth: number; columnHeaders: ColumnHeaderOptions[]; @@ -138,6 +146,7 @@ const transformControlColumns = ({ browserFields: BrowserFields; onSelectPage: OnSelectAll; sort: SortColumnTimeline[]; + theme: EuiTheme; }): EuiDataGridControlColumn[] => controlColumns.map( ({ id: columnId, headerCellRender = EmptyHeaderCellRender, rowCellRender, width }, i) => ({ @@ -173,29 +182,33 @@ const transformControlColumns = ({ isExpanded, rowIndex, setCellProps, - }: EuiDataGridCellValueElementProps) => ( - - ), + }: EuiDataGridCellValueElementProps) => { + addBuildingBlockStyle(data[rowIndex].ecs, theme, setCellProps); + + return ( + + ); + }, width: width ?? actionColumnsWidth, }) ); @@ -252,6 +265,7 @@ export const BodyComponent = React.memo( const selectedCount = useMemo(() => Object.keys(selectedEventIds).length, [selectedEventIds]); + const theme: EuiTheme = useContext(ThemeContext); const onRowSelected: OnRowSelected = useCallback( ({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => { setSelected({ @@ -444,6 +458,7 @@ export const BodyComponent = React.memo( sort, browserFields, onSelectPage, + theme, }) ); }, [ @@ -463,6 +478,7 @@ export const BodyComponent = React.memo( browserFields, onSelectPage, sort, + theme, ]); const columnsWithCellActions: EuiDataGridColumn[] = useMemo( @@ -483,34 +499,37 @@ export const BodyComponent = React.memo( [browserFields, columnHeaders, data, defaultCellActions] ); - const renderTGridCellValue: (x: EuiDataGridCellValueElementProps) => React.ReactNode = ({ - columnId, - rowIndex, - setCellProps, - }) => { - const rowData = rowIndex < data.length ? data[rowIndex].data : null; - const header = columnHeaders.find((h) => h.id === columnId); - const eventId = rowIndex < data.length ? data[rowIndex]._id : null; - - if (rowData == null || header == null || eventId == null) { - return null; - } - - return renderCellValue({ - columnId: header.id, - eventId, - data: rowData, - header, - isDraggable: false, - isExpandable: true, - isExpanded: false, - isDetails: false, - linkValues: getOr([], header.linkField ?? '', data[rowIndex].ecs), - rowIndex, - setCellProps, - timelineId: tabType != null ? `${id}-${tabType}` : id, - }); - }; + const renderTGridCellValue: ( + x: EuiDataGridCellValueElementProps + ) => React.ReactNode = useCallback( + ({ columnId, rowIndex, setCellProps }) => { + const rowData = rowIndex < data.length ? data[rowIndex].data : null; + const header = columnHeaders.find((h) => h.id === columnId); + const eventId = rowIndex < data.length ? data[rowIndex]._id : null; + + addBuildingBlockStyle(data[rowIndex].ecs, theme, setCellProps); + + if (rowData == null || header == null || eventId == null) { + return null; + } + + return renderCellValue({ + columnId: header.id, + eventId, + data: rowData, + header, + isDraggable: false, + isExpandable: true, + isExpanded: false, + isDetails: false, + linkValues: getOr([], header.linkField ?? '', data[rowIndex].ecs), + rowIndex, + setCellProps, + timelineId: tabType != null ? `${id}-${tabType}` : id, + }); + }, + [columnHeaders, data, id, renderCellValue, tabType, theme] + ); return ( void; start: string; sort: Sort[]; - utilityBar?: (refetch: Refetch, totalCount: number) => React.ReactNode; + additionalFilters: React.ReactNode; // If truthy, the graph viewer (Resolver) is showing graphEventId: string | undefined; leadingControlColumns?: ControlColumnProps[]; @@ -167,7 +165,7 @@ const TGridIntegratedComponent: React.FC = ({ setGlobalFullScreen, start, sort, - utilityBar, + additionalFilters, graphEventId, leadingControlColumns, trailingControlColumns, @@ -239,7 +237,7 @@ const TGridIntegratedComponent: React.FC = ({ const [ loading, - { events, updatedAt, loadPage, pageInfo, refetch, totalCount = 0, inspect }, + { events, loadPage, pageInfo, refetch, totalCount = 0, inspect }, ] = useTimelineEvents({ alertConsumers: SECURITY_ALERTS_CONSUMERS, docValueFields, @@ -294,19 +292,17 @@ const TGridIntegratedComponent: React.FC = ({ height={ headerFilterGroup == null ? COMPACT_HEADER_HEIGHT : EVENTS_VIEWER_HEADER_HEIGHT } - subtitle={utilityBar} title={globalFullScreen ? titleWithExitFullScreen : justTitle} > {HeaderSectionContent} - - + {!resolverIsShowing(graphEventId) && additionalFilters} From 204efae5bfff81632a7e34a1bf1e009f5026edfa Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Tue, 10 Aug 2021 13:03:48 +0200 Subject: [PATCH 017/104] [Data cleanup] unify serializable state (#107745) * Use Serializable from package * Rename to align with core * fix * more replacements * docssss * fix * Move it to @kbn/utility-types and remove core export * buildy build * tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- api_docs/charts.json | 4 +- api_docs/dashboard.json | 12 +- api_docs/data.json | 172 +++++----- api_docs/data_index_patterns.json | 2 +- api_docs/data_query.json | 8 +- api_docs/data_search.json | 164 ++++----- api_docs/discover.json | 12 +- api_docs/embeddable.json | 32 +- api_docs/expression_repeat_image.json | 2 +- api_docs/expression_shape.json | 4 +- api_docs/expressions.json | 320 +++++++++--------- api_docs/index_lifecycle_management.json | 4 +- api_docs/kibana_utils.json | 44 +-- api_docs/lens.json | 4 +- api_docs/presentation_util.json | 2 +- api_docs/share.json | 8 +- api_docs/ui_actions_enhanced.json | 36 +- api_docs/visualizations.json | 14 +- ...ublic.aggconfig.toserializedfieldformat.md | 4 +- ...ns-data-public.aggconfigs._constructor_.md | 4 +- ...lugin-plugins-data-public.filtermanager.md | 2 +- ...ins-data-public.filtermanager.telemetry.md | 2 +- ...bana-plugin-plugins-data-server.eskuery.md | 2 +- ...ugins-embeddable-public.embeddableinput.md | 2 +- ...le-public.enhancementregistrydefinition.md | 2 +- ...le-server.enhancementregistrydefinition.md | 2 +- ...ins-expressions-public.executioncontext.md | 2 +- ...s-expressions-public.expressionfunction.md | 2 +- ...ns-public.expressionfunction.migrations.md | 2 +- ...expressions-public.expressionvalueerror.md | 2 +- ...ressions-public.iexpressionloaderparams.md | 2 +- ...c.iexpressionloaderparams.searchcontext.md | 2 +- ...ins-expressions-server.executioncontext.md | 2 +- ...s-expressions-server.expressionfunction.md | 2 +- ...ns-server.expressionfunction.migrations.md | 2 +- ...expressions-server.expressionvalueerror.md | 2 +- examples/locator_examples/public/locator.ts | 7 +- package.json | 1 - packages/BUILD.bazel | 1 - packages/kbn-common-utils/.babelrc | 3 - packages/kbn-common-utils/BUILD.bazel | 89 ----- packages/kbn-common-utils/README.md | 3 - packages/kbn-common-utils/jest.config.js | 13 - packages/kbn-common-utils/package.json | 8 - packages/kbn-common-utils/src/index.ts | 9 - packages/kbn-common-utils/src/json/index.ts | 9 - .../kbn-common-utils/src/json/typed_json.ts | 16 - packages/kbn-common-utils/tsconfig.json | 19 -- packages/kbn-es-query/BUILD.bazel | 4 +- packages/kbn-es-query/src/kuery/ast/ast.ts | 2 +- .../src/kuery/node_types/named_arg.ts | 2 +- .../src/kuery/node_types/types.ts | 2 +- packages/kbn-utility-types/BUILD.bazel | 1 + packages/kbn-utility-types/src/index.ts | 8 + .../src/serializable/index.ts | 10 + src/core/types/index.ts | 1 - src/plugins/dashboard/public/locator.ts | 8 +- .../dashboard_migrations.test.ts | 4 +- .../saved_objects/dashboard_migrations.ts | 6 +- .../data/common/query/persistable_state.ts | 4 +- .../data/common/search/aggs/agg_config.ts | 16 +- .../search/aggs/buckets/histogram.test.ts | 4 +- src/plugins/data/public/public.api.md | 14 +- .../query/filter_manager/filter_manager.ts | 6 +- src/plugins/data/server/server.api.md | 3 +- src/plugins/discover/public/locator.test.ts | 4 +- src/plugins/discover/public/locator.ts | 8 +- src/plugins/embeddable/common/lib/extract.ts | 4 +- src/plugins/embeddable/common/lib/inject.ts | 4 +- src/plugins/embeddable/common/lib/migrate.ts | 12 +- src/plugins/embeddable/common/types.ts | 6 +- src/plugins/embeddable/public/plugin.tsx | 11 +- src/plugins/embeddable/public/public.api.md | 10 +- src/plugins/embeddable/public/types.ts | 11 +- src/plugins/embeddable/server/plugin.ts | 6 +- src/plugins/embeddable/server/server.api.md | 4 +- src/plugins/embeddable/server/types.ts | 11 +- .../expressions/common/execution/types.ts | 5 +- .../expressions/common/executor/executor.ts | 4 +- .../expression_function.ts | 5 +- .../expression_types/specs/datatable.ts | 10 +- .../common/expression_types/specs/error.ts | 4 +- .../common/service/expressions_services.ts | 9 +- src/plugins/expressions/public/public.api.md | 11 +- src/plugins/expressions/public/types/index.ts | 5 +- src/plugins/expressions/server/server.api.md | 9 +- .../merge_migration_function_map.ts | 5 +- .../migrate_to_latest.test.ts | 9 +- .../persistable_state/migrate_to_latest.ts | 5 +- .../common/persistable_state/types.ts | 27 +- .../kibana_utils/public/ui/configurable.ts | 6 +- src/plugins/management/common/locator.ts | 4 +- .../common/url_service/__tests__/setup.ts | 4 +- .../common/url_service/locators/locator.ts | 5 +- .../url_service/locators/locator_client.ts | 6 +- .../common/url_service/locators/types.ts | 11 +- .../url_service/locators/use_locator_url.ts | 4 +- src/plugins/share/public/mocks.ts | 4 +- .../url_service/redirect/redirect_manager.ts | 4 +- .../redirect/util/parse_search_params.ts | 4 +- src/plugins/ui_actions/public/public.api.md | 1 + .../visualize_embeddable_factory.ts | 14 +- .../app1_hello_world_drilldown.tsx | 4 +- .../alerting/common/alert_navigation.ts | 2 +- .../public/alert_navigation_registry/types.ts | 2 +- .../authorization/alerting_authorization.ts | 2 +- .../alerting_authorization_kuery.ts | 2 +- .../server/routes/lib/rewrite_request_case.ts | 2 +- .../functions/common/csv.test.ts | 17 +- .../functions/common/dropdown_control.test.ts | 36 +- .../embeddables/embeddable_action_storage.ts | 4 +- x-pack/plugins/global_search/common/types.ts | 2 +- .../graph/public/types/workspace_state.ts | 2 +- .../public/locator.ts | 4 +- x-pack/plugins/infra/common/typed_json.ts | 2 +- .../components/log_stream/log_stream.tsx | 2 +- .../logging/log_text_stream/field_value.tsx | 2 +- .../log_entry_field_column.tsx | 2 +- .../utils/log_column_render_configuration.tsx | 2 +- .../lib/adapters/framework/adapter_types.ts | 2 +- .../log_entries/kibana_log_entries_adapter.ts | 2 +- .../log_entries_domain/log_entries_domain.ts | 2 +- .../snapshot/lib/get_metrics_aggregations.ts | 2 +- .../services/log_entries/message/message.ts | 2 +- .../log_entries/message/rule_types.ts | 2 +- .../infra/server/utils/serialized_query.ts | 2 +- .../server/utils/typed_search_strategy.ts | 2 +- .../ingest_pipelines/public/locator.ts | 4 +- .../embeddable/lens_embeddable_factory.ts | 6 +- x-pack/plugins/maps/public/locators.test.ts | 4 +- x-pack/plugins/maps/public/locators.ts | 18 +- .../visualize_geo_field_action.ts | 4 +- .../maps/server/embeddable_migrations.ts | 6 +- x-pack/plugins/ml/common/types/es_client.ts | 2 +- x-pack/plugins/ml/common/types/locator.ts | 4 +- x-pack/plugins/osquery/common/typed_json.ts | 2 +- .../public/management/ilm_policy_link.tsx | 4 +- .../reporting/public/shared_imports.ts | 2 +- .../security_solution/common/typed_json.ts | 2 +- .../public/common/lib/keury/index.ts | 2 +- .../routes/resolver/queries/events.ts | 2 +- .../resolver/tree/queries/descendants.ts | 2 +- .../routes/resolver/tree/queries/lifecycle.ts | 2 +- .../routes/resolver/tree/queries/stats.ts | 2 +- .../routes/resolver/utils/pagination.ts | 2 +- .../server/utils/serialized_query.ts | 2 +- .../server/lib/calculate_health_status.ts | 2 +- .../server/monitoring/capacity_estimation.ts | 2 +- .../monitoring/ephemeral_task_statistics.ts | 2 +- .../monitoring_stats_stream.test.ts | 2 +- .../monitoring/monitoring_stats_stream.ts | 2 +- .../runtime_statistics_aggregator.ts | 2 +- .../server/monitoring/task_run_calcultors.ts | 2 +- .../server/monitoring/task_run_statistics.ts | 2 +- .../server/monitoring/workload_statistics.ts | 2 +- .../timeline/events/all/index.ts | 2 +- .../timeline/events/details/index.ts | 2 +- x-pack/plugins/timelines/common/typed_json.ts | 2 +- .../public/components/utils/keury/index.ts | 2 +- .../details/query.events_details.dsl.ts | 2 +- .../ui_actions_enhanced/common/types.ts | 4 +- .../state/drilldown_manager_state.ts | 6 +- .../dynamic_action_enhancement.ts | 10 +- .../server/dynamic_action_enhancement.ts | 8 +- .../uptime/server/lib/alerts/status_check.ts | 2 +- .../server/lib/requests/get_monitor_status.ts | 2 +- .../apis/security_solution/events.ts | 2 +- .../apis/security_solution/utils.ts | 2 +- .../tests/services/annotations.ts | 2 +- .../basic/tests/annotations.ts | 2 +- .../trial/tests/annotations.ts | 2 +- .../apis/resolver/events.ts | 2 +- .../security_and_spaces/tests/basic/events.ts | 2 +- .../security_and_spaces/tests/trial/events.ts | 2 +- .../security_only/tests/basic/events.ts | 2 +- .../security_only/tests/trial/events.ts | 2 +- .../test/timeline/spaces_only/tests/events.ts | 2 +- yarn.lock | 4 - 178 files changed, 759 insertions(+), 927 deletions(-) delete mode 100644 packages/kbn-common-utils/.babelrc delete mode 100644 packages/kbn-common-utils/BUILD.bazel delete mode 100644 packages/kbn-common-utils/README.md delete mode 100644 packages/kbn-common-utils/jest.config.js delete mode 100644 packages/kbn-common-utils/package.json delete mode 100644 packages/kbn-common-utils/src/index.ts delete mode 100644 packages/kbn-common-utils/src/json/index.ts delete mode 100644 packages/kbn-common-utils/src/json/typed_json.ts delete mode 100644 packages/kbn-common-utils/tsconfig.json rename src/core/types/serializable.ts => packages/kbn-utility-types/src/serializable/index.ts (74%) diff --git a/api_docs/charts.json b/api_docs/charts.json index 177a63556d59b..7081f410ee8af 100644 --- a/api_docs/charts.json +++ b/api_docs/charts.json @@ -2539,7 +2539,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/charts/common/palette.ts", @@ -2597,7 +2597,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/charts/common/palette.ts", diff --git a/api_docs/dashboard.json b/api_docs/dashboard.json index 656364b835af5..cf504e4452a1a 100644 --- a/api_docs/dashboard.json +++ b/api_docs/dashboard.json @@ -1137,8 +1137,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" } ], "path": "src/plugins/dashboard/public/locator.ts", @@ -1204,8 +1204,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ") | undefined" ], @@ -1334,8 +1334,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ") | undefined" ], diff --git a/api_docs/data.json b/api_docs/data.json index e4f94168f50be..0456a1ed04414 100644 --- a/api_docs/data.json +++ b/api_docs/data.json @@ -241,7 +241,7 @@ "description": [], "signature": [ "Pick & Pick<{ type: ", { "pluginId": "data", @@ -608,7 +608,7 @@ "description": [], "signature": [ "() => { type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; }" ], "path": "src/plugins/data/common/search/aggs/agg_config.ts", @@ -629,7 +629,7 @@ "description": [], "signature": [ "() => { type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; }" ], "path": "src/plugins/data/common/search/aggs/agg_config.ts", @@ -1273,7 +1273,7 @@ "description": [], "signature": [ "Pick & Pick<{ type: string | ", { "pluginId": "data", @@ -1512,7 +1512,7 @@ "text": "AggConfig" }, ">(params: Pick & Pick<{ type: string | ", { "pluginId": "data", @@ -1543,7 +1543,7 @@ "description": [], "signature": [ "Pick & Pick<{ type: string | ", { "pluginId": "data", @@ -2274,7 +2274,7 @@ "description": [], "signature": [ "(agg: TAggConfig, state?: { type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined) => TAggConfig" ], "path": "src/plugins/data/common/search/aggs/param_types/agg.ts", @@ -2303,7 +2303,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/param_types/agg.ts", @@ -8188,7 +8188,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -8270,7 +8270,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -8320,7 +8320,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -8434,7 +8434,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -8516,7 +8516,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -8598,7 +8598,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -8648,7 +8648,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -8762,7 +8762,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -8844,7 +8844,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -8958,7 +8958,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9012,7 +9012,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9062,7 +9062,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9120,7 +9120,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9178,7 +9178,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9236,7 +9236,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9294,7 +9294,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9352,7 +9352,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9402,7 +9402,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9452,7 +9452,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9506,7 +9506,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9560,7 +9560,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9610,7 +9610,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9660,7 +9660,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9710,7 +9710,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9760,7 +9760,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9810,7 +9810,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9860,7 +9860,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9914,7 +9914,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -9964,7 +9964,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -10014,7 +10014,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -10068,7 +10068,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -10118,7 +10118,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -10168,7 +10168,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -10218,7 +10218,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -14889,7 +14889,7 @@ "text": "IAggType" }, "; enabled?: boolean | undefined; id?: string | undefined; schema?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; }" ], "path": "src/plugins/data/common/search/aggs/agg_config.ts", @@ -15020,7 +15020,7 @@ "text": "IndexPattern" }, ", configStates?: Pick & Pick<{ type: string | ", { "pluginId": "data", @@ -15131,7 +15131,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts", @@ -15170,7 +15170,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/esdsl.ts", @@ -17656,7 +17656,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/index_patterns/expressions/load_index_pattern.ts", @@ -24304,7 +24304,7 @@ "description": [], "signature": [ "(agg: TAggConfig, state?: { type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined) => TAggConfig" ], "path": "src/plugins/data/common/search/aggs/param_types/agg.ts", @@ -24333,7 +24333,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/param_types/agg.ts", @@ -28751,7 +28751,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -28833,7 +28833,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -28883,7 +28883,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -28997,7 +28997,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29079,7 +29079,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29161,7 +29161,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29211,7 +29211,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29325,7 +29325,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29407,7 +29407,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29521,7 +29521,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29575,7 +29575,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29625,7 +29625,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29683,7 +29683,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29741,7 +29741,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29799,7 +29799,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29857,7 +29857,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29915,7 +29915,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -29965,7 +29965,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30015,7 +30015,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30069,7 +30069,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30123,7 +30123,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30173,7 +30173,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30223,7 +30223,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30273,7 +30273,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30323,7 +30323,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30373,7 +30373,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30423,7 +30423,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30477,7 +30477,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30527,7 +30527,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30577,7 +30577,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30631,7 +30631,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30681,7 +30681,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30731,7 +30731,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -30781,7 +30781,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -32535,7 +32535,7 @@ "text": "IAggType" }, "; enabled?: boolean | undefined; id?: string | undefined; schema?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; }" ], "path": "src/plugins/data/common/search/aggs/agg_config.ts", @@ -32631,7 +32631,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts", @@ -34951,7 +34951,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/index_patterns/expressions/load_index_pattern.ts", diff --git a/api_docs/data_index_patterns.json b/api_docs/data_index_patterns.json index 3badbb04ce527..3ca81e7ff4b82 100644 --- a/api_docs/data_index_patterns.json +++ b/api_docs/data_index_patterns.json @@ -8503,7 +8503,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/index_patterns/expressions/load_index_pattern.ts", diff --git a/api_docs/data_query.json b/api_docs/data_query.json index 3ed8cc6ab9f02..af30ed33b0303 100644 --- a/api_docs/data_query.json +++ b/api_docs/data_query.json @@ -30,8 +30,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">" ], @@ -492,8 +492,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ", collector: unknown) => {}" ], diff --git a/api_docs/data_search.json b/api_docs/data_search.json index d6006dc315db1..e035cee56c6e0 100644 --- a/api_docs/data_search.json +++ b/api_docs/data_search.json @@ -918,7 +918,7 @@ "text": "IndexPattern" }, ", configStates?: Pick & Pick<{ type: string | ", { "pluginId": "data", @@ -4174,7 +4174,7 @@ "description": [], "signature": [ "Pick & Pick<{ type: ", { "pluginId": "data", @@ -4541,7 +4541,7 @@ "description": [], "signature": [ "() => { type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; }" ], "path": "src/plugins/data/common/search/aggs/agg_config.ts", @@ -4562,7 +4562,7 @@ "description": [], "signature": [ "() => { type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; }" ], "path": "src/plugins/data/common/search/aggs/agg_config.ts", @@ -5206,7 +5206,7 @@ "description": [], "signature": [ "Pick & Pick<{ type: string | ", { "pluginId": "data", @@ -5445,7 +5445,7 @@ "text": "AggConfig" }, ">(params: Pick & Pick<{ type: string | ", { "pluginId": "data", @@ -5476,7 +5476,7 @@ "description": [], "signature": [ "Pick & Pick<{ type: string | ", { "pluginId": "data", @@ -6207,7 +6207,7 @@ "description": [], "signature": [ "(agg: TAggConfig, state?: { type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined) => TAggConfig" ], "path": "src/plugins/data/common/search/aggs/param_types/agg.ts", @@ -6236,7 +6236,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/param_types/agg.ts", @@ -11247,7 +11247,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">) => any" ], "path": "src/plugins/data/common/search/expressions/utils/function_wrapper.ts", @@ -14922,7 +14922,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15004,7 +15004,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15054,7 +15054,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15168,7 +15168,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15250,7 +15250,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15332,7 +15332,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15382,7 +15382,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15496,7 +15496,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15578,7 +15578,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15692,7 +15692,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15746,7 +15746,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15796,7 +15796,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15854,7 +15854,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15912,7 +15912,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -15970,7 +15970,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16028,7 +16028,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16086,7 +16086,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16136,7 +16136,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16186,7 +16186,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16240,7 +16240,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16294,7 +16294,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16344,7 +16344,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16394,7 +16394,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16444,7 +16444,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16494,7 +16494,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16544,7 +16544,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16594,7 +16594,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16648,7 +16648,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16698,7 +16698,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16748,7 +16748,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16802,7 +16802,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16852,7 +16852,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16902,7 +16902,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -16952,7 +16952,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/aggs/types.ts", @@ -17102,7 +17102,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/bucket_avg.ts", @@ -17117,7 +17117,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/bucket_avg.ts", @@ -17156,7 +17156,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/bucket_max.ts", @@ -17171,7 +17171,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/bucket_max.ts", @@ -17210,7 +17210,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/bucket_min.ts", @@ -17225,7 +17225,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/bucket_min.ts", @@ -17264,7 +17264,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/bucket_sum.ts", @@ -17279,7 +17279,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/bucket_sum.ts", @@ -17365,7 +17365,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/cumulative_sum.ts", @@ -17702,7 +17702,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/derivative.ts", @@ -17843,7 +17843,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/filtered_metric.ts", @@ -17858,7 +17858,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/filtered_metric.ts", @@ -18557,7 +18557,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/moving_avg.ts", @@ -18770,7 +18770,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/metrics/serial_diff.ts", @@ -19027,7 +19027,7 @@ "description": [], "signature": [ "{ type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined" ], "path": "src/plugins/data/common/search/aggs/buckets/terms.ts", @@ -23574,7 +23574,7 @@ "text": "IAggType" }, "; enabled?: boolean | undefined; id?: string | undefined; schema?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; }" ], "path": "src/plugins/data/common/search/aggs/agg_config.ts", @@ -24027,7 +24027,7 @@ "text": "IndexPattern" }, ", configStates?: Pick & Pick<{ type: string | ", { "pluginId": "data", @@ -24216,7 +24216,7 @@ "text": "IAggType" }, "; enabled?: boolean | undefined; id?: string | undefined; schema?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; }" ], "path": "src/plugins/data/common/search/aggs/agg_configs.ts", @@ -24356,7 +24356,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts", @@ -24395,7 +24395,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/esdsl.ts", @@ -24575,7 +24575,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/cidr.ts", @@ -24638,7 +24638,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/date_range.ts", @@ -24687,7 +24687,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/exists_filter.ts", @@ -24750,7 +24750,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/extended_bounds.ts", @@ -24805,7 +24805,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/field.ts", @@ -24860,7 +24860,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/geo_bounding_box.ts", @@ -24915,7 +24915,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/geo_point.ts", @@ -24978,7 +24978,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/ip_range.ts", @@ -25149,7 +25149,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/kibana_filter.ts", @@ -25212,7 +25212,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/timerange.ts", @@ -25261,7 +25261,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/kql.ts", @@ -25310,7 +25310,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/lucene.ts", @@ -25373,7 +25373,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/numerical_range.ts", @@ -25422,7 +25422,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/phrase_filter.ts", @@ -25477,7 +25477,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/query_filter.ts", @@ -25524,7 +25524,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/range.ts", @@ -25573,7 +25573,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/data/common/search/expressions/range_filter.ts", @@ -30132,7 +30132,7 @@ "text": "IBucketAggConfig" }, ", state?: { type: string; enabled?: boolean | undefined; id?: string | undefined; params?: {} | ", - "SerializableState", + "Serializable", " | undefined; schema?: string | undefined; } | undefined) => ", { "pluginId": "data", diff --git a/api_docs/discover.json b/api_docs/discover.json index ec94f42df2a0e..61d2f39d0c87f 100644 --- a/api_docs/discover.json +++ b/api_docs/discover.json @@ -81,8 +81,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" } ], "path": "src/plugins/discover/public/locator.ts", @@ -163,8 +163,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ") | undefined" ], @@ -278,8 +278,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ") | undefined" ], diff --git a/api_docs/embeddable.json b/api_docs/embeddable.json index 44c5ecbe0c063..7e5ef7bbdcc1b 100644 --- a/api_docs/embeddable.json +++ b/api_docs/embeddable.json @@ -8095,8 +8095,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; disabledActions?: string[] | undefined; disableTriggers?: boolean | undefined; searchSessionId?: string | undefined; syncColors?: boolean | undefined; }" ], @@ -8511,8 +8511,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">) => void" ], @@ -8539,8 +8539,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">" ], @@ -9194,8 +9194,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">) => void" ], @@ -9222,8 +9222,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">" ], @@ -9965,8 +9965,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; disabledActions?: string[] | undefined; disableTriggers?: boolean | undefined; searchSessionId?: string | undefined; syncColors?: boolean | undefined; }" ], @@ -10037,16 +10037,16 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ", version: string) => ", { "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" } ], "path": "src/plugins/embeddable/common/lib/migrate.ts", diff --git a/api_docs/expression_repeat_image.json b/api_docs/expression_repeat_image.json index 4917dc1f62205..e2039e6e2ac45 100644 --- a/api_docs/expression_repeat_image.json +++ b/api_docs/expression_repeat_image.json @@ -350,7 +350,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expression_repeat_image/common/types/expression_functions.ts", diff --git a/api_docs/expression_shape.json b/api_docs/expression_shape.json index 607c10fb38240..94a969388a61c 100644 --- a/api_docs/expression_shape.json +++ b/api_docs/expression_shape.json @@ -891,7 +891,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expression_shape/common/types/expression_functions.ts", @@ -1405,7 +1405,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expression_shape/common/types/expression_functions.ts", diff --git a/api_docs/expressions.json b/api_docs/expressions.json index 6185466a02cc8..0abef59f1deef 100644 --- a/api_docs/expressions.json +++ b/api_docs/expressions.json @@ -76,8 +76,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>>>, ", { @@ -116,8 +116,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>>>, {}>" ], @@ -157,7 +157,7 @@ "text": "ExecutionContext" }, "" ], "path": "src/plugins/expressions/common/execution/execution.ts", @@ -203,8 +203,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>>>" ], @@ -350,8 +350,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>>>" ], @@ -840,8 +840,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>" ], @@ -1469,8 +1469,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>" ], @@ -1851,8 +1851,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">) => ", { @@ -1886,8 +1886,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">" ], @@ -2301,16 +2301,16 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ") => ", { "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, "; }" ], @@ -4000,8 +4000,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>" ], @@ -4616,8 +4616,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">) => ", { @@ -4651,8 +4651,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">" ], @@ -7709,7 +7709,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -7755,7 +7755,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -7793,7 +7793,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -7831,7 +7831,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -7869,7 +7869,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -7931,7 +7931,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -7993,7 +7993,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -8055,7 +8055,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -8117,7 +8117,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -8614,8 +8614,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>" ], @@ -9105,7 +9105,7 @@ "label": "searchContext", "description": [], "signature": [ - "SerializableState", + "Serializable", " | undefined" ], "path": "src/plugins/expressions/public/types/index.ts", @@ -10235,7 +10235,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -10638,8 +10638,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }" ], @@ -10786,8 +10786,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }" ], @@ -11195,8 +11195,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>; readonly fork: () => ", { @@ -11550,8 +11550,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>>>, ", { @@ -11590,8 +11590,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>>>, {}>" ], @@ -11631,7 +11631,7 @@ "text": "ExecutionContext" }, "" ], "path": "src/plugins/expressions/common/execution/execution.ts", @@ -11677,8 +11677,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>>>" ], @@ -11824,8 +11824,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>>>" ], @@ -12745,8 +12745,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>" ], @@ -13127,8 +13127,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">) => ", { @@ -13162,8 +13162,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">" ], @@ -13577,16 +13577,16 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ") => ", { "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, "; }" ], @@ -17443,7 +17443,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -17489,7 +17489,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -17527,7 +17527,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -17565,7 +17565,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -17603,7 +17603,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -17665,7 +17665,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -17727,7 +17727,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -17789,7 +17789,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -17851,7 +17851,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -18962,7 +18962,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -19314,8 +19314,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }" ], @@ -19462,8 +19462,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }" ], @@ -19839,8 +19839,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>; readonly fork: () => ", { @@ -19955,8 +19955,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>>>, ", { @@ -19995,8 +19995,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>>>, {}>" ], @@ -20036,7 +20036,7 @@ "text": "ExecutionContext" }, "" ], "path": "src/plugins/expressions/common/execution/execution.ts", @@ -20082,8 +20082,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>>>" ], @@ -20229,8 +20229,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>>>" ], @@ -20719,8 +20719,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>" ], @@ -21348,8 +21348,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>" ], @@ -21730,8 +21730,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">) => ", { @@ -21765,8 +21765,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">" ], @@ -22180,16 +22180,16 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ") => ", { "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, "; }" ], @@ -23248,8 +23248,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>" ], @@ -23864,8 +23864,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">) => ", { @@ -23899,8 +23899,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">" ], @@ -25367,8 +25367,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>" ], @@ -25533,7 +25533,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">" ], "path": "src/plugins/expressions/common/util/test_utils.ts", @@ -25923,8 +25923,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>" ], @@ -26796,7 +26796,7 @@ "\nany extra parameters for the source that produced this column" ], "signature": [ - "SerializableState", + "Serializable", " | undefined" ], "path": "src/plugins/expressions/common/expression_types/specs/datatable.ts", @@ -28555,8 +28555,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined" ], @@ -29009,7 +29009,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -29055,7 +29055,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -29093,7 +29093,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -29131,7 +29131,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -29169,7 +29169,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -29231,7 +29231,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -29293,7 +29293,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -29355,7 +29355,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -29417,7 +29417,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -29882,8 +29882,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>" ], @@ -31260,7 +31260,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/types.ts", @@ -31570,8 +31570,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | undefined; rawError?: any; duration: number | undefined; }" ], @@ -31640,7 +31640,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/specs/clog.ts", @@ -31703,7 +31703,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/specs/cumulative_sum.ts", @@ -31766,7 +31766,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/specs/derivative.ts", @@ -31813,7 +31813,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/specs/font.ts", @@ -31876,7 +31876,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/specs/moving_average.ts", @@ -31939,7 +31939,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/specs/overall_metric.ts", @@ -31978,7 +31978,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/specs/theme.ts", @@ -32033,7 +32033,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/specs/ui_setting.ts", @@ -32072,7 +32072,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/specs/var.ts", @@ -32111,7 +32111,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/expressions/common/expression_functions/specs/var_set.ts", @@ -32271,8 +32271,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>; readonly fork: () => ", { @@ -32380,8 +32380,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }" ], @@ -32550,8 +32550,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }" ], @@ -34894,8 +34894,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>) => ", { @@ -34926,8 +34926,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>, \"error\" | \"info\">; }>" ], @@ -34962,8 +34962,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }>" ], @@ -36961,7 +36961,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">) => ", { "pluginId": "expressions", @@ -37038,7 +37038,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">" ], "path": "src/plugins/expressions/common/expression_functions/specs/math_column.ts", @@ -39686,7 +39686,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">) => any" ], "path": "src/plugins/expressions/common/expression_functions/specs/theme.ts", @@ -39744,7 +39744,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">" ], "path": "src/plugins/expressions/common/expression_functions/specs/theme.ts", @@ -40166,7 +40166,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">) => any" ], "path": "src/plugins/expressions/common/expression_functions/specs/var.ts", @@ -40224,7 +40224,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">" ], "path": "src/plugins/expressions/common/expression_functions/specs/var.ts", @@ -40429,7 +40429,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">) => unknown" ], "path": "src/plugins/expressions/common/expression_functions/specs/var_set.ts", @@ -40487,7 +40487,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">" ], "path": "src/plugins/expressions/common/expression_functions/specs/var_set.ts", diff --git a/api_docs/index_lifecycle_management.json b/api_docs/index_lifecycle_management.json index 744bc3f12f3c8..d1d99aa17cff5 100644 --- a/api_docs/index_lifecycle_management.json +++ b/api_docs/index_lifecycle_management.json @@ -24,8 +24,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" } ], "path": "x-pack/plugins/index_lifecycle_management/public/locator.ts", diff --git a/api_docs/kibana_utils.json b/api_docs/kibana_utils.json index fa59e31712129..2bd4e7f310b68 100644 --- a/api_docs/kibana_utils.json +++ b/api_docs/kibana_utils.json @@ -9150,8 +9150,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">) => S" ], @@ -9198,8 +9198,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">" ], @@ -10007,8 +10007,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">) => P) | undefined" ], @@ -10037,8 +10037,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">" ], @@ -10964,16 +10964,16 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ", version: string) => ", { "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" } ], "path": "src/plugins/kibana_utils/common/persistable_state/types.ts", @@ -11216,16 +11216,16 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | ", { "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableValue", - "text": "SerializableValue" + "section": "def-common.SerializableRecord", + "text": "SerializableRecord" }, "[] | null | undefined" ], @@ -11235,10 +11235,10 @@ }, { "parentPluginId": "kibanaUtils", - "id": "def-common.SerializableState", + "id": "def-common.Serializable", "type": "Type", "tags": [], - "label": "SerializableState", + "label": "Serializable", "description": [ "\nSerializable state is something is a POJO JavaScript object that can be\nserialized to a JSON string." ], @@ -11259,10 +11259,10 @@ }, { "parentPluginId": "kibanaUtils", - "id": "def-common.SerializableValue", + "id": "def-common.SerializableRecord", "type": "Type", "tags": [], - "label": "SerializableValue", + "label": "SerializableRecord", "description": [], "signature": [ "string | number | boolean | ", @@ -11270,8 +11270,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | null | undefined" ], diff --git a/api_docs/lens.json b/api_docs/lens.json index 34408d962cead..0e4b2aee2ac2b 100644 --- a/api_docs/lens.json +++ b/api_docs/lens.json @@ -2742,8 +2742,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, " | undefined; }> | Output>>; readonly fork: () => ", { diff --git a/api_docs/presentation_util.json b/api_docs/presentation_util.json index d062fa63965e7..e14e04794e022 100644 --- a/api_docs/presentation_util.json +++ b/api_docs/presentation_util.json @@ -598,7 +598,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">) => any" ], "path": "src/plugins/presentation_util/common/lib/test_helpers/function_wrapper.ts", diff --git a/api_docs/share.json b/api_docs/share.json index d9b0ba64d94ce..826348efb8eb8 100644 --- a/api_docs/share.json +++ b/api_docs/share.json @@ -437,8 +437,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">(locator: ", { @@ -1884,8 +1884,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">(locator: ", { diff --git a/api_docs/ui_actions_enhanced.json b/api_docs/ui_actions_enhanced.json index 697231e9f7935..0847694c73025 100644 --- a/api_docs/ui_actions_enhanced.json +++ b/api_docs/ui_actions_enhanced.json @@ -1566,8 +1566,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">, triggers: string[]) => Promise" ], @@ -1596,8 +1596,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">" ], @@ -1647,8 +1647,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">, triggers: string[]) => Promise" ], @@ -1693,8 +1693,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">" ], @@ -1880,8 +1880,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">; }[]>" ], @@ -3132,8 +3132,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ", object, ", { @@ -3366,8 +3366,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">; }" ], @@ -3935,8 +3935,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">; }" ], @@ -4124,8 +4124,8 @@ "pluginId": "kibanaUtils", "scope": "common", "docId": "kibKibanaUtilsPluginApi", - "section": "def-common.SerializableState", - "text": "SerializableState" + "section": "def-common.Serializable", + "text": "Serializable" }, ">; }" ], diff --git a/api_docs/visualizations.json b/api_docs/visualizations.json index c779b4695fde5..655890dd601f2 100644 --- a/api_docs/visualizations.json +++ b/api_docs/visualizations.json @@ -1256,7 +1256,7 @@ "text": "SerializedFieldFormat" }, "> | undefined; source?: string | undefined; sourceParams?: ", - "SerializableState", + "Serializable", " | undefined; }; id: string; name: string; }[]; type: \"datatable\"; rows: Record[]; }" ], "path": "src/plugins/visualizations/common/prepare_log_table.ts", @@ -1651,7 +1651,7 @@ "description": [], "signature": [ "Pick & Pick<{ type: ", { "pluginId": "data", @@ -2075,7 +2075,7 @@ "description": [], "signature": [ "Pick & Pick<{ type: ", { "pluginId": "data", @@ -4881,7 +4881,7 @@ "text": "SerializedFieldFormat" }, "> | undefined; source?: string | undefined; sourceParams?: ", - "SerializableState", + "Serializable", " | undefined; }; id: string; name: string; }[]; type: \"datatable\"; rows: Record[]; }" ], "path": "src/plugins/visualizations/common/prepare_log_table.ts", @@ -4981,7 +4981,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/visualizations/common/expression_functions/range.ts", @@ -5047,7 +5047,7 @@ "text": "Adapters" }, ", ", - "SerializableState", + "Serializable", ">>" ], "path": "src/plugins/visualizations/common/expression_functions/vis_dimension.ts", @@ -5120,7 +5120,7 @@ "description": [], "signature": [ "Pick & Pick<{ type: ", { "pluginId": "data", diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.toserializedfieldformat.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.toserializedfieldformat.md index 7a75950f9cc6d..73b415f0a0b86 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.toserializedfieldformat.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.toserializedfieldformat.md @@ -9,9 +9,9 @@ Returns a serialized field format for the field used in this agg. This can be pa Signature: ```typescript -toSerializedFieldFormat(): {} | Ensure, SerializableState>; +toSerializedFieldFormat(): {} | Ensure, SerializableRecord>; ``` Returns: -`{} | Ensure, SerializableState>` +`{} | Ensure, SerializableRecord>` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs._constructor_.md index 6ca7a1a88b30e..c4d09001087de 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs._constructor_.md @@ -13,7 +13,7 @@ constructor(indexPattern: IndexPattern, configStates: Pick & Pick<{ type: string | IAggType; @@ -27,6 +27,6 @@ constructor(indexPattern: IndexPattern, configStates: PickIndexPattern | | -| configStates | Pick<Pick<{
type: string;
enabled?: boolean | undefined;
id?: string | undefined;
params?: {} | import("./agg_config").SerializableState | undefined;
schema?: string | undefined;
}, "schema" | "enabled" | "id" | "params"> & Pick<{
type: string | IAggType;
}, "type"> & Pick<{
type: string | IAggType;
}, never>, "schema" | "type" | "enabled" | "id" | "params">[] | undefined | | +| configStates | Pick<Pick<{
type: string;
enabled?: boolean | undefined;
id?: string | undefined;
params?: {} | import("@kbn/common-utils").SerializableRecord | undefined;
schema?: string | undefined;
}, "schema" | "enabled" | "id" | "params"> & Pick<{
type: string | IAggType;
}, "type"> & Pick<{
type: string | IAggType;
}, never>, "schema" | "type" | "enabled" | "id" | "params">[] | undefined | | | opts | AggConfigsOptions | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.md index a2028bb5c12c6..7baa23fffe0d3 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.md @@ -24,7 +24,7 @@ export declare class FilterManager implements PersistableStateService | [getAllMigrations](./kibana-plugin-plugins-data-public.filtermanager.getallmigrations.md) | | () => {} | | | [inject](./kibana-plugin-plugins-data-public.filtermanager.inject.md) | | any | | | [migrateToLatest](./kibana-plugin-plugins-data-public.filtermanager.migratetolatest.md) | | any | | -| [telemetry](./kibana-plugin-plugins-data-public.filtermanager.telemetry.md) | | (filters: import("../../../../kibana_utils/common/persistable_state").SerializableState, collector: unknown) => {} | | +| [telemetry](./kibana-plugin-plugins-data-public.filtermanager.telemetry.md) | | (filters: import("@kbn/common-utils").SerializableRecord, collector: unknown) => {} | | ## Methods diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.telemetry.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.telemetry.md index bab6452c34903..df5b4ea0a26c8 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.telemetry.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filtermanager.telemetry.md @@ -7,5 +7,5 @@ Signature: ```typescript -telemetry: (filters: import("../../../../kibana_utils/common/persistable_state").SerializableState, collector: unknown) => {}; +telemetry: (filters: import("@kbn/common-utils").SerializableRecord, collector: unknown) => {}; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md index 4989b2b5ad584..f0261648e32ab 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.eskuery.md @@ -10,6 +10,6 @@ esKuery: { nodeTypes: import("@kbn/es-query/target_types/kuery/node_types").NodeTypes; fromKueryExpression: (expression: any, parseOptions?: Partial | undefined) => import("@kbn/es-query").KueryNode; - toElasticsearchQuery: (node: import("@kbn/es-query").KueryNode, indexPattern?: import("@kbn/es-query").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; + toElasticsearchQuery: (node: import("@kbn/es-query").KueryNode, indexPattern?: import("@kbn/es-query").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/utility-types").JsonObject; } ``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md index 729cc23dac501..77db30e967782 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableinput.md @@ -13,7 +13,7 @@ export declare type EmbeddableInput = { id: string; lastReloadRequestTime?: number; hidePanelTitles?: boolean; - enhancements?: SerializableState; + enhancements?: SerializableRecord; disabledActions?: string[]; disableTriggers?: boolean; searchSessionId?: string; diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.enhancementregistrydefinition.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.enhancementregistrydefinition.md index c54ebe4b1712d..978873b6efbc1 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.enhancementregistrydefinition.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.enhancementregistrydefinition.md @@ -7,7 +7,7 @@ Signature: ```typescript -export interface EnhancementRegistryDefinition

extends PersistableStateDefinition

+export interface EnhancementRegistryDefinition

extends PersistableStateDefinition

``` ## Properties diff --git a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.enhancementregistrydefinition.md b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.enhancementregistrydefinition.md index 09ff48a92158d..34462de422218 100644 --- a/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.enhancementregistrydefinition.md +++ b/docs/development/plugins/embeddable/server/kibana-plugin-plugins-embeddable-server.enhancementregistrydefinition.md @@ -7,7 +7,7 @@ Signature: ```typescript -export interface EnhancementRegistryDefinition

extends PersistableStateDefinition

+export interface EnhancementRegistryDefinition

extends PersistableStateDefinition

``` ## Properties diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md index 1fd926f1a0c07..8b876a7bcc3d6 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md @@ -9,7 +9,7 @@ Signature: ```typescript -export interface ExecutionContext +export interface ExecutionContext ``` ## Properties diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.md index 3e75e9ab3ef6f..8a829659e6fb2 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.md @@ -29,7 +29,7 @@ export declare class ExpressionFunction implements PersistableStatestring | A short help text. | | [inject](./kibana-plugin-plugins-expressions-public.expressionfunction.inject.md) | | (state: ExpressionAstFunction['arguments'], references: SavedObjectReference[]) => ExpressionAstFunction['arguments'] | | | [inputTypes](./kibana-plugin-plugins-expressions-public.expressionfunction.inputtypes.md) | | string[] | undefined | Type of inputs that this function supports. | -| [migrations](./kibana-plugin-plugins-expressions-public.expressionfunction.migrations.md) | | {
[key: string]: (state: SerializableState) => SerializableState;
} | | +| [migrations](./kibana-plugin-plugins-expressions-public.expressionfunction.migrations.md) | | {
[key: string]: (state: SerializableRecord) => SerializableRecord;
} | | | [name](./kibana-plugin-plugins-expressions-public.expressionfunction.name.md) | | string | Name of function | | [telemetry](./kibana-plugin-plugins-expressions-public.expressionfunction.telemetry.md) | | (state: ExpressionAstFunction['arguments'], telemetryData: Record<string, any>) => Record<string, any> | | | [type](./kibana-plugin-plugins-expressions-public.expressionfunction.type.md) | | string | Return type of function. This SHOULD be supplied. We use it for UI and autocomplete hinting. We may also use it for optimizations in the future. | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.migrations.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.migrations.md index 28d521f4b3fe1..a8b55dae1592f 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.migrations.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunction.migrations.md @@ -8,6 +8,6 @@ ```typescript migrations: { - [key: string]: (state: SerializableState) => SerializableState; + [key: string]: (state: SerializableRecord) => SerializableRecord; }; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionvalueerror.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionvalueerror.md index 1dee4a139c660..6d30d45690844 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionvalueerror.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionvalueerror.md @@ -9,6 +9,6 @@ ```typescript export declare type ExpressionValueError = ExpressionValueBoxed<'error', { error: ErrorLike; - info?: SerializableState; + info?: SerializableRecord; }>; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md index 69ecd229b5aa6..fcb0299e3fb68 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md @@ -25,7 +25,7 @@ export interface IExpressionLoaderParams | [onRenderError](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.onrendererror.md) | RenderErrorHandlerFnType | | | [partial](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.partial.md) | boolean | The flag to toggle on emitting partial results. By default, the partial results are disabled. | | [renderMode](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.rendermode.md) | RenderMode | | -| [searchContext](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md) | SerializableState | | +| [searchContext](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md) | SerializableRecord | | | [searchSessionId](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchsessionid.md) | string | | | [syncColors](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.synccolors.md) | boolean | | | [throttle](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.throttle.md) | number | Throttling of partial results in milliseconds. 0 is disabling the throttling. By default, it equals 1000. | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md index 6b5fad950c4e9..7b832af0e90d8 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md @@ -7,5 +7,5 @@ Signature: ```typescript -searchContext?: SerializableState; +searchContext?: SerializableRecord; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md index 5958853d10903..7a7ead6b9b153 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md @@ -9,7 +9,7 @@ Signature: ```typescript -export interface ExecutionContext +export interface ExecutionContext ``` ## Properties diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.md index 00c8aa63bfbd8..3b3d60cc27366 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.md @@ -29,7 +29,7 @@ export declare class ExpressionFunction implements PersistableStatestring | A short help text. | | [inject](./kibana-plugin-plugins-expressions-server.expressionfunction.inject.md) | | (state: ExpressionAstFunction['arguments'], references: SavedObjectReference[]) => ExpressionAstFunction['arguments'] | | | [inputTypes](./kibana-plugin-plugins-expressions-server.expressionfunction.inputtypes.md) | | string[] | undefined | Type of inputs that this function supports. | -| [migrations](./kibana-plugin-plugins-expressions-server.expressionfunction.migrations.md) | | {
[key: string]: (state: SerializableState) => SerializableState;
} | | +| [migrations](./kibana-plugin-plugins-expressions-server.expressionfunction.migrations.md) | | {
[key: string]: (state: SerializableRecord) => SerializableRecord;
} | | | [name](./kibana-plugin-plugins-expressions-server.expressionfunction.name.md) | | string | Name of function | | [telemetry](./kibana-plugin-plugins-expressions-server.expressionfunction.telemetry.md) | | (state: ExpressionAstFunction['arguments'], telemetryData: Record<string, any>) => Record<string, any> | | | [type](./kibana-plugin-plugins-expressions-server.expressionfunction.type.md) | | string | Return type of function. This SHOULD be supplied. We use it for UI and autocomplete hinting. We may also use it for optimizations in the future. | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.migrations.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.migrations.md index 29031a9306b2f..5d9410b62bb13 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.migrations.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionfunction.migrations.md @@ -8,6 +8,6 @@ ```typescript migrations: { - [key: string]: (state: SerializableState) => SerializableState; + [key: string]: (state: SerializableRecord) => SerializableRecord; }; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionvalueerror.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionvalueerror.md index c8132948a8993..2a4f4dc7aab70 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionvalueerror.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionvalueerror.md @@ -9,6 +9,6 @@ ```typescript export declare type ExpressionValueError = ExpressionValueBoxed<'error', { error: ErrorLike; - info?: SerializableState; + info?: SerializableRecord; }>; ``` diff --git a/examples/locator_examples/public/locator.ts b/examples/locator_examples/public/locator.ts index 18caeca08564e..878402d357271 100644 --- a/examples/locator_examples/public/locator.ts +++ b/examples/locator_examples/public/locator.ts @@ -6,16 +6,17 @@ * Side Public License, v 1. */ -import { SerializableState, MigrateFunction } from 'src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; +import { MigrateFunction } from 'src/plugins/kibana_utils/common'; import { LocatorDefinition, LocatorPublic } from '../../../src/plugins/share/public'; export const HELLO_LOCATOR = 'HELLO_LOCATOR'; -export interface HelloLocatorV1Params extends SerializableState { +export interface HelloLocatorV1Params extends SerializableRecord { name: string; } -export interface HelloLocatorV2Params extends SerializableState { +export interface HelloLocatorV2Params extends SerializableRecord { firstName: string; lastName: string; } diff --git a/package.json b/package.json index 2058eed5f024d..ca64a4f9c3440 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,6 @@ "@kbn/analytics": "link:bazel-bin/packages/kbn-analytics", "@kbn/apm-config-loader": "link:bazel-bin/packages/kbn-apm-config-loader", "@kbn/apm-utils": "link:bazel-bin/packages/kbn-apm-utils", - "@kbn/common-utils": "link:bazel-bin/packages/kbn-common-utils", "@kbn/config": "link:bazel-bin/packages/kbn-config", "@kbn/config-schema": "link:bazel-bin/packages/kbn-config-schema", "@kbn/crypto": "link:bazel-bin/packages/kbn-crypto", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index fcadedf8630f1..5a8aa75ee255e 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -13,7 +13,6 @@ filegroup( "//packages/kbn-babel-code-parser:build", "//packages/kbn-babel-preset:build", "//packages/kbn-cli-dev-mode:build", - "//packages/kbn-common-utils:build", "//packages/kbn-config:build", "//packages/kbn-config-schema:build", "//packages/kbn-crypto:build", diff --git a/packages/kbn-common-utils/.babelrc b/packages/kbn-common-utils/.babelrc deleted file mode 100644 index 7da72d1779128..0000000000000 --- a/packages/kbn-common-utils/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@kbn/babel-preset/node_preset"] -} diff --git a/packages/kbn-common-utils/BUILD.bazel b/packages/kbn-common-utils/BUILD.bazel deleted file mode 100644 index 699f65da408f5..0000000000000 --- a/packages/kbn-common-utils/BUILD.bazel +++ /dev/null @@ -1,89 +0,0 @@ -load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") -load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") -load("//src/dev/bazel:index.bzl", "jsts_transpiler") - -PKG_BASE_NAME = "kbn-common-utils" -PKG_REQUIRE_NAME = "@kbn/common-utils" - -SOURCE_FILES = glob( - [ - "src/**/*.ts", - ], - exclude = ["**/*.test.*"], -) - -SRCS = SOURCE_FILES - -filegroup( - name = "srcs", - srcs = SRCS, -) - -NPM_MODULE_EXTRA_FILES = [ - "package.json", - "README.md" -] - -RUNTIME_DEPS = [ - "//packages/kbn-config-schema", - "@npm//tslib", -] - -TYPES_DEPS = [ - "//packages/kbn-config-schema", - "@npm//tslib", - "@npm//@types/jest", - "@npm//@types/node", -] - -jsts_transpiler( - name = "target_node", - srcs = SRCS, - build_pkg_name = package_name(), -) - -ts_config( - name = "tsconfig", - src = "tsconfig.json", - deps = [ - "//:tsconfig.base.json", - ], -) - -ts_project( - name = "tsc_types", - args = ['--pretty'], - srcs = SRCS, - deps = TYPES_DEPS, - declaration = True, - declaration_map = True, - emit_declaration_only = True, - incremental = False, - out_dir = "target_types", - source_map = True, - root_dir = "src", - tsconfig = ":tsconfig", -) - -js_library( - name = PKG_BASE_NAME, - srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node", ":tsc_types"], - package_name = PKG_REQUIRE_NAME, - visibility = ["//visibility:public"], -) - -pkg_npm( - name = "npm_module", - deps = [ - ":%s" % PKG_BASE_NAME, - ] -) - -filegroup( - name = "build", - srcs = [ - ":npm_module", - ], - visibility = ["//visibility:public"], -) diff --git a/packages/kbn-common-utils/README.md b/packages/kbn-common-utils/README.md deleted file mode 100644 index 7b64c9f18fe89..0000000000000 --- a/packages/kbn-common-utils/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @kbn/common-utils - -Shared common (client and server sie) utilities shared across packages and plugins. \ No newline at end of file diff --git a/packages/kbn-common-utils/jest.config.js b/packages/kbn-common-utils/jest.config.js deleted file mode 100644 index 08f1995c47423..0000000000000 --- a/packages/kbn-common-utils/jest.config.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../..', - roots: ['/packages/kbn-common-utils'], -}; diff --git a/packages/kbn-common-utils/package.json b/packages/kbn-common-utils/package.json deleted file mode 100644 index a9679c2f0fb18..0000000000000 --- a/packages/kbn-common-utils/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@kbn/common-utils", - "main": "./target_node/index.js", - "types": "./target_types/index.d.ts", - "version": "1.0.0", - "license": "SSPL-1.0 OR Elastic License 2.0", - "private": true -} \ No newline at end of file diff --git a/packages/kbn-common-utils/src/index.ts b/packages/kbn-common-utils/src/index.ts deleted file mode 100644 index 1b8bffe4bf158..0000000000000 --- a/packages/kbn-common-utils/src/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export * from './json'; diff --git a/packages/kbn-common-utils/src/json/index.ts b/packages/kbn-common-utils/src/json/index.ts deleted file mode 100644 index 96c94df1bb48e..0000000000000 --- a/packages/kbn-common-utils/src/json/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export { JsonArray, JsonValue, JsonObject } from './typed_json'; diff --git a/packages/kbn-common-utils/src/json/typed_json.ts b/packages/kbn-common-utils/src/json/typed_json.ts deleted file mode 100644 index 06e99b9b65d5a..0000000000000 --- a/packages/kbn-common-utils/src/json/typed_json.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export type JsonValue = null | boolean | number | string | JsonObject | JsonArray; - -export interface JsonObject { - [key: string]: JsonValue; -} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface JsonArray extends Array {} diff --git a/packages/kbn-common-utils/tsconfig.json b/packages/kbn-common-utils/tsconfig.json deleted file mode 100644 index 7d1ecaa10a234..0000000000000 --- a/packages/kbn-common-utils/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "declaration": true, - "declarationMap": true, - "emitDeclarationOnly": true, - "incremental": false, - "outDir": "target_types", - "sourceMap": true, - "sourceRoot": "../../../../packages/kbn-common-utils/src", - "types": [ - "jest", - "node" - ] - }, - "include": [ - "src/**/*" - ] -} diff --git a/packages/kbn-es-query/BUILD.bazel b/packages/kbn-es-query/BUILD.bazel index 7d6e2b4683b9b..1e40918e6b509 100644 --- a/packages/kbn-es-query/BUILD.bazel +++ b/packages/kbn-es-query/BUILD.bazel @@ -31,7 +31,7 @@ NPM_MODULE_EXTRA_FILES = [ ] RUNTIME_DEPS = [ - "//packages/kbn-common-utils", + "//packages/kbn-utility-types", "//packages/kbn-config-schema", "//packages/kbn-i18n", "@npm//@elastic/elasticsearch", @@ -42,7 +42,7 @@ RUNTIME_DEPS = [ ] TYPES_DEPS = [ - "//packages/kbn-common-utils", + "//packages/kbn-utility-types", "//packages/kbn-i18n", "@npm//@elastic/elasticsearch", "@npm//@types/jest", diff --git a/packages/kbn-es-query/src/kuery/ast/ast.ts b/packages/kbn-es-query/src/kuery/ast/ast.ts index 6f43098a752de..030b5a8f1c29a 100644 --- a/packages/kbn-es-query/src/kuery/ast/ast.ts +++ b/packages/kbn-es-query/src/kuery/ast/ast.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { nodeTypes } from '../node_types/index'; import { KQLSyntaxError } from '../kuery_syntax_error'; import { KueryNode, DslQuery, KueryParseOptions } from '../types'; diff --git a/packages/kbn-es-query/src/kuery/node_types/named_arg.ts b/packages/kbn-es-query/src/kuery/node_types/named_arg.ts index b1b202e4323af..1892a4885b705 100644 --- a/packages/kbn-es-query/src/kuery/node_types/named_arg.ts +++ b/packages/kbn-es-query/src/kuery/node_types/named_arg.ts @@ -7,7 +7,7 @@ */ import _ from 'lodash'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import * as ast from '../ast'; import { nodeTypes } from '../node_types'; import { NamedArgTypeBuildNode } from './types'; diff --git a/packages/kbn-es-query/src/kuery/node_types/types.ts b/packages/kbn-es-query/src/kuery/node_types/types.ts index ea8eb5e8a0618..7e6f454418555 100644 --- a/packages/kbn-es-query/src/kuery/node_types/types.ts +++ b/packages/kbn-es-query/src/kuery/node_types/types.ts @@ -10,7 +10,7 @@ * WARNING: these typings are incomplete */ -import { JsonValue } from '@kbn/common-utils'; +import { JsonValue } from '@kbn/utility-types'; import { KueryNode } from '..'; import { IndexPatternBase } from '../..'; diff --git a/packages/kbn-utility-types/BUILD.bazel b/packages/kbn-utility-types/BUILD.bazel index 047cbd77f81c0..70f814c30f415 100644 --- a/packages/kbn-utility-types/BUILD.bazel +++ b/packages/kbn-utility-types/BUILD.bazel @@ -7,6 +7,7 @@ PKG_REQUIRE_NAME = "@kbn/utility-types" SOURCE_FILES = glob([ "src/jest/index.ts", + "src/serializable/**", "src/index.ts" ]) diff --git a/packages/kbn-utility-types/src/index.ts b/packages/kbn-utility-types/src/index.ts index 1f5d95e316e1d..921f056c6b755 100644 --- a/packages/kbn-utility-types/src/index.ts +++ b/packages/kbn-utility-types/src/index.ts @@ -9,6 +9,14 @@ import { PromiseType } from 'utility-types'; export { $Values, Assign, Class, Optional, Required } from 'utility-types'; +export type { + JsonArray, + JsonValue, + JsonObject, + SerializableRecord, + Serializable, +} from './serializable'; + /** * A type that may or may not be a `Promise`. */ diff --git a/src/core/types/serializable.ts b/packages/kbn-utility-types/src/serializable/index.ts similarity index 74% rename from src/core/types/serializable.ts rename to packages/kbn-utility-types/src/serializable/index.ts index 19f9c6cb21541..3a248f9aea2ee 100644 --- a/src/core/types/serializable.ts +++ b/packages/kbn-utility-types/src/serializable/index.ts @@ -6,11 +6,21 @@ * Side Public License, v 1. */ +export type JsonValue = null | boolean | number | string | JsonObject | JsonArray; + +export interface JsonObject { + [key: string]: JsonValue; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface JsonArray extends Array {} + export type Serializable = | string | number | boolean | null + | undefined | SerializableArray | SerializableRecord; diff --git a/src/core/types/index.ts b/src/core/types/index.ts index 97f990f608c04..280310aee8c55 100644 --- a/src/core/types/index.ts +++ b/src/core/types/index.ts @@ -15,5 +15,4 @@ export * from './capabilities'; export * from './app_category'; export * from './ui_settings'; export * from './saved_objects'; -export * from './serializable'; export type { KibanaExecutionContext } from './execution_context'; diff --git a/src/plugins/dashboard/public/locator.ts b/src/plugins/dashboard/public/locator.ts index e154351819ee9..ed4e7a5dd4d4c 100644 --- a/src/plugins/dashboard/public/locator.ts +++ b/src/plugins/dashboard/public/locator.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import type { TimeRange, Filter, Query, QueryState, RefreshInterval } from '../../data/public'; import type { LocatorDefinition, LocatorPublic } from '../../share/public'; import type { SavedDashboardPanel } from '../common/types'; @@ -26,7 +26,7 @@ const cleanEmptyKeys = (stateObj: Record) => { export const DASHBOARD_APP_LOCATOR = 'DASHBOARD_APP_LOCATOR'; -export interface DashboardAppLocatorParams extends SerializableState { +export interface DashboardAppLocatorParams extends SerializableRecord { /** * If given, the dashboard saved object with this id will be loaded. If not given, * a new, unsaved dashboard will be loaded up. @@ -40,7 +40,7 @@ export interface DashboardAppLocatorParams extends SerializableState { /** * Optionally set the refresh interval. */ - refreshInterval?: RefreshInterval & SerializableState; + refreshInterval?: RefreshInterval & SerializableRecord; /** * Optionally apply filers. NOTE: if given and used in conjunction with `dashboardId`, and the @@ -80,7 +80,7 @@ export interface DashboardAppLocatorParams extends SerializableState { /** * List of dashboard panels */ - panels?: SavedDashboardPanel[] & SerializableState; + panels?: SavedDashboardPanel[] & SerializableRecord; /** * Saved query ID diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts index 59dfa92cdbce0..6e1a6ccf1c86e 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts @@ -16,7 +16,7 @@ import { createInject, } from '../../common/embeddable/dashboard_container_persistable_state'; import { EmbeddableStateWithType } from 'src/plugins/embeddable/common'; -import { SerializableState } from '../../../kibana_utils/common'; +import { SerializableRecord } from '@kbn/utility-types'; const embeddableSetupMock = createEmbeddableSetupMock(); const extract = createExtract(embeddableSetupMock); @@ -589,7 +589,7 @@ describe('dashboard', () => { it('runs migrations on by value panels only', () => { const newEmbeddableSetupMock = createEmbeddableSetupMock(); newEmbeddableSetupMock.getAllMigrations.mockImplementation(() => ({ - '7.13.0': (state: SerializableState) => { + '7.13.0': (state: SerializableRecord) => { state.superCoolKey = 'ONLY 4 BY VALUE EMBEDDABLES THANK YOU VERY MUCH'; return state; }, diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts index ceb77ba1b2f9e..7848a2e46487c 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { Serializable } from '@kbn/utility-types'; import { get, flow, mapValues } from 'lodash'; import { SavedObjectAttributes, @@ -29,7 +30,6 @@ import { mergeMigrationFunctionMaps, MigrateFunction, MigrateFunctionsObject, - SerializableValue, } from '../../../kibana_utils/common'; import { replaceIndexPatternReference } from './replace_index_pattern_reference'; @@ -154,8 +154,8 @@ function createExtractPanelReferencesMigration( } type ValueOrReferenceInput = SavedObjectEmbeddableInput & { - attributes?: SerializableValue; - savedVis?: SerializableValue; + attributes?: Serializable; + savedVis?: Serializable; }; // Runs the embeddable migrations on each panel diff --git a/src/plugins/data/common/query/persistable_state.ts b/src/plugins/data/common/query/persistable_state.ts index 08cda6eb59fbf..367234e9ff4f0 100644 --- a/src/plugins/data/common/query/persistable_state.ts +++ b/src/plugins/data/common/query/persistable_state.ts @@ -8,7 +8,7 @@ import uuid from 'uuid'; import { Filter } from '@kbn/es-query'; -import { SerializableState } from '../../../kibana_utils/common/persistable_state'; +import type { SerializableRecord } from '@kbn/utility-types'; import { SavedObjectReference } from '../../../../core/types'; export const extract = (filters: Filter[]) => { @@ -51,7 +51,7 @@ export const inject = (filters: Filter[], references: SavedObjectReference[]) => }); }; -export const telemetry = (filters: SerializableState, collector: unknown) => { +export const telemetry = (filters: SerializableRecord, collector: unknown) => { return {}; }; diff --git a/src/plugins/data/common/search/aggs/agg_config.ts b/src/plugins/data/common/search/aggs/agg_config.ts index f55bfe24cd465..1a70a41e72dd5 100644 --- a/src/plugins/data/common/search/aggs/agg_config.ts +++ b/src/plugins/data/common/search/aggs/agg_config.ts @@ -9,6 +9,7 @@ import moment from 'moment'; import _ from 'lodash'; import { i18n } from '@kbn/i18n'; +import type { SerializableRecord } from '@kbn/utility-types'; import { Assign, Ensure } from '@kbn/utility-types'; import { ISearchOptions, ISearchSource } from 'src/plugins/data/public'; @@ -23,23 +24,16 @@ import { writeParams } from './agg_params'; import { IAggConfigs } from './agg_configs'; import { parseTimeShift } from './utils'; -type State = string | number | boolean | null | undefined | SerializableState; - -/** @internal **/ -export interface SerializableState { - [key: string]: State | State[]; -} - /** @public **/ export type AggConfigSerialized = Ensure< { type: string; enabled?: boolean; id?: string; - params?: {} | SerializableState; + params?: {} | SerializableRecord; schema?: string; }, - SerializableState + SerializableRecord >; export type AggConfigOptions = Assign; @@ -311,7 +305,7 @@ export class AggConfig { id: this.id, enabled: this.enabled, type: this.type && this.type.name, - params: outParams as SerializableState, + params: outParams as SerializableRecord, ...(this.schema && { schema: this.schema }), }; } @@ -333,7 +327,7 @@ export class AggConfig { */ toSerializedFieldFormat(): | {} - | Ensure, SerializableState> { + | Ensure, SerializableRecord> { return this.type ? this.type.getSerializedFormat(this) : {}; } diff --git a/src/plugins/data/common/search/aggs/buckets/histogram.test.ts b/src/plugins/data/common/search/aggs/buckets/histogram.test.ts index 0a1ac2289b07d..ef5cd85e6f5a2 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram.test.ts @@ -12,7 +12,7 @@ import { AggTypesDependencies } from '../agg_types'; import { BUCKET_TYPES } from './bucket_agg_types'; import { IBucketHistogramAggConfig, getHistogramBucketAgg, AutoBounds } from './histogram'; import { BucketAggType } from './bucket_agg_type'; -import { SerializableState } from 'src/plugins/expressions/common'; +import { SerializableRecord } from '@kbn/utility-types'; describe('Histogram Agg', () => { let aggTypesDependencies: AggTypesDependencies; @@ -256,7 +256,7 @@ describe('Histogram Agg', () => { }); (aggConfigs.aggs[0] as IBucketHistogramAggConfig).setAutoBounds({ min: 0, max: 1000 }); const serializedAgg = aggConfigs.aggs[0].serialize(); - const serializedIntervalParam = (serializedAgg.params as SerializableState).used_interval; + const serializedIntervalParam = (serializedAgg.params as SerializableRecord).used_interval; expect(serializedIntervalParam).toBe(500); const freshHistogramAggConfig = getAggConfigs({ interval: 100, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 5f5286de4a1ab..2fee05760186b 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -106,6 +106,7 @@ import { SavedObjectsFindOptions } from 'kibana/public'; import { SavedObjectsFindResponse } from 'kibana/server'; import { SavedObjectsUpdateResponse } from 'kibana/server'; import { SchemaTypeError } from '@kbn/config-schema'; +import { SerializableRecord } from '@kbn/utility-types'; import { SerializedFieldFormat as SerializedFieldFormat_3 } from 'src/plugins/expressions/common'; import { StartServicesAccessor } from 'kibana/public'; import { ToastInputFields } from 'src/core/public/notifications'; @@ -200,8 +201,7 @@ export class AggConfig { toExpressionAst(): ExpressionAstExpression | undefined; // @deprecated (undocumented) toJSON(): AggConfigSerialized; - // Warning: (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts - toSerializedFieldFormat(): {} | Ensure, SerializableState_2>; + toSerializedFieldFormat(): {} | Ensure, SerializableRecord>; // (undocumented) get type(): IAggType; set type(type: IAggType); @@ -227,7 +227,7 @@ export class AggConfigs { type: string; enabled?: boolean | undefined; id?: string | undefined; - params?: {} | import("./agg_config").SerializableState | undefined; + params?: {} | import("@kbn/utility-types").SerializableRecord | undefined; schema?: string | undefined; }, "schema" | "enabled" | "id" | "params"> & Pick<{ type: string | IAggType; @@ -330,9 +330,9 @@ export type AggConfigSerialized = Ensure<{ type: string; enabled?: boolean; id?: string; - params?: {} | SerializableState_2; + params?: {} | SerializableRecord; schema?: string; -}, SerializableState_2>; +}, SerializableRecord>; // Warning: (ae-missing-release-tag) "AggFunctionsMapping" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -792,7 +792,7 @@ export const esFilters: { export const esKuery: { nodeTypes: import("@kbn/es-query/target_types/kuery/node_types").NodeTypes; fromKueryExpression: (expression: any, parseOptions?: Partial | undefined) => import("@kbn/es-query").KueryNode; - toElasticsearchQuery: (node: import("@kbn/es-query").KueryNode, indexPattern?: import("@kbn/es-query").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; + toElasticsearchQuery: (node: import("@kbn/es-query").KueryNode, indexPattern?: import("@kbn/es-query").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/utility-types").JsonObject; }; // Warning: (ae-missing-release-tag) "esQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -943,7 +943,7 @@ export class FilterManager implements PersistableStateService { static setFiltersStore(filters: Filter_2[], store: FilterStateStore, shouldOverrideStore?: boolean): void; setGlobalFilters(newGlobalFilters: Filter_2[]): void; // (undocumented) - telemetry: (filters: import("../../../../kibana_utils/common/persistable_state").SerializableState, collector: unknown) => {}; + telemetry: (filters: import("@kbn/utility-types").SerializableRecord, collector: unknown) => {}; } // Warning: (ae-missing-release-tag) "generateFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/src/plugins/data/public/query/filter_manager/filter_manager.ts b/src/plugins/data/public/query/filter_manager/filter_manager.ts index bcae190b50773..34af80a483e6b 100644 --- a/src/plugins/data/public/query/filter_manager/filter_manager.ts +++ b/src/plugins/data/public/query/filter_manager/filter_manager.ts @@ -228,15 +228,15 @@ export class FilterManager implements PersistableStateService { }); } - // Filter needs to implement SerializableState + // Filter needs to implement SerializableRecord public extract = extract as any; - // Filter needs to implement SerializableState + // Filter needs to implement SerializableRecord public inject = inject as any; public telemetry = telemetry; - // Filter needs to implement SerializableState + // Filter needs to implement SerializableRecord public migrateToLatest = migrateToLatest as any; public getAllMigrations = getAllMigrations; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 761d6f9345b07..121cd8ebc0af7 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -60,6 +60,7 @@ import { SavedObjectsClientContract as SavedObjectsClientContract_2 } from 'kiba import { SavedObjectsFindOptions } from 'kibana/server'; import { SavedObjectsFindResponse } from 'kibana/server'; import { SavedObjectsUpdateResponse } from 'kibana/server'; +import { SerializableRecord } from '@kbn/utility-types'; import { SerializedFieldFormat as SerializedFieldFormat_3 } from 'src/plugins/expressions/common'; import { ToastInputFields } from 'src/core/public/notifications'; import { Type } from '@kbn/config-schema'; @@ -124,7 +125,7 @@ export const esFilters: { export const esKuery: { nodeTypes: import("@kbn/es-query/target_types/kuery/node_types").NodeTypes; fromKueryExpression: (expression: any, parseOptions?: Partial | undefined) => import("@kbn/es-query").KueryNode; - toElasticsearchQuery: (node: import("@kbn/es-query").KueryNode, indexPattern?: import("@kbn/es-query").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/common-utils").JsonObject; + toElasticsearchQuery: (node: import("@kbn/es-query").KueryNode, indexPattern?: import("@kbn/es-query").IndexPatternBase | undefined, config?: Record | undefined, context?: Record | undefined) => import("@kbn/utility-types").JsonObject; }; // Warning: (ae-missing-release-tag) "esQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/src/plugins/discover/public/locator.test.ts b/src/plugins/discover/public/locator.test.ts index edbb0663d4aa3..9a0ece2a434b4 100644 --- a/src/plugins/discover/public/locator.test.ts +++ b/src/plugins/discover/public/locator.test.ts @@ -10,7 +10,7 @@ import { hashedItemStore, getStatesFromKbnUrl } from '../../kibana_utils/public' import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock'; import { FilterStateStore } from '../../data/common'; import { DiscoverAppLocatorDefinition } from './locator'; -import { SerializableState } from 'src/plugins/kibana_utils/common'; +import { SerializableRecord } from '@kbn/utility-types'; const indexPatternId: string = 'c367b774-a4c2-11ea-bb37-0242ac130002'; const savedSearchId: string = '571aaf70-4c88-11e8-b3d7-01146121b73d'; @@ -215,7 +215,7 @@ describe('Discover url generator', () => { const { path } = await locator.getLocation({ columns: ['_source'], interval: 'auto', - sort: [['timestamp, asc']] as string[][] & SerializableState, + sort: [['timestamp, asc']] as string[][] & SerializableRecord, savedQuery: '__savedQueryId__', }); diff --git a/src/plugins/discover/public/locator.ts b/src/plugins/discover/public/locator.ts index fff89903bc465..bc632c7e1ccb7 100644 --- a/src/plugins/discover/public/locator.ts +++ b/src/plugins/discover/public/locator.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import type { TimeRange, Filter, Query, QueryState, RefreshInterval } from '../../data/public'; import type { LocatorDefinition, LocatorPublic } from '../../share/public'; import { esFilters } from '../../data/public'; @@ -14,7 +14,7 @@ import { setStateToKbnUrl } from '../../kibana_utils/public'; export const DISCOVER_APP_LOCATOR = 'DISCOVER_APP_LOCATOR'; -export interface DiscoverAppLocatorParams extends SerializableState { +export interface DiscoverAppLocatorParams extends SerializableRecord { /** * Optionally set saved search ID. */ @@ -33,7 +33,7 @@ export interface DiscoverAppLocatorParams extends SerializableState { /** * Optionally set the refresh interval. */ - refreshInterval?: RefreshInterval & SerializableState; + refreshInterval?: RefreshInterval & SerializableRecord; /** * Optionally apply filters. @@ -69,7 +69,7 @@ export interface DiscoverAppLocatorParams extends SerializableState { /** * Array of the used sorting [[field,direction],...] */ - sort?: string[][] & SerializableState; + sort?: string[][] & SerializableRecord; /** * id of the used saved query diff --git a/src/plugins/embeddable/common/lib/extract.ts b/src/plugins/embeddable/common/lib/extract.ts index a68c2db5ad155..3e820d54965b3 100644 --- a/src/plugins/embeddable/common/lib/extract.ts +++ b/src/plugins/embeddable/common/lib/extract.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ +import type { SerializableRecord } from '@kbn/utility-types'; import { CommonEmbeddableStartContract, EmbeddableStateWithType } from '../types'; import { extractBaseEmbeddableInput } from './migrate_base_input'; -import { SerializableState } from '../../../kibana_utils/common/persistable_state'; export const getExtractFunction = (embeddables: CommonEmbeddableStartContract) => { return (state: EmbeddableStateWithType) => { @@ -30,7 +30,7 @@ export const getExtractFunction = (embeddables: CommonEmbeddableStartContract) = if (!enhancements[key]) return; const enhancementResult = embeddables .getEnhancement(key) - .extract(enhancements[key] as SerializableState); + .extract(enhancements[key] as SerializableRecord); refs.push(...enhancementResult.references); updatedInput.enhancements![key] = enhancementResult.state; }); diff --git a/src/plugins/embeddable/common/lib/inject.ts b/src/plugins/embeddable/common/lib/inject.ts index 169ad615b9b6f..6f72eb5c3721a 100644 --- a/src/plugins/embeddable/common/lib/inject.ts +++ b/src/plugins/embeddable/common/lib/inject.ts @@ -6,10 +6,10 @@ * Side Public License, v 1. */ +import type { SerializableRecord } from '@kbn/utility-types'; import { CommonEmbeddableStartContract, EmbeddableStateWithType } from '../types'; import { SavedObjectReference } from '../../../../core/types'; import { injectBaseEmbeddableInput } from './migrate_base_input'; -import { SerializableState } from '../../../kibana_utils/common/persistable_state'; export const getInjectFunction = (embeddables: CommonEmbeddableStartContract) => { return (state: EmbeddableStateWithType, references: SavedObjectReference[]) => { @@ -27,7 +27,7 @@ export const getInjectFunction = (embeddables: CommonEmbeddableStartContract) => if (!enhancements[key]) return; updatedInput.enhancements![key] = embeddables .getEnhancement(key) - .inject(enhancements[key] as SerializableState, references); + .inject(enhancements[key] as SerializableRecord, references); }); return updatedInput; diff --git a/src/plugins/embeddable/common/lib/migrate.ts b/src/plugins/embeddable/common/lib/migrate.ts index 7dde9e1d2b2ab..9323c7f56dd03 100644 --- a/src/plugins/embeddable/common/lib/migrate.ts +++ b/src/plugins/embeddable/common/lib/migrate.ts @@ -6,15 +6,15 @@ * Side Public License, v 1. */ +import type { SerializableRecord } from '@kbn/utility-types'; import { CommonEmbeddableStartContract } from '../types'; import { baseEmbeddableMigrations } from './migrate_base_input'; -import { SerializableState } from '../../../kibana_utils/common/persistable_state'; -export type MigrateFunction = (state: SerializableState, version: string) => SerializableState; +export type MigrateFunction = (state: SerializableRecord, version: string) => SerializableRecord; export const getMigrateFunction = (embeddables: CommonEmbeddableStartContract) => { - const migrateFn: MigrateFunction = (state: SerializableState, version: string) => { - const enhancements = (state.enhancements as SerializableState) || {}; + const migrateFn: MigrateFunction = (state: SerializableRecord, version: string) => { + const enhancements = (state.enhancements as SerializableRecord) || {}; const factory = embeddables.getEmbeddableFactory(state.type as string); let updatedInput = baseEmbeddableMigrations[version] @@ -26,7 +26,7 @@ export const getMigrateFunction = (embeddables: CommonEmbeddableStartContract) = } if (factory?.isContainerType) { - updatedInput.panels = ((state.panels as SerializableState[]) || []).map((panel) => { + updatedInput.panels = ((state.panels as SerializableRecord[]) || []).map((panel) => { return migrateFn(panel, version); }); } @@ -36,7 +36,7 @@ export const getMigrateFunction = (embeddables: CommonEmbeddableStartContract) = if (!enhancements[key]) return; const enhancementDefinition = embeddables.getEnhancement(key); const migratedEnhancement = enhancementDefinition?.migrations?.[version] - ? enhancementDefinition.migrations[version](enhancements[key] as SerializableState) + ? enhancementDefinition.migrations[version](enhancements[key] as SerializableRecord) : enhancements[key]; (updatedInput.enhancements! as Record)[key] = migratedEnhancement; }); diff --git a/src/plugins/embeddable/common/types.ts b/src/plugins/embeddable/common/types.ts index 1cfc5073d6125..22d8672e59a37 100644 --- a/src/plugins/embeddable/common/types.ts +++ b/src/plugins/embeddable/common/types.ts @@ -5,8 +5,10 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ + +import type { SerializableRecord } from '@kbn/utility-types'; import type { KibanaExecutionContext } from 'src/core/public'; -import { PersistableStateService, SerializableState } from '../../kibana_utils/common'; +import { PersistableStateService } from '../../kibana_utils/common'; export enum ViewMode { EDIT = 'edit', @@ -28,7 +30,7 @@ export type EmbeddableInput = { /** * Reserved key for enhancements added by other plugins. */ - enhancements?: SerializableState; + enhancements?: SerializableRecord; /** * List of action IDs that this embeddable should not render. diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index cfb16da7b46b8..c644e1f3fdc23 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { Subscription } from 'rxjs'; import { identity } from 'lodash'; +import type { SerializableRecord } from '@kbn/utility-types'; import { getSavedObjectFinder, showSaveModal } from '../../saved_objects/public'; import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public'; import { Start as InspectorStart } from '../../inspector/public'; @@ -39,11 +40,7 @@ import { import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition'; import { EmbeddableStateTransfer } from './lib/state_transfer'; import { Storage } from '../../kibana_utils/public'; -import { - migrateToLatest, - PersistableStateService, - SerializableState, -} from '../../kibana_utils/common'; +import { migrateToLatest, PersistableStateService } from '../../kibana_utils/common'; import { ATTRIBUTE_SERVICE_KEY, AttributeService } from './lib/attribute_service'; import { AttributeServiceOptions } from './lib/attribute_service/attribute_service'; import { EmbeddableStateWithType } from '../common/types'; @@ -240,7 +237,7 @@ export class EmbeddablePublicPlugin implements Plugin { + ((state: SerializableRecord) => { return { state, references: [] }; }), migrations: enhancement.migrations || {}, @@ -253,7 +250,7 @@ export class EmbeddablePublicPlugin implements Plugin stats, inject: identity, - extract: (state: SerializableState) => { + extract: (state: SerializableRecord) => { return { state, references: [] }; }, migrations: {}, diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 33c9f2c8f9ff9..cf28f65ceaa79 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -54,6 +54,7 @@ import { SavedObjectAttributes } from 'kibana/server'; import { SavedObjectAttributes as SavedObjectAttributes_2 } from 'src/core/public'; import { SavedObjectAttributes as SavedObjectAttributes_3 } from 'kibana/public'; import { SchemaTypeError } from '@kbn/config-schema'; +import { SerializableRecord } from '@kbn/utility-types'; import { SimpleSavedObject as SimpleSavedObject_2 } from 'src/core/public'; import { Start as Start_2 } from 'src/plugins/inspector/public'; import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; @@ -319,7 +320,7 @@ export abstract class Embeddable { +export class EmbeddableChildPanel extends React.Component { constructor(props: EmbeddableChildPanelProps); // (undocumented) [panel: string]: any; @@ -417,7 +418,7 @@ export type EmbeddableInput = { id: string; lastReloadRequestTime?: number; hidePanelTitles?: boolean; - enhancements?: SerializableState; + enhancements?: SerializableRecord; disabledActions?: string[]; disableTriggers?: boolean; searchSessionId?: string; @@ -477,7 +478,7 @@ export interface EmbeddablePackageState { // Warning: (ae-missing-release-tag) "EmbeddablePanel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class EmbeddablePanel extends React.Component { +export class EmbeddablePanel extends React.Component { constructor(props: Props); // (undocumented) closeMyContextMenuPanel: () => void; @@ -620,7 +621,7 @@ export class EmbeddableStateTransfer { // Warning: (ae-missing-release-tag) "EnhancementRegistryDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface EnhancementRegistryDefinition

extends PersistableStateDefinition

{ +export interface EnhancementRegistryDefinition

extends PersistableStateDefinition

{ // (undocumented) id: string; } @@ -897,7 +898,6 @@ export const withEmbeddableSubscription: ; export type EnhancementsRegistry = Map; -export interface EnhancementRegistryDefinition

+export interface EnhancementRegistryDefinition

extends PersistableStateDefinition

{ id: string; } -export interface EnhancementRegistryItem

+export interface EnhancementRegistryItem

extends PersistableState

{ id: string; } diff --git a/src/plugins/embeddable/server/plugin.ts b/src/plugins/embeddable/server/plugin.ts index c85f48e01d486..6f545070040d5 100644 --- a/src/plugins/embeddable/server/plugin.ts +++ b/src/plugins/embeddable/server/plugin.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { SerializableRecord } from '@kbn/utility-types'; import { CoreSetup, CoreStart, Plugin } from 'kibana/server'; import { identity } from 'lodash'; import { @@ -23,7 +24,6 @@ import { } from '../common/lib'; import { PersistableStateService, - SerializableState, PersistableStateMigrateFn, MigrateFunctionsObject, } from '../../kibana_utils/common'; @@ -96,7 +96,7 @@ export class EmbeddableServerPlugin implements Plugin { + ((state: SerializableRecord) => { return { state, references: [] }; }), migrations: enhancement.migrations || {}, @@ -109,7 +109,7 @@ export class EmbeddableServerPlugin implements Plugin stats, inject: identity, - extract: (state: SerializableState) => { + extract: (state: SerializableRecord) => { return { state, references: [] }; }, migrations: {}, diff --git a/src/plugins/embeddable/server/server.api.md b/src/plugins/embeddable/server/server.api.md index 4409a37d31621..e17f40423b00b 100644 --- a/src/plugins/embeddable/server/server.api.md +++ b/src/plugins/embeddable/server/server.api.md @@ -8,6 +8,7 @@ import { CoreSetup } from 'kibana/server'; import { CoreStart } from 'kibana/server'; import { KibanaExecutionContext } from 'src/core/public'; import { Plugin } from 'kibana/server'; +import { SerializableRecord } from '@kbn/utility-types'; // Warning: (ae-forgotten-export) The symbol "EmbeddableStateWithType" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "PersistableStateDefinition" needs to be exported by the entry point index.d.ts @@ -39,11 +40,10 @@ export interface EmbeddableSetup extends PersistableStateService; -// Warning: (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "EnhancementRegistryDefinition" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface EnhancementRegistryDefinition

extends PersistableStateDefinition

{ +export interface EnhancementRegistryDefinition

extends PersistableStateDefinition

{ // (undocumented) id: string; } diff --git a/src/plugins/embeddable/server/types.ts b/src/plugins/embeddable/server/types.ts index a07d036e1bdd8..ba5314692e430 100644 --- a/src/plugins/embeddable/server/types.ts +++ b/src/plugins/embeddable/server/types.ts @@ -6,22 +6,19 @@ * Side Public License, v 1. */ -import { - PersistableState, - PersistableStateDefinition, - SerializableState, -} from '../../kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; +import { PersistableState, PersistableStateDefinition } from '../../kibana_utils/common'; import { EmbeddableStateWithType } from '../common/types'; export type EmbeddableFactoryRegistry = Map; export type EnhancementsRegistry = Map; -export interface EnhancementRegistryDefinition

+export interface EnhancementRegistryDefinition

extends PersistableStateDefinition

{ id: string; } -export interface EnhancementRegistryItem

+export interface EnhancementRegistryItem

extends PersistableState

{ id: string; } diff --git a/src/plugins/expressions/common/execution/types.ts b/src/plugins/expressions/common/execution/types.ts index 2d164778605ae..06eac98feba67 100644 --- a/src/plugins/expressions/common/execution/types.ts +++ b/src/plugins/expressions/common/execution/types.ts @@ -6,11 +6,12 @@ * Side Public License, v 1. */ +import type { SerializableRecord } from '@kbn/utility-types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import type { KibanaRequest } from 'src/core/server'; import type { KibanaExecutionContext } from 'src/core/public'; -import { ExpressionType, SerializableState } from '../expression_types'; +import { ExpressionType } from '../expression_types'; import { Adapters, RequestAdapter } from '../../../inspector/common'; import { TablesAdapter } from '../util/tables_adapter'; @@ -20,7 +21,7 @@ import { TablesAdapter } from '../util/tables_adapter'; */ export interface ExecutionContext< InspectorAdapters extends Adapters = Adapters, - ExecutionContextSearch extends SerializableState = SerializableState + ExecutionContextSearch extends SerializableRecord = SerializableRecord > { /** * Get search context of the expression. diff --git a/src/plugins/expressions/common/executor/executor.ts b/src/plugins/expressions/common/executor/executor.ts index 930c9a4f04243..2767c8bc6ecbe 100644 --- a/src/plugins/expressions/common/executor/executor.ts +++ b/src/plugins/expressions/common/executor/executor.ts @@ -10,6 +10,7 @@ import { cloneDeep, mapValues } from 'lodash'; import { Observable } from 'rxjs'; +import type { SerializableRecord } from '@kbn/utility-types'; import { ExecutorState, ExecutorContainer } from './container'; import { createExecutorContainer } from './container'; import { AnyExpressionFunctionDefinition, ExpressionFunction } from '../expression_functions'; @@ -25,7 +26,6 @@ import { MigrateFunctionsObject, migrateToLatest, PersistableStateService, - SerializableState, VersionedState, } from '../../../kibana_utils/common'; import { ExpressionExecutionParams } from '../service'; @@ -272,7 +272,7 @@ export class Executor = Record { if (!fn.migrations[version]) return link; const updatedAst = fn.migrations[version](link) as ExpressionAstFunction; diff --git a/src/plugins/expressions/common/expression_functions/expression_function.ts b/src/plugins/expressions/common/expression_functions/expression_function.ts index a4cb114110498..963d2186af733 100644 --- a/src/plugins/expressions/common/expression_functions/expression_function.ts +++ b/src/plugins/expressions/common/expression_functions/expression_function.ts @@ -7,12 +7,13 @@ */ import { identity } from 'lodash'; +import type { SerializableRecord } from '@kbn/utility-types'; import { AnyExpressionFunctionDefinition } from './types'; import { ExpressionFunctionParameter } from './expression_function_parameter'; import { ExpressionValue } from '../expression_types/types'; import { ExpressionAstFunction } from '../ast'; import { SavedObjectReference } from '../../../../core/types'; -import { PersistableState, SerializableState } from '../../../kibana_utils/common'; +import { PersistableState } from '../../../kibana_utils/common'; export class ExpressionFunction implements PersistableState { /** @@ -65,7 +66,7 @@ export class ExpressionFunction implements PersistableState ExpressionAstFunction['arguments']; migrations: { - [key: string]: (state: SerializableState) => SerializableState; + [key: string]: (state: SerializableRecord) => SerializableRecord; }; constructor(functionDefinition: AnyExpressionFunctionDefinition) { diff --git a/src/plugins/expressions/common/expression_types/specs/datatable.ts b/src/plugins/expressions/common/expression_types/specs/datatable.ts index a094ce39d6caa..c268557936ac5 100644 --- a/src/plugins/expressions/common/expression_types/specs/datatable.ts +++ b/src/plugins/expressions/common/expression_types/specs/datatable.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { SerializableRecord } from '@kbn/utility-types'; import { map, pick, zipObject } from 'lodash'; import { ExpressionTypeDefinition } from '../types'; @@ -13,13 +14,6 @@ import { PointSeries, PointSeriesColumn } from './pointseries'; import { ExpressionValueRender } from './render'; import { SerializedFieldFormat } from '../../types'; -type State = string | number | boolean | null | undefined | SerializableState; - -/** @internal **/ -export interface SerializableState { - [key: string]: State | State[]; -} - const name = 'datatable'; /** @@ -84,7 +78,7 @@ export interface DatatableColumnMeta { /** * any extra parameters for the source that produced this column */ - sourceParams?: SerializableState; + sourceParams?: SerializableRecord; } /** diff --git a/src/plugins/expressions/common/expression_types/specs/error.ts b/src/plugins/expressions/common/expression_types/specs/error.ts index f8a95628c9447..75e49633866f7 100644 --- a/src/plugins/expressions/common/expression_types/specs/error.ts +++ b/src/plugins/expressions/common/expression_types/specs/error.ts @@ -6,10 +6,10 @@ * Side Public License, v 1. */ +import type { SerializableRecord } from '@kbn/utility-types'; import { ExpressionTypeDefinition, ExpressionValueBoxed } from '../types'; import { ExpressionValueRender } from './render'; import { getType } from '../get_type'; -import { SerializableState } from '../../../../kibana_utils/common'; import { ErrorLike } from '../../util'; const name = 'error'; @@ -18,7 +18,7 @@ export type ExpressionValueError = ExpressionValueBoxed< 'error', { error: ErrorLike; - info?: SerializableState; + info?: SerializableRecord; } >; diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index 68d868d61ad05..b4dda3de5c93c 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -7,6 +7,7 @@ */ import { Observable } from 'rxjs'; +import type { SerializableRecord } from '@kbn/utility-types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import type { KibanaRequest } from 'src/core/server'; import type { KibanaExecutionContext } from 'src/core/public'; @@ -18,11 +19,7 @@ import { ExecutionContract, ExecutionResult } from '../execution'; import { AnyExpressionTypeDefinition, ExpressionValueError } from '../expression_types'; import { AnyExpressionFunctionDefinition } from '../expression_functions'; import { SavedObjectReference } from '../../../../core/types'; -import { - PersistableStateService, - SerializableState, - VersionedState, -} from '../../../kibana_utils/common'; +import { PersistableStateService, VersionedState } from '../../../kibana_utils/common'; import { Adapters } from '../../../inspector/common/adapters'; import { clog, @@ -60,7 +57,7 @@ export type ExpressionsServiceSetup = Pick< >; export interface ExpressionExecutionParams { - searchContext?: SerializableState; + searchContext?: SerializableRecord; variables?: Record; diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 4af6b4f1e797e..3126af02286c9 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -17,6 +17,7 @@ import { PackageInfo } from '@kbn/config'; import { Plugin as Plugin_2 } from 'src/core/public'; import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/public'; import React from 'react'; +import { SerializableRecord } from '@kbn/utility-types'; import { UnwrapObservable } from '@kbn/utility-types'; import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; @@ -133,11 +134,10 @@ export class Execution = StateContainer, ExecutionPureTransitions>; -// Warning: (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ExecutionContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export interface ExecutionContext { +export interface ExecutionContext { abortSignal: AbortSignal; getExecutionContext: () => KibanaExecutionContext | undefined; getKibanaRequest?: () => KibanaRequest; @@ -356,7 +356,7 @@ export class ExpressionFunction implements PersistableState SerializableState; + [key: string]: (state: SerializableRecord) => SerializableRecord; }; name: string; // (undocumented) @@ -764,7 +764,7 @@ export type ExpressionValueConverter; // Warning: (ae-missing-release-tag) "ExpressionValueFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -923,7 +923,7 @@ export interface IExpressionLoaderParams { // (undocumented) renderMode?: RenderMode; // (undocumented) - searchContext?: SerializableState_2; + searchContext?: SerializableRecord; // (undocumented) searchSessionId?: string; // (undocumented) @@ -1193,7 +1193,6 @@ export type UnmappedTypeStrings = 'date' | 'filter'; // Warnings were encountered during analysis: // // src/plugins/expressions/common/ast/types.ts:29:3 - (ae-forgotten-export) The symbol "ExpressionAstFunctionDebug" needs to be exported by the entry point index.d.ts -// src/plugins/expressions/common/expression_functions/expression_function.ts:68:5 - (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts // src/plugins/expressions/common/expression_types/specs/error.ts:20:5 - (ae-forgotten-export) The symbol "ErrorLike" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/expressions/public/types/index.ts b/src/plugins/expressions/public/types/index.ts index ce1381ba8ea43..5a2198bb4f2e5 100644 --- a/src/plugins/expressions/public/types/index.ts +++ b/src/plugins/expressions/public/types/index.ts @@ -5,13 +5,14 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ + +import type { SerializableRecord } from '@kbn/utility-types'; import type { KibanaExecutionContext } from 'src/core/public'; import { Adapters } from '../../../inspector/public'; import { IInterpreterRenderHandlers, ExpressionValue, ExpressionsService, - SerializableState, RenderMode, } from '../../common'; import { ExpressionRenderHandlerParams } from '../render'; @@ -33,7 +34,7 @@ export interface ExpressionInterpreter { } export interface IExpressionLoaderParams { - searchContext?: SerializableState; + searchContext?: SerializableRecord; context?: ExpressionValue; variables?: Record; // Enables debug tracking on each expression in the AST diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index 22280c1bd4b22..05b8cb1a033d1 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -14,6 +14,7 @@ import { Observable } from 'rxjs'; import { ObservableLike } from '@kbn/utility-types'; import { Plugin as Plugin_2 } from 'src/core/server'; import { PluginInitializerContext } from 'src/core/server'; +import { SerializableRecord } from '@kbn/utility-types'; import { UnwrapObservable } from '@kbn/utility-types'; import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; @@ -131,11 +132,10 @@ export class Execution = StateContainer, ExecutionPureTransitions>; -// Warning: (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ExecutionContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export interface ExecutionContext { +export interface ExecutionContext { abortSignal: AbortSignal; getExecutionContext: () => KibanaExecutionContext | undefined; getKibanaRequest?: () => KibanaRequest; @@ -328,7 +328,7 @@ export class ExpressionFunction implements PersistableState SerializableState; + [key: string]: (state: SerializableRecord) => SerializableRecord; }; name: string; // (undocumented) @@ -604,7 +604,7 @@ export type ExpressionValueConverter; // Warning: (ae-missing-release-tag) "ExpressionValueFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -947,7 +947,6 @@ export type UnmappedTypeStrings = 'date' | 'filter'; // Warnings were encountered during analysis: // // src/plugins/expressions/common/ast/types.ts:29:3 - (ae-forgotten-export) The symbol "ExpressionAstFunctionDebug" needs to be exported by the entry point index.d.ts -// src/plugins/expressions/common/expression_functions/expression_function.ts:68:5 - (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts // src/plugins/expressions/common/expression_types/specs/error.ts:20:5 - (ae-forgotten-export) The symbol "ErrorLike" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/kibana_utils/common/persistable_state/merge_migration_function_map.ts b/src/plugins/kibana_utils/common/persistable_state/merge_migration_function_map.ts index fc48ab119b02c..efcdc7cbc46a8 100644 --- a/src/plugins/kibana_utils/common/persistable_state/merge_migration_function_map.ts +++ b/src/plugins/kibana_utils/common/persistable_state/merge_migration_function_map.ts @@ -7,7 +7,8 @@ */ import { mergeWith } from 'lodash'; -import { MigrateFunctionsObject, MigrateFunction, SerializableState } from './types'; +import type { SerializableRecord } from '@kbn/utility-types'; +import { MigrateFunctionsObject, MigrateFunction } from './types'; export const mergeMigrationFunctionMaps = ( obj1: MigrateFunctionsObject, @@ -17,7 +18,7 @@ export const mergeMigrationFunctionMaps = ( if (!srcValue || !objValue) { return srcValue || objValue; } - return (state: SerializableState) => objValue(srcValue(state)); + return (state: SerializableRecord) => objValue(srcValue(state)); }; return mergeWith({ ...obj1 }, obj2, customizer); diff --git a/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.test.ts b/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.test.ts index 32fb652d41632..2a857b821aced 100644 --- a/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.test.ts +++ b/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.test.ts @@ -6,19 +6,20 @@ * Side Public License, v 1. */ -import { SerializableState, MigrateFunction } from './types'; +import { SerializableRecord } from '@kbn/utility-types'; +import { MigrateFunction } from './types'; import { migrateToLatest } from './migrate_to_latest'; -interface StateV1 extends SerializableState { +interface StateV1 extends SerializableRecord { name: string; } -interface StateV2 extends SerializableState { +interface StateV2 extends SerializableRecord { firstName: string; lastName: string; } -interface StateV3 extends SerializableState { +interface StateV3 extends SerializableRecord { firstName: string; lastName: string; isAdmin: boolean; diff --git a/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.ts b/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.ts index 6f81d0a7b9b63..9481e333819bd 100644 --- a/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.ts +++ b/src/plugins/kibana_utils/common/persistable_state/migrate_to_latest.ts @@ -7,9 +7,10 @@ */ import { compare } from 'semver'; -import { SerializableState, VersionedState, MigrateFunctionsObject } from './types'; +import { SerializableRecord } from '@kbn/utility-types'; +import { VersionedState, MigrateFunctionsObject } from './types'; -export function migrateToLatest( +export function migrateToLatest( migrations: MigrateFunctionsObject, { state, version: oldVersion }: VersionedState ): S { diff --git a/src/plugins/kibana_utils/common/persistable_state/types.ts b/src/plugins/kibana_utils/common/persistable_state/types.ts index a2d1751297a9f..6fea0a3a4eab6 100644 --- a/src/plugins/kibana_utils/common/persistable_state/types.ts +++ b/src/plugins/kibana_utils/common/persistable_state/types.ts @@ -6,18 +6,9 @@ * Side Public License, v 1. */ +import type { SerializableRecord } from '@kbn/utility-types'; import { SavedObjectReference } from '../../../../core/types'; -/** - * Serializable state is something is a POJO JavaScript object that can be - * serialized to a JSON string. - */ -export type SerializableState = { - [key: string]: Serializable; -}; -export type SerializableValue = string | number | boolean | null | undefined | SerializableState; -export type Serializable = SerializableValue | SerializableValue[]; - /** * Versioned state is a POJO JavaScript object that can be serialized to JSON, * and which also contains the version information. The version is stored in @@ -35,7 +26,7 @@ export type Serializable = SerializableValue | SerializableValue[]; * }; * ``` */ -export interface VersionedState { +export interface VersionedState { version: string; state: S; } @@ -50,7 +41,7 @@ export interface VersionedState * * @todo Maybe rename it to `PersistableStateItem`? */ -export interface PersistableState

{ +export interface PersistableState

{ /** * Function which reports telemetry information. This function is essentially * a "reducer" - it receives the existing "stats" object and returns an @@ -101,8 +92,8 @@ export interface PersistableState

}; export type MigrateFunction< - FromVersion extends SerializableState = SerializableState, - ToVersion extends SerializableState = SerializableState + FromVersion extends SerializableRecord = SerializableRecord, + ToVersion extends SerializableRecord = SerializableRecord > = (state: FromVersion) => ToVersion; /** @@ -111,21 +102,21 @@ export type MigrateFunction< * @param version */ export type PersistableStateMigrateFn = ( - state: SerializableState, + state: SerializableRecord, version: string -) => SerializableState; +) => SerializableRecord; /** * @todo Shall we remove this? */ -export type PersistableStateDefinition

= Partial< +export type PersistableStateDefinition

= Partial< PersistableState

>; /** * @todo Add description. */ -export interface PersistableStateService

{ +export interface PersistableStateService

{ /** * Function which reports telemetry information. This function is essentially * a "reducer" - it receives the existing "stats" object and returns an diff --git a/src/plugins/kibana_utils/public/ui/configurable.ts b/src/plugins/kibana_utils/public/ui/configurable.ts index 70f91915771f8..cf02b9e977d16 100644 --- a/src/plugins/kibana_utils/public/ui/configurable.ts +++ b/src/plugins/kibana_utils/public/ui/configurable.ts @@ -6,14 +6,14 @@ * Side Public License, v 1. */ +import type { SerializableRecord } from '@kbn/utility-types'; import { UiComponent } from '../../common/ui/ui_component'; -import { SerializableState } from '../../common'; /** * Represents something that can be configured by user using UI. */ export interface Configurable< - Config extends SerializableState = SerializableState, + Config extends SerializableRecord = SerializableRecord, Context = object > { /** @@ -36,7 +36,7 @@ export interface Configurable< * Props provided to `CollectConfig` component on every re-render. */ export interface CollectConfigProps< - Config extends SerializableState = SerializableState, + Config extends SerializableRecord = SerializableRecord, Context = object > { /** diff --git a/src/plugins/management/common/locator.ts b/src/plugins/management/common/locator.ts index 7dbf5e2888011..f56b2885be092 100644 --- a/src/plugins/management/common/locator.ts +++ b/src/plugins/management/common/locator.ts @@ -6,13 +6,13 @@ * Side Public License, v 1. */ -import { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import { LocatorDefinition, LocatorPublic } from 'src/plugins/share/common'; import { MANAGEMENT_APP_ID } from './contants'; export const MANAGEMENT_APP_LOCATOR = 'MANAGEMENT_APP_LOCATOR'; -export interface ManagementAppLocatorParams extends SerializableState { +export interface ManagementAppLocatorParams extends SerializableRecord { sectionId: string; appId?: string; } diff --git a/src/plugins/share/common/url_service/__tests__/setup.ts b/src/plugins/share/common/url_service/__tests__/setup.ts index fea3e1b945f99..1662b1f4a2d49 100644 --- a/src/plugins/share/common/url_service/__tests__/setup.ts +++ b/src/plugins/share/common/url_service/__tests__/setup.ts @@ -6,11 +6,11 @@ * Side Public License, v 1. */ -import type { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import { LocatorDefinition } from '../locators'; import { UrlService, UrlServiceDependencies } from '../url_service'; -export interface TestLocatorState extends SerializableState { +export interface TestLocatorState extends SerializableRecord { savedObjectId: string; showFlyout: boolean; pageNumber: number; diff --git a/src/plugins/share/common/url_service/locators/locator.ts b/src/plugins/share/common/url_service/locators/locator.ts index bae57b6d8a31d..a251e87702c7f 100644 --- a/src/plugins/share/common/url_service/locators/locator.ts +++ b/src/plugins/share/common/url_service/locators/locator.ts @@ -6,9 +6,10 @@ * Side Public License, v 1. */ +import type { SerializableRecord } from '@kbn/utility-types'; import type { SavedObjectReference } from 'kibana/server'; import { DependencyList } from 'react'; -import type { PersistableState, SerializableState } from 'src/plugins/kibana_utils/common'; +import type { PersistableState } from 'src/plugins/kibana_utils/common'; import { useLocatorUrl } from './use_locator_url'; import type { LocatorDefinition, @@ -30,7 +31,7 @@ export interface LocatorDependencies { getUrl: (location: KibanaLocation, getUrlParams: LocatorGetUrlParams) => Promise; } -export class Locator

implements LocatorPublic

{ +export class Locator

implements LocatorPublic

{ public readonly migrations: PersistableState

['migrations']; constructor( diff --git a/src/plugins/share/common/url_service/locators/locator_client.ts b/src/plugins/share/common/url_service/locators/locator_client.ts index fc6b23f94a386..587083551aa6d 100644 --- a/src/plugins/share/common/url_service/locators/locator_client.ts +++ b/src/plugins/share/common/url_service/locators/locator_client.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import type { LocatorDependencies } from './locator'; import type { LocatorDefinition, LocatorPublic, ILocatorClient } from './types'; import { Locator } from './locator'; @@ -27,7 +27,7 @@ export class LocatorClient implements ILocatorClient { * @param definition A definition of URL locator. * @returns A public interface of URL locator. */ - public create

(definition: LocatorDefinition

): LocatorPublic

{ + public create

(definition: LocatorDefinition

): LocatorPublic

{ const locator = new Locator

(definition, this.deps); this.locators.set(definition.id, locator); @@ -41,7 +41,7 @@ export class LocatorClient implements ILocatorClient { * @param id ID of a URL locator. * @returns A public interface of a registered URL locator. */ - public get

(id: string): undefined | LocatorPublic

{ + public get

(id: string): undefined | LocatorPublic

{ return this.locators.get(id); } } diff --git a/src/plugins/share/common/url_service/locators/types.ts b/src/plugins/share/common/url_service/locators/types.ts index 0429d52a8f52d..ba9c6c9185d23 100644 --- a/src/plugins/share/common/url_service/locators/types.ts +++ b/src/plugins/share/common/url_service/locators/types.ts @@ -6,8 +6,9 @@ * Side Public License, v 1. */ +import type { SerializableRecord } from '@kbn/utility-types'; import { DependencyList } from 'react'; -import { PersistableState, SerializableState } from 'src/plugins/kibana_utils/common'; +import { PersistableState } from 'src/plugins/kibana_utils/common'; /** * URL locator registry. @@ -18,20 +19,20 @@ export interface ILocatorClient { * * @param urlGenerator Definition of the new locator. */ - create

(locatorDefinition: LocatorDefinition

): LocatorPublic

; + create

(locatorDefinition: LocatorDefinition

): LocatorPublic

; /** * Retrieve a previously registered locator. * * @param id Unique ID of the locator. */ - get

(id: string): undefined | LocatorPublic

; + get

(id: string): undefined | LocatorPublic

; } /** * A convenience interface used to define and register a locator. */ -export interface LocatorDefinition

+export interface LocatorDefinition

extends Partial> { /** * Unique ID of the locator. Should be constant and unique across Kibana. @@ -50,7 +51,7 @@ export interface LocatorDefinition

/** * Public interface of a registered locator. */ -export interface LocatorPublic

extends PersistableState

{ +export interface LocatorPublic

extends PersistableState

{ /** * Returns a reference to a Kibana client-side location. * diff --git a/src/plugins/share/common/url_service/locators/use_locator_url.ts b/src/plugins/share/common/url_service/locators/use_locator_url.ts index a84c712e16248..a8fefc5010bcf 100644 --- a/src/plugins/share/common/url_service/locators/use_locator_url.ts +++ b/src/plugins/share/common/url_service/locators/use_locator_url.ts @@ -8,10 +8,10 @@ import { DependencyList, useEffect, useState } from 'react'; import useMountedState from 'react-use/lib/useMountedState'; -import { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import { LocatorGetUrlParams, LocatorPublic } from '../../../common/url_service'; -export const useLocatorUrl =

( +export const useLocatorUrl =

( locator: LocatorPublic

| null | undefined, params: P, getUrlParams?: LocatorGetUrlParams, diff --git a/src/plugins/share/public/mocks.ts b/src/plugins/share/public/mocks.ts index 7d21ef5c8fca0..3333878676e20 100644 --- a/src/plugins/share/public/mocks.ts +++ b/src/plugins/share/public/mocks.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { SerializableState } from 'src/plugins/kibana_utils/common'; +import { SerializableRecord } from '@kbn/utility-types'; import { SharePluginSetup, SharePluginStart } from '.'; import { LocatorPublic, UrlService } from '../common/url_service'; @@ -42,7 +42,7 @@ const createStartContract = (): Start => { return startContract; }; -const createLocator = (): jest.Mocked< +const createLocator = (): jest.Mocked< LocatorPublic > => ({ getLocation: jest.fn(), diff --git a/src/plugins/share/public/url_service/redirect/redirect_manager.ts b/src/plugins/share/public/url_service/redirect/redirect_manager.ts index ad99be43f678a..494fb623a48af 100644 --- a/src/plugins/share/public/url_service/redirect/redirect_manager.ts +++ b/src/plugins/share/public/url_service/redirect/redirect_manager.ts @@ -9,8 +9,8 @@ import type { CoreSetup } from 'src/core/public'; import { i18n } from '@kbn/i18n'; import { BehaviorSubject } from 'rxjs'; +import type { SerializableRecord } from '@kbn/utility-types'; import { migrateToLatest } from '../../../../kibana_utils/common'; -import type { SerializableState } from '../../../../kibana_utils/common'; import type { UrlService } from '../../../common/url_service'; import { render } from './render'; import { parseSearchParams } from './util/parse_search_params'; @@ -23,7 +23,7 @@ export interface RedirectOptions { version: string; /** Locator params. */ - params: unknown & SerializableState; + params: unknown & SerializableRecord; } export interface RedirectManagerDependencies { diff --git a/src/plugins/share/public/url_service/redirect/util/parse_search_params.ts b/src/plugins/share/public/url_service/redirect/util/parse_search_params.ts index a60c1d1b68a97..7745e6dad43b1 100644 --- a/src/plugins/share/public/url_service/redirect/util/parse_search_params.ts +++ b/src/plugins/share/public/url_service/redirect/util/parse_search_params.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import { i18n } from '@kbn/i18n'; import type { RedirectOptions } from '../redirect_manager'; @@ -63,7 +63,7 @@ export function parseSearchParams(urlSearch: string): RedirectOptions { throw new Error(message); } - let params: unknown & SerializableState; + let params: unknown & SerializableRecord; try { params = JSON.parse(paramsJson); } catch { diff --git a/src/plugins/ui_actions/public/public.api.md b/src/plugins/ui_actions/public/public.api.md index f8c473a3e2c0a..8e4e61d4cafc7 100644 --- a/src/plugins/ui_actions/public/public.api.md +++ b/src/plugins/ui_actions/public/public.api.md @@ -13,6 +13,7 @@ import { Plugin } from 'src/core/public'; import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/public'; import { PublicMethodsOf } from '@kbn/utility-types'; import React from 'react'; +import { SerializableRecord } from '@kbn/utility-types'; import { UiComponent } from 'src/plugins/kibana_utils/public'; // Warning: (ae-missing-release-tag) "Action" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts b/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts index 0f549584af672..043f6195cf7b4 100644 --- a/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts +++ b/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts @@ -8,7 +8,7 @@ import { flow } from 'lodash'; import { EmbeddableRegistryDefinition } from 'src/plugins/embeddable/server'; -import { SerializableState } from '../../../kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import { commonAddSupportOfDualIndexSelectionModeInTSVB, commonHideTSVBLastValueIndicator, @@ -18,42 +18,42 @@ import { commonMigrateTagCloud, } from '../migrations/visualization_common_migrations'; -const byValueAddSupportOfDualIndexSelectionModeInTSVB = (state: SerializableState) => { +const byValueAddSupportOfDualIndexSelectionModeInTSVB = (state: SerializableRecord) => { return { ...state, savedVis: commonAddSupportOfDualIndexSelectionModeInTSVB(state.savedVis), }; }; -const byValueHideTSVBLastValueIndicator = (state: SerializableState) => { +const byValueHideTSVBLastValueIndicator = (state: SerializableRecord) => { return { ...state, savedVis: commonHideTSVBLastValueIndicator(state.savedVis), }; }; -const byValueRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel = (state: SerializableState) => { +const byValueRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel = (state: SerializableRecord) => { return { ...state, savedVis: commonRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel(state.savedVis), }; }; -const byValueAddEmptyValueColorRule = (state: SerializableState) => { +const byValueAddEmptyValueColorRule = (state: SerializableRecord) => { return { ...state, savedVis: commonAddEmptyValueColorRule(state.savedVis), }; }; -const byValueMigrateVislibPie = (state: SerializableState) => { +const byValueMigrateVislibPie = (state: SerializableRecord) => { return { ...state, savedVis: commonMigrateVislibPie(state.savedVis), }; }; -const byValueMigrateTagcloud = (state: SerializableState) => { +const byValueMigrateTagcloud = (state: SerializableRecord) => { return { ...state, savedVis: commonMigrateTagCloud(state.savedVis), diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app1_hello_world_drilldown/app1_hello_world_drilldown.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app1_hello_world_drilldown/app1_hello_world_drilldown.tsx index d6979749b7efe..2db7255e607d0 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app1_hello_world_drilldown/app1_hello_world_drilldown.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/drilldowns/app1_hello_world_drilldown/app1_hello_world_drilldown.tsx @@ -7,13 +7,13 @@ import React from 'react'; import { EuiFieldText, EuiFormRow } from '@elastic/eui'; +import type { SerializableRecord } from '@kbn/utility-types'; import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public'; import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../../plugins/ui_actions_enhanced/public'; import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../../src/plugins/kibana_utils/public'; import { SAMPLE_APP1_CLICK_TRIGGER, SampleApp1ClickContext } from '../../triggers'; -import { SerializableState } from '../../../../../../src/plugins/kibana_utils/common'; -export interface Config extends SerializableState { +export interface Config extends SerializableRecord { name: string; } diff --git a/x-pack/plugins/alerting/common/alert_navigation.ts b/x-pack/plugins/alerting/common/alert_navigation.ts index 7c9e428f9a09e..6ac21232b51a5 100644 --- a/x-pack/plugins/alerting/common/alert_navigation.ts +++ b/x-pack/plugins/alerting/common/alert_navigation.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; export interface AlertUrlNavigation { path: string; } diff --git a/x-pack/plugins/alerting/public/alert_navigation_registry/types.ts b/x-pack/plugins/alerting/public/alert_navigation_registry/types.ts index 12ac906142647..ea36d0edd1366 100644 --- a/x-pack/plugins/alerting/public/alert_navigation_registry/types.ts +++ b/x-pack/plugins/alerting/public/alert_navigation_registry/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { SanitizedAlert } from '../../common'; /** diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts index dcde577bb5fd8..ed14c2dd7f0ae 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts @@ -8,7 +8,7 @@ import Boom from '@hapi/boom'; import { map, mapValues, fromPairs, has } from 'lodash'; import { KibanaRequest } from 'src/core/server'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { RuleTypeRegistry } from '../types'; import { SecurityPluginSetup } from '../../../security/server'; import { RegistryRuleType } from '../rule_type_registry'; diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts index 32b3bcc866577..c8000d9892678 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.ts @@ -6,7 +6,7 @@ */ import { remove } from 'lodash'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { EsQueryConfig, nodeBuilder, toElasticsearchQuery, KueryNode } from '@kbn/es-query'; import { RegistryAlertTypeWithAuth } from './alerting_authorization'; diff --git a/x-pack/plugins/alerting/server/routes/lib/rewrite_request_case.ts b/x-pack/plugins/alerting/server/routes/lib/rewrite_request_case.ts index f5455d1a63093..2874f9567231b 100644 --- a/x-pack/plugins/alerting/server/routes/lib/rewrite_request_case.ts +++ b/x-pack/plugins/alerting/server/routes/lib/rewrite_request_case.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { JsonValue } from '@kbn/common-utils'; +import { JsonValue } from '@kbn/utility-types'; type RenameAlertToRule = K extends `alertTypeId` ? `ruleTypeId` diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts index 6feb22b2ef15e..7401bbb6aabee 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { SerializableRecord } from '@kbn/utility-types'; import { functionWrapper } from '../../../../../../src/plugins/presentation_util/common/lib'; import { getFunctionErrors } from '../../../i18n'; import { csv } from './csv'; -import { Datatable, ExecutionContext, SerializableState } from 'src/plugins/expressions'; +import { Datatable, ExecutionContext } from 'src/plugins/expressions'; import { Adapters } from 'src/plugins/inspector'; const errors = getFunctionErrors().csv; @@ -39,7 +40,7 @@ one,1 two,2 fourty two,42`, }, - {} as ExecutionContext + {} as ExecutionContext ) ).toEqual(expected); }); @@ -55,7 +56,7 @@ two\t2 fourty two\t42`, delimiter: '\t', }, - {} as ExecutionContext + {} as ExecutionContext ) ).toEqual(expected); @@ -69,7 +70,7 @@ two%SPLIT%2 fourty two%SPLIT%42`, delimiter: '%SPLIT%', }, - {} as ExecutionContext + {} as ExecutionContext ) ).toEqual(expected); }); @@ -82,7 +83,7 @@ fourty two%SPLIT%42`, data: `name,number\rone,1\rtwo,2\rfourty two,42`, newline: '\r', }, - {} as ExecutionContext + {} as ExecutionContext ) ).toEqual(expected); }); @@ -106,7 +107,7 @@ fourty two%SPLIT%42`, data: `foo," bar ", baz, " buz " 1,2,3,4`, }, - {} as ExecutionContext + {} as ExecutionContext ) ).toEqual(expectedResult); }); @@ -134,7 +135,7 @@ fourty two%SPLIT%42`, 1," best ",3, " ok" " good", bad, better , " worst " `, }, - {} as ExecutionContext + {} as ExecutionContext ) ).toEqual(expectedResult); }); @@ -149,7 +150,7 @@ one|1 two.2 fourty two,42`, }, - {} as ExecutionContext + {} as ExecutionContext ); }).toThrow(new RegExp(errors.invalidInputCSV().message)); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.ts index 6f785f1b9d479..b64761bb5a822 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.ts @@ -8,8 +8,9 @@ import { functionWrapper } from '../../../../../../src/plugins/presentation_util/common/lib'; import { testTable, relationalTable } from './__fixtures__/test_tables'; import { dropdownControl } from './dropdownControl'; -import { ExecutionContext, SerializableState } from 'src/plugins/expressions'; +import { ExecutionContext } from 'src/plugins/expressions'; import { Adapters } from 'src/plugins/inspector'; +import { SerializableRecord } from '@kbn/utility-types'; describe('dropdownControl', () => { const fn = functionWrapper(dropdownControl); @@ -19,14 +20,14 @@ describe('dropdownControl', () => { fn( testTable, { filterColumn: 'name', valueColumn: 'name' }, - {} as ExecutionContext + {} as ExecutionContext ) ).toHaveProperty('type', 'render'); expect( fn( testTable, { filterColumn: 'name', valueColumn: 'name' }, - {} as ExecutionContext + {} as ExecutionContext ) ).toHaveProperty('as', 'dropdown_filter'); }); @@ -43,18 +44,21 @@ describe('dropdownControl', () => { fn( testTable, { valueColumn: 'name' }, - {} as ExecutionContext + {} as ExecutionContext )?.value?.choices ).toEqual(uniqueNames); }); it('returns an empty array when provided an invalid column', () => { expect( - fn(testTable, { valueColumn: 'foo' }, {} as ExecutionContext) - ?.value?.choices + fn( + testTable, + { valueColumn: 'foo' }, + {} as ExecutionContext + )?.value?.choices ).toEqual([]); expect( - fn(testTable, { valueColumn: '' }, {} as ExecutionContext) + fn(testTable, { valueColumn: '' }, {} as ExecutionContext) ?.value?.choices ).toEqual([]); }); @@ -67,7 +71,7 @@ describe('dropdownControl', () => { fn( relationalTable, { valueColumn: 'id', labelColumn: 'name' }, - {} as ExecutionContext + {} as ExecutionContext )?.value?.choices ).toEqual(expectedChoices); }); @@ -77,22 +81,28 @@ describe('dropdownControl', () => { describe('filterColumn', () => { it('sets which column the filter is applied to', () => { expect( - fn(testTable, { filterColumn: 'name' }, {} as ExecutionContext) - ?.value + fn( + testTable, + { filterColumn: 'name' }, + {} as ExecutionContext + )?.value ).toHaveProperty('column', 'name'); expect( fn( testTable, { filterColumn: 'name', valueColumn: 'price' }, - {} as ExecutionContext + {} as ExecutionContext )?.value ).toHaveProperty('column', 'name'); }); it('defaults to valueColumn if not provided', () => { expect( - fn(testTable, { valueColumn: 'price' }, {} as ExecutionContext) - ?.value + fn( + testTable, + { valueColumn: 'price' }, + {} as ExecutionContext + )?.value ).toHaveProperty('column', 'price'); }); }); diff --git a/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts index de8317f3f7b88..65904ab5826ff 100644 --- a/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts +++ b/x-pack/plugins/embeddable_enhanced/public/embeddables/embeddable_action_storage.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { SerializableRecord } from '@kbn/utility-types'; import { DynamicActionsState, UiActionsEnhancedAbstractActionStorage as AbstractActionStorage, @@ -15,12 +16,11 @@ import { EmbeddableOutput, IEmbeddable, } from '../../../../../src/plugins/embeddable/public'; -import { SerializableState } from '../../../../../src/plugins/kibana_utils/common'; export interface EmbeddableWithDynamicActionsInput extends EmbeddableInput { enhancements?: { dynamicActions: DynamicActionsState; - [key: string]: SerializableState; + [key: string]: SerializableRecord; }; } diff --git a/x-pack/plugins/global_search/common/types.ts b/x-pack/plugins/global_search/common/types.ts index a910cc5cd3ae7..fabae7ea01e8f 100644 --- a/x-pack/plugins/global_search/common/types.ts +++ b/x-pack/plugins/global_search/common/types.ts @@ -6,7 +6,7 @@ */ import { Observable } from 'rxjs'; -import { Serializable } from 'src/core/types'; +import { Serializable } from '@kbn/utility-types'; /** * Options provided to {@link GlobalSearchResultProvider | a result provider}'s `find` method. diff --git a/x-pack/plugins/graph/public/types/workspace_state.ts b/x-pack/plugins/graph/public/types/workspace_state.ts index e511a2eb5c779..86f05376b9526 100644 --- a/x-pack/plugins/graph/public/types/workspace_state.ts +++ b/x-pack/plugins/graph/public/types/workspace_state.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { FontawesomeIcon } from '../helpers/style_choices'; import { WorkspaceField, AdvancedSettings } from './app_state'; diff --git a/x-pack/plugins/index_lifecycle_management/public/locator.ts b/x-pack/plugins/index_lifecycle_management/public/locator.ts index 025946a095a6f..4df7cf85ddebb 100644 --- a/x-pack/plugins/index_lifecycle_management/public/locator.ts +++ b/x-pack/plugins/index_lifecycle_management/public/locator.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import { ManagementAppLocator } from 'src/plugins/management/common'; import { LocatorDefinition } from '../../../../src/plugins/share/public/'; import { @@ -17,7 +17,7 @@ import { PLUGIN } from '../common/constants'; export const ILM_LOCATOR_ID = 'ILM_LOCATOR_ID'; -export interface IlmLocatorParams extends SerializableState { +export interface IlmLocatorParams extends SerializableRecord { page: 'policies_list' | 'policy_edit' | 'policy_create'; policyName?: string; } diff --git a/x-pack/plugins/infra/common/typed_json.ts b/x-pack/plugins/infra/common/typed_json.ts index 44409ab433a60..fee846b437a7a 100644 --- a/x-pack/plugins/infra/common/typed_json.ts +++ b/x-pack/plugins/infra/common/typed_json.ts @@ -6,7 +6,7 @@ */ import * as rt from 'io-ts'; -import { JsonArray, JsonObject, JsonValue } from '@kbn/common-utils'; +import { JsonArray, JsonObject, JsonValue } from '@kbn/utility-types'; export { JsonArray, JsonObject, JsonValue }; diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx b/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx index ff9b749911c84..b927505a42c8a 100644 --- a/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx @@ -7,7 +7,7 @@ import React, { useMemo, useCallback, useEffect } from 'react'; import { noop } from 'lodash'; -import { JsonValue } from '@kbn/common-utils'; +import { JsonValue } from '@kbn/utility-types'; import { DataPublicPluginStart, esQuery, Filter } from '../../../../../../src/plugins/data/public'; import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; import { LogEntryCursor } from '../../../common/log_entry'; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/field_value.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/field_value.tsx index 9cffef270219e..e52d302a9193f 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/field_value.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/field_value.tsx @@ -7,7 +7,7 @@ import stringify from 'json-stable-stringify'; import React from 'react'; -import { JsonArray, JsonValue } from '@kbn/common-utils'; +import { JsonArray, JsonValue } from '@kbn/utility-types'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { ActiveHighlightMarker, highlightFieldValue, HighlightMarker } from './highlighting'; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx index 33e81756552d8..d8021aa0279d5 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { JsonValue } from '@kbn/common-utils'; +import { JsonValue } from '@kbn/utility-types'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { LogColumn } from '../../../../common/log_entry'; import { isFieldColumn, isHighlightFieldColumn } from '../../../utils/log_entry'; diff --git a/x-pack/plugins/infra/public/utils/log_column_render_configuration.tsx b/x-pack/plugins/infra/public/utils/log_column_render_configuration.tsx index a6adc716e02fb..ff4a24f1498a6 100644 --- a/x-pack/plugins/infra/public/utils/log_column_render_configuration.tsx +++ b/x-pack/plugins/infra/public/utils/log_column_render_configuration.tsx @@ -6,7 +6,7 @@ */ import { ReactNode } from 'react'; -import { JsonValue } from '@kbn/common-utils'; +import { JsonValue } from '@kbn/utility-types'; /** * Interface for common configuration properties, regardless of the column type. diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts index f33bcd2fcab0c..3cd435ab0f6e8 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -8,7 +8,7 @@ import type { estypes } from '@elastic/elasticsearch'; import { Lifecycle } from '@hapi/hapi'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { JsonArray, JsonValue } from '@kbn/common-utils'; +import { JsonArray, JsonValue } from '@kbn/utility-types'; import { RouteConfig, RouteMethod } from '../../../../../../../src/core/server'; import { PluginSetup as DataPluginSetup, diff --git a/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts index 9f2e9e2713bbc..4ad2fa656f9b2 100644 --- a/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts @@ -11,7 +11,7 @@ import { constant, identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import * as runtimeTypes from 'io-ts'; import { compact } from 'lodash'; -import { JsonArray } from '@kbn/common-utils'; +import { JsonArray } from '@kbn/utility-types'; import type { InfraPluginRequestHandlerContext } from '../../../types'; import { LogEntriesAdapter, diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts index f8268570710f2..f6be310d79ed2 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts @@ -6,7 +6,7 @@ */ import type { estypes } from '@elastic/elasticsearch'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import type { InfraPluginRequestHandlerContext } from '../../../types'; diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/get_metrics_aggregations.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/get_metrics_aggregations.ts index 33060f428b7ff..1f8760993c867 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/get_metrics_aggregations.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/get_metrics_aggregations.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { InventoryItemType, MetricsUIAggregation, diff --git a/x-pack/plugins/infra/server/services/log_entries/message/message.ts b/x-pack/plugins/infra/server/services/log_entries/message/message.ts index 2deee584f5187..fc547126b3b44 100644 --- a/x-pack/plugins/infra/server/services/log_entries/message/message.ts +++ b/x-pack/plugins/infra/server/services/log_entries/message/message.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonArray, JsonValue } from '@kbn/common-utils'; +import { JsonArray, JsonValue } from '@kbn/utility-types'; import { LogMessagePart } from '../../../../common/log_entry'; import { LogMessageFormattingCondition, diff --git a/x-pack/plugins/infra/server/services/log_entries/message/rule_types.ts b/x-pack/plugins/infra/server/services/log_entries/message/rule_types.ts index 56d1b38e7e390..65229a747e5ea 100644 --- a/x-pack/plugins/infra/server/services/log_entries/message/rule_types.ts +++ b/x-pack/plugins/infra/server/services/log_entries/message/rule_types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonValue } from '@kbn/common-utils'; +import { JsonValue } from '@kbn/utility-types'; export interface LogMessageFormattingRule { when: LogMessageFormattingCondition; diff --git a/x-pack/plugins/infra/server/utils/serialized_query.ts b/x-pack/plugins/infra/server/utils/serialized_query.ts index 4169e123d8532..b3b2569528aea 100644 --- a/x-pack/plugins/infra/server/utils/serialized_query.ts +++ b/x-pack/plugins/infra/server/utils/serialized_query.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; export const parseFilterQuery = ( filterQuery: string | null | undefined diff --git a/x-pack/plugins/infra/server/utils/typed_search_strategy.ts b/x-pack/plugins/infra/server/utils/typed_search_strategy.ts index 2482694474b0e..7dcda66e1bb98 100644 --- a/x-pack/plugins/infra/server/utils/typed_search_strategy.ts +++ b/x-pack/plugins/infra/server/utils/typed_search_strategy.ts @@ -7,7 +7,7 @@ import * as rt from 'io-ts'; import stringify from 'json-stable-stringify'; -import { JsonValue } from '@kbn/common-utils'; +import { JsonValue } from '@kbn/utility-types'; import { jsonValueRT } from '../../common/typed_json'; import { SearchStrategyError } from '../../common/search_strategies/common/errors'; import { ShardFailure } from './elasticsearch_runtime_types'; diff --git a/x-pack/plugins/ingest_pipelines/public/locator.ts b/x-pack/plugins/ingest_pipelines/public/locator.ts index bfcc2f0fc9ce2..e4d6bf3539492 100644 --- a/x-pack/plugins/ingest_pipelines/public/locator.ts +++ b/x-pack/plugins/ingest_pipelines/public/locator.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import { ManagementAppLocator } from 'src/plugins/management/common'; import { LocatorPublic, @@ -27,7 +27,7 @@ export enum INGEST_PIPELINES_PAGES { CLONE = 'pipeline_clone', } -interface IngestPipelinesBaseParams extends SerializableState { +interface IngestPipelinesBaseParams extends SerializableRecord { pipelineId: string; } export interface IngestPipelinesListParams extends Partial { diff --git a/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.ts b/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.ts index 4f21378cc8115..6f1ec38ea951a 100644 --- a/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.ts +++ b/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.ts @@ -6,7 +6,7 @@ */ import { EmbeddableRegistryDefinition } from 'src/plugins/embeddable/server'; -import { SerializableState } from '../../../../../src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import { DOC_TYPE } from '../../common'; import { commonRemoveTimezoneDateHistogramParam, @@ -25,7 +25,7 @@ export const lensEmbeddableFactory = (): EmbeddableRegistryDefinition => { return ({ ...lensState, attributes: migratedLensState, - } as unknown) as SerializableState; + } as unknown) as SerializableRecord; }, '7.14.0': (state) => { const lensState = (state as unknown) as { attributes: LensDocShape713 }; @@ -33,7 +33,7 @@ export const lensEmbeddableFactory = (): EmbeddableRegistryDefinition => { return ({ ...lensState, attributes: migratedLensState, - } as unknown) as SerializableState; + } as unknown) as SerializableRecord; }, }, }; diff --git a/x-pack/plugins/maps/public/locators.test.ts b/x-pack/plugins/maps/public/locators.test.ts index d6e82d1cdb601..838d272120324 100644 --- a/x-pack/plugins/maps/public/locators.test.ts +++ b/x-pack/plugins/maps/public/locators.test.ts @@ -8,7 +8,7 @@ import { LAYER_TYPE, SOURCE_TYPES, SCALING_TYPES } from '../common/constants'; import { esFilters } from '../../../../src/plugins/data/public'; import { MapsAppLocatorDefinition } from './locators'; -import { SerializableState } from '../../../../src/plugins/kibana_utils/common'; +import { SerializableRecord } from '@kbn/utility-types'; import { LayerDescriptor } from '../common/descriptor_types'; const MAP_ID: string = '2c9c1f60-1909-11e9-919b-ffe5949a18d2'; @@ -65,7 +65,7 @@ describe('visualize url generator', () => { }, ]; const location = await locator.getLocation({ - initialLayers: (initialLayers as unknown) as LayerDescriptor[] & SerializableState, + initialLayers: (initialLayers as unknown) as LayerDescriptor[] & SerializableRecord, }); expect(location).toMatchObject({ diff --git a/x-pack/plugins/maps/public/locators.ts b/x-pack/plugins/maps/public/locators.ts index 7e2be7c6c7ec9..9689be8c133d4 100644 --- a/x-pack/plugins/maps/public/locators.ts +++ b/x-pack/plugins/maps/public/locators.ts @@ -8,6 +8,7 @@ /* eslint-disable max-classes-per-file */ import rison from 'rison-node'; +import type { SerializableRecord } from '@kbn/utility-types'; import type { TimeRange, Filter, @@ -17,13 +18,12 @@ import type { } from '../../../../src/plugins/data/public'; import { esFilters } from '../../../../src/plugins/data/public'; import { setStateToKbnUrl } from '../../../../src/plugins/kibana_utils/public'; -import { SerializableState } from '../../../../src/plugins/kibana_utils/common'; import type { LocatorDefinition, LocatorPublic } from '../../../../src/plugins/share/public'; import type { LayerDescriptor } from '../common/descriptor_types'; import { INITIAL_LAYERS_KEY, APP_ID } from '../common/constants'; import { lazyLoadMapModules } from './lazy_load_bundle'; -export interface MapsAppLocatorParams extends SerializableState { +export interface MapsAppLocatorParams extends SerializableRecord { /** * If given, it will load the given map else will load the create a new map page. */ @@ -37,12 +37,12 @@ export interface MapsAppLocatorParams extends SerializableState { /** * Optionally set the initial Layers. */ - initialLayers?: LayerDescriptor[] & SerializableState; + initialLayers?: LayerDescriptor[] & SerializableRecord; /** * Optionally set the refresh interval. */ - refreshInterval?: RefreshInterval & SerializableState; + refreshInterval?: RefreshInterval & SerializableRecord; /** * Optionally apply filers. NOTE: if given and used in conjunction with `mapId`, and the @@ -101,7 +101,7 @@ export class MapsAppLocatorDefinition implements LocatorDefinition string; }).encode_array(initialLayers); path = `${path}&${INITIAL_LAYERS_KEY}=${encodeURIComponent(risonEncodedInitialLayers)}`; @@ -115,7 +115,7 @@ export class MapsAppLocatorDefinition implements LocatorDefinition { const location = await locator.getLocation({ filters: getData().query.filterManager.getFilters(), query: getData().query.queryString.getQuery(), - initialLayers: (initialLayers as unknown) as LayerDescriptor[] & SerializableState, + initialLayers: (initialLayers as unknown) as LayerDescriptor[] & SerializableRecord, timeRange: getData().query.timefilter.timefilter.getTime(), }); diff --git a/x-pack/plugins/maps/server/embeddable_migrations.ts b/x-pack/plugins/maps/server/embeddable_migrations.ts index 4bf39dc1f999c..2a53198d8d247 100644 --- a/x-pack/plugins/maps/server/embeddable_migrations.ts +++ b/x-pack/plugins/maps/server/embeddable_migrations.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SerializableState } from '../../../../src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import { MapSavedObjectAttributes } from '../common/map_saved_object_type'; import { moveAttribution } from '../common/migrations/move_attribution'; @@ -17,10 +17,10 @@ import { moveAttribution } from '../common/migrations/move_attribution'; * This is the embeddable migration registry. */ export const embeddableMigrations = { - '7.14.0': (state: SerializableState) => { + '7.14.0': (state: SerializableRecord) => { return { ...state, attributes: moveAttribution(state as { attributes: MapSavedObjectAttributes }), - } as SerializableState; + } as SerializableRecord; }, }; diff --git a/x-pack/plugins/ml/common/types/es_client.ts b/x-pack/plugins/ml/common/types/es_client.ts index 433deac02bc9c..b3d36283b5d5e 100644 --- a/x-pack/plugins/ml/common/types/es_client.ts +++ b/x-pack/plugins/ml/common/types/es_client.ts @@ -7,7 +7,7 @@ import { estypes } from '@elastic/elasticsearch'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { buildEsQuery, DslQuery } from '@kbn/es-query'; import { isPopulatedObject } from '../util/object_utils'; diff --git a/x-pack/plugins/ml/common/types/locator.ts b/x-pack/plugins/ml/common/types/locator.ts index 9d294e1323f72..bfb953777d857 100644 --- a/x-pack/plugins/ml/common/types/locator.ts +++ b/x-pack/plugins/ml/common/types/locator.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import type { LocatorPublic } from 'src/plugins/share/public'; import type { RefreshInterval, TimeRange } from '../../../../../src/plugins/data/common/query'; import type { JobId } from './anomaly_detection_jobs/job'; @@ -253,6 +253,6 @@ export type MlLocatorState = | FilterEditUrlState | MlGenericUrlState; -export type MlLocatorParams = MlLocatorState & SerializableState; +export type MlLocatorParams = MlLocatorState & SerializableRecord; export type MlLocator = LocatorPublic; diff --git a/x-pack/plugins/osquery/common/typed_json.ts b/x-pack/plugins/osquery/common/typed_json.ts index 7ef7469a5ebe7..3735778b87491 100644 --- a/x-pack/plugins/osquery/common/typed_json.ts +++ b/x-pack/plugins/osquery/common/typed_json.ts @@ -6,7 +6,7 @@ */ import { DslQuery, Filter } from '@kbn/es-query'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; export type ESQuery = | ESRangeQuery diff --git a/x-pack/plugins/reporting/public/management/ilm_policy_link.tsx b/x-pack/plugins/reporting/public/management/ilm_policy_link.tsx index a40f167de5bc3..1dccb11dbbbc5 100644 --- a/x-pack/plugins/reporting/public/management/ilm_policy_link.tsx +++ b/x-pack/plugins/reporting/public/management/ilm_policy_link.tsx @@ -12,11 +12,11 @@ import { EuiButtonEmpty } from '@elastic/eui'; import type { ApplicationStart } from 'src/core/public'; import { ILM_POLICY_NAME } from '../../common/constants'; -import { LocatorPublic, SerializableState } from '../shared_imports'; +import { LocatorPublic, SerializableRecord } from '../shared_imports'; interface Props { navigateToUrl: ApplicationStart['navigateToUrl']; - locator: LocatorPublic; + locator: LocatorPublic; } const i18nTexts = { diff --git a/x-pack/plugins/reporting/public/shared_imports.ts b/x-pack/plugins/reporting/public/shared_imports.ts index 02717351e315f..a18ceaf151c7d 100644 --- a/x-pack/plugins/reporting/public/shared_imports.ts +++ b/x-pack/plugins/reporting/public/shared_imports.ts @@ -19,7 +19,7 @@ import { useKibana as _useKibana } from '../../../../src/plugins/kibana_react/pu import { KibanaContext } from './types'; export const useKibana = () => _useKibana(); -export type { SerializableState } from 'src/plugins/kibana_utils/common'; +export type { SerializableRecord } from '@kbn/utility-types'; export type { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public'; diff --git a/x-pack/plugins/security_solution/common/typed_json.ts b/x-pack/plugins/security_solution/common/typed_json.ts index 1c42ab3a6fd24..c1d281eccb1fa 100644 --- a/x-pack/plugins/security_solution/common/typed_json.ts +++ b/x-pack/plugins/security_solution/common/typed_json.ts @@ -7,7 +7,7 @@ import { DslQuery, Filter } from '@kbn/es-query'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; export type ESQuery = | ESRangeQuery diff --git a/x-pack/plugins/security_solution/public/common/lib/keury/index.ts b/x-pack/plugins/security_solution/public/common/lib/keury/index.ts index 13db6e94d2eea..1e286931bf799 100644 --- a/x-pack/plugins/security_solution/public/common/lib/keury/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/keury/index.ts @@ -7,7 +7,7 @@ import { isEmpty, isString, flow } from 'lodash/fp'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { EsQueryConfig, Query, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts index 70e74356188c7..424be81e6e56a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts @@ -6,7 +6,7 @@ */ import type { IScopedClusterClient } from 'kibana/server'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { parseFilterQuery } from '../../../../utils/serialized_query'; import { SafeResolverEvent } from '../../../../../common/endpoint/types'; import { PaginationBuilder } from '../utils/pagination'; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/descendants.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/descendants.ts index 331f622951515..ba9ac98cbffe4 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/descendants.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/descendants.ts @@ -7,7 +7,7 @@ import type { ApiResponse, estypes } from '@elastic/elasticsearch'; import { IScopedClusterClient } from 'src/core/server'; -import { JsonObject, JsonValue } from '@kbn/common-utils'; +import { JsonObject, JsonValue } from '@kbn/utility-types'; import { FieldsObject, ResolverSchema } from '../../../../../../common/endpoint/types'; import { NodeID, TimeRange, docValueFields, validIDs } from '../utils/index'; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/lifecycle.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/lifecycle.ts index 7de038ccc9ae4..0ea8f672aad64 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/lifecycle.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/lifecycle.ts @@ -6,7 +6,7 @@ */ import { IScopedClusterClient } from 'src/core/server'; -import { JsonObject, JsonValue } from '@kbn/common-utils'; +import { JsonObject, JsonValue } from '@kbn/utility-types'; import { FieldsObject, ResolverSchema } from '../../../../../../common/endpoint/types'; import { NodeID, TimeRange, docValueFields, validIDs } from '../utils/index'; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts index b64390f4e382f..5365fddbd436b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/tree/queries/stats.ts @@ -6,7 +6,7 @@ */ import { IScopedClusterClient } from 'src/core/server'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { EventStats, ResolverSchema } from '../../../../../../common/endpoint/types'; import { NodeID, TimeRange } from '../utils/index'; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts index 24fc447173ba6..caa28abe1abfd 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { SafeResolverEvent } from '../../../../../common/endpoint/types'; import { eventIDSafeVersion, diff --git a/x-pack/plugins/security_solution/server/utils/serialized_query.ts b/x-pack/plugins/security_solution/server/utils/serialized_query.ts index 7f8603ccab4b7..0c53de4b37f8a 100644 --- a/x-pack/plugins/security_solution/server/utils/serialized_query.ts +++ b/x-pack/plugins/security_solution/server/utils/serialized_query.ts @@ -7,7 +7,7 @@ import { isEmpty, isPlainObject, isString } from 'lodash/fp'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; export const parseFilterQuery = (filterQuery: string): JsonObject => { try { diff --git a/x-pack/plugins/task_manager/server/lib/calculate_health_status.ts b/x-pack/plugins/task_manager/server/lib/calculate_health_status.ts index 7a6bc59862100..d0e99690066dd 100644 --- a/x-pack/plugins/task_manager/server/lib/calculate_health_status.ts +++ b/x-pack/plugins/task_manager/server/lib/calculate_health_status.ts @@ -6,7 +6,7 @@ */ import { isString } from 'lodash'; -import { JsonValue } from '@kbn/common-utils'; +import { JsonValue } from '@kbn/utility-types'; import { HealthStatus, RawMonitoringStats } from '../monitoring'; import { TaskManagerConfig } from '../config'; diff --git a/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.ts b/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.ts index 03efcff10eb63..9cc223f63b196 100644 --- a/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.ts +++ b/x-pack/plugins/task_manager/server/monitoring/capacity_estimation.ts @@ -7,7 +7,7 @@ import { mapValues } from 'lodash'; import stats from 'stats-lite'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { RawMonitoringStats, RawMonitoredStat, HealthStatus } from './monitoring_stats_stream'; import { AveragedStat } from './task_run_calcultors'; import { TaskPersistenceTypes } from './task_run_statistics'; diff --git a/x-pack/plugins/task_manager/server/monitoring/ephemeral_task_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/ephemeral_task_statistics.ts index d1f3ef9c14055..2378c7f4606a6 100644 --- a/x-pack/plugins/task_manager/server/monitoring/ephemeral_task_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/ephemeral_task_statistics.ts @@ -6,7 +6,7 @@ */ import { map, filter, startWith, buffer, share } from 'rxjs/operators'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { combineLatest, Observable, zip } from 'rxjs'; import { isOk, Ok } from '../lib/result_type'; import { AggregatedStat, AggregatedStatProvider } from './runtime_statistics_aggregator'; diff --git a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts index 8e615fb861717..50d4b6af9a4cf 100644 --- a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts @@ -9,7 +9,7 @@ import { TaskManagerConfig } from '../config'; import { of, Subject } from 'rxjs'; import { take, bufferCount } from 'rxjs/operators'; import { createMonitoringStatsStream, AggregatedStat } from './monitoring_stats_stream'; -import { JsonValue } from '@kbn/common-utils'; +import { JsonValue } from '@kbn/utility-types'; beforeEach(() => { jest.resetAllMocks(); diff --git a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts index b187faf9e9648..fdddfc41e590a 100644 --- a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts +++ b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.ts @@ -9,7 +9,7 @@ import { merge, of, Observable } from 'rxjs'; import { map, scan } from 'rxjs/operators'; import { set } from '@elastic/safer-lodash-set'; import { Logger } from 'src/core/server'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { TaskStore } from '../task_store'; import { TaskPollingLifecycle } from '../polling_lifecycle'; import { diff --git a/x-pack/plugins/task_manager/server/monitoring/runtime_statistics_aggregator.ts b/x-pack/plugins/task_manager/server/monitoring/runtime_statistics_aggregator.ts index 799ea054596c0..872da8e0cbd50 100644 --- a/x-pack/plugins/task_manager/server/monitoring/runtime_statistics_aggregator.ts +++ b/x-pack/plugins/task_manager/server/monitoring/runtime_statistics_aggregator.ts @@ -6,7 +6,7 @@ */ import { Observable } from 'rxjs'; -import { JsonValue } from '@kbn/common-utils'; +import { JsonValue } from '@kbn/utility-types'; export interface AggregatedStat { key: string; diff --git a/x-pack/plugins/task_manager/server/monitoring/task_run_calcultors.ts b/x-pack/plugins/task_manager/server/monitoring/task_run_calcultors.ts index b0611437d87be..f65c28562d2b4 100644 --- a/x-pack/plugins/task_manager/server/monitoring/task_run_calcultors.ts +++ b/x-pack/plugins/task_manager/server/monitoring/task_run_calcultors.ts @@ -6,7 +6,7 @@ */ import stats from 'stats-lite'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { isUndefined, countBy, mapValues } from 'lodash'; export interface AveragedStat extends JsonObject { diff --git a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts index d43137d237a99..3946827827fee 100644 --- a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts @@ -7,7 +7,7 @@ import { combineLatest, Observable } from 'rxjs'; import { filter, startWith, map } from 'rxjs/operators'; -import { JsonObject, JsonValue } from '@kbn/common-utils'; +import { JsonObject, JsonValue } from '@kbn/utility-types'; import { isNumber, mapValues } from 'lodash'; import { AggregatedStatProvider, AggregatedStat } from './runtime_statistics_aggregator'; import { TaskLifecycleEvent } from '../polling_lifecycle'; diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts index 5c4e7d6cbe2cf..6f70df4b8c5c4 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.ts @@ -8,7 +8,7 @@ import { combineLatest, Observable, timer } from 'rxjs'; import { mergeMap, map, filter, switchMap, catchError } from 'rxjs/operators'; import { Logger } from 'src/core/server'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { keyBy, mapValues } from 'lodash'; import { estypes } from '@elastic/elasticsearch'; import { AggregatedStatProvider } from './runtime_statistics_aggregator'; diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/events/all/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/events/all/index.ts index 4f4d21484267b..c585d93330b20 100644 --- a/x-pack/plugins/timelines/common/search_strategy/timeline/events/all/index.ts +++ b/x-pack/plugins/timelines/common/search_strategy/timeline/events/all/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import type { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; import type { Ecs } from '../../../../ecs'; diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/events/details/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/events/details/index.ts index 7a344a163ebac..5bceb31081687 100644 --- a/x-pack/plugins/timelines/common/search_strategy/timeline/events/details/index.ts +++ b/x-pack/plugins/timelines/common/search_strategy/timeline/events/details/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; import { Inspect, Maybe } from '../../../common'; diff --git a/x-pack/plugins/timelines/common/typed_json.ts b/x-pack/plugins/timelines/common/typed_json.ts index c639c1c0322dc..679a68a16f700 100644 --- a/x-pack/plugins/timelines/common/typed_json.ts +++ b/x-pack/plugins/timelines/common/typed_json.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { DslQuery, Filter } from '@kbn/es-query'; diff --git a/x-pack/plugins/timelines/public/components/utils/keury/index.ts b/x-pack/plugins/timelines/public/components/utils/keury/index.ts index 391b15e8fdbac..fe3d8c4bf2e24 100644 --- a/x-pack/plugins/timelines/public/components/utils/keury/index.ts +++ b/x-pack/plugins/timelines/public/components/utils/keury/index.ts @@ -6,7 +6,7 @@ */ import { isEmpty, isString, flow } from 'lodash/fp'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { EsQueryConfig, Filter, Query } from '@kbn/es-query'; import { esQuery, esKuery, IIndexPattern } from '../../../../../../../src/plugins/data/public'; diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts index 0517dcfb64901..4297cd595e261 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { DocValueFields } from '../../../../../../common/search_strategy'; export const buildTimelineDetailsQuery = ( diff --git a/x-pack/plugins/ui_actions_enhanced/common/types.ts b/x-pack/plugins/ui_actions_enhanced/common/types.ts index f26134a236570..02cab5d17c0b2 100644 --- a/x-pack/plugins/ui_actions_enhanced/common/types.ts +++ b/x-pack/plugins/ui_actions_enhanced/common/types.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { SerializableState } from '../../../../src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; -export type BaseActionConfig = SerializableState; +export type BaseActionConfig = SerializableRecord; export type SerializedAction = { readonly factoryId: string; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.ts index 0e374010139f0..5a34a002bf4c3 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_manager/state/drilldown_manager_state.ts @@ -8,7 +8,7 @@ import useObservable from 'react-use/lib/useObservable'; import { BehaviorSubject } from 'rxjs'; import { map } from 'rxjs/operators'; -import { SerializableState } from 'src/plugins/kibana_utils/common'; +import type { SerializableRecord } from '@kbn/utility-types'; import { PublicDrilldownManagerProps, DrilldownManagerDependencies, @@ -352,7 +352,7 @@ export class DrilldownManagerState { const action: SerializedAction = { factoryId: template.factoryId, name, - config: (template.config || {}) as SerializableState, + config: (template.config || {}) as SerializableRecord, }; await dynamicActionManager.createEvent(action, template.triggers); } @@ -395,7 +395,7 @@ export class DrilldownManagerState { if (drilldownState) { drilldownState.setName(this.pickName(template.name)); drilldownState.setTriggers(template.triggers); - drilldownState.setConfig(template.config as SerializableState); + drilldownState.setConfig(template.config as SerializableRecord); } }; diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_enhancement.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_enhancement.ts index 26134b7e17bec..71ae3cfcc19e3 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_enhancement.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_enhancement.ts @@ -5,9 +5,9 @@ * 2.0. */ +import type { SerializableRecord } from '@kbn/utility-types'; import { EnhancementRegistryDefinition } from '../../../../../src/plugins/embeddable/public'; import { SavedObjectReference } from '../../../../../src/core/types'; -import { SerializableState } from '../../../../../src/plugins/kibana_utils/common'; import { DynamicActionsState } from '../../../ui_actions_enhanced/public'; import { UiActionsServiceEnhancements } from '../services'; @@ -16,14 +16,14 @@ export const dynamicActionEnhancement = ( ): EnhancementRegistryDefinition => { return { id: 'dynamicActions', - telemetry: (state: SerializableState, telemetryData: Record) => { + telemetry: (state: SerializableRecord, telemetryData: Record) => { return uiActionsEnhanced.telemetry(state as DynamicActionsState, telemetryData); }, - extract: (state: SerializableState) => { + extract: (state: SerializableRecord) => { return uiActionsEnhanced.extract(state as DynamicActionsState); }, - inject: (state: SerializableState, references: SavedObjectReference[]) => { + inject: (state: SerializableRecord, references: SavedObjectReference[]) => { return uiActionsEnhanced.inject(state as DynamicActionsState, references); }, - } as EnhancementRegistryDefinition; + } as EnhancementRegistryDefinition; }; diff --git a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts index 88e4809a70fa8..07cafef084a61 100644 --- a/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts +++ b/x-pack/plugins/ui_actions_enhanced/server/dynamic_action_enhancement.ts @@ -5,10 +5,10 @@ * 2.0. */ +import type { SerializableRecord } from '@kbn/utility-types'; import { EnhancementRegistryDefinition } from '../../../../src/plugins/embeddable/server'; import { SavedObjectReference } from '../../../../src/core/types'; import { ActionFactory, DynamicActionsState, SerializedEvent } from './types'; -import { SerializableState } from '../../../../src/plugins/kibana_utils/common'; import { dynamicActionsCollector } from './telemetry/dynamic_actions_collector'; import { dynamicActionFactoriesCollector } from './telemetry/dynamic_action_factories_collector'; @@ -17,14 +17,14 @@ export const dynamicActionEnhancement = ( ): EnhancementRegistryDefinition => { return { id: 'dynamicActions', - telemetry: (serializableState: SerializableState, stats: Record) => { + telemetry: (serializableState: SerializableRecord, stats: Record) => { const state = serializableState as DynamicActionsState; stats = dynamicActionsCollector(state, stats); stats = dynamicActionFactoriesCollector(getActionFactory, state, stats); return stats; }, - extract: (state: SerializableState) => { + extract: (state: SerializableRecord) => { const references: SavedObjectReference[] = []; const newState: DynamicActionsState = { events: (state as DynamicActionsState).events.map((event: SerializedEvent) => { @@ -41,7 +41,7 @@ export const dynamicActionEnhancement = ( }; return { state: newState, references }; }, - inject: (state: SerializableState, references: SavedObjectReference[]) => { + inject: (state: SerializableRecord, references: SavedObjectReference[]) => { return { events: (state as DynamicActionsState).events.map((event: SerializedEvent) => { const factory = getActionFactory(event.action.factoryId); diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index 249eaa33ec24e..8fe11000725a7 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -8,7 +8,7 @@ import { min } from 'lodash'; import datemath from '@elastic/datemath'; import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { UptimeAlertTypeFactory } from './types'; import { esKuery } from '../../../../../../src/plugins/data/server'; import { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts index 15e6fe30db186..5227b8ca7dcc2 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_status.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { UMElasticsearchQueryFn } from '../adapters'; diff --git a/x-pack/test/api_integration/apis/security_solution/events.ts b/x-pack/test/api_integration/apis/security_solution/events.ts index c99e41e011a9a..f6a668679b11d 100644 --- a/x-pack/test/api_integration/apis/security_solution/events.ts +++ b/x-pack/test/api_integration/apis/security_solution/events.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { Direction, diff --git a/x-pack/test/api_integration/apis/security_solution/utils.ts b/x-pack/test/api_integration/apis/security_solution/utils.ts index 9265a0066d208..c17a6abca27f1 100644 --- a/x-pack/test/api_integration/apis/security_solution/utils.ts +++ b/x-pack/test/api_integration/apis/security_solution/utils.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { JsonObject, JsonArray } from '@kbn/common-utils'; +import { JsonObject, JsonArray } from '@kbn/utility-types'; export const getFilterValue = (hostName: string, from: string, to: string): JsonObject => ({ bool: { diff --git a/x-pack/test/apm_api_integration/tests/services/annotations.ts b/x-pack/test/apm_api_integration/tests/services/annotations.ts index 34eadbe3c609c..a91fb59ce998c 100644 --- a/x-pack/test/apm_api_integration/tests/services/annotations.ts +++ b/x-pack/test/apm_api_integration/tests/services/annotations.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { merge, cloneDeep, isPlainObject } from 'lodash'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; diff --git a/x-pack/test/observability_api_integration/basic/tests/annotations.ts b/x-pack/test/observability_api_integration/basic/tests/annotations.ts index 4a2c7b68f612e..6b1cdb43f5f54 100644 --- a/x-pack/test/observability_api_integration/basic/tests/annotations.ts +++ b/x-pack/test/observability_api_integration/basic/tests/annotations.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export diff --git a/x-pack/test/observability_api_integration/trial/tests/annotations.ts b/x-pack/test/observability_api_integration/trial/tests/annotations.ts index b1ef717ddfd88..48b16b712bf3a 100644 --- a/x-pack/test/observability_api_integration/trial/tests/annotations.ts +++ b/x-pack/test/observability_api_integration/trial/tests/annotations.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { Annotation } from '../../../../plugins/observability/common/annotations'; import { FtrProviderContext } from '../../common/ftr_provider_context'; diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts index b3aeb55eb38a1..81299e1cc7e2b 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { eventsIndexPattern } from '../../../../plugins/security_solution/common/endpoint/constants'; import { eventIDSafeVersion, diff --git a/x-pack/test/timeline/security_and_spaces/tests/basic/events.ts b/x-pack/test/timeline/security_and_spaces/tests/basic/events.ts index 79f5768b9a3ba..12f5012b0b08c 100644 --- a/x-pack/test/timeline/security_and_spaces/tests/basic/events.ts +++ b/x-pack/test/timeline/security_and_spaces/tests/basic/events.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import expect from '@kbn/expect'; import { ALERT_ID, ALERT_OWNER } from '@kbn/rule-data-utils'; diff --git a/x-pack/test/timeline/security_and_spaces/tests/trial/events.ts b/x-pack/test/timeline/security_and_spaces/tests/trial/events.ts index 2ebb534c4a451..c51532967cd09 100644 --- a/x-pack/test/timeline/security_and_spaces/tests/trial/events.ts +++ b/x-pack/test/timeline/security_and_spaces/tests/trial/events.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import expect from '@kbn/expect'; import { ALERT_ID, ALERT_OWNER } from '@kbn/rule-data-utils'; diff --git a/x-pack/test/timeline/security_only/tests/basic/events.ts b/x-pack/test/timeline/security_only/tests/basic/events.ts index fc8ee95dedfc4..8c118de8f3287 100644 --- a/x-pack/test/timeline/security_only/tests/basic/events.ts +++ b/x-pack/test/timeline/security_only/tests/basic/events.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { ALERT_ID, ALERT_OWNER } from '@kbn/rule-data-utils'; import { getSpaceUrlPrefix } from '../../../../rule_registry/common/lib/authentication/spaces'; diff --git a/x-pack/test/timeline/security_only/tests/trial/events.ts b/x-pack/test/timeline/security_only/tests/trial/events.ts index fc8ee95dedfc4..8c118de8f3287 100644 --- a/x-pack/test/timeline/security_only/tests/trial/events.ts +++ b/x-pack/test/timeline/security_only/tests/trial/events.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import { ALERT_ID, ALERT_OWNER } from '@kbn/rule-data-utils'; import { getSpaceUrlPrefix } from '../../../../rule_registry/common/lib/authentication/spaces'; diff --git a/x-pack/test/timeline/spaces_only/tests/events.ts b/x-pack/test/timeline/spaces_only/tests/events.ts index 829d46905b6d1..2c2d221129721 100644 --- a/x-pack/test/timeline/spaces_only/tests/events.ts +++ b/x-pack/test/timeline/spaces_only/tests/events.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JsonObject } from '@kbn/common-utils'; +import { JsonObject } from '@kbn/utility-types'; import expect from '@kbn/expect'; import { ALERT_ID, ALERT_OWNER } from '@kbn/rule-data-utils'; diff --git a/yarn.lock b/yarn.lock index 7f42fc1902b11..c5f9de5dfe305 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2750,10 +2750,6 @@ version "0.0.0" uid "" -"@kbn/common-utils@link:bazel-bin/packages/kbn-common-utils": - version "0.0.0" - uid "" - "@kbn/config-schema@link:bazel-bin/packages/kbn-config-schema": version "0.0.0" uid "" From 8f22fedba05cfc1fb17d77e1d9ee308c308928b7 Mon Sep 17 00:00:00 2001 From: Khristinin Nikita Date: Tue, 10 Aug 2021 13:15:43 +0200 Subject: [PATCH 018/104] Update Styleguide path to .mdx (#107890) * Fix Styleduide URL STYLEGUIDE.md -> STYLEGUIDE.mdx * Fix type for a path to the scripts folder --- STYLEGUIDE.mdx | 2 +- dev_docs/best_practices.mdx | 2 +- docs/developer/best-practices/index.asciidoc | 2 +- docs/developer/contributing/linting.asciidoc | 2 +- docs/developer/contributing/pr-review.asciidoc | 2 +- examples/routing_example/README.md | 2 +- examples/routing_example/public/app.tsx | 3 ++- 7 files changed, 8 insertions(+), 7 deletions(-) diff --git a/STYLEGUIDE.mdx b/STYLEGUIDE.mdx index afe00476640b3..95f29c674da9b 100644 --- a/STYLEGUIDE.mdx +++ b/STYLEGUIDE.mdx @@ -35,7 +35,7 @@ remove it, don't simply comment it out. We are gradually moving the Kibana code base over to Prettier. All TypeScript code and some JavaScript code (check `.eslintrc.js`) is using Prettier to format code. You -can run `node script/eslint --fix` to fix linting issues and apply Prettier formatting. +can run `node scripts/eslint --fix` to fix linting issues and apply Prettier formatting. We recommend you to enable running ESLint via your IDE. Whenever possible we are trying to use Prettier and linting over written style guide rules. diff --git a/dev_docs/best_practices.mdx b/dev_docs/best_practices.mdx index d87c6eb618993..0bc86da6998dd 100644 --- a/dev_docs/best_practices.mdx +++ b/dev_docs/best_practices.mdx @@ -171,7 +171,7 @@ Kibana is translated into other languages. Use our i18n utilities to ensure your ## Styleguide -We use es-lint rules when possible, but please review our [styleguide](https://github.com/elastic/kibana/blob/master/STYLEGUIDE.md), which includes recommendations that can't be linted on. +We use es-lint rules when possible, but please review our [styleguide](https://github.com/elastic/kibana/blob/master/STYLEGUIDE.mdx), which includes recommendations that can't be linted on. Es-lint overrides on a per-plugin level are discouraged. diff --git a/docs/developer/best-practices/index.asciidoc b/docs/developer/best-practices/index.asciidoc index b048e59e6c98c..04422a613475a 100644 --- a/docs/developer/best-practices/index.asciidoc +++ b/docs/developer/best-practices/index.asciidoc @@ -45,7 +45,7 @@ guidelines] === Conventions * Become familiar with our -{kib-repo}blob/{branch}/STYLEGUIDE.md[styleguide] +{kib-repo}blob/{branch}/STYLEGUIDE.mdx[styleguide] (use Typescript!) * Write all new code on {kib-repo}blob/{branch}/src/core/README.md[the diff --git a/docs/developer/contributing/linting.asciidoc b/docs/developer/contributing/linting.asciidoc index eb7c22c517e4b..144e23c228535 100644 --- a/docs/developer/contributing/linting.asciidoc +++ b/docs/developer/contributing/linting.asciidoc @@ -2,7 +2,7 @@ == Linting A note about linting: We use http://eslint.org[eslint] to check that the -link:STYLEGUIDE.md[styleguide] is being followed. It runs in a +link:STYLEGUIDE.mdx[styleguide] is being followed. It runs in a pre-commit hook and as a part of the tests, but most contributors integrate it with their code editors for real-time feedback. diff --git a/docs/developer/contributing/pr-review.asciidoc b/docs/developer/contributing/pr-review.asciidoc index 885725795b0b9..95f012d569c3a 100644 --- a/docs/developer/contributing/pr-review.asciidoc +++ b/docs/developer/contributing/pr-review.asciidoc @@ -75,7 +75,7 @@ Reviewers are not simply evaluating the code itself, they are also evaluating th Having a relatively consistent codebase is an important part of us building a sustainable project. With dozens of active contributors at any given time, we rely on automation to help ensure consistency - we enforce a comprehensive set of linting rules through CI. We're also rolling out prettier to make this even more automatic. -For things that can't be easily automated, we maintain a link:{kib-repo}tree/{branch}/STYLEGUIDE.md[style guide] that authors should adhere to and reviewers should keep in mind when they review a pull request. +For things that can't be easily automated, we maintain a link:{kib-repo}tree/{branch}/STYLEGUIDE.mdx[style guide] that authors should adhere to and reviewers should keep in mind when they review a pull request. Beyond that, we're into subjective territory. Statements like "this isn't very readable" are hardly helpful since they can't be qualified, but that doesn't mean a reviewer should outright ignore code that is hard to understand due to how it is written. There isn't one definitively "best" way to write any particular code, so pursuing such shouldn't be our goal. Instead, reviewers and authors alike must accept that there are likely many different appropriate ways to accomplish the same thing with code, and so long as the contribution is utilizing one of those ways, then we're in good shape. diff --git a/examples/routing_example/README.md b/examples/routing_example/README.md index 0a88707bf70bb..1ac7540508360 100644 --- a/examples/routing_example/README.md +++ b/examples/routing_example/README.md @@ -6,4 +6,4 @@ Read more: - [IRouter API Docs](../../docs/development/core/server/kibana-plugin-core-server.irouter.md) - [HttpHandler (core.http.fetch) API Docs](../../docs/development/core/public/kibana-plugin-core-public.httphandler.md) -- [Routing Conventions](../../STYLEGUIDE.md#api-endpoints) \ No newline at end of file +- [Routing Conventions](../../STYLEGUIDE.mdx#api-endpoints) \ No newline at end of file diff --git a/examples/routing_example/public/app.tsx b/examples/routing_example/public/app.tsx index 7dadde2936e80..c0f01b0b6d534 100644 --- a/examples/routing_example/public/app.tsx +++ b/examples/routing_example/public/app.tsx @@ -63,7 +63,8 @@ function RoutingExplorer({ }, { label: 'Conventions', - href: 'https://github.com/elastic/kibana/tree/master/STYLEGUIDE.md#api-endpoints', + href: + 'https://github.com/elastic/kibana/tree/master/STYLEGUIDE.mdx#api-endpoints', iconType: 'logoGithub', target: '_blank', size: 's', From ea6c9045975d5caaaefc50120ac2d1ab821937a9 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 10 Aug 2021 14:17:56 +0300 Subject: [PATCH 019/104] [Vega] Add the ability to override runtime_mappings (#107875) * Add the ability to override runtime_mappings Closes: #107855 * runtime_mappings not correctly logged in Inspect panel. --- .../public/data_model/search_api.test.ts | 79 +++++++++++++++++++ .../public/data_model/search_api.ts | 50 +++++++----- .../public/vega_request_handler.ts | 5 +- .../vega_view/vega_map_view/view.test.ts | 1 + .../public/vega_visualization.test.js | 2 + 5 files changed, 116 insertions(+), 21 deletions(-) create mode 100644 src/plugins/vis_type_vega/public/data_model/search_api.test.ts diff --git a/src/plugins/vis_type_vega/public/data_model/search_api.test.ts b/src/plugins/vis_type_vega/public/data_model/search_api.test.ts new file mode 100644 index 0000000000000..d0739453e43ec --- /dev/null +++ b/src/plugins/vis_type_vega/public/data_model/search_api.test.ts @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { extendSearchParamsWithRuntimeFields } from './search_api'; +import { dataPluginMock } from '../../../data/public/mocks'; + +import { getSearchParamsFromRequest, DataPublicPluginStart } from '../../../data/public'; + +const mockComputedFields = ( + dataStart: DataPublicPluginStart, + index: string, + runtimeFields: Record +) => { + dataStart.indexPatterns.find = jest.fn().mockReturnValue([ + { + title: index, + getComputedFields: () => ({ + runtimeFields, + }), + }, + ]); +}; + +describe('extendSearchParamsWithRuntimeFields', () => { + let dataStart: DataPublicPluginStart; + + beforeEach(() => { + dataStart = dataPluginMock.createStartContract(); + }); + + test('should inject default runtime_mappings for known indexes', async () => { + const requestParams = {}; + const runtimeFields = { foo: {} }; + + mockComputedFields(dataStart, 'index', runtimeFields); + + expect( + await extendSearchParamsWithRuntimeFields(dataStart.indexPatterns, requestParams, 'index') + ).toMatchInlineSnapshot(` + Object { + "body": Object { + "runtime_mappings": Object { + "foo": Object {}, + }, + }, + } + `); + }); + + test('should use runtime mappings from spec if it is specified', async () => { + const requestParams = ({ + body: { + runtime_mappings: { + test: {}, + }, + }, + } as unknown) as ReturnType; + const runtimeFields = { foo: {} }; + + mockComputedFields(dataStart, 'index', runtimeFields); + + expect( + await extendSearchParamsWithRuntimeFields(dataStart.indexPatterns, requestParams, 'index') + ).toMatchInlineSnapshot(` + Object { + "body": Object { + "runtime_mappings": Object { + "test": Object {}, + }, + }, + } + `); + }); +}); diff --git a/src/plugins/vis_type_vega/public/data_model/search_api.ts b/src/plugins/vis_type_vega/public/data_model/search_api.ts index c6aba6eccdc9f..7a468b7bfce18 100644 --- a/src/plugins/vis_type_vega/public/data_model/search_api.ts +++ b/src/plugins/vis_type_vega/public/data_model/search_api.ts @@ -8,8 +8,7 @@ import { combineLatest, from } from 'rxjs'; import { map, tap, switchMap } from 'rxjs/operators'; -import { CoreStart, IUiSettingsClient } from 'kibana/public'; -import { getData } from '../services'; +import type { CoreStart, IUiSettingsClient } from 'kibana/public'; import { getSearchParamsFromRequest, SearchRequest, @@ -17,22 +16,28 @@ import { IEsSearchResponse, } from '../../../data/public'; import { search as dataPluginSearch } from '../../../data/public'; -import { VegaInspectorAdapters } from '../vega_inspector'; -import { RequestResponder } from '../../../inspector/public'; +import type { VegaInspectorAdapters } from '../vega_inspector'; +import type { RequestResponder } from '../../../inspector/public'; -const extendSearchParamsWithRuntimeFields = async ( +/** @internal **/ +export const extendSearchParamsWithRuntimeFields = async ( + indexPatterns: SearchAPIDependencies['indexPatterns'], requestParams: ReturnType, indexPatternString?: string ) => { if (indexPatternString) { - const indexPattern = (await getData().indexPatterns.find(indexPatternString)).find( - (index) => index.title === indexPatternString - ); - const runtimeFields = indexPattern?.getComputedFields().runtimeFields; + let runtimeMappings = requestParams.body?.runtime_mappings; + + if (!runtimeMappings) { + const indexPattern = (await indexPatterns.find(indexPatternString)).find( + (index) => index.title === indexPatternString + ); + runtimeMappings = indexPattern?.getComputedFields().runtimeFields; + } return { ...requestParams, - body: { ...requestParams.body, runtime_mappings: runtimeFields }, + body: { ...requestParams.body, runtime_mappings: runtimeMappings }, }; } @@ -43,6 +48,7 @@ export interface SearchAPIDependencies { uiSettings: IUiSettingsClient; injectedMetadata: CoreStart['injectedMetadata']; search: DataPublicPluginStart['search']; + indexPatterns: DataPublicPluginStart['indexPatterns']; } export class SearchAPI { @@ -54,7 +60,7 @@ export class SearchAPI { ) {} search(searchRequests: SearchRequest[]) { - const { search } = this.dependencies; + const { search, indexPatterns } = this.dependencies; const requestResponders: any = {}; return combineLatest( @@ -64,15 +70,19 @@ export class SearchAPI { getConfig: this.dependencies.uiSettings.get.bind(this.dependencies.uiSettings), }); - if (this.inspectorAdapters) { - requestResponders[requestId] = this.inspectorAdapters.requests.start(requestId, { - ...request, - searchSessionId: this.searchSessionId, - }); - requestResponders[requestId].json(requestParams.body); - } - - return from(extendSearchParamsWithRuntimeFields(requestParams, request.index)).pipe( + return from( + extendSearchParamsWithRuntimeFields(indexPatterns, requestParams, request.index) + ).pipe( + tap((params) => { + /** inspect request data **/ + if (this.inspectorAdapters) { + requestResponders[requestId] = this.inspectorAdapters.requests.start(requestId, { + ...request, + searchSessionId: this.searchSessionId, + }); + requestResponders[requestId].json(params.body); + } + }), switchMap((params) => search .search( diff --git a/src/plugins/vis_type_vega/public/vega_request_handler.ts b/src/plugins/vis_type_vega/public/vega_request_handler.ts index eba5f9a40c9d9..4c523714a2530 100644 --- a/src/plugins/vis_type_vega/public/vega_request_handler.ts +++ b/src/plugins/vis_type_vega/public/vega_request_handler.ts @@ -45,10 +45,13 @@ export function createVegaRequestHandler( searchSessionId, }: VegaRequestHandlerParams) { if (!searchAPI) { + const { search, indexPatterns } = getData(); + searchAPI = new SearchAPI( { uiSettings, - search: getData().search, + search, + indexPatterns, injectedMetadata: getInjectedMetadata(), }, context.abortSignal, diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts index ee3bf305e9427..17a098649ebbf 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.test.ts @@ -129,6 +129,7 @@ describe('vega_map_view/view', () => { JSON.stringify(vegaMap), new SearchAPI({ search: dataPluginStart.search, + indexPatterns: dataPluginStart.indexPatterns, uiSettings: coreStart.uiSettings, injectedMetadata: coreStart.injectedMetadata, }), diff --git a/src/plugins/vis_type_vega/public/vega_visualization.test.js b/src/plugins/vis_type_vega/public/vega_visualization.test.js index 776f8898b3e3a..ba1121b8894e0 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.test.js +++ b/src/plugins/vis_type_vega/public/vega_visualization.test.js @@ -92,6 +92,7 @@ describe('VegaVisualizations', () => { JSON.stringify(vegaliteGraph), new SearchAPI({ search: dataPluginStart.search, + indexPatterns: dataPluginStart.indexPatterns, uiSettings: coreStart.uiSettings, injectedMetadata: coreStart.injectedMetadata, }), @@ -123,6 +124,7 @@ describe('VegaVisualizations', () => { JSON.stringify(vegaGraph), new SearchAPI({ search: dataPluginStart.search, + indexPatterns: dataPluginStart.indexPatterns, uiSettings: coreStart.uiSettings, injectedMetadata: coreStart.injectedMetadata, }), From 2ddaddc2e958f60c6685ab9f9840cdd86da9d398 Mon Sep 17 00:00:00 2001 From: Marius Dragomir Date: Tue, 10 Aug 2021 13:33:43 +0200 Subject: [PATCH 020/104] [Sample Data] Add new Lens vis from eCommerce dashboard to the Vis Library (#107746) * Add ecommerce dashboard lens vis to library * layout and existing translations * remove old replace from maps * remove unused translation string Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../data_sets/ecommerce/saved_objects.ts | 1369 +++++++++++++++-- x-pack/plugins/maps/server/plugin.ts | 12 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 4 files changed, 1226 insertions(+), 157 deletions(-) diff --git a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts index 971b426458ce4..1f0ce6186bb8a 100644 --- a/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts +++ b/src/plugins/home/server/services/sample_data/data_sets/ecommerce/saved_objects.ts @@ -13,25 +13,45 @@ import { SavedObject } from 'kibana/server'; export const getSavedObjects = (): SavedObject[] => [ { - id: '45e07720-b890-11e8-a6d9-e546fe2bba5f', - type: 'visualization', - updated_at: '2021-07-16T20:14:25.894Z', - version: '3', + attributes: { + fieldAttrs: + '{"products.manufacturer":{"count":1},"products.price":{"count":1},"products.product_name":{"count":1},"total_quantity":{"count":1}}', + fieldFormatMap: + '{"taxful_total_price":{"id":"number","params":{"pattern":"$0,0.[00]"}},"products.price":{"id":"number","params":{"pattern":"$0,0.00"}},"taxless_total_price":{"id":"number","params":{"pattern":"$0,0.00"}},"products.taxless_price":{"id":"number","params":{"pattern":"$0,0.00"}},"products.taxful_price":{"id":"number","params":{"pattern":"$0,0.00"}},"products.min_price":{"id":"number","params":{"pattern":"$0,0.00"}},"products.base_unit_price":{"id":"number","params":{"pattern":"$0,0.00"}},"products.base_price":{"id":"number","params":{"pattern":"$0,0.00"}}}', + fields: '[]', + runtimeFieldMap: '{}', + timeFieldName: 'order_date', + title: 'kibana_sample_data_ecommerce', + typeMeta: '{}', + }, + coreMigrationVersion: '8.0.0', + id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', migrationVersion: { - visualization: '7.14.0', + 'index-pattern': '7.11.0', }, + references: [], + type: 'index-pattern', + updated_at: '2021-08-05T12:23:57.577Z', + version: 'WzI1LDFd', + }, + { attributes: { + description: '', + kibanaSavedObjectMeta: { + searchSourceJSON: '{"query":{"query":"","language":"kuery"},"filter":[]}', + }, title: i18n.translate('home.sampleData.ecommerceSpec.promotionTrackingTitle', { defaultMessage: '[eCommerce] Promotion Tracking', }), - visState: - '{"title":"[eCommerce] Promotion Tracking","type":"metrics","aggs":[],"params":{"time_range_mode":"entire_time_range","id":"61ca57f0-469d-11e7-af02-69e470af7417","type":"timeseries","series":[{"id":"ea20ae70-b88d-11e8-a451-f37365e9f268","color":"rgba(211,96,134,1)","split_mode":"everything","metrics":[{"id":"ea20ae71-b88d-11e8-a451-f37365e9f268","type":"sum","field":"taxful_total_price"}],"separate_axis":0,"axis_position":"right","formatter":"number","chart_type":"line","line_width":"2","point_size":"5","fill":"0","stacked":"none","filter":{"query":"products.product_name:*trouser*","language":"lucene"},"label":"Revenue Trousers","value_template":"${{value}}","split_color_mode":"gradient"},{"id":"062d77b0-b88e-11e8-a451-f37365e9f268","color":"rgba(84,179,153,1)","split_mode":"everything","metrics":[{"id":"062d77b1-b88e-11e8-a451-f37365e9f268","type":"sum","field":"taxful_total_price"}],"separate_axis":0,"axis_position":"right","formatter":"number","chart_type":"line","line_width":"2","point_size":"05","fill":"0","stacked":"none","filter":{"query":"products.product_name:*watch*","language":"lucene"},"label":"Revenue Watches","value_template":"${{value}}","split_color_mode":"gradient"},{"id":"61ca57f1-469d-11e7-af02-69e470af7417","color":"rgba(96,146,192,1)","split_mode":"everything","metrics":[{"id":"61ca57f2-469d-11e7-af02-69e470af7417","type":"sum","field":"taxful_total_price"}],"separate_axis":0,"axis_position":"right","formatter":"number","chart_type":"line","line_width":"2","point_size":"5","fill":"0","stacked":"none","filter":{"query":"products.product_name:*bag*","language":"lucene"},"label":"Revenue Bags","value_template":"${{value}}","split_color_mode":"gradient"},{"id":"faa2c170-b88d-11e8-a451-f37365e9f268","color":"rgba(202,142,174,1)","split_mode":"everything","metrics":[{"id":"faa2c171-b88d-11e8-a451-f37365e9f268","type":"sum","field":"taxful_total_price"}],"separate_axis":0,"axis_position":"right","formatter":"number","chart_type":"line","line_width":"2","point_size":"5","fill":"0","stacked":"none","filter":{"query":"products.product_name:*cocktail dress*","language":"lucene"},"label":"Revenue Cocktail Dresses","value_template":"${{value}}","split_color_mode":"gradient"}],"time_field":"order_date","interval":"12h","use_kibana_indexes":true,"axis_position":"left","axis_formatter":"number","axis_scale":"normal","show_legend":1,"show_grid":1,"legend_position":"bottom","annotations":[{"fields":"taxful_total_price","template":"Ring the bell! ${{taxful_total_price}}","query_string":{"query":"taxful_total_price:>250","language":"lucene"},"id":"c8c30be0-b88f-11e8-a451-f37365e9f268","color":"rgba(25,77,51,1)","time_field":"order_date","icon":"fa-bell","ignore_global_filters":1,"ignore_panel_filters":1,"index_pattern_ref_name":"metrics_1_index_pattern"}],"tooltip_mode":"show_all","drop_last_bucket":0,"isModelInvalid":false,"index_pattern_ref_name":"metrics_0_index_pattern"}}', uiStateJSON: '{}', - description: '', version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: '{"query":{"query":"","language":"kuery"},"filter":[]}', - }, + visState: + '{"title":"[eCommerce] Promotion Tracking","type":"metrics","aggs":[],"params":{"time_range_mode":"entire_time_range","id":"61ca57f0-469d-11e7-af02-69e470af7417","type":"timeseries","series":[{"id":"ea20ae70-b88d-11e8-a451-f37365e9f268","color":"rgba(211,96,134,1)","split_mode":"everything","metrics":[{"id":"ea20ae71-b88d-11e8-a451-f37365e9f268","type":"sum","field":"taxful_total_price"}],"separate_axis":0,"axis_position":"right","formatter":"number","chart_type":"line","line_width":"2","point_size":"5","fill":"0","stacked":"none","filter":{"query":"products.product_name:*trouser*","language":"lucene"},"label":"Revenue Trousers","value_template":"${{value}}","split_color_mode":"gradient"},{"id":"062d77b0-b88e-11e8-a451-f37365e9f268","color":"rgba(84,179,153,1)","split_mode":"everything","metrics":[{"id":"062d77b1-b88e-11e8-a451-f37365e9f268","type":"sum","field":"taxful_total_price"}],"separate_axis":0,"axis_position":"right","formatter":"number","chart_type":"line","line_width":"2","point_size":"05","fill":"0","stacked":"none","filter":{"query":"products.product_name:*watch*","language":"lucene"},"label":"Revenue Watches","value_template":"${{value}}","split_color_mode":"gradient"},{"id":"61ca57f1-469d-11e7-af02-69e470af7417","color":"rgba(96,146,192,1)","split_mode":"everything","metrics":[{"id":"61ca57f2-469d-11e7-af02-69e470af7417","type":"sum","field":"taxful_total_price"}],"separate_axis":0,"axis_position":"right","formatter":"number","chart_type":"line","line_width":"2","point_size":"5","fill":"0","stacked":"none","filter":{"query":"products.product_name:*bag*","language":"lucene"},"label":"Revenue Bags","value_template":"${{value}}","split_color_mode":"gradient"},{"id":"faa2c170-b88d-11e8-a451-f37365e9f268","color":"rgba(202,142,174,1)","split_mode":"everything","metrics":[{"id":"faa2c171-b88d-11e8-a451-f37365e9f268","type":"sum","field":"taxful_total_price"}],"separate_axis":0,"axis_position":"right","formatter":"number","chart_type":"line","line_width":"2","point_size":"5","fill":"0","stacked":"none","filter":{"query":"products.product_name:*cocktail dress*","language":"lucene"},"label":"Revenue Cocktail Dresses","value_template":"${{value}}","split_color_mode":"gradient"}],"time_field":"order_date","interval":"12h","use_kibana_indexes":true,"axis_position":"left","axis_formatter":"number","axis_scale":"normal","show_legend":1,"show_grid":1,"legend_position":"bottom","annotations":[{"fields":"taxful_total_price","template":"Ring the bell! ${{taxful_total_price}}","query_string":{"query":"taxful_total_price:>250","language":"lucene"},"id":"c8c30be0-b88f-11e8-a451-f37365e9f268","color":"rgba(25,77,51,1)","time_field":"order_date","icon":"fa-bell","ignore_global_filters":1,"ignore_panel_filters":1,"index_pattern_ref_name":"metrics_1_index_pattern"}],"tooltip_mode":"show_all","drop_last_bucket":0,"isModelInvalid":false,"index_pattern_ref_name":"metrics_0_index_pattern"}}', + }, + coreMigrationVersion: '8.0.0', + id: '45e07720-b890-11e8-a6d9-e546fe2bba5f', + migrationVersion: { + visualization: '7.14.0', }, references: [ { @@ -45,27 +65,28 @@ export const getSavedObjects = (): SavedObject[] => [ type: 'index-pattern', }, ], + type: 'visualization', + updated_at: '2021-08-05T12:23:57.577Z', + version: 'WzIxLDFd', }, { - id: 'b80e6540-b891-11e8-a6d9-e546fe2bba5f', - type: 'visualization', - updated_at: '2021-07-14T20:45:27.899Z', - version: '2', - migrationVersion: { - visualization: '7.14.0', - }, attributes: { + description: '', + kibanaSavedObjectMeta: { + searchSourceJSON: '{"query":{"query":"","language":"kuery"},"filter":[]}', + }, title: i18n.translate('home.sampleData.ecommerceSpec.soldProductsPerDayTitle', { defaultMessage: '[eCommerce] Sold Products per Day', }), - visState: - '{"title":"[eCommerce] Sold Products per Day","type":"metrics","aggs":[],"params":{"time_range_mode":"entire_time_range","id":"61ca57f0-469d-11e7-af02-69e470af7417","type":"gauge","series":[{"id":"61ca57f1-469d-11e7-af02-69e470af7417","color":"#68BC00","split_mode":"everything","metrics":[{"id":"61ca57f2-469d-11e7-af02-69e470af7417","type":"count"},{"id":"fd1e1b90-e4e3-11eb-8234-cb7bfd534fce","type":"math","variables":[{"id":"00374270-e4e4-11eb-8234-cb7bfd534fce","name":"c","field":"61ca57f2-469d-11e7-af02-69e470af7417"}],"script":"params.c / (params._interval / 1000 / 60 / 60 / 24)"}],"separate_axis":0,"axis_position":"right","formatter":"0.0","chart_type":"line","line_width":1,"point_size":1,"fill":0.5,"stacked":"none","label":"Trxns / day","split_color_mode":"gradient","value_template":""}],"time_field":"order_date","interval":"1d","axis_position":"left","axis_formatter":"number","axis_scale":"normal","show_legend":1,"show_grid":1,"gauge_color_rules":[{"value":150,"id":"6da070c0-b891-11e8-b645-195edeb9de84","gauge":"rgba(104,188,0,1)","operator":"gte"},{"value":150,"id":"9b0cdbc0-b891-11e8-b645-195edeb9de84","gauge":"rgba(244,78,59,1)","operator":"lt"}],"gauge_width":"15","gauge_inner_width":"10","gauge_style":"half","filter":"","gauge_max":"300","use_kibana_indexes":true,"hide_last_value_indicator":true,"tooltip_mode":"show_all","drop_last_bucket":0,"isModelInvalid":false,"index_pattern_ref_name":"metrics_0_index_pattern"}}', uiStateJSON: '{}', - description: '', version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: '{"query":{"query":"","language":"kuery"},"filter":[]}', - }, + visState: + '{"title":"[eCommerce] Sold Products per Day","type":"metrics","aggs":[],"params":{"time_range_mode":"entire_time_range","id":"61ca57f0-469d-11e7-af02-69e470af7417","type":"gauge","series":[{"id":"61ca57f1-469d-11e7-af02-69e470af7417","color":"#68BC00","split_mode":"everything","metrics":[{"id":"61ca57f2-469d-11e7-af02-69e470af7417","type":"count"},{"id":"fd1e1b90-e4e3-11eb-8234-cb7bfd534fce","type":"math","variables":[{"id":"00374270-e4e4-11eb-8234-cb7bfd534fce","name":"c","field":"61ca57f2-469d-11e7-af02-69e470af7417"}],"script":"params.c / (params._interval / 1000 / 60 / 60 / 24)"}],"separate_axis":0,"axis_position":"right","formatter":"0.0","chart_type":"line","line_width":1,"point_size":1,"fill":0.5,"stacked":"none","label":"Trxns / day","split_color_mode":"gradient","value_template":""}],"time_field":"order_date","interval":"1d","axis_position":"left","axis_formatter":"number","axis_scale":"normal","show_legend":1,"show_grid":1,"gauge_color_rules":[{"value":150,"id":"6da070c0-b891-11e8-b645-195edeb9de84","gauge":"rgba(104,188,0,1)","operator":"gte"},{"value":150,"id":"9b0cdbc0-b891-11e8-b645-195edeb9de84","gauge":"rgba(244,78,59,1)","operator":"lt"}],"gauge_width":"15","gauge_inner_width":"10","gauge_style":"half","filter":"","gauge_max":"300","use_kibana_indexes":true,"hide_last_value_indicator":true,"tooltip_mode":"show_all","drop_last_bucket":0,"isModelInvalid":false,"index_pattern_ref_name":"metrics_0_index_pattern"}}', + }, + coreMigrationVersion: '8.0.0', + id: 'b80e6540-b891-11e8-a6d9-e546fe2bba5f', + migrationVersion: { + visualization: '7.14.0', }, references: [ { @@ -74,21 +95,12 @@ export const getSavedObjects = (): SavedObject[] => [ type: 'index-pattern', }, ], + type: 'visualization', + updated_at: '2021-08-05T12:23:57.577Z', + version: 'WzIyLDFd', }, { - id: '3ba638e0-b894-11e8-a6d9-e546fe2bba5f', - type: 'search', - updated_at: '2021-07-16T20:05:53.880Z', - version: '2', - migrationVersion: { - search: '7.9.3', - }, attributes: { - title: i18n.translate('home.sampleData.ecommerceSpec.ordersTitle', { - defaultMessage: '[eCommerce] Orders', - }), - description: '', - hits: 0, columns: [ 'category', 'taxful_total_price', @@ -97,12 +109,22 @@ export const getSavedObjects = (): SavedObject[] => [ 'products.manufacturer', 'sku', ], - sort: [['order_date', 'desc']], - version: 1, + description: '', + hits: 0, kibanaSavedObjectMeta: { searchSourceJSON: '{"highlightAll":true,"version":true,"query":{"query":"","language":"kuery"},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}', }, + sort: [['order_date', 'desc']], + title: i18n.translate('home.sampleData.ecommerceSpec.ordersTitle', { + defaultMessage: '[eCommerce] Orders', + }), + version: 1, + }, + coreMigrationVersion: '8.0.0', + id: '3ba638e0-b894-11e8-a6d9-e546fe2bba5f', + migrationVersion: { + search: '7.9.3', }, references: [ { @@ -111,225 +133,1286 @@ export const getSavedObjects = (): SavedObject[] => [ type: 'index-pattern', }, ], + type: 'search', + updated_at: '2021-08-05T12:23:57.577Z', + version: 'WzIzLDFd', }, { - id: '9c6f83f0-bb4d-11e8-9c84-77068524bcab', - type: 'visualization', - updated_at: '2018-10-01T15:13:03.270Z', - version: '1', - migrationVersion: {}, attributes: { - title: i18n.translate('home.sampleData.ecommerceSpec.salesCountMapTitle', { - defaultMessage: '[eCommerce] Sales Count Map', - }), - visState: - '{"title":"[eCommerce] Sales Count Map","type":"vega","aggs":[],"params":{"spec":"{\\n $schema: https://vega.github.io/schema/vega/v5.json\\n config: {\\n kibana: {type: \\"map\\", latitude: 25, longitude: -40, zoom: 3}\\n }\\n data: [\\n {\\n name: table\\n url: {\\n index: kibana_sample_data_ecommerce\\n %context%: true\\n %timefield%: order_date\\n body: {\\n size: 0\\n aggs: {\\n gridSplit: {\\n geotile_grid: {field: \\"geoip.location\\", precision: 4, size: 10000}\\n aggs: {\\n gridCentroid: {\\n geo_centroid: {\\n field: \\"geoip.location\\"\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n format: {property: \\"aggregations.gridSplit.buckets\\"}\\n transform: [\\n {\\n type: geopoint\\n projection: projection\\n fields: [\\n gridCentroid.location.lon\\n gridCentroid.location.lat\\n ]\\n }\\n ]\\n }\\n ]\\n scales: [\\n {\\n name: gridSize\\n type: linear\\n domain: {data: \\"table\\", field: \\"doc_count\\"}\\n range: [\\n 50\\n 1000\\n ]\\n }\\n ]\\n marks: [\\n {\\n name: gridMarker\\n type: symbol\\n from: {data: \\"table\\"}\\n encode: {\\n update: {\\n size: {scale: \\"gridSize\\", field: \\"doc_count\\"}\\n xc: {signal: \\"datum.x\\"}\\n yc: {signal: \\"datum.y\\"}\\n }\\n }\\n },\\n {\\n name: gridLabel\\n type: text\\n from: {data: \\"table\\"}\\n encode: {\\n enter: {\\n fill: {value: \\"firebrick\\"}\\n text: {signal: \\"datum.doc_count\\"}\\n }\\n update: {\\n x: {signal: \\"datum.x\\"}\\n y: {signal: \\"datum.y\\"}\\n dx: {value: -6}\\n dy: {value: 6}\\n fontSize: {value: 18}\\n fontWeight: {value: \\"bold\\"}\\n }\\n }\\n }\\n ]\\n}"}}', - uiStateJSON: '{}', description: '', - version: 1, - kibanaSavedObjectMeta: { - searchSourceJSON: - '{"version":true,"query":{"query":"","language":"kuery"},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}', - }, + layerListJSON: + '[{"id":"0hmz5","alpha":1,"sourceDescriptor":{"type":"EMS_TMS","isAutoSelect":true},"visible":true,"style":{},"type":"VECTOR_TILE","minZoom":0,"maxZoom":24},{"id":"7ameq","label":null,"minZoom":0,"maxZoom":24,"alpha":1,"sourceDescriptor":{"type":"EMS_FILE","id":"world_countries","tooltipProperties":["name","iso2"]},"visible":true,"style":{"type":"VECTOR","properties":{"fillColor":{"type":"DYNAMIC","options":{"field":{"name":"__kbnjoin__count__741db9c6-8ebb-4ea9-9885-b6b4ac019d14","origin":"join"},"color":"Green to Red","fieldMetaOptions":{"isEnabled":false,"sigma":3}}},"lineColor":{"type":"STATIC","options":{"color":"#FFFFFF"}},"lineWidth":{"type":"STATIC","options":{"size":1}},"iconSize":{"type":"STATIC","options":{"size":10}},"symbolizeAs":{"options":{"value":"circle"}},"icon":{"type":"STATIC","options":{"value":"marker"}}}},"type":"VECTOR","joins":[{"leftField":"iso2","right":{"type":"ES_TERM_SOURCE","id":"741db9c6-8ebb-4ea9-9885-b6b4ac019d14","indexPatternTitle":"kibana_sample_data_ecommerce","term":"geoip.country_iso_code","indexPatternRefName":"layer_1_join_0_index_pattern","metrics":[{"type":"count","label":"sales count"}],"applyGlobalQuery":true}}]},{"id":"jmtgf","label":"United States","minZoom":0,"maxZoom":24,"alpha":1,"sourceDescriptor":{"type":"EMS_FILE","id":"usa_states","tooltipProperties":["name"]},"visible":true,"style":{"type":"VECTOR","properties":{"fillColor":{"type":"DYNAMIC","options":{"field":{"name":"__kbnjoin__count__30a0ec24-49b6-476a-b4ed-6c1636333695","origin":"join"},"color":"Blues","fieldMetaOptions":{"isEnabled":false,"sigma":3}}},"lineColor":{"type":"STATIC","options":{"color":"#FFFFFF"}},"lineWidth":{"type":"STATIC","options":{"size":1}},"iconSize":{"type":"STATIC","options":{"size":10}},"symbolizeAs":{"options":{"value":"circle"}},"icon":{"type":"STATIC","options":{"value":"marker"}}}},"type":"VECTOR","joins":[{"leftField":"name","right":{"type":"ES_TERM_SOURCE","id":"30a0ec24-49b6-476a-b4ed-6c1636333695","indexPatternTitle":"kibana_sample_data_ecommerce","term":"geoip.region_name","indexPatternRefName":"layer_2_join_0_index_pattern","metrics":[{"type":"count","label":"sales count"}],"applyGlobalQuery":true}}]},{"id":"ui5f8","label":"France","minZoom":0,"maxZoom":24,"alpha":1,"sourceDescriptor":{"type":"EMS_FILE","id":"france_departments","tooltipProperties":["label_en"]},"visible":true,"style":{"type":"VECTOR","properties":{"fillColor":{"type":"DYNAMIC","options":{"field":{"name":"__kbnjoin__count__e325c9da-73fa-4b3b-8b59-364b99370826","origin":"join"},"color":"Blues","fieldMetaOptions":{"isEnabled":false,"sigma":3}}},"lineColor":{"type":"STATIC","options":{"color":"#FFFFFF"}},"lineWidth":{"type":"STATIC","options":{"size":1}},"iconSize":{"type":"STATIC","options":{"size":10}},"symbolizeAs":{"options":{"value":"circle"}},"icon":{"type":"STATIC","options":{"value":"marker"}}}},"type":"VECTOR","joins":[{"leftField":"label_en","right":{"type":"ES_TERM_SOURCE","id":"e325c9da-73fa-4b3b-8b59-364b99370826","indexPatternTitle":"kibana_sample_data_ecommerce","term":"geoip.region_name","indexPatternRefName":"layer_3_join_0_index_pattern","metrics":[{"type":"count","label":"sales count"}],"applyGlobalQuery":true}}]},{"id":"y3fjb","label":"United Kingdom","minZoom":0,"maxZoom":24,"alpha":1,"sourceDescriptor":{"type":"EMS_FILE","id":"uk_subdivisions","tooltipProperties":["label_en"]},"visible":true,"style":{"type":"VECTOR","properties":{"fillColor":{"type":"DYNAMIC","options":{"field":{"name":"__kbnjoin__count__612d805d-8533-43a9-ac0e-cbf51fe63dcd","origin":"join"},"color":"Blues","fieldMetaOptions":{"isEnabled":false,"sigma":3}}},"lineColor":{"type":"STATIC","options":{"color":"#FFFFFF"}},"lineWidth":{"type":"STATIC","options":{"size":1}},"iconSize":{"type":"STATIC","options":{"size":10}},"symbolizeAs":{"options":{"value":"circle"}},"icon":{"type":"STATIC","options":{"value":"marker"}}}},"type":"VECTOR","joins":[{"leftField":"label_en","right":{"type":"ES_TERM_SOURCE","id":"612d805d-8533-43a9-ac0e-cbf51fe63dcd","indexPatternTitle":"kibana_sample_data_ecommerce","term":"geoip.region_name","indexPatternRefName":"layer_4_join_0_index_pattern","metrics":[{"type":"count","label":"sales count"}],"applyGlobalQuery":true}}]},{"id":"c54wk","label":"Sales","minZoom":9,"maxZoom":24,"alpha":1,"sourceDescriptor":{"id":"04c983b0-8cfa-4e6a-a64b-52c10b7008fe","type":"ES_SEARCH","geoField":"geoip.location","limit":2048,"filterByMapBounds":true,"tooltipProperties":["category","customer_gender","manufacturer","order_id","total_quantity","total_unique_products","taxful_total_price","order_date","geoip.region_name","geoip.country_iso_code"],"indexPatternRefName":"layer_5_source_index_pattern","applyGlobalQuery":true,"scalingType":"LIMIT"},"visible":true,"style":{"type":"VECTOR","properties":{"fillColor":{"type":"DYNAMIC","options":{"field":{"name":"taxful_total_price","origin":"source"},"color":"Greens","fieldMetaOptions":{"isEnabled":false,"sigma":3}}},"lineColor":{"type":"STATIC","options":{"color":"#FFFFFF"}},"lineWidth":{"type":"STATIC","options":{"size":1}},"iconSize":{"type":"STATIC","options":{"size":10}},"symbolizeAs":{"options":{"value":"circle"}},"icon":{"type":"STATIC","options":{"value":"marker"}}}},"type":"VECTOR"},{"id":"qvhh3","label":"Total Sales Revenue","minZoom":0,"maxZoom":9,"alpha":1,"sourceDescriptor":{"type":"ES_GEO_GRID","resolution":"COARSE","id":"aa7f87b8-9dc5-42be-b19e-1a2fa09b6cad","geoField":"geoip.location","requestType":"point","metrics":[{"type":"count","label":"sales count"},{"type":"sum","field":"taxful_total_price","label":"total sales price"}],"indexPatternRefName":"layer_6_source_index_pattern","applyGlobalQuery":true},"visible":true,"style":{"type":"VECTOR","properties":{"fillColor":{"type":"DYNAMIC","options":{"field":{"name":"doc_count","origin":"source"},"color":"Greens","fieldMetaOptions":{"isEnabled":false,"sigma":3}}},"lineColor":{"type":"STATIC","options":{"color":"#cccccc"}},"lineWidth":{"type":"STATIC","options":{"size":1}},"iconSize":{"type":"DYNAMIC","options":{"field":{"name":"sum_of_taxful_total_price","origin":"source"},"minSize":1,"maxSize":20,"fieldMetaOptions":{"isEnabled":false,"sigma":3}}},"labelText":{"type":"DYNAMIC","options":{"field":{"name":"sum_of_taxful_total_price","origin":"source"},"fieldMetaOptions":{"isEnabled":false,"sigma":3}}},"labelSize":{"type":"DYNAMIC","options":{"field":{"name":"sum_of_taxful_total_price","origin":"source"},"minSize":12,"maxSize":24,"fieldMetaOptions":{"isEnabled":false,"sigma":3}}},"labelBorderSize":{"options":{"size":"MEDIUM"}},"symbolizeAs":{"options":{"value":"circle"}},"icon":{"type":"STATIC","options":{"value":"marker"}}}},"type":"VECTOR"}]', + mapStateJSON: + '{"zoom":2.11,"center":{"lon":-15.07605,"lat":45.88578},"timeFilters":{"from":"now-7d","to":"now"},"refreshConfig":{"isPaused":true,"interval":0},"query":{"query":"","language":"kuery"},"settings":{"autoFitToDataBounds":false}}', + title: '[eCommerce] Orders by Country', + uiStateJSON: '{"isDarkMode":false}', + }, + coreMigrationVersion: '8.0.0', + id: '2c9c1f60-1909-11e9-919b-ffe5949a18d2', + migrationVersion: { + map: '7.14.0', }, references: [ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + name: 'layer_1_join_0_index_pattern', + type: 'index-pattern', + }, + { + id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + name: 'layer_2_join_0_index_pattern', + type: 'index-pattern', + }, + { + id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + name: 'layer_3_join_0_index_pattern', + type: 'index-pattern', + }, + { + id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + name: 'layer_4_join_0_index_pattern', + type: 'index-pattern', + }, + { + id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + name: 'layer_5_source_index_pattern', + type: 'index-pattern', + }, + { + id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + name: 'layer_6_source_index_pattern', type: 'index-pattern', }, ], + type: 'map', + updated_at: '2021-08-05T12:23:57.577Z', + version: 'WzI5LDFd', }, { - id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - type: 'index-pattern', - updated_at: '2021-07-16T20:08:12.675Z', - version: '2', - migrationVersion: { - 'index-pattern': '7.11.0', - }, attributes: { - title: 'kibana_sample_data_ecommerce', - timeFieldName: 'order_date', - fieldAttrs: - '{"products.manufacturer":{"count":1},"products.price":{"count":1},"products.product_name":{"count":1},"total_quantity":{"count":1}}', - fieldFormatMap: - '{"taxful_total_price":{"id":"number","params":{"pattern":"$0,0.[00]"}},"products.price":{"id":"number","params":{"pattern":"$0,0.00"}},"taxless_total_price":{"id":"number","params":{"pattern":"$0,0.00"}},"products.taxless_price":{"id":"number","params":{"pattern":"$0,0.00"}},"products.taxful_price":{"id":"number","params":{"pattern":"$0,0.00"}},"products.min_price":{"id":"number","params":{"pattern":"$0,0.00"}},"products.base_unit_price":{"id":"number","params":{"pattern":"$0,0.00"}},"products.base_price":{"id":"number","params":{"pattern":"$0,0.00"}}}', - fields: '[]', - runtimeFieldMap: '{}', - typeMeta: '{}', + description: '', + kibanaSavedObjectMeta: { + searchSourceJSON: '{"query":{"query":"","language":"kuery"},"filter":[]}', + }, + title: '[eCommerce] Markdown', + uiStateJSON: '{}', + version: 1, + visState: + '{"title":"[eCommerce] Markdown","type":"markdown","params":{"fontSize":12,"openLinksInNewTab":false,"markdown":"### Sample eCommerce Data\\nThis dashboard contains sample data for you to play with. You can view it, search it, and interact with the visualizations. For more information about Kibana, check our [docs](https://www.elastic.co/guide/en/kibana/current/index.html)."},"aggs":[]}', + }, + coreMigrationVersion: '8.0.0', + id: 'c00d1f90-f5ea-11eb-a78e-83aac3c38a60', + migrationVersion: { + visualization: '7.14.0', }, references: [], + type: 'visualization', + updated_at: '2021-08-05T12:43:35.817Z', + version: 'WzE3MSwxXQ==', }, { - id: '722b74f0-b882-11e8-a6d9-e546fe2bba5f', - type: 'dashboard', - updated_at: '2021-07-16T20:43:03.136Z', - version: '2', - references: [ - { - id: '45e07720-b890-11e8-a6d9-e546fe2bba5f', - name: '5:panel_5', - type: 'visualization', - }, - { - id: 'b80e6540-b891-11e8-a6d9-e546fe2bba5f', - name: '7:panel_7', - type: 'visualization', - }, - { - id: '3ba638e0-b894-11e8-a6d9-e546fe2bba5f', - name: '10:panel_10', - type: 'search', - }, - { - id: '9c6f83f0-bb4d-11e8-9c84-77068524bcab', - name: '11:panel_11', - type: 'map', + attributes: { + description: '', + kibanaSavedObjectMeta: { + searchSourceJSON: '{"query":{"query":"","language":"kuery"},"filter":[]}', }, + title: '[eCommerce] Controls', + uiStateJSON: '{}', + version: 1, + visState: + '{"title":"[eCommerce] Controls","type":"input_control_vis","params":{"controls":[{"id":"1536977437774","fieldName":"manufacturer.keyword","parent":"","label":"Manufacturer","type":"list","options":{"type":"terms","multiselect":true,"dynamicOptions":true,"size":5,"order":"desc"},"indexPatternRefName":"control_0_index_pattern"},{"id":"1536977465554","fieldName":"category.keyword","parent":"","label":"Category","type":"list","options":{"type":"terms","multiselect":true,"dynamicOptions":true,"size":5,"order":"desc"},"indexPatternRefName":"control_1_index_pattern"},{"id":"1536977596163","fieldName":"total_quantity","parent":"","label":"Quantity","type":"range","options":{"decimalPlaces":0,"step":1},"indexPatternRefName":"control_2_index_pattern"}],"updateFiltersOnChange":false,"useTimeFilter":true,"pinFilters":false},"aggs":[]}', + }, + coreMigrationVersion: '8.0.0', + id: 'c3378480-f5ea-11eb-a78e-83aac3c38a60', + migrationVersion: { + visualization: '7.14.0', + }, + references: [ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: - 'a5914d17-81fe-4f27-b240-23ac529c1499:control_a5914d17-81fe-4f27-b240-23ac529c1499_0_index_pattern', + name: 'control_0_index_pattern', type: 'index-pattern', }, { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: - 'a5914d17-81fe-4f27-b240-23ac529c1499:control_a5914d17-81fe-4f27-b240-23ac529c1499_1_index_pattern', + name: 'control_1_index_pattern', type: 'index-pattern', }, { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: - 'a5914d17-81fe-4f27-b240-23ac529c1499:control_a5914d17-81fe-4f27-b240-23ac529c1499_2_index_pattern', + name: 'control_2_index_pattern', type: 'index-pattern', }, + ], + type: 'visualization', + updated_at: '2021-08-05T12:43:41.128Z', + version: 'WzE3NiwxXQ==', + }, + { + attributes: { + state: { + datasourceStates: { + indexpattern: { + layers: { + 'c7478794-6767-4286-9d65-1c0ecd909dd8': { + columnOrder: [ + '8289349e-6d1b-4abf-b164-0208183d2c34', + '041db33b-5c9c-47f3-a5d3-ef5e255d1663', + '041db33b-5c9c-47f3-a5d3-ef5e255d1663X0', + '041db33b-5c9c-47f3-a5d3-ef5e255d1663X1', + ], + columns: { + '041db33b-5c9c-47f3-a5d3-ef5e255d1663': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: '% of target ($10k)', + operationType: 'formula', + params: { + format: { + id: 'percent', + params: { + decimals: 0, + }, + }, + formula: 'sum(taxful_total_price) / 10000 - 1', + isFormulaBroken: false, + }, + references: ['041db33b-5c9c-47f3-a5d3-ef5e255d1663X1'], + scale: 'ratio', + }, + '041db33b-5c9c-47f3-a5d3-ef5e255d1663X0': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Part of Weekly revenue', + operationType: 'sum', + scale: 'ratio', + sourceField: 'taxful_total_price', + }, + '041db33b-5c9c-47f3-a5d3-ef5e255d1663X1': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Part of Weekly revenue', + operationType: 'math', + params: { + tinymathAst: { + args: [ + { + args: ['041db33b-5c9c-47f3-a5d3-ef5e255d1663X0', 10000], + location: { + max: 32, + min: 0, + }, + name: 'divide', + text: 'sum(taxful_total_price) / 10000 ', + type: 'function', + }, + 1, + ], + location: { + max: 35, + min: 0, + }, + name: 'subtract', + text: 'sum(taxful_total_price) / 10000 - 1', + type: 'function', + }, + }, + references: ['041db33b-5c9c-47f3-a5d3-ef5e255d1663X0'], + scale: 'ratio', + }, + '8289349e-6d1b-4abf-b164-0208183d2c34': { + dataType: 'date', + isBucketed: true, + label: 'order_date', + operationType: 'date_histogram', + params: { + interval: '1d', + }, + scale: 'interval', + sourceField: 'order_date', + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + filters: [], + query: { + language: 'kuery', + query: '', + }, + visualization: { + axisTitlesVisibilitySettings: { + x: false, + yLeft: false, + yRight: true, + }, + fittingFunction: 'None', + gridlinesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + layers: [ + { + accessors: ['041db33b-5c9c-47f3-a5d3-ef5e255d1663'], + layerId: 'c7478794-6767-4286-9d65-1c0ecd909dd8', + seriesType: 'bar_stacked', + xAccessor: '8289349e-6d1b-4abf-b164-0208183d2c34', + }, + ], + legend: { + isVisible: true, + position: 'right', + }, + preferredSeriesType: 'bar_stacked', + tickLabelsVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + valueLabels: 'hide', + yLeftExtent: { + mode: 'full', + }, + yRightExtent: { + mode: 'full', + }, + }, + }, + title: '% of target revenue ($10k)', + visualizationType: 'lnsXY', + }, + coreMigrationVersion: '8.0.0', + id: 'c762b7a0-f5ea-11eb-a78e-83aac3c38a60', + migrationVersion: { + lens: '7.14.0', + }, + references: [ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: 'c65434d6-fe64-460f-b07a-c7d267c856ff:indexpattern-datasource-current-indexpattern', + name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', }, { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: - 'c65434d6-fe64-460f-b07a-c7d267c856ff:indexpattern-datasource-layer-c7478794-6767-4286-9d65-1c0ecd909dd8', + name: 'indexpattern-datasource-layer-c7478794-6767-4286-9d65-1c0ecd909dd8', type: 'index-pattern', }, + ], + type: 'lens', + updated_at: '2021-08-05T12:43:48.122Z', + version: 'WzE4NCwxXQ==', + }, + { + attributes: { + state: { + datasourceStates: { + indexpattern: { + layers: { + 'c7478794-6767-4286-9d65-1c0ecd909dd8': { + columnOrder: ['041db33b-5c9c-47f3-a5d3-ef5e255d1663'], + columns: { + '041db33b-5c9c-47f3-a5d3-ef5e255d1663': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Sum of revenue', + operationType: 'sum', + scale: 'ratio', + sourceField: 'taxful_total_price', + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + filters: [], + query: { + language: 'kuery', + query: '', + }, + visualization: { + accessor: '041db33b-5c9c-47f3-a5d3-ef5e255d1663', + layerId: 'c7478794-6767-4286-9d65-1c0ecd909dd8', + }, + }, + title: 'Sum of revenue', + visualizationType: 'lnsMetric', + }, + coreMigrationVersion: '8.0.0', + id: 'ce02e260-f5ea-11eb-a78e-83aac3c38a60', + migrationVersion: { + lens: '7.14.0', + }, + references: [ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: '2e6ef14d-7b03-46d4-a6b8-a962ee36a805:indexpattern-datasource-current-indexpattern', + name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', }, { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: - '2e6ef14d-7b03-46d4-a6b8-a962ee36a805:indexpattern-datasource-layer-c7478794-6767-4286-9d65-1c0ecd909dd8', + name: 'indexpattern-datasource-layer-c7478794-6767-4286-9d65-1c0ecd909dd8', type: 'index-pattern', }, + ], + type: 'lens', + updated_at: '2021-08-05T12:43:59.238Z', + version: 'WzE4OSwxXQ==', + }, + { + attributes: { + state: { + datasourceStates: { + indexpattern: { + layers: { + '4fb42a8e-b133-43c8-805c-a38472053938': { + columnOrder: ['020bbfdf-9ef8-4802-aa9e-342d2ea0bebf'], + columns: { + '020bbfdf-9ef8-4802-aa9e-342d2ea0bebf': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Median spending', + operationType: 'median', + scale: 'ratio', + sourceField: 'taxful_total_price', + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + filters: [], + query: { + language: 'kuery', + query: '', + }, + visualization: { + accessor: '020bbfdf-9ef8-4802-aa9e-342d2ea0bebf', + layerId: '4fb42a8e-b133-43c8-805c-a38472053938', + }, + }, + title: 'Median spending', + visualizationType: 'lnsMetric', + }, + coreMigrationVersion: '8.0.0', + id: 'd5f90030-f5ea-11eb-a78e-83aac3c38a60', + migrationVersion: { + lens: '7.14.0', + }, + references: [ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: '5108a3bc-d1cf-4255-8c95-2df52577b956:indexpattern-datasource-current-indexpattern', + name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', }, { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: - '5108a3bc-d1cf-4255-8c95-2df52577b956:indexpattern-datasource-layer-4fb42a8e-b133-43c8-805c-a38472053938', + name: 'indexpattern-datasource-layer-4fb42a8e-b133-43c8-805c-a38472053938', type: 'index-pattern', }, + ], + type: 'lens', + updated_at: '2021-08-05T12:44:12.595Z', + version: 'WzE5NywxXQ==', + }, + { + attributes: { + state: { + datasourceStates: { + indexpattern: { + layers: { + 'b6093a53-884f-42c2-9fcc-ba56cfb66c53': { + columnOrder: [ + '15c45f89-a149-443a-a830-aa8c3a9317db', + '2b41b3d8-2f62-407a-a866-960f254c679d', + 'eadae280-2da3-4d1d-a0e1-f9733f89c15b', + 'ddc92e50-4d5c-413e-b91b-3e504889fa65', + '5e31e5d3-2aaa-4475-a130-3b69bf2f748a', + ], + columns: { + '15c45f89-a149-443a-a830-aa8c3a9317db': { + dataType: 'date', + isBucketed: true, + label: 'order_date', + operationType: 'date_histogram', + params: { + interval: '1d', + }, + scale: 'interval', + sourceField: 'order_date', + }, + '2b41b3d8-2f62-407a-a866-960f254c679d': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Total items', + operationType: 'sum', + scale: 'ratio', + sourceField: 'products.quantity', + }, + '5e31e5d3-2aaa-4475-a130-3b69bf2f748a': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Tx. last week', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + timeShift: '1w', + }, + 'ddc92e50-4d5c-413e-b91b-3e504889fa65': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Transactions', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + 'eadae280-2da3-4d1d-a0e1-f9733f89c15b': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Last week', + operationType: 'sum', + scale: 'ratio', + sourceField: 'products.quantity', + timeShift: '1w', + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + filters: [], + query: { + language: 'kuery', + query: '', + }, + visualization: { + axisTitlesVisibilitySettings: { + x: false, + yLeft: false, + yRight: true, + }, + curveType: 'LINEAR', + fittingFunction: 'None', + gridlinesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + layers: [ + { + accessors: [ + '2b41b3d8-2f62-407a-a866-960f254c679d', + 'eadae280-2da3-4d1d-a0e1-f9733f89c15b', + '5e31e5d3-2aaa-4475-a130-3b69bf2f748a', + 'ddc92e50-4d5c-413e-b91b-3e504889fa65', + ], + layerId: 'b6093a53-884f-42c2-9fcc-ba56cfb66c53', + position: 'top', + seriesType: 'line', + showGridlines: false, + xAccessor: '15c45f89-a149-443a-a830-aa8c3a9317db', + yConfig: [ + { + color: '#b6e0d5', + forAccessor: 'eadae280-2da3-4d1d-a0e1-f9733f89c15b', + }, + { + color: '#edafc4', + forAccessor: '5e31e5d3-2aaa-4475-a130-3b69bf2f748a', + }, + ], + }, + ], + legend: { + isVisible: true, + position: 'right', + }, + preferredSeriesType: 'line', + tickLabelsVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + valueLabels: 'hide', + yLeftExtent: { + mode: 'full', + }, + yRightExtent: { + mode: 'full', + }, + }, + }, + title: 'Transactions per day', + visualizationType: 'lnsXY', + }, + coreMigrationVersion: '8.0.0', + id: 'dde978b0-f5ea-11eb-a78e-83aac3c38a60', + migrationVersion: { + lens: '7.14.0', + }, + references: [ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: '6bc3fa4a-8f1b-436f-afc1-f3516ee531ce:indexpattern-datasource-current-indexpattern', + name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', }, { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: - '6bc3fa4a-8f1b-436f-afc1-f3516ee531ce:indexpattern-datasource-layer-b6093a53-884f-42c2-9fcc-ba56cfb66c53', + name: 'indexpattern-datasource-layer-b6093a53-884f-42c2-9fcc-ba56cfb66c53', type: 'index-pattern', }, + ], + type: 'lens', + updated_at: '2021-08-05T12:44:25.915Z', + version: 'WzIwMywxXQ==', + }, + { + attributes: { + state: { + datasourceStates: { + indexpattern: { + layers: { + '667067a2-7cdf-4f0e-a9fe-eb4f4f1f2f17': { + columnOrder: ['c52c2003-ae58-4604-bae7-52ba0fb38a01'], + columns: { + 'c52c2003-ae58-4604-bae7-52ba0fb38a01': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Avg. items sold', + operationType: 'average', + params: { + format: { + id: 'number', + params: { + decimals: 1, + }, + }, + }, + scale: 'ratio', + sourceField: 'total_quantity', + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + filters: [], + query: { + language: 'kuery', + query: '', + }, + visualization: { + accessor: 'c52c2003-ae58-4604-bae7-52ba0fb38a01', + layerId: '667067a2-7cdf-4f0e-a9fe-eb4f4f1f2f17', + }, + }, + title: 'Avg. items sold', + visualizationType: 'lnsMetric', + }, + coreMigrationVersion: '8.0.0', + id: 'e3902840-f5ea-11eb-a78e-83aac3c38a60', + migrationVersion: { + lens: '7.14.0', + }, + references: [ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: '222c1f05-ca21-4e62-a04a-9a059b4534a7:indexpattern-datasource-current-indexpattern', + name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', }, { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: - '222c1f05-ca21-4e62-a04a-9a059b4534a7:indexpattern-datasource-layer-667067a2-7cdf-4f0e-a9fe-eb4f4f1f2f17', + name: 'indexpattern-datasource-layer-667067a2-7cdf-4f0e-a9fe-eb4f4f1f2f17', type: 'index-pattern', }, + ], + type: 'lens', + updated_at: '2021-08-05T12:44:35.396Z', + version: 'WzIwOSwxXQ==', + }, + { + attributes: { + state: { + datasourceStates: { + indexpattern: { + layers: { + '97c63ea6-9305-4755-97d1-0f26817c6f9a': { + columnOrder: [ + '9f61a7df-198e-4754-b34c-81ed544136ba', + 'ebcb19af-0900-4439-949f-d8cd9bccde19', + '5575214b-7f21-4b6c-8bc1-34433c6a0c58', + ], + columns: { + '5575214b-7f21-4b6c-8bc1-34433c6a0c58': { + dataType: 'number', + isBucketed: false, + label: 'Count of records', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + '9f61a7df-198e-4754-b34c-81ed544136ba': { + dataType: 'string', + isBucketed: true, + label: 'Top values of category.keyword', + operationType: 'terms', + params: { + missingBucket: false, + orderBy: { + columnId: '5575214b-7f21-4b6c-8bc1-34433c6a0c58', + type: 'column', + }, + orderDirection: 'desc', + otherBucket: true, + size: 10, + }, + scale: 'ordinal', + sourceField: 'category.keyword', + }, + 'ebcb19af-0900-4439-949f-d8cd9bccde19': { + dataType: 'date', + isBucketed: true, + label: 'order_date', + operationType: 'date_histogram', + params: { + interval: '1d', + }, + scale: 'interval', + sourceField: 'order_date', + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + filters: [], + query: { + language: 'kuery', + query: '', + }, + visualization: { + axisTitlesVisibilitySettings: { + x: false, + yLeft: false, + yRight: true, + }, + fittingFunction: 'None', + gridlinesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + layers: [ + { + accessors: ['5575214b-7f21-4b6c-8bc1-34433c6a0c58'], + layerId: '97c63ea6-9305-4755-97d1-0f26817c6f9a', + position: 'top', + seriesType: 'bar_percentage_stacked', + showGridlines: false, + splitAccessor: '9f61a7df-198e-4754-b34c-81ed544136ba', + xAccessor: 'ebcb19af-0900-4439-949f-d8cd9bccde19', + }, + ], + legend: { + isVisible: true, + position: 'right', + }, + preferredSeriesType: 'bar_percentage_stacked', + tickLabelsVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + valueLabels: 'inside', + yLeftExtent: { + mode: 'full', + }, + yRightExtent: { + mode: 'full', + }, + }, + }, + title: 'Breakdown by category', + visualizationType: 'lnsXY', + }, + coreMigrationVersion: '8.0.0', + id: 'eddf7850-f5ea-11eb-a78e-83aac3c38a60', + migrationVersion: { + lens: '7.14.0', + }, + references: [ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: 'a885226c-6830-4731-88a0-8c1d1047841e:indexpattern-datasource-current-indexpattern', + name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', }, { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: - 'a885226c-6830-4731-88a0-8c1d1047841e:indexpattern-datasource-layer-0731ee8b-31c5-4be9-92d9-69ee760465d7', + name: 'indexpattern-datasource-layer-97c63ea6-9305-4755-97d1-0f26817c6f9a', type: 'index-pattern', }, + ], + type: 'lens', + updated_at: '2021-08-05T12:44:52.693Z', + version: 'WzIxNSwxXQ==', + }, + { + attributes: { + state: { + datasourceStates: { + indexpattern: { + layers: { + '0731ee8b-31c5-4be9-92d9-69ee760465d7': { + columnOrder: [ + '7bf8f089-1542-40bd-b349-45fdfc309ac6', + '826b2f39-b616-40b2-a222-972fdc1d7596', + 'cfd45c47-fc41-430c-9e7a-b71dc0c916b0', + 'bf51c1af-443e-49f4-a21f-54c87bfc5677', + 'bf51c1af-443e-49f4-a21f-54c87bfc5677X0', + 'bf51c1af-443e-49f4-a21f-54c87bfc5677X1', + 'bf51c1af-443e-49f4-a21f-54c87bfc5677X2', + ], + columns: { + '7bf8f089-1542-40bd-b349-45fdfc309ac6': { + dataType: 'date', + isBucketed: true, + label: 'order_date', + operationType: 'date_histogram', + params: { + interval: '1d', + }, + scale: 'interval', + sourceField: 'order_date', + }, + '826b2f39-b616-40b2-a222-972fdc1d7596': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'This week', + operationType: 'sum', + scale: 'ratio', + sourceField: 'taxful_total_price', + }, + 'bf51c1af-443e-49f4-a21f-54c87bfc5677': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Difference', + operationType: 'formula', + params: { + format: { + id: 'number', + params: { + decimals: 2, + }, + }, + formula: "sum(taxful_total_price) - sum(taxful_total_price, shift='1w')", + isFormulaBroken: false, + }, + references: ['bf51c1af-443e-49f4-a21f-54c87bfc5677X2'], + scale: 'ratio', + }, + 'bf51c1af-443e-49f4-a21f-54c87bfc5677X0': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Part of Difference', + operationType: 'sum', + scale: 'ratio', + sourceField: 'taxful_total_price', + }, + 'bf51c1af-443e-49f4-a21f-54c87bfc5677X1': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Part of Difference', + operationType: 'sum', + scale: 'ratio', + sourceField: 'taxful_total_price', + timeShift: '1w', + }, + 'bf51c1af-443e-49f4-a21f-54c87bfc5677X2': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Part of Difference', + operationType: 'math', + params: { + tinymathAst: { + args: [ + 'bf51c1af-443e-49f4-a21f-54c87bfc5677X0', + 'bf51c1af-443e-49f4-a21f-54c87bfc5677X1', + ], + location: { + max: 61, + min: 0, + }, + name: 'subtract', + text: "sum(taxful_total_price) - sum(taxful_total_price, shift='1w')", + type: 'function', + }, + }, + references: [ + 'bf51c1af-443e-49f4-a21f-54c87bfc5677X0', + 'bf51c1af-443e-49f4-a21f-54c87bfc5677X1', + ], + scale: 'ratio', + }, + 'cfd45c47-fc41-430c-9e7a-b71dc0c916b0': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: '1 week ago', + operationType: 'sum', + scale: 'ratio', + sourceField: 'taxful_total_price', + timeShift: '1w', + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + filters: [], + query: { + language: 'kuery', + query: '', + }, + visualization: { + columns: [ + { + columnId: '7bf8f089-1542-40bd-b349-45fdfc309ac6', + }, + { + alignment: 'left', + columnId: '826b2f39-b616-40b2-a222-972fdc1d7596', + }, + { + columnId: 'cfd45c47-fc41-430c-9e7a-b71dc0c916b0', + }, + { + colorMode: 'text', + columnId: 'bf51c1af-443e-49f4-a21f-54c87bfc5677', + isTransposed: false, + palette: { + name: 'custom', + params: { + colorStops: [ + { + color: '#D36086', + stop: -10000, + }, + { + color: '#209280', + stop: 0, + }, + ], + continuity: 'above', + name: 'custom', + rangeMax: 0, + rangeMin: -10000, + rangeType: 'number', + steps: 5, + stops: [ + { + color: '#D36086', + stop: 0, + }, + { + color: '#209280', + stop: 2249.03125, + }, + ], + }, + type: 'palette', + }, + }, + ], + layerId: '0731ee8b-31c5-4be9-92d9-69ee760465d7', + }, + }, + title: 'Daily comparison', + visualizationType: 'lnsDatatable', + }, + coreMigrationVersion: '8.0.0', + id: 'ff6a21b0-f5ea-11eb-a78e-83aac3c38a60', + migrationVersion: { + lens: '7.14.0', + }, + references: [ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: '003bdfc7-4d9e-4bd0-b088-3b18f79588d1:indexpattern-datasource-current-indexpattern', + name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', }, { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: - '003bdfc7-4d9e-4bd0-b088-3b18f79588d1:indexpattern-datasource-layer-97c63ea6-9305-4755-97d1-0f26817c6f9a', + name: 'indexpattern-datasource-layer-0731ee8b-31c5-4be9-92d9-69ee760465d7', type: 'index-pattern', }, + ], + type: 'lens', + updated_at: '2021-08-05T12:45:22.123Z', + version: 'WzIyMiwxXQ==', + }, + { + attributes: { + state: { + datasourceStates: { + indexpattern: { + layers: { + '5ed846c2-a70b-4d9c-a244-f254bef763b8': { + columnOrder: [ + 'd77cdd24-dedc-48dd-9a4b-d34c6f1a6c46', + '7ac31901-277a-46e2-8128-8d684b2c1127', + ], + columns: { + '7ac31901-277a-46e2-8128-8d684b2c1127': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Items', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + 'd77cdd24-dedc-48dd-9a4b-d34c6f1a6c46': { + customLabel: true, + dataType: 'string', + isBucketed: true, + label: 'Product name', + operationType: 'terms', + params: { + missingBucket: false, + orderBy: { + columnId: '7ac31901-277a-46e2-8128-8d684b2c1127', + type: 'column', + }, + orderDirection: 'desc', + otherBucket: false, + size: 5, + }, + scale: 'ordinal', + sourceField: 'products.product_name.keyword', + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + filters: [], + query: { + language: 'kuery', + query: '', + }, + visualization: { + axisTitlesVisibilitySettings: { + x: false, + yLeft: true, + yRight: true, + }, + fittingFunction: 'None', + gridlinesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + layers: [ + { + accessors: ['7ac31901-277a-46e2-8128-8d684b2c1127'], + layerId: '5ed846c2-a70b-4d9c-a244-f254bef763b8', + position: 'top', + seriesType: 'bar_horizontal', + showGridlines: false, + xAccessor: 'd77cdd24-dedc-48dd-9a4b-d34c6f1a6c46', + }, + ], + legend: { + isVisible: true, + position: 'right', + }, + preferredSeriesType: 'bar_horizontal', + tickLabelsVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + valueLabels: 'inside', + yLeftExtent: { + mode: 'full', + }, + yRightExtent: { + mode: 'full', + }, + }, + }, + title: 'Top products this week', + visualizationType: 'lnsXY', + }, + coreMigrationVersion: '8.0.0', + id: '03071e90-f5eb-11eb-a78e-83aac3c38a60', + migrationVersion: { + lens: '7.14.0', + }, + references: [ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: 'b1697063-c817-4847-aa0d-5bed47137b7e:indexpattern-datasource-current-indexpattern', + name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', }, { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: - 'b1697063-c817-4847-aa0d-5bed47137b7e:indexpattern-datasource-layer-5ed846c2-a70b-4d9c-a244-f254bef763b8', + name: 'indexpattern-datasource-layer-5ed846c2-a70b-4d9c-a244-f254bef763b8', type: 'index-pattern', }, + ], + type: 'lens', + updated_at: '2021-08-05T12:45:28.185Z', + version: 'WzIyOCwxXQ==', + }, + { + attributes: { + state: { + datasourceStates: { + indexpattern: { + layers: { + '5ed846c2-a70b-4d9c-a244-f254bef763b8': { + columnOrder: [ + 'd77cdd24-dedc-48dd-9a4b-d34c6f1a6c46', + '7ac31901-277a-46e2-8128-8d684b2c1127', + ], + columns: { + '7ac31901-277a-46e2-8128-8d684b2c1127': { + customLabel: true, + dataType: 'number', + isBucketed: false, + label: 'Items', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + }, + 'd77cdd24-dedc-48dd-9a4b-d34c6f1a6c46': { + customLabel: true, + dataType: 'string', + isBucketed: true, + label: 'Product name', + operationType: 'terms', + params: { + missingBucket: false, + orderBy: { + columnId: '7ac31901-277a-46e2-8128-8d684b2c1127', + type: 'column', + }, + orderDirection: 'desc', + otherBucket: false, + size: 5, + }, + scale: 'ordinal', + sourceField: 'products.product_name.keyword', + }, + }, + incompleteColumns: {}, + }, + }, + }, + }, + filters: [], + query: { + language: 'kuery', + query: '', + }, + visualization: { + axisTitlesVisibilitySettings: { + x: false, + yLeft: true, + yRight: true, + }, + fittingFunction: 'None', + gridlinesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + layers: [ + { + accessors: ['7ac31901-277a-46e2-8128-8d684b2c1127'], + layerId: '5ed846c2-a70b-4d9c-a244-f254bef763b8', + position: 'top', + seriesType: 'bar_horizontal', + showGridlines: false, + xAccessor: 'd77cdd24-dedc-48dd-9a4b-d34c6f1a6c46', + }, + ], + legend: { + isVisible: true, + position: 'right', + }, + preferredSeriesType: 'bar_horizontal', + tickLabelsVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + valueLabels: 'inside', + yLeftExtent: { + mode: 'full', + }, + yRightExtent: { + mode: 'full', + }, + }, + }, + title: 'Top products last week', + visualizationType: 'lnsXY', + }, + coreMigrationVersion: '8.0.0', + id: '06379e00-f5eb-11eb-a78e-83aac3c38a60', + migrationVersion: { + lens: '7.14.0', + }, + references: [ { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: '562bb4bd-16b5-4c7e-9dfa-0f24cae6d1ba:indexpattern-datasource-current-indexpattern', + name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', }, { id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - name: - '562bb4bd-16b5-4c7e-9dfa-0f24cae6d1ba:indexpattern-datasource-layer-5ed846c2-a70b-4d9c-a244-f254bef763b8', + name: 'indexpattern-datasource-layer-5ed846c2-a70b-4d9c-a244-f254bef763b8', type: 'index-pattern', }, ], - migrationVersion: { - dashboard: '7.14.0', - }, + type: 'lens', + updated_at: '2021-08-05T12:45:33.536Z', + version: 'WzIzMywxXQ==', + }, + { attributes: { - title: i18n.translate('home.sampleData.ecommerceSpec.revenueDashboardTitle', { - defaultMessage: '[eCommerce] Revenue Dashboard', - }), - hits: 0, description: i18n.translate('home.sampleData.ecommerceSpec.revenueDashboardDescription', { defaultMessage: 'Analyze mock eCommerce orders and revenue', }), + hits: 0, + kibanaSavedObjectMeta: { + searchSourceJSON: '{"query":{"language":"kuery","query":""},"filter":[]}', + }, optionsJSON: '{"hidePanelTitles":false,"useMargins":true}', panelsJSON: - '[{"version":"7.14.0","type":"visualization","gridData":{"x":0,"y":22,"w":24,"h":10,"i":"5"},"panelIndex":"5","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_5"},{"version":"7.14.0","type":"visualization","gridData":{"x":36,"y":15,"w":12,"h":7,"i":"7"},"panelIndex":"7","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_7"},{"version":"7.14.0","type":"search","gridData":{"x":0,"y":55,"w":48,"h":18,"i":"10"},"panelIndex":"10","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_10"},{"version":"7.14.0","type":"map","gridData":{"x":0,"y":32,"w":24,"h":14,"i":"11"},"panelIndex":"11","embeddableConfig":{"isLayerTOCOpen":false,"enhancements":{},"mapCenter":{"lat":45.88578,"lon":-15.07605,"zoom":2.11},"mapBuffer":{"minLon":-90,"minLat":0,"maxLon":45,"maxLat":66.51326},"openTOCDetails":[],"hiddenLayers":[]},"panelRefName":"panel_11"},{"version":"7.14.0","type":"visualization","gridData":{"x":0,"y":0,"w":18,"h":7,"i":"585b11d3-3461-49a7-8f5b-f56521b9dc8b"},"panelIndex":"585b11d3-3461-49a7-8f5b-f56521b9dc8b","embeddableConfig":{"savedVis":{"title":"[eCommerce] Markdown","description":"","type":"markdown","params":{"fontSize":12,"openLinksInNewTab":false,"markdown":"### Sample eCommerce Data\\nThis dashboard contains sample data for you to play with. You can view it, search it, and interact with the visualizations. For more information about Kibana, check our [docs](https://www.elastic.co/guide/en/kibana/current/index.html)."},"uiState":{},"data":{"aggs":[],"searchSource":{"query":{"query":"","language":"kuery"},"filter":[]}}},"enhancements":{}}},{"version":"7.14.0","type":"visualization","gridData":{"x":18,"y":0,"w":30,"h":7,"i":"a5914d17-81fe-4f27-b240-23ac529c1499"},"panelIndex":"a5914d17-81fe-4f27-b240-23ac529c1499","embeddableConfig":{"savedVis":{"title":"[eCommerce] Controls","description":"","type":"input_control_vis","params":{"controls":[{"id":"1536977437774","fieldName":"manufacturer.keyword","parent":"","label":"Manufacturer","type":"list","options":{"type":"terms","multiselect":true,"dynamicOptions":true,"size":5,"order":"desc"},"indexPatternRefName":"control_a5914d17-81fe-4f27-b240-23ac529c1499_0_index_pattern"},{"id":"1536977465554","fieldName":"category.keyword","parent":"","label":"Category","type":"list","options":{"type":"terms","multiselect":true,"dynamicOptions":true,"size":5,"order":"desc"},"indexPatternRefName":"control_a5914d17-81fe-4f27-b240-23ac529c1499_1_index_pattern"},{"id":"1536977596163","fieldName":"total_quantity","parent":"","label":"Quantity","type":"range","options":{"decimalPlaces":0,"step":1},"indexPatternRefName":"control_a5914d17-81fe-4f27-b240-23ac529c1499_2_index_pattern"}],"updateFiltersOnChange":false,"useTimeFilter":true,"pinFilters":false},"uiState":{},"data":{"aggs":[],"searchSource":{"query":{"query":"","language":"kuery"},"filter":[]}}},"enhancements":{}}},{"version":"7.14.0","type":"lens","gridData":{"x":0,"y":7,"w":24,"h":8,"i":"c65434d6-fe64-460f-b07a-c7d267c856ff"},"panelIndex":"c65434d6-fe64-460f-b07a-c7d267c856ff","embeddableConfig":{"attributes":{"title":"","type":"lens","visualizationType":"lnsXY","state":{"datasourceStates":{"indexpattern":{"layers":{"c7478794-6767-4286-9d65-1c0ecd909dd8":{"columns":{"8289349e-6d1b-4abf-b164-0208183d2c34":{"label":"order_date","dataType":"date","operationType":"date_histogram","sourceField":"order_date","isBucketed":true,"scale":"interval","params":{"interval":"1d"}},"041db33b-5c9c-47f3-a5d3-ef5e255d1663X0":{"label":"Part of Weekly revenue","dataType":"number","operationType":"sum","sourceField":"taxful_total_price","isBucketed":false,"scale":"ratio","customLabel":true},"041db33b-5c9c-47f3-a5d3-ef5e255d1663X1":{"label":"Part of Weekly revenue","dataType":"number","operationType":"math","isBucketed":false,"scale":"ratio","params":{"tinymathAst":{"type":"function","name":"subtract","args":[{"type":"function","name":"divide","args":["041db33b-5c9c-47f3-a5d3-ef5e255d1663X0",10000],"location":{"min":0,"max":32},"text":"sum(taxful_total_price) / 10000 "},1],"location":{"min":0,"max":35},"text":"sum(taxful_total_price) / 10000 - 1"}},"references":["041db33b-5c9c-47f3-a5d3-ef5e255d1663X0"],"customLabel":true},"041db33b-5c9c-47f3-a5d3-ef5e255d1663":{"label":"% of target ($10k)","dataType":"number","operationType":"formula","isBucketed":false,"scale":"ratio","params":{"formula":"sum(taxful_total_price) / 10000 - 1","isFormulaBroken":false,"format":{"id":"percent","params":{"decimals":0}}},"references":["041db33b-5c9c-47f3-a5d3-ef5e255d1663X1"],"customLabel":true}},"columnOrder":["8289349e-6d1b-4abf-b164-0208183d2c34","041db33b-5c9c-47f3-a5d3-ef5e255d1663","041db33b-5c9c-47f3-a5d3-ef5e255d1663X0","041db33b-5c9c-47f3-a5d3-ef5e255d1663X1"],"incompleteColumns":{}}}}},"visualization":{"legend":{"isVisible":true,"position":"right"},"valueLabels":"hide","fittingFunction":"None","yLeftExtent":{"mode":"full"},"yRightExtent":{"mode":"full"},"axisTitlesVisibilitySettings":{"x":false,"yLeft":false,"yRight":true},"tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"preferredSeriesType":"bar_stacked","layers":[{"layerId":"c7478794-6767-4286-9d65-1c0ecd909dd8","seriesType":"bar_stacked","xAccessor":"8289349e-6d1b-4abf-b164-0208183d2c34","accessors":["041db33b-5c9c-47f3-a5d3-ef5e255d1663"]}]},"query":{"query":"","language":"kuery"},"filters":[]},"references":[{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-layer-c7478794-6767-4286-9d65-1c0ecd909dd8"}]},"enhancements":{},"hidePanelTitles":false},"title":"% of target revenue ($10k)"},{"version":"7.14.0","type":"lens","gridData":{"x":24,"y":7,"w":12,"h":8,"i":"2e6ef14d-7b03-46d4-a6b8-a962ee36a805"},"panelIndex":"2e6ef14d-7b03-46d4-a6b8-a962ee36a805","embeddableConfig":{"attributes":{"title":"","type":"lens","visualizationType":"lnsMetric","state":{"datasourceStates":{"indexpattern":{"layers":{"c7478794-6767-4286-9d65-1c0ecd909dd8":{"columns":{"041db33b-5c9c-47f3-a5d3-ef5e255d1663":{"label":"Sum of revenue","dataType":"number","operationType":"sum","sourceField":"taxful_total_price","isBucketed":false,"scale":"ratio","customLabel":true}},"columnOrder":["041db33b-5c9c-47f3-a5d3-ef5e255d1663"],"incompleteColumns":{}}}}},"visualization":{"layerId":"c7478794-6767-4286-9d65-1c0ecd909dd8","accessor":"041db33b-5c9c-47f3-a5d3-ef5e255d1663"},"query":{"query":"","language":"kuery"},"filters":[]},"references":[{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-layer-c7478794-6767-4286-9d65-1c0ecd909dd8"}]},"enhancements":{}}},{"version":"7.14.0","type":"lens","gridData":{"x":36,"y":7,"w":12,"h":8,"i":"5108a3bc-d1cf-4255-8c95-2df52577b956"},"panelIndex":"5108a3bc-d1cf-4255-8c95-2df52577b956","embeddableConfig":{"attributes":{"title":"","type":"lens","visualizationType":"lnsMetric","state":{"datasourceStates":{"indexpattern":{"layers":{"4fb42a8e-b133-43c8-805c-a38472053938":{"columns":{"020bbfdf-9ef8-4802-aa9e-342d2ea0bebf":{"label":"Median spending","dataType":"number","operationType":"median","sourceField":"taxful_total_price","isBucketed":false,"scale":"ratio","customLabel":true}},"columnOrder":["020bbfdf-9ef8-4802-aa9e-342d2ea0bebf"],"incompleteColumns":{}}}}},"visualization":{"layerId":"4fb42a8e-b133-43c8-805c-a38472053938","accessor":"020bbfdf-9ef8-4802-aa9e-342d2ea0bebf"},"query":{"query":"","language":"kuery"},"filters":[]},"references":[{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-layer-4fb42a8e-b133-43c8-805c-a38472053938"}]},"enhancements":{}}},{"version":"7.14.0","type":"lens","gridData":{"x":0,"y":15,"w":24,"h":7,"i":"6bc3fa4a-8f1b-436f-afc1-f3516ee531ce"},"panelIndex":"6bc3fa4a-8f1b-436f-afc1-f3516ee531ce","embeddableConfig":{"attributes":{"title":"","type":"lens","visualizationType":"lnsXY","state":{"datasourceStates":{"indexpattern":{"layers":{"b6093a53-884f-42c2-9fcc-ba56cfb66c53":{"columns":{"15c45f89-a149-443a-a830-aa8c3a9317db":{"label":"order_date","dataType":"date","operationType":"date_histogram","sourceField":"order_date","isBucketed":true,"scale":"interval","params":{"interval":"1d"}},"2b41b3d8-2f62-407a-a866-960f254c679d":{"label":"Total items","dataType":"number","operationType":"sum","sourceField":"products.quantity","isBucketed":false,"scale":"ratio","customLabel":true},"ddc92e50-4d5c-413e-b91b-3e504889fa65":{"label":"Transactions","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records","customLabel":true},"eadae280-2da3-4d1d-a0e1-f9733f89c15b":{"label":"Last week","dataType":"number","operationType":"sum","sourceField":"products.quantity","isBucketed":false,"scale":"ratio","timeShift":"1w","customLabel":true},"5e31e5d3-2aaa-4475-a130-3b69bf2f748a":{"label":"Tx. last week","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records","timeShift":"1w","customLabel":true}},"columnOrder":["15c45f89-a149-443a-a830-aa8c3a9317db","2b41b3d8-2f62-407a-a866-960f254c679d","eadae280-2da3-4d1d-a0e1-f9733f89c15b","ddc92e50-4d5c-413e-b91b-3e504889fa65","5e31e5d3-2aaa-4475-a130-3b69bf2f748a"],"incompleteColumns":{}}}}},"visualization":{"legend":{"isVisible":true,"position":"right"},"valueLabels":"hide","fittingFunction":"None","yLeftExtent":{"mode":"full"},"yRightExtent":{"mode":"full"},"axisTitlesVisibilitySettings":{"x":false,"yLeft":false,"yRight":true},"tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"preferredSeriesType":"line","layers":[{"layerId":"b6093a53-884f-42c2-9fcc-ba56cfb66c53","accessors":["2b41b3d8-2f62-407a-a866-960f254c679d","eadae280-2da3-4d1d-a0e1-f9733f89c15b","5e31e5d3-2aaa-4475-a130-3b69bf2f748a","ddc92e50-4d5c-413e-b91b-3e504889fa65"],"position":"top","seriesType":"line","showGridlines":false,"xAccessor":"15c45f89-a149-443a-a830-aa8c3a9317db","yConfig":[{"forAccessor":"eadae280-2da3-4d1d-a0e1-f9733f89c15b","color":"#b6e0d5"},{"forAccessor":"5e31e5d3-2aaa-4475-a130-3b69bf2f748a","color":"#edafc4"}]}],"curveType":"LINEAR"},"query":{"query":"","language":"kuery"},"filters":[]},"references":[{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-layer-b6093a53-884f-42c2-9fcc-ba56cfb66c53"}]},"enhancements":{}}},{"version":"7.14.0","type":"lens","gridData":{"x":24,"y":15,"w":12,"h":7,"i":"222c1f05-ca21-4e62-a04a-9a059b4534a7"},"panelIndex":"222c1f05-ca21-4e62-a04a-9a059b4534a7","embeddableConfig":{"attributes":{"title":"","type":"lens","visualizationType":"lnsMetric","state":{"datasourceStates":{"indexpattern":{"layers":{"667067a2-7cdf-4f0e-a9fe-eb4f4f1f2f17":{"columns":{"c52c2003-ae58-4604-bae7-52ba0fb38a01":{"label":"Avg. items sold","dataType":"number","operationType":"average","sourceField":"total_quantity","isBucketed":false,"scale":"ratio","params":{"format":{"id":"number","params":{"decimals":1}}},"customLabel":true}},"columnOrder":["c52c2003-ae58-4604-bae7-52ba0fb38a01"],"incompleteColumns":{}}}}},"visualization":{"layerId":"667067a2-7cdf-4f0e-a9fe-eb4f4f1f2f17","accessor":"c52c2003-ae58-4604-bae7-52ba0fb38a01"},"query":{"query":"","language":"kuery"},"filters":[]},"references":[{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-layer-667067a2-7cdf-4f0e-a9fe-eb4f4f1f2f17"}]},"enhancements":{}}},{"version":"7.14.0","type":"lens","gridData":{"x":24,"y":22,"w":24,"h":10,"i":"003bdfc7-4d9e-4bd0-b088-3b18f79588d1"},"panelIndex":"003bdfc7-4d9e-4bd0-b088-3b18f79588d1","embeddableConfig":{"attributes":{"title":"","type":"lens","visualizationType":"lnsXY","state":{"datasourceStates":{"indexpattern":{"layers":{"97c63ea6-9305-4755-97d1-0f26817c6f9a":{"columns":{"9f61a7df-198e-4754-b34c-81ed544136ba":{"label":"Top values of category.keyword","dataType":"string","operationType":"terms","scale":"ordinal","sourceField":"category.keyword","isBucketed":true,"params":{"size":10,"orderBy":{"type":"column","columnId":"5575214b-7f21-4b6c-8bc1-34433c6a0c58"},"orderDirection":"desc","otherBucket":true,"missingBucket":false}},"ebcb19af-0900-4439-949f-d8cd9bccde19":{"label":"order_date","dataType":"date","operationType":"date_histogram","sourceField":"order_date","isBucketed":true,"scale":"interval","params":{"interval":"1d"}},"5575214b-7f21-4b6c-8bc1-34433c6a0c58":{"label":"Count of records","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records"}},"columnOrder":["9f61a7df-198e-4754-b34c-81ed544136ba","ebcb19af-0900-4439-949f-d8cd9bccde19","5575214b-7f21-4b6c-8bc1-34433c6a0c58"],"incompleteColumns":{}}}}},"visualization":{"legend":{"isVisible":true,"position":"right"},"valueLabels":"inside","fittingFunction":"None","yLeftExtent":{"mode":"full"},"yRightExtent":{"mode":"full"},"axisTitlesVisibilitySettings":{"x":false,"yLeft":false,"yRight":true},"tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"preferredSeriesType":"bar_percentage_stacked","layers":[{"layerId":"97c63ea6-9305-4755-97d1-0f26817c6f9a","accessors":["5575214b-7f21-4b6c-8bc1-34433c6a0c58"],"position":"top","seriesType":"bar_percentage_stacked","showGridlines":false,"xAccessor":"ebcb19af-0900-4439-949f-d8cd9bccde19","splitAccessor":"9f61a7df-198e-4754-b34c-81ed544136ba"}]},"query":{"query":"","language":"kuery"},"filters":[]},"references":[{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-layer-97c63ea6-9305-4755-97d1-0f26817c6f9a"}]},"enhancements":{}}},{"version":"7.14.0","type":"lens","gridData":{"x":24,"y":32,"w":24,"h":14,"i":"a885226c-6830-4731-88a0-8c1d1047841e"},"panelIndex":"a885226c-6830-4731-88a0-8c1d1047841e","embeddableConfig":{"attributes":{"title":"","type":"lens","visualizationType":"lnsDatatable","state":{"datasourceStates":{"indexpattern":{"layers":{"0731ee8b-31c5-4be9-92d9-69ee760465d7":{"columns":{"7bf8f089-1542-40bd-b349-45fdfc309ac6":{"label":"order_date","dataType":"date","operationType":"date_histogram","sourceField":"order_date","isBucketed":true,"scale":"interval","params":{"interval":"1d"}},"826b2f39-b616-40b2-a222-972fdc1d7596":{"label":"This week","dataType":"number","operationType":"sum","sourceField":"taxful_total_price","isBucketed":false,"scale":"ratio","customLabel":true},"cfd45c47-fc41-430c-9e7a-b71dc0c916b0":{"label":"1 week ago","dataType":"number","operationType":"sum","sourceField":"taxful_total_price","isBucketed":false,"scale":"ratio","timeShift":"1w","customLabel":true},"bf51c1af-443e-49f4-a21f-54c87bfc5677X0":{"label":"Part of Difference","dataType":"number","operationType":"sum","sourceField":"taxful_total_price","isBucketed":false,"scale":"ratio","customLabel":true},"bf51c1af-443e-49f4-a21f-54c87bfc5677X1":{"label":"Part of Difference","dataType":"number","operationType":"sum","sourceField":"taxful_total_price","isBucketed":false,"scale":"ratio","timeShift":"1w","customLabel":true},"bf51c1af-443e-49f4-a21f-54c87bfc5677X2":{"label":"Part of Difference","dataType":"number","operationType":"math","isBucketed":false,"scale":"ratio","params":{"tinymathAst":{"type":"function","name":"subtract","args":["bf51c1af-443e-49f4-a21f-54c87bfc5677X0","bf51c1af-443e-49f4-a21f-54c87bfc5677X1"],"location":{"min":0,"max":61},"text":"sum(taxful_total_price) - sum(taxful_total_price, shift=\'1w\')"}},"references":["bf51c1af-443e-49f4-a21f-54c87bfc5677X0","bf51c1af-443e-49f4-a21f-54c87bfc5677X1"],"customLabel":true},"bf51c1af-443e-49f4-a21f-54c87bfc5677":{"label":"Difference","dataType":"number","operationType":"formula","isBucketed":false,"scale":"ratio","params":{"formula":"sum(taxful_total_price) - sum(taxful_total_price, shift=\'1w\')","isFormulaBroken":false,"format":{"id":"number","params":{"decimals":2}}},"references":["bf51c1af-443e-49f4-a21f-54c87bfc5677X2"],"customLabel":true}},"columnOrder":["7bf8f089-1542-40bd-b349-45fdfc309ac6","826b2f39-b616-40b2-a222-972fdc1d7596","cfd45c47-fc41-430c-9e7a-b71dc0c916b0","bf51c1af-443e-49f4-a21f-54c87bfc5677","bf51c1af-443e-49f4-a21f-54c87bfc5677X0","bf51c1af-443e-49f4-a21f-54c87bfc5677X1","bf51c1af-443e-49f4-a21f-54c87bfc5677X2"],"incompleteColumns":{}}}}},"visualization":{"layerId":"0731ee8b-31c5-4be9-92d9-69ee760465d7","columns":[{"columnId":"7bf8f089-1542-40bd-b349-45fdfc309ac6"},{"columnId":"826b2f39-b616-40b2-a222-972fdc1d7596","alignment":"left"},{"columnId":"cfd45c47-fc41-430c-9e7a-b71dc0c916b0"},{"columnId":"bf51c1af-443e-49f4-a21f-54c87bfc5677","isTransposed":false,"colorMode":"text","palette":{"name":"custom","type":"palette","params":{"steps":5,"stops":[{"color":"#D36086","stop":0},{"color":"#209280","stop":2249.03125}],"continuity":"above","rangeType":"number","colorStops":[{"color":"#D36086","stop":-10000},{"color":"#209280","stop":0}],"rangeMin":-10000,"rangeMax":0,"name":"custom"}}}]},"query":{"query":"","language":"kuery"},"filters":[]},"references":[{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-layer-0731ee8b-31c5-4be9-92d9-69ee760465d7"}]},"enhancements":{}}},{"version":"7.14.0","type":"lens","gridData":{"x":24,"y":46,"w":24,"h":9,"i":"562bb4bd-16b5-4c7e-9dfa-0f24cae6d1ba"},"panelIndex":"562bb4bd-16b5-4c7e-9dfa-0f24cae6d1ba","embeddableConfig":{"attributes":{"title":"","type":"lens","visualizationType":"lnsXY","state":{"datasourceStates":{"indexpattern":{"layers":{"5ed846c2-a70b-4d9c-a244-f254bef763b8":{"columns":{"d77cdd24-dedc-48dd-9a4b-d34c6f1a6c46":{"label":"Product name","dataType":"string","operationType":"terms","scale":"ordinal","sourceField":"products.product_name.keyword","isBucketed":true,"params":{"size":5,"orderBy":{"type":"column","columnId":"7ac31901-277a-46e2-8128-8d684b2c1127"},"orderDirection":"desc","otherBucket":false,"missingBucket":false},"customLabel":true},"7ac31901-277a-46e2-8128-8d684b2c1127":{"label":"Items","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records","customLabel":true}},"columnOrder":["d77cdd24-dedc-48dd-9a4b-d34c6f1a6c46","7ac31901-277a-46e2-8128-8d684b2c1127"],"incompleteColumns":{}}}}},"visualization":{"legend":{"isVisible":true,"position":"right"},"valueLabels":"inside","fittingFunction":"None","yLeftExtent":{"mode":"full"},"yRightExtent":{"mode":"full"},"axisTitlesVisibilitySettings":{"x":false,"yLeft":true,"yRight":true},"tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"preferredSeriesType":"bar_horizontal","layers":[{"layerId":"5ed846c2-a70b-4d9c-a244-f254bef763b8","accessors":["7ac31901-277a-46e2-8128-8d684b2c1127"],"position":"top","seriesType":"bar_horizontal","showGridlines":false,"xAccessor":"d77cdd24-dedc-48dd-9a4b-d34c6f1a6c46"}]},"query":{"query":"","language":"kuery"},"filters":[]},"references":[{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-layer-5ed846c2-a70b-4d9c-a244-f254bef763b8"}]},"timeRange":{"from":"now-2w","to":"now-1w"},"hidePanelTitles":false,"enhancements":{}},"title":"Top products last week"},{"version":"7.14.0","type":"lens","gridData":{"x":0,"y":46,"w":24,"h":9,"i":"b1697063-c817-4847-aa0d-5bed47137b7e"},"panelIndex":"b1697063-c817-4847-aa0d-5bed47137b7e","embeddableConfig":{"attributes":{"title":"","type":"lens","visualizationType":"lnsXY","state":{"datasourceStates":{"indexpattern":{"layers":{"5ed846c2-a70b-4d9c-a244-f254bef763b8":{"columns":{"d77cdd24-dedc-48dd-9a4b-d34c6f1a6c46":{"label":"Product name","dataType":"string","operationType":"terms","scale":"ordinal","sourceField":"products.product_name.keyword","isBucketed":true,"params":{"size":5,"orderBy":{"type":"column","columnId":"7ac31901-277a-46e2-8128-8d684b2c1127"},"orderDirection":"desc","otherBucket":false,"missingBucket":false},"customLabel":true},"7ac31901-277a-46e2-8128-8d684b2c1127":{"label":"Items","dataType":"number","operationType":"count","isBucketed":false,"scale":"ratio","sourceField":"Records","customLabel":true}},"columnOrder":["d77cdd24-dedc-48dd-9a4b-d34c6f1a6c46","7ac31901-277a-46e2-8128-8d684b2c1127"],"incompleteColumns":{}}}}},"visualization":{"legend":{"isVisible":true,"position":"right"},"valueLabels":"inside","fittingFunction":"None","yLeftExtent":{"mode":"full"},"yRightExtent":{"mode":"full"},"axisTitlesVisibilitySettings":{"x":false,"yLeft":true,"yRight":true},"tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"preferredSeriesType":"bar_horizontal","layers":[{"layerId":"5ed846c2-a70b-4d9c-a244-f254bef763b8","accessors":["7ac31901-277a-46e2-8128-8d684b2c1127"],"position":"top","seriesType":"bar_horizontal","showGridlines":false,"xAccessor":"d77cdd24-dedc-48dd-9a4b-d34c6f1a6c46"}]},"query":{"query":"","language":"kuery"},"filters":[]},"references":[{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"ff959d40-b880-11e8-a6d9-e546fe2bba5f","name":"indexpattern-datasource-layer-5ed846c2-a70b-4d9c-a244-f254bef763b8"}]},"hidePanelTitles":false,"enhancements":{}},"title":"Top products this week"}]', - version: 1, - timeRestore: true, - timeTo: 'now', - timeFrom: 'now-7d', + '[{"version":"8.0.0-SNAPSHOT","type":"visualization","gridData":{"x":0,"y":22,"w":24,"h":10,"i":"5"},"panelIndex":"5","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_5"},{"version":"8.0.0-SNAPSHOT","type":"visualization","gridData":{"x":36,"y":15,"w":12,"h":7,"i":"7"},"panelIndex":"7","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_7"},{"version":"8.0.0-SNAPSHOT","type":"search","gridData":{"x":0,"y":55,"w":48,"h":18,"i":"10"},"panelIndex":"10","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_10"},{"version":"8.0.0-SNAPSHOT","type":"map","gridData":{"x":0,"y":32,"w":24,"h":14,"i":"11"},"panelIndex":"11","embeddableConfig":{"isLayerTOCOpen":false,"enhancements":{},"mapCenter":{"lat":45.88578,"lon":-15.07605,"zoom":2.11},"mapBuffer":{"minLon":-135,"minLat":0,"maxLon":90,"maxLat":66.51326},"openTOCDetails":[],"hiddenLayers":[]},"panelRefName":"panel_11"},{"version":"8.0.0-SNAPSHOT","type":"visualization","gridData":{"x":0,"y":0,"w":18,"h":7,"i":"a71cf076-6895-491c-8878-63592e429ed5"},"panelIndex":"a71cf076-6895-491c-8878-63592e429ed5","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_a71cf076-6895-491c-8878-63592e429ed5"},{"version":"8.0.0-SNAPSHOT","type":"visualization","gridData":{"x":18,"y":0,"w":30,"h":7,"i":"adc0a2f4-481c-45eb-b422-0ea59a3e5163"},"panelIndex":"adc0a2f4-481c-45eb-b422-0ea59a3e5163","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_adc0a2f4-481c-45eb-b422-0ea59a3e5163"},{"version":"8.0.0-SNAPSHOT","type":"lens","gridData":{"x":0,"y":7,"w":24,"h":8,"i":"7077b79f-2a99-4fcb-bbd4-456982843278"},"panelIndex":"7077b79f-2a99-4fcb-bbd4-456982843278","embeddableConfig":{"enhancements":{},"hidePanelTitles":false},"title":"% of target revenue ($10k)","panelRefName":"panel_7077b79f-2a99-4fcb-bbd4-456982843278"},{"version":"8.0.0-SNAPSHOT","type":"lens","gridData":{"x":24,"y":7,"w":12,"h":8,"i":"19a3c101-ad2e-4421-a71b-a4734ec1f03e"},"panelIndex":"19a3c101-ad2e-4421-a71b-a4734ec1f03e","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_19a3c101-ad2e-4421-a71b-a4734ec1f03e"},{"version":"8.0.0-SNAPSHOT","type":"lens","gridData":{"x":36,"y":7,"w":12,"h":8,"i":"491469e7-7d24-4216-aeb3-bca00e5c8c1b"},"panelIndex":"491469e7-7d24-4216-aeb3-bca00e5c8c1b","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_491469e7-7d24-4216-aeb3-bca00e5c8c1b"},{"version":"8.0.0-SNAPSHOT","type":"lens","gridData":{"x":0,"y":15,"w":24,"h":7,"i":"a1b03eb9-a36b-4e12-aa1b-bb29b5d6c4ef"},"panelIndex":"a1b03eb9-a36b-4e12-aa1b-bb29b5d6c4ef","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_a1b03eb9-a36b-4e12-aa1b-bb29b5d6c4ef"},{"version":"8.0.0-SNAPSHOT","type":"lens","gridData":{"x":24,"y":15,"w":12,"h":7,"i":"da51079b-952f-43dc-96e6-6f9415a3708b"},"panelIndex":"da51079b-952f-43dc-96e6-6f9415a3708b","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_da51079b-952f-43dc-96e6-6f9415a3708b"},{"version":"8.0.0-SNAPSHOT","type":"lens","gridData":{"x":24,"y":22,"w":24,"h":10,"i":"64fd5dcf-30c5-4f5a-a78c-70b1fbf87e5b"},"panelIndex":"64fd5dcf-30c5-4f5a-a78c-70b1fbf87e5b","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_64fd5dcf-30c5-4f5a-a78c-70b1fbf87e5b"},{"version":"8.0.0-SNAPSHOT","type":"lens","gridData":{"x":24,"y":32,"w":24,"h":14,"i":"bd330ede-2eef-4e2a-8100-22a21abf5038"},"panelIndex":"bd330ede-2eef-4e2a-8100-22a21abf5038","embeddableConfig":{"enhancements":{}},"panelRefName":"panel_bd330ede-2eef-4e2a-8100-22a21abf5038"},{"version":"8.0.0-SNAPSHOT","type":"lens","gridData":{"x":0,"y":46,"w":24,"h":9,"i":"b897d4be-cf83-46fb-a111-c7fbec9ef403"},"panelIndex":"b897d4be-cf83-46fb-a111-c7fbec9ef403","embeddableConfig":{"hidePanelTitles":false,"enhancements":{}},"title":"Top products this week","panelRefName":"panel_b897d4be-cf83-46fb-a111-c7fbec9ef403"},{"version":"8.0.0-SNAPSHOT","type":"lens","gridData":{"x":24,"y":46,"w":24,"h":9,"i":"e0f68f93-30f2-4da7-889a-6cd128a68d3f"},"panelIndex":"e0f68f93-30f2-4da7-889a-6cd128a68d3f","embeddableConfig":{"timeRange":{"from":"now-2w","to":"now-1w"},"hidePanelTitles":false,"enhancements":{}},"title":"Top products last week","panelRefName":"panel_e0f68f93-30f2-4da7-889a-6cd128a68d3f"}]', refreshInterval: { pause: true, value: 0, }, - kibanaSavedObjectMeta: { - searchSourceJSON: '{"query":{"language":"kuery","query":""},"filter":[]}', - }, + timeFrom: 'now-7d', + timeRestore: true, + timeTo: 'now', + title: i18n.translate('home.sampleData.ecommerceSpec.revenueDashboardTitle', { + defaultMessage: '[eCommerce] Revenue Dashboard', + }), + version: 1, + }, + coreMigrationVersion: '8.0.0', + id: '722b74f0-b882-11e8-a6d9-e546fe2bba5f', + migrationVersion: { + dashboard: '7.14.0', }, + references: [ + { + id: '45e07720-b890-11e8-a6d9-e546fe2bba5f', + name: '5:panel_5', + type: 'visualization', + }, + { + id: 'b80e6540-b891-11e8-a6d9-e546fe2bba5f', + name: '7:panel_7', + type: 'visualization', + }, + { + id: '3ba638e0-b894-11e8-a6d9-e546fe2bba5f', + name: '10:panel_10', + type: 'search', + }, + { + id: '2c9c1f60-1909-11e9-919b-ffe5949a18d2', + name: '11:panel_11', + type: 'map', + }, + { + id: 'c00d1f90-f5ea-11eb-a78e-83aac3c38a60', + name: 'a71cf076-6895-491c-8878-63592e429ed5:panel_a71cf076-6895-491c-8878-63592e429ed5', + type: 'visualization', + }, + { + id: 'c3378480-f5ea-11eb-a78e-83aac3c38a60', + name: 'adc0a2f4-481c-45eb-b422-0ea59a3e5163:panel_adc0a2f4-481c-45eb-b422-0ea59a3e5163', + type: 'visualization', + }, + { + id: 'c762b7a0-f5ea-11eb-a78e-83aac3c38a60', + name: '7077b79f-2a99-4fcb-bbd4-456982843278:panel_7077b79f-2a99-4fcb-bbd4-456982843278', + type: 'lens', + }, + { + id: 'ce02e260-f5ea-11eb-a78e-83aac3c38a60', + name: '19a3c101-ad2e-4421-a71b-a4734ec1f03e:panel_19a3c101-ad2e-4421-a71b-a4734ec1f03e', + type: 'lens', + }, + { + id: 'd5f90030-f5ea-11eb-a78e-83aac3c38a60', + name: '491469e7-7d24-4216-aeb3-bca00e5c8c1b:panel_491469e7-7d24-4216-aeb3-bca00e5c8c1b', + type: 'lens', + }, + { + id: 'dde978b0-f5ea-11eb-a78e-83aac3c38a60', + name: 'a1b03eb9-a36b-4e12-aa1b-bb29b5d6c4ef:panel_a1b03eb9-a36b-4e12-aa1b-bb29b5d6c4ef', + type: 'lens', + }, + { + id: 'e3902840-f5ea-11eb-a78e-83aac3c38a60', + name: 'da51079b-952f-43dc-96e6-6f9415a3708b:panel_da51079b-952f-43dc-96e6-6f9415a3708b', + type: 'lens', + }, + { + id: 'eddf7850-f5ea-11eb-a78e-83aac3c38a60', + name: '64fd5dcf-30c5-4f5a-a78c-70b1fbf87e5b:panel_64fd5dcf-30c5-4f5a-a78c-70b1fbf87e5b', + type: 'lens', + }, + { + id: 'ff6a21b0-f5ea-11eb-a78e-83aac3c38a60', + name: 'bd330ede-2eef-4e2a-8100-22a21abf5038:panel_bd330ede-2eef-4e2a-8100-22a21abf5038', + type: 'lens', + }, + { + id: '03071e90-f5eb-11eb-a78e-83aac3c38a60', + name: 'b897d4be-cf83-46fb-a111-c7fbec9ef403:panel_b897d4be-cf83-46fb-a111-c7fbec9ef403', + type: 'lens', + }, + { + id: '06379e00-f5eb-11eb-a78e-83aac3c38a60', + name: 'e0f68f93-30f2-4da7-889a-6cd128a68d3f:panel_e0f68f93-30f2-4da7-889a-6cd128a68d3f', + type: 'lens', + }, + ], + type: 'dashboard', + updated_at: '2021-08-05T12:45:46.525Z', + version: 'WzIzOSwxXQ==', }, ]; diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index 1119be32a04de..ecf89c8774c72 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -83,18 +83,6 @@ export class MapsPlugin implements Plugin { }, ]); - home.sampleData.replacePanelInSampleDatasetDashboard({ - sampleDataId: 'ecommerce', - dashboardId: '722b74f0-b882-11e8-a6d9-e546fe2bba5f', - oldEmbeddableId: '9c6f83f0-bb4d-11e8-9c84-77068524bcab', - embeddableId: '2c9c1f60-1909-11e9-919b-ffe5949a18d2', - // @ts-ignore - embeddableType: 'map', - embeddableConfig: { - isLayerTOCOpen: false, - }, - }); - home.sampleData.addSavedObjectsToSampleDataset('flights', getFlightsSavedObjects()); home.sampleData.addAppLinksToSampleDataset('flights', [ diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 14e1516ba1f15..1d81b0ed7785d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1993,7 +1993,6 @@ "home.sampleData.ecommerceSpec.promotionTrackingTitle": "[e コマース] プロモーショントラッキング", "home.sampleData.ecommerceSpec.revenueDashboardDescription": "サンプルの e コマースの注文と収益を分析します", "home.sampleData.ecommerceSpec.revenueDashboardTitle": "[e コマース] 収益ダッシュボード", - "home.sampleData.ecommerceSpec.salesCountMapTitle": "[eコマース] 売上カウントマップ", "home.sampleData.ecommerceSpec.soldProductsPerDayTitle": "[e コマース] 1 日の販売製品", "home.sampleData.ecommerceSpecDescription": "e コマースの注文をトラッキングするサンプルデータ、ビジュアライゼーション、ダッシュボードです。", "home.sampleData.ecommerceSpecTitle": "サンプル e コマース注文", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 40db95bd4fb7c..244b3dee8f0d4 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2004,7 +2004,6 @@ "home.sampleData.ecommerceSpec.promotionTrackingTitle": "[电子商务] 促销追踪", "home.sampleData.ecommerceSpec.revenueDashboardDescription": "分析模拟的电子商务订单和收入", "home.sampleData.ecommerceSpec.revenueDashboardTitle": "[电子商务] 收入仪表板", - "home.sampleData.ecommerceSpec.salesCountMapTitle": "[电子商务] 销售计数地图", "home.sampleData.ecommerceSpecDescription": "用于追踪电子商务订单的样例数据、可视化和仪表板。", "home.sampleData.ecommerceSpecTitle": "样例电子商务订单", "home.sampleData.flightsSpec.airportConnectionsTitle": "[航班] 机场航线(将鼠标悬停在机场上)", From ad0760d96ecd8208eb4f77fc17378a48c1ecc4c9 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Tue, 10 Aug 2021 08:45:10 -0400 Subject: [PATCH 021/104] [Elastic Agent] add heartbeat to monitoring datasets (#107989) --- x-pack/plugins/fleet/server/services/agent_policy.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index e3ecdcda20d2a..d3cccd4c07f3c 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -74,6 +74,7 @@ const MONITORING_DATASETS = [ 'elastic_agent.packetbeat', 'elastic_agent.endpoint_security', 'elastic_agent.auditbeat', + 'elastic_agent.heartbeat', ]; class AgentPolicyService { From fa47c33140aa24dfde9dc5c3c1c3ebe71e3b68f2 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Tue, 10 Aug 2021 13:56:00 +0100 Subject: [PATCH 022/104] [Security Solution] Use alert status actions from timeline plugin (#107928) * use alert status from balk actions * remove unused comment * fix types * fix cypress test --- .../add_endpoint_exception.tsx | 4 +- .../timeline_actions/add_event_filter.tsx | 6 +- .../timeline_actions/add_exception.tsx | 6 +- .../timeline_actions/alert_context_menu.tsx | 61 +++++--- .../alerts_status_actions/close_status.tsx | 4 +- .../in_progress_alert_status.tsx | 4 +- .../open_alert_status.tsx | 4 +- .../use_add_exception_actions.tsx | 20 ++- .../timeline_actions/use_alerts_actions.tsx | 137 ++---------------- .../components/take_action_dropdown/index.tsx | 10 +- .../hooks/use_status_bulk_action_items.tsx | 18 ++- x-pack/plugins/timelines/public/index.ts | 2 +- x-pack/plugins/timelines/public/plugin.ts | 1 + 13 files changed, 104 insertions(+), 173 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_endpoint_exception.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_endpoint_exception.tsx index 23709269a4c13..7be51c4eaa41a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_endpoint_exception.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_endpoint_exception.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiContextMenuItem, EuiText } from '@elastic/eui'; +import { EuiContextMenuItem } from '@elastic/eui'; import React from 'react'; import * as i18n from '../translations'; @@ -27,7 +27,7 @@ const AddEndpointExceptionComponent: React.FC = ({ onClick={onClick} disabled={disabled} > - {i18n.ACTION_ADD_ENDPOINT_EXCEPTION} + {i18n.ACTION_ADD_ENDPOINT_EXCEPTION} ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_event_filter.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_event_filter.tsx index 1104b3eb83081..9b14c01371c9b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_event_filter.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_event_filter.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiContextMenuItem, EuiText } from '@elastic/eui'; +import { EuiContextMenuItem } from '@elastic/eui'; import React from 'react'; import * as i18n from '../translations'; @@ -24,9 +24,7 @@ const AddEventFilterComponent: React.FC = ({ onClick, disab onClick={onClick} disabled={disabled} > - - {i18n.ACTION_ADD_EVENT_FILTER} - + {i18n.ACTION_ADD_EVENT_FILTER} ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_exception.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_exception.tsx index 030f67c9e708c..99eef3aefd42c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_exception.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/add_exception.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiContextMenuItem, EuiText } from '@elastic/eui'; +import { EuiContextMenuItem } from '@elastic/eui'; import React from 'react'; import * as i18n from '../translations'; @@ -24,9 +24,7 @@ const AddExceptionComponent: React.FC = ({ disabled, onClick onClick={onClick} disabled={disabled} > - - {i18n.ACTION_ADD_EXCEPTION} - + {i18n.ACTION_ADD_EXCEPTION} ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 3a9a4e875369e..2dae69fec43e1 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -7,8 +7,7 @@ import React, { useCallback, useMemo, useState } from 'react'; -import { EuiButtonIcon, EuiContextMenu, EuiPopover, EuiToolTip } from '@elastic/eui'; -import styled from 'styled-components'; +import { EuiButtonIcon, EuiContextMenuPanel, EuiPopover, EuiToolTip } from '@elastic/eui'; import { indexOf } from 'lodash'; import { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; @@ -31,8 +30,10 @@ import { useAlertsActions } from './use_alerts_actions'; import { useExceptionModal } from './use_add_exception_modal'; import { useExceptionActions } from './use_add_exception_actions'; import { useEventFilterModal } from './use_event_filter_modal'; -import { useEventFilterAction } from './use_event_filter_action'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { AddEventFilter } from './add_event_filter'; +import { AddException } from './add_exception'; +import { AddEndpointException } from './add_endpoint_exception'; interface AlertContextMenuProps { ariaLabel?: string; @@ -112,7 +113,7 @@ const AlertContextMenuComponent: React.FC = ({ onAddEventFilterClick, } = useEventFilterModal(); - const { statusActions } = useAlertsActions({ + const { actionItems } = useAlertsActions({ alertStatus, eventId: ecsRowData?._id, timelineId, @@ -132,23 +133,41 @@ const AlertContextMenuComponent: React.FC = ({ closePopover(); }, [closePopover, onAddEventFilterClick]); - const exceptionActions = useExceptionActions({ + const { + disabledAddEndpointException, + disabledAddException, + handleEndpointExceptionModal, + handleDetectionExceptionModal, + } = useExceptionActions({ isEndpointAlert, onAddExceptionTypeClick: handleOnAddExceptionTypeClick, }); - const eventFilterActions = useEventFilterAction({ - onAddEventFilterClick: handleOnAddEventFilterClick, - }); - - const panels = useMemo( - () => [ - { - id: 0, - items: !isEvent && ruleId ? [...statusActions, ...exceptionActions] : [eventFilterActions], - }, - ], - [eventFilterActions, exceptionActions, isEvent, ruleId, statusActions] + const items = useMemo( + () => + !isEvent && ruleId + ? [ + ...actionItems, + , + , + ] + : [], + [ + actionItems, + disabledAddEndpointException, + disabledAddException, + handleDetectionExceptionModal, + handleEndpointExceptionModal, + handleOnAddEventFilterClick, + isEvent, + ruleId, + ] ); return ( @@ -164,7 +183,7 @@ const AlertContextMenuComponent: React.FC = ({ anchorPosition="downLeft" repositionOnScroll > - +

@@ -191,12 +210,6 @@ const AlertContextMenuComponent: React.FC = ({ ); }; -const ContextMenuPanel = styled(EuiContextMenu)` - font-size: ${({ theme }) => theme.eui.euiFontSizeS}; -`; - -ContextMenuPanel.displayName = 'ContextMenuPanel'; - export const AlertContextMenu = React.memo(AlertContextMenuComponent); type AddExceptionModalWrapperProps = Omit< diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/close_status.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/close_status.tsx index 038d58c38a013..28a34c549ef16 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/close_status.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/close_status.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiContextMenuItem, EuiText } from '@elastic/eui'; +import { EuiContextMenuItem } from '@elastic/eui'; import React from 'react'; import { FILTER_CLOSED } from '../../alerts_filter_group'; import * as i18n from '../../translations'; @@ -25,7 +25,7 @@ const CloseAlertActionComponent: React.FC = ({ onClick, d onClick={onClick} disabled={disabled} > - {i18n.ACTION_CLOSE_ALERT} + {i18n.ACTION_CLOSE_ALERT} ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/in_progress_alert_status.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/in_progress_alert_status.tsx index 2bca569032827..f273833c1c1b3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/in_progress_alert_status.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/in_progress_alert_status.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiContextMenuItem, EuiText } from '@elastic/eui'; +import { EuiContextMenuItem } from '@elastic/eui'; import React from 'react'; import { FILTER_IN_PROGRESS } from '../../alerts_filter_group'; import * as i18n from '../../translations'; @@ -28,7 +28,7 @@ const InProgressAlertStatusComponent: React.FC = ({ onClick={onClick} disabled={disabled} > - {i18n.ACTION_IN_PROGRESS_ALERT} + {i18n.ACTION_IN_PROGRESS_ALERT} ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/open_alert_status.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/open_alert_status.tsx index 34832ee07ea75..2042acea4d604 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/open_alert_status.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alerts_status_actions/open_alert_status.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiContextMenuItem, EuiText } from '@elastic/eui'; +import { EuiContextMenuItem } from '@elastic/eui'; import React from 'react'; import { FILTER_OPEN } from '../../alerts_filter_group'; import * as i18n from '../../translations'; @@ -25,7 +25,7 @@ const OpenAlertStatusComponent: React.FC = ({ onClick, dis onClick={onClick} disabled={disabled} > - {i18n.ACTION_OPEN_ALERT} + {i18n.ACTION_OPEN_ALERT} ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_exception_actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_exception_actions.tsx index 0f8fa00a3ac40..9f1f699241e21 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_exception_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_exception_actions.tsx @@ -11,12 +11,20 @@ import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { useUserData } from '../../user_info'; import { ACTION_ADD_ENDPOINT_EXCEPTION, ACTION_ADD_EXCEPTION } from '../translations'; -interface UseExceptionActions { +interface ExceptionActions { name: string; onClick: () => void; disabled: boolean; } +interface UseExceptionActions { + disabledAddEndpointException: boolean; + disabledAddException: boolean; + exceptionActions: ExceptionActions[]; + handleEndpointExceptionModal: () => void; + handleDetectionExceptionModal: () => void; +} + interface UseExceptionActionProps { isEndpointAlert: boolean; onAddExceptionTypeClick: (type: ExceptionListType) => void; @@ -25,7 +33,7 @@ interface UseExceptionActionProps { export const useExceptionActions = ({ isEndpointAlert, onAddExceptionTypeClick, -}: UseExceptionActionProps): UseExceptionActions[] => { +}: UseExceptionActionProps): UseExceptionActions => { const [{ canUserCRUD, hasIndexWrite }] = useUserData(); const handleDetectionExceptionModal = useCallback(() => { @@ -62,5 +70,11 @@ export const useExceptionActions = ({ ] ); - return exceptionActions; + return { + disabledAddEndpointException, + disabledAddException, + exceptionActions, + handleEndpointExceptionModal, + handleDetectionExceptionModal, + }; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx index 855eb2dd5fef4..4fdebee6e1f4d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx @@ -5,14 +5,12 @@ * 2.0. */ -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; import { useDispatch } from 'react-redux'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; import { timelineActions } from '../../../../timelines/store/timeline'; -import { FILTER_OPEN, FILTER_CLOSED, FILTER_IN_PROGRESS } from '../alerts_filter_group'; -import { updateAlertStatusAction } from '../actions'; import { SetEventsDeletedProps, SetEventsLoadingProps } from '../types'; import * as i18nCommon from '../../../../common/translations'; import * as i18n from '../translations'; @@ -22,12 +20,12 @@ import { displaySuccessToast, displayErrorToast, } from '../../../../common/components/toasters'; -import { useUserData } from '../../user_info'; +import { useStatusBulkActionItems } from '../../../../../../timelines/public'; interface Props { - alertStatus?: string; + alertStatus?: Status; closePopover: () => void; - eventId: string | null | undefined; + eventId: string; timelineId: string; } @@ -37,10 +35,9 @@ export const useAlertsActions = ({ alertStatus, closePopover, eventId, timelineI const { addWarning } = useAppToasts(); - const [{ canUserCRUD, hasIndexMaintenance, hasIndexUpdateDelete }] = useUserData(); - const onAlertStatusUpdateSuccess = useCallback( (updated: number, conflicts: number, newStatus: Status) => { + closePopover(); if (conflicts > 0) { // Partial failure addWarning({ @@ -63,12 +60,14 @@ export const useAlertsActions = ({ alertStatus, closePopover, eventId, timelineI displaySuccessToast(title, dispatchToaster); } }, - [addWarning, dispatchToaster] + [addWarning, closePopover, dispatchToaster] ); const onAlertStatusUpdateFailure = useCallback( (newStatus: Status, error: Error) => { let title: string; + closePopover(); + switch (newStatus) { case 'closed': title = i18n.CLOSED_ALERT_FAILED_TOAST; @@ -81,7 +80,7 @@ export const useAlertsActions = ({ alertStatus, closePopover, eventId, timelineI } displayErrorToast(title, [error.message], dispatchToaster); }, - [dispatchToaster] + [closePopover, dispatchToaster] ); const setEventsLoading = useCallback( @@ -98,120 +97,16 @@ export const useAlertsActions = ({ alertStatus, closePopover, eventId, timelineI [dispatch, timelineId] ); - const openAlertActionOnClick = useCallback(() => { - if (eventId) { - updateAlertStatusAction({ - alertIds: [eventId], - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, - setEventsDeleted, - setEventsLoading, - selectedStatus: FILTER_OPEN, - }); - } - closePopover(); - }, [ - closePopover, - eventId, - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, - setEventsDeleted, - setEventsLoading, - ]); - - const closeAlertActionClick = useCallback(() => { - if (eventId) { - updateAlertStatusAction({ - alertIds: [eventId], - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, - setEventsDeleted, - setEventsLoading, - selectedStatus: FILTER_CLOSED, - }); - } - - closePopover(); - }, [ - closePopover, - eventId, - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, - setEventsDeleted, + const actionItems = useStatusBulkActionItems({ + eventIds: [eventId], + currentStatus: alertStatus, setEventsLoading, - ]); - - const inProgressAlertActionClick = useCallback(() => { - if (eventId) { - updateAlertStatusAction({ - alertIds: [eventId], - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, - setEventsDeleted, - setEventsLoading, - selectedStatus: FILTER_IN_PROGRESS, - }); - } - - closePopover(); - }, [ - closePopover, - eventId, - onAlertStatusUpdateFailure, - onAlertStatusUpdateSuccess, setEventsDeleted, - setEventsLoading, - ]); - - const disabledInProgressAlertAction = !canUserCRUD || !hasIndexUpdateDelete; - - const inProgressAlertAction = useMemo(() => { - return { - name: i18n.ACTION_IN_PROGRESS_ALERT, - disabled: disabledInProgressAlertAction, - onClick: inProgressAlertActionClick, - [`data-test-subj`]: 'in-progress-alert-status', - }; - }, [disabledInProgressAlertAction, inProgressAlertActionClick]); - - const disabledCloseAlertAction = !hasIndexUpdateDelete && !hasIndexMaintenance; - const closeAlertAction = useMemo(() => { - return { - name: i18n.ACTION_CLOSE_ALERT, - disabled: disabledCloseAlertAction, - onClick: closeAlertActionClick, - [`data-test-subj`]: 'close-alert-status', - }; - }, [disabledCloseAlertAction, closeAlertActionClick]); - - const disabledOpenAlertAction = !hasIndexUpdateDelete && !hasIndexMaintenance; - const openAlertAction = useMemo(() => { - return { - name: i18n.ACTION_OPEN_ALERT, - disabled: disabledOpenAlertAction, - onClick: openAlertActionOnClick, - [`data-test-subj`]: 'open-alert-status', - }; - }, [disabledOpenAlertAction, openAlertActionOnClick]); - - const statusActions = useMemo(() => { - if (!alertStatus) { - return []; - } - - switch (alertStatus) { - case 'open': - return [inProgressAlertAction, closeAlertAction]; - case 'in-progress': - return [openAlertAction, closeAlertAction]; - case 'closed': - return [openAlertAction, inProgressAlertAction]; - default: - return []; - } - }, [alertStatus, inProgressAlertAction, closeAlertAction, openAlertAction]); + onUpdateSuccess: onAlertStatusUpdateSuccess, + onUpdateFailure: onAlertStatusUpdateFailure, + }); return { - statusActions, + actionItems, }; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx index d0f26894bf7d2..ffaea216f3fe3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx @@ -6,7 +6,7 @@ */ import React, { useState, useCallback, useMemo } from 'react'; -import { EuiContextMenu, EuiButton, EuiPopover } from '@elastic/eui'; +import { EuiContextMenu, EuiContextMenuPanel, EuiButton, EuiPopover } from '@elastic/eui'; import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { TAKE_ACTION } from '../alerts_table/alerts_utility_bar/translations'; @@ -135,7 +135,7 @@ export const TakeActionDropdown = React.memo( [onAddExceptionTypeClick] ); - const exceptionActions = useExceptionActions({ + const { exceptionActions } = useExceptionActions({ isEndpointAlert, onAddExceptionTypeClick: handleOnAddExceptionTypeClick, }); @@ -149,7 +149,7 @@ export const TakeActionDropdown = React.memo( onAddEventFilterClick: handleOnAddEventFilterClick, }); - const { statusActions } = useAlertsActions({ + const { actionItems } = useAlertsActions({ alertStatus: actionsData.alertStatus, eventId: actionsData.eventId, timelineId, @@ -191,7 +191,7 @@ export const TakeActionDropdown = React.memo( { id: 1, title: CHANGE_ALERT_STATUS, - items: statusActions, + content: , }, /* Todo: Uncomment case action after getAddToCaseAction is split into action and modal { @@ -210,7 +210,7 @@ export const TakeActionDropdown = React.memo( ), },*/ ], - [alertsActionItems, hostIsolationAction, investigateInTimelineAction, statusActions] + [actionItems, alertsActionItems, hostIsolationAction, investigateInTimelineAction] ); const takeActionButton = useMemo(() => { diff --git a/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx b/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx index 335953e7ee43e..69d7ad36324de 100644 --- a/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx +++ b/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx @@ -83,21 +83,33 @@ export const useStatusBulkActionItems = ({ const actionItems = []; if (currentStatus !== FILTER_OPEN) { actionItems.push( - onClickUpdate(FILTER_OPEN)}> + onClickUpdate(FILTER_OPEN)} + > {i18n.BULK_ACTION_OPEN_SELECTED} ); } if (currentStatus !== FILTER_IN_PROGRESS) { actionItems.push( - onClickUpdate(FILTER_IN_PROGRESS)}> + onClickUpdate(FILTER_IN_PROGRESS)} + > {i18n.BULK_ACTION_IN_PROGRESS_SELECTED} ); } if (currentStatus !== FILTER_CLOSED) { actionItems.push( - onClickUpdate(FILTER_CLOSED)}> + onClickUpdate(FILTER_CLOSED)} + > {i18n.BULK_ACTION_CLOSE_SELECTED} ); diff --git a/x-pack/plugins/timelines/public/index.ts b/x-pack/plugins/timelines/public/index.ts index 6f4de1dd2559e..d31e80de7d285 100644 --- a/x-pack/plugins/timelines/public/index.ts +++ b/x-pack/plugins/timelines/public/index.ts @@ -52,7 +52,7 @@ export { getTimelineIdFromColumnDroppableId, } from './components/drag_and_drop/helpers'; export { StatefulFieldsBrowser } from './components/t_grid/toolbar/fields_browser'; - +export { useStatusBulkActionItems } from './hooks/use_status_bulk_action_items'; // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. export function plugin(initializerContext: PluginInitializerContext) { diff --git a/x-pack/plugins/timelines/public/plugin.ts b/x-pack/plugins/timelines/public/plugin.ts index 24bc99e59aaf0..2ec35ef1a51f3 100644 --- a/x-pack/plugins/timelines/public/plugin.ts +++ b/x-pack/plugins/timelines/public/plugin.ts @@ -27,6 +27,7 @@ import { tGridReducer } from './store/t_grid/reducer'; import { useDraggableKeyboardWrapper } from './components/drag_and_drop/draggable_keyboard_wrapper_hook'; import { useAddToTimeline, useAddToTimelineSensor } from './hooks/use_add_to_timeline'; import { getHoverActions } from './components/hover_actions'; + export class TimelinesPlugin implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} private _store: Store | undefined; From cdf90aae42a100c0219884c15e237f22a7675806 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Tue, 10 Aug 2021 06:16:55 -0700 Subject: [PATCH 023/104] Adds new SavedObjectsRespository error type for 404 that do not originate from Elasticsearch responses (#107301) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...creategenericnotfoundesunavailableerror.md | 23 +++ ...in-core-server.savedobjectserrorhelpers.md | 1 + src/core/server/elasticsearch/client/mocks.ts | 5 +- src/core/server/elasticsearch/index.ts | 1 + .../supported_server_response_check.ts | 17 +++ .../saved_objects/service/lib/errors.test.ts | 41 +++++ .../saved_objects/service/lib/errors.ts | 8 + .../service/lib/repository.test.js | 140 ++++++++++++++---- .../saved_objects/service/lib/repository.ts | 48 ++++-- src/core/server/server.api.md | 2 + 10 files changed, 248 insertions(+), 38 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfoundesunavailableerror.md create mode 100644 src/core/server/elasticsearch/supported_server_response_check.ts diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfoundesunavailableerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfoundesunavailableerror.md new file mode 100644 index 0000000000000..e05f9466aa9ee --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfoundesunavailableerror.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [createGenericNotFoundEsUnavailableError](./kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfoundesunavailableerror.md) + +## SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError() method + +Signature: + +```typescript +static createGenericNotFoundEsUnavailableError(type: string, id: string): DecoratedError; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | +| id | string | | + +Returns: + +`DecoratedError` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md index 2dc78f2df3a83..67056c8a3cb50 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md @@ -18,6 +18,7 @@ export declare class SavedObjectsErrorHelpers | [createBadRequestError(reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.createbadrequesterror.md) | static | | | [createConflictError(type, id, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md) | static | | | [createGenericNotFoundError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfounderror.md) | static | | +| [createGenericNotFoundEsUnavailableError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfoundesunavailableerror.md) | static | | | [createIndexAliasNotFoundError(alias)](./kibana-plugin-core-server.savedobjectserrorhelpers.createindexaliasnotfounderror.md) | static | | | [createInvalidVersionError(versionInput)](./kibana-plugin-core-server.savedobjectserrorhelpers.createinvalidversionerror.md) | static | | | [createTooManyRequestsError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.createtoomanyrequestserror.md) | static | | diff --git a/src/core/server/elasticsearch/client/mocks.ts b/src/core/server/elasticsearch/client/mocks.ts index a7fbce7180223..848d9c204bfbf 100644 --- a/src/core/server/elasticsearch/client/mocks.ts +++ b/src/core/server/elasticsearch/client/mocks.ts @@ -141,9 +141,10 @@ export type MockedTransportRequestPromise = TransportRequestPromise & { const createSuccessTransportRequestPromise = ( body: T, - { statusCode = 200 }: { statusCode?: number } = {} + { statusCode = 200 }: { statusCode?: number } = {}, + headers?: Record ): MockedTransportRequestPromise> => { - const response = createApiResponse({ body, statusCode }); + const response = createApiResponse({ body, statusCode, headers }); const promise = Promise.resolve(response); (promise as MockedTransportRequestPromise>).abort = jest.fn(); diff --git a/src/core/server/elasticsearch/index.ts b/src/core/server/elasticsearch/index.ts index d97e3331c7cf5..8bcc841669fc9 100644 --- a/src/core/server/elasticsearch/index.ts +++ b/src/core/server/elasticsearch/index.ts @@ -37,3 +37,4 @@ export type { GetResponse, DeleteDocumentResponse, } from './client'; +export { isSupportedEsServer } from './supported_server_response_check'; diff --git a/src/core/server/elasticsearch/supported_server_response_check.ts b/src/core/server/elasticsearch/supported_server_response_check.ts new file mode 100644 index 0000000000000..6fe812bc58518 --- /dev/null +++ b/src/core/server/elasticsearch/supported_server_response_check.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +export const PRODUCT_RESPONSE_HEADER = 'x-elastic-product'; +/** + * Response headers check to determine if the response is from Elasticsearch + * @param headers Response headers + * @returns boolean + */ +// This check belongs to the elasticsearch service as a dedicated helper method. +export const isSupportedEsServer = (headers: Record | null) => { + return !!headers && headers[PRODUCT_RESPONSE_HEADER] === 'Elasticsearch'; +}; diff --git a/src/core/server/saved_objects/service/lib/errors.test.ts b/src/core/server/saved_objects/service/lib/errors.test.ts index a366dce626ec2..3bea693429254 100644 --- a/src/core/server/saved_objects/service/lib/errors.test.ts +++ b/src/core/server/saved_objects/service/lib/errors.test.ts @@ -439,4 +439,45 @@ describe('savedObjectsClient/errorTypes', () => { }); }); }); + + describe('NotFoundEsUnavailableError', () => { + it('makes an error identifiable as an EsUnavailable error', () => { + const error = SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError('foo', 'bar'); + expect(SavedObjectsErrorHelpers.isEsUnavailableError(error)).toBe(true); + }); + + it('returns a boom error', () => { + const error = SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError('foo', 'bar'); + expect(error).toHaveProperty('isBoom', true); + }); + + it('decorates the error message with the saved object that was not found', () => { + const error = SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError('foo', 'bar'); + expect(error.output.payload).toHaveProperty( + 'message', + 'x-elastic-product not present or not recognized: Saved object [foo/bar] not found' + ); + }); + + describe('error.output', () => { + it('specifies the saved object that was not found', () => { + const error = SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError( + 'foo', + 'bar' + ); + expect(error.output.payload).toHaveProperty( + 'message', + 'x-elastic-product not present or not recognized: Saved object [foo/bar] not found' + ); + }); + + it('sets statusCode to 503', () => { + const error = SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError( + 'foo', + 'bar' + ); + expect(error.output).toHaveProperty('statusCode', 503); + }); + }); + }); }); diff --git a/src/core/server/saved_objects/service/lib/errors.ts b/src/core/server/saved_objects/service/lib/errors.ts index 581145c7c09d1..c1e1e9589b9ae 100644 --- a/src/core/server/saved_objects/service/lib/errors.ts +++ b/src/core/server/saved_objects/service/lib/errors.ts @@ -202,4 +202,12 @@ export class SavedObjectsErrorHelpers { public static isGeneralError(error: Error | DecoratedError) { return isSavedObjectsClientError(error) && error[code] === CODE_GENERAL_ERROR; } + + public static createGenericNotFoundEsUnavailableError(type: string, id: string) { + const notFoundError = this.createGenericNotFoundError(type, id); + return this.decorateEsUnavailableError( + new Error(`${notFoundError.message}`), + `x-elastic-product not present or not recognized` + ); + } } 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 78af9f0753374..c025adce29808 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -44,6 +44,8 @@ const createGenericNotFoundError = (...args) => SavedObjectsErrorHelpers.createGenericNotFoundError(...args).output.payload; const createUnsupportedTypeError = (...args) => SavedObjectsErrorHelpers.createUnsupportedTypeError(...args).output.payload; +const createGenericNotFoundEsUnavailableError = (...args) => + SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(...args).output.payload; describe('SavedObjectsRepository', () => { let client; @@ -2202,6 +2204,11 @@ describe('SavedObjectsRepository', () => { createGenericNotFoundError(type, id) ); }; + const expectNotFoundEsUnavailableError = async (type, id) => { + await expect(savedObjectsRepository.delete(type, id)).rejects.toThrowError( + createGenericNotFoundEsUnavailableError(type, id) + ); + }; it(`throws when options.namespace is '*'`, async () => { await expect( @@ -2221,7 +2228,11 @@ describe('SavedObjectsRepository', () => { it(`throws when ES is unable to find the document during get`, async () => { client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) + elasticsearchClientMock.createSuccessTransportRequestPromise( + { found: false }, + undefined, + { 'x-elastic-product': 'Elasticsearch' } + ) ); await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); expect(client.get).toHaveBeenCalledTimes(1); @@ -2229,12 +2240,30 @@ describe('SavedObjectsRepository', () => { it(`throws when ES is unable to find the index during get`, async () => { client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + elasticsearchClientMock.createSuccessTransportRequestPromise( + {}, + { statusCode: 404 }, + { 'x-elastic-product': 'Elasticsearch' } + ) ); await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); expect(client.get).toHaveBeenCalledTimes(1); }); + it(`throws when ES is unable to find the document during get with missing Elasticsearch header`, async () => { + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) + ); + await expectNotFoundEsUnavailableError(MULTI_NAMESPACE_ISOLATED_TYPE, id); + }); + + it(`throws when ES is unable to find the index during get with missing Elasticsearch header`, async () => { + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + ); + await expectNotFoundEsUnavailableError(MULTI_NAMESPACE_ISOLATED_TYPE, id); + }); + it(`throws when the type is multi-namespace and the document exists, but not in this namespace`, async () => { const response = getMockGetResponse({ type: MULTI_NAMESPACE_ISOLATED_TYPE, id }, namespace); client.get.mockResolvedValueOnce( @@ -2278,7 +2307,7 @@ describe('SavedObjectsRepository', () => { client.delete.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise({ result: 'not_found' }) ); - await expectNotFoundError(type, id); + await expectNotFoundEsUnavailableError(type, id); expect(client.delete).toHaveBeenCalledTimes(1); }); @@ -2288,7 +2317,7 @@ describe('SavedObjectsRepository', () => { error: { type: 'index_not_found_exception' }, }) ); - await expectNotFoundError(type, id); + await expectNotFoundEsUnavailableError(type, id); expect(client.delete).toHaveBeenCalledTimes(1); }); @@ -3170,7 +3199,11 @@ describe('SavedObjectsRepository', () => { createGenericNotFoundError(type, id) ); }; - + const expectNotFoundEsUnavailableError = async (type, id) => { + await expect(savedObjectsRepository.get(type, id)).rejects.toThrowError( + createGenericNotFoundEsUnavailableError(type, id) + ); + }; it(`throws when options.namespace is '*'`, async () => { await expect( savedObjectsRepository.get(type, id, { namespace: ALL_NAMESPACES_STRING }) @@ -3189,7 +3222,11 @@ describe('SavedObjectsRepository', () => { it(`throws when ES is unable to find the document during get`, async () => { client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) + elasticsearchClientMock.createSuccessTransportRequestPromise( + { found: false }, + undefined, + { 'x-elastic-product': 'Elasticsearch' } + ) ); await expectNotFoundError(type, id); expect(client.get).toHaveBeenCalledTimes(1); @@ -3197,7 +3234,11 @@ describe('SavedObjectsRepository', () => { it(`throws when ES is unable to find the index during get`, async () => { client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + elasticsearchClientMock.createSuccessTransportRequestPromise( + {}, + { statusCode: 404 }, + { 'x-elastic-product': 'Elasticsearch' } + ) ); await expectNotFoundError(type, id); expect(client.get).toHaveBeenCalledTimes(1); @@ -3213,6 +3254,15 @@ describe('SavedObjectsRepository', () => { }); expect(client.get).toHaveBeenCalledTimes(1); }); + + it(`throws when ES does not return the correct header when finding the document during get`, async () => { + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) + ); + await expectNotFoundEsUnavailableError(type, id); + + expect(client.get).toHaveBeenCalledTimes(1); + }); }); describe('returns', () => { @@ -3314,9 +3364,12 @@ describe('SavedObjectsRepository', () => { it('because alias is not used and actual object is not found', async () => { const options = { namespace: undefined }; - const response = { found: false }; client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(response) // for actual target + elasticsearchClientMock.createSuccessTransportRequestPromise( + { found: false }, + undefined, + { 'x-elastic-product': 'Elasticsearch' } + ) // for actual target ); await expectNotFoundError(type, id, options); @@ -3854,26 +3907,34 @@ describe('SavedObjectsRepository', () => { if (registry.isMultiNamespace(type)) { const mockGetResponse = getMockGetResponse({ type, id }, options?.namespace); client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise(mockGetResponse) + elasticsearchClientMock.createSuccessTransportRequestPromise( + { ...mockGetResponse }, + { statusCode: 200 }, + { 'x-elastic-product': 'Elasticsearch' } + ) ); } client.update.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ - _id: `${type}:${id}`, - ...mockVersionProps, - result: 'updated', - // don't need the rest of the source for test purposes, just the namespace and namespaces attributes - get: { - _source: { - namespaces: [options?.namespace ?? 'default'], - namespace: options?.namespace, + elasticsearchClientMock.createSuccessTransportRequestPromise( + { + _id: `${type}:${id}`, + ...mockVersionProps, + result: 'updated', + // don't need the rest of the source for test purposes, just the namespace and namespaces attributes + get: { + _source: { + namespaces: [options?.namespace ?? 'default'], + namespace: options?.namespace, - // "includeOriginId" is not an option for the operation; however, if the existing saved object contains an originId attribute, the - // operation will return it in the result. This flag is just used for test purposes to modify the mock cluster call response. - ...(includeOriginId && { originId }), + // "includeOriginId" is not an option for the operation; however, if the existing saved object contains an originId attribute, the + // operation will return it in the result. This flag is just used for test purposes to modify the mock cluster call response. + ...(includeOriginId && { originId }), + }, }, }, - }) + { statusCode: 200 }, + { 'x-elastic-product': 'Elasticsearch' } + ) ); const result = await savedObjectsRepository.update(type, id, attributes, options); expect(client.get).toHaveBeenCalledTimes(registry.isMultiNamespace(type) ? 1 : 0); @@ -4059,6 +4120,11 @@ describe('SavedObjectsRepository', () => { createGenericNotFoundError(type, id) ); }; + const expectNotFoundEsUnavailableError = async (type, id) => { + await expect(savedObjectsRepository.update(type, id)).rejects.toThrowError( + createGenericNotFoundEsUnavailableError(type, id) + ); + }; it(`throws when options.namespace is '*'`, async () => { await expect( @@ -4078,7 +4144,11 @@ describe('SavedObjectsRepository', () => { it(`throws when ES is unable to find the document during get`, async () => { client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) + elasticsearchClientMock.createSuccessTransportRequestPromise( + { found: false }, + undefined, + { 'x-elastic-product': 'Elasticsearch' } + ) ); await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); expect(client.get).toHaveBeenCalledTimes(1); @@ -4086,12 +4156,32 @@ describe('SavedObjectsRepository', () => { it(`throws when ES is unable to find the index during get`, async () => { client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + elasticsearchClientMock.createSuccessTransportRequestPromise( + {}, + { statusCode: 404 }, + { 'x-elastic-product': 'Elasticsearch' } + ) ); await expectNotFoundError(MULTI_NAMESPACE_ISOLATED_TYPE, id); expect(client.get).toHaveBeenCalledTimes(1); }); + it(`throws when ES is unable to find the document during get with missing Elasticsearch header`, async () => { + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({ found: false }) + ); + await expectNotFoundEsUnavailableError(MULTI_NAMESPACE_ISOLATED_TYPE, id); + expect(client.get).toHaveBeenCalledTimes(1); + }); + + it(`throws when ES is unable to find the index during get with missing Elasticsearch`, async () => { + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise({}, { statusCode: 404 }) + ); + await expectNotFoundEsUnavailableError(MULTI_NAMESPACE_ISOLATED_TYPE, id); + expect(client.get).toHaveBeenCalledTimes(1); + }); + it(`throws when type is multi-namespace and the document exists, but not in this namespace`, async () => { const response = getMockGetResponse({ type: MULTI_NAMESPACE_ISOLATED_TYPE, id }, namespace); client.get.mockResolvedValueOnce( diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 6899f8613b07f..7ac4fe87bfc19 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -14,6 +14,7 @@ import { REPOSITORY_RESOLVE_OUTCOME_STATS, } from '../../../core_usage_data'; import type { ElasticsearchClient } from '../../../elasticsearch/'; +import { isSupportedEsServer } from '../../../elasticsearch'; import type { Logger } from '../../../logging'; import { getRootPropertiesObjects, IndexMapping } from '../../mappings'; import { @@ -648,7 +649,7 @@ export class SavedObjectsRepository { } } - const { body, statusCode } = await this.client.delete( + const { body, statusCode, headers } = await this.client.delete( { id: rawId, index: this.getIndexForType(type), @@ -665,9 +666,15 @@ export class SavedObjectsRepository { const deleteDocNotFound = body.result === 'not_found'; const deleteIndexNotFound = body.error && body.error.type === 'index_not_found_exception'; + const esServerSupported = isSupportedEsServer(headers); if (deleteDocNotFound || deleteIndexNotFound) { - // see "404s from missing index" above - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + if (esServerSupported) { + // see "404s from missing index" above + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } else { + // throw if we can't verify the response is from Elasticsearch + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(type, id); + } } throw new Error( @@ -1009,19 +1016,19 @@ export class SavedObjectsRepository { if (!this._allowedTypes.includes(type)) { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } - const namespace = normalizeNamespace(options.namespace); - - const { body, statusCode } = await this.client.get( + const { body, statusCode, headers } = await this.client.get( { id: this._serializer.generateRawId(namespace, type, id), index: this.getIndexForType(type), }, { ignore: [404] } ); - const indexNotFound = statusCode === 404; - + // check if we have the elasticsearch header when index is not found and if we do, ensure it is Elasticsearch + if (!isFoundGetResponse(body) && !isSupportedEsServer(headers)) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(type, id); + } if ( !isFoundGetResponse(body) || indexNotFound || @@ -1030,7 +1037,6 @@ export class SavedObjectsRepository { // see "404s from missing index" above throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } - return getSavedObjectFromSource(this._registry, type, id, body); } @@ -1248,7 +1254,19 @@ export class SavedObjectsRepository { _source_includes: ['namespace', 'namespaces', 'originId'], require_alias: true, }) + .then((res) => { + const indexNotFound = res.statusCode === 404; + const esServerSupported = isSupportedEsServer(res.headers); + // check if we have the elasticsearch header when index is not found and if we do, ensure it is Elasticsearch + if (indexNotFound && !esServerSupported) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(type, id); + } + return res; + }) .catch((err) => { + if (SavedObjectsErrorHelpers.isEsUnavailableError(err)) { + throw err; + } if (SavedObjectsErrorHelpers.isNotFoundError(err)) { // see "404s from missing index" above throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); @@ -2070,7 +2088,7 @@ export class SavedObjectsRepository { * @param id The ID of the saved object. * @param namespace The target namespace. * @returns Raw document from Elasticsearch. - * @throws Will throw an error if the saved object is not found, or if it doesn't include the target namespace. + * @throws Will throw an error if the saved object is not found, if it doesn't include the target namespace or if the response is not identifiable as an Elasticsearch response. */ private async preflightCheckIncludesNamespace(type: string, id: string, namespace?: string) { if (!this._registry.isMultiNamespace(type)) { @@ -2078,7 +2096,7 @@ export class SavedObjectsRepository { } const rawId = this._serializer.generateRawId(undefined, type, id); - const { body, statusCode } = await this.client.get( + const { body, statusCode, headers } = await this.client.get( { id: rawId, index: this.getIndexForType(type), @@ -2087,6 +2105,14 @@ export class SavedObjectsRepository { ); const indexFound = statusCode !== 404; + + // check if we have the elasticsearch header when index is not found and if we do, ensure it is Elasticsearch + const esServerSupported = isSupportedEsServer(headers); + + if (!isFoundGetResponse(body) && !esServerSupported) { + throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(type, id); + } + if ( !indexFound || !isFoundGetResponse(body) || diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 50cb98939ec57..985e0e337d75c 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2517,6 +2517,8 @@ export class SavedObjectsErrorHelpers { // (undocumented) static createGenericNotFoundError(type?: string | null, id?: string | null): DecoratedError; // (undocumented) + static createGenericNotFoundEsUnavailableError(type: string, id: string): DecoratedError; + // (undocumented) static createIndexAliasNotFoundError(alias: string): DecoratedError; // (undocumented) static createInvalidVersionError(versionInput?: string): DecoratedError; From 7d6a2b520fd1ce34bc5e239ed12e19eec74da8e4 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 10 Aug 2021 15:40:36 +0200 Subject: [PATCH 024/104] [APM] Default env for creating rule outside of APM app (#107942) --- .../alerting/error_count_alert_trigger/index.tsx | 2 ++ x-pack/plugins/apm/public/components/alerting/fields.tsx | 7 +++++-- .../alerting/transaction_duration_alert_trigger/index.tsx | 3 +++ .../transaction_duration_anomaly_alert_trigger/index.tsx | 2 ++ .../transaction_error_rate_alert_trigger/index.tsx | 2 ++ 5 files changed, 14 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx index 3c79a2475bf29..a06520f1c5bfc 100644 --- a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { defaults, omit } from 'lodash'; import React from 'react'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { asInteger } from '../../../../common/utils/formatters'; import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; @@ -47,6 +48,7 @@ export function ErrorCountAlertTrigger(props: Props) { threshold: 25, windowSize: 1, windowUnit: 'm', + environment: ENVIRONMENT_ALL.value, } ); diff --git a/x-pack/plugins/apm/public/components/alerting/fields.tsx b/x-pack/plugins/apm/public/components/alerting/fields.tsx index 2e16599c02716..8480fd276cb74 100644 --- a/x-pack/plugins/apm/public/components/alerting/fields.tsx +++ b/x-pack/plugins/apm/public/components/alerting/fields.tsx @@ -9,7 +9,10 @@ import { EuiSelect, EuiExpression, EuiFieldNumber } from '@elastic/eui'; import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSelectOption } from '@elastic/eui'; -import { getEnvironmentLabel } from '../../../common/environment_filter_values'; +import { + ENVIRONMENT_ALL, + getEnvironmentLabel, +} from '../../../common/environment_filter_values'; import { PopoverExpression } from './service_alert_trigger/popover_expression'; const ALL_OPTION = i18n.translate('xpack.apm.alerting.fields.all_option', { @@ -42,7 +45,7 @@ export function EnvironmentField({ // "1" means "All" is the only option and we should not show a select. if (options.length === 1) { - return ; + return ; } return ( diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx index c54f32d805818..2a73cba0a63d5 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx @@ -9,6 +9,7 @@ import { EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { defaults, map, omit } from 'lodash'; import React from 'react'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { CoreStart } from '../../../../../../../src/core/public'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; @@ -90,6 +91,8 @@ export function TransactionDurationAlertTrigger(props: Props) { threshold: 1500, windowSize: 5, windowUnit: 'm', + environment: ENVIRONMENT_ALL.value, + transactionType: transactionTypes[0], } ); diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/index.tsx index 97ce73a746329..519b18cd3a6b6 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_duration_anomaly_alert_trigger/index.tsx @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { defaults, omit } from 'lodash'; import React from 'react'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { ANOMALY_SEVERITY } from '../../../../common/ml_constants'; import { useServiceTransactionTypesFetcher } from '../../../context/apm_service/use_service_transaction_types_fetcher'; import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; @@ -60,6 +61,7 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) { windowSize: 15, windowUnit: 'm', anomalySeverityType: ANOMALY_SEVERITY.CRITICAL, + environment: ENVIRONMENT_ALL.value, } ); diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx index 6118f99e4bd9f..8f2e212a22681 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx @@ -7,6 +7,7 @@ import { defaults, omit } from 'lodash'; import React from 'react'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { CoreStart } from '../../../../../../../src/core/public'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; @@ -57,6 +58,7 @@ export function TransactionErrorRateAlertTrigger(props: Props) { threshold: 30, windowSize: 5, windowUnit: 'm', + environment: ENVIRONMENT_ALL.value, } ); From 6450df1885a5df1ef08e9c06a638536d37b1f625 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 10 Aug 2021 15:44:24 +0200 Subject: [PATCH 025/104] [FieldFormats] Add editors tests (#107770) --- .../bytes/__snapshots__/bytes.test.tsx.snap | 1 + .../color/__snapshots__/color.test.tsx.snap | 6 + .../editors/color/color.tsx | 12 +- .../__snapshots__/duration.test.tsx.snap | 6 + .../editors/duration/duration.tsx | 2 + .../number/__snapshots__/number.test.tsx.snap | 1 + .../editors/number/number.tsx | 1 + .../__snapshots__/percent.test.tsx.snap | 1 + .../__snapshots__/static_lookup.test.tsx.snap | 6 + .../editors/static_lookup/static_lookup.tsx | 11 +- .../__snapshots__/truncate.test.tsx.snap | 1 + .../editors/truncate/truncate.tsx | 1 + test/common/services/index.ts | 2 + test/common/services/index_patterns.ts | 35 ++ .../apps/management/_field_formatter.js | 53 -- .../apps/management/_field_formatter.ts | 571 ++++++++++++++++++ 16 files changed, 655 insertions(+), 55 deletions(-) create mode 100644 test/common/services/index_patterns.ts delete mode 100644 test/functional/apps/management/_field_formatter.js create mode 100644 test/functional/apps/management/_field_formatter.ts diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap index 2ab8037639f85..2b71d1882b0cf 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap @@ -43,6 +43,7 @@ exports[`BytesFormatEditor should render normally 1`] = ` labelType="label" > { this.onColorChange( { @@ -120,6 +121,7 @@ export class ColorFormatEditor extends DefaultFormatEditor { this.onColorChange( { @@ -144,6 +146,7 @@ export class ColorFormatEditor extends DefaultFormatEditor { this.onColorChange( { @@ -168,6 +171,7 @@ export class ColorFormatEditor extends DefaultFormatEditor { this.onColorChange( { @@ -220,6 +224,7 @@ export class ColorFormatEditor extends DefaultFormatEditor items.length > 1, + 'data-test-subj': 'colorEditorRemoveColor', }, ], }, @@ -229,7 +234,12 @@ export class ColorFormatEditor extends DefaultFormatEditor - + { return { @@ -126,6 +127,7 @@ export class DurationFormatEditor extends DefaultFormatEditor< isInvalid={!!error} > { return { diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap index 4d42e3848d3cd..8b59c0da10167 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap @@ -43,6 +43,7 @@ exports[`NumberFormatEditor should render normally 1`] = ` labelType="label" > { diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap index fce51e8fa3871..7d8ab5e682a3e 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap @@ -43,6 +43,7 @@ exports[`PercentFormatEditor should render normally 1`] = ` labelType="label" > { this.onLookupChange( { @@ -105,6 +106,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor { this.onLookupChange( { @@ -136,6 +138,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor items.length > 1, }, ], @@ -147,7 +150,12 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor - + { if (e.target.checkValidity()) { this.onChange({ diff --git a/test/common/services/index.ts b/test/common/services/index.ts index 370d289093517..c04bd778468a9 100644 --- a/test/common/services/index.ts +++ b/test/common/services/index.ts @@ -15,6 +15,7 @@ import { RandomnessService } from './randomness'; import { SecurityServiceProvider } from './security'; import { EsDeleteAllIndicesProvider } from './es_delete_all_indices'; import { SavedObjectInfoService } from './saved_object_info'; +import { IndexPatternsService } from './index_patterns'; export const services = { deployment: DeploymentService, @@ -26,4 +27,5 @@ export const services = { security: SecurityServiceProvider, esDeleteAllIndices: EsDeleteAllIndicesProvider, savedObjectInfo: SavedObjectInfoService, + indexPatterns: IndexPatternsService, }; diff --git a/test/common/services/index_patterns.ts b/test/common/services/index_patterns.ts new file mode 100644 index 0000000000000..5b6d20990b6d1 --- /dev/null +++ b/test/common/services/index_patterns.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrService } from '../ftr_provider_context'; +import { IndexPatternSpec } from '../../../src/plugins/data/common'; + +export class IndexPatternsService extends FtrService { + private readonly kibanaServer = this.ctx.getService('kibanaServer'); + + /** + * Create a new index pattern + */ + async create( + indexPattern: { title: string }, + { override = false }: { override: boolean } = { override: false } + ): Promise { + const response = await this.kibanaServer.request<{ + index_pattern: IndexPatternSpec; + }>({ + path: '/api/index_patterns/index_pattern', + method: 'POST', + body: { + override, + index_pattern: indexPattern, + }, + }); + + return response.data.index_pattern; + } +} diff --git a/test/functional/apps/management/_field_formatter.js b/test/functional/apps/management/_field_formatter.js deleted file mode 100644 index 383b4faecc40c..0000000000000 --- a/test/functional/apps/management/_field_formatter.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export default function ({ getService, getPageObjects }) { - const esArchiver = getService('esArchiver'); - const kibanaServer = getService('kibanaServer'); - const browser = getService('browser'); - const PageObjects = getPageObjects(['settings']); - const testSubjects = getService('testSubjects'); - - describe('field formatter', function () { - this.tags(['skipFirefox']); - - before(async function () { - await browser.setWindowSize(1200, 800); - await esArchiver.load('test/functional/fixtures/es_archiver/discover'); - await kibanaServer.uiSettings.replace({}); - await kibanaServer.uiSettings.update({}); - }); - - after(async function afterAll() { - await PageObjects.settings.navigateTo(); - await esArchiver.emptyKibanaIndex(); - }); - - describe('set and change field formatter', function describeIndexTests() { - // addresses https://github.com/elastic/kibana/issues/93349 - it('can change format more than once', async function () { - await PageObjects.settings.navigateTo(); - await PageObjects.settings.clickKibanaIndexPatterns(); - await PageObjects.settings.clickIndexPatternLogstash(); - await PageObjects.settings.clickAddField(); - await PageObjects.settings.setFieldType('Long'); - const formatRow = await testSubjects.find('formatRow'); - const formatRowToggle = ( - await formatRow.findAllByCssSelector('[data-test-subj="toggle"]') - )[0]; - - await formatRowToggle.click(); - await PageObjects.settings.setFieldFormat('duration'); - await PageObjects.settings.setFieldFormat('bytes'); - await PageObjects.settings.setFieldFormat('duration'); - await testSubjects.click('euiFlyoutCloseButton'); - await PageObjects.settings.closeIndexPatternFieldEditor(); - }); - }); - }); -} diff --git a/test/functional/apps/management/_field_formatter.ts b/test/functional/apps/management/_field_formatter.ts new file mode 100644 index 0000000000000..60c1bbe7b3d1d --- /dev/null +++ b/test/functional/apps/management/_field_formatter.ts @@ -0,0 +1,571 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { ES_FIELD_TYPES } from '@kbn/field-types'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { FIELD_FORMAT_IDS } from '../../../../src/plugins/field_formats/common'; +import { WebElementWrapper } from '../../services/lib/web_element_wrapper'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const browser = getService('browser'); + const PageObjects = getPageObjects(['settings', 'common']); + const testSubjects = getService('testSubjects'); + const es = getService('es'); + const indexPatterns = getService('indexPatterns'); + const toasts = getService('toasts'); + + describe('field formatter', function () { + this.tags(['skipFirefox']); + + before(async function () { + await browser.setWindowSize(1200, 800); + await esArchiver.load('test/functional/fixtures/es_archiver/discover'); + await kibanaServer.uiSettings.replace({}); + await kibanaServer.uiSettings.update({}); + }); + + after(async function afterAll() { + await PageObjects.settings.navigateTo(); + await esArchiver.emptyKibanaIndex(); + }); + + describe('set and change field formatter', function describeIndexTests() { + // addresses https://github.com/elastic/kibana/issues/93349 + it('can change format more than once', async function () { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaIndexPatterns(); + await PageObjects.settings.clickIndexPatternLogstash(); + await PageObjects.settings.clickAddField(); + await PageObjects.settings.setFieldType('Long'); + const formatRow = await testSubjects.find('formatRow'); + const formatRowToggle = ( + await formatRow.findAllByCssSelector('[data-test-subj="toggle"]') + )[0]; + + await formatRowToggle.click(); + await PageObjects.settings.setFieldFormat('duration'); + await PageObjects.settings.setFieldFormat('bytes'); + await PageObjects.settings.setFieldFormat('duration'); + await testSubjects.click('euiFlyoutCloseButton'); + await PageObjects.settings.closeIndexPatternFieldEditor(); + }); + }); + + /** + * The purpose of these tests is to cover **editing experience** of different field formats editors, + * The logic of each converter is extensively covered by unit tests. + * TODO: these tests also could check field formats behaviour with different combinations of browser locale, timezone and ui settings + */ + describe('field format editors', () => { + describe('String format', () => { + testFormatEditors([ + { + fieldType: ES_FIELD_TYPES.TEXT, + fieldValue: 'A regular text', + applyFormatterType: FIELD_FORMAT_IDS.STRING, + expectFormattedValue: 'A regular text', + + // check available formats for ES_FIELD_TYPES.TEXT + expectFormatterTypes: [ + FIELD_FORMAT_IDS.BOOLEAN, + FIELD_FORMAT_IDS.COLOR, + FIELD_FORMAT_IDS.STATIC_LOOKUP, + FIELD_FORMAT_IDS.STRING, + FIELD_FORMAT_IDS.TRUNCATE, + FIELD_FORMAT_IDS.URL, + ], + }, + { + fieldType: ES_FIELD_TYPES.TEXT, + fieldValue: 'A regular text', + applyFormatterType: FIELD_FORMAT_IDS.STRING, + expectFormattedValue: 'a regular text', + beforeSave: async () => { + await testSubjects.selectValue('stringEditorTransform', 'lower'); + }, + }, + { + fieldType: ES_FIELD_TYPES.KEYWORD, + fieldValue: 'a keyword', + applyFormatterType: FIELD_FORMAT_IDS.STRING, + expectFormattedValue: 'A KEYWORD', + beforeSave: async () => { + await testSubjects.selectValue('stringEditorTransform', 'upper'); + }, + // check available formats for ES_FIELD_TYPES.KEYWORD + expectFormatterTypes: [ + FIELD_FORMAT_IDS.BOOLEAN, + FIELD_FORMAT_IDS.COLOR, + FIELD_FORMAT_IDS.STATIC_LOOKUP, + FIELD_FORMAT_IDS.STRING, + FIELD_FORMAT_IDS.TRUNCATE, + FIELD_FORMAT_IDS.URL, + ], + }, + { + fieldType: ES_FIELD_TYPES.KEYWORD, + fieldValue: 'a keyword', + applyFormatterType: FIELD_FORMAT_IDS.STRING, + expectFormattedValue: 'A Keyword', + beforeSave: async () => { + await testSubjects.selectValue('stringEditorTransform', 'title'); + }, + }, + { + fieldType: ES_FIELD_TYPES.KEYWORD, + fieldValue: 'com.organizations.project.ClassName', + applyFormatterType: FIELD_FORMAT_IDS.STRING, + expectFormattedValue: 'c.o.p.ClassName', + beforeSave: async () => { + await testSubjects.selectValue('stringEditorTransform', 'short'); + }, + }, + { + fieldType: ES_FIELD_TYPES.KEYWORD, + fieldValue: 'SGVsbG8gd29ybGQ=', + applyFormatterType: FIELD_FORMAT_IDS.STRING, + expectFormattedValue: 'Hello world', + beforeSave: async () => { + await testSubjects.selectValue('stringEditorTransform', 'base64'); + }, + }, + { + fieldType: ES_FIELD_TYPES.KEYWORD, + fieldValue: '%EC%95%88%EB%85%95%20%ED%82%A4%EB%B0%94%EB%82%98', + applyFormatterType: FIELD_FORMAT_IDS.STRING, + expectFormattedValue: '안녕 키바나', + beforeSave: async () => { + await testSubjects.selectValue('stringEditorTransform', 'urlparam'); + }, + }, + { + fieldType: ES_FIELD_TYPES.KEYWORD, + fieldValue: '123456789', + applyFormatterType: FIELD_FORMAT_IDS.TRUNCATE, + expectFormattedValue: '123...', + beforeSave: async () => { + await testSubjects.setValue('truncateEditorLength', '3'); + }, + }, + { + fieldType: ES_FIELD_TYPES.INTEGER, + fieldValue: 324, + applyFormatterType: FIELD_FORMAT_IDS.STRING, + expectFormattedValue: '324', + // check available formats for ES_FIELD_TYPES.INTEGER + expectFormatterTypes: [ + FIELD_FORMAT_IDS.BOOLEAN, + FIELD_FORMAT_IDS.BYTES, + FIELD_FORMAT_IDS.COLOR, + FIELD_FORMAT_IDS.DURATION, + FIELD_FORMAT_IDS.NUMBER, + FIELD_FORMAT_IDS.PERCENT, + FIELD_FORMAT_IDS.STATIC_LOOKUP, + FIELD_FORMAT_IDS.STRING, + FIELD_FORMAT_IDS.URL, + ], + }, + ]); + }); + + describe('Number format', () => { + testFormatEditors([ + { + fieldType: ES_FIELD_TYPES.LONG, + fieldValue: 324, + applyFormatterType: FIELD_FORMAT_IDS.NUMBER, + expectFormattedValue: '324', + // check available formats for ES_FIELD_TYPES.LONG + expectFormatterTypes: [ + FIELD_FORMAT_IDS.BOOLEAN, + FIELD_FORMAT_IDS.BYTES, + FIELD_FORMAT_IDS.COLOR, + FIELD_FORMAT_IDS.DURATION, + FIELD_FORMAT_IDS.NUMBER, + FIELD_FORMAT_IDS.PERCENT, + FIELD_FORMAT_IDS.STATIC_LOOKUP, + FIELD_FORMAT_IDS.STRING, + FIELD_FORMAT_IDS.URL, + ], + }, + { + fieldType: ES_FIELD_TYPES.LONG, + fieldValue: 324, + applyFormatterType: FIELD_FORMAT_IDS.NUMBER, + expectFormattedValue: '+324', + beforeSave: async () => { + await testSubjects.setValue('numberEditorFormatPattern', '+0,0'); + }, + }, + ]); + }); + + describe('URL format', () => { + testFormatEditors([ + { + fieldType: ES_FIELD_TYPES.LONG, + fieldValue: 100, + applyFormatterType: FIELD_FORMAT_IDS.URL, + expectFormattedValue: 'https://elastic.co/?value=100', + beforeSave: async () => { + await testSubjects.setValue( + 'urlEditorUrlTemplate', + 'https://elastic.co/?value={{value}}' + ); + }, + expect: async (renderedValueContainer) => { + expect( + await (await renderedValueContainer.findByTagName('a')).getAttribute('href') + ).to.be('https://elastic.co/?value=100'); + }, + }, + { + fieldType: ES_FIELD_TYPES.LONG, + fieldValue: 100, + applyFormatterType: FIELD_FORMAT_IDS.URL, + expectFormattedValue: 'url label', + beforeSave: async () => { + await testSubjects.setValue( + 'urlEditorUrlTemplate', + 'https://elastic.co/?value={{value}}' + ); + await testSubjects.setValue('urlEditorLabelTemplate', 'url label'); + }, + expect: async (renderedValueContainer) => { + expect( + await (await renderedValueContainer.findByTagName('a')).getAttribute('href') + ).to.be('https://elastic.co/?value=100'); + }, + }, + ]); + }); + + describe('Date format', () => { + testFormatEditors([ + { + fieldType: ES_FIELD_TYPES.DATE, + fieldValue: '2021-08-05T15:05:37.151Z', + applyFormatterType: FIELD_FORMAT_IDS.DATE, + expectFormattedValue: 'Aug 5, 2021', + beforeSave: async () => { + await testSubjects.setValue('dateEditorPattern', 'MMM D, YYYY'); + }, + // check available formats for ES_FIELD_TYPES.DATE + expectFormatterTypes: [ + FIELD_FORMAT_IDS.DATE, + FIELD_FORMAT_IDS.DATE_NANOS, + FIELD_FORMAT_IDS.RELATIVE_DATE, + FIELD_FORMAT_IDS.STRING, + FIELD_FORMAT_IDS.URL, + ], + }, + { + fieldType: ES_FIELD_TYPES.DATE_NANOS, + fieldValue: '2015-01-01T12:10:30.123456789Z', + applyFormatterType: FIELD_FORMAT_IDS.DATE, + expectFormattedValue: 'Jan 1, 2015 @ 12:10:30.123', + // check available formats for ES_FIELD_TYPES.DATE_NANOS + expectFormatterTypes: [ + FIELD_FORMAT_IDS.DATE, + FIELD_FORMAT_IDS.DATE_NANOS, + FIELD_FORMAT_IDS.RELATIVE_DATE, + FIELD_FORMAT_IDS.STRING, + FIELD_FORMAT_IDS.URL, + ], + }, + { + fieldType: ES_FIELD_TYPES.DATE_NANOS, + fieldValue: '2015-01-01T12:10:30.123456789Z', + applyFormatterType: FIELD_FORMAT_IDS.DATE_NANOS, + expectFormattedValue: 'Jan 1, 2015 @ 12:10:30.123456789', + }, + ]); + }); + + describe('Static lookup format', () => { + testFormatEditors([ + { + fieldType: ES_FIELD_TYPES.KEYWORD, + fieldValue: 'look me up', + applyFormatterType: FIELD_FORMAT_IDS.STATIC_LOOKUP, + expectFormattedValue: 'looked up!', + beforeSave: async () => { + await testSubjects.click('staticLookupEditorAddEntry'); + await testSubjects.setValue('~staticLookupEditorKey', 'look me up'); + await testSubjects.setValue('~staticLookupEditorValue', 'looked up!'); + }, + }, + { + fieldType: ES_FIELD_TYPES.BOOLEAN, + fieldValue: 'true', + applyFormatterType: FIELD_FORMAT_IDS.STATIC_LOOKUP, + // check available formats for ES_FIELD_TYPES.BOOLEAN + expectFormatterTypes: [ + FIELD_FORMAT_IDS.BOOLEAN, + FIELD_FORMAT_IDS.STATIC_LOOKUP, + FIELD_FORMAT_IDS.STRING, + FIELD_FORMAT_IDS.URL, + ], + expectFormattedValue: 'yes', + beforeSave: async () => { + await testSubjects.click('staticLookupEditorAddEntry'); + await testSubjects.setValue('~staticLookupEditorKey', 'true'); + await testSubjects.setValue('~staticLookupEditorValue', 'yes'); + await testSubjects.setValue('staticLookupEditorUnknownValue', 'no'); + }, + }, + { + fieldType: ES_FIELD_TYPES.BOOLEAN, + fieldValue: 'false', + applyFormatterType: FIELD_FORMAT_IDS.STATIC_LOOKUP, + expectFormattedValue: 'no', + beforeSave: async () => { + await testSubjects.click('staticLookupEditorAddEntry'); + await testSubjects.setValue('~staticLookupEditorKey', 'true'); + await testSubjects.setValue('~staticLookupEditorValue', 'yes'); + await testSubjects.setValue('staticLookupEditorUnknownValue', 'no'); + }, + }, + { + fieldType: ES_FIELD_TYPES.BOOLEAN, + fieldValue: 'false', + applyFormatterType: FIELD_FORMAT_IDS.STATIC_LOOKUP, + expectFormattedValue: 'false', + beforeSave: async () => { + await testSubjects.click('staticLookupEditorAddEntry'); + await testSubjects.setValue('~staticLookupEditorKey', 'true'); + await testSubjects.setValue('~staticLookupEditorValue', 'yes'); + }, + }, + ]); + }); + + describe('Other formats', () => { + testFormatEditors([ + { + fieldType: ES_FIELD_TYPES.LONG, + fieldValue: 123292, + applyFormatterType: FIELD_FORMAT_IDS.DURATION, + expectFormattedValue: '2 minutes', + beforeSave: async () => { + await testSubjects.setValue('durationEditorInputFormat', 'milliseconds'); + }, + }, + { + fieldType: ES_FIELD_TYPES.DOUBLE, + fieldValue: 0.1, + applyFormatterType: FIELD_FORMAT_IDS.PERCENT, + // check available formats for ES_FIELD_TYPES.DOUBLE + expectFormatterTypes: [ + FIELD_FORMAT_IDS.BOOLEAN, + FIELD_FORMAT_IDS.BYTES, + FIELD_FORMAT_IDS.COLOR, + FIELD_FORMAT_IDS.DURATION, + FIELD_FORMAT_IDS.NUMBER, + FIELD_FORMAT_IDS.PERCENT, + FIELD_FORMAT_IDS.STATIC_LOOKUP, + FIELD_FORMAT_IDS.STRING, + FIELD_FORMAT_IDS.URL, + ], + expectFormattedValue: '10.0%', + beforeSave: async () => { + await testSubjects.setValue('numberEditorFormatPattern', '0.0%'); + }, + }, + { + fieldType: ES_FIELD_TYPES.LONG, + fieldValue: 1990000000, + applyFormatterType: FIELD_FORMAT_IDS.BYTES, + expectFormattedValue: '2GB', + beforeSave: async () => { + await testSubjects.setValue('numberEditorFormatPattern', '0b'); + }, + }, + { + fieldType: ES_FIELD_TYPES.KEYWORD, + fieldValue: 'red', + applyFormatterType: FIELD_FORMAT_IDS.COLOR, + expectFormattedValue: 'red', + beforeSave: async () => { + await testSubjects.click('colorEditorAddColor'); + await testSubjects.setValue('~colorEditorKeyPattern', 'red'); + await testSubjects.setValue('~colorEditorColorPicker', '#ffffff'); + await testSubjects.setValue('~colorEditorBackgroundPicker', '#ff0000'); + }, + expect: async (renderedValueContainer) => { + const span = await renderedValueContainer.findByTagName('span'); + expect(await span.getComputedStyle('color')).to.be('rgba(255, 255, 255, 1)'); + expect(await span.getComputedStyle('background-color')).to.be('rgba(255, 0, 0, 1)'); + }, + }, + ]); + }); + }); + }); + + /** + * Runs a field format editors tests covering data setup, editing a field and checking a resulting formatting in Discover app + * TODO: might be useful to reuse this util for runtime fields formats tests + * @param specs - {@link FieldFormatEditorSpecDescriptor} + */ + function testFormatEditors(specs: FieldFormatEditorSpecDescriptor[]) { + const indexTitle = 'field_formats_management_functional_tests'; + let indexPatternId: string; + let testDocumentId: string; + + before(async () => { + if ((await es.indices.exists({ index: indexTitle })).body) { + await es.indices.delete({ index: indexTitle }); + } + + await es.indices.create({ + index: indexTitle, + body: { + mappings: { + properties: specs.reduce((properties, spec, index) => { + properties[`${index}`] = { type: spec.fieldType }; + return properties; + }, {} as Record), + }, + }, + }); + + const docResult = await es.index({ + index: indexTitle, + body: specs.reduce((properties, spec, index) => { + properties[`${index}`] = spec.fieldValue; + return properties; + }, {} as Record), + refresh: 'wait_for', + }); + testDocumentId = docResult.body._id; + + const indexPatternResult = await indexPatterns.create( + { title: indexTitle }, + { override: true } + ); + indexPatternId = indexPatternResult.id!; + }); + + describe('edit formats', () => { + before(async () => { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaIndexPatterns(); + await PageObjects.settings.clickIndexPatternByName(indexTitle); + }); + + afterEach(async () => { + try { + await PageObjects.settings.controlChangeSave(); + } catch (e) { + // in case previous test failed in a state when save is disabled + await PageObjects.settings.controlChangeCancel(); + } + + await toasts.dismissAllToasts(); // dismiss "saved" toast, otherwise it could overlap save button for a next test + }); + + specs.forEach((spec, index) => { + const fieldName = `${index}`; + it( + `edit field format of "${fieldName}" field to "${spec.applyFormatterType}"` + + spec.expectFormatterTypes + ? `, and check available formats types` + : '', + async () => { + await PageObjects.settings.filterField(fieldName); + await PageObjects.settings.openControlsByName(fieldName); + await PageObjects.settings.toggleRow('formatRow'); + + if (spec.expectFormatterTypes) { + expect( + ( + await Promise.all( + ( + await (await testSubjects.find('editorSelectedFormatId')).findAllByTagName( + 'option' + ) + ).map((option) => option.getAttribute('value')) + ) + ).filter(Boolean) + ).to.eql(spec.expectFormatterTypes); + } + + await PageObjects.settings.setFieldFormat(spec.applyFormatterType); + if (spec.beforeSave) { + await spec.beforeSave(await testSubjects.find('formatRow')); + } + } + ); + }); + }); + + describe('check formats', async () => { + before(async () => { + await PageObjects.common.navigateToApp('discover', { + hash: `/doc/${indexPatternId}/${indexTitle}?id=${testDocumentId}`, + }); + await testSubjects.exists('doc-hit'); + }); + + specs.forEach((spec, index) => { + it(`check field format of "${index}" field`, async () => { + const renderedValue = await testSubjects.find(`tableDocViewRow-${index}-value`); + const text = await renderedValue.getVisibleText(); + expect(text).to.be(spec.expectFormattedValue); + if (spec.expect) { + await spec.expect(renderedValue); + } + }); + }); + }); + } +} + +/** + * Describes a field format editor test + */ +interface FieldFormatEditorSpecDescriptor { + /** + * Raw field value to put into document + */ + fieldValue: string | number | boolean | null; + /** + * Explicitly specify a type for a {@link fieldValue} + */ + fieldType: ES_FIELD_TYPES; + /** + * Type of a field formatter to apply + */ + applyFormatterType: FIELD_FORMAT_IDS; + + /** + * Optionally check available formats for {@link fieldType} + */ + expectFormatterTypes?: FIELD_FORMAT_IDS[]; + + /** + * Function to execute before field format is applied. + * Use it set specific configuration params for applied field formatter + * @param formatRowContainer - field format editor container + */ + beforeSave?: (formatRowContainer: WebElementWrapper) => Promise; + + /** + * An expected formatted value rendered by Discover app, + * Use this for final assertion + */ + expectFormattedValue: string; + + /** + * Run additional assertions on rendered element + */ + expect?: (renderedValueContainer: WebElementWrapper) => Promise; +} From 1c9b9e84a1a4359b75c1f834133856001d56a475 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Tue, 10 Aug 2021 10:04:27 -0400 Subject: [PATCH 026/104] [Security Solution][Endpoint] Ensure fleet setup is done prior to attempting to install/upgrade the Endpoint package (#107929) * Ensure install/upgrade of endpoint package first checks to see that fleet is setup * Delete un-used `` component * Test cases for `useUpgradeSecurityPackages()` hook --- .../public/app/home/setup.tsx | 41 ------------ .../use_upgrade_secuirty_packages.test.tsx | 65 +++++++++++++++++++ .../hooks/use_upgrade_security_packages.ts | 16 +++-- .../common/lib/kibana/kibana_react.mock.ts | 3 + .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 6 files changed, 77 insertions(+), 52 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/app/home/setup.tsx create mode 100644 x-pack/plugins/security_solution/public/common/hooks/use_upgrade_secuirty_packages.test.tsx diff --git a/x-pack/plugins/security_solution/public/app/home/setup.tsx b/x-pack/plugins/security_solution/public/app/home/setup.tsx deleted file mode 100644 index b0d2035eb4460..0000000000000 --- a/x-pack/plugins/security_solution/public/app/home/setup.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as React from 'react'; -import { i18n } from '@kbn/i18n'; -import { NotificationsStart } from 'kibana/public'; -import { FleetStart } from '../../../../fleet/public'; - -export const Setup: React.FunctionComponent<{ - fleet: FleetStart; - notifications: NotificationsStart; -}> = ({ fleet, notifications }) => { - React.useEffect(() => { - const defaultText = i18n.translate('xpack.securitySolution.endpoint.ingestToastMessage', { - defaultMessage: 'Fleet failed during its setup.', - }); - - const title = i18n.translate('xpack.securitySolution.endpoint.ingestToastTitle', { - defaultMessage: 'App failed to initialize', - }); - - const displayToastWithModal = (text: string) => { - const errorText = new Error(defaultText); - // we're leveraging the notification's error toast which is usually used for displaying stack traces of an - // actually Error. Instead of displaying a stack trace we'll display the more detailed error text when the - // user clicks `See the full error` button to see the modal - errorText.stack = text; - notifications.toasts.addError(errorText, { - title, - }); - }; - - fleet.isInitialized().catch((error: Error) => displayToastWithModal(error.message)); - }, [fleet, notifications.toasts]); - - return null; -}; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_secuirty_packages.test.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_secuirty_packages.test.tsx new file mode 100644 index 0000000000000..f1d1b09f45f60 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_secuirty_packages.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { useKibana } from '../lib/kibana'; +import { renderHook as _renderHook, RenderHookResult } from '@testing-library/react-hooks'; +import { useUpgradeSecurityPackages } from './use_upgrade_security_packages'; + +jest.mock('../components/user_privileges', () => { + return { + useUserPrivileges: jest.fn().mockReturnValue({ + endpointPrivileges: { + canAccessFleet: true, + }, + }), + }; +}); +jest.mock('../lib/kibana'); + +describe('When using the `useUpgradeSecurityPackages()` hook', () => { + let renderResult: RenderHookResult; + let renderHook: () => RenderHookResult; + let kibana: ReturnType; + + // eslint-disable-next-line react/display-name + const Wrapper = memo(({ children }) => { + kibana = useKibana(); + return <>{children}; + }); + + beforeEach(() => { + renderHook = () => { + renderResult = _renderHook(() => useUpgradeSecurityPackages(), { wrapper: Wrapper }); + return renderResult; + }; + }); + + afterEach(() => { + if (renderResult) { + renderResult.unmount(); + } + }); + + it('should call fleet setup first via `isInitialized()` and then send upgrade request', async () => { + renderHook(); + + expect(kibana.services.fleet?.isInitialized).toHaveBeenCalled(); + expect(kibana.services.http.post).not.toHaveBeenCalled(); + + await renderResult.waitFor( + () => (kibana.services.http.post as jest.Mock).mock.calls.length > 0 + ); + + expect(kibana.services.http.post).toHaveBeenCalledWith( + '/api/fleet/epm/packages/_bulk', + expect.objectContaining({ + body: '{"packages":["endpoint","security_detection_engine"]}', + }) + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts b/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts index ef1e658d349bf..c09ae2715277a 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_upgrade_security_packages.ts @@ -7,9 +7,8 @@ import { useEffect } from 'react'; import { HttpFetchOptions, HttpStart } from 'kibana/public'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { useKibana } from '../lib/kibana'; import { epmRouteService, BulkInstallPackagesResponse } from '../../../../fleet/common'; -import { StartServices } from '../../types'; import { useUserPrivileges } from '../components/user_privileges'; /** @@ -31,7 +30,7 @@ const sendUpgradeSecurityPackages = async ( }; export const useUpgradeSecurityPackages = () => { - const context = useKibana(); + const context = useKibana(); const canAccessFleet = useUserPrivileges().endpointPrivileges.canAccessFleet; useEffect(() => { @@ -47,20 +46,23 @@ export const useUpgradeSecurityPackages = () => { (async () => { try { + // Make sure fleet is initialized first + await context.services.fleet?.isInitialized(); + // ignore the response for now since we aren't notifying the user await sendUpgradeSecurityPackages(context.services.http, { signal }); } catch (error) { // Ignore Errors, since this should not hinder the user's ability to use the UI - // ignore the error that occurs from aborting a request + // log to console, except if the error occurred due to aborting a request if (!abortController.signal.aborted) { // eslint-disable-next-line no-console console.error(error); } } - - return abortRequests; })(); + + return abortRequests; } - }, [canAccessFleet, context.services.http]); + }, [canAccessFleet, context.services.fleet, context.services.http]); }; diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts index 5fd83b72af09b..62d8628dc1592 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts @@ -40,6 +40,7 @@ import { createSecuritySolutionStorageMock } from '../../mock/mock_local_storage import { MlLocatorDefinition } from '../../../../../ml/public'; import { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common'; import { MockUrlService } from 'src/plugins/share/common/mocks'; +import { fleetMock } from '../../../../../fleet/public/mocks'; const mockUiSettings: Record = { [DEFAULT_TIME_RANGE]: { from: 'now-15m', to: 'now', mode: 'quick' }, @@ -96,6 +97,7 @@ export const createStartServicesMock = (): StartServices => { const security = securityMock.createSetup(); const urlService = new MockUrlService(); const locator = urlService.locators.create(new MlLocatorDefinition()); + const fleet = fleetMock.createStartMock(); return ({ ...core, @@ -141,6 +143,7 @@ export const createStartServicesMock = (): StartServices => { }, security, storage, + fleet, ml: { locator, }, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1d81b0ed7785d..1b20d91d480b2 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -21418,8 +21418,6 @@ "xpack.securitySolution.endpoint.hostIsolation.unIsolateThisHost": "現在{hostName}は{isolated}です。このホストを{unisolate}しますか?", "xpack.securitySolution.endpoint.hostIsolationStatus.isolated": "分離済み", "xpack.securitySolution.endpoint.ingestManager.createPackagePolicy.endpointConfiguration": "推奨のデフォルト値で統合が保存されます。後からこれを変更するには、エージェントポリシー内で Endpoint Security 統合を編集します。", - "xpack.securitySolution.endpoint.ingestToastMessage": "Fleetが設定中に失敗しました。", - "xpack.securitySolution.endpoint.ingestToastTitle": "アプリを初期化できませんでした", "xpack.securitySolution.endpoint.list.actionmenu": "開く", "xpack.securitySolution.endpoint.list.actions": "アクション", "xpack.securitySolution.endpoint.list.endpointsEnrolling": "エンドポイントを登録しています。進行状況を追跡するには、{agentsLink}してください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 244b3dee8f0d4..d3c89b74eca8b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -21902,8 +21902,6 @@ "xpack.securitySolution.endpoint.hostIsolation.unIsolateThisHost": "{hostName} 当前{isolated}。是否确定要{unisolate}此主机?", "xpack.securitySolution.endpoint.hostIsolationStatus.isolated": "已隔离", "xpack.securitySolution.endpoint.ingestManager.createPackagePolicy.endpointConfiguration": "我们将使用建议的默认值保存您的集成。稍后,您可以通过在代理策略中编辑 Endpoint Security 集成对其进行更改。", - "xpack.securitySolution.endpoint.ingestToastMessage": "Fleet 在设置期间失败。", - "xpack.securitySolution.endpoint.ingestToastTitle": "应用无法初始化", "xpack.securitySolution.endpoint.list.actionmenu": "未结", "xpack.securitySolution.endpoint.list.actions": "操作", "xpack.securitySolution.endpoint.list.endpointsEnrolling": "正在注册终端。{agentsLink}以跟踪进度。", From 1498a9179633f03419c6e75e5461d70b313ee03d Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Tue, 10 Aug 2021 09:09:33 -0500 Subject: [PATCH 027/104] [build] Clean images from png-js. Closes #107617 (#107975) --- src/dev/build/tasks/clean_tasks.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/dev/build/tasks/clean_tasks.ts b/src/dev/build/tasks/clean_tasks.ts index d4b4f98ed295b..f9fcbc74b0efc 100644 --- a/src/dev/build/tasks/clean_tasks.ts +++ b/src/dev/build/tasks/clean_tasks.ts @@ -151,6 +151,9 @@ export const CleanExtraFilesFromModules: Task = { '**/.DS_Store', '**/Dockerfile', '**/docker-compose.yml', + + // https://github.com/elastic/kibana/issues/107617 + '**/png-js/images/*.png', ]); log.info( From 5a92a7ef31028541517ec8dc67a82e0eb7f33e4c Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 10 Aug 2021 10:35:59 -0400 Subject: [PATCH 028/104] [Fleet] Support pipeline version for Fleet Final pipeline (#107892) --- .../fleet/server/constants/fleet_es_assets.ts | 3 +++ .../plugins/fleet/server/constants/index.ts | 1 + .../elasticsearch/ingest_pipeline/install.ts | 13 ++++++++-- .../apis/epm/final_pipeline.ts | 25 ++++++++++++++++++- 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts b/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts index eb70aff3238a0..5f9a4bfde6335 100644 --- a/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts +++ b/x-pack/plugins/fleet/server/constants/fleet_es_assets.ts @@ -36,7 +36,10 @@ export const FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT = { }, }; +export const FLEET_FINAL_PIPELINE_VERSION = 1; +// If the content is updated you probably need to update the FLEET_FINAL_PIPELINE_VERSION too to allow upgrade of the pipeline export const FLEET_FINAL_PIPELINE_CONTENT = `--- +version: ${FLEET_FINAL_PIPELINE_VERSION} description: > Final pipeline for processing all incoming Fleet Agent documents. processors: diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index 3aca5e8800dc5..28f3ea96f732e 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -63,4 +63,5 @@ export { FLEET_GLOBAL_COMPONENT_TEMPLATE_CONTENT, FLEET_FINAL_PIPELINE_ID, FLEET_FINAL_PIPELINE_CONTENT, + FLEET_FINAL_PIPELINE_VERSION, } from './fleet_es_assets'; diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts index a6aa87c5ed0f5..46750105900d5 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts @@ -14,7 +14,11 @@ import { getAsset, getPathParts } from '../../archive'; import type { ArchiveEntry } from '../../archive'; import { saveInstalledEsRefs } from '../../packages/install'; import { getInstallationObject } from '../../packages'; -import { FLEET_FINAL_PIPELINE_CONTENT, FLEET_FINAL_PIPELINE_ID } from '../../../../constants'; +import { + FLEET_FINAL_PIPELINE_CONTENT, + FLEET_FINAL_PIPELINE_ID, + FLEET_FINAL_PIPELINE_VERSION, +} from '../../../../constants'; import { deletePipelineRefs } from './remove'; @@ -195,7 +199,12 @@ export async function ensureFleetFinalPipelineIsInstalled(esClient: Elasticsearc esClientRequestOptions ); - if (res.statusCode === 404) { + const installedVersion = res?.body[FLEET_FINAL_PIPELINE_ID]?.version; + if ( + res.statusCode === 404 || + !installedVersion || + installedVersion < FLEET_FINAL_PIPELINE_VERSION + ) { await installPipeline({ esClient, pipeline: { diff --git a/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts b/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts index 073d08c5b1d8d..8c6603a3e38b0 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts @@ -14,6 +14,8 @@ const TEST_INDEX = 'logs-log.log-test'; const FINAL_PIPELINE_ID = '.fleet_final_pipeline-1'; +const FINAL_PIPELINE_VERSION = 1; + let pkgKey: string; export default function (providerContext: FtrProviderContext) { @@ -81,11 +83,32 @@ export default function (providerContext: FtrProviderContext) { } }); + it('should correctly update the final pipeline', async () => { + await es.ingest.putPipeline({ + id: FINAL_PIPELINE_ID, + body: { + description: 'Test PIPELINE WITHOUT version', + processors: [ + { + set: { + field: 'my-keyword-field', + value: 'foo', + }, + }, + ], + }, + }); + await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxxx'); + const pipelineRes = await es.ingest.getPipeline({ id: FINAL_PIPELINE_ID }); + expect(pipelineRes.body).to.have.property(FINAL_PIPELINE_ID); + expect(pipelineRes.body[FINAL_PIPELINE_ID].version).to.be(1); + }); + it('should correctly setup the final pipeline and apply to fleet managed index template', async () => { const pipelineRes = await es.ingest.getPipeline({ id: FINAL_PIPELINE_ID }); expect(pipelineRes.body).to.have.property(FINAL_PIPELINE_ID); const res = await es.indices.getIndexTemplate({ name: 'logs-log.log' }); - expect(res.body.index_templates.length).to.be(1); + expect(res.body.index_templates.length).to.be(FINAL_PIPELINE_VERSION); expect(res.body.index_templates[0]?.index_template?.composed_of).to.contain( '.fleet_component_template-1' ); From 9edcf9e71ee026cc1023ad37f90f2a9bbe73ff0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Tue, 10 Aug 2021 17:36:27 +0300 Subject: [PATCH 029/104] [Osquery] RBAC (#106669) --- .../migrations/core/elastic_index.ts | 2 + .../type_registrations.test.ts | 1 + .../server/collectors/agent_collectors.ts | 2 +- .../collectors/fleet_server_collector.ts | 1 - x-pack/plugins/fleet/server/mocks/index.ts | 1 + x-pack/plugins/fleet/server/plugin.ts | 2 + .../fleet/server/routes/agent/handlers.ts | 2 - .../fleet/server/services/agents/status.ts | 3 +- x-pack/plugins/fleet/server/services/index.ts | 10 + x-pack/plugins/osquery/common/types.ts | 7 +- .../action_results/action_results_summary.tsx | 4 + .../action_results/use_action_privileges.tsx | 33 ++++ .../agent_policies/use_agent_policies.ts | 13 +- .../public/agent_policies/use_agent_policy.ts | 3 +- .../osquery/public/agents/agents_table.tsx | 14 +- .../public/agents/use_agent_details.ts | 4 +- .../public/agents/use_agent_policies.ts | 4 +- .../osquery/public/agents/use_agent_status.ts | 4 +- .../osquery/public/agents/use_all_agents.ts | 11 +- .../public/agents/use_osquery_policies.ts | 9 +- .../common/hooks/use_osquery_integration.tsx | 28 +-- .../plugins/osquery/public/editor/index.tsx | 4 +- .../fleet_integration/disabled_callout.tsx | 28 +++ ...squery_managed_custom_button_extension.tsx | 30 ++- ...managed_policy_create_import_extension.tsx | 19 +- .../public/live_queries/form/index.tsx | 9 +- .../form/live_query_query_field.tsx | 24 ++- x-pack/plugins/osquery/public/plugin.ts | 61 +----- .../osquery/public/results/results_table.tsx | 17 ++ .../osquery/public/routes/components/index.ts | 8 + .../routes/components/missing_privileges.tsx | 47 +++++ .../public/routes/live_queries/index.tsx | 13 +- .../public/routes/live_queries/list/index.tsx | 12 +- .../public/routes/saved_queries/edit/form.tsx | 70 +++---- .../routes/saved_queries/edit/index.tsx | 42 +++-- .../public/routes/saved_queries/index.tsx | 9 +- .../routes/saved_queries/list/index.tsx | 96 +++++++--- .../scheduled_query_groups/details/index.tsx | 12 +- .../routes/scheduled_query_groups/index.tsx | 11 +- .../scheduled_query_groups/list/index.tsx | 12 +- .../public/saved_queries/form/index.tsx | 128 +++++++------ .../active_state_switch.tsx | 6 +- .../use_scheduled_query_group.ts | 4 +- .../use_scheduled_query_groups.ts | 9 +- .../lib/osquery_app_context_services.ts | 3 +- x-pack/plugins/osquery/server/plugin.ts | 174 +++++++++++++++++- .../routes/action/create_action_route.ts | 19 +- .../routes/fleet_wrapper/get_agent_details.ts | 33 ++++ .../fleet_wrapper/get_agent_policies.ts | 34 ++++ .../routes/fleet_wrapper/get_agent_policy.ts | 34 ++++ .../get_agent_status_for_agent_policy.ts | 46 +++++ .../server/routes/fleet_wrapper/get_agents.ts | 33 ++++ .../get_package_policies.ts} | 8 +- .../server/routes/fleet_wrapper/index.ts | 24 +++ x-pack/plugins/osquery/server/routes/index.ts | 6 + .../server/routes/privileges_check/index.ts | 14 ++ .../privileges_check_route.ts | 43 +++++ .../saved_query/create_saved_query_route.ts | 3 +- .../saved_query/delete_saved_query_route.ts | 3 +- .../saved_query/find_saved_query_route.ts | 3 +- .../saved_query/read_saved_query_route.ts | 3 +- .../saved_query/update_saved_query_route.ts | 3 +- .../create_scheduled_query_route.ts | 4 +- .../delete_scheduled_query_route.ts | 5 +- .../find_scheduled_query_group_route.ts | 38 ++++ .../index.ts | 10 +- .../read_scheduled_query_group_route.ts} | 20 +- .../update_scheduled_query_route.ts | 3 +- .../routes/status/create_status_route.ts | 10 +- .../routes/usage/saved_object_mappings.ts | 2 +- .../server/search_strategy/osquery/index.ts | 41 +++-- .../plugins/osquery/server/usage/collector.ts | 14 +- .../apis/features/features/features.ts | 1 + .../apis/security/privileges.ts | 13 ++ .../apis/security/privileges_basic.ts | 1 + .../security_and_spaces/tests/catalogue.ts | 2 + .../security_and_spaces/tests/nav_links.ts | 5 +- .../security_only/tests/catalogue.ts | 2 + .../security_only/tests/nav_links.ts | 5 +- 79 files changed, 1135 insertions(+), 356 deletions(-) create mode 100644 x-pack/plugins/osquery/public/action_results/use_action_privileges.tsx create mode 100644 x-pack/plugins/osquery/public/fleet_integration/disabled_callout.tsx create mode 100644 x-pack/plugins/osquery/public/routes/components/index.ts create mode 100644 x-pack/plugins/osquery/public/routes/components/missing_privileges.tsx create mode 100644 x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_details.ts create mode 100644 x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts create mode 100644 x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policy.ts create mode 100644 x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_status_for_agent_policy.ts create mode 100644 x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agents.ts rename x-pack/plugins/osquery/server/routes/{scheduled_query/find_scheduled_query_route.ts => fleet_wrapper/get_package_policies.ts} (81%) create mode 100644 x-pack/plugins/osquery/server/routes/fleet_wrapper/index.ts create mode 100644 x-pack/plugins/osquery/server/routes/privileges_check/index.ts create mode 100644 x-pack/plugins/osquery/server/routes/privileges_check/privileges_check_route.ts rename x-pack/plugins/osquery/server/routes/{scheduled_query => scheduled_query_group}/create_scheduled_query_route.ts (87%) rename x-pack/plugins/osquery/server/routes/{scheduled_query => scheduled_query_group}/delete_scheduled_query_route.ts (87%) create mode 100644 x-pack/plugins/osquery/server/routes/scheduled_query_group/find_scheduled_query_group_route.ts rename x-pack/plugins/osquery/server/routes/{scheduled_query => scheduled_query_group}/index.ts (67%) rename x-pack/plugins/osquery/server/routes/{scheduled_query/read_scheduled_query_route.ts => scheduled_query_group/read_scheduled_query_group_route.ts} (65%) rename x-pack/plugins/osquery/server/routes/{scheduled_query => scheduled_query_group}/update_scheduled_query_route.ts (92%) diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.ts b/src/core/server/saved_objects/migrations/core/elastic_index.ts index 8bda77563be8c..f473b3ed02526 100644 --- a/src/core/server/saved_objects/migrations/core/elastic_index.ts +++ b/src/core/server/saved_objects/migrations/core/elastic_index.ts @@ -43,6 +43,8 @@ export const REMOVED_TYPES: string[] = [ 'server', // https://github.com/elastic/kibana/issues/95617 'tsvb-validation-telemetry', + // replaced by osquery-manager-usage-metric + 'osquery-usage-metric', ].sort(); // When migrating from the outdated index we use a read query which excludes diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/type_registrations.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/type_registrations.test.ts index 0bad209ad9cef..ce4c8078e0c95 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/type_registrations.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/type_registrations.test.ts @@ -72,6 +72,7 @@ const previouslyRegisteredTypes = [ 'monitoring-telemetry', 'osquery-saved-query', 'osquery-usage-metric', + 'osquery-manager-usage-metric', 'query', 'sample-data-telemetry', 'search', diff --git a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts index 6a9a4cd9ba83c..716c81573e85a 100644 --- a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts +++ b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts @@ -44,7 +44,7 @@ export const getAgentUsage = async ( error, offline, updating, - } = await AgentService.getAgentStatusForAgentPolicy(soClient, esClient); + } = await AgentService.getAgentStatusForAgentPolicy(esClient); return { total_enrolled: total, healthy: online, diff --git a/x-pack/plugins/fleet/server/collectors/fleet_server_collector.ts b/x-pack/plugins/fleet/server/collectors/fleet_server_collector.ts index 9616ba11545e0..47440e791747c 100644 --- a/x-pack/plugins/fleet/server/collectors/fleet_server_collector.ts +++ b/x-pack/plugins/fleet/server/collectors/fleet_server_collector.ts @@ -76,7 +76,6 @@ export const getFleetServerUsage = async ( } const { total, inactive, online, error, updating, offline } = await getAgentStatusForAgentPolicy( - soClient, esClient, undefined, Array.from(policyIds) diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index c4ba7e363bc5a..9f07dfac9670b 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -101,6 +101,7 @@ export const createMockAgentPolicyService = (): jest.Mocked => { return { getAgentStatusById: jest.fn(), + getAgentStatusForAgentPolicy: jest.fn(), authenticateAgentWithAccessToken: jest.fn(), getAgent: jest.fn(), listAgents: jest.fn(), diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 0ab102d91cd4d..94e8032f3375b 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -72,6 +72,7 @@ import { } from './services'; import { getAgentStatusById, + getAgentStatusForAgentPolicy, authenticateAgentWithAccessToken, getAgentsByKuery, getAgentById, @@ -309,6 +310,7 @@ export class FleetPlugin getAgent: getAgentById, listAgents: getAgentsByKuery, getAgentStatusById, + getAgentStatusForAgentPolicy, authenticateAgentWithAccessToken, }, agentPolicyService: { diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index 72a7f4e35ddf5..fd4721309eebb 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -202,13 +202,11 @@ export const getAgentStatusForAgentPolicyHandler: RequestHandler< undefined, TypeOf > = async (context, request, response) => { - const soClient = context.core.savedObjects.client; const esClient = context.core.elasticsearch.client.asCurrentUser; try { // TODO change path const results = await AgentService.getAgentStatusForAgentPolicy( - soClient, esClient, request.query.policyId, request.query.kuery diff --git a/x-pack/plugins/fleet/server/services/agents/status.ts b/x-pack/plugins/fleet/server/services/agents/status.ts index 26cca630f9581..cd8f9b95599b8 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; +import type { ElasticsearchClient } from 'src/core/server'; import pMap from 'p-map'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; @@ -49,7 +49,6 @@ function joinKuerys(...kuerys: Array) { } export async function getAgentStatusForAgentPolicy( - soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, agentPolicyId?: string, filterKuery?: string diff --git a/x-pack/plugins/fleet/server/services/index.ts b/x-pack/plugins/fleet/server/services/index.ts index f4355320c5a6a..ecef04af6b11e 100644 --- a/x-pack/plugins/fleet/server/services/index.ts +++ b/x-pack/plugins/fleet/server/services/index.ts @@ -10,6 +10,8 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/ser import type { AgentStatus, Agent } from '../types'; +import type { GetAgentStatusResponse } from '../../common'; + import type { getAgentById, getAgentsByKuery } from './agents'; import type { agentPolicyService } from './agent_policy'; import * as settingsService from './settings'; @@ -56,6 +58,14 @@ export interface AgentService { * Return the status by the Agent's id */ getAgentStatusById(esClient: ElasticsearchClient, agentId: string): Promise; + /** + * Return the status by the Agent's Policy id + */ + getAgentStatusForAgentPolicy( + esClient: ElasticsearchClient, + agentPolicyId?: string, + filterKuery?: string + ): Promise; /** * List agents */ diff --git a/x-pack/plugins/osquery/common/types.ts b/x-pack/plugins/osquery/common/types.ts index d195198e54a73..7244066f798ba 100644 --- a/x-pack/plugins/osquery/common/types.ts +++ b/x-pack/plugins/osquery/common/types.ts @@ -9,8 +9,11 @@ import { PackagePolicy, PackagePolicyInput, PackagePolicyInputStream } from '../ export const savedQuerySavedObjectType = 'osquery-saved-query'; export const packSavedObjectType = 'osquery-pack'; -export const usageMetricSavedObjectType = 'osquery-usage-metric'; -export type SavedObjectType = 'osquery-saved-query' | 'osquery-pack' | 'osquery-usage-metric'; +export const usageMetricSavedObjectType = 'osquery-manager-usage-metric'; +export type SavedObjectType = + | 'osquery-saved-query' + | 'osquery-pack' + | 'osquery-manager-usage-metric'; /** * This makes any optional property the same as Required would but also has the diff --git a/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx b/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx index 75277059bbf97..083d0193be2a2 100644 --- a/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx +++ b/x-pack/plugins/osquery/public/action_results/action_results_summary.tsx @@ -15,6 +15,7 @@ import { AgentIdToName } from '../agents/agent_id_to_name'; import { useActionResults } from './use_action_results'; import { useAllResults } from '../results/use_all_results'; import { Direction } from '../../common/search_strategy'; +import { useActionResultsPrivileges } from './use_action_privileges'; interface ActionResultsSummaryProps { actionId: string; @@ -41,6 +42,7 @@ const ActionResultsSummaryComponent: React.FC = ({ expirationDate, ]); const [isLive, setIsLive] = useState(true); + const { data: hasActionResultsPrivileges } = useActionResultsPrivileges(); const { // @ts-expect-error update types data: { aggregations, edges }, @@ -52,6 +54,7 @@ const ActionResultsSummaryComponent: React.FC = ({ direction: Direction.asc, sortField: '@timestamp', isLive, + skip: !hasActionResultsPrivileges, }); if (expired) { // @ts-expect-error update types @@ -77,6 +80,7 @@ const ActionResultsSummaryComponent: React.FC = ({ }, ], isLive, + skip: !hasActionResultsPrivileges, }); const renderAgentIdColumn = useCallback((agentId) => , []); diff --git a/x-pack/plugins/osquery/public/action_results/use_action_privileges.tsx b/x-pack/plugins/osquery/public/action_results/use_action_privileges.tsx new file mode 100644 index 0000000000000..2c80c874e89fa --- /dev/null +++ b/x-pack/plugins/osquery/public/action_results/use_action_privileges.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery } from 'react-query'; + +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../common/lib/kibana'; +import { useErrorToast } from '../common/hooks/use_error_toast'; + +export const useActionResultsPrivileges = () => { + const { http } = useKibana().services; + const setErrorToast = useErrorToast(); + + return useQuery( + ['actionResultsPrivileges'], + () => http.get('/internal/osquery/privileges_check'), + { + keepPreviousData: true, + select: (response) => response?.has_all_requested ?? false, + onSuccess: () => setErrorToast(), + onError: (error: Error) => + setErrorToast(error, { + title: i18n.translate('xpack.osquery.action_results_privileges.fetchError', { + defaultMessage: 'Error while fetching action results privileges', + }), + }), + } + ); +}; diff --git a/x-pack/plugins/osquery/public/agent_policies/use_agent_policies.ts b/x-pack/plugins/osquery/public/agent_policies/use_agent_policies.ts index c51f2d2f44a5c..74061915d3b86 100644 --- a/x-pack/plugins/osquery/public/agent_policies/use_agent_policies.ts +++ b/x-pack/plugins/osquery/public/agent_policies/use_agent_policies.ts @@ -9,11 +9,7 @@ import { useQuery } from 'react-query'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../common/lib/kibana'; -import { - agentPolicyRouteService, - GetAgentPoliciesResponse, - GetAgentPoliciesResponseItem, -} from '../../../fleet/common'; +import { GetAgentPoliciesResponse, GetAgentPoliciesResponseItem } from '../../../fleet/common'; import { useErrorToast } from '../common/hooks/use_error_toast'; export const useAgentPolicies = () => { @@ -22,12 +18,7 @@ export const useAgentPolicies = () => { return useQuery( ['agentPolicies'], - () => - http.get(agentPolicyRouteService.getListPath(), { - query: { - perPage: 100, - }, - }), + () => http.get('/internal/osquery/fleet_wrapper/agent_policies/'), { initialData: { items: [], total: 0, page: 1, perPage: 100 }, keepPreviousData: true, diff --git a/x-pack/plugins/osquery/public/agent_policies/use_agent_policy.ts b/x-pack/plugins/osquery/public/agent_policies/use_agent_policy.ts index dcebf136b6773..302567ef25640 100644 --- a/x-pack/plugins/osquery/public/agent_policies/use_agent_policy.ts +++ b/x-pack/plugins/osquery/public/agent_policies/use_agent_policy.ts @@ -9,7 +9,6 @@ import { useQuery } from 'react-query'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../common/lib/kibana'; -import { agentPolicyRouteService } from '../../../fleet/common'; import { useErrorToast } from '../common/hooks/use_error_toast'; interface UseAgentPolicy { @@ -23,7 +22,7 @@ export const useAgentPolicy = ({ policyId, skip }: UseAgentPolicy) => { return useQuery( ['agentPolicy', { policyId }], - () => http.get(agentPolicyRouteService.getInfoPath(policyId)), + () => http.get(`/internal/osquery/fleet_wrapper/agent_policies/${policyId}`), { enabled: !skip, keepPreviousData: true, diff --git a/x-pack/plugins/osquery/public/agents/agents_table.tsx b/x-pack/plugins/osquery/public/agents/agents_table.tsx index 53e2ce1d53420..8a40cb171070d 100644 --- a/x-pack/plugins/osquery/public/agents/agents_table.tsx +++ b/x-pack/plugins/osquery/public/agents/agents_table.tsx @@ -65,9 +65,13 @@ const AgentsTableComponent: React.FC = ({ agentSelection, onCh osqueryPolicyData ); const grouper = useMemo(() => new AgentGrouper(), []); - const { agentsLoading, agents } = useAllAgents(osqueryPolicyData, debouncedSearchValue, { - perPage, - }); + const { isLoading: agentsLoading, data: agents } = useAllAgents( + osqueryPolicyData, + debouncedSearchValue, + { + perPage, + } + ); // option related const [options, setOptions] = useState([]); @@ -108,8 +112,8 @@ const AgentsTableComponent: React.FC = ({ agentSelection, onCh grouper.setTotalAgents(totalNumAgents); grouper.updateGroup(AGENT_GROUP_KEY.Platform, groups.platforms); grouper.updateGroup(AGENT_GROUP_KEY.Policy, groups.policies); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - grouper.updateGroup(AGENT_GROUP_KEY.Agent, agents!); + // @ts-expect-error update types + grouper.updateGroup(AGENT_GROUP_KEY.Agent, agents); const newOptions = grouper.generateOptions(); setOptions(newOptions); }, [groups.platforms, groups.policies, totalNumAgents, groupsLoading, agents, grouper]); diff --git a/x-pack/plugins/osquery/public/agents/use_agent_details.ts b/x-pack/plugins/osquery/public/agents/use_agent_details.ts index 1a0663812dec3..b0c2fb2e1cbaf 100644 --- a/x-pack/plugins/osquery/public/agents/use_agent_details.ts +++ b/x-pack/plugins/osquery/public/agents/use_agent_details.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { useQuery } from 'react-query'; -import { GetOneAgentResponse, agentRouteService } from '../../../fleet/common'; +import { GetOneAgentResponse } from '../../../fleet/common'; import { useErrorToast } from '../common/hooks/use_error_toast'; import { useKibana } from '../common/lib/kibana'; @@ -21,7 +21,7 @@ export const useAgentDetails = ({ agentId }: UseAgentDetails) => { const setErrorToast = useErrorToast(); return useQuery( ['agentDetails', agentId], - () => http.get(agentRouteService.getInfoPath(agentId)), + () => http.get(`/internal/osquery/fleet_wrapper/agents/${agentId}`), { enabled: agentId.length > 0, onSuccess: () => setErrorToast(), diff --git a/x-pack/plugins/osquery/public/agents/use_agent_policies.ts b/x-pack/plugins/osquery/public/agents/use_agent_policies.ts index 115b5af9d3a1b..e8d6fe7eb97ac 100644 --- a/x-pack/plugins/osquery/public/agents/use_agent_policies.ts +++ b/x-pack/plugins/osquery/public/agents/use_agent_policies.ts @@ -9,7 +9,7 @@ import { mapKeys } from 'lodash'; import { useQueries, UseQueryResult } from 'react-query'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../common/lib/kibana'; -import { agentPolicyRouteService, GetOneAgentPolicyResponse } from '../../../fleet/common'; +import { GetOneAgentPolicyResponse } from '../../../fleet/common'; import { useErrorToast } from '../common/hooks/use_error_toast'; export const useAgentPolicies = (policyIds: string[] = []) => { @@ -19,7 +19,7 @@ export const useAgentPolicies = (policyIds: string[] = []) => { const agentResponse = useQueries( policyIds.map((policyId) => ({ queryKey: ['agentPolicy', policyId], - queryFn: () => http.get(agentPolicyRouteService.getInfoPath(policyId)), + queryFn: () => http.get(`/internal/osquery/fleet_wrapper/agent_policies/${policyId}`), enabled: policyIds.length > 0, onSuccess: () => setErrorToast(), onError: (error) => diff --git a/x-pack/plugins/osquery/public/agents/use_agent_status.ts b/x-pack/plugins/osquery/public/agents/use_agent_status.ts index c8bc8d2fe5c0e..ba2237dbe57ea 100644 --- a/x-pack/plugins/osquery/public/agents/use_agent_status.ts +++ b/x-pack/plugins/osquery/public/agents/use_agent_status.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { useQuery } from 'react-query'; -import { GetAgentStatusResponse, agentRouteService } from '../../../fleet/common'; +import { GetAgentStatusResponse } from '../../../fleet/common'; import { useErrorToast } from '../common/hooks/use_error_toast'; import { useKibana } from '../common/lib/kibana'; @@ -25,7 +25,7 @@ export const useAgentStatus = ({ policyId, skip }: UseAgentStatus) => { ['agentStatus', policyId], () => http.get( - agentRouteService.getStatusPath(), + `/internal/osquery/fleet_wrapper/agent-status`, policyId ? { query: { diff --git a/x-pack/plugins/osquery/public/agents/use_all_agents.ts b/x-pack/plugins/osquery/public/agents/use_all_agents.ts index fac43eaa7ffc3..42e4954989c66 100644 --- a/x-pack/plugins/osquery/public/agents/use_all_agents.ts +++ b/x-pack/plugins/osquery/public/agents/use_all_agents.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { useQuery } from 'react-query'; -import { GetAgentsResponse, agentRouteService } from '../../../fleet/common'; +import { GetAgentsResponse } from '../../../fleet/common'; import { useErrorToast } from '../common/hooks/use_error_toast'; import { useKibana } from '../common/lib/kibana'; @@ -31,7 +31,8 @@ export const useAllAgents = ( const { perPage } = opts; const { http } = useKibana().services; const setErrorToast = useErrorToast(); - const { isLoading: agentsLoading, data: agentData } = useQuery( + + return useQuery( ['agents', osqueryPolicies, searchValue, perPage], () => { let kuery = `${osqueryPolicies.map((p) => `policy_id:${p}`).join(' or ')}`; @@ -40,7 +41,7 @@ export const useAllAgents = ( kuery += ` and (local_metadata.host.hostname:*${searchValue}* or local_metadata.elastic.agent.id:*${searchValue}*)`; } - return http.get(agentRouteService.getListPath(), { + return http.get(`/internal/osquery/fleet_wrapper/agents`, { query: { kuery, perPage, @@ -48,6 +49,8 @@ export const useAllAgents = ( }); }, { + // @ts-expect-error update types + select: (data) => data?.agents || [], enabled: !osqueryPoliciesLoading && osqueryPolicies.length > 0, onSuccess: () => setErrorToast(), onError: (error) => @@ -58,6 +61,4 @@ export const useAllAgents = ( }), } ); - - return { agentsLoading, agents: agentData?.list }; }; diff --git a/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts b/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts index 9064dac1ae5d0..4b9ff931f3a91 100644 --- a/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts +++ b/x-pack/plugins/osquery/public/agents/use_osquery_policies.ts @@ -10,8 +10,6 @@ import { useQuery } from 'react-query'; import { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../common/lib/kibana'; -import { packagePolicyRouteService, PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../fleet/common'; -import { OSQUERY_INTEGRATION_NAME } from '../../common'; import { useErrorToast } from '../common/hooks/use_error_toast'; export const useOsqueryPolicies = () => { @@ -20,12 +18,7 @@ export const useOsqueryPolicies = () => { const { isLoading: osqueryPoliciesLoading, data: osqueryPolicies = [] } = useQuery( ['osqueryPolicies'], - () => - http.get(packagePolicyRouteService.getListPath(), { - query: { - kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${OSQUERY_INTEGRATION_NAME}`, - }, - }), + () => http.get('/internal/osquery/fleet_wrapper/package_policies'), { select: (response) => uniq(response.items.map((p: { policy_id: string }) => p.policy_id)), diff --git a/x-pack/plugins/osquery/public/common/hooks/use_osquery_integration.tsx b/x-pack/plugins/osquery/public/common/hooks/use_osquery_integration.tsx index 236fdb1af1815..58f9f8dbec61d 100644 --- a/x-pack/plugins/osquery/public/common/hooks/use_osquery_integration.tsx +++ b/x-pack/plugins/osquery/public/common/hooks/use_osquery_integration.tsx @@ -6,11 +6,8 @@ */ import { i18n } from '@kbn/i18n'; -import { find } from 'lodash/fp'; import { useQuery } from 'react-query'; -import { GetPackagesResponse, epmRouteService } from '../../../../fleet/common'; -import { OSQUERY_INTEGRATION_NAME } from '../../../common'; import { useKibana } from '../lib/kibana'; import { useErrorToast } from './use_error_toast'; @@ -18,23 +15,12 @@ export const useOsqueryIntegration = () => { const { http } = useKibana().services; const setErrorToast = useErrorToast(); - return useQuery( - 'integrations', - () => - http.get(epmRouteService.getListPath(), { - query: { - experimental: true, - }, - }), - { - select: ({ response }: GetPackagesResponse) => - find(['name', OSQUERY_INTEGRATION_NAME], response), - onError: (error: Error) => - setErrorToast(error, { - title: i18n.translate('xpack.osquery.osquery_integration.fetchError', { - defaultMessage: 'Error while fetching osquery integration', - }), + return useQuery('integration', () => http.get('/internal/osquery/status'), { + onError: (error: Error) => + setErrorToast(error, { + title: i18n.translate('xpack.osquery.osquery_integration.fetchError', { + defaultMessage: 'Error while fetching osquery integration', }), - } - ); + }), + }); }; diff --git a/x-pack/plugins/osquery/public/editor/index.tsx b/x-pack/plugins/osquery/public/editor/index.tsx index 5be2b1816ad86..8c844d9eda3bc 100644 --- a/x-pack/plugins/osquery/public/editor/index.tsx +++ b/x-pack/plugins/osquery/public/editor/index.tsx @@ -28,13 +28,13 @@ interface OsqueryEditorProps { const OsqueryEditorComponent: React.FC = ({ defaultValue, - // disabled, + disabled, onChange, }) => ( ( + <> + + + + + + + +); + +export const DisabledCallout = React.memo(DisabledCalloutComponent); diff --git a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_custom_button_extension.tsx b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_custom_button_extension.tsx index 775b5c7a06d21..67791cb34e683 100644 --- a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_custom_button_extension.tsx +++ b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_custom_button_extension.tsx @@ -5,16 +5,42 @@ * 2.0. */ -import React from 'react'; +import { EuiLoadingContent } from '@elastic/eui'; +import React, { useEffect } from 'react'; import { PackageCustomExtensionComponentProps } from '../../../fleet/public'; import { NavigationButtons } from './navigation_buttons'; +import { DisabledCallout } from './disabled_callout'; +import { useKibana } from '../common/lib/kibana'; /** * Exports Osquery-specific package policy instructions * for use in the Fleet app custom tab */ export const OsqueryManagedCustomButtonExtension = React.memo( - () => + () => { + const [disabled, setDisabled] = React.useState(null); + const { http } = useKibana().services; + + useEffect(() => { + const fetchStatus = () => { + http.get('/internal/osquery/status').then((response) => { + setDisabled(response.install_status !== 'installed'); + }); + }; + fetchStatus(); + }, [http]); + + if (disabled === null) { + return ; + } + + return ( + <> + {disabled ? : null} + + + ); + } ); OsqueryManagedCustomButtonExtension.displayName = 'OsqueryManagedCustomButtonExtension'; diff --git a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx index 63036f5f693f7..9fd3c9b032ef8 100644 --- a/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx +++ b/x-pack/plugins/osquery/public/fleet_integration/osquery_managed_policy_create_import_extension.tsx @@ -11,7 +11,6 @@ import React, { useEffect, useMemo, useState } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { produce } from 'immer'; -import { i18n } from '@kbn/i18n'; import { agentRouteService, agentPolicyRouteService, @@ -29,6 +28,7 @@ import { import { ScheduledQueryGroupQueriesTable } from '../scheduled_query_groups/scheduled_query_group_queries_table'; import { useKibana } from '../common/lib/kibana'; import { NavigationButtons } from './navigation_buttons'; +import { DisabledCallout } from './disabled_callout'; import { OsqueryManagerPackagePolicy } from '../../common/types'; /** @@ -163,22 +163,7 @@ export const OsqueryManagedPolicyCreateImportExtension = React.memo< return ( <> - {!editMode ? ( - <> - - - - - - - - ) : null} + {!editMode ? : null} {policyAgentsCount === 0 ? ( <> diff --git a/x-pack/plugins/osquery/public/live_queries/form/index.tsx b/x-pack/plugins/osquery/public/live_queries/form/index.tsx index 8654a74fecfb4..bf614ff4e9bcd 100644 --- a/x-pack/plugins/osquery/public/live_queries/form/index.tsx +++ b/x-pack/plugins/osquery/public/live_queries/form/index.tsx @@ -47,6 +47,7 @@ const LiveQueryFormComponent: React.FC = ({ defaultValue, onSuccess, }) => { + const permissions = useKibana().services.application.capabilities.osquery; const { http } = useKibana().services; const [showSavedQueryFlyout, setShowSavedQueryFlyout] = useState(false); const setErrorToast = useErrorToast(); @@ -175,7 +176,12 @@ const LiveQueryFormComponent: React.FC = ({ {!agentId && ( = ({ [ agentId, agentSelected, + permissions.writeSavedQueries, handleShowSaveQueryFlout, queryComponentProps, queryValueProvided, diff --git a/x-pack/plugins/osquery/public/live_queries/form/live_query_query_field.tsx b/x-pack/plugins/osquery/public/live_queries/form/live_query_query_field.tsx index 070339bb58af2..c79fae9eb5d21 100644 --- a/x-pack/plugins/osquery/public/live_queries/form/live_query_query_field.tsx +++ b/x-pack/plugins/osquery/public/live_queries/form/live_query_query_field.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import { EuiFormRow, EuiSpacer } from '@elastic/eui'; +import { EuiCodeBlock, EuiFormRow, EuiSpacer } from '@elastic/eui'; import React, { useCallback, useRef } from 'react'; +import styled from 'styled-components'; import { OsquerySchemaLink } from '../../components/osquery_schema_link'; import { FieldHook } from '../../shared_imports'; @@ -15,6 +16,11 @@ import { SavedQueriesDropdown, SavedQueriesDropdownRef, } from '../../saved_queries/saved_queries_dropdown'; +import { useKibana } from '../../common/lib/kibana'; + +const StyledEuiCodeBlock = styled(EuiCodeBlock)` + min-height: 150px; +`; interface LiveQueryQueryFieldProps { disabled?: boolean; @@ -22,6 +28,7 @@ interface LiveQueryQueryFieldProps { } const LiveQueryQueryFieldComponent: React.FC = ({ disabled, field }) => { + const permissions = useKibana().services.application.capabilities.osquery; const { value, setValue, errors } = field; const error = errors[0]?.message; const savedQueriesDropdownRef = useRef(null); @@ -46,12 +53,23 @@ const LiveQueryQueryFieldComponent: React.FC = ({ disa <> }> - + {!permissions.writeLiveQueries ? ( + + {value} + + ) : ( + + )} diff --git a/x-pack/plugins/osquery/public/plugin.ts b/x-pack/plugins/osquery/public/plugin.ts index 12f9025e406db..8555997d61787 100644 --- a/x-pack/plugins/osquery/public/plugin.ts +++ b/x-pack/plugins/osquery/public/plugin.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { BehaviorSubject, Subject } from 'rxjs'; import { AppMountParameters, CoreSetup, @@ -13,9 +12,6 @@ import { PluginInitializerContext, CoreStart, DEFAULT_APP_CATEGORIES, - AppStatus, - AppNavLinkStatus, - AppUpdater, } from '../../../../src/core/public'; import { Storage } from '../../../../src/plugins/kibana_utils/public'; import { @@ -25,7 +21,6 @@ import { AppPluginStartDependencies, } from './types'; import { OSQUERY_INTEGRATION_NAME, PLUGIN_NAME } from '../common'; -import { Installation } from '../../fleet/common'; import { LazyOsqueryManagedPolicyCreateImportExtension, LazyOsqueryManagedPolicyEditExtension, @@ -33,48 +28,7 @@ import { } from './fleet_integration'; import { getLazyOsqueryAction } from './shared_components'; -export function toggleOsqueryPlugin( - updater$: Subject, - http: CoreStart['http'], - registerExtension?: StartPlugins['fleet']['registerExtension'] -) { - if (http.anonymousPaths.isAnonymous(window.location.pathname)) { - updater$.next(() => ({ - status: AppStatus.inaccessible, - navLinkStatus: AppNavLinkStatus.hidden, - })); - return; - } - - http - .fetch(`/internal/osquery/status`) - .then((response) => { - const installed = response?.install_status === 'installed'; - - if (installed && registerExtension) { - registerExtension({ - package: OSQUERY_INTEGRATION_NAME, - view: 'package-detail-custom', - Component: LazyOsqueryManagedCustomButtonExtension, - }); - } - - updater$.next(() => ({ - navLinkStatus: installed ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden, - })); - }) - .catch(() => { - updater$.next(() => ({ - status: AppStatus.inaccessible, - navLinkStatus: AppNavLinkStatus.hidden, - })); - }); -} - export class OsqueryPlugin implements Plugin { - private readonly appUpdater$ = new BehaviorSubject(() => ({ - navLinkStatus: AppNavLinkStatus.hidden, - })); private kibanaVersion: string; private storage = new Storage(localStorage); @@ -102,8 +56,6 @@ export class OsqueryPlugin implements Plugin ({ - status: AppStatus.inaccessible, - navLinkStatus: AppNavLinkStatus.hidden, - })); + + registerExtension({ + package: OSQUERY_INTEGRATION_NAME, + view: 'package-detail-custom', + Component: LazyOsqueryManagedCustomButtonExtension, + }); } return { diff --git a/x-pack/plugins/osquery/public/results/results_table.tsx b/x-pack/plugins/osquery/public/results/results_table.tsx index d82737ab51e7c..d293847215d68 100644 --- a/x-pack/plugins/osquery/public/results/results_table.tsx +++ b/x-pack/plugins/osquery/public/results/results_table.tsx @@ -8,6 +8,7 @@ import { isEmpty, isEqual, keys, map } from 'lodash/fp'; import { EuiCallOut, + EuiCode, EuiDataGrid, EuiDataGridSorting, EuiDataGridProps, @@ -31,6 +32,8 @@ import { ViewResultsInLensAction, ViewResultsActionButtonType, } from '../scheduled_query_groups/scheduled_query_group_queries_table'; +import { useActionResultsPrivileges } from '../action_results/use_action_privileges'; +import { OSQUERY_INTEGRATION_NAME } from '../../common'; const DataContext = createContext([]); @@ -49,6 +52,7 @@ const ResultsTableComponent: React.FC = ({ endDate, }) => { const [isLive, setIsLive] = useState(true); + const { data: hasActionResultsPrivileges } = useActionResultsPrivileges(); const { // @ts-expect-error update types data: { aggregations }, @@ -60,6 +64,7 @@ const ResultsTableComponent: React.FC = ({ direction: Direction.asc, sortField: '@timestamp', isLive, + skip: !hasActionResultsPrivileges, }); const expired = useMemo(() => (!endDate ? false : new Date(endDate) < new Date()), [endDate]); const { getUrlForApp } = useKibana().services.application; @@ -104,6 +109,7 @@ const ResultsTableComponent: React.FC = ({ field: sortedColumn.id, direction: sortedColumn.direction as Direction, })), + skip: !hasActionResultsPrivileges, }); const [visibleColumns, setVisibleColumns] = useState([]); @@ -237,6 +243,17 @@ const ResultsTableComponent: React.FC = ({ ] ); + if (!hasActionResultsPrivileges) { + return ( + +

+ You're missing read privileges to read from + logs-{OSQUERY_INTEGRATION_NAME}.result*. +

+
+ ); + } + if (!isFetched) { return ; } diff --git a/x-pack/plugins/osquery/public/routes/components/index.ts b/x-pack/plugins/osquery/public/routes/components/index.ts new file mode 100644 index 0000000000000..877c25fe7cdad --- /dev/null +++ b/x-pack/plugins/osquery/public/routes/components/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './missing_privileges'; diff --git a/x-pack/plugins/osquery/public/routes/components/missing_privileges.tsx b/x-pack/plugins/osquery/public/routes/components/missing_privileges.tsx new file mode 100644 index 0000000000000..6adabff599124 --- /dev/null +++ b/x-pack/plugins/osquery/public/routes/components/missing_privileges.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiEmptyPrompt, EuiPanel, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import styled from 'styled-components'; + +const Panel = styled(EuiPanel)` + max-width: 500px; + margin-right: auto; + margin-left: auto; +`; + +const MissingPrivilegesComponent = () => ( +
+ + + + + + } + body={ +

+ +

+ } + /> +
+ +
+); + +export const MissingPrivileges = React.memo(MissingPrivilegesComponent); diff --git a/x-pack/plugins/osquery/public/routes/live_queries/index.tsx b/x-pack/plugins/osquery/public/routes/live_queries/index.tsx index af039e85e9785..47815516dd726 100644 --- a/x-pack/plugins/osquery/public/routes/live_queries/index.tsx +++ b/x-pack/plugins/osquery/public/routes/live_queries/index.tsx @@ -12,15 +12,26 @@ import { LiveQueriesPage } from './list'; import { NewLiveQueryPage } from './new'; import { LiveQueryDetailsPage } from './details'; import { useBreadcrumbs } from '../../common/hooks/use_breadcrumbs'; +import { useKibana } from '../../common/lib/kibana'; +import { MissingPrivileges } from '../components'; const LiveQueriesComponent = () => { + const permissions = useKibana().services.application.capabilities.osquery; useBreadcrumbs('live_queries'); const match = useRouteMatch(); + if (!permissions.readLiveQueries) { + return ; + } + return ( - + {permissions.runSavedQueries || permissions.writeLiveQueries ? ( + + ) : ( + + )} diff --git a/x-pack/plugins/osquery/public/routes/live_queries/list/index.tsx b/x-pack/plugins/osquery/public/routes/live_queries/list/index.tsx index 90ac7b5cc17ae..23bc44b455405 100644 --- a/x-pack/plugins/osquery/public/routes/live_queries/list/index.tsx +++ b/x-pack/plugins/osquery/public/routes/live_queries/list/index.tsx @@ -9,13 +9,14 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useMemo } from 'react'; -import { useRouterNavigate } from '../../../common/lib/kibana'; +import { useKibana, useRouterNavigate } from '../../../common/lib/kibana'; import { ActionsTable } from '../../../actions/actions_table'; import { WithHeaderLayout } from '../../../components/layouts'; import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs'; import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge'; const LiveQueriesPageComponent = () => { + const permissions = useKibana().services.application.capabilities.osquery; useBreadcrumbs('live_queries'); const newQueryLinkProps = useRouterNavigate('live_queries/new'); @@ -40,14 +41,19 @@ const LiveQueriesPageComponent = () => { const RightColumn = useMemo( () => ( - + ), - [newQueryLinkProps] + [permissions.writeLiveQueries, permissions.runSavedQueries, newQueryLinkProps] ); return ( diff --git a/x-pack/plugins/osquery/public/routes/saved_queries/edit/form.tsx b/x-pack/plugins/osquery/public/routes/saved_queries/edit/form.tsx index 8d77b7819bd3e..a7596575b90c4 100644 --- a/x-pack/plugins/osquery/public/routes/saved_queries/edit/form.tsx +++ b/x-pack/plugins/osquery/public/routes/saved_queries/edit/form.tsx @@ -24,11 +24,13 @@ import { useSavedQueryForm } from '../../../saved_queries/form/use_saved_query_f interface EditSavedQueryFormProps { defaultValue?: unknown; handleSubmit: () => Promise; + viewMode?: boolean; } const EditSavedQueryFormComponent: React.FC = ({ defaultValue, handleSubmit, + viewMode, }) => { const savedQueryListProps = useRouterNavigate('saved_queries'); @@ -39,41 +41,45 @@ const EditSavedQueryFormComponent: React.FC = ({ return (
- - - - - + + {!viewMode && ( + <> + + - - - - - - - - + + + + + + + + + + + + - - - - - - + + + + + + )} ); }; diff --git a/x-pack/plugins/osquery/public/routes/saved_queries/edit/index.tsx b/x-pack/plugins/osquery/public/routes/saved_queries/edit/index.tsx index 5bdba133fad72..401966460a7e7 100644 --- a/x-pack/plugins/osquery/public/routes/saved_queries/edit/index.tsx +++ b/x-pack/plugins/osquery/public/routes/saved_queries/edit/index.tsx @@ -17,7 +17,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { useParams } from 'react-router-dom'; -import { useRouterNavigate } from '../../../common/lib/kibana'; +import { useKibana, useRouterNavigate } from '../../../common/lib/kibana'; import { WithHeaderLayout } from '../../../components/layouts'; import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs'; import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge'; @@ -25,6 +25,8 @@ import { EditSavedQueryForm } from './form'; import { useDeleteSavedQuery, useUpdateSavedQuery, useSavedQuery } from '../../../saved_queries'; const EditSavedQueryPageComponent = () => { + const permissions = useKibana().services.application.capabilities.osquery; + const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const { savedQueryId } = useParams<{ savedQueryId: string }>(); const savedQueryListProps = useRouterNavigate('saved_queries'); @@ -35,6 +37,8 @@ const EditSavedQueryPageComponent = () => { useBreadcrumbs('saved_query_edit', { savedQueryName: savedQueryDetails?.attributes?.id ?? '' }); + const viewMode = useMemo(() => !permissions.writeSavedQueries, [permissions.writeSavedQueries]); + const handleCloseDeleteConfirmationModal = useCallback(() => { setIsDeleteModalVisible(false); }, []); @@ -63,21 +67,32 @@ const EditSavedQueryPageComponent = () => {

- + {viewMode ? ( + + ) : ( + + )}

), - [savedQueryDetails?.attributes?.id, savedQueryListProps] + [savedQueryDetails?.attributes?.id, savedQueryListProps, viewMode] ); const RightColumn = useMemo( @@ -95,12 +110,17 @@ const EditSavedQueryPageComponent = () => { if (isLoading) return null; return ( - + {!isLoading && !isEmpty(savedQueryDetails) && ( )} {isDeleteModalVisible ? ( diff --git a/x-pack/plugins/osquery/public/routes/saved_queries/index.tsx b/x-pack/plugins/osquery/public/routes/saved_queries/index.tsx index f986129bdfefc..a2241ae017df4 100644 --- a/x-pack/plugins/osquery/public/routes/saved_queries/index.tsx +++ b/x-pack/plugins/osquery/public/routes/saved_queries/index.tsx @@ -12,15 +12,22 @@ import { QueriesPage } from './list'; import { NewSavedQueryPage } from './new'; import { EditSavedQueryPage } from './edit'; import { useBreadcrumbs } from '../../common/hooks/use_breadcrumbs'; +import { MissingPrivileges } from '../components'; +import { useKibana } from '../../common/lib/kibana'; const SavedQueriesComponent = () => { + const permissions = useKibana().services.application.capabilities.osquery; useBreadcrumbs('saved_queries'); const match = useRouteMatch(); + if (!permissions.readSavedQueries) { + return ; + } + return ( - + {permissions.writeSavedQueries ? : } diff --git a/x-pack/plugins/osquery/public/routes/saved_queries/list/index.tsx b/x-pack/plugins/osquery/public/routes/saved_queries/list/index.tsx index 0c04e816dae7a..e82dcf85780e1 100644 --- a/x-pack/plugins/osquery/public/routes/saved_queries/list/index.tsx +++ b/x-pack/plugins/osquery/public/routes/saved_queries/list/index.tsx @@ -21,16 +21,63 @@ import { useHistory } from 'react-router-dom'; import { SavedObject } from 'kibana/public'; import { WithHeaderLayout } from '../../../components/layouts'; import { useBreadcrumbs } from '../../../common/hooks/use_breadcrumbs'; -import { useRouterNavigate } from '../../../common/lib/kibana'; +import { useKibana, useRouterNavigate } from '../../../common/lib/kibana'; import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge'; import { useSavedQueries } from '../../../saved_queries/use_saved_queries'; +interface PlayButtonProps { + disabled: boolean; + savedQueryId: string; + savedQueryName: string; +} + +const PlayButtonComponent: React.FC = ({ + disabled = false, + savedQueryId, + savedQueryName, +}) => { + const { push } = useHistory(); + + // TODO: Fix href + const handlePlayClick = useCallback( + () => + push('/live_queries/new', { + form: { + savedQueryId, + }, + }), + [push, savedQueryId] + ); + + return ( + + ); +}; + +const PlayButton = React.memo(PlayButtonComponent); + interface EditButtonProps { + disabled?: boolean; savedQueryId: string; savedQueryName: string; } -const EditButtonComponent: React.FC = ({ savedQueryId, savedQueryName }) => { +const EditButtonComponent: React.FC = ({ + disabled = false, + savedQueryId, + savedQueryName, +}) => { const buttonProps = useRouterNavigate(`saved_queries/${savedQueryId}`); return ( @@ -38,6 +85,7 @@ const EditButtonComponent: React.FC = ({ savedQueryId, savedQue color="primary" {...buttonProps} iconType="pencil" + isDisabled={disabled} aria-label={i18n.translate('xpack.osquery.savedQueryList.queriesTable.editActionAriaLabel', { defaultMessage: 'Edit {savedQueryName}', values: { @@ -51,8 +99,9 @@ const EditButtonComponent: React.FC = ({ savedQueryId, savedQue const EditButton = React.memo(EditButtonComponent); const SavedQueriesPageComponent = () => { + const permissions = useKibana().services.application.capabilities.osquery; + useBreadcrumbs('saved_queries'); - const { push } = useHistory(); const newQueryLinkProps = useRouterNavigate('saved_queries/new'); const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(20); @@ -61,16 +110,6 @@ const SavedQueriesPageComponent = () => { const { data } = useSavedQueries({ isLive: true }); - const handlePlayClick = useCallback( - (item) => - push('/live_queries/new', { - form: { - savedQueryId: item.id, - }, - }), - [push] - ); - const renderEditAction = useCallback( (item: SavedObject<{ name: string }>) => ( @@ -78,6 +117,17 @@ const SavedQueriesPageComponent = () => { [] ); + const renderPlayAction = useCallback( + (item: SavedObject<{ name: string }>) => ( + + ), + [permissions.runSavedQueries, permissions.writeLiveQueries] + ); + const renderUpdatedAt = useCallback((updatedAt, item) => { if (!updatedAt) return '-'; @@ -128,17 +178,10 @@ const SavedQueriesPageComponent = () => { name: i18n.translate('xpack.osquery.savedQueries.table.actionsColumnTitle', { defaultMessage: 'Actions', }), - actions: [ - { - type: 'icon', - icon: 'play', - onClick: handlePlayClick, - }, - { render: renderEditAction }, - ], + actions: [{ render: renderPlayAction }, { render: renderEditAction }], }, ], - [handlePlayClick, renderEditAction, renderUpdatedAt] + [renderEditAction, renderPlayAction, renderUpdatedAt] ); const onTableChange = useCallback(({ page = {}, sort = {} }) => { @@ -189,14 +232,19 @@ const SavedQueriesPageComponent = () => { const RightColumn = useMemo( () => ( - + ), - [newQueryLinkProps] + [permissions.writeSavedQueries, newQueryLinkProps] ); return ( diff --git a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx index 960de043eac6e..dc6df49615093 100644 --- a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx +++ b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/details/index.tsx @@ -21,7 +21,7 @@ import React, { useMemo } from 'react'; import { useParams } from 'react-router-dom'; import styled from 'styled-components'; -import { useRouterNavigate } from '../../../common/lib/kibana'; +import { useKibana, useRouterNavigate } from '../../../common/lib/kibana'; import { WithHeaderLayout } from '../../../components/layouts'; import { useScheduledQueryGroup } from '../../../scheduled_query_groups/use_scheduled_query_group'; import { ScheduledQueryGroupQueriesTable } from '../../../scheduled_query_groups/scheduled_query_group_queries_table'; @@ -36,6 +36,7 @@ const Divider = styled.div` `; const ScheduledQueryGroupDetailsPageComponent = () => { + const permissions = useKibana().services.application.capabilities.osquery; const { scheduledQueryGroupId } = useParams<{ scheduledQueryGroupId: string }>(); const scheduledQueryGroupsListProps = useRouterNavigate('scheduled_query_groups'); const editQueryLinkProps = useRouterNavigate( @@ -111,7 +112,12 @@ const ScheduledQueryGroupDetailsPageComponent = () => { - + { ), - [data?.policy_id, editQueryLinkProps] + [data?.policy_id, editQueryLinkProps, permissions] ); return ( diff --git a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/index.tsx b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/index.tsx index 76ca2bf14d303..53bf4ae79a908 100644 --- a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/index.tsx +++ b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/index.tsx @@ -13,18 +13,25 @@ import { AddScheduledQueryGroupPage } from './add'; import { EditScheduledQueryGroupPage } from './edit'; import { ScheduledQueryGroupDetailsPage } from './details'; import { useBreadcrumbs } from '../../common/hooks/use_breadcrumbs'; +import { useKibana } from '../../common/lib/kibana'; +import { MissingPrivileges } from '../components'; const ScheduledQueryGroupsComponent = () => { + const permissions = useKibana().services.application.capabilities.osquery; useBreadcrumbs('scheduled_query_groups'); const match = useRouteMatch(); + if (!permissions.readPacks) { + return ; + } + return ( - + {permissions.writePacks ? : } - + {permissions.writePacks ? : } diff --git a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/list/index.tsx b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/list/index.tsx index b02ef95498b5c..006dd0e6ec1b6 100644 --- a/x-pack/plugins/osquery/public/routes/scheduled_query_groups/list/index.tsx +++ b/x-pack/plugins/osquery/public/routes/scheduled_query_groups/list/index.tsx @@ -9,12 +9,13 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useMemo } from 'react'; -import { useRouterNavigate } from '../../../common/lib/kibana'; +import { useKibana, useRouterNavigate } from '../../../common/lib/kibana'; import { WithHeaderLayout } from '../../../components/layouts'; import { ScheduledQueryGroupsTable } from '../../../scheduled_query_groups/scheduled_query_groups_table'; import { BetaBadge, BetaBadgeRowWrapper } from '../../../components/beta_badge'; const ScheduledQueryGroupsPageComponent = () => { + const permissions = useKibana().services.application.capabilities.osquery; const newQueryLinkProps = useRouterNavigate('scheduled_query_groups/add'); const LeftColumn = useMemo( @@ -38,14 +39,19 @@ const ScheduledQueryGroupsPageComponent = () => { const RightColumn = useMemo( () => ( - + ), - [newQueryLinkProps] + [newQueryLinkProps, permissions.writePacks] ); return ( diff --git a/x-pack/plugins/osquery/public/saved_queries/form/index.tsx b/x-pack/plugins/osquery/public/saved_queries/form/index.tsx index 9bbf847c4d2a0..beff34a8919a0 100644 --- a/x-pack/plugins/osquery/public/saved_queries/form/index.tsx +++ b/x-pack/plugins/osquery/public/saved_queries/form/index.tsx @@ -6,7 +6,7 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle, EuiText } from '@elastic/eui'; -import React from 'react'; +import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -17,64 +17,78 @@ import { CodeEditorField } from './code_editor_field'; export const CommonUseField = getUseField({ component: Field }); -const SavedQueryFormComponent = () => ( - <> - - - - - - - - - -
+interface SavedQueryFormProps { + viewMode?: boolean; +} + +const SavedQueryFormComponent: React.FC = ({ viewMode }) => { + const euiFieldProps = useMemo( + () => ({ + isDisabled: !!viewMode, + }), + [viewMode] + ); + + return ( + <> + + + + + + + + + +
+ +
+
+ -
-
- - +
+
+ + + + + + - - - - - - - - - - - - - - - - -); + + + + + + + + ); +}; export const SavedQueryForm = React.memo(SavedQueryFormComponent); diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/active_state_switch.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/active_state_switch.tsx index bcb47d0adc833..7f26534626b12 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/active_state_switch.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/active_state_switch.tsx @@ -28,12 +28,16 @@ const StyledEuiLoadingSpinner = styled(EuiLoadingSpinner)` `; interface ActiveStateSwitchProps { + disabled?: boolean; item: PackagePolicy; } const ActiveStateSwitchComponent: React.FC = ({ item }) => { const queryClient = useQueryClient(); const { + application: { + capabilities: { osquery: permissions }, + }, http, notifications: { toasts }, } = useKibana().services; @@ -126,7 +130,7 @@ const ActiveStateSwitchComponent: React.FC = ({ item }) {isLoading && } ( ['scheduledQueryGroup', { scheduledQueryGroupId }], - () => http.get(packagePolicyRouteService.getInfoPath(scheduledQueryGroupId)), + () => http.get(`/internal/osquery/scheduled_query_group/${scheduledQueryGroupId}`), { keepPreviousData: true, enabled: !skip || !scheduledQueryGroupId, diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_groups.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_groups.ts index 3302d8e621eb7..01b67a3d5164a 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_groups.ts +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_groups.ts @@ -9,12 +9,7 @@ import { produce } from 'immer'; import { useQuery } from 'react-query'; import { useKibana } from '../common/lib/kibana'; -import { - ListResult, - PackagePolicy, - packagePolicyRouteService, - PACKAGE_POLICY_SAVED_OBJECT_TYPE, -} from '../../../fleet/common'; +import { ListResult, PackagePolicy, PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../fleet/common'; import { OSQUERY_INTEGRATION_NAME } from '../../common'; export const useScheduledQueryGroups = () => { @@ -23,7 +18,7 @@ export const useScheduledQueryGroups = () => { return useQuery>( ['scheduledQueries'], () => - http.get(packagePolicyRouteService.getListPath(), { + http.get('/internal/osquery/scheduled_query_group', { query: { page: 1, perPage: 10000, diff --git a/x-pack/plugins/osquery/server/lib/osquery_app_context_services.ts b/x-pack/plugins/osquery/server/lib/osquery_app_context_services.ts index 6ebf469b8fb29..ca4fd1ebeffd2 100644 --- a/x-pack/plugins/osquery/server/lib/osquery_app_context_services.ts +++ b/x-pack/plugins/osquery/server/lib/osquery_app_context_services.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Logger, LoggerFactory } from 'src/core/server'; +import { CoreSetup, Logger, LoggerFactory } from '../../../../../src/core/server'; import { SecurityPluginStart } from '../../../security/server'; import { AgentService, @@ -71,6 +71,7 @@ export interface OsqueryAppContext { logFactory: LoggerFactory; config(): ConfigType; security: SecurityPluginStart; + getStartServices: CoreSetup['getStartServices']; /** * Object readiness is tied to plugin start method */ diff --git a/x-pack/plugins/osquery/server/plugin.ts b/x-pack/plugins/osquery/server/plugin.ts index 6bc12f5736e5e..ff8483fdb385a 100644 --- a/x-pack/plugins/osquery/server/plugin.ts +++ b/x-pack/plugins/osquery/server/plugin.ts @@ -5,14 +5,20 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; +import { + PACKAGE_POLICY_SAVED_OBJECT_TYPE, + AGENT_POLICY_SAVED_OBJECT_TYPE, + PACKAGES_SAVED_OBJECT_TYPE, +} from '../../fleet/common'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger, + DEFAULT_APP_CATEGORIES, } from '../../../../src/core/server'; - import { createConfig } from './create_config'; import { OsqueryPluginSetup, OsqueryPluginStart, SetupPlugins, StartPlugins } from './types'; import { defineRoutes } from './routes'; @@ -21,6 +27,169 @@ import { initSavedObjects } from './saved_objects'; import { initUsageCollectors } from './usage'; import { OsqueryAppContext, OsqueryAppContextService } from './lib/osquery_app_context_services'; import { ConfigType } from './config'; +import { packSavedObjectType, savedQuerySavedObjectType } from '../common/types'; +import { PLUGIN_ID } from '../common'; + +const registerFeatures = (features: SetupPlugins['features']) => { + features.registerKibanaFeature({ + id: PLUGIN_ID, + name: i18n.translate('xpack.osquery.features.osqueryFeatureName', { + defaultMessage: 'Osquery', + }), + category: DEFAULT_APP_CATEGORIES.management, + app: [PLUGIN_ID, 'kibana'], + catalogue: [PLUGIN_ID], + order: 2300, + excludeFromBasePrivileges: true, + privileges: { + all: { + api: [`${PLUGIN_ID}-read`, `${PLUGIN_ID}-write`], + app: [PLUGIN_ID, 'kibana'], + catalogue: [PLUGIN_ID], + savedObject: { + all: [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + read: [PACKAGES_SAVED_OBJECT_TYPE, AGENT_POLICY_SAVED_OBJECT_TYPE], + }, + ui: ['write'], + }, + read: { + api: [`${PLUGIN_ID}-read`], + app: [PLUGIN_ID, 'kibana'], + catalogue: [PLUGIN_ID], + savedObject: { + all: [], + read: [ + PACKAGE_POLICY_SAVED_OBJECT_TYPE, + PACKAGES_SAVED_OBJECT_TYPE, + AGENT_POLICY_SAVED_OBJECT_TYPE, + ], + }, + ui: ['read'], + }, + }, + subFeatures: [ + { + name: i18n.translate('xpack.osquery.features.liveQueriesSubFeatureName', { + defaultMessage: 'Live queries', + }), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + api: [`${PLUGIN_ID}-writeLiveQueries`, `${PLUGIN_ID}-readLiveQueries`], + id: 'live_queries_all', + includeIn: 'all', + name: 'All', + savedObject: { + all: [], + read: [], + }, + ui: ['writeLiveQueries', 'readLiveQueries'], + }, + { + api: [`${PLUGIN_ID}-readLiveQueries`], + id: 'live_queries_read', + includeIn: 'read', + name: 'Read', + savedObject: { + all: [], + read: [], + }, + ui: ['readLiveQueries'], + }, + ], + }, + { + groupType: 'independent', + privileges: [ + { + api: [`${PLUGIN_ID}-runSavedQueries`], + id: 'run_saved_queries', + name: i18n.translate('xpack.osquery.features.runSavedQueriesPrivilegeName', { + defaultMessage: 'Run Saved queries', + }), + includeIn: 'all', + savedObject: { + all: [], + read: [], + }, + ui: ['runSavedQueries'], + }, + ], + }, + ], + }, + { + name: i18n.translate('xpack.osquery.features.savedQueriesSubFeatureName', { + defaultMessage: 'Saved queries', + }), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + id: 'saved_queries_all', + includeIn: 'all', + name: 'All', + savedObject: { + all: [savedQuerySavedObjectType], + read: [], + }, + ui: ['writeSavedQueries', 'readSavedQueries'], + }, + { + id: 'saved_queries_read', + includeIn: 'read', + name: 'Read', + savedObject: { + all: [], + read: [savedQuerySavedObjectType], + }, + ui: ['readSavedQueries'], + }, + ], + }, + ], + }, + { + // TODO: Rename it to "Packs" as part of https://github.com/elastic/kibana/pull/107345 + name: i18n.translate('xpack.osquery.features.scheduledQueryGroupsSubFeatureName', { + defaultMessage: 'Scheduled query groups', + }), + privilegeGroups: [ + { + groupType: 'mutually_exclusive', + privileges: [ + { + api: [`${PLUGIN_ID}-writePacks`], + id: 'packs_all', + includeIn: 'all', + name: 'All', + savedObject: { + all: [packSavedObjectType], + read: [], + }, + ui: ['writePacks', 'readPacks'], + }, + { + api: [`${PLUGIN_ID}-readPacks`], + id: 'packs_read', + includeIn: 'read', + name: 'Read', + savedObject: { + all: [], + read: [packSavedObjectType], + }, + ui: ['readPacks'], + }, + ], + }, + ], + }, + ], + }); +}; export class OsqueryPlugin implements Plugin { private readonly logger: Logger; @@ -40,10 +209,13 @@ export class OsqueryPlugin implements Plugin config, security: plugins.security, diff --git a/x-pack/plugins/osquery/server/routes/action/create_action_route.ts b/x-pack/plugins/osquery/server/routes/action/create_action_route.ts index 478bfc1053bdf..79c1149675b0d 100644 --- a/x-pack/plugins/osquery/server/routes/action/create_action_route.ts +++ b/x-pack/plugins/osquery/server/routes/action/create_action_route.ts @@ -8,6 +8,7 @@ import uuid from 'uuid'; import moment from 'moment'; +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; @@ -19,6 +20,7 @@ import { } from '../../../common/schemas/routes/action/create_action_request_body_schema'; import { incrementCount } from '../usage'; +import { getInternalSavedObjectsClient } from '../../usage/collector'; export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { router.post( @@ -30,10 +32,17 @@ export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppCon CreateActionRequestBodySchema >(createActionRequestBodySchema), }, + options: { + tags: [`access:${PLUGIN_ID}-readLiveQueries`, `access:${PLUGIN_ID}-runSavedQueries`], + }, }, async (context, request, response) => { - const esClient = context.core.elasticsearch.client.asCurrentUser; + const esClient = context.core.elasticsearch.client.asInternalUser; const soClient = context.core.savedObjects.client; + const internalSavedObjectsClient = await getInternalSavedObjectsClient( + osqueryContext.getStartServices + ); + const { agentSelection } = request.body as { agentSelection: AgentSelection }; const selectedAgents = await parseAgentSelection( esClient, @@ -41,12 +50,14 @@ export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppCon osqueryContext, agentSelection ); - incrementCount(soClient, 'live_query'); + incrementCount(internalSavedObjectsClient, 'live_query'); if (!selectedAgents.length) { - incrementCount(soClient, 'live_query', 'errors'); + incrementCount(internalSavedObjectsClient, 'live_query', 'errors'); return response.badRequest({ body: new Error('No agents found for selection') }); } + // TODO: Add check for `runSavedQueries` only + try { const currentUser = await osqueryContext.security.authc.getCurrentUser(request)?.username; const action = { @@ -74,7 +85,7 @@ export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppCon }, }); } catch (error) { - incrementCount(soClient, 'live_query', 'errors'); + incrementCount(internalSavedObjectsClient, 'live_query', 'errors'); return response.customError({ statusCode: 500, body: new Error(`Error occurred while processing ${error}`), diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_details.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_details.ts new file mode 100644 index 0000000000000..67b4a27ab9ec7 --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_details.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { PLUGIN_ID } from '../../../common'; +import { IRouter } from '../../../../../../src/core/server'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; + +export const getAgentDetailsRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { + router.get( + { + path: '/internal/osquery/fleet_wrapper/agents/{id}', + validate: { + params: schema.object({}, { unknowns: 'allow' }), + }, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + async (context, request, response) => { + const esClient = context.core.elasticsearch.client.asInternalUser; + + const agent = await osqueryContext.service + .getAgentService() + // @ts-expect-error update types + ?.getAgent(esClient, request.params.id); + + return response.ok({ body: { item: agent } }); + } + ); +}; diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts new file mode 100644 index 0000000000000..e35e776cb1958 --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { PLUGIN_ID } from '../../../common'; +import { IRouter } from '../../../../../../src/core/server'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; + +export const getAgentPoliciesRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { + router.get( + { + path: '/internal/osquery/fleet_wrapper/agent_policies', + validate: { + params: schema.object({}, { unknowns: 'allow' }), + query: schema.object({}, { unknowns: 'allow' }), + }, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + async (context, request, response) => { + const soClient = context.core.savedObjects.client; + + const agentPolicies = await osqueryContext.service.getAgentPolicyService()?.list(soClient, { + ...(request.query || {}), + perPage: 100, + }); + + return response.ok({ body: agentPolicies }); + } + ); +}; diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policy.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policy.ts new file mode 100644 index 0000000000000..f845b04e99c93 --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policy.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { PLUGIN_ID } from '../../../common'; +import { IRouter } from '../../../../../../src/core/server'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; + +export const getAgentPolicyRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { + router.get( + { + path: '/internal/osquery/fleet_wrapper/agent_policies/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + async (context, request, response) => { + const soClient = context.core.savedObjects.client; + + const packageInfo = await osqueryContext.service + .getAgentPolicyService() + ?.get(soClient, request.params.id); + + return response.ok({ body: { item: packageInfo } }); + } + ); +}; diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_status_for_agent_policy.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_status_for_agent_policy.ts new file mode 100644 index 0000000000000..dea4402472958 --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_status_for_agent_policy.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { PLUGIN_ID } from '../../../common'; +import { GetAgentStatusResponse } from '../../../../fleet/common'; +import { IRouter } from '../../../../../../src/core/server'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; + +export const getAgentStatusForAgentPolicyRoute = ( + router: IRouter, + osqueryContext: OsqueryAppContext +) => { + router.get( + { + path: '/internal/osquery/fleet_wrapper/agent-status', + validate: { + query: schema.object({ + policyId: schema.string(), + kuery: schema.maybe(schema.string()), + }), + params: schema.object({}, { unknowns: 'allow' }), + }, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + async (context, request, response) => { + const esClient = context.core.elasticsearch.client.asInternalUser; + + const results = await osqueryContext.service + .getAgentService() + ?.getAgentStatusForAgentPolicy(esClient, request.query.policyId, request.query.kuery); + + if (!results) { + return response.ok({ body: {} }); + } + + const body: GetAgentStatusResponse = { results }; + + return response.ok({ body }); + } + ); +}; diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agents.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agents.ts new file mode 100644 index 0000000000000..d45cb26e0d199 --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agents.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { PLUGIN_ID } from '../../../common'; +import { IRouter } from '../../../../../../src/core/server'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; + +export const getAgentsRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { + router.get( + { + path: '/internal/osquery/fleet_wrapper/agents', + validate: { + query: schema.object({}, { unknowns: 'allow' }), + }, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + async (context, request, response) => { + const esClient = context.core.elasticsearch.client.asInternalUser; + + const agents = await osqueryContext.service + .getAgentService() + // @ts-expect-error update types + ?.listAgents(esClient, request.query); + + return response.ok({ body: agents }); + } + ); +}; diff --git a/x-pack/plugins/osquery/server/routes/scheduled_query/find_scheduled_query_route.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_package_policies.ts similarity index 81% rename from x-pack/plugins/osquery/server/routes/scheduled_query/find_scheduled_query_route.ts rename to x-pack/plugins/osquery/server/routes/fleet_wrapper/get_package_policies.ts index 43d5f3fc893f0..b95dfbdfb9cb4 100644 --- a/x-pack/plugins/osquery/server/routes/scheduled_query/find_scheduled_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_package_policies.ts @@ -6,19 +6,19 @@ */ import { schema } from '@kbn/config-schema'; -import { OSQUERY_INTEGRATION_NAME } from '../../../common'; - +import { PLUGIN_ID, OSQUERY_INTEGRATION_NAME } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../fleet/common'; import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; -export const findScheduledQueryRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { +export const getPackagePoliciesRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { router.get( { - path: '/internal/osquery/scheduled_query', + path: '/internal/osquery/fleet_wrapper/package_policies', validate: { query: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-read`] }, }, async (context, request, response) => { const kuery = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.package.name: ${OSQUERY_INTEGRATION_NAME}`; diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/index.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/index.ts new file mode 100644 index 0000000000000..1821e19da975e --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from '../../../../../../src/core/server'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; +import { getAgentPoliciesRoute } from './get_agent_policies'; +import { getAgentPolicyRoute } from './get_agent_policy'; +import { getAgentStatusForAgentPolicyRoute } from './get_agent_status_for_agent_policy'; +import { getPackagePoliciesRoute } from './get_package_policies'; +import { getAgentsRoute } from './get_agents'; +import { getAgentDetailsRoute } from './get_agent_details'; + +export const initFleetWrapperRoutes = (router: IRouter, context: OsqueryAppContext) => { + getAgentDetailsRoute(router, context); + getAgentPoliciesRoute(router, context); + getAgentPolicyRoute(router, context); + getAgentStatusForAgentPolicyRoute(router, context); + getPackagePoliciesRoute(router, context); + getAgentsRoute(router, context); +}; diff --git a/x-pack/plugins/osquery/server/routes/index.ts b/x-pack/plugins/osquery/server/routes/index.ts index dd11141b2553f..c927c711a23cb 100644 --- a/x-pack/plugins/osquery/server/routes/index.ts +++ b/x-pack/plugins/osquery/server/routes/index.ts @@ -10,13 +10,19 @@ import { initActionRoutes } from './action'; import { OsqueryAppContext } from '../lib/osquery_app_context_services'; import { initSavedQueryRoutes } from './saved_query'; import { initStatusRoutes } from './status'; +import { initFleetWrapperRoutes } from './fleet_wrapper'; import { initPackRoutes } from './pack'; +import { initScheduledQueryGroupRoutes } from './scheduled_query_group'; +import { initPrivilegesCheckRoutes } from './privileges_check'; export const defineRoutes = (router: IRouter, context: OsqueryAppContext) => { const config = context.config(); initActionRoutes(router, context); initStatusRoutes(router, context); + initScheduledQueryGroupRoutes(router, context); + initFleetWrapperRoutes(router, context); + initPrivilegesCheckRoutes(router, context); if (config.packs) { initPackRoutes(router); diff --git a/x-pack/plugins/osquery/server/routes/privileges_check/index.ts b/x-pack/plugins/osquery/server/routes/privileges_check/index.ts new file mode 100644 index 0000000000000..8932b23b85f5a --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/privileges_check/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IRouter } from '../../../../../../src/core/server'; +import { privilegesCheckRoute } from './privileges_check_route'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; + +export const initPrivilegesCheckRoutes = (router: IRouter, context: OsqueryAppContext) => { + privilegesCheckRoute(router, context); +}; diff --git a/x-pack/plugins/osquery/server/routes/privileges_check/privileges_check_route.ts b/x-pack/plugins/osquery/server/routes/privileges_check/privileges_check_route.ts new file mode 100644 index 0000000000000..80c335c1c46d3 --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/privileges_check/privileges_check_route.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { OSQUERY_INTEGRATION_NAME, PLUGIN_ID } from '../../../common'; +import { IRouter } from '../../../../../../src/core/server'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const privilegesCheckRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { + router.get( + { + path: '/internal/osquery/privileges_check', + validate: {}, + options: { + tags: [`access:${PLUGIN_ID}-readLiveQueries`], + }, + }, + async (context, request, response) => { + const esClient = context.core.elasticsearch.client.asCurrentUser; + + const privileges = ( + await esClient.security.hasPrivileges({ + body: { + index: [ + { + names: [`logs-${OSQUERY_INTEGRATION_NAME}.result*`], + privileges: ['read'], + }, + ], + }, + }) + ).body; + + return response.ok({ + body: privileges, + }); + } + ); +}; diff --git a/x-pack/plugins/osquery/server/routes/saved_query/create_saved_query_route.ts b/x-pack/plugins/osquery/server/routes/saved_query/create_saved_query_route.ts index a41cb7cc39b40..fe8220c559de8 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/create_saved_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/create_saved_query_route.ts @@ -6,7 +6,7 @@ */ import { IRouter } from '../../../../../../src/core/server'; - +import { PLUGIN_ID } from '../../../common'; import { createSavedQueryRequestSchema, CreateSavedQueryRequestSchemaDecoded, @@ -24,6 +24,7 @@ export const createSavedQueryRoute = (router: IRouter) => { CreateSavedQueryRequestSchemaDecoded >(createSavedQueryRequestSchema), }, + options: { tags: [`access:${PLUGIN_ID}-writeSavedQueries`] }, }, async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/osquery/server/routes/saved_query/delete_saved_query_route.ts b/x-pack/plugins/osquery/server/routes/saved_query/delete_saved_query_route.ts index 5b8e231ba61ec..a34db8c11ddc3 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/delete_saved_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/delete_saved_query_route.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; - +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { savedQuerySavedObjectType } from '../../../common/types'; @@ -17,6 +17,7 @@ export const deleteSavedQueryRoute = (router: IRouter) => { validate: { body: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-writeSavedQueries`] }, }, async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/osquery/server/routes/saved_query/find_saved_query_route.ts b/x-pack/plugins/osquery/server/routes/saved_query/find_saved_query_route.ts index 6d737ba0d0220..79d6927d06722 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/find_saved_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/find_saved_query_route.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; - +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { savedQuerySavedObjectType } from '../../../common/types'; @@ -17,6 +17,7 @@ export const findSavedQueryRoute = (router: IRouter) => { validate: { query: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-readSavedQueries`] }, }, async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/osquery/server/routes/saved_query/read_saved_query_route.ts b/x-pack/plugins/osquery/server/routes/saved_query/read_saved_query_route.ts index 2d399648df4cc..4157ed1582305 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/read_saved_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/read_saved_query_route.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; - +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { savedQuerySavedObjectType } from '../../../common/types'; @@ -17,6 +17,7 @@ export const readSavedQueryRoute = (router: IRouter) => { validate: { params: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-readSavedQueries`] }, }, async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts b/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts index f9ecf675489dc..8edf95e311543 100644 --- a/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/saved_query/update_saved_query_route.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; - +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { savedQuerySavedObjectType } from '../../../common/types'; @@ -18,6 +18,7 @@ export const updateSavedQueryRoute = (router: IRouter) => { params: schema.object({}, { unknowns: 'allow' }), body: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-writeSavedQueries`] }, }, async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/osquery/server/routes/scheduled_query/create_scheduled_query_route.ts b/x-pack/plugins/osquery/server/routes/scheduled_query_group/create_scheduled_query_route.ts similarity index 87% rename from x-pack/plugins/osquery/server/routes/scheduled_query/create_scheduled_query_route.ts rename to x-pack/plugins/osquery/server/routes/scheduled_query_group/create_scheduled_query_route.ts index a3b882392989f..831fb30f6e320 100644 --- a/x-pack/plugins/osquery/server/routes/scheduled_query/create_scheduled_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/scheduled_query_group/create_scheduled_query_route.ts @@ -6,16 +6,18 @@ */ import { schema } from '@kbn/config-schema'; +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; export const createScheduledQueryRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { router.post( { - path: '/internal/osquery/scheduled', + path: '/internal/osquery/scheduled_query_group', validate: { body: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-writePacks`] }, }, async (context, request, response) => { const esClient = context.core.elasticsearch.client.asCurrentUser; diff --git a/x-pack/plugins/osquery/server/routes/scheduled_query/delete_scheduled_query_route.ts b/x-pack/plugins/osquery/server/routes/scheduled_query_group/delete_scheduled_query_route.ts similarity index 87% rename from x-pack/plugins/osquery/server/routes/scheduled_query/delete_scheduled_query_route.ts rename to x-pack/plugins/osquery/server/routes/scheduled_query_group/delete_scheduled_query_route.ts index 5b8e231ba61ec..c914512bb155e 100644 --- a/x-pack/plugins/osquery/server/routes/scheduled_query/delete_scheduled_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/scheduled_query_group/delete_scheduled_query_route.ts @@ -6,17 +6,18 @@ */ import { schema } from '@kbn/config-schema'; - +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { savedQuerySavedObjectType } from '../../../common/types'; export const deleteSavedQueryRoute = (router: IRouter) => { router.delete( { - path: '/internal/osquery/saved_query', + path: '/internal/osquery/scheduled_query_group', validate: { body: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-writePacks`] }, }, async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/osquery/server/routes/scheduled_query_group/find_scheduled_query_group_route.ts b/x-pack/plugins/osquery/server/routes/scheduled_query_group/find_scheduled_query_group_route.ts new file mode 100644 index 0000000000000..15c45e09b1bfd --- /dev/null +++ b/x-pack/plugins/osquery/server/routes/scheduled_query_group/find_scheduled_query_group_route.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { PLUGIN_ID, OSQUERY_INTEGRATION_NAME } from '../../../common'; +import { IRouter } from '../../../../../../src/core/server'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../fleet/common'; +import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; + +export const findScheduledQueryGroupRoute = ( + router: IRouter, + osqueryContext: OsqueryAppContext +) => { + router.get( + { + path: '/internal/osquery/scheduled_query_group', + validate: { + query: schema.object({}, { unknowns: 'allow' }), + }, + options: { tags: [`access:${PLUGIN_ID}-readPacks`] }, + }, + async (context, request, response) => { + const kuery = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.package.name: ${OSQUERY_INTEGRATION_NAME}`; + const packagePolicyService = osqueryContext.service.getPackagePolicyService(); + const policies = await packagePolicyService?.list(context.core.savedObjects.client, { + kuery, + }); + + return response.ok({ + body: policies, + }); + } + ); +}; diff --git a/x-pack/plugins/osquery/server/routes/scheduled_query/index.ts b/x-pack/plugins/osquery/server/routes/scheduled_query_group/index.ts similarity index 67% rename from x-pack/plugins/osquery/server/routes/scheduled_query/index.ts rename to x-pack/plugins/osquery/server/routes/scheduled_query_group/index.ts index 706bc38397296..416981a5cb5f2 100644 --- a/x-pack/plugins/osquery/server/routes/scheduled_query/index.ts +++ b/x-pack/plugins/osquery/server/routes/scheduled_query_group/index.ts @@ -10,14 +10,14 @@ import { IRouter } from '../../../../../../src/core/server'; import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; // import { createScheduledQueryRoute } from './create_scheduled_query_route'; // import { deleteScheduledQueryRoute } from './delete_scheduled_query_route'; -import { findScheduledQueryRoute } from './find_scheduled_query_route'; -import { readScheduledQueryRoute } from './read_scheduled_query_route'; +import { findScheduledQueryGroupRoute } from './find_scheduled_query_group_route'; +import { readScheduledQueryGroupRoute } from './read_scheduled_query_group_route'; // import { updateScheduledQueryRoute } from './update_scheduled_query_route'; -export const initScheduledQueryRoutes = (router: IRouter, context: OsqueryAppContext) => { +export const initScheduledQueryGroupRoutes = (router: IRouter, context: OsqueryAppContext) => { // createScheduledQueryRoute(router); // deleteScheduledQueryRoute(router); - findScheduledQueryRoute(router, context); - readScheduledQueryRoute(router, context); + findScheduledQueryGroupRoute(router, context); + readScheduledQueryGroupRoute(router, context); // updateScheduledQueryRoute(router); }; diff --git a/x-pack/plugins/osquery/server/routes/scheduled_query/read_scheduled_query_route.ts b/x-pack/plugins/osquery/server/routes/scheduled_query_group/read_scheduled_query_group_route.ts similarity index 65% rename from x-pack/plugins/osquery/server/routes/scheduled_query/read_scheduled_query_route.ts rename to x-pack/plugins/osquery/server/routes/scheduled_query_group/read_scheduled_query_group_route.ts index 009374f6a2e9e..de8125aab5b29 100644 --- a/x-pack/plugins/osquery/server/routes/scheduled_query/read_scheduled_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/scheduled_query_group/read_scheduled_query_group_route.ts @@ -6,28 +6,34 @@ */ import { schema } from '@kbn/config-schema'; - +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; -export const readScheduledQueryRoute = (router: IRouter, osqueryContext: OsqueryAppContext) => { +export const readScheduledQueryGroupRoute = ( + router: IRouter, + osqueryContext: OsqueryAppContext +) => { router.get( { - path: '/internal/osquery/scheduled_query/{id}', + path: '/internal/osquery/scheduled_query_group/{id}', validate: { params: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-readPacks`] }, }, async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; const packagePolicyService = osqueryContext.service.getPackagePolicyService(); - // @ts-expect-error update types - const scheduledQuery = await packagePolicyService?.get(savedObjectsClient, request.params.id); + const scheduledQueryGroup = await packagePolicyService?.get( + savedObjectsClient, + // @ts-expect-error update types + request.params.id + ); return response.ok({ - // @ts-expect-error update types - body: scheduledQuery, + body: { item: scheduledQueryGroup }, }); } ); diff --git a/x-pack/plugins/osquery/server/routes/scheduled_query/update_scheduled_query_route.ts b/x-pack/plugins/osquery/server/routes/scheduled_query_group/update_scheduled_query_route.ts similarity index 92% rename from x-pack/plugins/osquery/server/routes/scheduled_query/update_scheduled_query_route.ts rename to x-pack/plugins/osquery/server/routes/scheduled_query_group/update_scheduled_query_route.ts index efb4f2990e942..2a6e7a33fcddd 100644 --- a/x-pack/plugins/osquery/server/routes/scheduled_query/update_scheduled_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/scheduled_query_group/update_scheduled_query_route.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; - +import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { savedQuerySavedObjectType } from '../../../common/types'; @@ -18,6 +18,7 @@ export const updateSavedQueryRoute = (router: IRouter) => { params: schema.object({}, { unknowns: 'allow' }), body: schema.object({}, { unknowns: 'allow' }), }, + options: { tags: [`access:${PLUGIN_ID}-writePacks`] }, }, async (context, request, response) => { const savedObjectsClient = context.core.savedObjects.client; diff --git a/x-pack/plugins/osquery/server/routes/status/create_status_route.ts b/x-pack/plugins/osquery/server/routes/status/create_status_route.ts index d7ea49c6152cd..0a527424f9f42 100644 --- a/x-pack/plugins/osquery/server/routes/status/create_status_route.ts +++ b/x-pack/plugins/osquery/server/routes/status/create_status_route.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { OSQUERY_INTEGRATION_NAME } from '../../../common'; +import { PLUGIN_ID, OSQUERY_INTEGRATION_NAME } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; @@ -14,16 +14,10 @@ export const createStatusRoute = (router: IRouter, osqueryContext: OsqueryAppCon { path: '/internal/osquery/status', validate: false, + options: { tags: [`access:${PLUGIN_ID}-read`] }, }, async (context, request, response) => { const soClient = context.core.savedObjects.client; - const isSuperUser = osqueryContext.security.authc - .getCurrentUser(request) - ?.roles.includes('superuser'); - - if (!isSuperUser) { - return response.ok({ body: undefined }); - } const packageInfo = await osqueryContext.service .getPackageService() diff --git a/x-pack/plugins/osquery/server/routes/usage/saved_object_mappings.ts b/x-pack/plugins/osquery/server/routes/usage/saved_object_mappings.ts index 92709f92d9e5f..603bcad87cf80 100644 --- a/x-pack/plugins/osquery/server/routes/usage/saved_object_mappings.ts +++ b/x-pack/plugins/osquery/server/routes/usage/saved_object_mappings.ts @@ -23,6 +23,6 @@ export const usageMetricSavedObjectMappings: SavedObjectsType['mappings'] = { export const usageMetricType: SavedObjectsType = { name: usageMetricSavedObjectType, hidden: false, - namespaceType: 'single', + namespaceType: 'agnostic', mappings: usageMetricSavedObjectMappings, }; diff --git a/x-pack/plugins/osquery/server/search_strategy/osquery/index.ts b/x-pack/plugins/osquery/server/search_strategy/osquery/index.ts index 9fffb0726dce6..2fa9ee04ab534 100644 --- a/x-pack/plugins/osquery/server/search_strategy/osquery/index.ts +++ b/x-pack/plugins/osquery/server/search_strategy/osquery/index.ts @@ -23,7 +23,7 @@ import { OsqueryFactory } from './factory/types'; export const osquerySearchStrategyProvider = ( data: PluginStart ): ISearchStrategy, StrategyResponseType> => { - const es = data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY); + let es: typeof data.search.searchAsInternalUser; return { search: (request, options, deps) => { @@ -32,20 +32,35 @@ export const osquerySearchStrategyProvider = ( } const queryFactory: OsqueryFactory = osqueryFactory[request.factoryQueryType]; const dsl = queryFactory.buildDsl(request); - return es.search({ ...request, params: dsl }, options, deps).pipe( - map((response) => { - return { - ...response, - ...{ - rawResponse: shimHitsTotal(response.rawResponse), - }, - }; - }), - mergeMap((esSearchRes) => queryFactory.parse(request, esSearchRes)) - ); + + // use internal user for searching .fleet* indicies + es = dsl.index?.includes('fleet') + ? data.search.searchAsInternalUser + : data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY); + + return es + .search( + { + ...request, + params: dsl, + }, + options, + deps + ) + .pipe( + map((response) => { + return { + ...response, + ...{ + rawResponse: shimHitsTotal(response.rawResponse), + }, + }; + }), + mergeMap((esSearchRes) => queryFactory.parse(request, esSearchRes)) + ); }, cancel: async (id, options, deps) => { - if (es.cancel) { + if (es?.cancel) { return es.cancel(id, options, deps); } }, diff --git a/x-pack/plugins/osquery/server/usage/collector.ts b/x-pack/plugins/osquery/server/usage/collector.ts index 4432592a4e063..b04fc34e52453 100644 --- a/x-pack/plugins/osquery/server/usage/collector.ts +++ b/x-pack/plugins/osquery/server/usage/collector.ts @@ -11,11 +11,12 @@ import { getBeatUsage, getLiveQueryUsage, getPolicyLevelUsage } from './fetchers import { CollectorDependencies, usageSchema, UsageData } from './types'; export type RegisterCollector = (deps: CollectorDependencies) => void; -export async function getInternalSavedObjectsClient(core: CoreSetup) { - return core.getStartServices().then(async ([coreStart]) => { - return coreStart.savedObjects.createInternalRepository(); - }); -} +export const getInternalSavedObjectsClient = async ( + getStartServices: CoreSetup['getStartServices'] +) => { + const [coreStart] = await getStartServices(); + return new SavedObjectsClient(coreStart.savedObjects.createInternalRepository()); +}; export const registerCollector: RegisterCollector = ({ core, osqueryContext, usageCollection }) => { if (!usageCollection) { @@ -26,7 +27,8 @@ export const registerCollector: RegisterCollector = ({ core, osqueryContext, usa schema: usageSchema, isReady: () => true, fetch: async ({ esClient }: CollectorFetchContext): Promise => { - const savedObjectsClient = new SavedObjectsClient(await getInternalSavedObjectsClient(core)); + const savedObjectsClient = await getInternalSavedObjectsClient(core.getStartServices); + return { beat_metrics: { usage: await getBeatUsage(esClient), diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts index 275626664bef0..6a6a0e13a1e1e 100644 --- a/x-pack/test/api_integration/apis/features/features/features.ts +++ b/x-pack/test/api_integration/apis/features/features/features.ts @@ -115,6 +115,7 @@ export default function ({ getService }: FtrProviderContext) { 'logs', 'maps', 'observabilityCases', + 'osquery', 'uptime', 'siem', 'fleet', diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 2576a5eaf9bc9..bbb0fc60cb3ce 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -70,6 +70,19 @@ export default function ({ getService }: FtrProviderContext) { indexPatterns: ['all', 'read'], savedObjectsManagement: ['all', 'read'], timelion: ['all', 'read'], + osquery: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'live_queries_all', + 'live_queries_read', + 'run_saved_queries', + 'saved_queries_all', + 'saved_queries_read', + 'packs_all', + 'packs_read', + ], }, reserved: ['ml_user', 'ml_admin', 'ml_apm_user', 'monitoring'], }; diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts index 25266da2cdfb3..dc00be028412b 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -37,6 +37,7 @@ export default function ({ getService }: FtrProviderContext) { logs: ['all', 'read'], uptime: ['all', 'read'], apm: ['all', 'read'], + osquery: ['all', 'read'], ml: ['all', 'read'], siem: ['all', 'read'], fleet: ['all', 'read'], diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts index afd8d1fb54cf6..aeaaf7fca1cb7 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts @@ -53,6 +53,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) { catalogueId !== 'ml' && catalogueId !== 'ml_file_data_visualizer' && catalogueId !== 'monitoring' && + catalogueId !== 'osquery' && !esFeatureExceptions.includes(catalogueId) ); expect(uiCapabilities.value!.catalogue).to.eql(expected); @@ -74,6 +75,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) { 'appSearch', 'workplaceSearch', 'spaces', + 'osquery', ...esFeatureExceptions, ]; const expected = mapValues( diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts index b87a6475526f7..6a6b618c2c8c8 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts @@ -42,7 +42,7 @@ export default function navLinksTests({ getService }: FtrProviderContext) { expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('navLinks'); expect(uiCapabilities.value!.navLinks).to.eql( - navLinksBuilder.except('ml', 'monitoring') + navLinksBuilder.except('ml', 'monitoring', 'osquery') ); break; case 'everything_space_all at everything_space': @@ -57,7 +57,8 @@ export default function navLinksTests({ getService }: FtrProviderContext) { 'monitoring', 'enterpriseSearch', 'appSearch', - 'workplaceSearch' + 'workplaceSearch', + 'osquery' ) ); break; diff --git a/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts index d64b4f75e20a6..da4b26106afac 100644 --- a/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts @@ -53,6 +53,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) { catalogueId !== 'ml' && catalogueId !== 'monitoring' && catalogueId !== 'ml_file_data_visualizer' && + catalogueId !== 'osquery' && !esFeatureExceptions.includes(catalogueId) ); expect(uiCapabilities.value!.catalogue).to.eql(expected); @@ -70,6 +71,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) { 'enterpriseSearch', 'appSearch', 'workplaceSearch', + 'osquery', ...esFeatureExceptions, ]; const expected = mapValues( diff --git a/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts index 4d96532b83d4a..6a44b3d8f0b71 100644 --- a/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts +++ b/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts @@ -42,7 +42,7 @@ export default function navLinksTests({ getService }: FtrProviderContext) { expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('navLinks'); expect(uiCapabilities.value!.navLinks).to.eql( - navLinksBuilder.except('ml', 'monitoring') + navLinksBuilder.except('ml', 'monitoring', 'osquery') ); break; case 'read': @@ -55,7 +55,8 @@ export default function navLinksTests({ getService }: FtrProviderContext) { 'monitoring', 'enterpriseSearch', 'appSearch', - 'workplaceSearch' + 'workplaceSearch', + 'osquery' ) ); break; From 283349ac2b7b4980b743ea8fdf6aa6ed96c0afda Mon Sep 17 00:00:00 2001 From: Josh Dover <1813008+joshdover@users.noreply.github.com> Date: Tue, 10 Aug 2021 16:52:10 +0200 Subject: [PATCH 030/104] Add SO migration testing guidance to testing guide (#105959) --- dev_docs/tutorials/saved_objects.mdx | 21 +-- dev_docs/tutorials/testing_plugins.mdx | 188 ++++++++++++++++++++++++- 2 files changed, 199 insertions(+), 10 deletions(-) diff --git a/dev_docs/tutorials/saved_objects.mdx b/dev_docs/tutorials/saved_objects.mdx index bd7d231218af1..644afadb268cb 100644 --- a/dev_docs/tutorials/saved_objects.mdx +++ b/dev_docs/tutorials/saved_objects.mdx @@ -122,7 +122,7 @@ Since Elasticsearch has a default limit of 1000 fields per index, plugins should fields they add to the mappings. Similarly, Saved Object types should never use `dynamic: true` as this can cause an arbitrary amount of fields to be added to the .kibana index. - ## References +## References Declare by adding an id, type and name to the `references` array. @@ -155,12 +155,14 @@ identify this reference. This guarantees that the id the reference points to alw visualization id was directly stored in `dashboard.panels[0].visualization` there is a risk that this id gets updated without updating the reference in the references array. -## Writing migrations +## Migrations Saved Objects support schema changes between Kibana versions, which we call migrations. Migrations are - applied when a Kibana installation is upgraded from one version to the next, when exports are imported via + applied when a Kibana installation is upgraded from one version to a newer version, when exports are imported via the Saved Objects Management UI, or when a new object is created via the HTTP API. +### Writing migrations + Each Saved Object type may define migrations for its schema. Migrations are specified by the Kibana version number, receive an input document, and must return the fully migrated document to be persisted to Elasticsearch. @@ -241,10 +243,11 @@ export const dashboardVisualization: SavedObjectsType = { in which this migration was released. So if you are creating a migration which will be part of the v7.10.0 release, but will also be backported and released as v7.9.3, the migration version should be: 7.9.3. - Migrations should be written defensively, an exception in a migration function will prevent a Kibana upgrade from succeeding and will cause downtime for our users. - Having said that, if a - document is encountered that is not in the expected shape, migrations are encouraged to throw an exception to abort the upgrade. In most scenarios, it is better to - fail an upgrade than to silently ignore a corrupt document which can cause unexpected behaviour at some future point in time. +Migrations should be written defensively, an exception in a migration function will prevent a Kibana upgrade from succeeding and will cause downtime for our users. +Having said that, if a document is encountered that is not in the expected shape, migrations are encouraged to throw an exception to abort the upgrade. In most scenarios, it is better to +fail an upgrade than to silently ignore a corrupt document which can cause unexpected behaviour at some future point in time. When such a scenario is encountered, +the error should be verbose and informative so that the corrupt document can be corrected, if possible. + +### Testing Migrations -It is critical that you have extensive tests to ensure that migrations behave as expected with all possible input documents. Given how simple it is to test all the branch -conditions in a migration function and the high impact of a bug in this code, there’s really no reason not to aim for 100% test code coverage. +Bugs in a migration function cause downtime for our users and therefore have a very high impact. Follow the . diff --git a/dev_docs/tutorials/testing_plugins.mdx b/dev_docs/tutorials/testing_plugins.mdx index 96e13555a36a2..55b662421cbd0 100644 --- a/dev_docs/tutorials/testing_plugins.mdx +++ b/dev_docs/tutorials/testing_plugins.mdx @@ -569,7 +569,7 @@ describe('renderApp', () => { }); ``` -### SavedObjects +### SavedObjectsClient #### Unit Tests @@ -794,6 +794,192 @@ Kibana and esArchiver to load fixture data into Elasticsearch. _todo: fully worked out example_ +### Saved Objects migrations + +_Also see ._ + +It is critical that you have extensive tests to ensure that migrations behave as expected with all possible input +documents. Given how simple it is to test all the branch conditions in a migration function and the high impact of a +bug in this code, there’s really no reason not to aim for 100% test code coverage. + +It's recommend that you primarily leverage unit testing with Jest for testing your migration function. Unit tests will +be a much more effective approach to testing all the different shapes of input data and edge cases that your migration +may need to handle. With more complex migrations that interact with several components or may behave different depending +on registry contents (such as Embeddable migrations), we recommend that you use the Jest Integration suite which allows +you to create a full instance Kibana and all plugins in memory and leverage the import API to test migrating documents. + +#### Throwing exceptions +Keep in mind that any exception thrown by your migration function will cause Kibana to fail to upgrade. This should almost +never happen for our end users and we should be exhaustive in our testing to be sure to catch as many edge cases that we +could possibly handle. This entails ensuring that the migration is written defensively; we should try to avoid every bug +possible in our implementation. + +In general, exceptions should only be thrown when the input data is corrupted and doesn't match the expected schema. In +such cases, it's important that an informative error message is included in the exception and we do not rely on implicit +runtime exceptions such as "null pointer exceptions" like `TypeError: Cannot read property 'foo' of undefined`. + +#### Unit testing + +Unit testing migration functions is typically pretty straight forward and comparable to other types of Jest testing. In +general, you should focus this tier of testing on validating output and testing input edge cases. One focus of this tier +should be trying to find edge cases that throw exceptions the migration shouldn't. As you can see in this simple +example, the coverage here is very exhaustive and verbose, which is intentional. + +```ts +import { migrateCaseFromV7_9_0ToV7_10_0 } from './case_migrations'; + +const validInput_7_9_0 = { + id: '1', + type: 'case', + attributes: { + connector_id: '1234'; + } +} + +describe('Case migrations v7.7.0 -> v7.8.0', () => { + it('transforms the connector field', () => { + expect(migrateCaseFromV7_9_0ToV7_10_0(validInput_7_9_0)).toEqual({ + id: '1', + type: 'case', + attributes: { + connector: { + id: '1234', // verify id was moved into subobject + name: 'none', // verify new default field was added + } + } + }); + }); + + it('handles empty string', () => { + expect(migrateCaseFromV7_9_0ToV7_10_0({ + id: '1', + type: 'case', + attributes: { + connector_id: '' + } + })).toEqual({ + id: '1', + type: 'case', + attributes: { + connector: { + id: 'none', + name: 'none', + } + } + }); + }); + + it('handles null', () => { + expect(migrateCaseFromV7_9_0ToV7_10_0({ + id: '1', + type: 'case', + attributes: { + connector_id: null + } + })).toEqual({ + id: '1', + type: 'case', + attributes: { + connector: { + id: 'none', + name: 'none', + } + } + }); + }); + + it('handles undefined', () => { + expect(migrateCaseFromV7_9_0ToV7_10_0({ + id: '1', + type: 'case', + attributes: { + // Even though undefined isn't a valid JSON or Elasticsearch value, we should test it anyways since there + // could be some JavaScript layer that casts the field to `undefined` for some reason. + connector_id: undefined + } + })).toEqual({ + id: '1', + type: 'case', + attributes: { + connector: { + id: 'none', + name: 'none', + } + } + }); + + expect(migrateCaseFromV7_9_0ToV7_10_0({ + id: '1', + type: 'case', + attributes: { + // also test without the field present at all + } + })).toEqual({ + id: '1', + type: 'case', + attributes: { + connector: { + id: 'none', + name: 'none', + } + } + }); + }); +}); +``` + +#### Integration testing +With more complicated migrations, the behavior of the migration may be dependent on values from other plugins which may +be difficult or even impossible to test with unit tests. You need to actually bootstrap Kibana, load the plugins, and +then test the full end-to-end migration. This type of set up will also test ingesting your documents into Elasticsearch +against the mappings defined by your Saved Object type. + +This can be achieved using the `jest_integration` suite and the `kbnTestServer` utility for starting an in-memory +instance of Kibana. You can then leverage the import API to test migrations. This API applies the same migrations to +imported documents as are applied at Kibana startup and is much easier to work with for testing. + +```ts +// You may need to adjust these paths depending on where your test file is located. +// The absolute path is src/core/test_helpers/so_migrations +import { createTestHarness, SavedObjectTestHarness } from '../../../../src/core/test_helpers/so_migrations'; + +describe('my plugin migrations', () => { + let testHarness: SavedObjectTestHarness; + + beforeAll(async () => { + testHarness = createTestHarness(); + await testHarness.start(); + }); + + afterAll(async () => { + await testHarness.stop(); + }); + + it('successfully migrates valid case documents', async () => { + expect( + await testHarness.migrate([ + { type: 'case', id: '1', attributes: { connector_id: '1234' }, references: [] }, + { type: 'case', id: '2', attributes: { connector_id: '' }, references: [] }, + { type: 'case', id: '3', attributes: { connector_id: null }, references: [] }, + ]) + ).toEqual([ + expect.objectContaining( + { type: 'case', id: '1', attributes: { connector: { id: '1234', name: 'none' } } }), + expect.objectContaining( + { type: 'case', id: '2', attributes: { connector: { id: 'none', name: 'none' } } }), + expect.objectContaining( + { type: 'case', id: '3', attributes: { connector: { id: 'none', name: 'none' } } }), + ]) + }) +}) +``` + +There are some caveats about using the import/export API for testing migrations: +- You cannot test the startup behavior of Kibana this way. This should not have any effect on type migrations but does + mean that this method cannot be used for testing the migration algorithm itself. +- While not yet supported, if support is added for migrations that affect multiple types, it's possible that the + behavior during import may vary slightly from the upgrade behavior. + ### Elasticsearch _How to test ES clients_ From f479259a25397c912a87b1c0edcd7761adb3c1e3 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Tue, 10 Aug 2021 16:04:57 +0100 Subject: [PATCH 031/104] [ML] Adds a 30 day model prune window to non-rare Security jobs (#107752) Adds the model_prune_window setting added in elastic/elasticsearch#75741 to all Security jobs that use functions that support model pruning. This means that the models for split field values that are not seen for 30 days will be dropped. If those split field values are subsequently seen again then new models will be created like for completely new entities. The "rare" function does not support model pruning, so jobs that use the "rare" function are not modified. --- .../modules/security_auth/ml/auth_high_count_logon_events.json | 3 ++- .../ml/auth_high_count_logon_events_for_a_source_ip.json | 3 ++- .../modules/security_auth/ml/auth_high_count_logon_fails.json | 3 ++- .../security_network/ml/high_count_by_destination_country.json | 3 ++- .../modules/security_network/ml/high_count_network_denies.json | 3 ++- .../modules/security_network/ml/high_count_network_events.json | 3 ++- .../siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json | 3 ++- .../siem_cloudtrail/ml/high_distinct_count_error_message.json | 3 ++- .../modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json | 3 ++- .../modules/siem_winlogbeat/ml/windows_anomalous_script.json | 3 ++- 10 files changed, 20 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json index ee84fb222bb5c..35fc14e23624f 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json @@ -14,7 +14,8 @@ "detector_index": 0 } ], - "influencers": [] + "influencers": [], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json index 7bbbc81b6de7a..0fe4b7805d077 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json @@ -19,7 +19,8 @@ "source.ip", "winlog.event_data.LogonType", "user.name" - ] + ], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json index 4b7094e92c6ec..cde52bf7d33cc 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json @@ -14,7 +14,8 @@ "detector_index": 0 } ], - "influencers": [] + "influencers": [], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json index aaee46d9cf80b..2360233937c2b 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json @@ -20,7 +20,8 @@ "destination.as.organization.name", "source.ip", "destination.ip" - ] + ], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json index bc08aa21f3277..2a3b4b0100183 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json @@ -19,7 +19,8 @@ "destination.as.organization.name", "source.ip", "destination.port" - ] + ], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json index d709eb21d7c6d..792d7f2513985 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json @@ -19,7 +19,8 @@ "destination.as.organization.name", "source.ip", "destination.ip" - ] + ], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json index 9ee26b314c640..d2ecf4df624fc 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat_auth/ml/suspicious_login_activity_ecs.json @@ -19,7 +19,8 @@ "host.name", "user.name", "source.ip" - ] + ], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/high_distinct_count_error_message.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/high_distinct_count_error_message.json index 98d145a91d9a7..b4294227c49d5 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/high_distinct_count_error_message.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_cloudtrail/ml/high_distinct_count_error_message.json @@ -18,7 +18,8 @@ "aws.cloudtrail.user_identity.arn", "source.ip", "source.geo.city_name" - ] + ], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json index 0332fd53814a6..ad2627ff4f21f 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_packetbeat/ml/packetbeat_dns_tunneling.json @@ -34,7 +34,8 @@ "destination.ip", "host.name", "dns.question.etld_plus_one" - ] + ], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json index 613a446750e5f..6fff7246a249a 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_anomalous_script.json @@ -19,7 +19,8 @@ "host.name", "user.name", "winlog.event_data.Path" - ] + ], + "model_prune_window": "30d" }, "allow_lazy_open": true, "analysis_limits": { From 328c36dedccc26229b34d9dad4b0b90714811234 Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Tue, 10 Aug 2021 18:21:08 +0300 Subject: [PATCH 032/104] [Discover] Deangularize classic table (#104361) * [Discover] move angular directives to react compoenents * [Discover] add support of infiniteScroll * [Discover] support paginated classic table * [Discover] refactor docTable component, remove redundant angular code * [Discover] remove redundant files * [Discover] fix some functional tests and pgination * [Discover] fix functionals * [Discover] code refactoring, adding tests * [Discover] update tests * [Discover] fix embeddable view of doc table * [Discover] update pagination view * [Discover] remove unused translations * [Discover] improve readability, fix pagination * [Discover] adjust isFilterable check * [Discover] improve doc viewer table row display * [Discover] clean up implementation, fix functional test * [Discover] fix skip button * [Discover] update test snapshot * [Discover] update test * [Discover] simplify pagination, update layout in embeddable * [Discover] fix functional, remove redundant i18n translations * [Discover] return indexPatternField * [Discover] add support of fixed footer for embeddable * [Discover] move doc_table to apps/components folder, update test * [Discover] fix imports * [Discover] update imports, beautify code * Update src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.tsx Co-authored-by: Tim Roes * [Discover] remove redundant styles * [Discover] fix lining * [Discover] fix discover grid embeddable * [Discover] fix by comments * [Discover] return extraWidth, describe the problem * [Discover] fix unresolved conflicts Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Tim Roes --- .../discover/public/__mocks__/services.ts | 13 +- .../angular/context/api/context.ts | 5 +- .../create_discover_grid_directive.tsx | 29 +- .../angular/directives/render_complete.ts | 20 - .../tool_bar_pager_buttons.test.tsx.snap | 38 -- .../tool_bar_pager_text.test.tsx.snap | 10 - .../doc_table/components/pager/index.ts | 20 - .../pager/tool_bar_pager_buttons.test.tsx | 51 -- .../pager/tool_bar_pager_buttons.tsx | 59 --- .../components/pager/tool_bar_pager_text.scss | 5 - .../pager/tool_bar_pager_text.test.tsx | 21 - .../components/pager/tool_bar_pager_text.tsx | 31 -- .../doc_table/components/row_headers.test.js | 474 ------------------ .../doc_table/components/table_header.ts | 37 -- .../angular/doc_table/components/table_row.ts | 230 --------- .../components/table_row/cell.test.ts | 138 ----- .../doc_table/components/table_row/cell.ts | 58 --- .../table_row/cell_with_buttons.html | 25 - .../table_row/cell_without_buttons.html | 4 - .../components/table_row/details.html | 53 -- .../doc_table/components/table_row/open.html | 10 - .../table_row/truncate_by_height.test.ts | 22 - .../doc_table/create_doc_table_embeddable.tsx | 85 ---- .../doc_table/create_doc_table_react.tsx | 173 ------- .../angular/doc_table/doc_table.html | 121 ----- .../angular/doc_table/doc_table.test.js | 140 ------ .../angular/doc_table/doc_table.ts | 98 ---- .../angular/doc_table/doc_table_strings.js | 21 - .../angular/doc_table/infinite_scroll.ts | 69 --- .../angular/doc_table/lib/pager/index.js | 10 - .../angular/doc_table/lib/pager/pager.js | 66 --- .../doc_table/lib/pager/pager_factory.ts | 18 - .../application/angular/get_inner_angular.ts | 31 +- .../application/angular/helpers/index.ts | 1 - .../public/application/angular/index.ts | 1 - .../components}/doc_table/_doc_table.scss | 29 +- .../doc_table/actions/columns.test.ts | 10 +- .../components}/doc_table/actions/columns.ts | 10 +- .../doc_table/components/_index.scss | 0 .../doc_table/components/_table_header.scss | 0 .../components/pager/tool_bar_pagination.tsx | 108 ++++ .../__snapshots__/table_header.test.tsx.snap | 0 .../components/table_header/helpers.tsx | 2 +- .../table_header/table_header.test.tsx | 2 +- .../components/table_header/table_header.tsx | 2 +- .../table_header/table_header_column.tsx | 0 .../doc_table/components/table_row.test.tsx | 107 ++++ .../doc_table/components/table_row.tsx | 204 ++++++++ .../__snapshots__/table_cell.test.tsx.snap | 183 +++++++ .../doc_table/components/table_row/_cell.scss | 15 +- .../components/table_row/_details.scss | 0 .../components/table_row/_index.scss | 0 .../doc_table/components/table_row/_open.scss | 0 .../components/table_row/table_cell.test.tsx | 64 +++ .../components/table_row/table_cell.tsx | 42 ++ .../table_row/table_cell_actions.tsx | 60 +++ .../components/table_row_details.tsx | 80 +++ .../doc_table/create_doc_table_embeddable.tsx | 35 ++ .../doc_table/doc_table_context.tsx | 33 ++ .../doc_table/doc_table_embeddable.tsx | 114 +++++ .../doc_table/doc_table_infinite.tsx | 113 +++++ .../doc_table/doc_table_wrapper.test.tsx | 75 +++ .../doc_table/doc_table_wrapper.tsx | 243 +++++++++ .../main/components}/doc_table/index.scss | 0 .../main/components}/doc_table/index.ts | 1 - .../doc_table/lib/get_default_sort.test.ts | 4 +- .../doc_table/lib/get_default_sort.ts | 2 +- .../doc_table/lib/get_sort.test.ts | 4 +- .../components}/doc_table/lib/get_sort.ts | 2 +- .../lib/get_sort_for_search_source.test.ts | 4 +- .../lib/get_sort_for_search_source.ts | 2 +- .../doc_table/lib}/row_formatter.test.ts | 173 +++++-- .../doc_table/lib}/row_formatter.tsx | 13 +- .../components/doc_table/lib/use_pager.ts | 78 +++ .../layout/discover_documents.test.tsx | 1 - .../components/layout/discover_documents.tsx | 118 ++--- .../components/layout/discover_layout.scss | 13 +- .../components/layout/discover_layout.tsx | 11 +- .../total_documents/total_documents.tsx | 25 + .../apps/main/services/use_discover_state.ts | 2 +- .../apps/main/utils/get_sharing_data.ts | 2 +- .../apps/main/utils/get_state_defaults.ts | 3 +- .../get_switch_index_pattern_app_state.ts | 3 +- .../apps/main/utils/update_search_source.ts | 2 +- .../components/context_app/context_app.tsx | 3 +- .../context_app/context_app_content.test.tsx | 118 +++-- .../context_app/context_app_content.tsx | 115 ++--- .../discover_grid/discover_grid.tsx | 4 +- .../discover_grid/discover_grid_flyout.tsx | 7 +- .../components/doc_viewer/doc_viewer.scss | 1 - .../components/table/table_helper.test.ts | 36 -- .../components/table/table_helper.tsx | 7 - .../application/doc_views/doc_views_types.ts | 2 +- .../embeddable/saved_search_embeddable.tsx | 34 +- .../saved_search_embeddable_component.tsx | 13 +- .../public/application/embeddable/types.ts | 2 +- .../get_single_doc_url.ts} | 4 +- .../helpers/use_data_grid_columns.ts | 2 +- test/functional/apps/discover/_doc_table.ts | 10 +- .../functional/page_objects/dashboard_page.ts | 8 +- .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - 102 files changed, 2006 insertions(+), 2547 deletions(-) delete mode 100644 src/plugins/discover/public/application/angular/directives/render_complete.ts delete mode 100644 src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap delete mode 100644 src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_text.test.tsx.snap delete mode 100644 src/plugins/discover/public/application/angular/doc_table/components/pager/index.ts delete mode 100644 src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx delete mode 100644 src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.tsx delete mode 100644 src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.scss delete mode 100644 src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.test.tsx delete mode 100644 src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.tsx delete mode 100644 src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js delete mode 100644 src/plugins/discover/public/application/angular/doc_table/components/table_header.ts delete mode 100644 src/plugins/discover/public/application/angular/doc_table/components/table_row.ts delete mode 100644 src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.test.ts delete mode 100644 src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.ts delete mode 100644 src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_with_buttons.html delete mode 100644 src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_without_buttons.html delete mode 100644 src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html delete mode 100644 src/plugins/discover/public/application/angular/doc_table/components/table_row/open.html delete mode 100644 src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.test.ts delete mode 100644 src/plugins/discover/public/application/angular/doc_table/create_doc_table_embeddable.tsx delete mode 100644 src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx delete mode 100644 src/plugins/discover/public/application/angular/doc_table/doc_table.html delete mode 100644 src/plugins/discover/public/application/angular/doc_table/doc_table.test.js delete mode 100644 src/plugins/discover/public/application/angular/doc_table/doc_table.ts delete mode 100644 src/plugins/discover/public/application/angular/doc_table/doc_table_strings.js delete mode 100644 src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts delete mode 100644 src/plugins/discover/public/application/angular/doc_table/lib/pager/index.js delete mode 100644 src/plugins/discover/public/application/angular/doc_table/lib/pager/pager.js delete mode 100644 src/plugins/discover/public/application/angular/doc_table/lib/pager/pager_factory.ts rename src/plugins/discover/public/application/{angular => apps/main/components}/doc_table/_doc_table.scss (86%) rename src/plugins/discover/public/application/{angular => apps/main/components}/doc_table/actions/columns.test.ts (86%) rename src/plugins/discover/public/application/{angular => apps/main/components}/doc_table/actions/columns.ts (92%) rename src/plugins/discover/public/application/{angular => apps/main/components}/doc_table/components/_index.scss (100%) rename src/plugins/discover/public/application/{angular => apps/main/components}/doc_table/components/_table_header.scss (100%) create mode 100644 src/plugins/discover/public/application/apps/main/components/doc_table/components/pager/tool_bar_pagination.tsx rename src/plugins/discover/public/application/{angular => apps/main/components}/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap (100%) rename src/plugins/discover/public/application/{angular => apps/main/components}/doc_table/components/table_header/helpers.tsx (97%) rename src/plugins/discover/public/application/{angular => apps/main/components}/doc_table/components/table_header/table_header.test.tsx (99%) rename src/plugins/discover/public/application/{angular => apps/main/components}/doc_table/components/table_header/table_header.tsx (96%) rename src/plugins/discover/public/application/{angular => apps/main/components}/doc_table/components/table_header/table_header_column.tsx (100%) create mode 100644 src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.test.tsx create mode 100644 src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.tsx create mode 100644 src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/__snapshots__/table_cell.test.tsx.snap rename src/plugins/discover/public/application/{angular => apps/main/components}/doc_table/components/table_row/_cell.scss (64%) rename src/plugins/discover/public/application/{angular => apps/main/components}/doc_table/components/table_row/_details.scss (100%) rename src/plugins/discover/public/application/{angular => apps/main/components}/doc_table/components/table_row/_index.scss (100%) rename src/plugins/discover/public/application/{angular => apps/main/components}/doc_table/components/table_row/_open.scss (100%) create mode 100644 src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell.test.tsx create mode 100644 src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell.tsx create mode 100644 src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell_actions.tsx create mode 100644 src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row_details.tsx create mode 100644 src/plugins/discover/public/application/apps/main/components/doc_table/create_doc_table_embeddable.tsx create mode 100644 src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_context.tsx create mode 100644 src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_embeddable.tsx create mode 100644 src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_infinite.tsx create mode 100644 src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.test.tsx create mode 100644 src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.tsx rename src/plugins/discover/public/application/{angular => apps/main/components}/doc_table/index.scss (100%) rename src/plugins/discover/public/application/{angular => apps/main/components}/doc_table/index.ts (90%) rename src/plugins/discover/public/application/{angular => apps/main/components}/doc_table/lib/get_default_sort.test.ts (91%) rename src/plugins/discover/public/application/{angular => apps/main/components}/doc_table/lib/get_default_sort.ts (93%) rename src/plugins/discover/public/application/{angular => apps/main/components}/doc_table/lib/get_sort.test.ts (96%) rename src/plugins/discover/public/application/{angular => apps/main/components}/doc_table/lib/get_sort.ts (97%) rename src/plugins/discover/public/application/{angular => apps/main/components}/doc_table/lib/get_sort_for_search_source.test.ts (94%) rename src/plugins/discover/public/application/{angular => apps/main/components}/doc_table/lib/get_sort_for_search_source.ts (95%) rename src/plugins/discover/public/application/{angular/helpers => apps/main/components/doc_table/lib}/row_formatter.test.ts (53%) rename src/plugins/discover/public/application/{angular/helpers => apps/main/components/doc_table/lib}/row_formatter.tsx (87%) create mode 100644 src/plugins/discover/public/application/apps/main/components/doc_table/lib/use_pager.ts create mode 100644 src/plugins/discover/public/application/apps/main/components/total_documents/total_documents.tsx delete mode 100644 src/plugins/discover/public/application/components/table/table_helper.test.ts rename src/plugins/discover/public/application/{angular/doc_table/components/table_row/truncate_by_height.ts => helpers/get_single_doc_url.ts} (65%) diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts index e137955674457..96888a07be68f 100644 --- a/src/plugins/discover/public/__mocks__/services.ts +++ b/src/plugins/discover/public/__mocks__/services.ts @@ -10,13 +10,16 @@ import { DiscoverServices } from '../build_services'; import { dataPluginMock } from '../../../data/public/mocks'; import { chromeServiceMock, coreMock, docLinksServiceMock } from '../../../../core/public/mocks'; import { + CONTEXT_STEP_SETTING, DEFAULT_COLUMNS_SETTING, + DOC_HIDE_TIME_COLUMN_SETTING, SAMPLE_SIZE_SETTING, SORT_DEFAULT_ORDER_SETTING, } from '../../common'; import { savedSearchMock } from './saved_search'; import { UI_SETTINGS } from '../../../data/common'; import { TopNavMenu } from '../../../navigation/public'; +import { FORMATS_UI_SETTINGS } from 'src/plugins/field_formats/common'; const dataPlugin = dataPluginMock.createStartContract(); export const discoverServiceMock = ({ @@ -49,10 +52,16 @@ export const discoverServiceMock = ({ return []; } else if (key === UI_SETTINGS.META_FIELDS) { return []; - } else if (key === SAMPLE_SIZE_SETTING) { - return 250; + } else if (key === DOC_HIDE_TIME_COLUMN_SETTING) { + return false; + } else if (key === CONTEXT_STEP_SETTING) { + return 5; } else if (key === SORT_DEFAULT_ORDER_SETTING) { return 'desc'; + } else if (key === FORMATS_UI_SETTINGS.SHORT_DOTS_ENABLE) { + return false; + } else if (key === SAMPLE_SIZE_SETTING) { + return 250; } }, isDefault: (key: string) => { diff --git a/src/plugins/discover/public/application/angular/context/api/context.ts b/src/plugins/discover/public/application/angular/context/api/context.ts index 727f941f5fee3..e9da3a7c4784f 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.ts @@ -22,10 +22,7 @@ export enum SurrDocType { } export type EsHitRecord = Required< - Pick< - estypes.SearchResponse['hits']['hits'][number], - '_id' | 'fields' | 'sort' | '_index' | '_version' - > + Pick > & { _source?: Record; _score?: number; diff --git a/src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx b/src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx index 810be94ce24b0..b79936bd6f385 100644 --- a/src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx +++ b/src/plugins/discover/public/application/angular/create_discover_grid_directive.tsx @@ -7,25 +7,40 @@ */ import React, { useState } from 'react'; import { I18nProvider } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { DiscoverGrid, DiscoverGridProps } from '../components/discover_grid/discover_grid'; import { getServices } from '../../kibana_services'; import { ElasticSearchHit } from '../doc_views/doc_views_types'; +import { TotalDocuments } from '../apps/main/components/total_documents/total_documents'; + +export interface DiscoverGridEmbeddableProps extends DiscoverGridProps { + totalHitCount: number; +} export const DataGridMemoized = React.memo((props: DiscoverGridProps) => ( )); -export function DiscoverGridEmbeddable(props: DiscoverGridProps) { +export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) { const [expandedDoc, setExpandedDoc] = useState(undefined); return ( - + + {props.totalHitCount !== 0 && ( + + + + )} + + + + ); } diff --git a/src/plugins/discover/public/application/angular/directives/render_complete.ts b/src/plugins/discover/public/application/angular/directives/render_complete.ts deleted file mode 100644 index b62820bf876d8..0000000000000 --- a/src/plugins/discover/public/application/angular/directives/render_complete.ts +++ /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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { IScope } from 'angular'; -import { RenderCompleteListener } from '../../../../../kibana_utils/public'; - -export function createRenderCompleteDirective() { - return { - controller($scope: IScope, $element: JQLite) { - const el = $element[0]; - const renderCompleteListener = new RenderCompleteListener(el); - $scope.$on('$destroy', renderCompleteListener.destroy); - }, - }; -} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap b/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap deleted file mode 100644 index fd0c1b4b2af8d..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_buttons.test.tsx.snap +++ /dev/null @@ -1,38 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`it renders ToolBarPagerButtons 1`] = ` - - - - - - - - -`; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_text.test.tsx.snap b/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_text.test.tsx.snap deleted file mode 100644 index 96d2994bbe68f..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/__snapshots__/tool_bar_pager_text.test.tsx.snap +++ /dev/null @@ -1,10 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`it renders ToolBarPagerText without crashing 1`] = ` -
- 1–2 of 3 -
-`; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/index.ts b/src/plugins/discover/public/application/angular/doc_table/components/pager/index.ts deleted file mode 100644 index 180da83beb2af..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/index.ts +++ /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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { ToolBarPagerText } from './tool_bar_pager_text'; -import { ToolBarPagerButtons } from './tool_bar_pager_buttons'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createToolBarPagerTextDirective(reactDirective: any) { - return reactDirective(ToolBarPagerText); -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createToolBarPagerButtonsDirective(reactDirective: any) { - return reactDirective(ToolBarPagerButtons); -} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx deleted file mode 100644 index 061dae2d50658..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.test.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest'; -import { ToolBarPagerButtons } from './tool_bar_pager_buttons'; -import { findTestSubject } from '@elastic/eui/lib/test'; - -test('it renders ToolBarPagerButtons', () => { - const props = { - hasPreviousPage: true, - hasNextPage: true, - onPageNext: jest.fn(), - onPagePrevious: jest.fn(), - }; - const wrapper = shallowWithIntl(); - expect(wrapper).toMatchSnapshot(); -}); - -test('it renders ToolBarPagerButtons with clickable next and previous button', () => { - const props = { - hasPreviousPage: true, - hasNextPage: true, - onPageNext: jest.fn(), - onPagePrevious: jest.fn(), - }; - const wrapper = mountWithIntl(); - findTestSubject(wrapper, 'btnPrevPage').simulate('click'); - expect(props.onPagePrevious).toHaveBeenCalledTimes(1); - findTestSubject(wrapper, 'btnNextPage').simulate('click'); - expect(props.onPageNext).toHaveBeenCalledTimes(1); -}); - -test('it renders ToolBarPagerButtons with disabled next and previous button', () => { - const props = { - hasPreviousPage: false, - hasNextPage: false, - onPageNext: jest.fn(), - onPagePrevious: jest.fn(), - }; - const wrapper = mountWithIntl(); - findTestSubject(wrapper, 'btnPrevPage').simulate('click'); - expect(props.onPagePrevious).toHaveBeenCalledTimes(0); - findTestSubject(wrapper, 'btnNextPage').simulate('click'); - expect(props.onPageNext).toHaveBeenCalledTimes(0); -}); diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.tsx b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.tsx deleted file mode 100644 index d825220163165..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_buttons.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; - -interface Props { - hasPreviousPage: boolean; - hasNextPage: boolean; - onPageNext: () => void; - onPagePrevious: () => void; -} - -export function ToolBarPagerButtons(props: Props) { - return ( - - - props.onPagePrevious()} - isDisabled={!props.hasPreviousPage} - data-test-subj="btnPrevPage" - aria-label={i18n.translate( - 'discover.docTable.pager.toolbarPagerButtons.previousButtonAriaLabel', - { - defaultMessage: 'Previous page in table', - } - )} - /> - - - props.onPageNext()} - isDisabled={!props.hasNextPage} - data-test-subj="btnNextPage" - aria-label={i18n.translate( - 'discover.docTable.pager.toolbarPagerButtons.nextButtonAriaLabel', - { - defaultMessage: 'Next page in table', - } - )} - /> - - - ); -} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.scss b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.scss deleted file mode 100644 index 446e852f51e05..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.scss +++ /dev/null @@ -1,5 +0,0 @@ -.kbnDocTable__toolBarText { - line-height: $euiLineHeight; - color: #69707D; - white-space: nowrap; -} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.test.tsx b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.test.tsx deleted file mode 100644 index a8ebfcc9bb311..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { renderWithIntl } from '@kbn/test/jest'; -import { ToolBarPagerText } from './tool_bar_pager_text'; - -test('it renders ToolBarPagerText without crashing', () => { - const props = { - startItem: 1, - endItem: 2, - totalItems: 3, - }; - const wrapper = renderWithIntl(); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.tsx b/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.tsx deleted file mode 100644 index 5db68952b69ca..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/tool_bar_pager_text.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import './tool_bar_pager_text.scss'; -import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; - -interface Props { - startItem: number; - endItem: number; - totalItems: number; -} - -export function ToolBarPagerText({ startItem, endItem, totalItems }: Props) { - return ( - -
- -
-
- ); -} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js b/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js deleted file mode 100644 index 1a3b34c45d05e..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/row_headers.test.js +++ /dev/null @@ -1,474 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import angular from 'angular'; -import 'angular-mocks'; -import 'angular-sanitize'; -import 'angular-route'; -import _ from 'lodash'; -import sinon from 'sinon'; -import { getFakeRow } from '../../../../__fixtures__/fake_row'; -import $ from 'jquery'; -import FixturesStubbedLogstashIndexPatternProvider from '../../../../__fixtures__/stubbed_logstash_index_pattern'; -import { setScopedHistory, setServices, setDocViewsRegistry } from '../../../../kibana_services'; -import { coreMock } from '../../../../../../../core/public/mocks'; -import { dataPluginMock } from '../../../../../../data/public/mocks'; -import { navigationPluginMock } from '../../../../../../navigation/public/mocks'; -import { initAngularBootstrap } from '../../../../../../kibana_legacy/public/angular_bootstrap'; -import { getInnerAngularModule } from '../../get_inner_angular'; -import { createBrowserHistory } from 'history'; - -const fakeRowVals = { - time: 'time_formatted', - bytes: 'bytes_formatted', - '@timestamp': '@timestamp_formatted', - request_body: 'request_body_formatted', -}; - -describe('Doc Table', () => { - const core = coreMock.createStart(); - const dataMock = dataPluginMock.createStartContract(); - let $parentScope; - let $scope; - let $elementScope; - let timeout; - let registry = []; - - // Stub out a minimal mapping of 4 fields - let mapping; - - beforeAll(async () => { - await initAngularBootstrap(); - }); - beforeAll(() => setScopedHistory(createBrowserHistory())); - beforeEach(() => { - angular.element.prototype.slice = jest.fn(function (index) { - return $(this).slice(index); - }); - angular.element.prototype.filter = jest.fn(function (condition) { - return $(this).filter(condition); - }); - angular.element.prototype.toggle = jest.fn(function (name) { - return $(this).toggle(name); - }); - angular.element.prototype.is = jest.fn(function (name) { - return $(this).is(name); - }); - setServices({ - uiSettings: core.uiSettings, - filterManager: dataMock.query.filterManager, - addBasePath: (path) => path, - }); - - setDocViewsRegistry({ - addDocView(view) { - registry.push(view); - }, - getDocViewsSorted() { - return registry; - }, - resetRegistry: () => { - registry = []; - }, - }); - - getInnerAngularModule( - 'app/discover', - core, - { - data: dataMock, - navigation: navigationPluginMock.createStartContract(), - }, - coreMock.createPluginInitializerContext() - ); - angular.mock.module('app/discover'); - }); - beforeEach( - angular.mock.inject(function ($rootScope, Private, $timeout) { - $parentScope = $rootScope; - timeout = $timeout; - $parentScope.indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - mapping = $parentScope.indexPattern.fields; - - // Stub `getConverterFor` for a field in the indexPattern to return mock data. - - const convertFn = (value, type, options) => { - const fieldName = _.get(options, 'field.name', null); - return fakeRowVals[fieldName] || ''; - }; - $parentScope.indexPattern.getFormatterForField = () => ({ - convert: convertFn, - getConverterFor: () => convertFn, - }); - }) - ); - - afterEach(() => { - delete angular.element.prototype.slice; - delete angular.element.prototype.filter; - delete angular.element.prototype.toggle; - delete angular.element.prototype.is; - }); - - // Sets up the directive, take an element, and a list of properties to attach to the parent scope. - const init = function ($elem, props) { - angular.mock.inject(function ($compile) { - _.assign($parentScope, props); - const el = $compile($elem)($parentScope); - $elementScope = el.scope(); - el.scope().$digest(); - $scope = el.isolateScope(); - }); - }; - - const destroy = () => { - $scope.$destroy(); - $parentScope.$destroy(); - }; - - // For testing column removing/adding for the header and the rows - const columnTests = function (elemType, parentElem) { - test('should create a time column if the timefield is defined', () => { - const childElems = parentElem.find(elemType); - expect(childElems.length).toBe(1); - }); - - test('should be able to add and remove columns', () => { - let childElems; - - // Should include a column for toggling and the time column by default - $parentScope.columns = ['bytes']; - $elementScope.$digest(); - childElems = parentElem.find(elemType); - expect(childElems.length).toBe(2); - expect($(childElems[1]).text()).toContain('bytes'); - - $parentScope.columns = ['bytes', 'request_body']; - $elementScope.$digest(); - childElems = parentElem.find(elemType); - expect(childElems.length).toBe(3); - expect($(childElems[2]).text()).toContain('request_body'); - - $parentScope.columns = ['request_body']; - $elementScope.$digest(); - childElems = parentElem.find(elemType); - expect(childElems.length).toBe(2); - expect($(childElems[1]).text()).toContain('request_body'); - }); - - test('should create only the toggle column if there is no timeField', () => { - delete $scope.indexPattern.timeFieldName; - $scope.$digest(); - timeout.flush(); - - const childElems = parentElem.find(elemType); - expect(childElems.length).toBe(0); - }); - }; - - describe('kbnTableRow', () => { - const $elem = $( - '' - ); - let row; - - beforeEach(() => { - row = getFakeRow(0, mapping); - - init($elem, { - row, - columns: [], - sorting: [], - filter: sinon.spy(), - maxLength: 50, - }); - }); - afterEach(() => { - destroy(); - }); - - describe('adding and removing columns', () => { - columnTests('[data-test-subj~="docTableField"]', $elem); - }); - - describe('details row', () => { - test('should be an empty tr by default', () => { - expect($elem.next().is('tr')).toBe(true); - expect($elem.next().text()).toBe(''); - }); - - test('should expand the detail row when the toggle arrow is clicked', () => { - $elem.children(':first-child').click(); - expect($elem.next().text()).not.toBe(''); - }); - - describe('expanded', () => { - let $details; - beforeEach(() => { - // Open the row - $scope.toggleRow(); - timeout.flush(); - $details = $elem.next(); - }); - afterEach(() => { - // Close the row - $scope.toggleRow(); - }); - - test('should be a tr with something in it', () => { - expect($details.is('tr')).toBe(true); - expect($details.text()).toBeTruthy(); - }); - }); - }); - }); - - describe('kbnTableRow meta', () => { - const $elem = angular.element( - '' - ); - let row; - - beforeEach(() => { - row = getFakeRow(0, mapping); - - init($elem, { - row: row, - columns: [], - sorting: [], - filtering: sinon.spy(), - maxLength: 50, - }); - - // Open the row - $scope.toggleRow(); - $scope.$digest(); - timeout.flush(); - $elem.next(); - }); - - afterEach(() => { - destroy(); - }); - - /** this no longer works with the new plugin approach - test('should render even when the row source contains a field with the same name as a meta field', () => { - setTimeout(() => { - //this should be overridden by later changes - }, 100); - expect($details.find('tr').length).toBe(_.keys($parentScope.indexPattern.flattenHit($scope.row)).length); - }); */ - }); - - describe('row diffing', () => { - let $row; - let $scope; - let $root; - let $before; - - beforeEach( - angular.mock.inject(function ($rootScope, $compile, Private) { - $root = $rootScope; - $root.row = getFakeRow(0, mapping); - $root.columns = ['_source']; - $root.sorting = []; - $root.filtering = sinon.spy(); - $root.maxLength = 50; - $root.mapping = mapping; - $root.indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - - $row = $('').attr({ - 'kbn-table-row': 'row', - columns: 'columns', - sorting: 'sorting', - filtering: 'filtering', - 'index-pattern': 'indexPattern', - }); - - $scope = $root.$new(); - $compile($row)($scope); - $root.$apply(); - - $before = $row.find('td'); - expect($before).toHaveLength(3); - expect($before.eq(0).text().trim()).toBe(''); - expect($before.eq(1).text().trim()).toMatch(/^time_formatted/); - }) - ); - - afterEach(() => { - $row.remove(); - }); - - test('handles a new column', () => { - $root.columns.push('bytes'); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(4); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - expect($after[2].outerHTML).toBe($before[2].outerHTML); - expect($after.eq(3).text().trim()).toMatch(/^bytes_formatted/); - }); - - test('handles two new columns at once', () => { - $root.columns.push('bytes'); - $root.columns.push('request_body'); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(5); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - expect($after[2].outerHTML).toBe($before[2].outerHTML); - expect($after.eq(3).text().trim()).toMatch(/^bytes_formatted/); - expect($after.eq(4).text().trim()).toMatch(/^request_body_formatted/); - }); - - test('handles three new columns in odd places', () => { - $root.columns = ['@timestamp', 'bytes', '_source', 'request_body']; - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(6); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - expect($after.eq(2).text().trim()).toMatch(/^@timestamp_formatted/); - expect($after.eq(3).text().trim()).toMatch(/^bytes_formatted/); - expect($after[4].outerHTML).toBe($before[2].outerHTML); - expect($after.eq(5).text().trim()).toMatch(/^request_body_formatted/); - }); - - test('handles a removed column', () => { - _.pull($root.columns, '_source'); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(2); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - }); - - test('handles two removed columns', () => { - // first add a column - $root.columns.push('@timestamp'); - $root.$apply(); - - const $mid = $row.find('td'); - expect($mid).toHaveLength(4); - - $root.columns.pop(); - $root.columns.pop(); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(2); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - }); - - test('handles three removed random columns', () => { - // first add two column - $root.columns.push('@timestamp', 'bytes'); - $root.$apply(); - - const $mid = $row.find('td'); - expect($mid).toHaveLength(5); - - $root.columns[0] = false; // _source - $root.columns[2] = false; // bytes - $root.columns = $root.columns.filter(Boolean); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(3); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - expect($after.eq(2).text().trim()).toMatch(/^@timestamp_formatted/); - }); - - test('handles two columns with the same content', () => { - const tempVal = fakeRowVals.request_body; - fakeRowVals.request_body = 'bytes_formatted'; - - $root.columns.length = 0; - $root.columns.push('bytes'); - $root.columns.push('request_body'); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(4); - expect($after.eq(2).text().trim()).toMatch(/^bytes_formatted/); - expect($after.eq(3).text().trim()).toMatch(/^bytes_formatted/); - fakeRowVals.request_body = tempVal; - }); - - test('handles two columns swapping position', () => { - $root.columns.push('bytes'); - $root.$apply(); - - const $mid = $row.find('td'); - expect($mid).toHaveLength(4); - - $root.columns.reverse(); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(4); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - expect($after[2].outerHTML).toBe($mid[3].outerHTML); - expect($after[3].outerHTML).toBe($mid[2].outerHTML); - }); - - test('handles four columns all reversing position', () => { - $root.columns.push('bytes', 'response', '@timestamp'); - $root.$apply(); - - const $mid = $row.find('td'); - expect($mid).toHaveLength(6); - - $root.columns.reverse(); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(6); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - expect($after[2].outerHTML).toBe($mid[5].outerHTML); - expect($after[3].outerHTML).toBe($mid[4].outerHTML); - expect($after[4].outerHTML).toBe($mid[3].outerHTML); - expect($after[5].outerHTML).toBe($mid[2].outerHTML); - }); - - test('handles multiple columns with the same name', () => { - $root.columns.push('bytes', 'bytes', 'bytes'); - $root.$apply(); - - const $after = $row.find('td'); - expect($after).toHaveLength(6); - expect($after[0].outerHTML).toBe($before[0].outerHTML); - expect($after[1].outerHTML).toBe($before[1].outerHTML); - expect($after[2].outerHTML).toBe($before[2].outerHTML); - expect($after.eq(3).text().trim()).toMatch(/^bytes_formatted/); - expect($after.eq(4).text().trim()).toMatch(/^bytes_formatted/); - expect($after.eq(5).text().trim()).toMatch(/^bytes_formatted/); - }); - }); -}); diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts deleted file mode 100644 index 0f6c86df0db64..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { TableHeader } from './table_header/table_header'; -import { getServices } from '../../../../kibana_services'; -import { SORT_DEFAULT_ORDER_SETTING, DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../../common'; -import { FORMATS_UI_SETTINGS } from '../../../../../../field_formats/common'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createTableHeaderDirective(reactDirective: any) { - const { uiSettings: config } = getServices(); - - return reactDirective( - TableHeader, - [ - ['columns', { watchDepth: 'collection' }], - ['hideTimeColumn', { watchDepth: 'value' }], - ['indexPattern', { watchDepth: 'reference' }], - ['isShortDots', { watchDepth: 'value' }], - ['onChangeSortOrder', { watchDepth: 'reference' }], - ['onMoveColumn', { watchDepth: 'reference' }], - ['onRemoveColumn', { watchDepth: 'reference' }], - ['sortOrder', { watchDepth: 'collection' }], - ], - { restrict: 'A' }, - { - hideTimeColumn: config.get(DOC_HIDE_TIME_COLUMN_SETTING, false), - isShortDots: config.get(FORMATS_UI_SETTINGS.SHORT_DOTS_ENABLE), - defaultSortOrder: config.get(SORT_DEFAULT_ORDER_SETTING, 'desc'), - } - ); -} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts deleted file mode 100644 index 1d6956fc80920..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { find } from 'lodash'; -import $ from 'jquery'; -import openRowHtml from './table_row/open.html'; -import detailsHtml from './table_row/details.html'; -import { dispatchRenderComplete } from '../../../../../../kibana_utils/public'; -import { DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../../common'; -import { getServices } from '../../../../kibana_services'; -import { getContextUrl } from '../../../helpers/get_context_url'; -import { formatRow, formatTopLevelObject } from '../../helpers'; -import { truncateByHeight } from './table_row/truncate_by_height'; -import { cell } from './table_row/cell'; - -// guesstimate at the minimum number of chars wide cells in the table should be -const MIN_LINE_LENGTH = 20; - -interface LazyScope extends ng.IScope { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; -} - -export function createTableRowDirective($compile: ng.ICompileService) { - return { - restrict: 'A', - scope: { - columns: '=', - filter: '=', - indexPattern: '=', - row: '=kbnTableRow', - onAddColumn: '=?', - onRemoveColumn: '=?', - useNewFieldsApi: '<', - }, - link: ($scope: LazyScope, $el: JQuery) => { - $el.after(''); - $el.empty(); - - // when we compile the details, we use this $scope - let $detailsScope: LazyScope; - - // when we compile the toggle button in the summary, we use this $scope - let $toggleScope; - - // toggle display of the rows details, a full list of the fields from each row - $scope.toggleRow = () => { - const $detailsTr = $el.next(); - - $scope.open = !$scope.open; - - /// - // add/remove $details children - /// - - $detailsTr.toggle($scope.open); - - if (!$scope.open) { - // close the child scope if it exists - $detailsScope.$destroy(); - // no need to go any further - return; - } else { - $detailsScope = $scope.$new(); - } - - // empty the details and rebuild it - $detailsTr.html(detailsHtml); - $detailsScope.row = $scope.row; - $detailsScope.hit = $scope.row; - $detailsScope.uriEncodedId = encodeURIComponent($detailsScope.hit._id); - - $compile($detailsTr)($detailsScope); - }; - - $scope.$watchMulti(['indexPattern.timeFieldName', 'row.highlight', '[]columns'], () => { - createSummaryRow($scope.row); - }); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - $scope.inlineFilter = function inlineFilter($event: any, type: string) { - const column = $($event.currentTarget).data().column; - const field = $scope.indexPattern.fields.getByName(column); - $scope.filter(field, $scope.flattenedRow[column], type); - }; - - $scope.getContextAppHref = () => { - return getContextUrl( - $scope.row._id, - $scope.indexPattern.id, - $scope.columns, - getServices().filterManager, - getServices().addBasePath - ); - }; - - $scope.getSingleDocHref = () => { - return getServices().addBasePath( - `/app/discover#/doc/${$scope.indexPattern.id}/${ - $scope.row._index - }?id=${encodeURIComponent($scope.row._id)}` - ); - }; - - // create a tr element that lists the value for each *column* - // eslint-disable-next-line @typescript-eslint/no-explicit-any - function createSummaryRow(row: any) { - const indexPattern = $scope.indexPattern; - $scope.flattenedRow = indexPattern.flattenHit(row); - - // We just create a string here because its faster. - const newHtmls = [openRowHtml]; - - const mapping = indexPattern.fields.getByName; - const hideTimeColumn = getServices().uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false); - if (indexPattern.timeFieldName && !hideTimeColumn) { - newHtmls.push( - cell({ - timefield: true, - formatted: _displayField(row, indexPattern.timeFieldName), - filterable: mapping(indexPattern.timeFieldName).filterable && $scope.filter, - column: indexPattern.timeFieldName, - }) - ); - } - - if ($scope.columns.length === 0 && $scope.useNewFieldsApi) { - const formatted = formatRow(row, indexPattern); - - newHtmls.push( - cell({ - timefield: false, - sourcefield: true, - formatted, - filterable: false, - column: '__document__', - }) - ); - } else { - $scope.columns.forEach(function (column: string) { - const isFilterable = mapping(column) && mapping(column).filterable && $scope.filter; - if ($scope.useNewFieldsApi && !mapping(column) && !row.fields[column]) { - const innerColumns = Object.fromEntries( - Object.entries(row.fields).filter(([key]) => { - return key.indexOf(`${column}.`) === 0; - }) - ); - newHtmls.push( - cell({ - timefield: false, - sourcefield: true, - formatted: formatTopLevelObject(row, innerColumns, indexPattern), - filterable: false, - column, - }) - ); - } else { - newHtmls.push( - cell({ - timefield: false, - sourcefield: column === '_source', - formatted: _displayField(row, column, true), - filterable: isFilterable, - column, - }) - ); - } - }); - } - - let $cells = $el.children(); - newHtmls.forEach(function (html, i) { - const $cell = $cells.eq(i); - if ($cell.data('discover:html') === html) return; - - const reuse = find($cells.slice(i + 1), (c) => { - return $.data(c, 'discover:html') === html; - }); - - const $target = reuse ? $(reuse).detach() : $(html); - $target.data('discover:html', html); - const $before = $cells.eq(i - 1); - if ($before.length) { - $before.after($target); - } else { - $el.append($target); - } - - // rebuild cells since we modified the children - $cells = $el.children(); - - if (!reuse) { - $toggleScope = $scope.$new(); - $compile($target)($toggleScope); - } - }); - - if ($scope.open) { - $detailsScope.row = row; - } - - // trim off cells that were not used rest of the cells - $cells.filter(':gt(' + (newHtmls.length - 1) + ')').remove(); - dispatchRenderComplete($el[0]); - } - - /** - * Fill an element with the value of a field - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - function _displayField(row: any, fieldName: string, truncate = false) { - const indexPattern = $scope.indexPattern; - const text = indexPattern.formatField(row, fieldName); - - if (truncate && text.length > MIN_LINE_LENGTH) { - return truncateByHeight({ - body: text, - }); - } - - return text; - } - }, - }; -} diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.test.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.test.ts deleted file mode 100644 index c6d0d324b9bc2..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { cell } from './cell'; - -describe('cell renderer', () => { - it('renders a cell without filter buttons if it is not filterable', () => { - expect( - cell({ - filterable: false, - column: 'foo', - timefield: true, - sourcefield: false, - formatted: 'formatted content', - }) - ).toMatchInlineSnapshot(` - "formatted content - " - `); - }); - - it('renders a cell with filter buttons if it is filterable', () => { - expect( - cell({ - filterable: true, - column: 'foo', - timefield: true, - sourcefield: false, - formatted: 'formatted content', - }) - ).toMatchInlineSnapshot(` - "formatted content - " - `); - }); - - it('renders a sourcefield', () => { - expect( - cell({ - filterable: false, - column: 'foo', - timefield: false, - sourcefield: true, - formatted: 'formatted content', - }) - ).toMatchInlineSnapshot(` - "formatted content - " - `); - }); - - it('renders a field that is neither a timefield or sourcefield', () => { - expect( - cell({ - filterable: false, - column: 'foo', - timefield: false, - sourcefield: false, - formatted: 'formatted content', - }) - ).toMatchInlineSnapshot(` - "formatted content - " - `); - }); - - it('renders the "formatted" contents without any manipulation', () => { - expect( - cell({ - filterable: false, - column: 'foo', - timefield: true, - sourcefield: false, - formatted: - '
 hey you can put HTML & stuff in here 
', - }) - ).toMatchInlineSnapshot(` - "
 hey you can put HTML & stuff in here 
- " - `); - }); - - it('escapes the contents of "column" within the "data-column" attribute', () => { - expect( - cell({ - filterable: true, - column: '', - timefield: true, - sourcefield: false, - formatted: 'formatted content', - }) - ).toMatchInlineSnapshot(` - "formatted content - " - `); - }); -}); diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.ts deleted file mode 100644 index 8138e0f4a4fd8..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.ts +++ /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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -import { escape } from 'lodash'; -import cellWithFilters from './cell_with_buttons.html'; -import cellWithoutFilters from './cell_without_buttons.html'; - -const TAGS_WITH_WS = />\s+<'); -} - -const cellWithFiltersTemplate = noWhiteSpace(cellWithFilters); -const cellWithoutFiltersTemplate = noWhiteSpace(cellWithoutFilters); - -interface CellProps { - timefield: boolean; - sourcefield?: boolean; - formatted: string; - filterable: boolean; - column: string; -} - -export const cell = (props: CellProps) => { - let classes = ''; - let extraAttrs = ''; - if (props.timefield) { - classes = 'eui-textNoWrap'; - extraAttrs = 'width="1%"'; - } else if (props.sourcefield) { - classes = 'eui-textBreakAll eui-textBreakWord'; - } else { - classes = 'kbnDocTableCell__dataField eui-textBreakAll eui-textBreakWord'; - } - - if (props.filterable) { - const escapedColumnContents = escape(props.column); - return cellWithFiltersTemplate - .replace('__classes__', classes) - .replace('__extraAttrs__', extraAttrs) - .replace('__column__', escapedColumnContents) - .replace('__column__', escapedColumnContents) - .replace('', props.formatted); - } - return cellWithoutFiltersTemplate - .replace('__classes__', classes) - .replace('__extraAttrs__', extraAttrs) - .replace('', props.formatted); -}; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_with_buttons.html b/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_with_buttons.html deleted file mode 100644 index 99c65e6034013..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_with_buttons.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_without_buttons.html b/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_without_buttons.html deleted file mode 100644 index 8dc33cbfb8353..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell_without_buttons.html +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html b/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html deleted file mode 100644 index faa3d51c19fee..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html +++ /dev/null @@ -1,53 +0,0 @@ - -
-
-
-
- -
-
-

-
-
-
-
-
-
- -
-
- -
-
-
-
-
- -
- - diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/open.html b/src/plugins/discover/public/application/angular/doc_table/components/table_row/open.html deleted file mode 100644 index 1a5d974a1b081..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/open.html +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.test.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.test.ts deleted file mode 100644 index 70d8465589237..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { truncateByHeight } from './truncate_by_height'; - -describe('truncateByHeight', () => { - it('renders input without any formatting or escaping', () => { - expect( - truncateByHeight({ - body: - '
 hey you can put HTML & stuff in here 
', - }) - ).toMatchInlineSnapshot( - `"
 hey you can put HTML & stuff in here 
"` - ); - }); -}); diff --git a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_embeddable.tsx b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_embeddable.tsx deleted file mode 100644 index 19913ed6de870..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_embeddable.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React, { useRef, useEffect } from 'react'; -import { I18nProvider } from '@kbn/i18n/react'; -import { IScope } from 'angular'; -import { getServices } from '../../../kibana_services'; -import { DocTableLegacyProps, injectAngularElement } from './create_doc_table_react'; - -type AngularEmbeddableScope = IScope & { renderProps?: DocTableEmbeddableProps }; - -export interface DocTableEmbeddableProps extends Partial { - refs: HTMLElement; -} - -function getRenderFn(domNode: Element, props: DocTableEmbeddableProps) { - const directive = { - template: ``, - }; - - return async () => { - try { - const injector = await getServices().getEmbeddableInjector(); - return await injectAngularElement(domNode, directive.template, props, injector); - } catch (e) { - // eslint-disable-next-line no-console - console.error(e); - throw e; - } - }; -} - -export function DiscoverDocTableEmbeddable(props: DocTableEmbeddableProps) { - return ( - - - - ); -} - -function DocTableLegacyInner(renderProps: DocTableEmbeddableProps) { - const scope = useRef(); - - useEffect(() => { - if (renderProps.refs && !scope.current) { - const fn = getRenderFn(renderProps.refs, renderProps); - fn().then((newScope) => { - scope.current = newScope; - }); - } else if (scope?.current) { - scope.current.renderProps = { ...renderProps }; - scope.current.$applyAsync(); - } - }, [renderProps]); - - useEffect(() => { - return () => { - scope.current?.$destroy(); - }; - }, []); - return ; -} diff --git a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx deleted file mode 100644 index 73a67310bf4be..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import angular, { auto, ICompileService, IScope } from 'angular'; -import { render } from 'react-dom'; -import React, { useRef, useEffect, useState, useCallback } from 'react'; -import type { estypes } from '@elastic/elasticsearch'; -import { EuiButtonEmpty } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { getServices, IndexPattern } from '../../../kibana_services'; -import { IndexPatternField } from '../../../../../data/common'; -import { SkipBottomButton } from '../../apps/main/components/skip_bottom_button'; - -export interface DocTableLegacyProps { - columns: string[]; - searchDescription?: string; - searchTitle?: string; - onFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; - rows: estypes.SearchHit[]; - indexPattern: IndexPattern; - minimumVisibleRows?: number; - onAddColumn?: (column: string) => void; - onBackToTop: () => void; - onSort?: (sort: string[][]) => void; - onMoveColumn?: (columns: string, newIdx: number) => void; - onRemoveColumn?: (column: string) => void; - sampleSize: number; - sort?: string[][]; - useNewFieldsApi?: boolean; -} -export interface AngularDirective { - template: string; -} -export type AngularScope = IScope & { renderProps?: DocTableLegacyProps }; - -/** - * Compiles and injects the give angular template into the given dom node - * returns a function to cleanup the injected angular element - */ -export async function injectAngularElement( - domNode: Element, - template: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - renderProps: any, - injector: auto.IInjectorService -) { - const rootScope: IScope = injector.get('$rootScope'); - const $compile: ICompileService = injector.get('$compile'); - const newScope = Object.assign(rootScope.$new(), { renderProps }); - - const $target = angular.element(domNode); - const $element = angular.element(template); - - newScope.$apply(() => { - const linkFn = $compile($element); - $target.empty().append($element); - linkFn(newScope); - }); - - return newScope; -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function getRenderFn(domNode: Element, props: any) { - const directive = { - template: ``, - }; - - return async () => { - try { - const injector = await getServices().getEmbeddableInjector(); - return await injectAngularElement(domNode, directive.template, props, injector); - } catch (e) { - render(
error
, domNode); - } - }; -} - -export function DocTableLegacy(renderProps: DocTableLegacyProps) { - const ref = useRef(null); - const scope = useRef(); - const [rows, setRows] = useState(renderProps.rows); - const [minimumVisibleRows, setMinimumVisibleRows] = useState(50); - const onSkipBottomButtonClick = useCallback(async () => { - // delay scrolling to after the rows have been rendered - const bottomMarker = document.getElementById('discoverBottomMarker'); - const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - // show all the rows - setMinimumVisibleRows(renderProps.rows.length); - - while (renderProps.rows.length !== document.getElementsByClassName('kbnDocTable__row').length) { - await wait(50); - } - bottomMarker!.focus(); - await wait(50); - bottomMarker!.blur(); - }, [setMinimumVisibleRows, renderProps.rows]); - - useEffect(() => { - setMinimumVisibleRows(50); - setRows(renderProps.rows); - }, [renderProps.rows, setMinimumVisibleRows]); - - useEffect(() => { - if (ref && ref.current && !scope.current) { - const fn = getRenderFn(ref.current, { ...renderProps, rows, minimumVisibleRows }); - fn().then((newScope) => { - scope.current = newScope; - }); - } else if (scope && scope.current) { - scope.current.renderProps = { ...renderProps, rows, minimumVisibleRows }; - scope.current.$applyAsync(); - } - }, [renderProps, minimumVisibleRows, rows]); - - useEffect(() => { - return () => { - if (scope.current) { - scope.current.$destroy(); - } - }; - }, []); - return ( -
- -
- {renderProps.rows.length === renderProps.sampleSize ? ( -
- - - - -
- ) : ( - - ​ - - )} -
- ); -} diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.html b/src/plugins/discover/public/application/angular/doc_table/doc_table.html deleted file mode 100644 index ecd7aa8f3dcf4..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/doc_table.html +++ /dev/null @@ -1,121 +0,0 @@ -
-
-
-
-
- {{ limitedResultsWarning }} -
- - - -
-
-
- - - - - -
-
- - -
- - - - - - -
- -
- -
-
- - -
- -

-

-
-
diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js b/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js deleted file mode 100644 index 097f32965b141..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/doc_table.test.js +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import angular from 'angular'; -import _ from 'lodash'; -import 'angular-mocks'; -import 'angular-sanitize'; -import 'angular-route'; -import { createBrowserHistory } from 'history'; -import FixturesStubbedLogstashIndexPatternProvider from '../../../__fixtures__/stubbed_logstash_index_pattern'; -import hits from '../../../__fixtures__/real_hits'; -import { coreMock } from '../../../../../../core/public/mocks'; -import { dataPluginMock } from '../../../../../data/public/mocks'; -import { navigationPluginMock } from '../../../../../navigation/public/mocks'; -import { initAngularBootstrap } from '../../../../../kibana_legacy/public/angular_bootstrap'; -import { setScopedHistory, setServices } from '../../../kibana_services'; -import { getInnerAngularModule } from '../get_inner_angular'; - -let $parentScope; - -let $scope; - -let $timeout; - -let indexPattern; - -const init = function ($elem, props) { - angular.mock.inject(function ($rootScope, $compile, _$timeout_) { - $timeout = _$timeout_; - $parentScope = $rootScope; - _.assign($parentScope, props); - - $compile($elem)($parentScope); - - // I think the prereq requires this? - $timeout(() => { - $elem.scope().$digest(); - }, 0); - - $scope = $elem.isolateScope(); - }); -}; - -const destroy = () => { - $scope.$destroy(); - $parentScope.$destroy(); -}; - -describe('docTable', () => { - const core = coreMock.createStart(); - let $elem; - - beforeAll(async () => { - await initAngularBootstrap(); - }); - beforeAll(() => setScopedHistory(createBrowserHistory())); - beforeEach(() => { - angular.element.prototype.slice = jest.fn(() => { - return null; - }); - angular.element.prototype.filter = jest.fn(() => { - return { - remove: jest.fn(), - }; - }); - setServices({ - uiSettings: core.uiSettings, - }); - getInnerAngularModule( - 'app/discover', - core, - { - data: dataPluginMock.createStartContract(), - navigation: navigationPluginMock.createStartContract(), - }, - coreMock.createPluginInitializerContext() - ); - angular.mock.module('app/discover'); - }); - beforeEach(() => { - $elem = angular.element(` - - `); - angular.mock.inject(function (Private) { - indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider); - }); - init($elem, { - indexPattern, - hits: [...hits], - totalHitCount: hits.length, - columns: [], - sorting: ['@timestamp', 'desc'], - }); - $scope.$digest(); - }); - - afterEach(() => { - delete angular.element.prototype.slice; - delete angular.element.prototype.filter; - destroy(); - }); - - test('should compile', () => { - expect($elem.text()).toBeTruthy(); - }); - - test('should have an addRows function that increases the row count', () => { - expect($scope.addRows).toBeInstanceOf(Function); - $scope.$digest(); - expect($scope.limit).toBe(50); - $scope.addRows(); - expect($scope.limit).toBe(100); - }); - - test('should reset the row limit when results are received', () => { - $scope.limit = 100; - expect($scope.limit).toBe(100); - $scope.hits = [...hits]; - $scope.$digest(); - expect($scope.limit).toBe(50); - }); - - test('should have a header and a table element', () => { - $scope.$digest(); - - expect($elem.find('thead').length).toBe(1); - expect($elem.find('table').length).toBe(1); - }); -}); diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.ts b/src/plugins/discover/public/application/angular/doc_table/doc_table.ts deleted file mode 100644 index 64c045a682296..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/doc_table.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import html from './doc_table.html'; -import { dispatchRenderComplete } from '../../../../../kibana_utils/public'; -import { SAMPLE_SIZE_SETTING } from '../../../../common'; -// @ts-expect-error -import { getLimitedSearchResultsMessage } from './doc_table_strings'; -import { getServices } from '../../../kibana_services'; -import './index.scss'; - -export interface LazyScope extends ng.IScope { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createDocTableDirective(pagerFactory: any, $filter: any) { - return { - restrict: 'E', - template: html, - scope: { - sorting: '=', - columns: '=', - hits: '=', - totalHitCount: '=', - indexPattern: '=', - isLoading: '=?', - infiniteScroll: '=?', - filter: '=?', - minimumVisibleRows: '=?', - onAddColumn: '=?', - onChangeSortOrder: '=?', - onMoveColumn: '=?', - onRemoveColumn: '=?', - inspectorAdapters: '=?', - useNewFieldsApi: '<', - }, - link: ($scope: LazyScope, $el: JQuery) => { - $scope.persist = { - sorting: $scope.sorting, - columns: $scope.columns, - }; - - const limitTo = $filter('limitTo'); - const calculateItemsOnPage = () => { - $scope.pager.setTotalItems($scope.hits.length); - $scope.pageOfItems = limitTo($scope.hits, $scope.pager.pageSize, $scope.pager.startIndex); - }; - - $scope.limitedResultsWarning = getLimitedSearchResultsMessage( - getServices().uiSettings.get(SAMPLE_SIZE_SETTING, 500) - ); - - $scope.addRows = function () { - $scope.limit += 50; - }; - - $scope.$watch('minimumVisibleRows', (minimumVisibleRows: number) => { - $scope.limit = Math.max(minimumVisibleRows || 50, $scope.limit || 50); - }); - - $scope.$watch('hits', (hits: unknown[]) => { - if (!hits) return; - - // Reset infinite scroll limit - $scope.limit = $scope.minimumVisibleRows || 50; - - if (hits.length === 0) { - dispatchRenderComplete($el[0]); - } - - if ($scope.infiniteScroll) return; - $scope.pager = pagerFactory.create(hits.length, 50, 1); - calculateItemsOnPage(); - }); - - $scope.pageOfItems = []; - $scope.onPageNext = () => { - $scope.pager.nextPage(); - calculateItemsOnPage(); - }; - - $scope.onPagePrevious = () => { - $scope.pager.previousPage(); - calculateItemsOnPage(); - }; - - $scope.shouldShowLimitedResultsWarning = () => - !$scope.pager.hasNextPage && $scope.pager.totalItems < $scope.totalHitCount; - }, - }; -} diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table_strings.js b/src/plugins/discover/public/application/angular/doc_table/doc_table_strings.js deleted file mode 100644 index aac4b9cfe155d..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/doc_table_strings.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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; - -/** - * A message letting the user know the results that have been retrieved is limited - * to a certain size. - * @param resultCount {Number} - */ -export function getLimitedSearchResultsMessage(resultCount) { - return i18n.translate('discover.docTable.limitedSearchResultLabel', { - defaultMessage: 'Limited to {resultCount} results. Refine your search.', - values: { resultCount }, - }); -} diff --git a/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts b/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts deleted file mode 100644 index 2029354376f26..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import $ from 'jquery'; - -interface LazyScope extends ng.IScope { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; -} - -export function createInfiniteScrollDirective() { - return { - restrict: 'E', - scope: { - more: '=', - }, - link: ($scope: LazyScope, $element: JQuery) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let checkTimer: any; - /** - * depending on which version of Discover is displayed, different elements are scrolling - * and have therefore to be considered for calculation of infinite scrolling - */ - const scrollDiv = $element.parents('.dscTable'); - const scrollDivMobile = $(window); - - function onScroll() { - if (!$scope.more) return; - const isMobileView = document.getElementsByClassName('dscSidebar__mobile').length > 0; - const usedScrollDiv = isMobileView ? scrollDivMobile : scrollDiv; - const scrollTop = usedScrollDiv.scrollTop(); - const scrollOffset = usedScrollDiv.prop('offsetTop') || 0; - - const winHeight = Number(usedScrollDiv.height()); - const winBottom = Number(winHeight) + Number(scrollTop); - const elTop = $element.get(0).offsetTop || 0; - const remaining = elTop - scrollOffset - winBottom; - - if (remaining <= winHeight) { - $scope[$scope.$$phase ? '$eval' : '$apply'](function () { - $scope.more(); - }); - } - } - - function scheduleCheck() { - if (checkTimer) return; - checkTimer = setTimeout(function () { - checkTimer = null; - onScroll(); - }, 50); - } - - scrollDiv.on('scroll', scheduleCheck); - window.addEventListener('scroll', scheduleCheck); - $scope.$on('$destroy', function () { - clearTimeout(checkTimer); - scrollDiv.off('scroll', scheduleCheck); - window.removeEventListener('scroll', scheduleCheck); - }); - scheduleCheck(); - }, - }; -} diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/pager/index.js b/src/plugins/discover/public/application/angular/doc_table/lib/pager/index.js deleted file mode 100644 index db99dbe76d99f..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/lib/pager/index.js +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import './pager_factory'; -export { Pager } from './pager'; diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager.js b/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager.js deleted file mode 100644 index 1bd27a8854ca3..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager.js +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -function clamp(val, min, max) { - return Math.min(Math.max(min, val), max); -} - -export class Pager { - constructor(totalItems, pageSize, startingPage) { - this.currentPage = startingPage; - this.totalItems = totalItems; - this.pageSize = pageSize; - this.startIndex = 0; - this.updateMeta(); - } - - get pageCount() { - return Math.ceil(this.totalItems / this.pageSize); - } - - get hasNextPage() { - return this.currentPage < this.totalPages; - } - - get hasPreviousPage() { - return this.currentPage > 1; - } - - nextPage() { - this.currentPage += 1; - this.updateMeta(); - } - - previousPage() { - this.currentPage -= 1; - this.updateMeta(); - } - - setTotalItems(count) { - this.totalItems = count; - this.updateMeta(); - } - - setPageSize(count) { - this.pageSize = count; - this.updateMeta(); - } - - updateMeta() { - this.totalPages = Math.ceil(this.totalItems / this.pageSize); - this.currentPage = clamp(this.currentPage, 1, this.totalPages); - - this.startItem = (this.currentPage - 1) * this.pageSize + 1; - this.startItem = clamp(this.startItem, 0, this.totalItems); - - this.endItem = this.startItem - 1 + this.pageSize; - this.endItem = clamp(this.endItem, 0, this.totalItems); - - this.startIndex = this.startItem - 1; - } -} diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager_factory.ts b/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager_factory.ts deleted file mode 100644 index 7cd36d419969e..0000000000000 --- a/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager_factory.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -// @ts-expect-error -import { Pager } from './pager'; - -export function createPagerFactory() { - return { - create(...args: unknown[]) { - return new Pager(...args); - }, - }; -} diff --git a/src/plugins/discover/public/application/angular/get_inner_angular.ts b/src/plugins/discover/public/application/angular/get_inner_angular.ts index 992d82795302b..5f459c369ce4d 100644 --- a/src/plugins/discover/public/application/angular/get_inner_angular.ts +++ b/src/plugins/discover/public/application/angular/get_inner_angular.ts @@ -19,19 +19,8 @@ import { CoreStart, PluginInitializerContext } from 'kibana/public'; import { DataPublicPluginStart } from '../../../../data/public'; import { Storage } from '../../../../kibana_utils/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../navigation/public'; -import { createDocTableDirective } from './doc_table'; -import { createTableHeaderDirective } from './doc_table/components/table_header'; -import { - createToolBarPagerButtonsDirective, - createToolBarPagerTextDirective, -} from './doc_table/components/pager'; import { createContextAppLegacy } from '../components/context_app/context_app_legacy_directive'; -import { createTableRowDirective } from './doc_table/components/table_row'; -import { createPagerFactory } from './doc_table/lib/pager/pager_factory'; -import { createInfiniteScrollDirective } from './doc_table/infinite_scroll'; -import { createDocViewerDirective } from './doc_viewer'; import { createDiscoverGridDirective } from './create_discover_grid_directive'; -import { createRenderCompleteDirective } from './directives/render_complete'; import { configureAppAngularModule, PrivateProvider, @@ -83,7 +72,6 @@ export function initializeInnerAngularModule( createLocalPrivateModule(); createLocalPromiseModule(); createLocalStorageModule(); - createPagerFactoryModule(); createDocTableModule(); initialized = true; } @@ -97,12 +85,10 @@ export function initializeInnerAngularModule( 'discoverI18n', 'discoverPrivate', 'discoverDocTable', - 'discoverPagerFactory', 'discoverPromise', ]) .config(watchMultiDecorator) - .directive('icon', (reactDirective) => reactDirective(EuiIcon)) - .directive('renderComplete', createRenderCompleteDirective); + .directive('icon', (reactDirective) => reactDirective(EuiIcon)); } return angular @@ -116,11 +102,9 @@ export function initializeInnerAngularModule( 'discoverPromise', 'discoverLocalStorageProvider', 'discoverDocTable', - 'discoverPagerFactory', ]) .config(watchMultiDecorator) .run(registerListenEventListener) - .directive('renderComplete', createRenderCompleteDirective) .directive('discover', createDiscoverDirective); } @@ -153,20 +137,9 @@ const createLocalStorageService = function (type: string) { }; }; -function createPagerFactoryModule() { - angular.module('discoverPagerFactory', []).factory('pagerFactory', createPagerFactory); -} - function createDocTableModule() { angular - .module('discoverDocTable', ['discoverPagerFactory', 'react']) - .directive('docTable', createDocTableDirective) - .directive('kbnTableHeader', createTableHeaderDirective) - .directive('toolBarPagerText', createToolBarPagerTextDirective) - .directive('kbnTableRow', createTableRowDirective) - .directive('toolBarPagerButtons', createToolBarPagerButtonsDirective) - .directive('kbnInfiniteScroll', createInfiniteScrollDirective) + .module('discoverDocTable', ['react']) .directive('discoverGrid', createDiscoverGridDirective) - .directive('docViewer', createDocViewerDirective) .directive('contextAppLegacy', createContextAppLegacy); } diff --git a/src/plugins/discover/public/application/angular/helpers/index.ts b/src/plugins/discover/public/application/angular/helpers/index.ts index 6a7f75b7e81a2..a7d9d4581d989 100644 --- a/src/plugins/discover/public/application/angular/helpers/index.ts +++ b/src/plugins/discover/public/application/angular/helpers/index.ts @@ -6,6 +6,5 @@ * Side Public License, v 1. */ -export { formatRow, formatTopLevelObject } from './row_formatter'; export { handleSourceColumnState } from './state_helpers'; export { PromiseServiceCreator } from './promises'; diff --git a/src/plugins/discover/public/application/angular/index.ts b/src/plugins/discover/public/application/angular/index.ts index e75add7910b74..c4f6415c771f9 100644 --- a/src/plugins/discover/public/application/angular/index.ts +++ b/src/plugins/discover/public/application/angular/index.ts @@ -14,5 +14,4 @@ import 'angular-route'; import './discover'; import './doc'; import './context'; -import './doc_viewer'; import './redirect'; diff --git a/src/plugins/discover/public/application/angular/doc_table/_doc_table.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/_doc_table.scss similarity index 86% rename from src/plugins/discover/public/application/angular/doc_table/_doc_table.scss rename to src/plugins/discover/public/application/apps/main/components/doc_table/_doc_table.scss index ead426fa9c4eb..add2d4e753c60 100644 --- a/src/plugins/discover/public/application/angular/doc_table/_doc_table.scss +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/_doc_table.scss @@ -2,7 +2,7 @@ * 1. Stack content vertically so the table can scroll when its constrained by a fixed container height. */ // stylelint-disable selector-no-qualifying-type -doc-table { +.kbnDocTableWrapper { @include euiScrollBar; overflow: auto; flex: 1 1 100%; @@ -21,6 +21,18 @@ doc-table { z-index: $euiZLevel1; opacity: .5; } + + // SASSTODO: add a monospace modifier to the doc-table component + .kbnDocTable__row { + font-family: $euiCodeFontFamily; + font-size: $euiFontSizeXS; + } +} + +.kbnDocTable__footer { + background-color: $euiColorLightShade; + padding: $euiSizeXS $euiSizeS; + text-align: center; } .kbnDocTable__container.loading { @@ -28,8 +40,6 @@ doc-table { } .kbnDocTable { - font-size: $euiFontSizeXS; - th { white-space: nowrap; padding-right: $euiSizeS; @@ -84,19 +94,6 @@ doc-table { } } -.kbnDocTable__bar { - margin: $euiSizeXS $euiSizeXS 0; -} - -.kbnDocTable__bar--footer { - position: relative; - margin: -($euiSize * 3) $euiSizeXS 0; -} - -.kbnDocTable__padBottom { - padding-bottom: $euiSizeXL; -} - .kbnDocTable__error { display: flex; flex-direction: column; diff --git a/src/plugins/discover/public/application/angular/doc_table/actions/columns.test.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.test.ts similarity index 86% rename from src/plugins/discover/public/application/angular/doc_table/actions/columns.test.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.test.ts index e1aa96f4625de..3b73044b68e07 100644 --- a/src/plugins/discover/public/application/angular/doc_table/actions/columns.test.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.test.ts @@ -7,11 +7,11 @@ */ import { getStateColumnActions } from './columns'; -import { configMock } from '../../../../__mocks__/config'; -import { indexPatternMock } from '../../../../__mocks__/index_pattern'; -import { indexPatternsMock } from '../../../../__mocks__/index_patterns'; -import { Capabilities } from '../../../../../../../core/types'; -import { AppState } from '../../../apps/main/services/discover_state'; +import { configMock } from '../../../../../../__mocks__/config'; +import { indexPatternMock } from '../../../../../../__mocks__/index_pattern'; +import { indexPatternsMock } from '../../../../../../__mocks__/index_patterns'; +import { Capabilities } from '../../../../../../../../../core/types'; +import { AppState } from '../../../services/discover_state'; function getStateColumnAction(state: {}, setAppState: (state: Partial) => void) { return getStateColumnActions({ diff --git a/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.ts similarity index 92% rename from src/plugins/discover/public/application/angular/doc_table/actions/columns.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.ts index 9ef5d45947afb..130b43539d9b5 100644 --- a/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/actions/columns.ts @@ -6,17 +6,17 @@ * Side Public License, v 1. */ import { Capabilities, IUiSettingsClient } from 'kibana/public'; -import { popularizeField } from '../../../helpers/popularize_field'; -import { IndexPattern, IndexPatternsContract } from '../../../../kibana_services'; +import { SORT_DEFAULT_ORDER_SETTING } from '../../../../../../../common'; +import { popularizeField } from '../../../../../../application/helpers/popularize_field'; import { AppState as DiscoverState, GetStateReturn as DiscoverGetStateReturn, -} from '../../../apps/main/services/discover_state'; +} from '../../../../../../application/apps/main/services/discover_state'; import { AppState as ContextState, GetStateReturn as ContextGetStateReturn, -} from '../../context_state'; -import { SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; +} from '../../../../../../application/angular/context_state'; +import { IndexPattern, IndexPatternsContract } from '../../../../../../../../data/public'; /** * Helper function to provide a fallback to a single _source column if the given array of columns diff --git a/src/plugins/discover/public/application/angular/doc_table/components/_index.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/components/_index.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/_index.scss rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/_index.scss diff --git a/src/plugins/discover/public/application/angular/doc_table/components/_table_header.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/components/_table_header.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/_table_header.scss rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/_table_header.scss diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/pager/tool_bar_pagination.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/pager/tool_bar_pagination.tsx new file mode 100644 index 0000000000000..878a9b8162628 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/pager/tool_bar_pagination.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import { + EuiButtonEmpty, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiPagination, + EuiPopover, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n/'; + +interface ToolBarPaginationProps { + pageSize: number; + pageCount: number; + activePage: number; + onPageClick: (page: number) => void; + onPageSizeChange: (size: number) => void; +} + +export const ToolBarPagination = ({ + pageSize, + pageCount, + activePage, + onPageSizeChange, + onPageClick, +}: ToolBarPaginationProps) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const rowsWord = i18n.translate('discover.docTable.rows', { + defaultMessage: 'rows', + }); + + const onChooseRowsClick = () => setIsPopoverOpen((prevIsPopoverOpen) => !prevIsPopoverOpen); + + const closePopover = () => setIsPopoverOpen(false); + + const getIconType = (size: number) => { + return size === pageSize ? 'check' : 'empty'; + }; + + const rowsPerPageOptions = [25, 50, 100].map((cur) => ( + { + closePopover(); + onPageSizeChange(cur); + }} + > + {cur} {rowsWord} + + )); + + return ( + + + + + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + > + + + + + + + + + ); +}; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/__snapshots__/table_header.test.tsx.snap diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/helpers.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/helpers.tsx similarity index 97% rename from src/plugins/discover/public/application/angular/doc_table/components/table_header/helpers.tsx rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/helpers.tsx index a75aea7169737..5afa26d35b4f5 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header/helpers.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/helpers.tsx @@ -7,7 +7,7 @@ */ import { i18n } from '@kbn/i18n'; -import { IndexPattern } from '../../../../../kibana_services'; +import { IndexPattern } from '../../../../../../../kibana_services'; export type SortOrder = [string, string]; export interface ColumnProps { diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.test.tsx similarity index 99% rename from src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.test.tsx index 48ea7ffc46384..7b72e94169cfe 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.test.tsx @@ -11,7 +11,7 @@ import { mountWithIntl } from '@kbn/test/jest'; import { TableHeader } from './table_header'; import { findTestSubject } from '@elastic/eui/lib/test'; import { SortOrder } from './helpers'; -import { IndexPattern, IndexPatternField } from '../../../../../kibana_services'; +import { IndexPattern, IndexPatternField } from '../../../../../../../kibana_services'; function getMockIndexPattern() { return ({ diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.tsx similarity index 96% rename from src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.tsx rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.tsx index 57f7382bd98ca..cb8198f1d6d6a 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header.tsx @@ -7,7 +7,7 @@ */ import React from 'react'; -import { IndexPattern } from '../../../../../kibana_services'; +import { IndexPattern } from '../../../../../../../kibana_services'; import { TableHeaderColumn } from './table_header_column'; import { SortOrder, getDisplayedColumns } from './helpers'; import { getDefaultSort } from '../../lib/get_default_sort'; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header_column.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header_column.tsx similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header_column.tsx rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_header/table_header_column.tsx diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.test.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.test.tsx new file mode 100644 index 0000000000000..59ced9d5668a0 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.test.tsx @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { mountWithIntl, findTestSubject } from '@kbn/test/jest'; +import { TableRow, TableRowProps } from './table_row'; +import { setDocViewsRegistry, setServices } from '../../../../../../kibana_services'; +import { createFilterManagerMock } from '../../../../../../../../data/public/query/filter_manager/filter_manager.mock'; +import { DiscoverServices } from '../../../../../../build_services'; +import { indexPatternWithTimefieldMock } from '../../../../../../__mocks__/index_pattern_with_timefield'; +import { uiSettingsMock } from '../../../../../../__mocks__/ui_settings'; +import { DocViewsRegistry } from '../../../../../doc_views/doc_views_registry'; + +jest.mock('../lib/row_formatter', () => { + const originalModule = jest.requireActual('../lib/row_formatter'); + return { + ...originalModule, + formatRow: () => mocked_document_cell, + }; +}); + +const mountComponent = (props: TableRowProps) => { + return mountWithIntl( + + + + +
+ ); +}; + +const mockHit = { + _index: 'mock_index', + _id: '1', + _score: 1, + _type: '_doc', + fields: [ + { + timestamp: '2020-20-01T12:12:12.123', + }, + ], + _source: { message: 'mock_message', bytes: 20 }, +}; + +const mockFilterManager = createFilterManagerMock(); + +describe('Doc table row component', () => { + let mockInlineFilter; + let defaultProps: TableRowProps; + + beforeEach(() => { + mockInlineFilter = jest.fn(); + + defaultProps = ({ + columns: ['_source'], + filter: mockInlineFilter, + indexPattern: indexPatternWithTimefieldMock, + row: mockHit, + useNewFieldsApi: true, + filterManager: mockFilterManager, + addBasePath: (path: string) => path, + hideTimeColumn: true, + } as unknown) as TableRowProps; + + setServices(({ + uiSettings: uiSettingsMock, + } as unknown) as DiscoverServices); + + setDocViewsRegistry(new DocViewsRegistry()); + }); + + it('should render __document__ column', () => { + const component = mountComponent({ ...defaultProps, columns: [] }); + const docTableField = findTestSubject(component, 'docTableField'); + expect(docTableField.first().text()).toBe('mocked_document_cell'); + }); + + it('should render message, _index and bytes fields', () => { + const component = mountComponent({ ...defaultProps, columns: ['message', '_index', 'bytes'] }); + + const fields = findTestSubject(component, 'docTableField'); + expect(fields.first().text()).toBe('mock_message'); + expect(fields.last().text()).toBe('20'); + expect(fields.length).toBe(3); + }); + + describe('details row', () => { + it('should be empty by default', () => { + const component = mountComponent(defaultProps); + expect(findTestSubject(component, 'docTableRowDetailsTitle').exists()).toBeFalsy(); + }); + + it('should expand the detail row when the toggle arrow is clicked', () => { + const component = mountComponent(defaultProps); + const toggleButton = findTestSubject(component, 'docTableExpandToggleColumn'); + + expect(findTestSubject(component, 'docTableRowDetailsTitle').exists()).toBeFalsy(); + toggleButton.simulate('click'); + expect(findTestSubject(component, 'docTableRowDetailsTitle').exists()).toBeTruthy(); + }); + }); +}); diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.tsx new file mode 100644 index 0000000000000..886aeffc06667 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.tsx @@ -0,0 +1,204 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { Fragment, useCallback, useMemo, useState } from 'react'; +import classNames from 'classnames'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonEmpty, EuiIcon } from '@elastic/eui'; +import { DocViewer } from '../../../../../components/doc_viewer/doc_viewer'; +import { FilterManager, IndexPattern } from '../../../../../../../../data/public'; +import { TableCell } from './table_row/table_cell'; +import { ElasticSearchHit, DocViewFilterFn } from '../../../../../doc_views/doc_views_types'; +import { trimAngularSpan } from '../../../../../components/table/table_helper'; +import { getContextUrl } from '../../../../../helpers/get_context_url'; +import { getSingleDocUrl } from '../../../../../helpers/get_single_doc_url'; +import { TableRowDetails } from './table_row_details'; +import { formatRow, formatTopLevelObject } from '../lib/row_formatter'; + +export type DocTableRow = ElasticSearchHit & { + isAnchor?: boolean; +}; + +export interface TableRowProps { + columns: string[]; + filter: DocViewFilterFn; + indexPattern: IndexPattern; + row: DocTableRow; + onAddColumn?: (column: string) => void; + onRemoveColumn?: (column: string) => void; + useNewFieldsApi: boolean; + hideTimeColumn: boolean; + filterManager: FilterManager; + addBasePath: (path: string) => string; +} + +export const TableRow = ({ + columns, + filter, + row, + indexPattern, + useNewFieldsApi, + hideTimeColumn, + onAddColumn, + onRemoveColumn, + filterManager, + addBasePath, +}: TableRowProps) => { + const [open, setOpen] = useState(false); + const docTableRowClassName = classNames('kbnDocTable__row', { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'kbnDocTable__row--highlight': row.isAnchor, + }); + const anchorDocTableRowSubj = row.isAnchor ? ' docTableAnchorRow' : ''; + + const flattenedRow = useMemo(() => indexPattern.flattenHit(row), [indexPattern, row]); + const mapping = useMemo(() => indexPattern.fields.getByName, [indexPattern]); + + // toggle display of the rows details, a full list of the fields from each row + const toggleRow = () => setOpen((prevOpen) => !prevOpen); + + /** + * Fill an element with the value of a field + */ + const displayField = (fieldName: string) => { + const text = indexPattern.formatField(row, fieldName); + const formattedField = trimAngularSpan(String(text)); + + // field formatters take care of escaping + // eslint-disable-next-line react/no-danger + const fieldElement = ; + + return
{fieldElement}
; + }; + const inlineFilter = useCallback( + (column: string, type: '+' | '-') => { + const field = indexPattern.fields.getByName(column); + filter(field!, flattenedRow[column], type); + }, + [filter, flattenedRow, indexPattern.fields] + ); + + const getContextAppHref = () => { + return getContextUrl(row._id, indexPattern.id!, columns, filterManager, addBasePath); + }; + + const getSingleDocHref = () => { + return addBasePath(getSingleDocUrl(indexPattern.id!, row._index, row._id)); + }; + + const rowCells = [ + + + {open ? ( + + ) : ( + + )} + + , + ]; + + if (indexPattern.timeFieldName && !hideTimeColumn) { + rowCells.push( + + ); + } + + if (columns.length === 0 && useNewFieldsApi) { + const formatted = formatRow(row, indexPattern); + + rowCells.push( + + ); + } else { + columns.forEach(function (column: string) { + // when useNewFieldsApi is true, addressing to the fields property is safe + if (useNewFieldsApi && !mapping(column) && !row.fields![column]) { + const innerColumns = Object.fromEntries( + Object.entries(row.fields!).filter(([key]) => { + return key.indexOf(`${column}.`) === 0; + }) + ); + + rowCells.push( + + ); + } else { + const isFilterable = Boolean(mapping(column)?.filterable && filter); + rowCells.push( + + ); + } + }); + } + + return ( + + + {rowCells} + + + + + + + + ); +}; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/__snapshots__/table_cell.test.tsx.snap b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/__snapshots__/table_cell.test.tsx.snap new file mode 100644 index 0000000000000..5f3564174adf8 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/__snapshots__/table_cell.test.tsx.snap @@ -0,0 +1,183 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Doc table cell component renders a cell with filter buttons if it is filterable 1`] = ` + + formatted content +
+ } + inlineFilter={[Function]} + sourcefield={false} + timefield={true} +> + + + formatted content + + + + + + + + + + + + + + + + + +`; + +exports[`Doc table cell component renders a cell without filter buttons if it is not filterable 1`] = ` + + formatted content + + } + inlineFilter={[Function]} + sourcefield={false} + timefield={true} +> + + + formatted content + + + + +`; + +exports[`Doc table cell component renders a field that is neither a timefield or sourcefield 1`] = ` + + formatted content + + } + inlineFilter={[Function]} + sourcefield={false} + timefield={false} +> + + + formatted content + + + + +`; + +exports[`Doc table cell component renders a sourcefield 1`] = ` + + formatted content + + } + inlineFilter={[Function]} + sourcefield={true} + timefield={false} +> + + + formatted content + + + + +`; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/_cell.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_cell.scss similarity index 64% rename from src/plugins/discover/public/application/angular/doc_table/components/table_row/_cell.scss rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_cell.scss index e175a2f3383e2..2e643c195208c 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/_cell.scss +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_cell.scss @@ -3,7 +3,15 @@ } .kbnDocTableCell__toggleDetails { - padding: $euiSizeXS 0 0 0!important; + padding: $euiSizeXS 0 0 0 !important; +} + +/** + * Fixes time column width in Firefox after toggle display of the rows details. + * Described issue - https://github.com/elastic/kibana/pull/104361#issuecomment-894271241 + */ +.kbnDocTableCell--extraWidth { + width: 1%; } .kbnDocTableCell__filter { @@ -12,6 +20,11 @@ right: 0; } +.kbnDocTableCell__filterButton { + font-size: $euiFontSizeXS; + padding: $euiSizeXS; +} + /** * 1. Align icon with text in cell. * 2. Use opacity to make this element accessible to screen readers and keyboard. diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/_details.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_details.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_row/_details.scss rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_details.scss diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/_index.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_index.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_row/_index.scss rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_index.scss diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/_open.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_open.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/components/table_row/_open.scss rename to src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/_open.scss diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell.test.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell.test.tsx new file mode 100644 index 0000000000000..316c2b27357a9 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell.test.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { CellProps, TableCell } from './table_cell'; + +const mountComponent = (props: Omit) => { + return mount( {}} />); +}; + +describe('Doc table cell component', () => { + test('renders a cell without filter buttons if it is not filterable', () => { + const component = mountComponent({ + filterable: false, + column: 'foo', + timefield: true, + sourcefield: false, + formatted: formatted content, + }); + expect(component).toMatchSnapshot(); + }); + + it('renders a cell with filter buttons if it is filterable', () => { + expect( + mountComponent({ + filterable: true, + column: 'foo', + timefield: true, + sourcefield: false, + formatted: formatted content, + }) + ).toMatchSnapshot(); + }); + + it('renders a sourcefield', () => { + expect( + mountComponent({ + filterable: false, + column: 'foo', + timefield: false, + sourcefield: true, + formatted: formatted content, + }) + ).toMatchSnapshot(); + }); + + it('renders a field that is neither a timefield or sourcefield', () => { + expect( + mountComponent({ + filterable: false, + column: 'foo', + timefield: false, + sourcefield: false, + formatted: formatted content, + }) + ).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell.tsx new file mode 100644 index 0000000000000..ad2368439d6d8 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell.tsx @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import classNames from 'classnames'; +import { TableCellActions } from './table_cell_actions'; +export interface CellProps { + timefield: boolean; + sourcefield?: boolean; + formatted: JSX.Element; + filterable: boolean; + column: string; + inlineFilter: (column: string, type: '+' | '-') => void; +} + +export const TableCell = (props: CellProps) => { + const classes = classNames({ + ['eui-textNoWrap kbnDocTableCell--extraWidth']: props.timefield, + ['eui-textBreakAll eui-textBreakWord']: props.sourcefield, + ['kbnDocTableCell__dataField eui-textBreakAll eui-textBreakWord']: + !props.timefield && !props.sourcefield, + }); + + const handleFilterFor = () => props.inlineFilter(props.column, '+'); + const handleFilterOut = () => props.inlineFilter(props.column, '-'); + + return ( + + {props.formatted} + {props.filterable ? ( + + ) : ( + + )} + + ); +}; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell_actions.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell_actions.tsx new file mode 100644 index 0000000000000..f252c8d801399 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row/table_cell_actions.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiIcon, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface TableCellActionsProps { + handleFilterFor: () => void; + handleFilterOut: () => void; +} + +export const TableCellActions = ({ handleFilterFor, handleFilterOut }: TableCellActionsProps) => { + return ( + + + + + + + + + + ); +}; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row_details.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row_details.tsx new file mode 100644 index 0000000000000..c3ff53fe2d3a8 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row_details.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiTitle } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +interface TableRowDetailsProps { + open: boolean; + colLength: number; + isTimeBased: boolean; + getContextAppHref: () => string; + getSingleDocHref: () => string; + children: JSX.Element; +} + +export const TableRowDetails = ({ + open, + colLength, + isTimeBased, + getContextAppHref, + getSingleDocHref, + children, +}: TableRowDetailsProps) => { + if (!open) { + return null; + } + + return ( + + + + + + + + + +

+ +

+
+
+
+
+ + + + {isTimeBased && ( + + + + )} + + + + + + + + +
+
{children}
+ + ); +}; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/create_doc_table_embeddable.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/create_doc_table_embeddable.tsx new file mode 100644 index 0000000000000..c745fbf64d294 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/create_doc_table_embeddable.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { I18nProvider } from '@kbn/i18n/react'; +import { DocTableEmbeddable, DocTableEmbeddableProps } from './doc_table_embeddable'; + +export function DiscoverDocTableEmbeddable(renderProps: DocTableEmbeddableProps) { + return ( + + + + ); +} diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_context.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_context.tsx new file mode 100644 index 0000000000000..8d29efec73716 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_context.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { Fragment } from 'react'; +import './index.scss'; +import { SkipBottomButton } from '../skip_bottom_button'; +import { DocTableProps, DocTableRenderProps, DocTableWrapper } from './doc_table_wrapper'; + +const DocTableWrapperMemoized = React.memo(DocTableWrapper); + +const renderDocTable = (tableProps: DocTableRenderProps) => { + return ( + + + + {tableProps.renderHeader()} + {tableProps.renderRows(tableProps.rows)} +
+ + ​ + +
+ ); +}; + +export const DocTableContext = (props: DocTableProps) => { + return ; +}; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_embeddable.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_embeddable.tsx new file mode 100644 index 0000000000000..04902af692b74 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_embeddable.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { memo, useCallback, useMemo } from 'react'; +import './index.scss'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { SAMPLE_SIZE_SETTING } from '../../../../../../common'; +import { usePager } from './lib/use_pager'; +import { ToolBarPagination } from './components/pager/tool_bar_pagination'; +import { DocTableProps, DocTableRenderProps, DocTableWrapper } from './doc_table_wrapper'; +import { TotalDocuments } from '../total_documents/total_documents'; +import { getServices } from '../../../../../kibana_services'; + +export interface DocTableEmbeddableProps extends DocTableProps { + totalHitCount: number; +} + +const DocTableWrapperMemoized = memo(DocTableWrapper); + +export const DocTableEmbeddable = (props: DocTableEmbeddableProps) => { + const pager = usePager({ totalItems: props.rows.length }); + + const pageOfItems = useMemo( + () => props.rows.slice(pager.startIndex, pager.pageSize + pager.startIndex), + [pager.pageSize, pager.startIndex, props.rows] + ); + + const shouldShowLimitedResultsWarning = () => + !pager.hasNextPage && props.rows.length < props.totalHitCount; + + const scrollTop = () => { + const scrollDiv = document.querySelector('.kbnDocTableWrapper') as HTMLElement; + scrollDiv.scrollTo(0, 0); + }; + + const onPageChange = (page: number) => { + scrollTop(); + pager.onPageChange(page); + }; + + const onPageSizeChange = (size: number) => { + scrollTop(); + pager.onPageSizeChange(size); + }; + + const sampleSize = useMemo(() => { + return getServices().uiSettings.get(SAMPLE_SIZE_SETTING, 500); + }, []); + + const renderDocTable = useCallback( + (renderProps: DocTableRenderProps) => { + return ( +
+ + {renderProps.renderHeader()} + {renderProps.renderRows(pageOfItems)} +
+
+ ); + }, + [pageOfItems] + ); + + return ( + + + + {shouldShowLimitedResultsWarning() && ( + + + + + + )} + {props.totalHitCount !== 0 && ( + + + + )} + + + + + + + + + + + + ); +}; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_infinite.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_infinite.tsx new file mode 100644 index 0000000000000..8e9066151b368 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_infinite.tsx @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { Fragment, memo, useCallback, useEffect, useState } from 'react'; +import './index.scss'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { debounce } from 'lodash'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { DocTableProps, DocTableRenderProps, DocTableWrapper } from './doc_table_wrapper'; +import { SkipBottomButton } from '../skip_bottom_button'; + +const DocTableInfiniteContent = (props: DocTableRenderProps) => { + const [limit, setLimit] = useState(props.minimumVisibleRows); + + // Reset infinite scroll limit + useEffect(() => { + setLimit(props.minimumVisibleRows); + }, [props.rows, props.minimumVisibleRows]); + + /** + * depending on which version of Discover is displayed, different elements are scrolling + * and have therefore to be considered for calculation of infinite scrolling + */ + useEffect(() => { + const scrollDiv = document.querySelector('.kbnDocTableWrapper') as HTMLElement; + const scrollMobileElem = document.documentElement; + + const scheduleCheck = debounce(() => { + const isMobileView = document.getElementsByClassName('dscSidebar__mobile').length > 0; + const usedScrollDiv = isMobileView ? scrollMobileElem : scrollDiv; + + const scrollusedHeight = usedScrollDiv.scrollHeight; + const scrollTop = Math.abs(usedScrollDiv.scrollTop); + const clientHeight = usedScrollDiv.clientHeight; + + if (scrollTop + clientHeight === scrollusedHeight) { + setLimit((prevLimit) => prevLimit + 50); + } + }, 50); + + scrollDiv.addEventListener('scroll', scheduleCheck); + window.addEventListener('scroll', scheduleCheck); + + scheduleCheck(); + + return () => { + scrollDiv.removeEventListener('scroll', scheduleCheck); + window.removeEventListener('scroll', scheduleCheck); + }; + }, []); + + const onBackToTop = useCallback(() => { + const isMobileView = document.getElementsByClassName('dscSidebar__mobile').length > 0; + const focusElem = document.querySelector('.dscTable') as HTMLElement; + focusElem.focus(); + + // Only the desktop one needs to target a specific container + if (!isMobileView) { + const scrollDiv = document.querySelector('.kbnDocTableWrapper') as HTMLElement; + scrollDiv.scrollTo(0, 0); + } else if (window) { + window.scrollTo(0, 0); + } + }, []); + + return ( + + + + {props.renderHeader()} + {props.renderRows(props.rows.slice(0, limit))} +
+ {props.rows.length === props.sampleSize ? ( +
+ + + + +
+ ) : ( + + ​ + + )} +
+ ); +}; + +const DocTableWrapperMemoized = memo(DocTableWrapper); +const DocTableInfiniteContentMemoized = memo(DocTableInfiniteContent); + +const renderDocTable = (tableProps: DocTableRenderProps) => ( + +); + +export const DocTableInfinite = (props: DocTableProps) => { + return ; +}; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.test.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.test.tsx new file mode 100644 index 0000000000000..df5869bd61e52 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.test.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { findTestSubject, mountWithIntl } from '@kbn/test/jest'; +import { setServices } from '../../../../../kibana_services'; +import { indexPatternMock } from '../../../../../__mocks__/index_pattern'; +import { DocTableWrapper, DocTableWrapperProps } from './doc_table_wrapper'; +import { DocTableRow } from './components/table_row'; +import { discoverServiceMock } from '../../../../../__mocks__/services'; + +const mountComponent = (props: DocTableWrapperProps) => { + return mountWithIntl(); +}; + +describe('Doc table component', () => { + let defaultProps: DocTableWrapperProps; + + const initDefaults = (rows?: DocTableRow[]) => { + defaultProps = { + columns: ['_source'], + indexPattern: indexPatternMock, + rows: rows || [ + { + _index: 'mock_index', + _id: '1', + _score: 1, + _type: '_doc', + fields: [ + { + timestamp: '2020-20-01T12:12:12.123', + }, + ], + _source: { message: 'mock_message', bytes: 20 }, + }, + ], + sort: [['order_date', 'desc']], + isLoading: false, + searchDescription: '', + onAddColumn: () => {}, + onFilter: () => {}, + onMoveColumn: () => {}, + onRemoveColumn: () => {}, + onSort: () => {}, + useNewFieldsApi: true, + dataTestSubj: 'discoverDocTable', + render: () => { + return
mock
; + }, + }; + + setServices(discoverServiceMock); + }; + + it('should render infinite table correctly', () => { + initDefaults(); + const component = mountComponent(defaultProps); + expect(findTestSubject(component, defaultProps.dataTestSubj).exists()).toBeTruthy(); + expect(findTestSubject(component, 'docTable').exists()).toBeTruthy(); + expect(component.find('.kbnDocTable__error').exists()).toBeFalsy(); + }); + + it('should render error fallback if rows array is empty', () => { + initDefaults([]); + const component = mountComponent(defaultProps); + expect(findTestSubject(component, defaultProps.dataTestSubj).exists()).toBeTruthy(); + expect(findTestSubject(component, 'docTable').exists()).toBeFalsy(); + expect(component.find('.kbnDocTable__error').exists()).toBeTruthy(); + }); +}); diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.tsx new file mode 100644 index 0000000000000..c875bf155bd79 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/doc_table_wrapper.tsx @@ -0,0 +1,243 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback, useMemo, useState } from 'react'; +import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { TableHeader } from './components/table_header/table_header'; +import { FORMATS_UI_SETTINGS } from '../../../../../../../field_formats/common'; +import { + DOC_HIDE_TIME_COLUMN_SETTING, + SAMPLE_SIZE_SETTING, + SORT_DEFAULT_ORDER_SETTING, +} from '../../../../../../common'; +import { getServices, IndexPattern } from '../../../../../kibana_services'; +import { SortOrder } from './components/table_header/helpers'; +import { DocTableRow, TableRow } from './components/table_row'; +import { DocViewFilterFn } from '../../../../doc_views/doc_views_types'; + +export interface DocTableProps { + /** + * Rows of classic table + */ + rows: DocTableRow[]; + /** + * Columns of classic table + */ + columns: string[]; + /** + * Current IndexPattern + */ + indexPattern: IndexPattern; + /** + * Current sorting + */ + sort: string[][]; + /** + * New fields api switch + */ + useNewFieldsApi: boolean; + /** + * Current search description + */ + searchDescription?: string; + /** + * Current shared item title + */ + sharedItemTitle?: string; + /** + * Current data test subject + */ + dataTestSubj: string; + /** + * Loading state + */ + isLoading: boolean; + /** + * Filter callback + */ + onFilter: DocViewFilterFn; + /** + * Sorting callback + */ + onSort?: (sort: string[][]) => void; + /** + * Add columns callback + */ + onAddColumn?: (column: string) => void; + /** + * Reordering column callback + */ + onMoveColumn?: (columns: string, newIdx: number) => void; + /** + * Remove column callback + */ + onRemoveColumn?: (column: string) => void; +} + +export interface DocTableRenderProps { + rows: DocTableRow[]; + minimumVisibleRows: number; + sampleSize: number; + renderRows: (row: DocTableRow[]) => JSX.Element[]; + renderHeader: () => JSX.Element; + onSkipBottomButtonClick: () => void; +} + +export interface DocTableWrapperProps extends DocTableProps { + /** + * Renders Doc table content + */ + render: (params: DocTableRenderProps) => JSX.Element; +} + +export const DocTableWrapper = ({ + render, + columns, + rows, + indexPattern, + onSort, + onAddColumn, + onMoveColumn, + onRemoveColumn, + sort, + onFilter, + useNewFieldsApi, + searchDescription, + sharedItemTitle, + dataTestSubj, + isLoading, +}: DocTableWrapperProps) => { + const [minimumVisibleRows, setMinimumVisibleRows] = useState(50); + const [ + defaultSortOrder, + hideTimeColumn, + isShortDots, + sampleSize, + filterManager, + addBasePath, + ] = useMemo(() => { + const services = getServices(); + return [ + services.uiSettings.get(SORT_DEFAULT_ORDER_SETTING, 'desc'), + services.uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false), + services.uiSettings.get(FORMATS_UI_SETTINGS.SHORT_DOTS_ENABLE), + services.uiSettings.get(SAMPLE_SIZE_SETTING, 500), + services.filterManager, + services.addBasePath, + ]; + }, []); + + const onSkipBottomButtonClick = useCallback(async () => { + // delay scrolling to after the rows have been rendered + const bottomMarker = document.getElementById('discoverBottomMarker'); + const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + // show all the rows + setMinimumVisibleRows(rows.length); + + while (rows.length !== document.getElementsByClassName('kbnDocTable__row').length) { + await wait(50); + } + bottomMarker!.focus(); + await wait(50); + bottomMarker!.blur(); + }, [setMinimumVisibleRows, rows]); + + const renderHeader = useCallback( + () => ( + + ), + [ + columns, + defaultSortOrder, + hideTimeColumn, + indexPattern, + isShortDots, + onMoveColumn, + onRemoveColumn, + onSort, + sort, + ] + ); + + const renderRows = useCallback( + (rowsToRender: DocTableRow[]) => { + return rowsToRender.map((current) => ( + + )); + }, + [ + columns, + onFilter, + indexPattern, + useNewFieldsApi, + hideTimeColumn, + onAddColumn, + onRemoveColumn, + filterManager, + addBasePath, + ] + ); + + return ( +
+ {rows.length !== 0 && + render({ + rows, + minimumVisibleRows, + sampleSize, + onSkipBottomButtonClick, + renderHeader, + renderRows, + })} + {!rows.length && ( +
+ + + + + +
+ )} +
+ ); +}; diff --git a/src/plugins/discover/public/application/angular/doc_table/index.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/index.scss similarity index 100% rename from src/plugins/discover/public/application/angular/doc_table/index.scss rename to src/plugins/discover/public/application/apps/main/components/doc_table/index.scss diff --git a/src/plugins/discover/public/application/angular/doc_table/index.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/index.ts similarity index 90% rename from src/plugins/discover/public/application/angular/doc_table/index.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/index.ts index 3a8f170f8680d..513183cc99468 100644 --- a/src/plugins/discover/public/application/angular/doc_table/index.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/index.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -export { createDocTableDirective } from './doc_table'; export { getSort, getSortArray } from './lib/get_sort'; export { getSortForSearchSource } from './lib/get_sort_for_search_source'; export { getDefaultSort } from './lib/get_default_sort'; diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.test.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.test.ts similarity index 91% rename from src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.test.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.test.ts index f181d583f0211..b2c7499b4a040 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.test.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.test.ts @@ -8,8 +8,8 @@ import { getDefaultSort } from './get_default_sort'; // @ts-expect-error -import FixturesStubbedLogstashIndexPatternProvider from '../../../../__fixtures__/stubbed_logstash_index_pattern'; -import { IndexPattern } from '../../../../kibana_services'; +import FixturesStubbedLogstashIndexPatternProvider from '../../../../../../__fixtures__/stubbed_logstash_index_pattern'; +import { IndexPattern } from '../../../../../../kibana_services'; describe('getDefaultSort function', function () { let indexPattern: IndexPattern; diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.ts similarity index 93% rename from src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.ts index aa1cf4a61066d..e01ff0b00e2b0 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_default_sort.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { IndexPattern } from '../../../../kibana_services'; +import { IndexPattern } from '../../../../../../kibana_services'; import { isSortable } from './get_sort'; import { SortOrder } from '../components/table_header/helpers'; diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.test.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.test.ts similarity index 96% rename from src/plugins/discover/public/application/angular/doc_table/lib/get_sort.test.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.test.ts index 19d629e14da66..865ef1d3fb729 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.test.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.test.ts @@ -8,8 +8,8 @@ import { getSort, getSortArray } from './get_sort'; // @ts-expect-error -import FixturesStubbedLogstashIndexPatternProvider from '../../../../__fixtures__/stubbed_logstash_index_pattern'; -import { IndexPattern } from '../../../../kibana_services'; +import FixturesStubbedLogstashIndexPatternProvider from '../../../../../../__fixtures__/stubbed_logstash_index_pattern'; +import { IndexPattern } from '../../../../../../kibana_services'; describe('docTable', function () { let indexPattern: IndexPattern; diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.ts similarity index 97% rename from src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.ts index 4b16c1aa3dcc6..2c687a59ea291 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort.ts @@ -7,7 +7,7 @@ */ import _ from 'lodash'; -import { IndexPattern } from '../../../../../../data/public'; +import { IndexPattern } from '../../../../../../../../data/public'; export type SortPairObj = Record; export type SortPairArr = [string, string]; diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.test.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.test.ts similarity index 94% rename from src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.test.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.test.ts index dc7817d95dd38..3753597ced163 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.test.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.test.ts @@ -8,8 +8,8 @@ import { getSortForSearchSource } from './get_sort_for_search_source'; // @ts-expect-error -import FixturesStubbedLogstashIndexPatternProvider from '../../../../__fixtures__/stubbed_logstash_index_pattern'; -import { IndexPattern } from '../../../../kibana_services'; +import FixturesStubbedLogstashIndexPatternProvider from '../../../../../../__fixtures__/stubbed_logstash_index_pattern'; +import { IndexPattern } from '../../../../../../kibana_services'; import { SortOrder } from '../components/table_header/helpers'; describe('getSortForSearchSource function', function () { diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.ts similarity index 95% rename from src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.ts index 58a690b70529e..2bc8a71301df9 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/get_sort_for_search_source.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { EsQuerySortValue, IndexPattern } from '../../../../kibana_services'; +import { EsQuerySortValue, IndexPattern } from '../../../../../../kibana_services'; import { SortOrder } from '../components/table_header/helpers'; import { getSort } from './get_sort'; diff --git a/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.test.ts similarity index 53% rename from src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts rename to src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.test.ts index 6b356446850e6..8c108e7d4dcf6 100644 --- a/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.test.ts @@ -6,12 +6,13 @@ * Side Public License, v 1. */ +import ReactDOM from 'react-dom/server'; import { formatRow, formatTopLevelObject } from './row_formatter'; -import { stubbedSavedObjectIndexPattern } from '../../../__mocks__/stubbed_saved_object_index_pattern'; -import { IndexPattern } from '../../../../../data/common/index_patterns/index_patterns'; -import { fieldFormatsMock } from '../../../../../field_formats/common/mocks'; -import { setServices } from '../../../kibana_services'; -import { DiscoverServices } from '../../../build_services'; +import { stubbedSavedObjectIndexPattern } from '../../../../../../__mocks__/stubbed_saved_object_index_pattern'; +import { IndexPattern } from '../../../../../../../../data/common/index_patterns/index_patterns'; +import { fieldFormatsMock } from '../../../../../../../../field_formats/common/mocks'; +import { setServices } from '../../../../../../kibana_services'; +import { DiscoverServices } from '../../../../../../build_services'; describe('Row formatter', () => { const hit = { @@ -68,9 +69,42 @@ describe('Row formatter', () => { }); it('formats document properly', () => { - expect(formatRow(hit, indexPattern).trim()).toMatchInlineSnapshot( - `"
also:
with \\\\"quotes\\\\" or 'single qoutes'
foo:
bar
number:
42
hello:
<h1>World</h1>
_id:
a
_type:
doc
_score:
1
"` - ); + expect(formatRow(hit, indexPattern)).toMatchInlineSnapshot(` + + `); }); it('limits number of rendered items', () => { @@ -79,17 +113,57 @@ describe('Row formatter', () => { get: () => 1, }, } as unknown) as DiscoverServices); - expect(formatRow(hit, indexPattern).trim()).toMatchInlineSnapshot( - `"
also:
with \\\\"quotes\\\\" or 'single qoutes'
"` - ); + expect(formatRow(hit, indexPattern)).toMatchInlineSnapshot(` + + `); }); it('formats document with highlighted fields first', () => { - expect( - formatRow({ ...hit, highlight: { number: '42' } }, indexPattern).trim() - ).toMatchInlineSnapshot( - `"
number:
42
also:
with \\\\"quotes\\\\" or 'single qoutes'
foo:
bar
hello:
<h1>World</h1>
_id:
a
_type:
doc
_score:
1
"` - ); + expect(formatRow({ ...hit, highlight: { number: '42' } }, indexPattern)).toMatchInlineSnapshot(` + + `); }); it('formats top level objects using formatter', () => { @@ -111,10 +185,19 @@ describe('Row formatter', () => { getByName: jest.fn(), }, indexPattern - ).trim() - ).toMatchInlineSnapshot( - `"
object.value:
formatted, formatted
"` - ); + ) + ).toMatchInlineSnapshot(` + + `); }); it('formats top level objects in alphabetical order', () => { @@ -124,11 +207,13 @@ describe('Row formatter', () => { indexPattern.getFormatterForField = jest.fn().mockReturnValue({ convert: () => 'formatted', }); - const formatted = formatTopLevelObject( - { fields: { 'a.zzz': [100], 'a.ccc': [50] } }, - { 'a.zzz': [100], 'a.ccc': [50], getByName: jest.fn() }, - indexPattern - ).trim(); + const formatted = ReactDOM.renderToStaticMarkup( + formatTopLevelObject( + { fields: { 'a.zzz': [100], 'a.ccc': [50] } }, + { 'a.zzz': [100], 'a.ccc': [50], getByName: jest.fn() }, + indexPattern + ) + ); expect(formatted.indexOf('
a.ccc:
')).toBeLessThan(formatted.indexOf('
a.zzz:
')); }); @@ -156,10 +241,23 @@ describe('Row formatter', () => { getByName: jest.fn(), }, indexPattern - ).trim() - ).toMatchInlineSnapshot( - `"
object.keys:
formatted, formatted
object.value:
formatted, formatted
"` - ); + ) + ).toMatchInlineSnapshot(` + + `); }); it('formats top level objects, converting unknown fields to string', () => { @@ -177,9 +275,18 @@ describe('Row formatter', () => { getByName: jest.fn(), }, indexPattern - ).trim() - ).toMatchInlineSnapshot( - `"
object.value:
5, 10
"` - ); + ) + ).toMatchInlineSnapshot(` + + `); }); }); diff --git a/src/plugins/discover/public/application/angular/helpers/row_formatter.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.tsx similarity index 87% rename from src/plugins/discover/public/application/angular/helpers/row_formatter.tsx rename to src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.tsx index c410273cc7510..51e83f78f9f1c 100644 --- a/src/plugins/discover/public/application/angular/helpers/row_formatter.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.tsx @@ -7,9 +7,8 @@ */ import React, { Fragment } from 'react'; -import ReactDOM from 'react-dom/server'; -import { MAX_DOC_FIELDS_DISPLAYED } from '../../../../common'; -import { getServices, IndexPattern } from '../../../kibana_services'; +import { MAX_DOC_FIELDS_DISPLAYED } from '../../../../../../../common'; +import { getServices, IndexPattern } from '../../../../../../kibana_services'; interface Props { defPairs: Array<[string, unknown]>; @@ -44,9 +43,7 @@ export const formatRow = (hit: Record, indexPattern: IndexPattern) pairs.push([displayKey ? displayKey : key, val]); }); const maxEntries = getServices().uiSettings.get(MAX_DOC_FIELDS_DISPLAYED); - return ReactDOM.renderToStaticMarkup( - - ); + return ; }; export const formatTopLevelObject = ( @@ -80,7 +77,5 @@ export const formatTopLevelObject = ( pairs.push([displayKey ? displayKey : key, formatted]); }); const maxEntries = getServices().uiSettings.get(MAX_DOC_FIELDS_DISPLAYED); - return ReactDOM.renderToStaticMarkup( - - ); + return ; }; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/use_pager.ts b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/use_pager.ts new file mode 100644 index 0000000000000..5522e3c150213 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/use_pager.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { useCallback, useEffect, useState } from 'react'; + +interface MetaParams { + currentPage: number; + totalItems: number; + totalPages: number; + startIndex: number; + hasNextPage: boolean; + pageSize: number; +} + +interface ProvidedMeta { + updatedPageSize?: number; + updatedCurrentPage?: number; +} + +const INITIAL_PAGE_SIZE = 50; + +export const usePager = ({ totalItems }: { totalItems: number }) => { + const [meta, setMeta] = useState({ + currentPage: 0, + totalItems, + startIndex: 0, + totalPages: Math.ceil(totalItems / INITIAL_PAGE_SIZE), + hasNextPage: true, + pageSize: INITIAL_PAGE_SIZE, + }); + + const getNewMeta = useCallback( + (newMeta: ProvidedMeta) => { + const actualCurrentPage = newMeta.updatedCurrentPage ?? meta.currentPage; + const actualPageSize = newMeta.updatedPageSize ?? meta.pageSize; + + const newTotalPages = Math.ceil(totalItems / actualPageSize); + const newStartIndex = actualPageSize * actualCurrentPage; + + return { + currentPage: actualCurrentPage, + totalPages: newTotalPages, + startIndex: newStartIndex, + totalItems, + hasNextPage: meta.currentPage + 1 < meta.totalPages, + pageSize: actualPageSize, + }; + }, + [meta.currentPage, meta.pageSize, meta.totalPages, totalItems] + ); + + const onPageChange = useCallback( + (pageIndex: number) => setMeta(getNewMeta({ updatedCurrentPage: pageIndex })), + [getNewMeta] + ); + + const onPageSizeChange = useCallback( + (newPageSize: number) => + setMeta(getNewMeta({ updatedPageSize: newPageSize, updatedCurrentPage: 0 })), + [getNewMeta] + ); + + /** + * Update meta on totalItems change + */ + useEffect(() => setMeta(getNewMeta({})), [getNewMeta, totalItems]); + + return { + ...meta, + onPageChange, + onPageSizeChange, + }; +}; diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.test.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.test.tsx index 1136b693c9e74..e5212e877e8ba 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.test.tsx @@ -36,7 +36,6 @@ function getProps(fetchStatus: FetchStatus, hits: ElasticSearchHit[]) { return { expandedDoc: undefined, indexPattern: indexPatternMock, - isMobile: jest.fn(() => false), onAddFilter: jest.fn(), savedSearch: savedSearchMock, documents$, diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.tsx index 13cf021ff2573..e0e0c9c6f8831 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.tsx @@ -5,11 +5,15 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React, { useRef, useMemo, useCallback, memo } from 'react'; -import { EuiFlexItem, EuiSpacer, EuiText, EuiLoadingSpinner } from '@elastic/eui'; +import React, { useMemo, useCallback, memo } from 'react'; +import { + EuiFlexItem, + EuiSpacer, + EuiText, + EuiLoadingSpinner, + EuiScreenReaderOnly, +} from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { DocTableLegacy } from '../../../../angular/doc_table/create_doc_table_react'; -import { SortPairArr } from '../../../../angular/doc_table/lib/get_sort'; import { DocViewFilterFn, ElasticSearchHit } from '../../../../doc_views/doc_views_types'; import { DiscoverGrid } from '../../../../components/discover_grid/discover_grid'; import { FetchStatus } from '../../../../types'; @@ -26,15 +30,16 @@ import { DataDocumentsMsg, DataDocuments$ } from '../../services/use_saved_searc import { DiscoverServices } from '../../../../../build_services'; import { AppState, GetStateReturn } from '../../services/discover_state'; import { useDataState } from '../../utils/use_data_state'; +import { DocTableInfinite } from '../doc_table/doc_table_infinite'; +import { SortPairArr } from '../doc_table/lib/get_sort'; -const DocTableLegacyMemoized = React.memo(DocTableLegacy); +const DocTableInfiniteMemoized = React.memo(DocTableInfinite); const DataGridMemoized = React.memo(DiscoverGrid); function DiscoverDocumentsComponent({ documents$, expandedDoc, indexPattern, - isMobile, onAddFilter, savedSearch, services, @@ -45,7 +50,6 @@ function DiscoverDocumentsComponent({ documents$: DataDocuments$; expandedDoc?: ElasticSearchHit; indexPattern: IndexPattern; - isMobile: () => boolean; navigateTo: (url: string) => void; onAddFilter: DocViewFilterFn; savedSearch: SavedSearch; @@ -57,11 +61,11 @@ function DiscoverDocumentsComponent({ const { capabilities, indexPatterns, uiSettings } = services; const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]); - const scrollableDesktop = useRef(null); const isLegacy = useMemo(() => uiSettings.get(DOC_TABLE_LEGACY), [uiSettings]); const sampleSize = useMemo(() => uiSettings.get(SAMPLE_SIZE_SETTING), [uiSettings]); const documentState: DataDocumentsMsg = useDataState(documents$); + const isLoading = documentState.fetchStatus === FetchStatus.LOADING; const rows = useMemo(() => documentState.result || [], [documentState.result]); @@ -75,21 +79,6 @@ function DiscoverDocumentsComponent({ useNewFieldsApi, }); - /** - * Legacy function, remove once legacy grid is removed - */ - const onBackToTop = useCallback(() => { - if (scrollableDesktop && scrollableDesktop.current) { - scrollableDesktop.current.focus(); - } - // Only the desktop one needs to target a specific container - if (!isMobile() && scrollableDesktop.current) { - scrollableDesktop.current.scrollTo(0, 0); - } else if (window) { - window.scrollTo(0, 0); - } - }, [scrollableDesktop, isMobile]); - const onResize = useCallback( (colSettings: { columnId: string; width: number }) => { const grid = { ...state.grid } || {}; @@ -131,62 +120,57 @@ function DiscoverDocumentsComponent({ } return ( - -
-

+ + +

- {isLegacy && rows && rows.length && ( - + {isLegacy && rows && rows.length && ( + + )} + {!isLegacy && ( +
+ - )} - {!isLegacy && ( -
- -
- )} -

+
+ )} ); } diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.scss b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.scss index 485bdc65c6cb6..2401325dd76f2 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.scss +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.scss @@ -84,17 +84,8 @@ discover-app { } .dscTable { - // SASSTODO: add a monospace modifier to the doc-table component - .kbnDocTable__row { - font-family: $euiCodeFontFamily; - font-size: $euiFontSizeXS; - } -} - -.dscTable__footer { - background-color: $euiColorLightShade; - padding: $euiSizeXS $euiSizeS; - text-align: center; + // needs for scroll container of lagacy table + min-height: 0; } .dscDocuments__loading { diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx index 8930722e813ce..94e28c3f1d54c 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import './discover_layout.scss'; -import React, { useState, useRef, useMemo, useCallback, useEffect } from 'react'; +import React, { useState, useMemo, useCallback, useEffect, useRef } from 'react'; import { EuiSpacer, EuiButtonIcon, @@ -66,13 +66,11 @@ export function DiscoverLayout({ stateContainer, }: DiscoverLayoutProps) { const { trackUiMetric, capabilities, indexPatterns, data, uiSettings, filterManager } = services; + const { main$, charts$, totalHits$ } = savedSearchData$; const [expandedDoc, setExpandedDoc] = useState(undefined); const [inspectorSession, setInspectorSession] = useState(undefined); - const collapseIcon = useRef(null); const fetchCounter = useRef(0); - const { main$, charts$, totalHits$ } = savedSearchData$; - const dataState: DataMainMsg = useDataState(main$); useEffect(() => { @@ -81,8 +79,6 @@ export function DiscoverLayout({ } }, [dataState.fetchStatus]); - // collapse icon isn't displayed in mobile view, use it to detect which view is displayed - const isMobile = useCallback(() => collapseIcon && !collapseIcon.current, []); const timeField = useMemo(() => { return indexPatternsUtils.isDefault(indexPattern) ? indexPattern.timeFieldName : undefined; }, [indexPattern]); @@ -208,7 +204,6 @@ export function DiscoverLayout({ aria-label={i18n.translate('discover.toggleSidebarAriaLabel', { defaultMessage: 'Toggle sidebar', })} - buttonRef={collapseIcon} />
@@ -261,11 +256,11 @@ export function DiscoverLayout({ /> + { + return ( + + {totalHitCount}, + }} + /> + + ); +}; diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts index 1bf9710def03c..a5a064a8fc1c6 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts +++ b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts @@ -24,7 +24,7 @@ import { import { useSearchSession } from './use_search_session'; import { FetchStatus } from '../../../types'; import { getSwitchIndexPatternAppState } from '../utils/get_switch_index_pattern_app_state'; -import { SortPairArr } from '../../../angular/doc_table/lib/get_sort'; +import { SortPairArr } from '../components/doc_table/lib/get_sort'; export function useDiscoverState({ services, diff --git a/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts b/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts index 00473956c57e3..225d90c61de12 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts @@ -10,8 +10,8 @@ import type { Capabilities, IUiSettingsClient } from 'kibana/public'; import { ISearchSource } from '../../../../../../data/common'; import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; import type { SavedSearch, SortOrder } from '../../../../saved_searches/types'; +import { getSortForSearchSource } from '../components/doc_table'; import { AppState } from '../services/discover_state'; -import { getSortForSearchSource } from '../../../angular/doc_table'; /** * Preparing data to share the current state as link or CSV/Report diff --git a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts index 426272fa8ce1c..fc835d4d3dd16 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_state_defaults.ts @@ -9,12 +9,11 @@ import { cloneDeep } from 'lodash'; import { IUiSettingsClient } from 'kibana/public'; import { DEFAULT_COLUMNS_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; -import { getSortArray } from '../../../angular/doc_table'; -import { getDefaultSort } from '../../../angular/doc_table/lib/get_default_sort'; import { SavedSearch } from '../../../../saved_searches'; import { DataPublicPluginStart } from '../../../../../../data/public'; import { AppState } from '../services/discover_state'; +import { getDefaultSort, getSortArray } from '../components/doc_table'; function getDefaultColumns(savedSearch: SavedSearch, config: IUiSettingsClient) { if (savedSearch.columns && savedSearch.columns.length > 0) { diff --git a/src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.ts b/src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.ts index f7154b26c7ed6..00f194662e410 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_switch_index_pattern_app_state.ts @@ -6,9 +6,8 @@ * Side Public License, v 1. */ -import { getSortArray } from '../../../angular/doc_table'; -import { SortPairArr } from '../../../angular/doc_table/lib/get_sort'; import { IndexPattern } from '../../../../kibana_services'; +import { getSortArray, SortPairArr } from '../components/doc_table/lib/get_sort'; /** * Helper function to remove or adapt the currently selected columns/sort to be valid with the next diff --git a/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts b/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts index 3fac75a198d53..b4a1dab41a096 100644 --- a/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts +++ b/src/plugins/discover/public/application/apps/main/utils/update_search_source.ts @@ -6,12 +6,12 @@ * Side Public License, v 1. */ -import { getSortForSearchSource } from '../../../angular/doc_table'; import { SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; import { IndexPattern, ISearchSource } from '../../../../../../data/common'; import { SortOrder } from '../../../../saved_searches/types'; import { DiscoverServices } from '../../../../build_services'; import { indexPatterns as indexPatternsUtils } from '../../../../../../data/public'; +import { getSortForSearchSource } from '../components/doc_table'; /** * Helper function to update the given searchSource before fetching/sharing/persisting diff --git a/src/plugins/discover/public/application/components/context_app/context_app.tsx b/src/plugins/discover/public/application/components/context_app/context_app.tsx index c52f22c60bb5b..37963eb2dfa93 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app.tsx @@ -25,6 +25,7 @@ import { useContextAppFetch } from './use_context_app_fetch'; import { popularizeField } from '../../helpers/popularize_field'; import { ContextAppContent } from './context_app_content'; import { SurrDocType } from '../../angular/context/api/context'; +import { DocViewFilterFn } from '../../doc_views/doc_views_types'; const ContextAppContentMemoized = memo(ContextAppContent); @@ -161,7 +162,7 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp predecessorCount={appState.predecessorCount} successorCount={appState.successorCount} setAppState={setAppState} - addFilter={addFilter} + addFilter={addFilter as DocViewFilterFn} rows={rows} predecessors={fetchedState.predecessors} successors={fetchedState.successors} diff --git a/src/plugins/discover/public/application/components/context_app/context_app_content.test.tsx b/src/plugins/discover/public/application/components/context_app/context_app_content.test.tsx index 0536ab7e6a025..1b95af8bdbe1c 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_content.test.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_content.test.tsx @@ -8,73 +8,76 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; -import { uiSettingsMock as mockUiSettings } from '../../../__mocks__/ui_settings'; -import { DocTableLegacy } from '../../angular/doc_table/create_doc_table_react'; import { findTestSubject } from '@elastic/eui/lib/test'; import { ActionBar } from '../../angular/context/components/action_bar/action_bar'; import { AppState, GetStateReturn } from '../../angular/context_state'; import { SortDirection } from 'src/plugins/data/common'; import { EsHitRecordList } from '../../angular/context/api/context'; import { ContextAppContent, ContextAppContentProps } from './context_app_content'; -import { getServices } from '../../../kibana_services'; +import { getServices, setServices } from '../../../kibana_services'; import { LoadingStatus } from '../../angular/context_query_state'; import { indexPatternMock } from '../../../__mocks__/index_pattern'; import { DiscoverGrid } from '../discover_grid/discover_grid'; - -jest.mock('../../../kibana_services', () => { - return { - getServices: () => ({ - uiSettings: mockUiSettings, - }), - }; -}); +import { discoverServiceMock } from '../../../__mocks__/services'; +import { DocTableWrapper } from '../../apps/main/components/doc_table/doc_table_wrapper'; describe('ContextAppContent test', () => { - const hit = { - _id: '123', - _index: 'test_index', - _score: null, - _version: 1, - _source: { - category: ["Men's Clothing"], - currency: 'EUR', - customer_first_name: 'Walker', - customer_full_name: 'Walker Texas Ranger', - customer_gender: 'MALE', - customer_last_name: 'Ranger', - }, - fields: [{ order_date: ['2020-10-19T13:35:02.000Z'] }], - sort: [1603114502000, 2092], - }; - const defaultProps = ({ - columns: ['Time (@timestamp)', '_source'], - indexPattern: indexPatternMock, - appState: ({} as unknown) as AppState, - stateContainer: ({} as unknown) as GetStateReturn, - anchorStatus: LoadingStatus.LOADED, - predecessorsStatus: LoadingStatus.LOADED, - successorsStatus: LoadingStatus.LOADED, - rows: ([hit] as unknown) as EsHitRecordList, - predecessors: [], - successors: [], - defaultStepSize: 5, - predecessorCount: 10, - successorCount: 10, - useNewFieldsApi: false, - isPaginationEnabled: false, - onAddColumn: () => {}, - onRemoveColumn: () => {}, - onSetColumns: () => {}, - services: getServices(), - sort: [['order_date', 'desc']] as Array<[string, SortDirection]>, - isLegacy: true, - setAppState: () => {}, - addFilter: () => {}, - } as unknown) as ContextAppContentProps; + let hit; + let defaultProps: ContextAppContentProps; + + beforeEach(() => { + setServices(discoverServiceMock); + + hit = { + _id: '123', + _index: 'test_index', + _score: null, + _version: 1, + fields: [ + { + order_date: ['2020-10-19T13:35:02.000Z'], + }, + ], + _source: { + category: ["Men's Clothing"], + currency: 'EUR', + customer_first_name: 'Walker', + customer_full_name: 'Walker Texas Ranger', + customer_gender: 'MALE', + customer_last_name: 'Ranger', + }, + sort: [1603114502000, 2092], + }; + defaultProps = ({ + columns: ['order_date', '_source'], + indexPattern: indexPatternMock, + appState: ({} as unknown) as AppState, + stateContainer: ({} as unknown) as GetStateReturn, + anchorStatus: LoadingStatus.LOADED, + predecessorsStatus: LoadingStatus.LOADED, + successorsStatus: LoadingStatus.LOADED, + rows: ([hit] as unknown) as EsHitRecordList, + predecessors: [], + successors: [], + defaultStepSize: 5, + predecessorCount: 10, + successorCount: 10, + useNewFieldsApi: true, + isPaginationEnabled: false, + onAddColumn: () => {}, + onRemoveColumn: () => {}, + onSetColumns: () => {}, + services: getServices(), + sort: [['order_date', 'desc']] as Array<[string, SortDirection]>, + isLegacy: true, + setAppState: () => {}, + addFilter: () => {}, + } as unknown) as ContextAppContentProps; + }); it('should render legacy table correctly', () => { const component = mountWithIntl(); - expect(component.find(DocTableLegacy).length).toBe(1); + expect(component.find(DocTableWrapper).length).toBe(1); const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator'); expect(loadingIndicator.length).toBe(0); expect(component.find(ActionBar).length).toBe(2); @@ -84,18 +87,11 @@ describe('ContextAppContent test', () => { const props = { ...defaultProps }; props.anchorStatus = LoadingStatus.LOADING; const component = mountWithIntl(); - expect(component.find(DocTableLegacy).length).toBe(0); const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator'); + expect(component.find(DocTableWrapper).length).toBe(1); expect(loadingIndicator.length).toBe(1); }); - it('renders error message', () => { - const props = { ...defaultProps }; - props.anchorStatus = LoadingStatus.FAILED; - const component = mountWithIntl(); - expect(component.find(DocTableLegacy).length).toBe(0); - }); - it('should render discover grid correctly', () => { const props = { ...defaultProps, isLegacy: false }; const component = mountWithIntl(); diff --git a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx index 4d7ce2aa52092..78c354cbf908d 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx @@ -10,20 +10,17 @@ import React, { useState, Fragment, useMemo, useCallback } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiHorizontalRule, EuiText } from '@elastic/eui'; import { CONTEXT_STEP_SETTING, DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../common'; -import { IndexPattern, IndexPatternField } from '../../../../../data/common'; +import { IndexPattern } from '../../../../../data/common'; import { SortDirection } from '../../../../../data/public'; -import { - DocTableLegacy, - DocTableLegacyProps, -} from '../../angular/doc_table/create_doc_table_react'; import { LoadingStatus } from '../../angular/context_query_state'; import { ActionBar } from '../../angular/context/components/action_bar/action_bar'; -import { DiscoverGrid, DiscoverGridProps } from '../discover_grid/discover_grid'; -import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { DiscoverGrid } from '../discover_grid/discover_grid'; +import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types'; import { AppState } from '../../angular/context_state'; -import { EsHitRecord, EsHitRecordList, SurrDocType } from '../../angular/context/api/context'; +import { EsHitRecordList, SurrDocType } from '../../angular/context/api/context'; import { DiscoverServices } from '../../../build_services'; import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from './utils/constants'; +import { DocTableContext } from '../../apps/main/components/doc_table/doc_table_context'; export interface ContextAppContentProps { columns: string[]; @@ -44,11 +41,7 @@ export interface ContextAppContentProps { useNewFieldsApi: boolean; isLegacy: boolean; setAppState: (newState: Partial) => void; - addFilter: ( - field: IndexPatternField | string, - values: unknown, - operation: string - ) => Promise; + addFilter: DocViewFilterFn; } const controlColumnIds = ['openDetails']; @@ -57,8 +50,8 @@ export function clamp(value: number) { return Math.max(Math.min(MAX_CONTEXT_SIZE, value), MIN_CONTEXT_SIZE); } -const DataGridMemoized = React.memo(DiscoverGrid); -const DocTableLegacyMemoized = React.memo(DocTableLegacy); +const DiscoverGridMemoized = React.memo(DiscoverGrid); +const DocTableContextMemoized = React.memo(DocTableContext); const ActionBarMemoized = React.memo(ActionBar); export function ContextAppContent({ @@ -84,8 +77,7 @@ export function ContextAppContent({ }: ContextAppContentProps) { const { uiSettings: config } = services; - const [expandedDoc, setExpandedDoc] = useState(undefined); - const isAnchorLoaded = anchorStatus === LoadingStatus.LOADED; + const [expandedDoc, setExpandedDoc] = useState(); const isAnchorLoading = anchorStatus === LoadingStatus.LOADING || anchorStatus === LoadingStatus.UNINITIALIZED; const arePredecessorsLoading = @@ -100,50 +92,8 @@ export function ContextAppContent({ ); const defaultStepSize = useMemo(() => parseInt(config.get(CONTEXT_STEP_SETTING), 10), [config]); - const docTableProps = () => { - return { - ariaLabelledBy: 'surDocumentsAriaLabel', - columns, - rows: rows as ElasticSearchHit[], - indexPattern, - expandedDoc, - isLoading: isAnchorLoading, - sampleSize: 0, - sort: sort as [[string, SortDirection]], - isSortEnabled: false, - showTimeCol, - services, - useNewFieldsApi, - isPaginationEnabled: false, - controlColumnIds, - setExpandedDoc, - onFilter: addFilter, - onAddColumn, - onRemoveColumn, - onSetColumns, - } as DiscoverGridProps; - }; - - const legacyDocTableProps = () => { - // @ts-expect-error doesn't implement full DocTableLegacyProps interface - return { - columns, - indexPattern, - minimumVisibleRows: rows.length, - rows, - onFilter: addFilter, - onAddColumn, - onRemoveColumn, - sort, - useNewFieldsApi, - } as DocTableLegacyProps; - }; - const loadingFeedback = () => { - if ( - isLegacy && - (anchorStatus === LoadingStatus.UNINITIALIZED || anchorStatus === LoadingStatus.LOADING) - ) { + if (isLegacy && isAnchorLoading) { return ( @@ -170,18 +120,47 @@ export function ContextAppContent({ docCountAvailable={predecessors.length} onChangeCount={onChangeCount} isLoading={arePredecessorsLoading} - isDisabled={!isAnchorLoaded} + isDisabled={isAnchorLoading} /> {loadingFeedback()} - {isLegacy && isAnchorLoaded && ( -
- -
+ {isLegacy && rows && rows.length !== 0 && ( + )} - {!isLegacy && ( + {!isLegacy && rows && rows.length && (
- +
)} @@ -192,7 +171,7 @@ export function ContextAppContent({ docCountAvailable={successors.length} onChangeCount={onChangeCount} isLoading={areSuccessorsLoading} - isDisabled={!isAnchorLoaded} + isDisabled={isAnchorLoading} /> ); diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx index f1c56b7a57195..c727e7784cca6 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx @@ -28,7 +28,6 @@ import { DiscoverGridFlyout } from './discover_grid_flyout'; import { DiscoverGridContext } from './discover_grid_context'; import { getRenderCellValueFn } from './get_render_cell_value'; import { DiscoverGridSettings } from './types'; -import { SortPairArr } from '../../angular/doc_table/lib/get_sort'; import { getEuiGridColumns, getLeadControlColumns, @@ -40,6 +39,7 @@ import { getDisplayedColumns } from '../../helpers/columns'; import { KibanaContextProvider } from '../../../../../kibana_react/public'; import { MAX_DOC_FIELDS_DISPLAYED } from '../../../../common'; import { DiscoverGridDocumentToolbarBtn, getDocId } from './discover_grid_document_selection'; +import { SortPairArr } from '../../apps/main/components/doc_table/lib/get_sort'; interface SortObj { id: string; @@ -369,7 +369,7 @@ export const DiscoverGrid = ({ > {i18n.translate('discover.grid.tableRow.viewSingleDocumentLinkTextSimple', { diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss index 12d56d564b855..e845ba7238303 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss +++ b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.scss @@ -45,7 +45,6 @@ } .kbnDocViewer__value { - display: inline-block; word-break: break-all; word-wrap: break-word; white-space: pre-wrap; diff --git a/src/plugins/discover/public/application/components/table/table_helper.test.ts b/src/plugins/discover/public/application/components/table/table_helper.test.ts deleted file mode 100644 index 738556aaea085..0000000000000 --- a/src/plugins/discover/public/application/components/table/table_helper.test.ts +++ /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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { arrayContainsObjects } from './table_helper'; - -describe('arrayContainsObjects', () => { - it(`returns false for an array of primitives`, () => { - const actual = arrayContainsObjects(['test', 'test']); - expect(actual).toBeFalsy(); - }); - - it(`returns true for an array of objects`, () => { - const actual = arrayContainsObjects([{}, {}]); - expect(actual).toBeTruthy(); - }); - - it(`returns true for an array of objects and primitves`, () => { - const actual = arrayContainsObjects([{}, 'sdf']); - expect(actual).toBeTruthy(); - }); - - it(`returns false for an array of null values`, () => { - const actual = arrayContainsObjects([null, null]); - expect(actual).toBeFalsy(); - }); - - it(`returns false if no array is given`, () => { - const actual = arrayContainsObjects([null, null]); - expect(actual).toBeFalsy(); - }); -}); diff --git a/src/plugins/discover/public/application/components/table/table_helper.tsx b/src/plugins/discover/public/application/components/table/table_helper.tsx index 6af349af11f1d..e1c3de8d87c34 100644 --- a/src/plugins/discover/public/application/components/table/table_helper.tsx +++ b/src/plugins/discover/public/application/components/table/table_helper.tsx @@ -6,13 +6,6 @@ * Side Public License, v 1. */ -/** - * Returns true if the given array contains at least 1 object - */ -export function arrayContainsObjects(value: unknown[]): boolean { - return Array.isArray(value) && value.some((v) => typeof v === 'object' && v !== null); -} - /** * Removes markup added by kibana fields html formatter */ diff --git a/src/plugins/discover/public/application/doc_views/doc_views_types.ts b/src/plugins/discover/public/application/doc_views/doc_views_types.ts index 58399f31e032f..fe185d7c21f03 100644 --- a/src/plugins/discover/public/application/doc_views/doc_views_types.ts +++ b/src/plugins/discover/public/application/doc_views/doc_views_types.ts @@ -18,7 +18,7 @@ export interface AngularDirective { export type AngularScope = IScope; -export type ElasticSearchHit = estypes.SearchResponse['hits']['hits'][number]; +export type ElasticSearchHit = estypes.SearchHit; export interface FieldMapping { filterable?: boolean; diff --git a/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx index 098c7f55fbd9f..3fd7b2f50d319 100644 --- a/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx @@ -22,11 +22,10 @@ import { Query, TimeRange, Filter, - IndexPatternField, IndexPattern, ISearchSource, + IndexPatternField, } from '../../../../data/common'; -import { SortOrder } from '../angular/doc_table/components/table_header/helpers'; import { ElasticSearchHit } from '../doc_views/doc_views_types'; import { SavedSearchEmbeddableComponent } from './saved_search_embeddable_component'; import { UiActionsStart } from '../../../../ui_actions/public'; @@ -38,23 +37,26 @@ import { SEARCH_FIELDS_FROM_SOURCE, SORT_DEFAULT_ORDER_SETTING, } from '../../../common'; -import * as columnActions from '../angular/doc_table/actions/columns'; -import { getSortForSearchSource, getDefaultSort } from '../angular/doc_table'; +import * as columnActions from '../apps/main/components/doc_table/actions/columns'; import { handleSourceColumnState } from '../angular/helpers'; import { DiscoverGridProps } from '../components/discover_grid/discover_grid'; import { DiscoverGridSettings } from '../components/discover_grid/types'; - -export interface SearchProps extends Partial { - settings?: DiscoverGridSettings; - description?: string; - sharedItemTitle?: string; - inspectorAdapters?: Adapters; - - filter?: (field: IndexPatternField, value: string[], operator: string) => void; - hits?: ElasticSearchHit[]; - totalHitCount?: number; - onMoveColumn?: (column: string, index: number) => void; -} +import { DocTableProps } from '../apps/main/components/doc_table/doc_table_wrapper'; +import { getDefaultSort, getSortForSearchSource } from '../apps/main/components/doc_table'; +import { SortOrder } from '../apps/main/components/doc_table/components/table_header/helpers'; + +export type SearchProps = Partial & + Partial & { + settings?: DiscoverGridSettings; + description?: string; + sharedItemTitle?: string; + inspectorAdapters?: Adapters; + + filter?: (field: IndexPatternField, value: string[], operator: string) => void; + hits?: ElasticSearchHit[]; + totalHitCount?: number; + onMoveColumn?: (column: string, index: number) => void; + }; interface SearchEmbeddableConfig { savedSearch: SavedSearch; diff --git a/src/plugins/discover/public/application/embeddable/saved_search_embeddable_component.tsx b/src/plugins/discover/public/application/embeddable/saved_search_embeddable_component.tsx index 5b2a2635d04bd..76b316d575cf2 100644 --- a/src/plugins/discover/public/application/embeddable/saved_search_embeddable_component.tsx +++ b/src/plugins/discover/public/application/embeddable/saved_search_embeddable_component.tsx @@ -8,9 +8,12 @@ import React from 'react'; -import { DiscoverGridEmbeddable } from '../angular/create_discover_grid_directive'; -import { DiscoverDocTableEmbeddable } from '../angular/doc_table/create_doc_table_embeddable'; -import { DiscoverGridProps } from '../components/discover_grid/discover_grid'; +import { + DiscoverGridEmbeddable, + DiscoverGridEmbeddableProps, +} from '../angular/create_discover_grid_directive'; +import { DiscoverDocTableEmbeddable } from '../apps/main/components/doc_table/create_doc_table_embeddable'; +import { DocTableEmbeddableProps } from '../apps/main/components/doc_table/doc_table_embeddable'; import { SearchProps } from './saved_search_embeddable'; interface SavedSearchEmbeddableComponentProps { @@ -32,8 +35,8 @@ export function SavedSearchEmbeddableComponent({ ...searchProps, refs, }; - return ; + return ; } - const discoverGridProps = searchProps as DiscoverGridProps; + const discoverGridProps = searchProps as DiscoverGridEmbeddableProps; return ; } diff --git a/src/plugins/discover/public/application/embeddable/types.ts b/src/plugins/discover/public/application/embeddable/types.ts index 642c65c4b2a55..5a08534918d4f 100644 --- a/src/plugins/discover/public/application/embeddable/types.ts +++ b/src/plugins/discover/public/application/embeddable/types.ts @@ -12,9 +12,9 @@ import { EmbeddableOutput, IEmbeddable, } from 'src/plugins/embeddable/public'; -import { SortOrder } from '../angular/doc_table/components/table_header/helpers'; import { Filter, IndexPattern, TimeRange, Query } from '../../../../data/public'; import { SavedSearch } from '../..'; +import { SortOrder } from '../apps/main/components/doc_table/components/table_header/helpers'; export interface SearchInput extends EmbeddableInput { timeRange: TimeRange; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.ts b/src/plugins/discover/public/application/helpers/get_single_doc_url.ts similarity index 65% rename from src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.ts rename to src/plugins/discover/public/application/helpers/get_single_doc_url.ts index 7eb31459eb4f5..913463e6d44a4 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/truncate_by_height.ts +++ b/src/plugins/discover/public/application/helpers/get_single_doc_url.ts @@ -6,6 +6,6 @@ * Side Public License, v 1. */ -export const truncateByHeight = ({ body }: { body: string }) => { - return `
${body}
`; +export const getSingleDocUrl = (indexPatternId: string, rowIndex: string, rowId: string) => { + return `/app/discover#/doc/${indexPatternId}/${rowIndex}?id=${encodeURIComponent(rowId)}`; }; diff --git a/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts b/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts index 418cbf6eac9cd..8a28369d1f5f2 100644 --- a/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts +++ b/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts @@ -18,7 +18,7 @@ import { AppState as ContextState, GetStateReturn as ContextGetStateReturn, } from '../angular/context_state'; -import { getStateColumnActions } from '../angular/doc_table/actions/columns'; +import { getStateColumnActions } from '../apps/main/components/doc_table/actions/columns'; interface UseDataGridColumnsProps { capabilities: Capabilities; diff --git a/test/functional/apps/discover/_doc_table.ts b/test/functional/apps/discover/_doc_table.ts index 7d0228131cc67..09a162e051bf6 100644 --- a/test/functional/apps/discover/_doc_table.ts +++ b/test/functional/apps/discover/_doc_table.ts @@ -228,13 +228,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should make the document table scrollable', async function () { await PageObjects.discover.clearFieldSearchInput(); - const dscTable = await find.byCssSelector('.dscTable'); + const dscTableWrapper = await find.byCssSelector('.kbnDocTableWrapper'); const fieldNames = await PageObjects.discover.getAllFieldNames(); - const clientHeight = await dscTable.getAttribute('clientHeight'); + const clientHeight = await dscTableWrapper.getAttribute('clientHeight'); let fieldCounter = 0; const checkScrollable = async () => { - const scrollWidth = await dscTable.getAttribute('scrollWidth'); - const clientWidth = await dscTable.getAttribute('clientWidth'); + const scrollWidth = await dscTableWrapper.getAttribute('scrollWidth'); + const clientWidth = await dscTableWrapper.getAttribute('clientWidth'); log.debug(`scrollWidth: ${scrollWidth}, clientWidth: ${clientWidth}`); return Number(scrollWidth) > Number(clientWidth); }; @@ -251,7 +251,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { return await checkScrollable(); }); // so now we need to check if the horizontal scrollbar is displayed - const newClientHeight = await dscTable.getAttribute('clientHeight'); + const newClientHeight = await dscTableWrapper.getAttribute('clientHeight'); expect(Number(clientHeight)).to.be.above(Number(newClientHeight)); }); }); diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index ecba9549cea02..ea727069c927d 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -228,9 +228,13 @@ export class DashboardPageObject extends FtrService { */ public async expectToolbarPaginationDisplayed() { - const isLegacyDefault = this.discover.useLegacyTable(); + const isLegacyDefault = await this.discover.useLegacyTable(); if (isLegacyDefault) { - const subjects = ['btnPrevPage', 'btnNextPage', 'toolBarPagerText']; + const subjects = [ + 'pagination-button-previous', + 'pagination-button-next', + 'toolBarTotalDocsText', + ]; await Promise.all(subjects.map(async (subj) => await this.testSubjects.existOrFail(subj))); } else { const subjects = ['pagination-button-previous', 'pagination-button-next']; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1b20d91d480b2..0d4432d8dac56 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1523,9 +1523,6 @@ "discover.doc.somethingWentWrongDescriptionAddon": "インデックスが存在することを確認してください。", "discover.docTable.limitedSearchResultLabel": "{resultCount}件の結果のみが表示されます。検索結果を絞り込みます。", "discover.docTable.noResultsTitle": "結果が見つかりませんでした", - "discover.docTable.pager.toolbarPagerButtons.nextButtonAriaLabel": "表内の次ページ", - "discover.docTable.pager.toolbarPagerButtons.previousButtonAriaLabel": "表内の前ページ", - "discover.docTable.pagerControl.pagesCountLabel": "{startItem}–{endItem}/{totalItems}", "discover.docTable.tableHeader.documentHeader": "ドキュメント", "discover.docTable.tableHeader.moveColumnLeftButtonAriaLabel": "{columnName}列を左に移動", "discover.docTable.tableHeader.moveColumnLeftButtonTooltip": "列を左に移動", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d3c89b74eca8b..fc8b27aa497cd 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1532,9 +1532,6 @@ "discover.doc.somethingWentWrongDescriptionAddon": "请确保索引存在。", "discover.docTable.limitedSearchResultLabel": "仅限于 {resultCount} 个结果。优化您的搜索。", "discover.docTable.noResultsTitle": "找不到结果", - "discover.docTable.pager.toolbarPagerButtons.nextButtonAriaLabel": "表中下一页", - "discover.docTable.pager.toolbarPagerButtons.previousButtonAriaLabel": "表中上一页", - "discover.docTable.pagerControl.pagesCountLabel": "{startItem}–{endItem}/{totalItems}", "discover.docTable.tableHeader.documentHeader": "文档", "discover.docTable.tableHeader.moveColumnLeftButtonAriaLabel": "向左移动“{columnName}”列", "discover.docTable.tableHeader.moveColumnLeftButtonTooltip": "向左移动列", From 1649661ffdc79d00f9d23451790335e5d25da25f Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Tue, 10 Aug 2021 11:52:49 -0400 Subject: [PATCH 033/104] [Observability][Exploratory View] revert exploratory view multi-series (#107647) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-test/src/jest/utils/get_url.ts | 9 +- test/functional/page_objects/common_page.ts | 10 +- .../app/RumDashboard/ActionMenu/index.tsx | 22 +- .../PageLoadDistribution/index.tsx | 19 +- .../app/RumDashboard/PageViewsTrend/index.tsx | 19 +- .../analyze_data_button.test.tsx | 10 +- .../analyze_data_button.tsx | 37 +- .../apm/server/lib/rum_client/has_rum_data.ts | 6 +- x-pack/plugins/observability/kibana.json | 12 +- .../add_data_buttons/mobile_add_data.tsx | 32 -- .../add_data_buttons/synthetics_add_data.tsx | 32 -- .../shared/add_data_buttons/ux_add_data.tsx | 32 -- .../action_menu/action_menu.test.tsx | 59 --- .../components/action_menu/action_menu.tsx | 92 ---- .../components/action_menu/index.tsx | 26 -- .../components/empty_view.tsx | 17 +- .../components/filter_label.test.tsx | 14 +- .../components/filter_label.tsx | 11 +- .../components/series_color_picker.tsx | 59 --- .../components/series_date_picker/index.tsx | 109 ----- .../configurations/constants/constants.ts | 13 - .../configurations/constants/url_constants.ts | 4 +- .../configurations/default_configs.ts | 17 +- .../configurations/lens_attributes.test.ts | 27 +- .../configurations/lens_attributes.ts | 50 +- .../mobile/device_distribution_config.ts | 8 +- .../mobile/distribution_config.ts | 4 +- .../mobile/kpi_over_time_config.ts | 10 +- .../rum/core_web_vitals_config.test.ts | 9 +- .../rum/core_web_vitals_config.ts | 4 +- .../rum/data_distribution_config.ts | 9 +- .../rum/kpi_over_time_config.ts | 10 +- .../synthetics/data_distribution_config.ts | 9 +- .../synthetics/kpi_over_time_config.ts | 4 +- .../test_data/sample_attribute.ts | 91 ++-- .../test_data/sample_attribute_cwv.ts | 4 +- .../test_data/sample_attribute_kpi.ts | 77 +-- .../exploratory_view/configurations/utils.ts | 27 +- .../exploratory_view.test.tsx | 28 +- .../exploratory_view/exploratory_view.tsx | 177 ++----- .../exploratory_view/header/header.test.tsx | 47 +- .../shared/exploratory_view/header/header.tsx | 81 +++- .../hooks/use_app_index_pattern.tsx | 2 +- .../hooks/use_discover_link.tsx | 92 ---- .../hooks/use_lens_attributes.ts | 59 +-- .../hooks/use_series_filters.ts | 43 +- .../hooks/use_series_storage.test.tsx | 91 ++-- .../hooks/use_series_storage.tsx | 151 ++---- .../shared/exploratory_view/index.tsx | 4 +- .../exploratory_view/lens_embeddable.tsx | 77 +-- .../shared/exploratory_view/rtl_helpers.tsx | 60 +-- .../columns/chart_types.test.tsx | 6 +- .../columns/chart_types.tsx | 52 ++- .../columns/data_types_col.test.tsx | 62 +++ .../series_builder/columns/data_types_col.tsx | 74 +++ .../columns/date_picker_col.tsx | 39 ++ .../columns/operation_type_select.test.tsx | 36 +- .../columns/operation_type_select.tsx | 13 +- .../columns/report_breakdowns.test.tsx | 74 +++ .../columns/report_breakdowns.tsx | 26 ++ .../columns/report_definition_col.test.tsx | 46 +- .../columns/report_definition_col.tsx | 106 +++++ .../columns/report_definition_field.tsx | 50 +- .../columns/report_filters.test.tsx | 28 ++ .../series_builder/columns/report_filters.tsx | 29 ++ .../columns/report_types_col.test.tsx | 79 ++++ .../columns/report_types_col.tsx | 108 +++++ .../last_updated.tsx | 21 +- .../series_builder/report_metric_options.tsx | 46 ++ .../series_builder/series_builder.tsx | 303 ++++++++++++ .../date_range_picker.tsx | 63 +-- .../series_date_picker/index.tsx | 58 +++ .../series_date_picker.test.tsx | 61 ++- .../series_editor/chart_edit_options.tsx | 30 ++ .../columns/breakdowns.test.tsx | 22 +- .../columns/breakdowns.tsx | 35 +- .../series_editor/columns/chart_options.tsx | 35 ++ .../columns/data_type_select.test.tsx | 39 -- .../columns/data_type_select.tsx | 105 ----- .../series_editor/columns/date_picker_col.tsx | 82 +--- .../columns/filter_expanded.test.tsx | 48 +- .../columns/filter_expanded.tsx | 139 +++--- .../columns/filter_value_btn.test.tsx | 145 +++--- .../columns/filter_value_btn.tsx | 15 +- .../series_editor/columns/remove_series.tsx | 34 ++ .../columns/report_definition_col.tsx | 59 --- .../columns/report_type_select.tsx | 64 --- .../series_editor/columns/series_actions.tsx | 103 ++++ .../series_editor/columns/series_filter.tsx | 155 ++++++ .../series_editor/expanded_series_row.tsx | 77 --- .../series_editor/report_metric_options.tsx | 101 ---- .../selected_filters.test.tsx | 18 +- .../series_editor/selected_filters.tsx | 101 ++++ .../series_editor/series_editor.tsx | 441 ++++-------------- .../series_viewer/columns/chart_types.tsx | 70 --- .../series_viewer/columns/remove_series.tsx | 51 -- .../series_viewer/columns/series_actions.tsx | 104 ----- .../series_viewer/columns/series_filter.tsx | 69 --- .../series_viewer/columns/series_info.tsx | 95 ---- .../series_viewer/columns/series_name.tsx | 38 -- .../series_viewer/columns/utils.ts | 104 ----- .../series_viewer/selected_filters.tsx | 132 ------ .../series_viewer/series_viewer.tsx | 120 ----- .../shared/exploratory_view/types.ts | 11 +- .../exploratory_view/views/series_views.tsx | 85 ---- .../exploratory_view/views/view_actions.tsx | 119 ----- .../field_value_combobox.tsx | 61 +-- .../field_value_selection.tsx | 3 +- .../field_value_suggestions/index.test.tsx | 2 - .../shared/field_value_suggestions/index.tsx | 6 - .../shared/field_value_suggestions/types.ts | 3 - .../filter_value_label/filter_value_label.tsx | 18 +- .../public/components/shared/index.tsx | 3 +- .../public/hooks/use_quick_time_ranges.tsx | 2 +- x-pack/plugins/observability/public/plugin.ts | 2 - .../observability/public/routes/index.tsx | 30 +- .../translations/translations/ja-JP.json | 12 + .../translations/translations/zh-CN.json | 12 + .../common/charts/ping_histogram.tsx | 25 +- .../common/header/action_menu_content.tsx | 29 +- .../monitor_duration_container.tsx | 21 +- .../apps/observability/exploratory_view.ts | 82 ---- .../functional/apps/observability/index.ts | 3 +- 123 files changed, 2590 insertions(+), 3866 deletions(-) delete mode 100644 x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx delete mode 100644 x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx delete mode 100644 x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_color_picker.tsx delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_editor => series_builder}/columns/chart_types.test.tsx (85%) rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_editor => series_builder}/columns/chart_types.tsx (77%) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_editor => series_builder}/columns/operation_type_select.test.tsx (69%) rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_editor => series_builder}/columns/operation_type_select.tsx (91%) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_editor => series_builder}/columns/report_definition_col.test.tsx (65%) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_editor => series_builder}/columns/report_definition_field.tsx (69%) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx rename x-pack/plugins/observability/public/components/shared/exploratory_view/{header => series_builder}/last_updated.tsx (55%) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx rename x-pack/plugins/observability/public/components/shared/exploratory_view/{components => series_date_picker}/date_range_picker.tsx (58%) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx rename x-pack/plugins/observability/public/components/shared/exploratory_view/{components => }/series_date_picker/series_date_picker.test.tsx (50%) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_viewer => series_editor}/columns/breakdowns.test.tsx (74%) rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_viewer => series_editor}/columns/breakdowns.tsx (71%) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_viewer => series_editor}/columns/filter_expanded.test.tsx (67%) rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_viewer => series_editor}/columns/filter_expanded.tsx (55%) rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_viewer => series_editor}/columns/filter_value_btn.test.tsx (64%) rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_viewer => series_editor}/columns/filter_value_btn.tsx (92%) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx rename x-pack/plugins/observability/public/components/shared/exploratory_view/{series_viewer => series_editor}/selected_filters.test.tsx (71%) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/chart_types.tsx delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/remove_series.tsx delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_actions.tsx delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_filter.tsx delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_info.tsx delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_name.tsx delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/utils.ts delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.tsx delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/series_viewer.tsx delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx delete mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx delete mode 100644 x-pack/test/functional/apps/observability/exploratory_view.ts diff --git a/packages/kbn-test/src/jest/utils/get_url.ts b/packages/kbn-test/src/jest/utils/get_url.ts index e08695b334e1b..734e26c5199d7 100644 --- a/packages/kbn-test/src/jest/utils/get_url.ts +++ b/packages/kbn-test/src/jest/utils/get_url.ts @@ -22,6 +22,11 @@ interface UrlParam { username?: string; } +interface App { + pathname?: string; + hash?: string; +} + /** * Converts a config and a pathname to a url * @param {object} config A url config @@ -41,11 +46,11 @@ interface UrlParam { * @return {string} */ -function getUrl(config: UrlParam, app: UrlParam) { +function getUrl(config: UrlParam, app: App) { return url.format(_.assign({}, config, app)); } -getUrl.noAuth = function getUrlNoAuth(config: UrlParam, app: UrlParam) { +getUrl.noAuth = function getUrlNoAuth(config: UrlParam, app: App) { config = _.pickBy(config, function (val, param) { return param !== 'auth'; }); diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 8a2a66c41d426..49d56d6f43784 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -202,13 +202,7 @@ export class CommonPageObject extends FtrService { async navigateToApp( appName: string, - { - basePath = '', - shouldLoginIfPrompted = true, - hash = '', - search = '', - insertTimestamp = true, - } = {} + { basePath = '', shouldLoginIfPrompted = true, hash = '', insertTimestamp = true } = {} ) { let appUrl: string; if (this.config.has(['apps', appName])) { @@ -217,13 +211,11 @@ export class CommonPageObject extends FtrService { appUrl = getUrl.noAuth(this.config.get('servers.kibana'), { pathname: `${basePath}${appConfig.pathname}`, hash: hash || appConfig.hash, - search, }); } else { appUrl = getUrl.noAuth(this.config.get('servers.kibana'), { pathname: `${basePath}/app/${appName}`, hash, - search, }); } diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx index 0bd873bd7064b..4e6544a20f301 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx @@ -11,11 +11,11 @@ import { i18n } from '@kbn/i18n'; import { createExploratoryViewUrl, HeaderMenuPortal, + SeriesUrl, } from '../../../../../../observability/public'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { AppMountParameters } from '../../../../../../../../src/core/public'; -import { SERVICE_NAME } from '../../../../../common/elasticsearch_fieldnames'; const ANALYZE_DATA = i18n.translate('xpack.apm.analyzeDataButtonLabel', { defaultMessage: 'Analyze data', @@ -38,22 +38,15 @@ export function UXActionMenu({ services: { http }, } = useKibana(); const { urlParams } = useUrlParams(); - const { rangeTo, rangeFrom, serviceName } = urlParams; + const { rangeTo, rangeFrom } = urlParams; const uxExploratoryViewLink = createExploratoryViewUrl( { - reportType: 'kpi-over-time', - allSeries: [ - { - dataType: 'ux', - name: `${serviceName}-page-views`, - time: { from: rangeFrom!, to: rangeTo! }, - reportDefinitions: { - [SERVICE_NAME]: serviceName ? [serviceName] : [], - }, - selectedMetricField: 'Records', - }, - ], + 'ux-series': ({ + dataType: 'ux', + isNew: true, + time: { from: rangeFrom, to: rangeTo }, + } as unknown) as SeriesUrl, }, http?.basePath.get() ); @@ -67,7 +60,6 @@ export function UXActionMenu({ {ANALYZE_MESSAGE}

}> { render(); expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( - 'http://localhost/app/observability/exploratory-view/configure#?reportType=kpi-over-time&sr=!((dt:ux,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),time:(from:now-15m,to:now)))' + 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:ux,isNew:!t,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' ); }); }); @@ -48,7 +48,7 @@ describe('AnalyzeDataButton', () => { render(); expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( - 'http://localhost/app/observability/exploratory-view/configure#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),time:(from:now-15m,to:now)))' + 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' ); }); }); @@ -58,7 +58,7 @@ describe('AnalyzeDataButton', () => { render(); expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( - 'http://localhost/app/observability/exploratory-view/configure#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.name:!(testServiceName)),time:(from:now-15m,to:now)))' + 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' ); }); }); @@ -68,7 +68,7 @@ describe('AnalyzeDataButton', () => { render(); expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( - 'http://localhost/app/observability/exploratory-view/configure#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(ENVIRONMENT_NOT_DEFINED),service.name:!(testServiceName)),time:(from:now-15m,to:now)))' + 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' ); }); }); @@ -78,7 +78,7 @@ describe('AnalyzeDataButton', () => { render(); expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( - 'http://localhost/app/observability/exploratory-view/configure#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(ALL_VALUES),service.name:!(testServiceName)),time:(from:now-15m,to:now)))' + 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.environment:!(ALL_VALUES),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' ); }); }); diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx index 5af6ea6cdc777..d8ff7fdf47c58 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx @@ -9,7 +9,10 @@ import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -import { createExploratoryViewUrl } from '../../../../../../observability/public'; +import { + createExploratoryViewUrl, + SeriesUrl, +} from '../../../../../../observability/public'; import { ALL_VALUES_SELECTED } from '../../../../../../observability/public'; import { isIosAgentName, @@ -18,7 +21,6 @@ import { import { SERVICE_ENVIRONMENT, SERVICE_NAME, - TRANSACTION_DURATION, } from '../../../../../common/elasticsearch_fieldnames'; import { ENVIRONMENT_ALL, @@ -27,11 +29,13 @@ import { import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; -function getEnvironmentDefinition(environment: string) { +function getEnvironmentDefinition(environment?: string) { switch (environment) { case ENVIRONMENT_ALL.value: return { [SERVICE_ENVIRONMENT]: [ALL_VALUES_SELECTED] }; case ENVIRONMENT_NOT_DEFINED.value: + case undefined: + return {}; default: return { [SERVICE_ENVIRONMENT]: [environment] }; } @@ -47,26 +51,21 @@ export function AnalyzeDataButton() { if ( (isRumAgentName(agentName) || isIosAgentName(agentName)) && - rangeFrom && - canShowDashboard && - rangeTo + canShowDashboard ) { const href = createExploratoryViewUrl( { - reportType: 'kpi-over-time', - allSeries: [ - { - name: `${serviceName}-response-latency`, - selectedMetricField: TRANSACTION_DURATION, - dataType: isRumAgentName(agentName) ? 'ux' : 'mobile', - time: { from: rangeFrom, to: rangeTo }, - reportDefinitions: { - [SERVICE_NAME]: [serviceName], - ...(environment ? getEnvironmentDefinition(environment) : {}), - }, - operationType: 'average', + 'apm-series': { + dataType: isRumAgentName(agentName) ? 'ux' : 'mobile', + time: { from: rangeFrom, to: rangeTo }, + reportType: 'kpi-over-time', + reportDefinitions: { + [SERVICE_NAME]: [serviceName], + ...getEnvironmentDefinition(environment), }, - ], + operationType: 'average', + isNew: true, + } as SeriesUrl, }, basepath ); diff --git a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts index 70585f9a9bf28..28fab3369b1eb 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts @@ -5,7 +5,6 @@ * 2.0. */ -import moment from 'moment'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { SERVICE_NAME, @@ -21,10 +20,7 @@ export async function hasRumData({ setup: Setup & Partial; }) { try { - const { - start = moment().subtract(24, 'h').valueOf(), - end = moment().valueOf(), - } = setup; + const { start, end } = setup; const params = { apm: { diff --git a/x-pack/plugins/observability/kibana.json b/x-pack/plugins/observability/kibana.json index b794f91231505..4273252850da4 100644 --- a/x-pack/plugins/observability/kibana.json +++ b/x-pack/plugins/observability/kibana.json @@ -6,8 +6,16 @@ }, "version": "8.0.0", "kibanaVersion": "kibana", - "configPath": ["xpack", "observability"], - "optionalPlugins": ["home", "discover", "lens", "licensing", "usageCollection"], + "configPath": [ + "xpack", + "observability" + ], + "optionalPlugins": [ + "home", + "lens", + "licensing", + "usageCollection" + ], "requiredPlugins": [ "alerting", "cases", diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx deleted file mode 100644 index 0e17c6277618b..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiHeaderLink } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useKibana } from '../../../utils/kibana_react'; - -export function MobileAddData() { - const kibana = useKibana(); - - return ( - - {ADD_DATA_LABEL} - - ); -} - -const ADD_DATA_LABEL = i18n.translate('xpack.observability.mobile.addDataButtonLabel', { - defaultMessage: 'Add Mobile data', -}); diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx deleted file mode 100644 index af91624769e6b..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiHeaderLink } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useKibana } from '../../../utils/kibana_react'; - -export function SyntheticsAddData() { - const kibana = useKibana(); - - return ( - - {ADD_DATA_LABEL} - - ); -} - -const ADD_DATA_LABEL = i18n.translate('xpack.observability..synthetics.addDataButtonLabel', { - defaultMessage: 'Add synthetics data', -}); diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx deleted file mode 100644 index c6aa0742466f1..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiHeaderLink } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useKibana } from '../../../utils/kibana_react'; - -export function UXAddData() { - const kibana = useKibana(); - - return ( - - {ADD_DATA_LABEL} - - ); -} - -const ADD_DATA_LABEL = i18n.translate('xpack.observability.ux.addDataButtonLabel', { - defaultMessage: 'Add UX data', -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx deleted file mode 100644 index 329192abc99d2..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render } from '../../rtl_helpers'; -import { fireEvent, screen } from '@testing-library/dom'; -import React from 'react'; -import { sampleAttribute } from '../../configurations/test_data/sample_attribute'; -import * as pluginHook from '../../../../../hooks/use_plugin_context'; -import { TypedLensByValueInput } from '../../../../../../../lens/public'; -import { ExpViewActionMenuContent } from './action_menu'; - -jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ - appMountParameters: { - setHeaderActionMenu: jest.fn(), - }, -} as any); - -describe('Action Menu', function () { - it('should be able to click open in lens', async function () { - const { findByText, core } = render( - - ); - - expect(await screen.findByText('Open in Lens')).toBeInTheDocument(); - - fireEvent.click(await findByText('Open in Lens')); - - expect(core.lens?.navigateToPrefilledEditor).toHaveBeenCalledTimes(1); - expect(core.lens?.navigateToPrefilledEditor).toHaveBeenCalledWith( - { - id: '', - attributes: sampleAttribute, - timeRange: { to: 'now', from: 'now-10m' }, - }, - true - ); - }); - - it('should be able to click save', async function () { - const { findByText } = render( - - ); - - expect(await screen.findByText('Save')).toBeInTheDocument(); - - fireEvent.click(await findByText('Save')); - - expect(await screen.findByText('Lens Save Modal Component')).toBeInTheDocument(); - }); -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx deleted file mode 100644 index 38011eb5f8ffb..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState } from 'react'; -import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { LensEmbeddableInput, TypedLensByValueInput } from '../../../../../../../lens/public'; -import { ObservabilityAppServices } from '../../../../../application/types'; -import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; - -export function ExpViewActionMenuContent({ - timeRange, - lensAttributes, -}: { - timeRange?: { from: string; to: string }; - lensAttributes: TypedLensByValueInput['attributes'] | null; -}) { - const kServices = useKibana().services; - - const { lens } = kServices; - - const [isSaveOpen, setIsSaveOpen] = useState(false); - - const LensSaveModalComponent = lens.SaveModalComponent; - - return ( - <> - - - { - if (lensAttributes) { - lens.navigateToPrefilledEditor( - { - id: '', - timeRange, - attributes: lensAttributes, - }, - true - ); - } - }} - > - {i18n.translate('xpack.observability.expView.heading.openInLens', { - defaultMessage: 'Open in Lens', - })} - - - - { - if (lensAttributes) { - setIsSaveOpen(true); - } - }} - size="s" - > - {i18n.translate('xpack.observability.expView.heading.saveLensVisualization', { - defaultMessage: 'Save', - })} - - - - - {isSaveOpen && lensAttributes && ( - setIsSaveOpen(false)} - // if we want to do anything after the viz is saved - // right now there is no action, so an empty function - onSave={() => {}} - /> - )} - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx deleted file mode 100644 index 23500b63e900a..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx +++ /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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { ExpViewActionMenuContent } from './action_menu'; -import HeaderMenuPortal from '../../../header_menu_portal'; -import { usePluginContext } from '../../../../../hooks/use_plugin_context'; -import { TypedLensByValueInput } from '../../../../../../../lens/public'; - -interface Props { - timeRange?: { from: string; to: string }; - lensAttributes: TypedLensByValueInput['attributes'] | null; -} -export function ExpViewActionMenu(props: Props) { - const { appMountParameters } = usePluginContext(); - - return ( - - - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx index d17e451ef702c..3566835b1701c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx @@ -10,19 +10,19 @@ import { isEmpty } from 'lodash'; import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer, EuiText } from '@elastic/eui'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; -import { LOADING_VIEW } from '../series_editor/series_editor'; -import { ReportViewType, SeriesUrl } from '../types'; +import { LOADING_VIEW } from '../series_builder/series_builder'; +import { SeriesUrl } from '../types'; export function EmptyView({ loading, + height, series, - reportType, }: { loading: boolean; - series?: SeriesUrl; - reportType: ReportViewType; + height: string; + series: SeriesUrl; }) { - const { dataType, reportDefinitions } = series ?? {}; + const { dataType, reportType, reportDefinitions } = series ?? {}; let emptyMessage = EMPTY_LABEL; @@ -45,7 +45,7 @@ export function EmptyView({ } return ( - + {loading && ( ` text-align: center; + height: ${(props) => props.height}; position: relative; `; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx index 03fd23631f755..fe2953edd36d6 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { fireEvent, screen, waitFor } from '@testing-library/react'; -import { mockAppIndexPattern, mockIndexPattern, mockUxSeries, render } from '../rtl_helpers'; +import { mockAppIndexPattern, mockIndexPattern, render } from '../rtl_helpers'; import { FilterLabel } from './filter_label'; import * as useSeriesHook from '../hooks/use_series_filters'; import { buildFilterLabel } from '../../filter_value_label/filter_value_label'; @@ -27,10 +27,9 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={false} - seriesId={0} + seriesId={'kpi-over-time'} removeFilter={jest.fn()} indexPattern={mockIndexPattern} - series={mockUxSeries} /> ); @@ -52,10 +51,9 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={false} - seriesId={0} + seriesId={'kpi-over-time'} removeFilter={removeFilter} indexPattern={mockIndexPattern} - series={mockUxSeries} /> ); @@ -76,10 +74,9 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={false} - seriesId={0} + seriesId={'kpi-over-time'} removeFilter={removeFilter} indexPattern={mockIndexPattern} - series={mockUxSeries} /> ); @@ -103,10 +100,9 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={true} - seriesId={0} + seriesId={'kpi-over-time'} removeFilter={jest.fn()} indexPattern={mockIndexPattern} - series={mockUxSeries} /> ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx index c6254a85de9ac..a08e777c5ea71 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx @@ -9,24 +9,21 @@ import React from 'react'; import { IndexPattern } from '../../../../../../../../src/plugins/data/public'; import { useSeriesFilters } from '../hooks/use_series_filters'; import { FilterValueLabel } from '../../filter_value_label/filter_value_label'; -import { SeriesUrl } from '../types'; interface Props { field: string; label: string; - value: string | string[]; - seriesId: number; - series: SeriesUrl; + value: string; + seriesId: string; negate: boolean; definitionFilter?: boolean; indexPattern: IndexPattern; - removeFilter: (field: string, value: string | string[], notVal: boolean) => void; + removeFilter: (field: string, value: string, notVal: boolean) => void; } export function FilterLabel({ label, seriesId, - series, field, value, negate, @@ -34,7 +31,7 @@ export function FilterLabel({ removeFilter, definitionFilter, }: Props) { - const { invertFilter } = useSeriesFilters({ seriesId, series }); + const { invertFilter } = useSeriesFilters({ seriesId }); return indexPattern ? ( { - setSeries(seriesId, { ...series, color: colorN }); - }; - - const color = - series.color ?? ((theme.eui as unknown) as Record)[`euiColorVis${seriesId}`]; - - const button = ( - - setIsOpen((prevState) => !prevState)} hasArrow={false}> - - - - ); - - return ( - setIsOpen(false)}> - - - - - ); -} - -const PICK_A_COLOR_LABEL = i18n.translate( - 'xpack.observability.overview.exploratoryView.pickColor', - { - defaultMessage: 'Pick a color', - } -); - -const EDIT_SERIES_COLOR_LABEL = i18n.translate( - 'xpack.observability.overview.exploratoryView.editSeriesColor', - { - defaultMessage: 'Edit color for series', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx deleted file mode 100644 index 23d6589fecbcb..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import moment from 'moment'; -import { EuiSuperDatePicker, EuiText } from '@elastic/eui'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; - -import { useHasData } from '../../../../../hooks/use_has_data'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { useQuickTimeRanges } from '../../../../../hooks/use_quick_time_ranges'; -import { parseTimeParts } from '../../series_viewer/columns/utils'; -import { useUiSetting } from '../../../../../../../../../src/plugins/kibana_react/public'; -import { SeriesUrl } from '../../types'; -import { ReportTypes } from '../../configurations/constants'; - -export interface TimePickerTime { - from: string; - to: string; -} - -export interface TimePickerQuickRange extends TimePickerTime { - display: string; -} - -interface Props { - seriesId: number; - series: SeriesUrl; - readonly?: boolean; -} -const readableUnit: Record = { - m: i18n.translate('xpack.observability.overview.exploratoryView.minutes', { - defaultMessage: 'Minutes', - }), - h: i18n.translate('xpack.observability.overview.exploratoryView.hour', { - defaultMessage: 'Hour', - }), - d: i18n.translate('xpack.observability.overview.exploratoryView.day', { - defaultMessage: 'Day', - }), -}; - -export function SeriesDatePicker({ series, seriesId, readonly = true }: Props) { - const { onRefreshTimeRange } = useHasData(); - - const commonlyUsedRanges = useQuickTimeRanges(); - - const { setSeries, reportType, allSeries, firstSeries } = useSeriesStorage(); - - function onTimeChange({ start, end }: { start: string; end: string }) { - onRefreshTimeRange(); - if (reportType === ReportTypes.KPI) { - allSeries.forEach((currSeries, seriesIndex) => { - setSeries(seriesIndex, { ...currSeries, time: { from: start, to: end } }); - }); - } else { - setSeries(seriesId, { ...series, time: { from: start, to: end } }); - } - } - - const seriesTime = series.time ?? firstSeries!.time; - - const dateFormat = useUiSetting('dateFormat').replace('ss.SSS', 'ss'); - - if (readonly) { - const timeParts = parseTimeParts(seriesTime?.from, seriesTime?.to); - - if (timeParts) { - const { - timeTense: timeTenseDefault, - timeUnits: timeUnitsDefault, - timeValue: timeValueDefault, - } = timeParts; - - return ( - {`${timeTenseDefault} ${timeValueDefault} ${ - readableUnit?.[timeUnitsDefault] ?? timeUnitsDefault - }`} - ); - } else { - return ( - - {i18n.translate('xpack.observability.overview.exploratoryView.dateRangeReadonly', { - defaultMessage: '{start} to {end}', - values: { - start: moment(seriesTime.from).format(dateFormat), - end: moment(seriesTime.to).format(dateFormat), - }, - })} - - ); - } - } - - return ( - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts index bf5feb7d5863c..ba1f2214223e3 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts @@ -94,19 +94,6 @@ export const DataViewLabels: Record = { 'device-data-distribution': DEVICE_DISTRIBUTION_LABEL, }; -export enum ReportTypes { - KPI = 'kpi-over-time', - DISTRIBUTION = 'data-distribution', - CORE_WEB_VITAL = 'core-web-vitals', - DEVICE_DISTRIBUTION = 'device-data-distribution', -} - -export enum DataTypes { - SYNTHETICS = 'synthetics', - UX = 'ux', - MOBILE = 'mobile', -} - export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN'; export const FILTER_RECORDS = 'FILTER_RECORDS'; export const TERMS_COLUMN = 'TERMS_COLUMN'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts index 55ac75b47c056..6f990015fbc62 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts @@ -8,12 +8,10 @@ export enum URL_KEYS { DATA_TYPE = 'dt', OPERATION_TYPE = 'op', + REPORT_TYPE = 'rt', SERIES_TYPE = 'st', BREAK_DOWN = 'bd', FILTERS = 'ft', REPORT_DEFINITIONS = 'rdf', SELECTED_METRIC = 'mt', - HIDDEN = 'h', - NAME = 'n', - COLOR = 'c', } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts index 3f6551986527c..574a9f6a2bc10 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts @@ -15,7 +15,6 @@ import { getCoreWebVitalsConfig } from './rum/core_web_vitals_config'; import { getMobileKPIConfig } from './mobile/kpi_over_time_config'; import { getMobileKPIDistributionConfig } from './mobile/distribution_config'; import { getMobileDeviceDistributionConfig } from './mobile/device_distribution_config'; -import { DataTypes, ReportTypes } from './constants'; interface Props { reportType: ReportViewType; @@ -25,24 +24,24 @@ interface Props { export const getDefaultConfigs = ({ reportType, dataType, indexPattern }: Props) => { switch (dataType) { - case DataTypes.UX: - if (reportType === ReportTypes.DISTRIBUTION) { + case 'ux': + if (reportType === 'data-distribution') { return getRumDistributionConfig({ indexPattern }); } - if (reportType === ReportTypes.CORE_WEB_VITAL) { + if (reportType === 'core-web-vitals') { return getCoreWebVitalsConfig({ indexPattern }); } return getKPITrendsLensConfig({ indexPattern }); - case DataTypes.SYNTHETICS: - if (reportType === ReportTypes.DISTRIBUTION) { + case 'synthetics': + if (reportType === 'data-distribution') { return getSyntheticsDistributionConfig({ indexPattern }); } return getSyntheticsKPIConfig({ indexPattern }); - case DataTypes.MOBILE: - if (reportType === ReportTypes.DISTRIBUTION) { + case 'mobile': + if (reportType === 'data-distribution') { return getMobileKPIDistributionConfig({ indexPattern }); } - if (reportType === ReportTypes.DEVICE_DISTRIBUTION) { + if (reportType === 'device-data-distribution') { return getMobileDeviceDistributionConfig({ indexPattern }); } return getMobileKPIConfig({ indexPattern }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts index 08d2da4714e47..ae70bbdcfa3b8 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts @@ -16,7 +16,7 @@ import { } from './constants/elasticsearch_fieldnames'; import { buildExistsFilter, buildPhrasesFilter } from './utils'; import { sampleAttributeKpi } from './test_data/sample_attribute_kpi'; -import { RECORDS_FIELD, REPORT_METRIC_FIELD, ReportTypes } from './constants'; +import { REPORT_METRIC_FIELD } from './constants'; describe('Lens Attribute', () => { mockAppIndexPattern(); @@ -38,9 +38,6 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, reportDefinitions: {}, time: { from: 'now-15m', to: 'now' }, - color: 'green', - name: 'test-series', - selectedMetricField: TRANSACTION_DURATION, }; beforeEach(() => { @@ -53,7 +50,7 @@ describe('Lens Attribute', () => { it('should return expected json for kpi report type', function () { const seriesConfigKpi = getDefaultConfigs({ - reportType: ReportTypes.KPI, + reportType: 'kpi-over-time', dataType: 'ux', indexPattern: mockIndexPattern, }); @@ -66,9 +63,6 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, reportDefinitions: { 'service.name': ['elastic-co'] }, time: { from: 'now-15m', to: 'now' }, - color: 'green', - name: 'test-series', - selectedMetricField: RECORDS_FIELD, }, ]); @@ -141,9 +135,6 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, reportDefinitions: { 'performance.metric': [LCP_FIELD] }, time: { from: 'now-15m', to: 'now' }, - color: 'green', - name: 'test-series', - selectedMetricField: TRANSACTION_DURATION, }; lnsAttr = new LensAttributes([layerConfig1]); @@ -286,7 +277,7 @@ describe('Lens Attribute', () => { 'transaction.type: page-load and processor.event: transaction and transaction.type : *', }, isBucketed: false, - label: 'test-series', + label: 'Pages loaded', operationType: 'formula', params: { format: { @@ -392,7 +383,7 @@ describe('Lens Attribute', () => { palette: undefined, seriesType: 'line', xAccessor: 'x-axis-column-layer0', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0' }], + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], }, ], legend: { isVisible: true, position: 'right' }, @@ -412,9 +403,6 @@ describe('Lens Attribute', () => { reportDefinitions: { 'performance.metric': [LCP_FIELD] }, breakdown: USER_AGENT_NAME, time: { from: 'now-15m', to: 'now' }, - color: 'green', - name: 'test-series', - selectedMetricField: TRANSACTION_DURATION, }; lnsAttr = new LensAttributes([layerConfig1]); @@ -434,7 +422,7 @@ describe('Lens Attribute', () => { seriesType: 'line', splitAccessor: 'breakdown-column-layer0', xAccessor: 'x-axis-column-layer0', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0' }], + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], }, ]); @@ -495,7 +483,7 @@ describe('Lens Attribute', () => { 'transaction.type: page-load and processor.event: transaction and transaction.type : *', }, isBucketed: false, - label: 'test-series', + label: 'Pages loaded', operationType: 'formula', params: { format: { @@ -601,9 +589,6 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, reportDefinitions: { 'performance.metric': [LCP_FIELD] }, time: { from: 'now-15m', to: 'now' }, - color: 'green', - name: 'test-series', - selectedMetricField: TRANSACTION_DURATION, }; const filters = lnsAttr.getLayerFilters(layerConfig1, 2); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index 5426d3bcd4233..dfb17ee470d35 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import { capitalize } from 'lodash'; - import { CountIndexPatternColumn, DateHistogramIndexPatternColumn, @@ -37,11 +36,10 @@ import { REPORT_METRIC_FIELD, RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD, - ReportTypes, } from './constants'; import { ColumnFilter, SeriesConfig, UrlFilter, URLReportDefinition } from '../types'; import { PersistableFilter } from '../../../../../../lens/common'; -import { parseAbsoluteDate } from '../components/date_range_picker'; +import { parseAbsoluteDate } from '../series_date_picker/date_range_picker'; import { getDistributionInPercentageColumn } from './lens_columns/overall_column'; function getLayerReferenceName(layerId: string) { @@ -75,6 +73,14 @@ export const parseCustomFieldName = (seriesConfig: SeriesConfig, selectedMetricF timeScale = currField?.timeScale; columnLabel = currField?.label; } + } else if (metricOptions?.[0].field || metricOptions?.[0].id) { + const firstMetricOption = metricOptions?.[0]; + + selectedMetricField = firstMetricOption.field || firstMetricOption.id; + columnType = firstMetricOption.columnType; + columnFilters = firstMetricOption.columnFilters; + timeScale = firstMetricOption.timeScale; + columnLabel = firstMetricOption.label; } return { fieldName: selectedMetricField!, columnType, columnFilters, timeScale, columnLabel }; @@ -89,9 +95,7 @@ export interface LayerConfig { reportDefinitions: URLReportDefinition; time: { to: string; from: string }; indexPattern: IndexPattern; - selectedMetricField: string; - color: string; - name: string; + selectedMetricField?: string; } export class LensAttributes { @@ -467,15 +471,14 @@ export class LensAttributes { getLayerFilters(layerConfig: LayerConfig, totalLayers: number) { const { filters, - time, + time: { from, to }, seriesConfig: { baseFilters: layerFilters, reportType }, } = layerConfig; let baseFilters = ''; - - if (reportType !== ReportTypes.KPI && totalLayers > 1 && time) { + if (reportType !== 'kpi-over-time' && totalLayers > 1) { // for kpi over time, we don't need to add time range filters // since those are essentially plotted along the x-axis - baseFilters += `@timestamp >= ${time.from} and @timestamp <= ${time.to}`; + baseFilters += `@timestamp >= ${from} and @timestamp <= ${to}`; } layerFilters?.forEach((filter: PersistableFilter | ExistsFilter) => { @@ -531,11 +534,7 @@ export class LensAttributes { } getTimeShift(mainLayerConfig: LayerConfig, layerConfig: LayerConfig, index: number) { - if ( - index === 0 || - mainLayerConfig.seriesConfig.reportType !== ReportTypes.KPI || - !layerConfig.time - ) { + if (index === 0 || mainLayerConfig.seriesConfig.reportType !== 'kpi-over-time') { return null; } @@ -547,14 +546,11 @@ export class LensAttributes { time: { from }, } = layerConfig; - const inDays = Math.abs(parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days')); + const inDays = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days'); if (inDays > 1) { return inDays + 'd'; } - const inHours = Math.abs(parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours')); - if (inHours === 0) { - return null; - } + const inHours = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours'); return inHours + 'h'; } @@ -572,12 +568,6 @@ export class LensAttributes { const { sourceField } = seriesConfig.xAxisColumn; - let label = timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label; - - if (layerConfig.seriesConfig.reportType !== ReportTypes.CORE_WEB_VITAL && layerConfig.name) { - label = layerConfig.name; - } - layers[layerId] = { columnOrder: [ `x-axis-column-${layerId}`, @@ -591,7 +581,7 @@ export class LensAttributes { [`x-axis-column-${layerId}`]: this.getXAxis(layerConfig, layerId), [`y-axis-column-${layerId}`]: { ...mainYAxis, - label, + label: timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label, filter: { query: columnFilter, language: 'kuery' }, ...(timeShift ? { timeShift } : {}), }, @@ -634,7 +624,7 @@ export class LensAttributes { seriesType: layerConfig.seriesType || layerConfig.seriesConfig.defaultSeriesType, palette: layerConfig.seriesConfig.palette, yConfig: layerConfig.seriesConfig.yConfig || [ - { forAccessor: `y-axis-column-layer${index}`, color: layerConfig.color }, + { forAccessor: `y-axis-column-layer${index}` }, ], xAccessor: `x-axis-column-layer${index}`, ...(layerConfig.breakdown && @@ -648,7 +638,7 @@ export class LensAttributes { }; } - getJSON(refresh?: number): TypedLensByValueInput['attributes'] { + getJSON(): TypedLensByValueInput['attributes'] { const uniqueIndexPatternsIds = Array.from( new Set([...this.layerConfigs.map(({ indexPattern }) => indexPattern.id)]) ); @@ -657,7 +647,7 @@ export class LensAttributes { return { title: 'Prefilled from exploratory view app', - description: String(refresh), + description: '', visualizationType: 'lnsXY', references: [ ...uniqueIndexPatternsIds.map((patternId) => ({ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts index 4e178bba7e02a..d1612a08f5551 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts @@ -6,7 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, REPORT_METRIC_FIELD, ReportTypes, USE_BREAK_DOWN_COLUMN } from '../constants'; +import { FieldLabels, REPORT_METRIC_FIELD, USE_BREAK_DOWN_COLUMN } from '../constants'; import { buildPhraseFilter } from '../utils'; import { SERVICE_NAME } from '../constants/elasticsearch_fieldnames'; import { MOBILE_APP, NUMBER_OF_DEVICES } from '../constants/labels'; @@ -14,7 +14,7 @@ import { MobileFields } from './mobile_fields'; export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: ReportTypes.DEVICE_DISTRIBUTION, + reportType: 'device-data-distribution', defaultSeriesType: 'bar', seriesTypes: ['bar', 'bar_horizontal'], xAxisColumn: { @@ -38,13 +38,13 @@ export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps) ...MobileFields, [SERVICE_NAME]: MOBILE_APP, }, - definitionFields: [SERVICE_NAME], metricOptions: [ { - field: 'labels.device_id', id: 'labels.device_id', + field: 'labels.device_id', label: NUMBER_OF_DEVICES, }, ], + definitionFields: [SERVICE_NAME], }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts index 1da27be4fcc95..9b1c4c8da3e9b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts @@ -6,7 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD, ReportTypes } from '../constants'; +import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; import { buildPhrasesFilter } from '../utils'; import { METRIC_SYSTEM_CPU_USAGE, @@ -21,7 +21,7 @@ import { MobileFields } from './mobile_fields'; export function getMobileKPIDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: ReportTypes.DISTRIBUTION, + reportType: 'data-distribution', defaultSeriesType: 'bar', seriesTypes: ['line', 'bar'], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts index 3ee5b3125fcda..945a631078a33 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts @@ -6,13 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { - FieldLabels, - OPERATION_COLUMN, - RECORDS_FIELD, - REPORT_METRIC_FIELD, - ReportTypes, -} from '../constants'; +import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; import { buildPhrasesFilter } from '../utils'; import { METRIC_SYSTEM_CPU_USAGE, @@ -32,7 +26,7 @@ import { MobileFields } from './mobile_fields'; export function getMobileKPIConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: ReportTypes.KPI, + reportType: 'kpi-over-time', defaultSeriesType: 'line', seriesTypes: ['line', 'bar', 'area'], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts index 35e094996f6f2..07bb13f957e45 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts @@ -9,7 +9,7 @@ import { mockAppIndexPattern, mockIndexPattern } from '../../rtl_helpers'; import { getDefaultConfigs } from '../default_configs'; import { LayerConfig, LensAttributes } from '../lens_attributes'; import { sampleAttributeCoreWebVital } from '../test_data/sample_attribute_cwv'; -import { LCP_FIELD, SERVICE_NAME, USER_AGENT_OS } from '../constants/elasticsearch_fieldnames'; +import { SERVICE_NAME, USER_AGENT_OS } from '../constants/elasticsearch_fieldnames'; describe('Core web vital config test', function () { mockAppIndexPattern(); @@ -24,13 +24,10 @@ describe('Core web vital config test', function () { const layerConfig: LayerConfig = { seriesConfig, - color: 'green', - name: 'test-series', - breakdown: USER_AGENT_OS, indexPattern: mockIndexPattern, - time: { from: 'now-15m', to: 'now' }, reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] }, - selectedMetricField: LCP_FIELD, + time: { from: 'now-15m', to: 'now' }, + breakdown: USER_AGENT_OS, }; beforeEach(() => { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts index e8d620388a89e..62455df248085 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts @@ -11,7 +11,6 @@ import { FieldLabels, FILTER_RECORDS, REPORT_METRIC_FIELD, - ReportTypes, USE_BREAK_DOWN_COLUMN, } from '../constants'; import { buildPhraseFilter } from '../utils'; @@ -39,7 +38,7 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesCon return { defaultSeriesType: 'bar_horizontal_percentage_stacked', - reportType: ReportTypes.CORE_WEB_VITAL, + reportType: 'core-web-vitals', seriesTypes: ['bar_horizontal_percentage_stacked'], xAxisColumn: { sourceField: USE_BREAK_DOWN_COLUMN, @@ -154,6 +153,5 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesCon { color: statusPallete[1], forAccessor: 'y-axis-column-1' }, { color: statusPallete[2], forAccessor: 'y-axis-column-2' }, ], - query: { query: 'transaction.type: "page-load"', language: 'kuery' }, }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts index de6f2c67b2aeb..f34c8db6c197d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts @@ -6,12 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { - FieldLabels, - REPORT_METRIC_FIELD, - RECORDS_PERCENTAGE_FIELD, - ReportTypes, -} from '../constants'; +import { FieldLabels, REPORT_METRIC_FIELD, RECORDS_PERCENTAGE_FIELD } from '../constants'; import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, @@ -46,7 +41,7 @@ import { export function getRumDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: ReportTypes.DISTRIBUTION, + reportType: 'data-distribution', defaultSeriesType: 'line', seriesTypes: [], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts index 9112778eadaa7..5899b16d12b4f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts @@ -6,13 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { - FieldLabels, - OPERATION_COLUMN, - RECORDS_FIELD, - REPORT_METRIC_FIELD, - ReportTypes, -} from '../constants'; +import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, @@ -49,7 +43,7 @@ export function getKPITrendsLensConfig({ indexPattern }: ConfigProps): SeriesCon return { defaultSeriesType: 'bar_stacked', seriesTypes: [], - reportType: ReportTypes.KPI, + reportType: 'kpi-over-time', xAxisColumn: { sourceField: '@timestamp', }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts index da90f45d15201..730e742f9d8c5 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts @@ -6,12 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { - FieldLabels, - REPORT_METRIC_FIELD, - RECORDS_PERCENTAGE_FIELD, - ReportTypes, -} from '../constants'; +import { FieldLabels, REPORT_METRIC_FIELD, RECORDS_PERCENTAGE_FIELD } from '../constants'; import { CLS_LABEL, DCL_LABEL, @@ -35,7 +30,7 @@ export function getSyntheticsDistributionConfig({ indexPattern, }: ConfigProps): SeriesConfig { return { - reportType: ReportTypes.DISTRIBUTION, + reportType: 'data-distribution', defaultSeriesType: series?.seriesType || 'line', seriesTypes: [], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts index 65b43a83a8fb5..4ee22181d4334 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts @@ -6,7 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, OPERATION_COLUMN, REPORT_METRIC_FIELD, ReportTypes } from '../constants'; +import { FieldLabels, OPERATION_COLUMN, REPORT_METRIC_FIELD } from '../constants'; import { CLS_LABEL, DCL_LABEL, @@ -30,7 +30,7 @@ const SUMMARY_DOWN = 'summary.down'; export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: ReportTypes.KPI, + reportType: 'kpi-over-time', defaultSeriesType: 'bar_stacked', seriesTypes: [], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts index a5898f33e0ec0..569d68ad4ebff 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts @@ -5,18 +5,12 @@ * 2.0. */ export const sampleAttribute = { - description: 'undefined', + title: 'Prefilled from exploratory view app', + description: '', + visualizationType: 'lnsXY', references: [ - { - id: 'apm-*', - name: 'indexpattern-datasource-current-indexpattern', - type: 'index-pattern', - }, - { - id: 'apm-*', - name: 'indexpattern-datasource-layer-layer0', - type: 'index-pattern', - }, + { id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' }, + { id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' }, ], state: { datasourceStates: { @@ -34,23 +28,17 @@ export const sampleAttribute = { ], columns: { 'x-axis-column-layer0': { - dataType: 'number', - isBucketed: true, + sourceField: 'transaction.duration.us', label: 'Page load time', + dataType: 'number', operationType: 'range', + isBucketed: true, + scale: 'interval', params: { - maxBars: 'auto', - ranges: [ - { - from: 0, - label: '', - to: 1000, - }, - ], type: 'histogram', + ranges: [{ from: 0, to: 1000, label: '' }], + maxBars: 'auto', }, - scale: 'interval', - sourceField: 'transaction.duration.us', }, 'y-axis-column-layer0': { dataType: 'number', @@ -60,7 +48,7 @@ export const sampleAttribute = { 'transaction.type: page-load and processor.event: transaction and transaction.type : *', }, isBucketed: false, - label: 'test-series', + label: 'Pages loaded', operationType: 'formula', params: { format: { @@ -93,16 +81,16 @@ export const sampleAttribute = { 'y-axis-column-layer0X1': { customLabel: true, dataType: 'number', - filter: { - language: 'kuery', - query: - 'transaction.type: page-load and processor.event: transaction and transaction.type : *', - }, isBucketed: false, label: 'Part of count() / overall_sum(count())', operationType: 'count', scale: 'ratio', sourceField: 'Records', + filter: { + language: 'kuery', + query: + 'transaction.type: page-load and processor.event: transaction and transaction.type : *', + }, }, 'y-axis-column-layer0X2': { customLabel: true, @@ -153,51 +141,26 @@ export const sampleAttribute = { }, }, }, - filters: [], - query: { - language: 'kuery', - query: 'transaction.duration.us < 60000000', - }, visualization: { - axisTitlesVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, - curveType: 'CURVE_MONOTONE_X', + legend: { isVisible: true, position: 'right' }, + valueLabels: 'hide', fittingFunction: 'Linear', - gridlinesVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, + curveType: 'CURVE_MONOTONE_X', + axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, + gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + preferredSeriesType: 'line', layers: [ { accessors: ['y-axis-column-layer0'], layerId: 'layer0', seriesType: 'line', + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], xAccessor: 'x-axis-column-layer0', - yConfig: [ - { - color: 'green', - forAccessor: 'y-axis-column-layer0', - }, - ], }, ], - legend: { - isVisible: true, - position: 'right', - }, - preferredSeriesType: 'line', - tickLabelsVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, - valueLabels: 'hide', }, + query: { query: 'transaction.duration.us < 60000000', language: 'kuery' }, + filters: [], }, - title: 'Prefilled from exploratory view app', - visualizationType: 'lnsXY', }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts index 425bf069cc87f..2087b85b81886 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts @@ -5,7 +5,7 @@ * 2.0. */ export const sampleAttributeCoreWebVital = { - description: 'undefined', + description: '', references: [ { id: 'apm-*', @@ -94,7 +94,7 @@ export const sampleAttributeCoreWebVital = { filters: [], query: { language: 'kuery', - query: 'transaction.type: "page-load"', + query: '', }, visualization: { axisTitlesVisibilitySettings: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts index 85bafdecabde0..7f066caf66bf1 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts @@ -5,18 +5,12 @@ * 2.0. */ export const sampleAttributeKpi = { - description: 'undefined', + title: 'Prefilled from exploratory view app', + description: '', + visualizationType: 'lnsXY', references: [ - { - id: 'apm-*', - name: 'indexpattern-datasource-current-indexpattern', - type: 'index-pattern', - }, - { - id: 'apm-*', - name: 'indexpattern-datasource-layer-layer0', - type: 'index-pattern', - }, + { id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' }, + { id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' }, ], state: { datasourceStates: { @@ -26,27 +20,25 @@ export const sampleAttributeKpi = { columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'], columns: { 'x-axis-column-layer0': { + sourceField: '@timestamp', dataType: 'date', isBucketed: true, label: '@timestamp', operationType: 'date_histogram', - params: { - interval: 'auto', - }, + params: { interval: 'auto' }, scale: 'interval', - sourceField: '@timestamp', }, 'y-axis-column-layer0': { dataType: 'number', - filter: { - language: 'kuery', - query: 'transaction.type: page-load and processor.event: transaction', - }, isBucketed: false, - label: 'test-series', + label: 'Page views', operationType: 'count', scale: 'ratio', sourceField: 'Records', + filter: { + query: 'transaction.type: page-load and processor.event: transaction', + language: 'kuery', + }, }, }, incompleteColumns: {}, @@ -54,51 +46,26 @@ export const sampleAttributeKpi = { }, }, }, - filters: [], - query: { - language: 'kuery', - query: '', - }, visualization: { - axisTitlesVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, - curveType: 'CURVE_MONOTONE_X', + legend: { isVisible: true, position: 'right' }, + valueLabels: 'hide', fittingFunction: 'Linear', - gridlinesVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, + curveType: 'CURVE_MONOTONE_X', + axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, + gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + preferredSeriesType: 'line', layers: [ { accessors: ['y-axis-column-layer0'], layerId: 'layer0', seriesType: 'line', + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], xAccessor: 'x-axis-column-layer0', - yConfig: [ - { - color: 'green', - forAccessor: 'y-axis-column-layer0', - }, - ], }, ], - legend: { - isVisible: true, - position: 'right', - }, - preferredSeriesType: 'line', - tickLabelsVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, - valueLabels: 'hide', }, + query: { query: '', language: 'kuery' }, + filters: [], }, - title: 'Prefilled from exploratory view app', - visualizationType: 'lnsXY', }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts index 694250e5749cb..f7df2939d9909 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ import rison, { RisonValue } from 'rison-node'; -import type { ReportViewType, SeriesUrl, UrlFilter } from '../types'; +import type { SeriesUrl, UrlFilter } from '../types'; import type { AllSeries, AllShortSeries } from '../hooks/use_series_storage'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; import { esFilters, ExistsFilter } from '../../../../../../../../src/plugins/data/public'; @@ -16,43 +16,40 @@ export function convertToShortUrl(series: SeriesUrl) { const { operationType, seriesType, + reportType, breakdown, filters, reportDefinitions, dataType, selectedMetricField, - hidden, - name, - color, ...restSeries } = series; return { [URL_KEYS.OPERATION_TYPE]: operationType, + [URL_KEYS.REPORT_TYPE]: reportType, [URL_KEYS.SERIES_TYPE]: seriesType, [URL_KEYS.BREAK_DOWN]: breakdown, [URL_KEYS.FILTERS]: filters, [URL_KEYS.REPORT_DEFINITIONS]: reportDefinitions, [URL_KEYS.DATA_TYPE]: dataType, [URL_KEYS.SELECTED_METRIC]: selectedMetricField, - [URL_KEYS.HIDDEN]: hidden, - [URL_KEYS.NAME]: name, - [URL_KEYS.COLOR]: color, ...restSeries, }; } -export function createExploratoryViewUrl( - { reportType, allSeries }: { reportType: ReportViewType; allSeries: AllSeries }, - baseHref = '' -) { - const allShortSeries: AllShortSeries = allSeries.map((series) => convertToShortUrl(series)); +export function createExploratoryViewUrl(allSeries: AllSeries, baseHref = '') { + const allSeriesIds = Object.keys(allSeries); + + const allShortSeries: AllShortSeries = {}; + + allSeriesIds.forEach((seriesKey) => { + allShortSeries[seriesKey] = convertToShortUrl(allSeries[seriesKey]); + }); return ( baseHref + - `/app/observability/exploratory-view/configure#?reportType=${reportType}&sr=${rison.encode( - (allShortSeries as unknown) as RisonValue - )}` + `/app/observability/exploratory-view#?sr=${rison.encode(allShortSeries as RisonValue)}` ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx index 21c749258bebe..989ebf17c2062 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx @@ -11,13 +11,6 @@ import { render, mockCore, mockAppIndexPattern } from './rtl_helpers'; import { ExploratoryView } from './exploratory_view'; import { getStubIndexPattern } from '../../../../../../../src/plugins/data/public/test_utils'; import * as obsvInd from './utils/observability_index_patterns'; -import * as pluginHook from '../../../hooks/use_plugin_context'; - -jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ - appMountParameters: { - setHeaderActionMenu: jest.fn(), - }, -} as any); describe('ExploratoryView', () => { mockAppIndexPattern(); @@ -48,18 +41,29 @@ describe('ExploratoryView', () => { it('renders exploratory view', async () => { render(); - expect(await screen.findByText(/Preview/i)).toBeInTheDocument(); - expect(await screen.findByText(/Configure series/i)).toBeInTheDocument(); - expect(await screen.findByText(/Hide chart/i)).toBeInTheDocument(); - expect(await screen.findByText(/Refresh/i)).toBeInTheDocument(); + expect(await screen.findByText(/open in lens/i)).toBeInTheDocument(); expect( await screen.findByRole('heading', { name: /Performance Distribution/i }) ).toBeInTheDocument(); }); it('renders lens component when there is series', async () => { - render(); + const initSeries = { + data: { + 'ux-series': { + isNew: true, + dataType: 'ux' as const, + reportType: 'data-distribution' as const, + breakdown: 'user_agent .name', + reportDefinitions: { 'service.name': ['elastic-co'] }, + time: { from: 'now-15m', to: 'now' }, + }, + }, + }; + + render(, { initSeries }); + expect(await screen.findByText(/open in lens/i)).toBeInTheDocument(); expect((await screen.findAllByText('Performance distribution'))[0]).toBeInTheDocument(); expect(await screen.findByText(/Lens Embeddable Component/i)).toBeInTheDocument(); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx index cb901b8b588f3..af04108c56790 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx @@ -4,13 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { i18n } from '@kbn/i18n'; import React, { useEffect, useRef, useState } from 'react'; -import { EuiButtonEmpty, EuiPanel, EuiResizableContainer, EuiTitle } from '@elastic/eui'; +import { EuiPanel, EuiTitle } from '@elastic/eui'; import styled from 'styled-components'; -import { useRouteMatch } from 'react-router-dom'; -import { PanelDirection } from '@elastic/eui/src/components/resizable_container/types'; +import { isEmpty } from 'lodash'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { ExploratoryViewHeader } from './header/header'; @@ -18,15 +16,40 @@ import { useSeriesStorage } from './hooks/use_series_storage'; import { useLensAttributes } from './hooks/use_lens_attributes'; import { TypedLensByValueInput } from '../../../../../lens/public'; import { useAppIndexPatternContext } from './hooks/use_app_index_pattern'; -import { SeriesViews } from './views/series_views'; +import { SeriesBuilder } from './series_builder/series_builder'; +import { SeriesUrl } from './types'; import { LensEmbeddable } from './lens_embeddable'; import { EmptyView } from './components/empty_view'; -export type PanelId = 'seriesPanel' | 'chartPanel'; +export const combineTimeRanges = ( + allSeries: Record, + firstSeries?: SeriesUrl +) => { + let to: string = ''; + let from: string = ''; + if (firstSeries?.reportType === 'kpi-over-time') { + return firstSeries.time; + } + Object.values(allSeries ?? {}).forEach((series) => { + if (series.dataType && series.reportType && !isEmpty(series.reportDefinitions)) { + const seriesTo = new Date(series.time.to); + const seriesFrom = new Date(series.time.from); + if (!to || seriesTo > new Date(to)) { + to = series.time.to; + } + if (!from || seriesFrom < new Date(from)) { + from = series.time.from; + } + } + }); + return { to, from }; +}; export function ExploratoryView({ saveAttributes, + multiSeries, }: { + multiSeries?: boolean; saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void; }) { const { @@ -46,19 +69,20 @@ export function ExploratoryView({ const { loadIndexPattern, loading } = useAppIndexPatternContext(); - const { firstSeries, allSeries, lastRefresh, reportType } = useSeriesStorage(); + const { firstSeries, firstSeriesId, allSeries } = useSeriesStorage(); const lensAttributesT = useLensAttributes(); const setHeightOffset = () => { if (seriesBuilderRef?.current && wrapperRef.current) { const headerOffset = wrapperRef.current.getBoundingClientRect().top; - setHeight(`calc(100vh - ${headerOffset + 40}px)`); + const seriesOffset = seriesBuilderRef.current.getBoundingClientRect().height; + setHeight(`calc(100vh - ${seriesOffset + headerOffset + 40}px)`); } }; useEffect(() => { - allSeries.forEach((seriesT) => { + Object.values(allSeries).forEach((seriesT) => { loadIndexPattern({ dataType: seriesT.dataType, }); @@ -72,104 +96,38 @@ export function ExploratoryView({ } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(lensAttributesT ?? {}), lastRefresh]); + }, [JSON.stringify(lensAttributesT ?? {})]); useEffect(() => { setHeightOffset(); }); - const collapseFn = useRef<(id: PanelId, direction: PanelDirection) => void>(); - - const [hiddenPanel, setHiddenPanel] = useState(''); - - const isPreview = !!useRouteMatch('/exploratory-view/preview'); - - const onCollapse = (panelId: string) => { - setHiddenPanel((prevState) => (panelId === prevState ? '' : panelId)); - }; - - const onChange = (panelId: PanelId) => { - onCollapse(panelId); - if (collapseFn.current) { - collapseFn.current(panelId, panelId === 'seriesPanel' ? 'right' : 'left'); - } - }; - return ( {lens ? ( <> - + - - {(EuiResizablePanel, EuiResizableButton, { togglePanel }) => { - collapseFn.current = (id, direction) => togglePanel?.(id, { direction }); - - return ( - <> - - {lensAttributes ? ( - - ) : ( - - )} - - - - {!isPreview && - (hiddenPanel === 'chartPanel' ? ( - onChange('chartPanel')} iconType="arrowDown"> - {SHOW_CHART_LABEL} - - ) : ( - onChange('chartPanel')} - iconType="arrowUp" - color="text" - > - {HIDE_CHART_LABEL} - - ))} - - - - ); - }} - - {hiddenPanel === 'seriesPanel' && ( - onChange('seriesPanel')} iconType="arrowUp"> - {PREVIEW_LABEL} - + {lensAttributes ? ( + + ) : ( + )} + ) : ( -

{LENS_NOT_AVAILABLE}

+

+ {i18n.translate('xpack.observability.overview.exploratoryView.lensDisabled', { + defaultMessage: + 'Lens app is not available, please enable Lens to use exploratory view.', + })} +

)}
@@ -189,39 +147,4 @@ const Wrapper = styled(EuiPanel)` margin: 0 auto; width: 100%; overflow-x: auto; - position: relative; -`; - -const ShowPreview = styled(EuiButtonEmpty)` - position: absolute; - bottom: 34px; -`; -const HideChart = styled(EuiButtonEmpty)` - position: absolute; - top: -35px; - right: 50px; `; -const ShowChart = styled(EuiButtonEmpty)` - position: absolute; - top: -10px; - right: 50px; -`; - -const HIDE_CHART_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.hideChart', { - defaultMessage: 'Hide chart', -}); - -const SHOW_CHART_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.showChart', { - defaultMessage: 'Show chart', -}); - -const PREVIEW_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.preview', { - defaultMessage: 'Preview', -}); - -const LENS_NOT_AVAILABLE = i18n.translate( - 'xpack.observability.overview.exploratoryView.lensDisabled', - { - defaultMessage: 'Lens app is not available, please enable Lens to use exploratory view.', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx index 1f910b946deb3..8cd8977fcf741 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx @@ -8,22 +8,51 @@ import React from 'react'; import { render } from '../rtl_helpers'; import { ExploratoryViewHeader } from './header'; -import * as pluginHook from '../../../../hooks/use_plugin_context'; - -jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ - appMountParameters: { - setHeaderActionMenu: jest.fn(), - }, -} as any); +import { fireEvent } from '@testing-library/dom'; describe('ExploratoryViewHeader', function () { it('should render properly', function () { const { getByText } = render( ); - getByText('Refresh'); + getByText('Open in Lens'); + }); + + it('should be able to click open in lens', function () { + const initSeries = { + data: { + 'uptime-pings-histogram': { + dataType: 'synthetics' as const, + reportType: 'kpi-over-time' as const, + breakdown: 'monitor.status', + time: { from: 'now-15m', to: 'now' }, + }, + }, + }; + + const { getByText, core } = render( + , + { initSeries } + ); + fireEvent.click(getByText('Open in Lens')); + + expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledTimes(1); + expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledWith( + { + attributes: { title: 'Performance distribution' }, + id: '', + timeRange: { + from: 'now-15m', + to: 'now', + }, + }, + true + ); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx index bec8673f88b4e..ded56ec9e817f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx @@ -5,37 +5,43 @@ * 2.0. */ -import React from 'react'; +import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiBetaBadge, EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { TypedLensByValueInput } from '../../../../../../lens/public'; +import { TypedLensByValueInput, LensEmbeddableInput } from '../../../../../../lens/public'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { DataViewLabels } from '../configurations/constants'; +import { ObservabilityAppServices } from '../../../../application/types'; import { useSeriesStorage } from '../hooks/use_series_storage'; -import { LastUpdated } from './last_updated'; -import { combineTimeRanges } from '../lens_embeddable'; -import { ExpViewActionMenu } from '../components/action_menu'; +import { combineTimeRanges } from '../exploratory_view'; interface Props { - seriesId?: number; - lastUpdated?: number; + seriesId: string; lensAttributes: TypedLensByValueInput['attributes'] | null; } -export function ExploratoryViewHeader({ seriesId, lensAttributes, lastUpdated }: Props) { - const { getSeries, allSeries, setLastRefresh, reportType } = useSeriesStorage(); +export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) { + const kServices = useKibana().services; - const series = seriesId ? getSeries(seriesId) : undefined; + const { lens } = kServices; - const timeRange = combineTimeRanges(reportType, allSeries, series); + const { getSeries, allSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + + const [isSaveOpen, setIsSaveOpen] = useState(false); + + const LensSaveModalComponent = lens.SaveModalComponent; + + const timeRange = combineTimeRanges(allSeries, series); return ( <> -

- {DataViewLabels[reportType] ?? + {DataViewLabels[series.reportType] ?? i18n.translate('xpack.observability.expView.heading.label', { defaultMessage: 'Analyze data', })}{' '} @@ -51,18 +57,53 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes, lastUpdated }: - + { + if (lensAttributes) { + lens.navigateToPrefilledEditor( + { + id: '', + timeRange, + attributes: lensAttributes, + }, + true + ); + } + }} + > + {i18n.translate('xpack.observability.expView.heading.openInLens', { + defaultMessage: 'Open in Lens', + })} + - setLastRefresh(Date.now())}> - {REFRESH_LABEL} + { + if (lensAttributes) { + setIsSaveOpen(true); + } + }} + > + {i18n.translate('xpack.observability.expView.heading.saveLensVisualization', { + defaultMessage: 'Save', + })} + + {isSaveOpen && lensAttributes && ( + setIsSaveOpen(false)} + onSave={() => {}} + /> + )} ); } - -const REFRESH_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.refresh', { - defaultMessage: 'Refresh', -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx index d65917093d129..7a5f12a72b1f0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx @@ -27,7 +27,7 @@ interface ProviderProps { } type HasAppDataState = Record; -export type IndexPatternState = Record; +type IndexPatternState = Record; type LoadingState = Record; export function IndexPatternContextProvider({ children }: ProviderProps) { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx deleted file mode 100644 index e86144c124949..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useCallback, useEffect, useState } from 'react'; -import { useKibana } from '../../../../utils/kibana_react'; -import { SeriesConfig, SeriesUrl } from '../types'; -import { useAppIndexPatternContext } from './use_app_index_pattern'; -import { buildExistsFilter, buildPhraseFilter, buildPhrasesFilter } from '../configurations/utils'; -import { getFiltersFromDefs } from './use_lens_attributes'; -import { RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD } from '../configurations/constants'; - -interface UseDiscoverLink { - seriesConfig: SeriesConfig; - series: SeriesUrl; -} - -export const useDiscoverLink = ({ series, seriesConfig }: UseDiscoverLink) => { - const kServices = useKibana().services; - const { - application: { navigateToUrl }, - } = kServices; - - const { indexPatterns } = useAppIndexPatternContext(); - - const urlGenerator = kServices.discover?.urlGenerator; - const [discoverUrl, setDiscoverUrl] = useState(''); - - useEffect(() => { - const indexPattern = indexPatterns?.[series.dataType]; - - const definitions = series.reportDefinitions ?? {}; - const filters = [...(seriesConfig?.baseFilters ?? [])]; - - const definitionFilters = getFiltersFromDefs(definitions); - - definitionFilters.forEach(({ field, values = [] }) => { - if (values.length > 1) { - filters.push(buildPhrasesFilter(field, values, indexPattern)[0]); - } else { - filters.push(buildPhraseFilter(field, values[0], indexPattern)[0]); - } - }); - - const selectedMetricField = series.selectedMetricField; - - if ( - selectedMetricField && - selectedMetricField !== RECORDS_FIELD && - selectedMetricField !== RECORDS_PERCENTAGE_FIELD - ) { - filters.push(buildExistsFilter(selectedMetricField, indexPattern)[0]); - } - - const getDiscoverUrl = async () => { - if (!urlGenerator?.createUrl) return; - - const newUrl = await urlGenerator.createUrl({ - filters, - indexPatternId: indexPattern?.id, - }); - setDiscoverUrl(newUrl); - }; - getDiscoverUrl(); - }, [ - indexPatterns, - series.dataType, - series.reportDefinitions, - series.selectedMetricField, - seriesConfig?.baseFilters, - urlGenerator, - ]); - - const onClick = useCallback( - (event: React.MouseEvent) => { - if (discoverUrl) { - event.preventDefault(); - - return navigateToUrl(discoverUrl); - } - }, - [discoverUrl, navigateToUrl] - ); - - return { - href: discoverUrl, - onClick, - }; -}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts index 71945734eeabc..8bb265b4f6d89 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts @@ -9,18 +9,12 @@ import { useMemo } from 'react'; import { isEmpty } from 'lodash'; import { TypedLensByValueInput } from '../../../../../../lens/public'; import { LayerConfig, LensAttributes } from '../configurations/lens_attributes'; -import { - AllSeries, - allSeriesKey, - convertAllShortSeries, - useSeriesStorage, -} from './use_series_storage'; +import { useSeriesStorage } from './use_series_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; import { SeriesUrl, UrlFilter } from '../types'; import { useAppIndexPatternContext } from './use_app_index_pattern'; import { ALL_VALUES_SELECTED } from '../../field_value_suggestions/field_value_combobox'; -import { useTheme } from '../../../../hooks/use_theme'; export const getFiltersFromDefs = (reportDefinitions: SeriesUrl['reportDefinitions']) => { return Object.entries(reportDefinitions ?? {}) @@ -34,56 +28,41 @@ export const getFiltersFromDefs = (reportDefinitions: SeriesUrl['reportDefinitio }; export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null => { - const { storage, autoApply, allSeries, lastRefresh, reportType } = useSeriesStorage(); + const { allSeriesIds, allSeries } = useSeriesStorage(); const { indexPatterns } = useAppIndexPatternContext(); - const theme = useTheme(); - return useMemo(() => { - if (isEmpty(indexPatterns) || isEmpty(allSeries) || !reportType) { + if (isEmpty(indexPatterns) || isEmpty(allSeriesIds)) { return null; } - const allSeriesT: AllSeries = autoApply - ? allSeries - : convertAllShortSeries(storage.get(allSeriesKey) ?? []); - const layerConfigs: LayerConfig[] = []; - allSeriesT.forEach((series, seriesIndex) => { - const indexPattern = indexPatterns?.[series?.dataType]; - - if ( - indexPattern && - !isEmpty(series.reportDefinitions) && - !series.hidden && - series.selectedMetricField - ) { + allSeriesIds.forEach((seriesIdT) => { + const seriesT = allSeries[seriesIdT]; + const indexPattern = indexPatterns?.[seriesT?.dataType]; + if (indexPattern && seriesT.reportType && !isEmpty(seriesT.reportDefinitions)) { const seriesConfig = getDefaultConfigs({ - reportType, + reportType: seriesT.reportType, + dataType: seriesT.dataType, indexPattern, - dataType: series.dataType, }); - const filters: UrlFilter[] = (series.filters ?? []).concat( - getFiltersFromDefs(series.reportDefinitions) + const filters: UrlFilter[] = (seriesT.filters ?? []).concat( + getFiltersFromDefs(seriesT.reportDefinitions) ); - const color = `euiColorVis${seriesIndex}`; - layerConfigs.push({ filters, indexPattern, seriesConfig, - time: series.time, - name: series.name, - breakdown: series.breakdown, - seriesType: series.seriesType, - operationType: series.operationType, - reportDefinitions: series.reportDefinitions ?? {}, - selectedMetricField: series.selectedMetricField, - color: series.color ?? ((theme.eui as unknown) as Record)[color], + time: seriesT.time, + breakdown: seriesT.breakdown, + seriesType: seriesT.seriesType, + operationType: seriesT.operationType, + reportDefinitions: seriesT.reportDefinitions ?? {}, + selectedMetricField: seriesT.selectedMetricField, }); } }); @@ -94,6 +73,6 @@ export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null const lensAttributes = new LensAttributes(layerConfigs); - return lensAttributes.getJSON(lastRefresh); - }, [indexPatterns, allSeries, reportType, autoApply, storage, theme, lastRefresh]); + return lensAttributes.getJSON(); + }, [indexPatterns, allSeriesIds, allSeries]); }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts index f2a6130cdc59d..2d2618bc46152 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts @@ -6,16 +6,18 @@ */ import { useSeriesStorage } from './use_series_storage'; -import { SeriesUrl, UrlFilter } from '../types'; +import { UrlFilter } from '../types'; export interface UpdateFilter { field: string; - value: string | string[]; + value: string; negate?: boolean; } -export const useSeriesFilters = ({ seriesId, series }: { seriesId: number; series: SeriesUrl }) => { - const { setSeries } = useSeriesStorage(); +export const useSeriesFilters = ({ seriesId }: { seriesId: string }) => { + const { getSeries, setSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); const filters = series.filters ?? []; @@ -24,14 +26,10 @@ export const useSeriesFilters = ({ seriesId, series }: { seriesId: number; serie .map((filter) => { if (filter.field === field) { if (negate) { - const notValuesN = filter.notValues?.filter((val) => - value instanceof Array ? !value.includes(val) : val !== value - ); + const notValuesN = filter.notValues?.filter((val) => val !== value); return { ...filter, notValues: notValuesN }; } else { - const valuesN = filter.values?.filter((val) => - value instanceof Array ? !value.includes(val) : val !== value - ); + const valuesN = filter.values?.filter((val) => val !== value); return { ...filter, values: valuesN }; } } @@ -45,9 +43,9 @@ export const useSeriesFilters = ({ seriesId, series }: { seriesId: number; serie const addFilter = ({ field, value, negate }: UpdateFilter) => { const currFilter: UrlFilter = { field }; if (negate) { - currFilter.notValues = value instanceof Array ? value : [value]; + currFilter.notValues = [value]; } else { - currFilter.values = value instanceof Array ? value : [value]; + currFilter.values = [value]; } if (filters.length === 0) { setSeries(seriesId, { ...series, filters: [currFilter] }); @@ -67,26 +65,13 @@ export const useSeriesFilters = ({ seriesId, series }: { seriesId: number; serie const currNotValues = currFilter.notValues ?? []; const currValues = currFilter.values ?? []; - const notValues = currNotValues.filter((val) => - value instanceof Array ? !value.includes(val) : val !== value - ); - - const values = currValues.filter((val) => - value instanceof Array ? !value.includes(val) : val !== value - ); + const notValues = currNotValues.filter((val) => val !== value); + const values = currValues.filter((val) => val !== value); if (negate) { - if (value instanceof Array) { - notValues.push(...value); - } else { - notValues.push(value); - } + notValues.push(value); } else { - if (value instanceof Array) { - values.push(...value); - } else { - values.push(value); - } + values.push(value); } currFilter.notValues = notValues.length > 0 ? notValues : undefined; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx index ce6d7bd94d8e4..c32acc47abd1b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx @@ -6,39 +6,37 @@ */ import React, { useEffect } from 'react'; -import { Route, Router } from 'react-router-dom'; -import { render } from '@testing-library/react'; + import { UrlStorageContextProvider, useSeriesStorage } from './use_series_storage'; -import { getHistoryFromUrl } from '../rtl_helpers'; +import { render } from '@testing-library/react'; -const mockSingleSeries = [ - { - name: 'performance-distribution', +const mockSingleSeries = { + 'performance-distribution': { + reportType: 'data-distribution', dataType: 'ux', breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, -]; +}; -const mockMultipleSeries = [ - { - name: 'performance-distribution', +const mockMultipleSeries = { + 'performance-distribution': { + reportType: 'data-distribution', dataType: 'ux', breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, - { - name: 'kpi-over-time', + 'kpi-over-time': { + reportType: 'kpi-over-time', dataType: 'synthetics', breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, -]; +}; -describe('userSeriesStorage', function () { +describe('userSeries', function () { function setupTestComponent(seriesData: any) { const setData = jest.fn(); - function TestComponent() { const data = useSeriesStorage(); @@ -50,20 +48,11 @@ describe('userSeriesStorage', function () { } render( - - - (key === 'sr' ? seriesData : null)), - set: jest.fn(), - }} - > - - - - + + + ); return setData; @@ -74,20 +63,22 @@ describe('userSeriesStorage', function () { expect(setData).toHaveBeenCalledTimes(2); expect(setData).toHaveBeenLastCalledWith( expect.objectContaining({ - allSeries: [ - { - name: 'performance-distribution', - dataType: 'ux', + allSeries: { + 'performance-distribution': { breakdown: 'user_agent.name', + dataType: 'ux', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }, - ], + }, + allSeriesIds: ['performance-distribution'], firstSeries: { - name: 'performance-distribution', - dataType: 'ux', breakdown: 'user_agent.name', + dataType: 'ux', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }, + firstSeriesId: 'performance-distribution', }) ); }); @@ -98,38 +89,42 @@ describe('userSeriesStorage', function () { expect(setData).toHaveBeenCalledTimes(2); expect(setData).toHaveBeenLastCalledWith( expect.objectContaining({ - allSeries: [ - { - name: 'performance-distribution', - dataType: 'ux', + allSeries: { + 'performance-distribution': { breakdown: 'user_agent.name', + dataType: 'ux', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }, - { - name: 'kpi-over-time', + 'kpi-over-time': { + reportType: 'kpi-over-time', dataType: 'synthetics', breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, - ], + }, + allSeriesIds: ['performance-distribution', 'kpi-over-time'], firstSeries: { - name: 'performance-distribution', - dataType: 'ux', breakdown: 'user_agent.name', + dataType: 'ux', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }, + firstSeriesId: 'performance-distribution', }) ); }); it('should return expected result when there are no series', function () { - const setData = setupTestComponent([]); + const setData = setupTestComponent({}); - expect(setData).toHaveBeenCalledTimes(1); + expect(setData).toHaveBeenCalledTimes(2); expect(setData).toHaveBeenLastCalledWith( expect.objectContaining({ - allSeries: [], + allSeries: {}, + allSeriesIds: [], firstSeries: undefined, + firstSeriesId: undefined, }) ); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx index 04f8751e2a0b6..a47a124d14b4d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx @@ -6,7 +6,6 @@ */ import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; -import { useRouteMatch } from 'react-router-dom'; import { IKbnUrlStateStorage, ISessionStorageStateStorage, @@ -23,19 +22,13 @@ import { OperationType, SeriesType } from '../../../../../../lens/public'; import { URL_KEYS } from '../configurations/constants/url_constants'; export interface SeriesContextValue { - firstSeries?: SeriesUrl; - autoApply: boolean; - lastRefresh: number; - setLastRefresh: (val: number) => void; - setAutoApply: (val: boolean) => void; - applyChanges: () => void; + firstSeries: SeriesUrl; + firstSeriesId: string; + allSeriesIds: string[]; allSeries: AllSeries; - setSeries: (seriesIndex: number, newValue: SeriesUrl) => void; - getSeries: (seriesIndex: number) => SeriesUrl | undefined; - removeSeries: (seriesIndex: number) => void; - setReportType: (reportType: string) => void; - storage: IKbnUrlStateStorage | ISessionStorageStateStorage; - reportType: ReportViewType; + setSeries: (seriesIdN: string, newValue: SeriesUrl) => void; + getSeries: (seriesId: string) => SeriesUrl; + removeSeries: (seriesId: string) => void; } export const UrlStorageContext = createContext({} as SeriesContextValue); @@ -43,112 +36,72 @@ interface ProviderProps { storage: IKbnUrlStateStorage | ISessionStorageStateStorage; } -export function convertAllShortSeries(allShortSeries: AllShortSeries) { - return (allShortSeries ?? []).map((shortSeries) => convertFromShortUrl(shortSeries)); -} +function convertAllShortSeries(allShortSeries: AllShortSeries) { + const allSeriesIds = Object.keys(allShortSeries); + const allSeriesN: AllSeries = {}; + allSeriesIds.forEach((seriesKey) => { + allSeriesN[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]); + }); -export const allSeriesKey = 'sr'; -const autoApplyKey = 'autoApply'; -const reportTypeKey = 'reportType'; + return allSeriesN; +} export function UrlStorageContextProvider({ children, storage, }: ProviderProps & { children: JSX.Element }) { - const [allSeries, setAllSeries] = useState(() => - convertAllShortSeries(storage.get(allSeriesKey) ?? []) - ); - - const [autoApply, setAutoApply] = useState(() => storage.get(autoApplyKey) ?? true); - const [lastRefresh, setLastRefresh] = useState(() => Date.now()); + const allSeriesKey = 'sr'; - const [reportType, setReportType] = useState( - () => (storage as IKbnUrlStateStorage).get(reportTypeKey) ?? '' + const [allShortSeries, setAllShortSeries] = useState( + () => storage.get(allSeriesKey) ?? {} ); - + const [allSeries, setAllSeries] = useState(() => + convertAllShortSeries(storage.get(allSeriesKey) ?? {}) + ); + const [firstSeriesId, setFirstSeriesId] = useState(''); const [firstSeries, setFirstSeries] = useState(); - const isPreview = !!useRouteMatch('/exploratory-view/preview'); - - useEffect(() => { - const allShortSeries = allSeries.map((series) => convertToShortUrl(series)); - - const firstSeriesT = allSeries?.[0]; - - setFirstSeries(firstSeriesT); - - if (autoApply) { - (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries); - } - }, [allSeries, autoApply, storage]); useEffect(() => { - // needed for tab change - const allShortSeries = allSeries.map((series) => convertToShortUrl(series)); + const allSeriesIds = Object.keys(allShortSeries); + const allSeriesN: AllSeries = convertAllShortSeries(allShortSeries ?? {}); + setAllSeries(allSeriesN); + setFirstSeriesId(allSeriesIds?.[0]); + setFirstSeries(allSeriesN?.[allSeriesIds?.[0]]); (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries); - (storage as IKbnUrlStateStorage).set(reportTypeKey, reportType); - // this is only needed for tab change, so we will not add allSeries into dependencies - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isPreview, storage]); - - const setSeries = useCallback((seriesIndex: number, newValue: SeriesUrl) => { - setAllSeries((prevAllSeries) => { - const newStateRest = prevAllSeries.map((series, index) => { - if (index === seriesIndex) { - return newValue; - } - return series; - }); - - if (prevAllSeries.length === seriesIndex) { - return [...newStateRest, newValue]; - } - - return [...newStateRest]; + }, [allShortSeries, storage]); + + const setSeries = (seriesIdN: string, newValue: SeriesUrl) => { + setAllShortSeries((prevState) => { + prevState[seriesIdN] = convertToShortUrl(newValue); + return { ...prevState }; }); - }, []); + }; - useEffect(() => { - (storage as IKbnUrlStateStorage).set(reportTypeKey, reportType); - }, [reportType, storage]); + const removeSeries = (seriesIdN: string) => { + setAllShortSeries((prevState) => { + delete prevState[seriesIdN]; + return { ...prevState }; + }); + }; - const removeSeries = useCallback((seriesIndex: number) => { - setAllSeries((prevAllSeries) => - prevAllSeries.filter((seriesT, index) => index !== seriesIndex) - ); - }, []); + const allSeriesIds = Object.keys(allShortSeries); const getSeries = useCallback( - (seriesIndex: number) => { - return allSeries[seriesIndex]; + (seriesId?: string) => { + return seriesId ? allSeries?.[seriesId] ?? {} : ({} as SeriesUrl); }, [allSeries] ); - const applyChanges = useCallback(() => { - const allShortSeries = allSeries.map((series) => convertToShortUrl(series)); - - (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries); - setLastRefresh(Date.now()); - }, [allSeries, storage]); - - useEffect(() => { - (storage as IKbnUrlStateStorage).set(autoApplyKey, autoApply); - }, [autoApply, storage]); - const value = { - autoApply, - setAutoApply, - applyChanges, storage, getSeries, setSeries, removeSeries, + firstSeriesId, allSeries, - lastRefresh, - setLastRefresh, - setReportType, - reportType: storage.get(reportTypeKey) as ReportViewType, + allSeriesIds, firstSeries: firstSeries!, }; return {children}; @@ -159,9 +112,10 @@ export function useSeriesStorage() { } function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl { - const { dt, op, st, bd, ft, time, rdf, mt, h, n, c, ...restSeries } = newValue; + const { dt, op, st, rt, bd, ft, time, rdf, mt, ...restSeries } = newValue; return { operationType: op, + reportType: rt!, seriesType: st, breakdown: bd, filters: ft!, @@ -169,31 +123,26 @@ function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl { reportDefinitions: rdf, dataType: dt!, selectedMetricField: mt, - hidden: h, - name: n, - color: c, ...restSeries, }; } interface ShortUrlSeries { [URL_KEYS.OPERATION_TYPE]?: OperationType; + [URL_KEYS.REPORT_TYPE]?: ReportViewType; [URL_KEYS.DATA_TYPE]?: AppDataType; [URL_KEYS.SERIES_TYPE]?: SeriesType; [URL_KEYS.BREAK_DOWN]?: string; [URL_KEYS.FILTERS]?: UrlFilter[]; [URL_KEYS.REPORT_DEFINITIONS]?: URLReportDefinition; [URL_KEYS.SELECTED_METRIC]?: string; - [URL_KEYS.HIDDEN]?: boolean; - [URL_KEYS.NAME]: string; - [URL_KEYS.COLOR]?: string; time?: { to: string; from: string; }; } -export type AllShortSeries = ShortUrlSeries[]; -export type AllSeries = SeriesUrl[]; +export type AllShortSeries = Record; +export type AllSeries = Record; -export const NEW_SERIES_KEY = 'new-series'; +export const NEW_SERIES_KEY = 'new-series-key'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx index 3de29b02853e8..e55752ceb62ba 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx @@ -25,9 +25,11 @@ import { TypedLensByValueInput } from '../../../../../lens/public'; export function ExploratoryViewPage({ saveAttributes, + multiSeries = false, useSessionStorage = false, }: { useSessionStorage?: boolean; + multiSeries?: boolean; saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void; }) { useTrackPageview({ app: 'observability-overview', path: 'exploratory-view' }); @@ -59,7 +61,7 @@ export function ExploratoryViewPage({ - + diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx index 9e4d9486dc155..4cb586fe94ceb 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx @@ -7,51 +7,16 @@ import { i18n } from '@kbn/i18n'; import React, { Dispatch, SetStateAction, useCallback } from 'react'; -import styled from 'styled-components'; -import { isEmpty } from 'lodash'; +import { combineTimeRanges } from './exploratory_view'; import { TypedLensByValueInput } from '../../../../../lens/public'; import { useSeriesStorage } from './hooks/use_series_storage'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { ReportViewType, SeriesUrl } from './types'; -import { ReportTypes } from './configurations/constants'; interface Props { lensAttributes: TypedLensByValueInput['attributes']; setLastUpdated: Dispatch>; } -export const combineTimeRanges = ( - reportType: ReportViewType, - allSeries: SeriesUrl[], - firstSeries?: SeriesUrl -) => { - let to: string = ''; - let from: string = ''; - - if (reportType === ReportTypes.KPI) { - return firstSeries?.time; - } - - allSeries.forEach((series) => { - if ( - series.dataType && - series.selectedMetricField && - !isEmpty(series.reportDefinitions) && - series.time - ) { - const seriesTo = new Date(series.time.to); - const seriesFrom = new Date(series.time.from); - if (!to || seriesTo > new Date(to)) { - to = series.time.to; - } - if (!from || seriesFrom < new Date(from)) { - from = series.time.from; - } - } - }); - - return { to, from }; -}; export function LensEmbeddable(props: Props) { const { lensAttributes, setLastUpdated } = props; @@ -62,11 +27,9 @@ export function LensEmbeddable(props: Props) { const LensComponent = lens?.EmbeddableComponent; - const { firstSeries, setSeries, allSeries, reportType } = useSeriesStorage(); + const { firstSeriesId, firstSeries: series, setSeries, allSeries } = useSeriesStorage(); - const firstSeriesId = 0; - - const timeRange = firstSeries ? combineTimeRanges(reportType, allSeries, firstSeries) : null; + const timeRange = combineTimeRanges(allSeries, series); const onLensLoad = useCallback(() => { setLastUpdated(Date.now()); @@ -74,9 +37,9 @@ export function LensEmbeddable(props: Props) { const onBrushEnd = useCallback( ({ range }: { range: number[] }) => { - if (reportType !== 'data-distribution' && firstSeries) { + if (series?.reportType !== 'data-distribution') { setSeries(firstSeriesId, { - ...firstSeries, + ...series, time: { from: new Date(range[0]).toISOString(), to: new Date(range[1]).toISOString(), @@ -90,30 +53,16 @@ export function LensEmbeddable(props: Props) { ); } }, - [reportType, setSeries, firstSeries, notifications?.toasts] + [notifications?.toasts, series, firstSeriesId, setSeries] ); - if (timeRange === null || !firstSeries) { - return null; - } - return ( - - - + ); } - -const LensWrapper = styled.div` - height: 100%; - - &&& > div { - height: 100%; - } -`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx index 0e609cbe6c9e5..972e3beb4b722 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx @@ -10,7 +10,7 @@ import React, { ReactElement } from 'react'; import { stringify } from 'query-string'; // eslint-disable-next-line import/no-extraneous-dependencies import { render as reactTestLibRender, RenderOptions } from '@testing-library/react'; -import { Route, Router } from 'react-router-dom'; +import { Router } from 'react-router-dom'; import { createMemoryHistory, History } from 'history'; import { CoreStart } from 'kibana/public'; import { I18nProvider } from '@kbn/i18n/react'; @@ -24,7 +24,7 @@ import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/ import { lensPluginMock } from '../../../../../lens/public/mocks'; import * as useAppIndexPatternHook from './hooks/use_app_index_pattern'; import { IndexPatternContextProvider } from './hooks/use_app_index_pattern'; -import { AllSeries, SeriesContextValue, UrlStorageContext } from './hooks/use_series_storage'; +import { AllSeries, UrlStorageContext } from './hooks/use_series_storage'; import * as fetcherHook from '../../../hooks/use_fetcher'; import * as useSeriesFilterHook from './hooks/use_series_filters'; @@ -39,10 +39,9 @@ import { IndexPattern, IndexPatternsContract, } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; -import { AppDataType, SeriesUrl, UrlFilter } from './types'; +import { AppDataType, UrlFilter } from './types'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { ListItem } from '../../../hooks/use_values_list'; -import { TRANSACTION_DURATION } from './configurations/constants/elasticsearch_fieldnames'; interface KibanaProps { services?: KibanaServices; @@ -159,11 +158,9 @@ export function MockRouter({ }: MockRouterProps) { return ( - - - {children} - - + + {children} + ); } @@ -176,7 +173,7 @@ export function render( core: customCore, kibanaProps, renderOptions, - url = '/app/observability/exploratory-view/configure#?autoApply=!t', + url, initSeries = {}, }: RenderRouterOptions = {} ) { @@ -206,7 +203,7 @@ export function render( }; } -export const getHistoryFromUrl = (url: Url) => { +const getHistoryFromUrl = (url: Url) => { if (typeof url === 'string') { return createMemoryHistory({ initialEntries: [url], @@ -255,15 +252,6 @@ export const mockUseValuesList = (values?: ListItem[]) => { return { spy, onRefreshTimeRange }; }; -export const mockUxSeries = { - name: 'performance-distribution', - dataType: 'ux', - breakdown: 'user_agent.name', - time: { from: 'now-15m', to: 'now' }, - reportDefinitions: { 'service.name': ['elastic-co'] }, - selectedMetricField: TRANSACTION_DURATION, -} as SeriesUrl; - function mockSeriesStorageContext({ data, filters, @@ -273,34 +261,34 @@ function mockSeriesStorageContext({ filters?: UrlFilter[]; breakdown?: string; }) { - const testSeries = { - ...mockUxSeries, - breakdown: breakdown || 'user_agent.name', - ...(filters ? { filters } : {}), + const mockDataSeries = data || { + 'performance-distribution': { + reportType: 'data-distribution', + dataType: 'ux', + breakdown: breakdown || 'user_agent.name', + time: { from: 'now-15m', to: 'now' }, + ...(filters ? { filters } : {}), + }, }; + const allSeriesIds = Object.keys(mockDataSeries); + const firstSeriesId = allSeriesIds?.[0]; - const mockDataSeries = data || [testSeries]; + const series = mockDataSeries[firstSeriesId]; const removeSeries = jest.fn(); const setSeries = jest.fn(); - const getSeries = jest.fn().mockReturnValue(testSeries); + const getSeries = jest.fn().mockReturnValue(series); return { + firstSeriesId, + allSeriesIds, removeSeries, setSeries, getSeries, - autoApply: true, - reportType: 'data-distribution', - lastRefresh: Date.now(), - setLastRefresh: jest.fn(), - setAutoApply: jest.fn(), - applyChanges: jest.fn(), - firstSeries: mockDataSeries[0], + firstSeries: mockDataSeries[firstSeriesId], allSeries: mockDataSeries, - setReportType: jest.fn(), - storage: { get: jest.fn() } as any, - } as SeriesContextValue; + }; } export function mockUseSeriesFilter() { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx similarity index 85% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx index 8f196b8a05dda..c054853d9c877 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx @@ -7,12 +7,12 @@ import React from 'react'; import { fireEvent, screen, waitFor } from '@testing-library/react'; -import { mockUxSeries, render } from '../../rtl_helpers'; +import { render } from '../../rtl_helpers'; import { SeriesChartTypesSelect, XYChartTypesSelect } from './chart_types'; describe.skip('SeriesChartTypesSelect', function () { it('should render properly', async function () { - render(); + render(); await waitFor(() => { screen.getByText(/chart type/i); @@ -21,7 +21,7 @@ describe.skip('SeriesChartTypesSelect', function () { it('should call set series on change', async function () { const { setSeries } = render( - + ); await waitFor(() => { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx similarity index 77% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx index 27d846502dbe6..50c2f91e6067d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx @@ -6,11 +6,11 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIcon, EuiSuperSelect } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSuperSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../../../plugin'; -import { SeriesUrl, useFetcher } from '../../../../..'; +import { useFetcher } from '../../../../..'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { SeriesType } from '../../../../../../../lens/public'; @@ -20,14 +20,16 @@ const CHART_TYPE_LABEL = i18n.translate('xpack.observability.expView.chartTypes. export function SeriesChartTypesSelect({ seriesId, - series, + seriesTypes, defaultChartType, }: { - seriesId: number; - series: SeriesUrl; + seriesId: string; + seriesTypes?: SeriesType[]; defaultChartType: SeriesType; }) { - const { setSeries } = useSeriesStorage(); + const { getSeries, setSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); const seriesType = series?.seriesType ?? defaultChartType; @@ -40,15 +42,17 @@ export function SeriesChartTypesSelect({ onChange={onChange} value={seriesType} excludeChartTypes={['bar_percentage_stacked']} - includeChartTypes={[ - 'bar', - 'bar_horizontal', - 'line', - 'area', - 'bar_stacked', - 'area_stacked', - 'bar_horizontal_percentage_stacked', - ]} + includeChartTypes={ + seriesTypes || [ + 'bar', + 'bar_horizontal', + 'line', + 'area', + 'bar_stacked', + 'area_stacked', + 'bar_horizontal_percentage_stacked', + ] + } label={CHART_TYPE_LABEL} /> ); @@ -101,14 +105,14 @@ export function XYChartTypesSelect({ }); return ( - - - + ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx new file mode 100644 index 0000000000000..b10702ebded57 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent, screen } from '@testing-library/react'; +import { mockAppIndexPattern, render } from '../../rtl_helpers'; +import { dataTypes, DataTypesCol } from './data_types_col'; + +describe('DataTypesCol', function () { + const seriesId = 'test-series-id'; + + mockAppIndexPattern(); + + it('should render properly', function () { + const { getByText } = render(); + + dataTypes.forEach(({ label }) => { + getByText(label); + }); + }); + + it('should set series on change', function () { + const { setSeries } = render(); + + fireEvent.click(screen.getByText(/user experience \(rum\)/i)); + + expect(setSeries).toHaveBeenCalledTimes(1); + expect(setSeries).toHaveBeenCalledWith(seriesId, { + dataType: 'ux', + isNew: true, + time: { + from: 'now-15m', + to: 'now', + }, + }); + }); + + it('should set series on change on already selected', function () { + const initSeries = { + data: { + [seriesId]: { + dataType: 'synthetics' as const, + reportType: 'kpi-over-time' as const, + breakdown: 'monitor.status', + time: { from: 'now-15m', to: 'now' }, + }, + }, + }; + + render(, { initSeries }); + + const button = screen.getByRole('button', { + name: /Synthetic Monitoring/i, + }); + + expect(button.classList).toContain('euiButton--fill'); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx new file mode 100644 index 0000000000000..f386f62d9ed73 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; +import { AppDataType } from '../../types'; +import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; + +export const dataTypes: Array<{ id: AppDataType; label: string }> = [ + { id: 'synthetics', label: 'Synthetic Monitoring' }, + { id: 'ux', label: 'User Experience (RUM)' }, + { id: 'mobile', label: 'Mobile Experience' }, + // { id: 'infra_logs', label: 'Logs' }, + // { id: 'infra_metrics', label: 'Metrics' }, + // { id: 'apm', label: 'APM' }, +]; + +export function DataTypesCol({ seriesId }: { seriesId: string }) { + const { getSeries, setSeries, removeSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + const { loading } = useAppIndexPatternContext(); + + const onDataTypeChange = (dataType?: AppDataType) => { + if (!dataType) { + removeSeries(seriesId); + } else { + setSeries(seriesId || `${dataType}-series`, { + dataType, + isNew: true, + time: series.time, + } as any); + } + }; + + const selectedDataType = series.dataType; + + return ( + + {dataTypes.map(({ id: dataTypeId, label }) => ( + + + + ))} + + ); +} + +const FlexGroup = styled(EuiFlexGroup)` + width: 100%; +`; + +const Button = styled(EuiButton)` + will-change: transform; +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx new file mode 100644 index 0000000000000..6be78084ae195 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import styled from 'styled-components'; +import { SeriesDatePicker } from '../../series_date_picker'; +import { DateRangePicker } from '../../series_date_picker/date_range_picker'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; + +interface Props { + seriesId: string; +} +export function DatePickerCol({ seriesId }: Props) { + const { firstSeriesId, getSeries } = useSeriesStorage(); + const { reportType } = getSeries(firstSeriesId); + + return ( + + {firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? ( + + ) : ( + + )} + + ); +} + +const Wrapper = styled.div` + .euiSuperDatePicker__flexWrapper { + width: 100%; + > .euiFlexItem { + margin-right: 0px; + } + } +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx similarity index 69% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx index ced4d3af057ff..516f04e3812ba 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx @@ -7,66 +7,62 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; -import { mockUxSeries, render } from '../../rtl_helpers'; +import { render } from '../../rtl_helpers'; import { OperationTypeSelect } from './operation_type_select'; describe('OperationTypeSelect', function () { it('should render properly', function () { - render(); + render(); screen.getByText('Select an option: , is selected'); }); it('should display selected value', function () { const initSeries = { - data: [ - { - name: 'performance-distribution', + data: { + 'performance-distribution': { dataType: 'ux' as const, + reportType: 'kpi-over-time' as const, operationType: 'median' as const, time: { from: 'now-15m', to: 'now' }, }, - ], + }, }; - render(, { - initSeries, - }); + render(, { initSeries }); screen.getByText('Median'); }); it('should call set series on change', function () { const initSeries = { - data: [ - { - name: 'performance-distribution', + data: { + 'series-id': { dataType: 'ux' as const, + reportType: 'kpi-over-time' as const, operationType: 'median' as const, time: { from: 'now-15m', to: 'now' }, }, - ], + }, }; - const { setSeries } = render(, { - initSeries, - }); + const { setSeries } = render(, { initSeries }); fireEvent.click(screen.getByTestId('operationTypeSelect')); - expect(setSeries).toHaveBeenCalledWith(0, { + expect(setSeries).toHaveBeenCalledWith('series-id', { operationType: 'median', dataType: 'ux', + reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, - name: 'performance-distribution', }); fireEvent.click(screen.getByText('95th Percentile')); - expect(setSeries).toHaveBeenCalledWith(0, { + expect(setSeries).toHaveBeenCalledWith('series-id', { operationType: '95th', dataType: 'ux', + reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, - name: 'performance-distribution', }); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx similarity index 91% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx index 4c10c9311704d..fce1383f30f34 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx @@ -11,18 +11,17 @@ import { EuiSuperSelect } from '@elastic/eui'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { OperationType } from '../../../../../../../lens/public'; -import { SeriesUrl } from '../../types'; export function OperationTypeSelect({ seriesId, - series, defaultOperationType, }: { - seriesId: number; - series: SeriesUrl; + seriesId: string; defaultOperationType?: OperationType; }) { - const { setSeries } = useSeriesStorage(); + const { getSeries, setSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); const operationType = series?.operationType; @@ -84,7 +83,11 @@ export function OperationTypeSelect({ return ( ); + + screen.getByText('Select an option: , is selected'); + screen.getAllByText('Browser family'); + }); + + it('should set new series breakdown on change', function () { + const { setSeries } = render( + + ); + + const btn = screen.getByRole('button', { + name: /select an option: Browser family , is selected/i, + hidden: true, + }); + + fireEvent.click(btn); + + fireEvent.click(screen.getByText(/operating system/i)); + + expect(setSeries).toHaveBeenCalledTimes(1); + expect(setSeries).toHaveBeenCalledWith(seriesId, { + breakdown: USER_AGENT_OS, + dataType: 'ux', + reportType: 'data-distribution', + time: { from: 'now-15m', to: 'now' }, + }); + }); + it('should set undefined on new series on no select breakdown', function () { + const { setSeries } = render( + + ); + + const btn = screen.getByRole('button', { + name: /select an option: Browser family , is selected/i, + hidden: true, + }); + + fireEvent.click(btn); + + fireEvent.click(screen.getByText(/no breakdown/i)); + + expect(setSeries).toHaveBeenCalledTimes(1); + expect(setSeries).toHaveBeenCalledWith(seriesId, { + breakdown: undefined, + dataType: 'ux', + reportType: 'data-distribution', + time: { from: 'now-15m', to: 'now' }, + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx new file mode 100644 index 0000000000000..fa2d01691ce1d --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { Breakdowns } from '../../series_editor/columns/breakdowns'; +import { SeriesConfig } from '../../types'; + +export function ReportBreakdowns({ + seriesId, + seriesConfig, +}: { + seriesConfig: SeriesConfig; + seriesId: string; +}) { + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx similarity index 65% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx index 544a294e021e2..3d156e0ee9c2b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx @@ -12,14 +12,14 @@ import { mockAppIndexPattern, mockIndexPattern, mockUseValuesList, - mockUxSeries, render, } from '../../rtl_helpers'; import { ReportDefinitionCol } from './report_definition_col'; +import { SERVICE_NAME } from '../../configurations/constants/elasticsearch_fieldnames'; describe('Series Builder ReportDefinitionCol', function () { mockAppIndexPattern(); - const seriesId = 0; + const seriesId = 'test-series-id'; const seriesConfig = getDefaultConfigs({ reportType: 'data-distribution', @@ -27,24 +27,36 @@ describe('Series Builder ReportDefinitionCol', function () { dataType: 'ux', }); + const initSeries = { + data: { + [seriesId]: { + dataType: 'ux' as const, + reportType: 'data-distribution' as const, + time: { from: 'now-30d', to: 'now' }, + reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] }, + }, + }, + }; + mockUseValuesList([{ label: 'elastic-co', count: 10 }]); - it('renders', async () => { - render( - - ); + it('should render properly', async function () { + render(, { + initSeries, + }); await waitFor(() => { - expect(screen.getByText('Web Application')).toBeInTheDocument(); - expect(screen.getByText('Environment')).toBeInTheDocument(); - expect(screen.getByText('Search Environment')).toBeInTheDocument(); + screen.getByText('Web Application'); + screen.getByText('Environment'); + screen.getByText('Select an option: Page load time, is selected'); + screen.getByText('Page load time'); }); }); it('should render selected report definitions', async function () { - render( - - ); + render(, { + initSeries, + }); expect(await screen.findByText('elastic-co')).toBeInTheDocument(); @@ -53,7 +65,8 @@ describe('Series Builder ReportDefinitionCol', function () { it('should be able to remove selected definition', async function () { const { setSeries } = render( - + , + { initSeries } ); expect( @@ -67,14 +80,11 @@ describe('Series Builder ReportDefinitionCol', function () { fireEvent.click(removeBtn); expect(setSeries).toHaveBeenCalledTimes(1); - expect(setSeries).toHaveBeenCalledWith(seriesId, { dataType: 'ux', - name: 'performance-distribution', - breakdown: 'user_agent.name', reportDefinitions: {}, - selectedMetricField: 'transaction.duration.us', - time: { from: 'now-15m', to: 'now' }, + reportType: 'data-distribution', + time: { from: 'now-30d', to: 'now' }, }); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx new file mode 100644 index 0000000000000..0c620abf56e8a --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; +import styled from 'styled-components'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { ReportMetricOptions } from '../report_metric_options'; +import { SeriesConfig } from '../../types'; +import { SeriesChartTypesSelect } from './chart_types'; +import { OperationTypeSelect } from './operation_type_select'; +import { DatePickerCol } from './date_picker_col'; +import { parseCustomFieldName } from '../../configurations/lens_attributes'; +import { ReportDefinitionField } from './report_definition_field'; + +function getColumnType(seriesConfig: SeriesConfig, selectedMetricField?: string) { + const { columnType } = parseCustomFieldName(seriesConfig, selectedMetricField); + + return columnType; +} + +export function ReportDefinitionCol({ + seriesConfig, + seriesId, +}: { + seriesConfig: SeriesConfig; + seriesId: string; +}) { + const { getSeries, setSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + + const { reportDefinitions: selectedReportDefinitions = {}, selectedMetricField } = series ?? {}; + + const { + definitionFields, + defaultSeriesType, + hasOperationType, + yAxisColumns, + metricOptions, + } = seriesConfig; + + const onChange = (field: string, value?: string[]) => { + if (!value?.[0]) { + delete selectedReportDefinitions[field]; + setSeries(seriesId, { + ...series, + reportDefinitions: { ...selectedReportDefinitions }, + }); + } else { + setSeries(seriesId, { + ...series, + reportDefinitions: { ...selectedReportDefinitions, [field]: value }, + }); + } + }; + + const columnType = getColumnType(seriesConfig, selectedMetricField); + + return ( + + + + + + {definitionFields.map((field) => ( + + + + ))} + {metricOptions && ( + + + + )} + {(hasOperationType || columnType === 'operation') && ( + + + + )} + + + + + ); +} + +const FlexGroup = styled(EuiFlexGroup)` + width: 100%; +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx similarity index 69% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx index 3651b4b7f075b..8a83b5c2a8cb0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx @@ -6,25 +6,30 @@ */ import React, { useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { isEmpty } from 'lodash'; import { ExistsFilter } from '@kbn/es-query'; import FieldValueSuggestions from '../../../field_value_suggestions'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearch'; import { PersistableFilter } from '../../../../../../../lens/common'; import { buildPhrasesFilter } from '../../configurations/utils'; -import { SeriesConfig, SeriesUrl } from '../../types'; +import { SeriesConfig } from '../../types'; import { ALL_VALUES_SELECTED } from '../../../field_value_suggestions/field_value_combobox'; interface Props { - seriesId: number; - series: SeriesUrl; + seriesId: string; field: string; seriesConfig: SeriesConfig; onChange: (field: string, value?: string[]) => void; } -export function ReportDefinitionField({ series, field, seriesConfig, onChange }: Props) { +export function ReportDefinitionField({ seriesId, field, seriesConfig, onChange }: Props) { + const { getSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + const { indexPattern } = useAppIndexPatternContext(series.dataType); const { reportDefinitions: selectedReportDefinitions = {} } = series; @@ -59,26 +64,23 @@ export function ReportDefinitionField({ series, field, seriesConfig, onChange }: // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(selectedReportDefinitions), JSON.stringify(baseFilters)]); - if (!indexPattern) { - return null; - } - return ( - onChange(field, val)} - filters={queryFilters} - time={series.time} - fullWidth={true} - asCombobox={true} - allowExclusions={false} - allowAllValuesSelection={true} - usePrependLabel={false} - compressed={false} - required={isEmpty(selectedReportDefinitions)} - /> + + + {indexPattern && ( + onChange(field, val)} + filters={queryFilters} + time={series.time} + fullWidth={true} + allowAllValuesSelection={true} + /> + )} + + ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx new file mode 100644 index 0000000000000..0b183b5f20c03 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen } from '@testing-library/react'; +import { ReportFilters } from './report_filters'; +import { getDefaultConfigs } from '../../configurations/default_configs'; +import { mockIndexPattern, render } from '../../rtl_helpers'; + +describe('Series Builder ReportFilters', function () { + const seriesId = 'test-series-id'; + + const dataViewSeries = getDefaultConfigs({ + reportType: 'data-distribution', + indexPattern: mockIndexPattern, + dataType: 'ux', + }); + + it('should render properly', function () { + render(); + + screen.getByText('Add filter'); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx new file mode 100644 index 0000000000000..d5938c5387e8f --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { SeriesFilter } from '../../series_editor/columns/series_filter'; +import { SeriesConfig } from '../../types'; + +export function ReportFilters({ + seriesConfig, + seriesId, +}: { + seriesConfig: SeriesConfig; + seriesId: string; +}) { + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx new file mode 100644 index 0000000000000..12ae8560453c9 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent, screen } from '@testing-library/react'; +import { mockAppIndexPattern, render } from '../../rtl_helpers'; +import { ReportTypesCol, SELECTED_DATA_TYPE_FOR_REPORT } from './report_types_col'; +import { ReportTypes } from '../series_builder'; +import { DEFAULT_TIME } from '../../configurations/constants'; + +describe('ReportTypesCol', function () { + const seriesId = 'performance-distribution'; + + mockAppIndexPattern(); + + it('should render properly', function () { + render(); + screen.getByText('Performance distribution'); + screen.getByText('KPI over time'); + }); + + it('should display empty message', function () { + render(); + screen.getByText(SELECTED_DATA_TYPE_FOR_REPORT); + }); + + it('should set series on change', function () { + const { setSeries } = render( + + ); + + fireEvent.click(screen.getByText(/KPI over time/i)); + + expect(setSeries).toHaveBeenCalledWith(seriesId, { + dataType: 'ux', + selectedMetricField: undefined, + reportType: 'kpi-over-time', + time: { from: 'now-15m', to: 'now' }, + }); + expect(setSeries).toHaveBeenCalledTimes(1); + }); + + it('should set selected as filled', function () { + const initSeries = { + data: { + [seriesId]: { + dataType: 'synthetics' as const, + reportType: 'kpi-over-time' as const, + breakdown: 'monitor.status', + time: { from: 'now-15m', to: 'now' }, + isNew: true, + }, + }, + }; + + const { setSeries } = render( + , + { initSeries } + ); + + const button = screen.getByRole('button', { + name: /KPI over time/i, + }); + + expect(button.classList).toContain('euiButton--fill'); + fireEvent.click(button); + + // undefined on click selected + expect(setSeries).toHaveBeenCalledWith(seriesId, { + dataType: 'synthetics', + time: DEFAULT_TIME, + isNew: true, + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx new file mode 100644 index 0000000000000..c4eebbfaca3eb --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { map } from 'lodash'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import styled from 'styled-components'; +import { ReportViewType, SeriesUrl } from '../../types'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { DEFAULT_TIME } from '../../configurations/constants'; +import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; +import { ReportTypeItem } from '../series_builder'; + +interface Props { + seriesId: string; + reportTypes: ReportTypeItem[]; +} + +export function ReportTypesCol({ seriesId, reportTypes }: Props) { + const { setSeries, getSeries, firstSeries, firstSeriesId } = useSeriesStorage(); + + const { reportType: selectedReportType, ...restSeries } = getSeries(seriesId); + + const { loading, hasData } = useAppIndexPatternContext(restSeries.dataType); + + if (!restSeries.dataType) { + return ( + + ); + } + + if (!loading && !hasData) { + return ( + + ); + } + + const disabledReportTypes: ReportViewType[] = map( + reportTypes.filter( + ({ reportType }) => firstSeriesId !== seriesId && reportType !== firstSeries.reportType + ), + 'reportType' + ); + + return reportTypes?.length > 0 ? ( + + {reportTypes.map(({ reportType, label }) => ( + + + + ))} + + ) : ( + {SELECTED_DATA_TYPE_FOR_REPORT} + ); +} + +export const SELECTED_DATA_TYPE_FOR_REPORT = i18n.translate( + 'xpack.observability.expView.reportType.noDataType', + { defaultMessage: 'No data type selected.' } +); + +const FlexGroup = styled(EuiFlexGroup)` + width: 100%; +`; + +const Button = styled(EuiButton)` + will-change: transform; +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx similarity index 55% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx index c352ec0423dd8..874171de123d2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx @@ -8,7 +8,6 @@ import React, { useEffect, useState } from 'react'; import { EuiIcon, EuiText } from '@elastic/eui'; import moment from 'moment'; -import { FormattedMessage } from '@kbn/i18n/react'; interface Props { lastUpdated?: number; @@ -19,34 +18,20 @@ export function LastUpdated({ lastUpdated }: Props) { useEffect(() => { const interVal = setInterval(() => { setRefresh(Date.now()); - }, 5000); + }, 1000); return () => { clearInterval(interVal); }; }, []); - useEffect(() => { - setRefresh(Date.now()); - }, [lastUpdated]); - if (!lastUpdated) { return null; } - const isWarning = moment().diff(moment(lastUpdated), 'minute') > 5; - const isDanger = moment().diff(moment(lastUpdated), 'minute') > 10; - return ( - - - + + Last Updated: {moment(lastUpdated).from(refresh)} ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx new file mode 100644 index 0000000000000..a2a3e34c21834 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiSuperSelect } from '@elastic/eui'; +import { useSeriesStorage } from '../hooks/use_series_storage'; +import { SeriesConfig } from '../types'; + +interface Props { + seriesId: string; + defaultValue?: string; + options: SeriesConfig['metricOptions']; +} + +export function ReportMetricOptions({ seriesId, options: opts }: Props) { + const { getSeries, setSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + + const onChange = (value: string) => { + setSeries(seriesId, { + ...series, + selectedMetricField: value, + }); + }; + + const options = opts ?? []; + + return ( + ({ + value: fd || id, + inputDisplay: label, + }))} + valueOfSelected={series.selectedMetricField || options?.[0].field || options?.[0].id} + onChange={(value) => onChange(value)} + /> + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx new file mode 100644 index 0000000000000..684cf3a210a51 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx @@ -0,0 +1,303 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { RefObject, useEffect, useState } from 'react'; +import { isEmpty } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { + EuiBasicTable, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiSwitch, +} from '@elastic/eui'; +import { rgba } from 'polished'; +import { AppDataType, SeriesConfig, ReportViewType, SeriesUrl } from '../types'; +import { DataTypesCol } from './columns/data_types_col'; +import { ReportTypesCol } from './columns/report_types_col'; +import { ReportDefinitionCol } from './columns/report_definition_col'; +import { ReportFilters } from './columns/report_filters'; +import { ReportBreakdowns } from './columns/report_breakdowns'; +import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage'; +import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { getDefaultConfigs } from '../configurations/default_configs'; +import { SeriesEditor } from '../series_editor/series_editor'; +import { SeriesActions } from '../series_editor/columns/series_actions'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; +import { LastUpdated } from './last_updated'; +import { + CORE_WEB_VITALS_LABEL, + DEVICE_DISTRIBUTION_LABEL, + KPI_OVER_TIME_LABEL, + PERF_DIST_LABEL, +} from '../configurations/constants/labels'; + +export interface ReportTypeItem { + id: string; + reportType: ReportViewType; + label: string; +} + +export const ReportTypes: Record = { + synthetics: [ + { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, + { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL }, + ], + ux: [ + { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, + { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL }, + { id: 'cwv', reportType: 'core-web-vitals', label: CORE_WEB_VITALS_LABEL }, + ], + mobile: [ + { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, + { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL }, + { id: 'mdd', reportType: 'device-data-distribution', label: DEVICE_DISTRIBUTION_LABEL }, + ], + apm: [], + infra_logs: [], + infra_metrics: [], +}; + +interface BuilderItem { + id: string; + series: SeriesUrl; + seriesConfig?: SeriesConfig; +} + +export function SeriesBuilder({ + seriesBuilderRef, + lastUpdated, + multiSeries, +}: { + seriesBuilderRef: RefObject; + lastUpdated?: number; + multiSeries?: boolean; +}) { + const [editorItems, setEditorItems] = useState([]); + const { getSeries, allSeries, allSeriesIds, setSeries, removeSeries } = useSeriesStorage(); + + const { loading, indexPatterns } = useAppIndexPatternContext(); + + useEffect(() => { + const getDataViewSeries = (dataType: AppDataType, reportType: SeriesUrl['reportType']) => { + if (indexPatterns?.[dataType]) { + return getDefaultConfigs({ + dataType, + indexPattern: indexPatterns[dataType], + reportType: reportType!, + }); + } + }; + + const seriesToEdit: BuilderItem[] = + allSeriesIds + .filter((sId) => { + return allSeries?.[sId]?.isNew; + }) + .map((sId) => { + const series = getSeries(sId); + const seriesConfig = getDataViewSeries(series.dataType, series.reportType); + + return { id: sId, series, seriesConfig }; + }) ?? []; + const initSeries: BuilderItem[] = [{ id: 'series-id', series: {} as SeriesUrl }]; + setEditorItems(multiSeries || seriesToEdit.length > 0 ? seriesToEdit : initSeries); + }, [allSeries, allSeriesIds, getSeries, indexPatterns, loading, multiSeries]); + + const columns = [ + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.dataType', { + defaultMessage: 'Data Type', + }), + field: 'id', + width: '15%', + render: (seriesId: string) => , + }, + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.report', { + defaultMessage: 'Report', + }), + width: '15%', + field: 'id', + render: (seriesId: string, { series: { dataType } }: BuilderItem) => ( + + ), + }, + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.definition', { + defaultMessage: 'Definition', + }), + width: '30%', + field: 'id', + render: ( + seriesId: string, + { series: { dataType, reportType }, seriesConfig }: BuilderItem + ) => { + if (dataType && seriesConfig) { + return loading ? ( + LOADING_VIEW + ) : reportType ? ( + + ) : ( + SELECT_REPORT_TYPE + ); + } + + return null; + }, + }, + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.filters', { + defaultMessage: 'Filters', + }), + width: '20%', + field: 'id', + render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) => + reportType && seriesConfig ? ( + + ) : null, + }, + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.breakdown', { + defaultMessage: 'Breakdowns', + }), + width: '20%', + field: 'id', + render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) => + reportType && seriesConfig ? ( + + ) : null, + }, + ...(multiSeries + ? [ + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.actions', { + defaultMessage: 'Actions', + }), + align: 'center' as const, + width: '10%', + field: 'id', + render: (seriesId: string, item: BuilderItem) => ( + + ), + }, + ] + : []), + ]; + + const applySeries = () => { + editorItems.forEach(({ series, id: seriesId }) => { + const { reportType, reportDefinitions, isNew, ...restSeries } = series; + + if (reportType && !isEmpty(reportDefinitions)) { + const reportDefId = Object.values(reportDefinitions ?? {})[0]; + const newSeriesId = `${reportDefId}-${reportType}`; + + const newSeriesN: SeriesUrl = { + ...restSeries, + reportType, + reportDefinitions, + }; + + setSeries(newSeriesId, newSeriesN); + removeSeries(seriesId); + } + }); + }; + + const addSeries = () => { + const prevSeries = allSeries?.[allSeriesIds?.[0]]; + setSeries( + `${NEW_SERIES_KEY}-${editorItems.length + 1}`, + prevSeries + ? ({ isNew: true, time: prevSeries.time } as SeriesUrl) + : ({ isNew: true } as SeriesUrl) + ); + }; + + return ( + + {multiSeries && ( + + + + + + {}} + compressed + /> + + + applySeries()} isDisabled={true} size="s"> + {i18n.translate('xpack.observability.expView.seriesBuilder.apply', { + defaultMessage: 'Apply changes', + })} + + + + addSeries()} size="s"> + {i18n.translate('xpack.observability.expView.seriesBuilder.addSeries', { + defaultMessage: 'Add Series', + })} + + + + )} +
+ {multiSeries && } + {editorItems.length > 0 && ( + + )} + +
+
+ ); +} + +const Wrapper = euiStyled.div` + max-height: 50vh; + overflow-y: scroll; + overflow-x: clip; + &::-webkit-scrollbar { + height: ${({ theme }) => theme.eui.euiScrollBar}; + width: ${({ theme }) => theme.eui.euiScrollBar}; + } + &::-webkit-scrollbar-thumb { + background-clip: content-box; + background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)}; + border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent; + } + &::-webkit-scrollbar-corner, + &::-webkit-scrollbar-track { + background-color: transparent; + } +`; + +export const LOADING_VIEW = i18n.translate( + 'xpack.observability.expView.seriesBuilder.loadingView', + { + defaultMessage: 'Loading view ...', + } +); + +export const SELECT_REPORT_TYPE = i18n.translate( + 'xpack.observability.expView.seriesBuilder.selectReportType', + { + defaultMessage: 'No report type selected', + } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx similarity index 58% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx index 0b8e1c1785c7f..c30863585b3b0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx @@ -6,48 +6,48 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import { EuiDatePicker, EuiDatePickerRange } from '@elastic/eui'; -import { Moment } from 'moment'; import DateMath from '@elastic/datemath'; -import { i18n } from '@kbn/i18n'; +import { Moment } from 'moment'; import { useSeriesStorage } from '../hooks/use_series_storage'; import { useUiSetting } from '../../../../../../../../src/plugins/kibana_react/public'; -import { SeriesUrl } from '../types'; -import { ReportTypes } from '../configurations/constants'; export const parseAbsoluteDate = (date: string, options = {}) => { return DateMath.parse(date, options)!; }; -export function DateRangePicker({ seriesId, series }: { seriesId: number; series: SeriesUrl }) { - const { firstSeries, setSeries, reportType } = useSeriesStorage(); +export function DateRangePicker({ seriesId }: { seriesId: string }) { + const { firstSeriesId, getSeries, setSeries } = useSeriesStorage(); const dateFormat = useUiSetting('dateFormat'); - const seriesFrom = series.time?.from; - const seriesTo = series.time?.to; + const { + time: { from, to }, + reportType, + } = getSeries(firstSeriesId); - const { from: mainFrom, to: mainTo } = firstSeries!.time; + const series = getSeries(seriesId); - const startDate = parseAbsoluteDate(seriesFrom ?? mainFrom)!; - const endDate = parseAbsoluteDate(seriesTo ?? mainTo, { roundUp: true })!; + const { + time: { from: seriesFrom, to: seriesTo }, + } = series; - const getTotalDuration = () => { - const mainStartDate = parseAbsoluteDate(mainTo)!; - const mainEndDate = parseAbsoluteDate(mainTo, { roundUp: true })!; - return mainEndDate.diff(mainStartDate, 'millisecond'); - }; + const startDate = parseAbsoluteDate(seriesFrom ?? from)!; + const endDate = parseAbsoluteDate(seriesTo ?? to, { roundUp: true })!; - const onStartChange = (newStartDate: Moment) => { - if (reportType === ReportTypes.KPI) { - const totalDuration = getTotalDuration(); - const newFrom = newStartDate.toISOString(); - const newTo = newStartDate.add(totalDuration, 'millisecond').toISOString(); + const onStartChange = (newDate: Moment) => { + if (reportType === 'kpi-over-time') { + const mainStartDate = parseAbsoluteDate(from)!; + const mainEndDate = parseAbsoluteDate(to, { roundUp: true })!; + const totalDuration = mainEndDate.diff(mainStartDate, 'millisecond'); + const newFrom = newDate.toISOString(); + const newTo = newDate.add(totalDuration, 'millisecond').toISOString(); setSeries(seriesId, { ...series, time: { from: newFrom, to: newTo }, }); } else { - const newFrom = newStartDate.toISOString(); + const newFrom = newDate.toISOString(); setSeries(seriesId, { ...series, @@ -55,19 +55,20 @@ export function DateRangePicker({ seriesId, series }: { seriesId: number; series }); } }; - - const onEndChange = (newEndDate: Moment) => { - if (reportType === ReportTypes.KPI) { - const totalDuration = getTotalDuration(); - const newTo = newEndDate.toISOString(); - const newFrom = newEndDate.subtract(totalDuration, 'millisecond').toISOString(); + const onEndChange = (newDate: Moment) => { + if (reportType === 'kpi-over-time') { + const mainStartDate = parseAbsoluteDate(from)!; + const mainEndDate = parseAbsoluteDate(to, { roundUp: true })!; + const totalDuration = mainEndDate.diff(mainStartDate, 'millisecond'); + const newTo = newDate.toISOString(); + const newFrom = newDate.subtract(totalDuration, 'millisecond').toISOString(); setSeries(seriesId, { ...series, time: { from: newFrom, to: newTo }, }); } else { - const newTo = newEndDate.toISOString(); + const newTo = newDate.toISOString(); setSeries(seriesId, { ...series, @@ -89,7 +90,7 @@ export function DateRangePicker({ seriesId, series }: { seriesId: number; series aria-label={i18n.translate('xpack.observability.expView.dateRanger.startDate', { defaultMessage: 'Start date', })} - dateFormat={dateFormat.replace('ss.SSS', 'ss')} + dateFormat={dateFormat} showTimeSelect /> } @@ -103,7 +104,7 @@ export function DateRangePicker({ seriesId, series }: { seriesId: number; series aria-label={i18n.translate('xpack.observability.expView.dateRanger.endDate', { defaultMessage: 'End date', })} - dateFormat={dateFormat.replace('ss.SSS', 'ss')} + dateFormat={dateFormat} showTimeSelect /> } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx new file mode 100644 index 0000000000000..e21da424b58c8 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiSuperDatePicker } from '@elastic/eui'; +import React, { useEffect } from 'react'; +import { useHasData } from '../../../../hooks/use_has_data'; +import { useSeriesStorage } from '../hooks/use_series_storage'; +import { useQuickTimeRanges } from '../../../../hooks/use_quick_time_ranges'; +import { DEFAULT_TIME } from '../configurations/constants'; + +export interface TimePickerTime { + from: string; + to: string; +} + +export interface TimePickerQuickRange extends TimePickerTime { + display: string; +} + +interface Props { + seriesId: string; +} + +export function SeriesDatePicker({ seriesId }: Props) { + const { onRefreshTimeRange } = useHasData(); + + const commonlyUsedRanges = useQuickTimeRanges(); + + const { getSeries, setSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + + function onTimeChange({ start, end }: { start: string; end: string }) { + onRefreshTimeRange(); + setSeries(seriesId, { ...series, time: { from: start, to: end } }); + } + + useEffect(() => { + if (!series || !series.time) { + setSeries(seriesId, { ...series, time: DEFAULT_TIME }); + } + }, [series, seriesId, setSeries]); + + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/series_date_picker.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx similarity index 50% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/series_date_picker.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx index 3517508300e4b..931dfbe07cd23 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/series_date_picker.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx @@ -6,48 +6,67 @@ */ import React from 'react'; -import { mockUseHasData, render } from '../../rtl_helpers'; +import { mockUseHasData, render } from '../rtl_helpers'; import { fireEvent, waitFor } from '@testing-library/react'; import { SeriesDatePicker } from './index'; +import { DEFAULT_TIME } from '../configurations/constants'; describe('SeriesDatePicker', function () { it('should render properly', function () { const initSeries = { - data: [ - { - name: 'uptime-pings-histogram', + data: { + 'uptime-pings-histogram': { dataType: 'synthetics' as const, + reportType: 'data-distribution' as const, breakdown: 'monitor.status', time: { from: 'now-30m', to: 'now' }, }, - ], + }, }; - const { getByText } = render(, { - initSeries, - }); + const { getByText } = render(, { initSeries }); - getByText('Last 30 Minutes'); + getByText('Last 30 minutes'); + }); + + it('should set defaults', async function () { + const initSeries = { + data: { + 'uptime-pings-histogram': { + reportType: 'kpi-over-time' as const, + dataType: 'synthetics' as const, + breakdown: 'monitor.status', + }, + }, + }; + const { setSeries: setSeries1 } = render( + , + { initSeries: initSeries as any } + ); + expect(setSeries1).toHaveBeenCalledTimes(1); + expect(setSeries1).toHaveBeenCalledWith('uptime-pings-histogram', { + breakdown: 'monitor.status', + dataType: 'synthetics' as const, + reportType: 'kpi-over-time' as const, + time: DEFAULT_TIME, + }); }); it('should set series data', async function () { const initSeries = { - data: [ - { - name: 'uptime-pings-histogram', + data: { + 'uptime-pings-histogram': { dataType: 'synthetics' as const, + reportType: 'kpi-over-time' as const, breakdown: 'monitor.status', time: { from: 'now-30m', to: 'now' }, }, - ], + }, }; const { onRefreshTimeRange } = mockUseHasData(); - const { getByTestId, setSeries } = render( - , - { - initSeries, - } - ); + const { getByTestId, setSeries } = render(, { + initSeries, + }); await waitFor(function () { fireEvent.click(getByTestId('superDatePickerToggleQuickMenuButton')); @@ -57,10 +76,10 @@ describe('SeriesDatePicker', function () { expect(onRefreshTimeRange).toHaveBeenCalledTimes(1); - expect(setSeries).toHaveBeenCalledWith(0, { - name: 'uptime-pings-histogram', + expect(setSeries).toHaveBeenCalledWith('series-id', { breakdown: 'monitor.status', dataType: 'synthetics', + reportType: 'kpi-over-time', time: { from: 'now/d', to: 'now/d' }, }); expect(setSeries).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx new file mode 100644 index 0000000000000..207a53e13f1ad --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { Breakdowns } from './columns/breakdowns'; +import { SeriesConfig } from '../types'; +import { ChartOptions } from './columns/chart_options'; + +interface Props { + seriesConfig: SeriesConfig; + seriesId: string; + breakdownFields: string[]; +} +export function ChartEditOptions({ seriesConfig, seriesId, breakdownFields }: Props) { + return ( + + + + + + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx similarity index 74% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx index 21b766227a562..84568e1c5068a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; import { Breakdowns } from './breakdowns'; -import { mockIndexPattern, mockUxSeries, render } from '../../rtl_helpers'; +import { mockIndexPattern, render } from '../../rtl_helpers'; import { getDefaultConfigs } from '../../configurations/default_configs'; import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames'; @@ -20,7 +20,13 @@ describe('Breakdowns', function () { }); it('should render properly', async function () { - render(); + render( + + ); screen.getAllByText('Browser family'); }); @@ -30,9 +36,9 @@ describe('Breakdowns', function () { const { setSeries } = render( , { initSeries } ); @@ -43,14 +49,10 @@ describe('Breakdowns', function () { fireEvent.click(screen.getByText('Browser family')); - expect(setSeries).toHaveBeenCalledWith(0, { + expect(setSeries).toHaveBeenCalledWith('series-id', { breakdown: 'user_agent.name', dataType: 'ux', - name: 'performance-distribution', - reportDefinitions: { - 'service.name': ['elastic-co'], - }, - selectedMetricField: 'transaction.duration.us', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }); expect(setSeries).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx similarity index 71% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx index 315f63e33bed0..2237935d466ad 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx @@ -8,20 +8,20 @@ import React from 'react'; import { EuiSuperSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useRouteMatch } from 'react-router-dom'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { USE_BREAK_DOWN_COLUMN } from '../../configurations/constants'; -import { SeriesConfig, SeriesUrl } from '../../types'; +import { SeriesConfig } from '../../types'; interface Props { - seriesId: number; - series: SeriesUrl; + seriesId: string; + breakdowns: string[]; seriesConfig: SeriesConfig; } -export function Breakdowns({ seriesConfig, seriesId, series }: Props) { - const { setSeries } = useSeriesStorage(); - const isPreview = !!useRouteMatch('/exploratory-view/preview'); +export function Breakdowns({ seriesConfig, seriesId, breakdowns = [] }: Props) { + const { setSeries, getSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); const selectedBreakdown = series.breakdown; const NO_BREAKDOWN = 'no_breakdown'; @@ -40,13 +40,9 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) { } }; - if (!seriesConfig) { - return null; - } - const hasUseBreakdownColumn = seriesConfig.xAxisColumn.sourceField === USE_BREAK_DOWN_COLUMN; - const items = seriesConfig.breakdownFields.map((breakdown) => ({ + const items = breakdowns.map((breakdown) => ({ id: breakdown, label: seriesConfig.labels[breakdown], })); @@ -54,12 +50,14 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) { if (!hasUseBreakdownColumn) { items.push({ id: NO_BREAKDOWN, - label: NO_BREAK_DOWN_LABEL, + label: i18n.translate('xpack.observability.exp.breakDownFilter.noBreakdown', { + defaultMessage: 'No breakdown', + }), }); } const options = items.map(({ id, label }) => ({ - inputDisplay: label, + inputDisplay: id === NO_BREAKDOWN ? label : {label}, value: id, dropdownDisplay: label, })); @@ -71,7 +69,7 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) {
onOptionChange(value)} @@ -80,10 +78,3 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) {
); } - -export const NO_BREAK_DOWN_LABEL = i18n.translate( - 'xpack.observability.exp.breakDownFilter.noBreakdown', - { - defaultMessage: 'No breakdown', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx new file mode 100644 index 0000000000000..f2a6377fd9b71 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { SeriesConfig } from '../../types'; +import { OperationTypeSelect } from '../../series_builder/columns/operation_type_select'; +import { SeriesChartTypesSelect } from '../../series_builder/columns/chart_types'; + +interface Props { + seriesConfig: SeriesConfig; + seriesId: string; +} + +export function ChartOptions({ seriesConfig, seriesId }: Props) { + return ( + + + + + {seriesConfig.hasOperationType && ( + + + + )} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx deleted file mode 100644 index 838631e1f05df..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx +++ /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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { fireEvent, screen } from '@testing-library/react'; -import { mockAppIndexPattern, mockUxSeries, render } from '../../rtl_helpers'; -import { DataTypesLabels, DataTypesSelect } from './data_type_select'; -import { DataTypes } from '../../configurations/constants'; - -describe('DataTypeSelect', function () { - const seriesId = 0; - - mockAppIndexPattern(); - - it('should render properly', function () { - render(); - }); - - it('should set series on change', async function () { - const { setSeries } = render(); - - fireEvent.click(await screen.findByText(DataTypesLabels[DataTypes.UX])); - fireEvent.click(await screen.findByText(DataTypesLabels[DataTypes.SYNTHETICS])); - - expect(setSeries).toHaveBeenCalledTimes(1); - expect(setSeries).toHaveBeenCalledWith(seriesId, { - dataType: 'synthetics', - name: 'synthetics-series-1', - time: { - from: 'now-15m', - to: 'now', - }, - }); - }); -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx deleted file mode 100644 index b0a6e3b5e26b0..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiSuperSelect } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { AppDataType, SeriesUrl } from '../../types'; -import { DataTypes, ReportTypes } from '../../configurations/constants'; - -interface Props { - seriesId: number; - series: SeriesUrl; -} - -export const DataTypesLabels = { - [DataTypes.UX]: i18n.translate('xpack.observability.overview.exploratoryView.uxLabel', { - defaultMessage: 'User experience (RUM)', - }), - - [DataTypes.SYNTHETICS]: i18n.translate( - 'xpack.observability.overview.exploratoryView.syntheticsLabel', - { - defaultMessage: 'Synthetics monitoring', - } - ), - - [DataTypes.MOBILE]: i18n.translate( - 'xpack.observability.overview.exploratoryView.mobileExperienceLabel', - { - defaultMessage: 'Mobile experience', - } - ), -}; - -export const dataTypes: Array<{ id: AppDataType; label: string }> = [ - { - id: DataTypes.SYNTHETICS, - label: DataTypesLabels[DataTypes.SYNTHETICS], - }, - { - id: DataTypes.UX, - label: DataTypesLabels[DataTypes.UX], - }, - { - id: DataTypes.MOBILE, - label: DataTypesLabels[DataTypes.MOBILE], - }, -]; - -const SELECT_DATA_TYPE = 'SELECT_DATA_TYPE'; - -export function DataTypesSelect({ seriesId, series }: Props) { - const { setSeries, reportType } = useSeriesStorage(); - - const onDataTypeChange = (dataType: AppDataType) => { - if (String(dataType) !== SELECT_DATA_TYPE) { - setSeries(seriesId, { - dataType, - time: series.time, - name: `${dataType}-series-${seriesId + 1}`, - }); - } - }; - - const options = dataTypes - .filter(({ id }) => { - if (reportType === ReportTypes.DEVICE_DISTRIBUTION) { - return id === DataTypes.MOBILE; - } - if (reportType === ReportTypes.CORE_WEB_VITAL) { - return id === DataTypes.UX; - } - return true; - }) - .map(({ id, label }) => ({ - value: id, - inputDisplay: label, - })); - - return ( - onDataTypeChange(value as AppDataType)} - style={{ minWidth: 220 }} - /> - ); -} - -const SELECT_DATA_TYPE_LABEL = i18n.translate( - 'xpack.observability.overview.exploratoryView.selectDataType', - { - defaultMessage: 'Select data type', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx index 032eb66dcfa4f..41e83f407af2b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx @@ -6,84 +6,24 @@ */ import React from 'react'; -import styled from 'styled-components'; -import { i18n } from '@kbn/i18n'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { SeriesDatePicker } from '../../series_date_picker'; import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { DateRangePicker } from '../../components/date_range_picker'; -import { SeriesDatePicker } from '../../components/series_date_picker'; -import { AppDataType, SeriesUrl } from '../../types'; -import { ReportTypes } from '../../configurations/constants'; -import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; -import { SyntheticsAddData } from '../../../add_data_buttons/synthetics_add_data'; -import { MobileAddData } from '../../../add_data_buttons/mobile_add_data'; -import { UXAddData } from '../../../add_data_buttons/ux_add_data'; +import { DateRangePicker } from '../../series_date_picker/date_range_picker'; interface Props { - seriesId: number; - series: SeriesUrl; + seriesId: string; } - -const AddDataComponents: Record = { - mobile: MobileAddData, - ux: UXAddData, - synthetics: SyntheticsAddData, - apm: null, - infra_logs: null, - infra_metrics: null, -}; - -export function DatePickerCol({ seriesId, series }: Props) { - const { reportType } = useSeriesStorage(); - - const { hasAppData } = useAppIndexPatternContext(); - - if (!series.dataType) { - return null; - } - - const AddDataButton = AddDataComponents[series.dataType]; - if (hasAppData[series.dataType] === false && AddDataButton !== null) { - return ( - - - - {i18n.translate('xpack.observability.overview.exploratoryView.noDataAvailable', { - defaultMessage: 'No {dataType} data available.', - values: { - dataType: series.dataType, - }, - })} - - - - - - - ); - } - - if (!series.selectedMetricField) { - return null; - } +export function DatePickerCol({ seriesId }: Props) { + const { firstSeriesId, getSeries } = useSeriesStorage(); + const { reportType } = getSeries(firstSeriesId); return ( - - {seriesId === 0 || reportType !== ReportTypes.KPI ? ( - +
+ {firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? ( + ) : ( - + )} - +
); } - -const Wrapper = styled.div` - width: 100%; - .euiSuperDatePicker__flexWrapper { - width: 100%; - > .euiFlexItem { - margin-right: 0; - } - } -`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx similarity index 67% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx index a88e2eadd10c9..90a039f6b44d0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx @@ -8,24 +8,20 @@ import React from 'react'; import { fireEvent, screen, waitFor } from '@testing-library/react'; import { FilterExpanded } from './filter_expanded'; -import { mockUxSeries, mockAppIndexPattern, mockUseValuesList, render } from '../../rtl_helpers'; +import { mockAppIndexPattern, mockUseValuesList, render } from '../../rtl_helpers'; import { USER_AGENT_NAME } from '../../configurations/constants/elasticsearch_fieldnames'; describe('FilterExpanded', function () { - const filters = [{ field: USER_AGENT_NAME, values: ['Chrome'] }]; - - const mockSeries = { ...mockUxSeries, filters }; - - it('render', async () => { - const initSeries = { filters }; + it('should render properly', async function () { + const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; mockAppIndexPattern(); render( , { initSeries } @@ -37,14 +33,15 @@ describe('FilterExpanded', function () { }); it('should call go back on click', async function () { - const initSeries = { filters }; + const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; + const goBack = jest.fn(); render( , { initSeries } @@ -52,23 +49,28 @@ describe('FilterExpanded', function () { await waitFor(() => { fireEvent.click(screen.getByText('Browser Family')); + + expect(goBack).toHaveBeenCalledTimes(1); + expect(goBack).toHaveBeenCalledWith(); }); }); - it('calls useValuesList on load', async () => { - const initSeries = { filters }; + it('should call useValuesList on load', async function () { + const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; const { spy } = mockUseValuesList([ { label: 'Chrome', count: 10 }, { label: 'Firefox', count: 5 }, ]); + const goBack = jest.fn(); + render( , { initSeries } @@ -85,8 +87,8 @@ describe('FilterExpanded', function () { }); }); - it('filters display values', async () => { - const initSeries = { filters }; + it('should filter display values', async function () { + const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; mockUseValuesList([ { label: 'Chrome', count: 10 }, @@ -95,20 +97,18 @@ describe('FilterExpanded', function () { render( , { initSeries } ); - await waitFor(() => { - fireEvent.click(screen.getByText('Browser Family')); - - expect(screen.queryByText('Firefox')).toBeTruthy(); + expect(screen.getByText('Firefox')).toBeTruthy(); + await waitFor(() => { fireEvent.input(screen.getByRole('searchbox'), { target: { value: 'ch' } }); expect(screen.queryByText('Firefox')).toBeFalsy(); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx similarity index 55% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx index 1ef25722aff5c..4310402a43a08 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx @@ -6,14 +6,7 @@ */ import React, { useState, Fragment } from 'react'; -import { - EuiFieldSearch, - EuiSpacer, - EuiFilterGroup, - EuiText, - EuiPopover, - EuiFilterButton, -} from '@elastic/eui'; +import { EuiFieldSearch, EuiSpacer, EuiButtonEmpty, EuiFilterGroup, EuiText } from '@elastic/eui'; import styled from 'styled-components'; import { rgba } from 'polished'; import { i18n } from '@kbn/i18n'; @@ -21,7 +14,8 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; import { map } from 'lodash'; import { ExistsFilter } from '@kbn/es-query'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; -import { SeriesConfig, SeriesUrl, UrlFilter } from '../../types'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { SeriesConfig, UrlFilter } from '../../types'; import { FilterValueButton } from './filter_value_btn'; import { useValuesList } from '../../../../../hooks/use_values_list'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; @@ -29,33 +23,31 @@ import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearc import { PersistableFilter } from '../../../../../../../lens/common'; interface Props { - seriesId: number; - series: SeriesUrl; + seriesId: string; label: string; field: string; isNegated?: boolean; + goBack: () => void; nestedField?: string; filters: SeriesConfig['baseFilters']; } -export interface NestedFilterOpen { - value: string; - negate: boolean; -} - export function FilterExpanded({ seriesId, - series, field, label, + goBack, nestedField, isNegated, filters: defaultFilters, }: Props) { const [value, setValue] = useState(''); - const [isOpen, setIsOpen] = useState(false); - const [isNestedOpen, setIsNestedOpen] = useState({ value: '', negate: false }); + const [isOpen, setIsOpen] = useState({ value: '', negate: false }); + + const { getSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); const queryFilters: ESFilter[] = []; @@ -89,71 +81,62 @@ export function FilterExpanded({ ); return ( - setIsOpen((prevState) => !prevState)} iconType="arrowDown"> - {label} - - } - isOpen={isOpen} - closePopover={() => setIsOpen(false)} - > - - { - setValue(evt.target.value); - }} - placeholder={i18n.translate('xpack.observability.filters.expanded.search', { - defaultMessage: 'Search for {label}', - values: { label }, - })} - /> - - - {displayValues.length === 0 && !loading && ( - - {i18n.translate('xpack.observability.filters.expanded.noFilter', { - defaultMessage: 'No filters found.', - })} - - )} - {displayValues.map((opt) => ( - - - {isNegated !== false && ( - - )} + + goBack()}> + {label} + + { + setValue(evt.target.value); + }} + placeholder={i18n.translate('xpack.observability.filters.expanded.search', { + defaultMessage: 'Search for {label}', + values: { label }, + })} + /> + + + {displayValues.length === 0 && !loading && ( + + {i18n.translate('xpack.observability.filters.expanded.noFilter', { + defaultMessage: 'No filters found.', + })} + + )} + {displayValues.map((opt) => ( + + + {isNegated !== false && ( - - - - ))} - - - + )} + + + + + ))} + +
); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx similarity index 64% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx index 764a27fd663f5..a9609abc70d69 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { fireEvent, screen, waitFor } from '@testing-library/react'; import { FilterValueButton } from './filter_value_btn'; -import { mockUxSeries, mockUseSeriesFilter, mockUseValuesList, render } from '../../rtl_helpers'; +import { mockUseSeriesFilter, mockUseValuesList, render } from '../../rtl_helpers'; import { USER_AGENT_NAME, USER_AGENT_VERSION, @@ -19,98 +19,84 @@ describe('FilterValueButton', function () { render( ); - await waitFor(() => { - expect(screen.getByText('Chrome')).toBeInTheDocument(); - }); + screen.getByText('Chrome'); }); - describe('when negate is true', () => { - it('displays negate stats', async () => { - render( - - ); + it('should render display negate state', async function () { + render( + + ); - await waitFor(() => { - expect(screen.getByText('Not Chrome')).toBeInTheDocument(); - expect(screen.getByTitle('Not Chrome')).toBeInTheDocument(); - const btn = screen.getByRole('button'); - expect(btn.classList).toContain('euiButtonEmpty--danger'); - }); + await waitFor(() => { + screen.getByText('Not Chrome'); + screen.getByTitle('Not Chrome'); + const btn = screen.getByRole('button'); + expect(btn.classList).toContain('euiButtonEmpty--danger'); }); + }); - it('calls setFilter on click', async () => { - const { setFilter, removeFilter } = mockUseSeriesFilter(); + it('should call set filter on click', async function () { + const { setFilter, removeFilter } = mockUseSeriesFilter(); - render( - - ); + render( + + ); + await waitFor(() => { fireEvent.click(screen.getByText('Not Chrome')); - - await waitFor(() => { - expect(removeFilter).toHaveBeenCalledTimes(0); - expect(setFilter).toHaveBeenCalledTimes(1); - - expect(setFilter).toHaveBeenCalledWith({ - field: 'user_agent.name', - negate: true, - value: 'Chrome', - }); + expect(removeFilter).toHaveBeenCalledTimes(0); + expect(setFilter).toHaveBeenCalledTimes(1); + expect(setFilter).toHaveBeenCalledWith({ + field: 'user_agent.name', + negate: true, + value: 'Chrome', }); }); }); - describe('when selected', () => { - it('removes the filter on click', async () => { - const { removeFilter } = mockUseSeriesFilter(); - - render( - - ); + it('should remove filter on click if already selected', async function () { + const { removeFilter } = mockUseSeriesFilter(); + render( + + ); + await waitFor(() => { fireEvent.click(screen.getByText('Chrome')); - - await waitFor(() => { - expect(removeFilter).toHaveBeenCalledWith({ - field: 'user_agent.name', - negate: false, - value: 'Chrome', - }); + expect(removeFilter).toHaveBeenCalledWith({ + field: 'user_agent.name', + negate: false, + value: 'Chrome', }); }); }); @@ -121,13 +107,12 @@ describe('FilterValueButton', function () { render( ); @@ -149,14 +134,13 @@ describe('FilterValueButton', function () { render( ); @@ -183,14 +167,13 @@ describe('FilterValueButton', function () { render( ); @@ -220,14 +203,13 @@ describe('FilterValueButton', function () { render( ); @@ -247,14 +229,13 @@ describe('FilterValueButton', function () { render( ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx similarity index 92% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx index 111f915a95f46..bf4ca6eb83d94 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx @@ -8,11 +8,10 @@ import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import { EuiFilterButton, hexToRgb } from '@elastic/eui'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; import { useSeriesFilters } from '../../hooks/use_series_filters'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import FieldValueSuggestions from '../../../field_value_suggestions'; -import { SeriesUrl } from '../../types'; -import { NestedFilterOpen } from './filter_expanded'; interface Props { value: string; @@ -20,13 +19,12 @@ interface Props { allSelectedValues?: string[]; negate: boolean; nestedField?: string; - seriesId: number; - series: SeriesUrl; + seriesId: string; isNestedOpen: { value: string; negate: boolean; }; - setIsNestedOpen: (val: NestedFilterOpen) => void; + setIsNestedOpen: (val: { value: string; negate: boolean }) => void; } export function FilterValueButton({ @@ -36,13 +34,16 @@ export function FilterValueButton({ field, negate, seriesId, - series, nestedField, allSelectedValues, }: Props) { + const { getSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + const { indexPatterns } = useAppIndexPatternContext(series.dataType); - const { setFilter, removeFilter } = useSeriesFilters({ seriesId, series }); + const { setFilter, removeFilter } = useSeriesFilters({ seriesId }); const hasActiveFilters = (allSelectedValues ?? []).includes(value); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx new file mode 100644 index 0000000000000..e75f308dab1e5 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { EuiButtonIcon } from '@elastic/eui'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; + +interface Props { + seriesId: string; +} + +export function RemoveSeries({ seriesId }: Props) { + const { removeSeries } = useSeriesStorage(); + + const onClick = () => { + removeSeries(seriesId); + }; + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx deleted file mode 100644 index dad2a7da2367b..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { SeriesConfig, SeriesUrl } from '../../types'; -import { ReportDefinitionField } from './report_definition_field'; - -export function ReportDefinitionCol({ - seriesId, - series, - seriesConfig, -}: { - seriesId: number; - series: SeriesUrl; - seriesConfig: SeriesConfig; -}) { - const { setSeries } = useSeriesStorage(); - - const { reportDefinitions: selectedReportDefinitions = {} } = series; - - const { definitionFields } = seriesConfig; - - const onChange = (field: string, value?: string[]) => { - if (!value?.[0]) { - delete selectedReportDefinitions[field]; - setSeries(seriesId, { - ...series, - reportDefinitions: { ...selectedReportDefinitions }, - }); - } else { - setSeries(seriesId, { - ...series, - reportDefinitions: { ...selectedReportDefinitions, [field]: value }, - }); - } - }; - - return ( - - {definitionFields.map((field) => ( - - - - ))} - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx deleted file mode 100644 index 01c9fce7637bb..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiSuperSelect } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { ReportViewType } from '../../types'; -import { - CORE_WEB_VITALS_LABEL, - DEVICE_DISTRIBUTION_LABEL, - KPI_OVER_TIME_LABEL, - PERF_DIST_LABEL, -} from '../../configurations/constants/labels'; - -const SELECT_REPORT_TYPE = 'SELECT_REPORT_TYPE'; - -export const reportTypesList: Array<{ - reportType: ReportViewType | typeof SELECT_REPORT_TYPE; - label: string; -}> = [ - { - reportType: SELECT_REPORT_TYPE, - label: i18n.translate('xpack.observability.expView.reportType.selectLabel', { - defaultMessage: 'Select report type', - }), - }, - { reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, - { reportType: 'data-distribution', label: PERF_DIST_LABEL }, - { reportType: 'core-web-vitals', label: CORE_WEB_VITALS_LABEL }, - { reportType: 'device-data-distribution', label: DEVICE_DISTRIBUTION_LABEL }, -]; - -export function ReportTypesSelect() { - const { setReportType, reportType: selectedReportType, allSeries } = useSeriesStorage(); - - const onReportTypeChange = (reportType: ReportViewType) => { - setReportType(reportType); - }; - - const options = reportTypesList - .filter(({ reportType }) => (selectedReportType ? reportType !== SELECT_REPORT_TYPE : true)) - .map(({ reportType, label }) => ({ - value: reportType, - inputDisplay: reportType === SELECT_REPORT_TYPE ? label : {label}, - dropdownDisplay: label, - })); - - return ( - onReportTypeChange(value as ReportViewType)} - style={{ minWidth: 200 }} - isInvalid={!selectedReportType && allSeries.length > 0} - disabled={allSeries.length > 0} - /> - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx new file mode 100644 index 0000000000000..51ebe6c6bd9d5 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { isEmpty } from 'lodash'; +import { RemoveSeries } from './remove_series'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { SeriesUrl } from '../../types'; + +interface Props { + seriesId: string; + editorMode?: boolean; +} +export function SeriesActions({ seriesId, editorMode = false }: Props) { + const { getSeries, setSeries, allSeriesIds, removeSeries } = useSeriesStorage(); + const series = getSeries(seriesId); + + const onEdit = () => { + setSeries(seriesId, { ...series, isNew: true }); + }; + + const copySeries = () => { + let copySeriesId: string = `${seriesId}-copy`; + if (allSeriesIds.includes(copySeriesId)) { + copySeriesId = copySeriesId + allSeriesIds.length; + } + setSeries(copySeriesId, series); + }; + + const { reportType, reportDefinitions, isNew, ...restSeries } = series; + const isSaveAble = reportType && !isEmpty(reportDefinitions); + + const saveSeries = () => { + if (isSaveAble) { + const reportDefId = Object.values(reportDefinitions ?? {})[0]; + let newSeriesId = `${reportDefId}-${reportType}`; + + if (allSeriesIds.includes(newSeriesId)) { + newSeriesId = `${newSeriesId}-${allSeriesIds.length}`; + } + const newSeriesN: SeriesUrl = { + ...restSeries, + reportType, + reportDefinitions, + }; + + setSeries(newSeriesId, newSeriesN); + removeSeries(seriesId); + } + }; + + return ( + + {!editorMode && ( + + + + )} + {editorMode && ( + + + + )} + {editorMode && ( + + + + )} + + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx new file mode 100644 index 0000000000000..02144c6929b38 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx @@ -0,0 +1,155 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React, { useState, Fragment } from 'react'; +import { + EuiButton, + EuiPopover, + EuiSpacer, + EuiButtonEmpty, + EuiFlexItem, + EuiFlexGroup, +} from '@elastic/eui'; +import { FilterExpanded } from './filter_expanded'; +import { SeriesConfig } from '../../types'; +import { FieldLabels } from '../../configurations/constants/constants'; +import { SelectedFilters } from '../selected_filters'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; + +interface Props { + seriesId: string; + filterFields: SeriesConfig['filterFields']; + baseFilters: SeriesConfig['baseFilters']; + seriesConfig: SeriesConfig; + isNew?: boolean; + labels?: Record; +} + +export interface Field { + label: string; + field: string; + nested?: string; + isNegated?: boolean; +} + +export function SeriesFilter({ + seriesConfig, + isNew, + seriesId, + filterFields = [], + baseFilters, + labels, +}: Props) { + const [isPopoverVisible, setIsPopoverVisible] = useState(false); + + const [selectedField, setSelectedField] = useState(); + + const options: Field[] = filterFields.map((field) => { + if (typeof field === 'string') { + return { label: labels?.[field] ?? FieldLabels[field], field }; + } + + return { + field: field.field, + nested: field.nested, + isNegated: field.isNegated, + label: labels?.[field.field] ?? FieldLabels[field.field], + }; + }); + + const { setSeries, getSeries } = useSeriesStorage(); + const urlSeries = getSeries(seriesId); + + const button = ( + { + setIsPopoverVisible((prevState) => !prevState); + }} + size="s" + > + {i18n.translate('xpack.observability.expView.seriesEditor.addFilter', { + defaultMessage: 'Add filter', + })} + + ); + + const mainPanel = ( + <> + + {options.map((opt) => ( + + { + setSelectedField(opt); + }} + > + {opt.label} + + + + ))} + + ); + + const childPanel = selectedField ? ( + { + setSelectedField(undefined); + }} + filters={baseFilters} + /> + ) : null; + + const closePopover = () => { + setIsPopoverVisible(false); + setSelectedField(undefined); + }; + + return ( + + + + + {!selectedField ? mainPanel : childPanel} + + + {(urlSeries.filters ?? []).length > 0 && ( + + { + setSeries(seriesId, { ...urlSeries, filters: undefined }); + }} + size="s" + > + {i18n.translate('xpack.observability.expView.seriesEditor.clearFilter', { + defaultMessage: 'Clear filters', + })} + + + )} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx deleted file mode 100644 index 801c885ec9a62..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; - -import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui'; -import { SeriesConfig, SeriesUrl } from '../types'; -import { ReportDefinitionCol } from './columns/report_definition_col'; -import { OperationTypeSelect } from './columns/operation_type_select'; -import { parseCustomFieldName } from '../configurations/lens_attributes'; -import { SeriesFilter } from '../series_viewer/columns/series_filter'; - -function getColumnType(seriesConfig: SeriesConfig, selectedMetricField?: string) { - const { columnType } = parseCustomFieldName(seriesConfig, selectedMetricField); - - return columnType; -} - -interface Props { - seriesId: number; - series: SeriesUrl; - seriesConfig: SeriesConfig; -} -export function ExpandedSeriesRow({ seriesId, series, seriesConfig }: Props) { - if (!seriesConfig) { - return null; - } - - const { selectedMetricField } = series ?? {}; - - const { hasOperationType, yAxisColumns } = seriesConfig; - - const columnType = getColumnType(seriesConfig, selectedMetricField); - - return ( -
- - - - - - - - - - - - - {(hasOperationType || columnType === 'operation') && ( - - - - - - )} - - -
- ); -} - -const FILTERS_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.selectFilters', { - defaultMessage: 'Filters', -}); - -const OPERATION_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.operation', { - defaultMessage: 'Operation', -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx deleted file mode 100644 index 85eb85e0fc30a..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiSuperSelect, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { useSeriesStorage } from '../hooks/use_series_storage'; -import { SeriesConfig, SeriesUrl } from '../types'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; -import { RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD } from '../configurations/constants'; - -interface Props { - seriesId: number; - series: SeriesUrl; - defaultValue?: string; - metricOptions: SeriesConfig['metricOptions']; -} - -const SELECT_REPORT_METRIC = 'SELECT_REPORT_METRIC'; - -export function ReportMetricOptions({ seriesId, series, metricOptions }: Props) { - const { setSeries } = useSeriesStorage(); - - const { indexPatterns } = useAppIndexPatternContext(); - - const onChange = (value: string) => { - setSeries(seriesId, { - ...series, - selectedMetricField: value, - }); - }; - - if (!series.dataType) { - return null; - } - - const indexPattern = indexPatterns?.[series.dataType]; - - const options = (metricOptions ?? []).map(({ label, field, id }) => { - let disabled = false; - - if (field !== RECORDS_FIELD && field !== RECORDS_PERCENTAGE_FIELD && field) { - disabled = !Boolean(indexPattern?.getFieldByName(field)); - } - return { - disabled, - value: field || id, - dropdownDisplay: disabled ? ( - {field}, - }} - /> - } - > - {label} - - ) : ( - label - ), - inputDisplay: label, - }; - }); - - return ( - onChange(value)} - style={{ minWidth: 220 }} - /> - ); -} - -const SELECT_REPORT_METRIC_LABEL = i18n.translate( - 'xpack.observability.expView.seriesEditor.selectReportMetric', - { - defaultMessage: 'Select report metric', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx similarity index 71% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx index 8fc5ae95fd41b..eb76772a66c7e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { screen, waitFor } from '@testing-library/react'; -import { mockAppIndexPattern, mockIndexPattern, mockUxSeries, render } from '../rtl_helpers'; +import { mockAppIndexPattern, mockIndexPattern, render } from '../rtl_helpers'; import { SelectedFilters } from './selected_filters'; import { getDefaultConfigs } from '../configurations/default_configs'; import { USER_AGENT_NAME } from '../configurations/constants/elasticsearch_fieldnames'; @@ -22,19 +22,11 @@ describe('SelectedFilters', function () { }); it('should render properly', async function () { - const filters = [{ field: USER_AGENT_NAME, values: ['Chrome'] }]; - const initSeries = { filters }; + const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; - render( - , - { - initSeries, - } - ); + render(, { + initSeries, + }); await waitFor(() => { screen.getByText('Chrome'); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx new file mode 100644 index 0000000000000..5d2ce6ba84951 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Fragment } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useSeriesStorage } from '../hooks/use_series_storage'; +import { FilterLabel } from '../components/filter_label'; +import { SeriesConfig, UrlFilter } from '../types'; +import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { useSeriesFilters } from '../hooks/use_series_filters'; +import { getFiltersFromDefs } from '../hooks/use_lens_attributes'; + +interface Props { + seriesId: string; + seriesConfig: SeriesConfig; + isNew?: boolean; +} +export function SelectedFilters({ seriesId, isNew, seriesConfig }: Props) { + const { getSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + + const { reportDefinitions = {} } = series; + + const { labels } = seriesConfig; + + const filters: UrlFilter[] = series.filters ?? []; + + let definitionFilters: UrlFilter[] = getFiltersFromDefs(reportDefinitions); + + // we don't want to display report definition filters in new series view + if (isNew) { + definitionFilters = []; + } + + const { removeFilter } = useSeriesFilters({ seriesId }); + + const { indexPattern } = useAppIndexPatternContext(series.dataType); + + return (filters.length > 0 || definitionFilters.length > 0) && indexPattern ? ( + + + {filters.map(({ field, values, notValues }) => ( + + {(values ?? []).map((val) => ( + + removeFilter({ field, value: val, negate: false })} + negate={false} + indexPattern={indexPattern} + /> + + ))} + {(notValues ?? []).map((val) => ( + + removeFilter({ field, value: val, negate: true })} + indexPattern={indexPattern} + /> + + ))} + + ))} + + {definitionFilters.map(({ field, values }) => ( + + {(values ?? []).map((val) => ( + + { + // FIXME handle this use case + }} + negate={false} + definitionFilter={true} + indexPattern={indexPattern} + /> + + ))} + + ))} + + + ) : null; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx index 80fe400830832..c3cc8484d1751 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx @@ -5,399 +5,134 @@ * 2.0. */ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; -import { - EuiBasicTable, - EuiButtonIcon, - EuiSpacer, - EuiFormRow, - EuiFlexItem, - EuiFlexGroup, - EuiButtonEmpty, -} from '@elastic/eui'; -import { rgba } from 'polished'; -import classNames from 'classnames'; -import { isEmpty } from 'lodash'; -import { euiStyled } from './../../../../../../../../src/plugins/kibana_react/common'; -import { AppDataType, SeriesConfig, ReportViewType, SeriesUrl } from '../types'; -import { SeriesContextValue, useSeriesStorage } from '../hooks/use_series_storage'; -import { IndexPatternState, useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { EuiBasicTable, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { SeriesFilter } from './columns/series_filter'; +import { SeriesConfig } from '../types'; +import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; -import { SeriesActions } from '../series_viewer/columns/series_actions'; -import { SeriesInfo } from '../series_viewer/columns/series_info'; -import { DataTypesSelect } from './columns/data_type_select'; import { DatePickerCol } from './columns/date_picker_col'; -import { ExpandedSeriesRow } from './expanded_series_row'; -import { SeriesName } from '../series_viewer/columns/series_name'; -import { ReportTypesSelect } from './columns/report_type_select'; -import { ViewActions } from '../views/view_actions'; -import { ReportMetricOptions } from './report_metric_options'; -import { Breakdowns } from '../series_viewer/columns/breakdowns'; +import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { SeriesActions } from './columns/series_actions'; +import { ChartEditOptions } from './chart_edit_options'; -export interface ReportTypeItem { - id: string; - reportType: ReportViewType; - label: string; -} - -export interface BuilderItem { - id: number; - series: SeriesUrl; +interface EditItem { seriesConfig: SeriesConfig; + id: string; } -type ExpandedRowMap = Record; - -export const getSeriesToEdit = ({ - indexPatterns, - allSeries, - reportType, -}: { - allSeries: SeriesContextValue['allSeries']; - indexPatterns: IndexPatternState; - reportType: ReportViewType; -}): BuilderItem[] => { - const getDataViewSeries = (dataType: AppDataType) => { - if (indexPatterns?.[dataType]) { - return getDefaultConfigs({ - dataType, - reportType, - indexPattern: indexPatterns[dataType], - }); - } - }; - - return allSeries.map((series, seriesIndex) => { - const seriesConfig = getDataViewSeries(series.dataType)!; - - return { id: seriesIndex, series, seriesConfig }; - }); -}; - -export const SeriesEditor = React.memo(function () { - const [editorItems, setEditorItems] = useState([]); - - const { getSeries, allSeries, reportType, removeSeries } = useSeriesStorage(); - - const { loading, indexPatterns } = useAppIndexPatternContext(); - - const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>( - {} - ); - - useEffect(() => { - const newExpandRows: ExpandedRowMap = {}; - - setEditorItems((prevState) => { - const newEditorItems = getSeriesToEdit({ - reportType, - allSeries, - indexPatterns, - }); - - newEditorItems.forEach(({ series, id, seriesConfig }) => { - const prevSeriesItem = prevState.find(({ id: prevId }) => prevId === id); - if ( - prevSeriesItem && - series.selectedMetricField && - prevSeriesItem.series.selectedMetricField !== series.selectedMetricField - ) { - newExpandRows[id] = ( - - ); - } - }); - return [...newEditorItems]; - }); - - setItemIdToExpandedRowMap((prevState) => { - return { ...prevState, ...newExpandRows }; - }); - }, [allSeries, getSeries, indexPatterns, loading, reportType]); - - useEffect(() => { - setItemIdToExpandedRowMap((prevState) => { - const itemIdToExpandedRowMapValues = { ...prevState }; - - const newEditorItems = getSeriesToEdit({ - reportType, - allSeries, - indexPatterns, - }); - - newEditorItems.forEach((item) => { - if (itemIdToExpandedRowMapValues[item.id]) { - itemIdToExpandedRowMapValues[item.id] = ( - - ); - } - }); - return itemIdToExpandedRowMapValues; - }); - }, [allSeries, editorItems, indexPatterns, reportType]); - - const toggleDetails = (item: BuilderItem) => { - const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; - if (itemIdToExpandedRowMapValues[item.id]) { - delete itemIdToExpandedRowMapValues[item.id]; - } else { - itemIdToExpandedRowMapValues[item.id] = ( - - ); - } - setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); - }; +export function SeriesEditor() { + const { allSeries, allSeriesIds } = useSeriesStorage(); const columns = [ - { - align: 'left' as const, - width: '40px', - isExpander: true, - field: 'id', - name: '', - render: (id: number, item: BuilderItem) => - item.series.dataType && item.series.selectedMetricField ? ( - toggleDetails(item)} - isDisabled={!item.series.dataType || !item.series.selectedMetricField} - aria-label={itemIdToExpandedRowMap[item.id] ? COLLAPSE_LABEL : EXPAND_LABEL} - iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'} - /> - ) : null, - }, - { - name: '', - field: 'id', - width: '40px', - render: (seriesId: number, { seriesConfig, series }: BuilderItem) => ( - - ), - }, { name: i18n.translate('xpack.observability.expView.seriesEditor.name', { defaultMessage: 'Name', }), field: 'id', - width: '20%', - render: (seriesId: number, { series }: BuilderItem) => ( - - ), - }, - { - name: i18n.translate('xpack.observability.expView.seriesEditor.dataType', { - defaultMessage: 'Data type', - }), - field: 'id', width: '15%', - render: (seriesId: number, { series }: BuilderItem) => ( - + render: (seriesId: string) => ( + + {' '} + {seriesId === NEW_SERIES_KEY ? 'series-preview' : seriesId} + ), }, { - name: i18n.translate('xpack.observability.expView.seriesEditor.reportMetric', { - defaultMessage: 'Report metric', + name: i18n.translate('xpack.observability.expView.seriesEditor.filters', { + defaultMessage: 'Filters', }), - field: 'id', + field: 'defaultFilters', width: '15%', - render: (seriesId: number, { seriesConfig, series }: BuilderItem) => ( - ( + ), }, { - name: i18n.translate('xpack.observability.expView.seriesEditor.time', { - defaultMessage: 'Time', + name: i18n.translate('xpack.observability.expView.seriesEditor.breakdowns', { + defaultMessage: 'Breakdowns', }), field: 'id', - width: '27%', - render: (seriesId: number, { series }: BuilderItem) => ( - + width: '25%', + render: (seriesId: string, { seriesConfig, id }: EditItem) => ( + ), }, - { - name: i18n.translate('xpack.observability.expView.seriesBuilder.breakdownBy', { - defaultMessage: 'Breakdown by', - }), - width: '10%', - field: 'id', - render: (seriesId: number, { series, seriesConfig }: BuilderItem) => ( - + name: ( +
+ +
), + width: '20%', + field: 'id', + align: 'right' as const, + render: (seriesId: string, item: EditItem) => , }, - { - name: i18n.translate('xpack.observability.expView.seriesBuilder.actions', { + name: i18n.translate('xpack.observability.expView.seriesEditor.actions', { defaultMessage: 'Actions', }), align: 'center' as const, - width: '8%', + width: '10%', field: 'id', - render: (seriesId: number, { series, seriesConfig }: BuilderItem) => ( - - ), + render: (seriesId: string, item: EditItem) => , }, ]; - const getRowProps = (item: BuilderItem) => { - const { dataType, reportDefinitions, selectedMetricField } = item.series; - - return { - className: classNames({ - isExpanded: itemIdToExpandedRowMap[item.id], - isIncomplete: !dataType || isEmpty(reportDefinitions) || !selectedMetricField, - }), - // commenting this for now, since adding on click on row, blocks adding space - // into text field for name column - // ...(dataType && selectedMetricField - // ? { - // onClick: (evt: MouseEvent) => { - // const targetElem = evt.target as HTMLElement; - // - // if ( - // targetElem.classList.contains('euiTableCellContent') && - // targetElem.tagName !== 'BUTTON' - // ) { - // toggleDetails(item); - // } - // evt.stopPropagation(); - // evt.preventDefault(); - // }, - // } - // : {}), - }; - }; - - const resetView = () => { - const totalSeries = allSeries.length; - for (let i = totalSeries; i >= 0; i--) { - removeSeries(i); - } - setEditorItems([]); - setItemIdToExpandedRowMap({}); - }; - - return ( - -
- - - - - - - - {reportType && ( - - resetView()} color="text"> - {RESET_LABEL} - - - )} - - - - - - - - {editorItems.length > 0 && ( - - )} - -
-
- ); -}); - -const Wrapper = euiStyled.div` - max-height: 50vh; - &::-webkit-scrollbar { - height: ${({ theme }) => theme.eui.euiScrollBar}; - width: ${({ theme }) => theme.eui.euiScrollBar}; - } - &::-webkit-scrollbar-thumb { - background-clip: content-box; - background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)}; - border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent; - } - &::-webkit-scrollbar-corner, - &::-webkit-scrollbar-track { - background-color: transparent; - } - - &&& { - .euiTableRow-isExpandedRow .euiTableRowCell { - border-top: none; - background-color: #FFFFFF; - border-bottom: 2px solid #d3dae6; - border-right: 2px solid rgb(211, 218, 230); - border-left: 2px solid rgb(211, 218, 230); - } - - .isExpanded { - border-right: 2px solid rgb(211, 218, 230); - border-left: 2px solid rgb(211, 218, 230); - .euiTableRowCell { - border-bottom: none; - } - } - .isIncomplete .euiTableRowCell { - background-color: rgba(254, 197, 20, 0.1); + const { indexPatterns } = useAppIndexPatternContext(); + const items: EditItem[] = []; + + allSeriesIds.forEach((seriesKey) => { + const series = allSeries[seriesKey]; + if (series?.reportType && indexPatterns[series.dataType] && !series.isNew) { + items.push({ + id: seriesKey, + seriesConfig: getDefaultConfigs({ + indexPattern: indexPatterns[series.dataType], + reportType: series.reportType, + dataType: series.dataType, + }), + }); } - } -`; - -export const LOADING_VIEW = i18n.translate( - 'xpack.observability.expView.seriesBuilder.loadingView', - { - defaultMessage: 'Loading view ...', - } -); - -export const SELECT_REPORT_TYPE = i18n.translate( - 'xpack.observability.expView.seriesBuilder.selectReportType', - { - defaultMessage: 'No report type selected', - } -); - -export const RESET_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.reset', { - defaultMessage: 'Reset', -}); + }); -export const REPORT_TYPE_LABEL = i18n.translate( - 'xpack.observability.expView.seriesBuilder.reportType', - { - defaultMessage: 'Report type', + if (items.length === 0 && allSeriesIds.length > 0) { + return null; } -); - -const COLLAPSE_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.collapse', { - defaultMessage: 'Collapse', -}); -const EXPAND_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.expand', { - defaultMessage: 'Exapnd', -}); + return ( + <> + + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/chart_types.tsx deleted file mode 100644 index e6ba505c82091..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/chart_types.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState } from 'react'; -import { EuiPopover, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { - useKibana, - ToolbarButton, -} from '../../../../../../../../../src/plugins/kibana_react/public'; -import { ObservabilityPublicPluginsStart } from '../../../../../plugin'; -import { SeriesUrl, useFetcher } from '../../../../..'; -import { SeriesConfig } from '../../types'; -import { SeriesChartTypesSelect } from '../../series_editor/columns/chart_types'; - -interface Props { - seriesId: number; - series: SeriesUrl; - seriesConfig: SeriesConfig; -} - -export function SeriesChartTypes({ seriesId, series, seriesConfig }: Props) { - const seriesType = series?.seriesType ?? seriesConfig.defaultSeriesType; - - const { - services: { lens }, - } = useKibana(); - - const { data = [] } = useFetcher(() => lens.getXyVisTypes(), [lens]); - - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - - return ( - setIsPopoverOpen(false)} - button={ - - id === seriesType)?.icon!} - aria-label={CHART_TYPE_LABEL} - onClick={() => setIsPopoverOpen((prevState) => !prevState)} - /> - - } - > - - - ); -} - -const EDIT_CHART_TYPE_LABEL = i18n.translate( - 'xpack.observability.expView.seriesEditor.editChartSeriesLabel', - { - defaultMessage: 'Edit chart type for series', - } -); - -const CHART_TYPE_LABEL = i18n.translate('xpack.observability.expView.chartTypes.label', { - defaultMessage: 'Chart type', -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/remove_series.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/remove_series.tsx deleted file mode 100644 index 2d38b81e12c9f..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/remove_series.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; - -interface Props { - seriesId: number; -} - -export function RemoveSeries({ seriesId }: Props) { - const { removeSeries, allSeries } = useSeriesStorage(); - - const onClick = () => { - removeSeries(seriesId); - }; - - const isDisabled = seriesId === 0 && allSeries.length > 1; - - return ( - - - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_actions.tsx deleted file mode 100644 index 72ae111f002b1..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_actions.tsx +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { RemoveSeries } from './remove_series'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { SeriesConfig, SeriesUrl } from '../../types'; -import { useDiscoverLink } from '../../hooks/use_discover_link'; - -interface Props { - seriesId: number; - series: SeriesUrl; - seriesConfig: SeriesConfig; -} -export function SeriesActions({ seriesId, series, seriesConfig }: Props) { - const { setSeries, allSeries } = useSeriesStorage(); - - const { href: discoverHref } = useDiscoverLink({ series, seriesConfig }); - - const copySeries = () => { - let copySeriesId: string = `${series.name}-copy`; - if (allSeries.find(({ name }) => name === copySeriesId)) { - copySeriesId = copySeriesId + allSeries.length; - } - setSeries(allSeries.length, { ...series, name: copySeriesId }); - }; - - const toggleSeries = () => { - if (series.hidden) { - setSeries(seriesId, { ...series, hidden: undefined }); - } else { - setSeries(seriesId, { ...series, hidden: true }); - } - }; - - return ( - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_filter.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_filter.tsx deleted file mode 100644 index 87c17d03282c3..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_filter.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiFilterGroup, EuiSpacer } from '@elastic/eui'; -import { useRouteMatch } from 'react-router-dom'; -import { FilterExpanded } from './filter_expanded'; -import { SeriesConfig, SeriesUrl } from '../../types'; -import { FieldLabels } from '../../configurations/constants/constants'; -import { SelectedFilters } from '../selected_filters'; - -interface Props { - seriesId: number; - seriesConfig: SeriesConfig; - series: SeriesUrl; -} - -export interface Field { - label: string; - field: string; - nested?: string; - isNegated?: boolean; -} - -export function SeriesFilter({ series, seriesConfig, seriesId }: Props) { - const isPreview = !!useRouteMatch('/exploratory-view/preview'); - - const options: Field[] = seriesConfig.filterFields.map((field) => { - if (typeof field === 'string') { - return { label: seriesConfig.labels?.[field] ?? FieldLabels[field], field }; - } - - return { - field: field.field, - nested: field.nested, - isNegated: field.isNegated, - label: seriesConfig.labels?.[field.field] ?? FieldLabels[field.field], - }; - }); - - return ( - <> - {!isPreview && ( - <> - - {options.map((opt) => ( - - ))} - - - - )} - - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_info.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_info.tsx deleted file mode 100644 index 3506acbeb528d..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_info.tsx +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { isEmpty } from 'lodash'; -import { EuiBadge, EuiBadgeGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useRouteMatch } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import { SeriesChartTypes } from './chart_types'; -import { SeriesConfig, SeriesUrl } from '../../types'; -import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; -import { SeriesColorPicker } from '../../components/series_color_picker'; -import { dataTypes } from '../../series_editor/columns/data_type_select'; - -interface Props { - seriesId: number; - series: SeriesUrl; - seriesConfig?: SeriesConfig; -} - -export function SeriesInfo({ seriesId, series, seriesConfig }: Props) { - const isConfigure = !!useRouteMatch('/exploratory-view/configure'); - - const { dataType, reportDefinitions, selectedMetricField } = series; - - const { loading } = useAppIndexPatternContext(); - - const isIncomplete = - (!dataType || isEmpty(reportDefinitions) || !selectedMetricField) && !loading; - - if (!seriesConfig) { - return null; - } - - const { definitionFields, labels } = seriesConfig; - - const incompleteDefinition = isEmpty(reportDefinitions) - ? i18n.translate('xpack.observability.overview.exploratoryView.missingReportDefinition', { - defaultMessage: 'Missing {reportDefinition}', - values: { reportDefinition: labels?.[definitionFields[0]] }, - }) - : ''; - - let incompleteMessage = !selectedMetricField ? MISSING_REPORT_METRIC_LABEL : incompleteDefinition; - - if (!dataType) { - incompleteMessage = MISSING_DATA_TYPE_LABEL; - } - - if (!isIncomplete && seriesConfig && isConfigure) { - return ( - - - - - - - - - ); - } - - return ( - - - {isIncomplete && {incompleteMessage}} - - {!isConfigure && ( - - - {dataTypes.find(({ id }) => id === dataType)!.label} - - - )} - - ); -} - -const MISSING_REPORT_METRIC_LABEL = i18n.translate( - 'xpack.observability.overview.exploratoryView.missingReportMetric', - { - defaultMessage: 'Missing report metric', - } -); - -const MISSING_DATA_TYPE_LABEL = i18n.translate( - 'xpack.observability.overview.exploratoryView.missingDataType', - { - defaultMessage: 'Missing data type', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_name.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_name.tsx deleted file mode 100644 index e35966a9fb0d2..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_name.tsx +++ /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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState, ChangeEvent, useEffect } from 'react'; -import { EuiFieldText } from '@elastic/eui'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { SeriesUrl } from '../../types'; - -interface Props { - seriesId: number; - series: SeriesUrl; -} - -export function SeriesName({ series, seriesId }: Props) { - const { setSeries } = useSeriesStorage(); - - const [value, setValue] = useState(series.name); - - const onChange = (e: ChangeEvent) => { - setValue(e.target.value); - }; - - const onSave = () => { - if (value !== series.name) { - setSeries(seriesId, { ...series, name: value }); - } - }; - - useEffect(() => { - setValue(series.name); - }, [series.name]); - - return ; -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/utils.ts deleted file mode 100644 index b9ee53a7e8e2d..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/utils.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import moment from 'moment'; -import dateMath from '@elastic/datemath'; -import _isString from 'lodash/isString'; - -const LAST = 'Last'; -const NEXT = 'Next'; - -const isNow = (value: string) => value === 'now'; - -export const isString = (value: any): value is string => _isString(value); -export interface QuickSelect { - timeTense: string; - timeValue: number; - timeUnits: TimeUnitId; -} -export type TimeUnitFromNowId = 's+' | 'm+' | 'h+' | 'd+' | 'w+' | 'M+' | 'y+'; -export type TimeUnitId = 's' | 'm' | 'h' | 'd' | 'w' | 'M' | 'y'; - -export interface RelativeOption { - text: string; - value: TimeUnitId | TimeUnitFromNowId; -} - -export const relativeOptions: RelativeOption[] = [ - { text: 'Seconds ago', value: 's' }, - { text: 'Minutes ago', value: 'm' }, - { text: 'Hours ago', value: 'h' }, - { text: 'Days ago', value: 'd' }, - { text: 'Weeks ago', value: 'w' }, - { text: 'Months ago', value: 'M' }, - { text: 'Years ago', value: 'y' }, - - { text: 'Seconds from now', value: 's+' }, - { text: 'Minutes from now', value: 'm+' }, - { text: 'Hours from now', value: 'h+' }, - { text: 'Days from now', value: 'd+' }, - { text: 'Weeks from now', value: 'w+' }, - { text: 'Months from now', value: 'M+' }, - { text: 'Years from now', value: 'y+' }, -]; - -const timeUnitIds = relativeOptions - .map(({ value }) => value) - .filter((value) => !value.includes('+')) as TimeUnitId[]; - -export const relativeUnitsFromLargestToSmallest = timeUnitIds.reverse(); - -/** - * This function returns time value, time unit and time tense for a given time string. - * - * For example: for `now-40m` it will parse output as time value to `40` time unit to `m` and time unit to `last`. - * - * If given a datetime string it will return a default value. - * - * If the given string is in the format such as `now/d` it will parse the string to moment object and find the time value, time unit and time tense using moment - * - * This function accepts two strings start and end time. I the start value is now then it uses the end value to parse. - */ -export function parseTimeParts(start: string, end: string): QuickSelect | null { - const value = isNow(start) ? end : start; - - const matches = isString(value) && value.match(/now(([-+])(\d+)([smhdwMy])(\/[smhdwMy])?)?/); - - if (!matches) { - return null; - } - - const operator = matches[2]; - const matchedTimeValue = matches[3]; - const timeUnits = matches[4] as TimeUnitId; - - if (matchedTimeValue && timeUnits && operator) { - return { - timeTense: operator === '+' ? NEXT : LAST, - timeUnits, - timeValue: parseInt(matchedTimeValue, 10), - }; - } - - const duration = moment.duration(moment().diff(dateMath.parse(value))); - let unitOp = ''; - for (let i = 0; i < relativeUnitsFromLargestToSmallest.length; i++) { - const as = duration.as(relativeUnitsFromLargestToSmallest[i]); - if (as < 0) { - unitOp = '+'; - } - if (Math.abs(as) > 1) { - return { - timeValue: Math.round(Math.abs(as)), - timeUnits: relativeUnitsFromLargestToSmallest[i], - timeTense: unitOp === '+' ? NEXT : LAST, - }; - } - } - - return null; -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.tsx deleted file mode 100644 index 46adba1dbde55..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.tsx +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { Fragment } from 'react'; -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useRouteMatch } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import { FilterLabel } from '../components/filter_label'; -import { SeriesConfig, SeriesUrl, UrlFilter } from '../types'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; -import { useSeriesFilters } from '../hooks/use_series_filters'; -import { getFiltersFromDefs } from '../hooks/use_lens_attributes'; -import { useSeriesStorage } from '../hooks/use_series_storage'; - -interface Props { - seriesId: number; - series: SeriesUrl; - seriesConfig: SeriesConfig; -} -export function SelectedFilters({ seriesId, series, seriesConfig }: Props) { - const { setSeries } = useSeriesStorage(); - - const isPreview = !!useRouteMatch('/exploratory-view/preview'); - - const { reportDefinitions = {} } = series; - - const { labels } = seriesConfig; - - const filters: UrlFilter[] = series.filters ?? []; - - let definitionFilters: UrlFilter[] = getFiltersFromDefs(reportDefinitions); - - const isConfigure = !!useRouteMatch('/exploratory-view/configure'); - - // we don't want to display report definition filters in new series view - if (isConfigure) { - definitionFilters = []; - } - - const { removeFilter } = useSeriesFilters({ seriesId, series }); - - const { indexPattern } = useAppIndexPatternContext(series.dataType); - - if ((filters.length === 0 && definitionFilters.length === 0) || !indexPattern) { - return null; - } - - return ( - - {filters.map(({ field, values, notValues }) => ( - - {(values ?? []).length > 0 && ( - - { - values?.forEach((val) => { - removeFilter({ field, value: val, negate: false }); - }); - }} - negate={false} - indexPattern={indexPattern} - /> - - )} - {(notValues ?? []).length > 0 && ( - - { - values?.forEach((val) => { - removeFilter({ field, value: val, negate: false }); - }); - }} - indexPattern={indexPattern} - /> - - )} - - ))} - - {definitionFilters.map(({ field, values }) => - values ? ( - - {}} - negate={false} - definitionFilter={true} - indexPattern={indexPattern} - /> - - ) : null - )} - - {(series.filters ?? []).length > 0 && !isPreview && ( - - { - setSeries(seriesId, { ...series, filters: undefined }); - }} - size="s" - > - {i18n.translate('xpack.observability.expView.seriesEditor.clearFilter', { - defaultMessage: 'Clear filters', - })} - - - )} - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/series_viewer.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/series_viewer.tsx deleted file mode 100644 index 85d65dcac6ac3..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/series_viewer.tsx +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { isEmpty } from 'lodash'; -import { EuiBasicTable, EuiSpacer, EuiText } from '@elastic/eui'; -import { SeriesFilter } from './columns/series_filter'; -import { SeriesConfig, SeriesUrl } from '../types'; -import { useSeriesStorage } from '../hooks/use_series_storage'; -import { getDefaultConfigs } from '../configurations/default_configs'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; -import { SeriesInfo } from './columns/series_info'; -import { SeriesDatePicker } from '../components/series_date_picker'; -import { NO_BREAK_DOWN_LABEL } from './columns/breakdowns'; - -interface EditItem { - id: number; - series: SeriesUrl; - seriesConfig: SeriesConfig; -} - -export function SeriesViewer() { - const { allSeries, reportType } = useSeriesStorage(); - - const columns = [ - { - name: '', - field: 'id', - width: '10%', - render: (seriesId: number, { seriesConfig, series }: EditItem) => ( - - ), - }, - { - name: i18n.translate('xpack.observability.expView.seriesEditor.name', { - defaultMessage: 'Name', - }), - field: 'id', - width: '15%', - render: (seriesId: number, { series }: EditItem) => {series.name}, - }, - { - name: i18n.translate('xpack.observability.expView.seriesEditor.filters', { - defaultMessage: 'Filters', - }), - field: 'id', - width: '35%', - render: (seriesId: number, { series, seriesConfig }: EditItem) => ( - - ), - }, - { - name: i18n.translate('xpack.observability.expView.seriesEditor.breakdownBy', { - defaultMessage: 'Breakdown by', - }), - field: 'seriesId', - width: '10%', - render: (seriesId: number, { seriesConfig: { labels }, series }: EditItem) => ( - {series.breakdown ? labels[series.breakdown] : NO_BREAK_DOWN_LABEL} - ), - }, - { - name: i18n.translate('xpack.observability.expView.seriesEditor.time', { - defaultMessage: 'Time', - }), - width: '30%', - field: 'id', - render: (seriesId: number, { series }: EditItem) => ( - - ), - }, - ]; - - const { indexPatterns } = useAppIndexPatternContext(); - const items: EditItem[] = []; - - allSeries.forEach((series, seriesIndex) => { - if (indexPatterns[series.dataType] && !isEmpty(series.reportDefinitions)) { - items.push({ - series, - id: seriesIndex, - seriesConfig: getDefaultConfigs({ - reportType, - dataType: series.dataType, - indexPattern: indexPatterns[series.dataType], - }), - }); - } - }); - - if (items.length === 0 && allSeries.length > 0) { - return null; - } - - return ( - <> - - - - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts index 4bba0c221f3c5..fbda2f4ff62e2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -6,7 +6,7 @@ */ import { PaletteOutput } from 'src/plugins/charts/public'; -import { ExistsFilter, PhraseFilter } from '@kbn/es-query'; +import { ExistsFilter } from '@kbn/es-query'; import { LastValueIndexPatternColumn, DateHistogramIndexPatternColumn, @@ -42,7 +42,7 @@ export interface MetricOption { field?: string; label: string; description?: string; - columnType?: 'range' | 'operation' | 'FILTER_RECORDS' | 'TERMS_COLUMN' | 'unique_count'; + columnType?: 'range' | 'operation' | 'FILTER_RECORDS' | 'TERMS_COLUMN'; columnFilters?: ColumnFilter[]; timeScale?: string; } @@ -55,7 +55,7 @@ export interface SeriesConfig { defaultSeriesType: SeriesType; filterFields: Array; seriesTypes: SeriesType[]; - baseFilters?: Array; + baseFilters?: PersistableFilter[] | ExistsFilter[]; definitionFields: string[]; metricOptions?: MetricOption[]; labels: Record; @@ -69,7 +69,6 @@ export interface SeriesConfig { export type URLReportDefinition = Record; export interface SeriesUrl { - name: string; time: { to: string; from: string; @@ -77,12 +76,12 @@ export interface SeriesUrl { breakdown?: string; filters?: UrlFilter[]; seriesType?: SeriesType; + reportType: ReportViewType; operationType?: OperationType; dataType: AppDataType; reportDefinitions?: URLReportDefinition; selectedMetricField?: string; - hidden?: boolean; - color?: string; + isNew?: boolean; } export interface UrlFilter { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx deleted file mode 100644 index e0b46102caba0..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { RefObject, useEffect, useState } from 'react'; - -import { EuiTabs, EuiTab, EuiButtonIcon } from '@elastic/eui'; -import { useHistory, useParams } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import { SeriesEditor } from '../series_editor/series_editor'; -import { SeriesViewer } from '../series_viewer/series_viewer'; -import { PanelId } from '../exploratory_view'; - -const tabs = [ - { - id: 'preview' as const, - name: i18n.translate('xpack.observability.overview.exploratoryView.preview', { - defaultMessage: 'Preview', - }), - }, - { - id: 'configure' as const, - name: i18n.translate('xpack.observability.overview.exploratoryView.configureSeries', { - defaultMessage: 'Configure series', - }), - }, -]; - -type ViewTab = 'preview' | 'configure'; - -export function SeriesViews({ - seriesBuilderRef, - onSeriesPanelCollapse, -}: { - seriesBuilderRef: RefObject; - onSeriesPanelCollapse: (panel: PanelId) => void; -}) { - const params = useParams<{ mode: ViewTab }>(); - - const history = useHistory(); - - const [selectedTabId, setSelectedTabId] = useState('configure'); - - const onSelectedTabChanged = (id: ViewTab) => { - setSelectedTabId(id); - history.push('/exploratory-view/' + id); - }; - - useEffect(() => { - setSelectedTabId(params.mode); - }, [params.mode]); - - const renderTabs = () => { - return tabs.map((tab, index) => ( - onSelectedTabChanged(tab.id)} - isSelected={tab.id === selectedTabId} - key={index} - > - {tab.id === 'preview' && selectedTabId === 'preview' ? ( - - onSeriesPanelCollapse('seriesPanel')} - /> -  {tab.name} - - ) : ( - tab.name - )} - - )); - }; - - return ( -
- {renderTabs()} - {selectedTabId === 'preview' && } - {selectedTabId === 'configure' && } -
- ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx deleted file mode 100644 index db1f23ad9b6e3..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect, useState } from 'react'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSwitch, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { isEqual } from 'lodash'; -import { - allSeriesKey, - convertAllShortSeries, - NEW_SERIES_KEY, - useSeriesStorage, -} from '../hooks/use_series_storage'; -import { SeriesUrl } from '../types'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; -import { BuilderItem, getSeriesToEdit } from '../series_editor/series_editor'; -import { DEFAULT_TIME, ReportTypes } from '../configurations/constants'; - -export function ViewActions() { - const [editorItems, setEditorItems] = useState([]); - const { - getSeries, - allSeries, - setSeries, - storage, - reportType, - autoApply, - setAutoApply, - applyChanges, - } = useSeriesStorage(); - - const { loading, indexPatterns } = useAppIndexPatternContext(); - - useEffect(() => { - setEditorItems(getSeriesToEdit({ allSeries, indexPatterns, reportType })); - }, [allSeries, getSeries, indexPatterns, loading, reportType]); - - const addSeries = () => { - const prevSeries = allSeries?.[0]; - const name = `${NEW_SERIES_KEY}-${editorItems.length + 1}`; - const nextSeries = { name } as SeriesUrl; - - const nextSeriesId = allSeries.length; - - if (reportType === 'data-distribution') { - setSeries(nextSeriesId, { - ...nextSeries, - time: prevSeries?.time || DEFAULT_TIME, - } as SeriesUrl); - } else { - setSeries( - nextSeriesId, - prevSeries ? nextSeries : ({ ...nextSeries, time: DEFAULT_TIME } as SeriesUrl) - ); - } - }; - - const noChanges = isEqual(allSeries, convertAllShortSeries(storage.get(allSeriesKey) ?? [])); - - const isAddDisabled = - !reportType || - ((reportType === ReportTypes.CORE_WEB_VITAL || - reportType === ReportTypes.DEVICE_DISTRIBUTION) && - allSeries.length > 0); - - return ( - - - setAutoApply(!autoApply)} - compressed - /> - - {!autoApply && ( - - applyChanges()} isDisabled={autoApply || noChanges} fill> - {i18n.translate('xpack.observability.expView.seriesBuilder.apply', { - defaultMessage: 'Apply changes', - })} - - - )} - - - addSeries()} isDisabled={isAddDisabled}> - {i18n.translate('xpack.observability.expView.seriesBuilder.addSeries', { - defaultMessage: 'Add series', - })} - - - - - ); -} - -const AUTO_APPLY_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.autoApply', { - defaultMessage: 'Auto apply', -}); diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx index 0735df53888aa..fc562fa80e26d 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx @@ -6,24 +6,15 @@ */ import React, { useEffect, useState } from 'react'; -import { union, isEmpty } from 'lodash'; -import { - EuiComboBox, - EuiFormControlLayout, - EuiComboBoxOptionOption, - EuiFormRow, -} from '@elastic/eui'; +import { union } from 'lodash'; +import { EuiComboBox, EuiFormControlLayout, EuiComboBoxOptionOption } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import { FieldValueSelectionProps } from './types'; export const ALL_VALUES_SELECTED = 'ALL_VALUES'; const formatOptions = (values?: string[], allowAllValuesSelection?: boolean) => { const uniqueValues = Array.from( - new Set( - allowAllValuesSelection && (values ?? []).length > 0 - ? ['ALL_VALUES', ...(values ?? [])] - : values - ) + new Set(allowAllValuesSelection ? ['ALL_VALUES', ...(values ?? [])] : values) ); return (uniqueValues ?? []).map((label) => ({ @@ -39,9 +30,7 @@ export function FieldValueCombobox({ loading, values, setQuery, - usePrependLabel = true, compressed = true, - required = true, allowAllValuesSelection, onChange: onSelectionChange, }: FieldValueSelectionProps) { @@ -65,35 +54,29 @@ export function FieldValueCombobox({ onSelectionChange(selectedValuesN.map(({ label: lbl }) => lbl)); }; - const comboBox = ( - { - setQuery(searchVal); - }} - options={options} - selectedOptions={options.filter((opt) => selectedValue?.includes(opt.label))} - onChange={onChange} - isInvalid={required && isEmpty(selectedValue)} - /> - ); - - return usePrependLabel ? ( + return ( - {comboBox} + { + setQuery(searchVal); + }} + options={options} + selectedOptions={options.filter((opt) => selectedValue?.includes(opt.label))} + onChange={onChange} + /> - ) : ( - - {comboBox} - ); } diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx index cee3ab8aea28b..f713af9768229 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx @@ -70,7 +70,6 @@ export function FieldValueSelection({ values = [], selectedValue, excludedValue, - allowExclusions = true, compressed = true, onChange: onSelectionChange, }: FieldValueSelectionProps) { @@ -174,8 +173,8 @@ export function FieldValueSelection({ }} options={options} onChange={onChange} - allowExclusions={allowExclusions} isLoading={loading && !query && options.length === 0} + allowExclusions={true} > {(list, search) => (
diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx index 6671c43dd8c7b..556a8e7052347 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx @@ -95,7 +95,6 @@ describe('FieldValueSuggestions', () => { selectedValue={[]} filters={[]} asCombobox={false} - allowExclusions={true} /> ); @@ -120,7 +119,6 @@ describe('FieldValueSuggestions', () => { excludedValue={['Pak']} filters={[]} asCombobox={false} - allowExclusions={true} /> ); diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx index 65e1d0932e4ed..54114c7604644 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx @@ -28,10 +28,7 @@ export function FieldValueSuggestions({ singleSelection, compressed, asFilterButton, - usePrependLabel, allowAllValuesSelection, - required, - allowExclusions = true, asCombobox = true, onChange: onSelectionChange, }: FieldValueSuggestionsProps) { @@ -67,10 +64,7 @@ export function FieldValueSuggestions({ width={width} compressed={compressed} asFilterButton={asFilterButton} - usePrependLabel={usePrependLabel} - allowExclusions={allowExclusions} allowAllValuesSelection={allowAllValuesSelection} - required={required} /> ); } diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts index 73b3d78ce8700..d857b39b074ac 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts @@ -23,10 +23,7 @@ interface CommonProps { compressed?: boolean; asFilterButton?: boolean; showCount?: boolean; - usePrependLabel?: boolean; - allowExclusions?: boolean; allowAllValuesSelection?: boolean; - required?: boolean; } export type FieldValueSuggestionsProps = CommonProps & { diff --git a/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx b/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx index 9e7b96b02206f..01d727071770d 100644 --- a/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx +++ b/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx @@ -18,25 +18,21 @@ export function buildFilterLabel({ negate, }: { label: string; - value: string | string[]; + value: string; negate: boolean; field: string; indexPattern: IndexPattern; }) { const indexField = indexPattern.getFieldByName(field)!; - const filter = - value instanceof Array && value.length > 1 - ? esFilters.buildPhrasesFilter(indexField, value, indexPattern) - : esFilters.buildPhraseFilter(indexField, value as string, indexPattern); + const filter = esFilters.buildPhraseFilter(indexField, value, indexPattern); - filter.meta.type = value instanceof Array && value.length > 1 ? 'phrases' : 'phrase'; - - filter.meta.value = value as string; + filter.meta.value = value; filter.meta.key = label; filter.meta.alias = null; filter.meta.negate = negate; filter.meta.disabled = false; + filter.meta.type = 'phrase'; return filter; } @@ -44,10 +40,10 @@ export function buildFilterLabel({ interface Props { field: string; label: string; - value: string | string[]; + value: string; negate: boolean; - removeFilter: (field: string, value: string | string[], notVal: boolean) => void; - invertFilter: (val: { field: string; value: string | string[]; negate: boolean }) => void; + removeFilter: (field: string, value: string, notVal: boolean) => void; + invertFilter: (val: { field: string; value: string; negate: boolean }) => void; indexPattern: IndexPattern; allowExclusion?: boolean; } diff --git a/x-pack/plugins/observability/public/components/shared/index.tsx b/x-pack/plugins/observability/public/components/shared/index.tsx index afc053604fcdf..9d557a40b7987 100644 --- a/x-pack/plugins/observability/public/components/shared/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/index.tsx @@ -6,7 +6,6 @@ */ import React, { lazy, Suspense } from 'react'; -import { EuiLoadingSpinner } from '@elastic/eui'; import type { CoreVitalProps, HeaderMenuPortalProps } from './types'; import type { FieldValueSuggestionsProps } from './field_value_suggestions/types'; @@ -27,7 +26,7 @@ const HeaderMenuPortalLazy = lazy(() => import('./header_menu_portal')); export function HeaderMenuPortal(props: HeaderMenuPortalProps) { return ( - }> + ); diff --git a/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx b/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx index 198b4092b0ed6..82a0fc39b8519 100644 --- a/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx +++ b/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx @@ -7,7 +7,7 @@ import { useUiSetting } from '../../../../../src/plugins/kibana_react/public'; import { UI_SETTINGS } from '../../../../../src/plugins/data/common'; -import { TimePickerQuickRange } from '../components/shared/exploratory_view/components/series_date_picker'; +import { TimePickerQuickRange } from '../components/shared/exploratory_view/series_date_picker'; export function useQuickTimeRanges() { const timePickerQuickRanges = useUiSetting( diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 334733e363495..71b83b9e05324 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -24,7 +24,6 @@ import type { DataPublicPluginSetup, DataPublicPluginStart, } from '../../../../src/plugins/data/public'; -import type { DiscoverStart } from '../../../../src/plugins/discover/public'; import type { HomePublicPluginSetup, HomePublicPluginStart, @@ -57,7 +56,6 @@ export interface ObservabilityPublicPluginsStart { triggersActionsUi: TriggersAndActionsUIPublicPluginStart; data: DataPublicPluginStart; lens: LensPublicStart; - discover: DiscoverStart; } export type ObservabilityPublicStart = ReturnType; diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx index 09d22496c98ff..f97e3fb996441 100644 --- a/x-pack/plugins/observability/public/routes/index.tsx +++ b/x-pack/plugins/observability/public/routes/index.tsx @@ -7,7 +7,6 @@ import * as t from 'io-ts'; import React from 'react'; -import { Redirect } from 'react-router-dom'; import { alertStatusRt } from '../../common/typings'; import { ExploratoryViewPage } from '../components/shared/exploratory_view'; import { AlertsPage } from '../pages/alerts'; @@ -100,20 +99,7 @@ export const routes = { }), }, }, - '/exploratory-view/': { - handler: () => { - return ; - }, - params: { - query: t.partial({ - rangeFrom: t.string, - rangeTo: t.string, - refreshPaused: jsonRt.pipe(t.boolean), - refreshInterval: jsonRt.pipe(t.number), - }), - }, - }, - '/exploratory-view/:mode': { + '/exploratory-view': { handler: () => { return ; }, @@ -126,4 +112,18 @@ export const routes = { }), }, }, + // enable this to test multi series architecture + // '/exploratory-view/multi': { + // handler: () => { + // return ; + // }, + // params: { + // query: t.partial({ + // rangeFrom: t.string, + // rangeTo: t.string, + // refreshPaused: jsonRt.pipe(t.boolean), + // refreshInterval: jsonRt.pipe(t.number), + // }), + // }, + // }, }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0d4432d8dac56..c8e3526005963 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -18514,10 +18514,20 @@ "xpack.observability.expView.operationType.99thPercentile": "99パーセンタイル", "xpack.observability.expView.operationType.average": "平均", "xpack.observability.expView.operationType.median": "中央", + "xpack.observability.expView.reportType.noDataType": "データ型を選択すると、系列の構築を開始します。", + "xpack.observability.expView.seriesBuilder.breakdown": "内訳", + "xpack.observability.expView.seriesBuilder.dataType": "データ型", + "xpack.observability.expView.seriesBuilder.definition": "定義", + "xpack.observability.expView.seriesBuilder.filters": "フィルター", + "xpack.observability.expView.seriesBuilder.report": "レポート", "xpack.observability.expView.seriesBuilder.selectReportType": "レポートタイプを選択すると、ビジュアライゼーションを定義します。", + "xpack.observability.expView.seriesEditor.actions": "アクション", + "xpack.observability.expView.seriesEditor.addFilter": "フィルターを追加します", + "xpack.observability.expView.seriesEditor.breakdowns": "内訳", "xpack.observability.expView.seriesEditor.clearFilter": "フィルターを消去", "xpack.observability.expView.seriesEditor.filters": "フィルター", "xpack.observability.expView.seriesEditor.name": "名前", + "xpack.observability.expView.seriesEditor.removeSeries": "クリックすると、系列を削除します", "xpack.observability.expView.seriesEditor.time": "時間", "xpack.observability.featureCatalogueDescription": "専用UIで、ログ、メトリック、アプリケーショントレース、システム可用性を連結します。", "xpack.observability.featureCatalogueDescription1": "インフラストラクチャメトリックを監視します。", @@ -18583,6 +18593,7 @@ "xpack.observability.overview.uptime.up": "アップ", "xpack.observability.overview.ux.appLink": "アプリで表示", "xpack.observability.overview.ux.title": "ユーザーエクスペリエンス", + "xpack.observability.reportTypeCol.nodata": "利用可能なデータがありません", "xpack.observability.resources.documentation": "ドキュメント", "xpack.observability.resources.forum": "ディスカッションフォーラム", "xpack.observability.resources.title": "リソース", @@ -18596,6 +18607,7 @@ "xpack.observability.section.apps.uptime.description": "サイトとサービスの可用性をアクティブに監視するアラートを受信し、問題をより迅速に解決して、ユーザーエクスペリエンスを最適化します。", "xpack.observability.section.apps.uptime.title": "アップタイム", "xpack.observability.section.errorPanel": "データの取得時にエラーが発生しました。再試行してください", + "xpack.observability.seriesEditor.edit": "系列を編集", "xpack.observability.transactionRateLabel": "{value} tpm", "xpack.observability.ux.coreVitals.average": "平均", "xpack.observability.ux.coreVitals.averageMessage": " {bad}未満", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index fc8b27aa497cd..cf31ec8d8eceb 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -18930,10 +18930,20 @@ "xpack.observability.expView.operationType.99thPercentile": "第 99 个百分位", "xpack.observability.expView.operationType.average": "平均值", "xpack.observability.expView.operationType.median": "中值", + "xpack.observability.expView.reportType.noDataType": "选择数据类型以开始构建序列。", + "xpack.observability.expView.seriesBuilder.breakdown": "分解", + "xpack.observability.expView.seriesBuilder.dataType": "数据类型", + "xpack.observability.expView.seriesBuilder.definition": "定义", + "xpack.observability.expView.seriesBuilder.filters": "筛选", + "xpack.observability.expView.seriesBuilder.report": "报告", "xpack.observability.expView.seriesBuilder.selectReportType": "选择报告类型以定义可视化。", + "xpack.observability.expView.seriesEditor.actions": "操作", + "xpack.observability.expView.seriesEditor.addFilter": "添加筛选", + "xpack.observability.expView.seriesEditor.breakdowns": "分解", "xpack.observability.expView.seriesEditor.clearFilter": "清除筛选", "xpack.observability.expView.seriesEditor.filters": "筛选", "xpack.observability.expView.seriesEditor.name": "名称", + "xpack.observability.expView.seriesEditor.removeSeries": "单击移除序列", "xpack.observability.expView.seriesEditor.time": "时间", "xpack.observability.featureCatalogueDescription": "通过专用 UI 整合您的日志、指标、应用程序跟踪和系统可用性。", "xpack.observability.featureCatalogueDescription1": "监测基础架构指标。", @@ -18999,6 +19009,7 @@ "xpack.observability.overview.uptime.up": "运行", "xpack.observability.overview.ux.appLink": "在应用中查看", "xpack.observability.overview.ux.title": "用户体验", + "xpack.observability.reportTypeCol.nodata": "没有可用数据", "xpack.observability.resources.documentation": "文档", "xpack.observability.resources.forum": "讨论论坛", "xpack.observability.resources.title": "资源", @@ -19012,6 +19023,7 @@ "xpack.observability.section.apps.uptime.description": "主动监测站点和服务的可用性。接收告警并更快地解决问题,从而优化用户体验。", "xpack.observability.section.apps.uptime.title": "运行时间", "xpack.observability.section.errorPanel": "尝试提取数据时发生错误。请重试", + "xpack.observability.seriesEditor.edit": "编辑序列", "xpack.observability.transactionRateLabel": "{value} tpm", "xpack.observability.ux.coreVitals.average": "平均值", "xpack.observability.ux.coreVitals.averageMessage": " 且小于 {bad}", diff --git a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx index aa981071b7ee2..1a53a2c9b64a0 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx @@ -22,7 +22,6 @@ import React, { useContext } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import numeral from '@elastic/numeral'; import moment from 'moment'; -import { useSelector } from 'react-redux'; import { getChartDateLabel } from '../../../lib/helper'; import { ChartWrapper } from './chart_wrapper'; import { UptimeThemeContext } from '../../../contexts'; @@ -33,7 +32,6 @@ import { getDateRangeFromChartElement } from './utils'; import { STATUS_DOWN_LABEL, STATUS_UP_LABEL } from '../translations'; import { createExploratoryViewUrl } from '../../../../../observability/public'; import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context'; -import { monitorStatusSelector } from '../../../state/selectors'; export interface PingHistogramComponentProps { /** @@ -75,8 +73,6 @@ export const PingHistogramComponent: React.FC = ({ const monitorId = useMonitorId(); - const selectedMonitor = useSelector(monitorStatusSelector); - const { basePath } = useUptimeSettingsContext(); const [getUrlParams, updateUrlParams] = useUrlParams(); @@ -193,21 +189,12 @@ export const PingHistogramComponent: React.FC = ({ const pingHistogramExploratoryViewLink = createExploratoryViewUrl( { - reportType: 'kpi-over-time', - allSeries: [ - { - name: `${monitorId}-pings`, - dataType: 'synthetics', - selectedMetricField: 'summary.up', - time: { from: dateRangeStart, to: dateRangeEnd }, - reportDefinitions: { - 'monitor.name': - monitorId && selectedMonitor?.monitor?.name - ? [selectedMonitor.monitor.name] - : ['ALL_VALUES'], - }, - }, - ], + 'pings-over-time': { + dataType: 'synthetics', + reportType: 'kpi-over-time', + time: { from: dateRangeStart, to: dateRangeEnd }, + ...(monitorId ? { filters: [{ field: 'monitor.id', values: [monitorId] }] } : {}), + }, }, basePath ); diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx index c459fe46da975..9f00dd2e8f061 100644 --- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx @@ -10,15 +10,13 @@ import { EuiHeaderLinks, EuiToolTip, EuiHeaderLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useHistory } from 'react-router-dom'; -import { useSelector } from 'react-redux'; -import { createExploratoryViewUrl } from '../../../../../observability/public'; +import { createExploratoryViewUrl, SeriesUrl } from '../../../../../observability/public'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context'; import { useGetUrlParams } from '../../../hooks'; import { ToggleAlertFlyoutButton } from '../../overview/alerts/alerts_containers'; import { SETTINGS_ROUTE } from '../../../../common/constants'; import { stringifyUrlParams } from '../../../lib/helper/stringify_url_params'; -import { monitorStatusSelector } from '../../../state/selectors'; const ADD_DATA_LABEL = i18n.translate('xpack.uptime.addDataButtonLabel', { defaultMessage: 'Add data', @@ -40,28 +38,13 @@ export function ActionMenuContent(): React.ReactElement { const { dateRangeStart, dateRangeEnd } = params; const history = useHistory(); - const selectedMonitor = useSelector(monitorStatusSelector); - - const monitorId = selectedMonitor?.monitor?.id; - const syntheticExploratoryViewLink = createExploratoryViewUrl( { - reportType: 'kpi-over-time', - allSeries: [ - { - dataType: 'synthetics', - seriesType: 'area_stacked', - selectedMetricField: 'monitor.duration.us', - time: { from: dateRangeStart, to: dateRangeEnd }, - breakdown: monitorId ? 'observer.geo.name' : 'monitor.type', - reportDefinitions: { - 'monitor.name': selectedMonitor?.monitor?.name - ? [selectedMonitor?.monitor?.name] - : ['ALL_VALUES'], - }, - name: monitorId ? `${monitorId}-response-duration` : 'All monitors response duration', - }, - ], + 'synthetics-series': ({ + dataType: 'synthetics', + isNew: true, + time: { from: dateRangeStart, to: dateRangeEnd }, + } as unknown) as SeriesUrl, }, basePath ); diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx index 18aba948eaa37..1590e225f9ca8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx @@ -55,19 +55,16 @@ export const MonitorDuration: React.FC = ({ monitorId }) => { const exploratoryViewLink = createExploratoryViewUrl( { - reportType: 'kpi-over-time', - allSeries: [ - { - name: `${monitorId}-response-duration`, - time: { from: dateRangeStart, to: dateRangeEnd }, - reportDefinitions: { - 'monitor.id': [monitorId] as string[], - }, - breakdown: 'observer.geo.name', - operationType: 'average', - dataType: 'synthetics', + [`monitor-duration`]: { + reportType: 'kpi-over-time', + time: { from: dateRangeStart, to: dateRangeEnd }, + reportDefinitions: { + 'monitor.id': [monitorId] as string[], }, - ], + breakdown: 'observer.geo.name', + operationType: 'average', + dataType: 'synthetics', + }, }, basePath ); diff --git a/x-pack/test/functional/apps/observability/exploratory_view.ts b/x-pack/test/functional/apps/observability/exploratory_view.ts deleted file mode 100644 index 8f27f20ce30e6..0000000000000 --- a/x-pack/test/functional/apps/observability/exploratory_view.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import Path from 'path'; -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['observability', 'common', 'header']); - const esArchiver = getService('esArchiver'); - const find = getService('find'); - - const testSubjects = getService('testSubjects'); - - const rangeFrom = '2021-01-17T16%3A46%3A15.338Z'; - const rangeTo = '2021-01-19T17%3A01%3A32.309Z'; - - // Failing: See https://github.com/elastic/kibana/issues/106934 - describe.skip('ExploratoryView', () => { - before(async () => { - await esArchiver.loadIfNeeded( - Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', '8.0.0') - ); - - await esArchiver.loadIfNeeded( - Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', 'rum_8.0.0') - ); - - await esArchiver.loadIfNeeded( - Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', 'rum_test_data') - ); - - await PageObjects.common.navigateToApp('ux', { - search: `?rangeFrom=${rangeFrom}&rangeTo=${rangeTo}`, - }); - await PageObjects.header.waitUntilLoadingHasFinished(); - }); - - after(async () => { - await esArchiver.unload( - Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', '8.0.0') - ); - - await esArchiver.unload( - Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', 'rum_8.0.0') - ); - }); - - it('should able to open exploratory view from ux app', async () => { - await testSubjects.exists('uxAnalyzeBtn'); - await testSubjects.click('uxAnalyzeBtn'); - expect(await find.existsByCssSelector('.euiBasicTable')).to.eql(true); - }); - - it('renders lens visualization', async () => { - expect(await testSubjects.exists('lnsVisualizationContainer')).to.eql(true); - - expect( - await find.existsByCssSelector('div[data-title="Prefilled from exploratory view app"]') - ).to.eql(true); - - expect((await find.byCssSelector('dd')).getVisibleText()).to.eql(true); - }); - - it('can do a breakdown per series', async () => { - await testSubjects.click('seriesBreakdown'); - - expect(await find.existsByCssSelector('[id="user_agent.name"]')).to.eql(true); - - await find.clickByCssSelector('[id="user_agent.name"]'); - - await PageObjects.header.waitUntilLoadingHasFinished(); - - expect(await find.existsByCssSelector('[title="Chrome Mobile iOS"]')).to.eql(true); - expect(await find.existsByCssSelector('[title="Mobile Safari"]')).to.eql(true); - }); - }); -} diff --git a/x-pack/test/functional/apps/observability/index.ts b/x-pack/test/functional/apps/observability/index.ts index cce07b9ff7d86..b7f03b5f27bae 100644 --- a/x-pack/test/functional/apps/observability/index.ts +++ b/x-pack/test/functional/apps/observability/index.ts @@ -8,9 +8,8 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { - describe('ObservabilityApp', function () { + describe('Observability specs', function () { this.tags('ciGroup6'); loadTestFile(require.resolve('./feature_controls')); - loadTestFile(require.resolve('./exploratory_view')); }); } From 37a97b435e95f8ddf838135aef41773a5b463158 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 10 Aug 2021 09:53:50 -0600 Subject: [PATCH 034/104] [file_upload] include caused_by field for import failures (#107907) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/file_upload/common/types.ts | 4 ++++ x-pack/plugins/file_upload/server/import_data.ts | 1 + 2 files changed, 5 insertions(+) diff --git a/x-pack/plugins/file_upload/common/types.ts b/x-pack/plugins/file_upload/common/types.ts index e10b9e90a71d8..8462f8983a67d 100644 --- a/x-pack/plugins/file_upload/common/types.ts +++ b/x-pack/plugins/file_upload/common/types.ts @@ -111,6 +111,10 @@ export interface ImportResponse { export interface ImportFailure { item: number; reason: string; + caused_by?: { + type: string; + reason: string; + }; doc: ImportDoc; } diff --git a/x-pack/plugins/file_upload/server/import_data.ts b/x-pack/plugins/file_upload/server/import_data.ts index f93d73647ed0e..deb170974ced8 100644 --- a/x-pack/plugins/file_upload/server/import_data.ts +++ b/x-pack/plugins/file_upload/server/import_data.ts @@ -164,6 +164,7 @@ export function importDataProvider({ asCurrentUser }: IScopedClusterClient) { failures.push({ item: i, reason: item.index.error.reason, + caused_by: item.index.error.caused_by, doc: data[i], }); } From aba804c36ff44512c7ac6225d130d2fdb3faa3d0 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Tue, 10 Aug 2021 11:08:43 -0500 Subject: [PATCH 035/104] [DOCS] Removes coming tag from 8.0.0-alpha1 release notes (#106781) --- docs/CHANGELOG.asciidoc | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index b2ce650a531dc..96a7e57ef3e4f 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -18,8 +18,6 @@ This section summarizes the changes in each release. [[release-notes-8.0.0-alpha1]] == {kib} 8.0.0-alpha1 -coming[8.0.0] - The following changes are released for the first time in {kib} 8.0.0-alpha1. Review the changes, then use the <> to complete the upgrade. [float] From 6579c6cb2c345c07e0ba06dffccd1f5b8ccbc949 Mon Sep 17 00:00:00 2001 From: Tre Date: Tue, 10 Aug 2021 17:46:04 +0100 Subject: [PATCH 036/104] [Archive Migration] x-pack..discover/feature_controls/spaces (#107644) --- .../feature_controls/discover_spaces.ts | 25 +- .../feature_controls/spaces/data.json | 77 --- .../feature_controls/spaces/mappings.json | 462 ------------------ .../feature_controls/custom_space.json | 16 + .../discover/feature_controls/spaces.json | 32 ++ 5 files changed, 65 insertions(+), 547 deletions(-) delete mode 100644 x-pack/test/functional/es_archives/discover/feature_controls/spaces/data.json delete mode 100644 x-pack/test/functional/es_archives/discover/feature_controls/spaces/mappings.json create mode 100644 x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/custom_space.json create mode 100644 x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/spaces.json diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts index c05b15905b932..3542abf9ea863 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts @@ -22,6 +22,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ]); const testSubjects = getService('testSubjects'); const appsMenu = getService('appsMenu'); + const kibanaServer = getService('kibanaServer'); async function setDiscoverTimeRange() { await PageObjects.timePicker.setDefaultAbsoluteRange(); @@ -36,8 +37,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { before(async () => { // we need to load the following in every situation as deleting // a space deletes all of the associated saved objects - await esArchiver.load( - 'x-pack/test/functional/es_archives/discover/feature_controls/spaces' + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/spaces' + ); + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/custom_space', + { space: 'custom_space' } ); await spacesService.create({ id: 'custom_space', @@ -48,8 +53,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { after(async () => { await spacesService.delete('custom_space'); - await esArchiver.unload( - 'x-pack/test/functional/es_archives/discover/feature_controls/spaces' + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/custom_space', + { space: 'custom_space' } + ); + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/spaces' ); }); @@ -84,8 +93,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { before(async () => { // we need to load the following in every situation as deleting // a space deletes all of the associated saved objects - await esArchiver.load( - 'x-pack/test/functional/es_archives/discover/feature_controls/spaces' + await kibanaServer.importExport.load( + 'x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/spaces' ); await spacesService.create({ id: 'custom_space', @@ -96,8 +105,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { after(async () => { await spacesService.delete('custom_space'); - await esArchiver.unload( - 'x-pack/test/functional/es_archives/discover/feature_controls/spaces' + await kibanaServer.importExport.unload( + 'x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/spaces' ); }); diff --git a/x-pack/test/functional/es_archives/discover/feature_controls/spaces/data.json b/x-pack/test/functional/es_archives/discover/feature_controls/spaces/data.json deleted file mode 100644 index af5aa6d043484..0000000000000 --- a/x-pack/test/functional/es_archives/discover/feature_controls/spaces/data.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "index-pattern:logstash-*", - "source": { - "index-pattern": { - "title": "logstash-*", - "timeFieldName": "@timestamp", - "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" - }, - "type": "index-pattern", - "migrationVersion": { - "index-pattern": "6.5.0" - }, - "updated_at": "2018-12-21T00:43:07.096Z" - } - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "config:6.0.0", - "source": { - "config": { - "buildNum": 9007199254740991, - "defaultIndex": "logstash-*" - }, - "type": "config", - "updated_at": "2019-01-22T19:32:02.235Z" - } - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "custom_space:index-pattern:logstash-*", - "source": { - "namespace": "custom_space", - "index-pattern": { - "title": "logstash-*", - "timeFieldName": "@timestamp", - "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" - }, - "type": "index-pattern", - "migrationVersion": { - "index-pattern": "6.5.0" - }, - "updated_at": "2018-12-21T00:43:07.096Z" - } - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "type": "doc", - "id": "custom_space:config:6.0.0", - "source": { - "namespace": "custom_space", - "config": { - "buildNum": 9007199254740991, - "defaultIndex": "logstash-*" - }, - "type": "config", - "updated_at": "2019-01-22T19:32:02.235Z" - } - } -} diff --git a/x-pack/test/functional/es_archives/discover/feature_controls/spaces/mappings.json b/x-pack/test/functional/es_archives/discover/feature_controls/spaces/mappings.json deleted file mode 100644 index 0cd1a29f92241..0000000000000 --- a/x-pack/test/functional/es_archives/discover/feature_controls/spaces/mappings.json +++ /dev/null @@ -1,462 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - ".kibana": {} - }, - "index": ".kibana_1", - "settings": { - "index": { - "number_of_shards": "1", - "auto_expand_replicas": "0-1", - "number_of_replicas": "0" - } - }, - "mappings": { - "dynamic": "strict", - "properties": { - "apm-telemetry": { - "properties": { - "has_any_services": { - "type": "boolean" - }, - "services_per_agent": { - "properties": { - "go": { - "type": "long", - "null_value": 0 - }, - "java": { - "type": "long", - "null_value": 0 - }, - "js-base": { - "type": "long", - "null_value": 0 - }, - "nodejs": { - "type": "long", - "null_value": 0 - }, - "python": { - "type": "long", - "null_value": 0 - }, - "ruby": { - "type": "long", - "null_value": 0 - } - } - } - } - }, - "canvas-workpad": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "id": { - "type": "text", - "index": false - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } - } - } - }, - "config": { - "dynamic": "true", - "properties": { - "accessibility:disableAnimations": { - "type": "boolean" - }, - "buildNum": { - "type": "keyword" - }, - "dateFormat:tz": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "defaultIndex": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "telemetry:optIn": { - "type": "boolean" - } - } - }, - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "gis-map" : { - "properties" : { - "bounds": { - "dynamic": false, - "properties": {} - }, - "description" : { - "type" : "text" - }, - "layerListJSON" : { - "type" : "text" - }, - "mapStateJSON" : { - "type" : "text" - }, - "title" : { - "type" : "text" - }, - "uiStateJSON" : { - "type" : "text" - }, - "version" : { - "type" : "integer" - } - } - }, - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "typeMeta": { - "type": "keyword" - } - } - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "index-pattern": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "space": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - } - } - }, - "namespace": { - "type": "keyword" - }, - "search": { - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "initials": { - "type": "keyword" - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } - } - } - }, - "spaceId": { - "type": "keyword" - }, - "telemetry": { - "properties": { - "enabled": { - "type": "boolean" - } - } - }, - "timelion-sheet": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "timelion_chart_height": { - "type": "integer" - }, - "timelion_columns": { - "type": "integer" - }, - "timelion_interval": { - "type": "keyword" - }, - "timelion_other_interval": { - "type": "keyword" - }, - "timelion_rows": { - "type": "integer" - }, - "timelion_sheet": { - "type": "text" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchId": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - } - } -} diff --git a/x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/custom_space.json b/x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/custom_space.json new file mode 100644 index 0000000000000..d6c52e43984e2 --- /dev/null +++ b/x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/custom_space.json @@ -0,0 +1,16 @@ +{ + "attributes": { + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "coreMigrationVersion": "7.15.0", + "id": "logstash-*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2018-12-21T00:43:07.096Z", + "version": "WzYsMl0=" +} diff --git a/x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/spaces.json b/x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/spaces.json new file mode 100644 index 0000000000000..9ddc185cf3535 --- /dev/null +++ b/x-pack/test/functional/fixtures/kbn_archiver/discover/feature_controls/spaces.json @@ -0,0 +1,32 @@ +{ + "attributes": { + "buildNum": 9007199254740991, + "defaultIndex": "logstash-*" + }, + "coreMigrationVersion": "7.15.0", + "id": "7.15.0", + "migrationVersion": { + "config": "7.13.0" + }, + "references": [], + "type": "config", + "updated_at": "2021-08-04T13:18:45.677Z", + "version": "WzksMl0=" +} + +{ + "attributes": { + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "coreMigrationVersion": "7.15.0", + "id": "logstash-*", + "migrationVersion": { + "index-pattern": "7.11.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2018-12-21T00:43:07.096Z", + "version": "WzQsMl0=" +} From 4f7e62fff3b47fc1e6019928176734cdcc73bdb9 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Tue, 10 Aug 2021 10:06:11 -0700 Subject: [PATCH 037/104] [DOCS] Updates file upload details for geospatial data (#107985) --- docs/maps/import-geospatial-data.asciidoc | 32 ++++++++++++----------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/docs/maps/import-geospatial-data.asciidoc b/docs/maps/import-geospatial-data.asciidoc index 0218bac58815a..c7b12c4ac32f6 100644 --- a/docs/maps/import-geospatial-data.asciidoc +++ b/docs/maps/import-geospatial-data.asciidoc @@ -6,6 +6,9 @@ To import geospatical data into the Elastic Stack, the data must be indexed as { Geospatial data comes in many formats. Choose an import tool based on the format of your geospatial data. +TIP: When you upload GeoJSON or delimited files in {kib}, there is a file size +limit, which is configurable in <>. + [discrete] [[import-geospatial-privileges]] === Security privileges @@ -18,37 +21,36 @@ spaces in **{stack-manage-app}** in {kib}. For more information, see To upload GeoJSON files in {kib} with *Maps*, you must have: -* The `all` {kib} privilege for *Maps*. -* The `all` {kib} privilege for *Index Pattern Management*. -* The `create` and `create_index` index privileges for destination indices. -* To use the index in *Maps*, you must also have the `read` and `view_index_metadata` index privileges for destination indices. +* The `all` {kib} privilege for *Maps* +* The `all` {kib} privilege for *{ipm-app}* +* The `create` and `create_index` index privileges for destination indices +* To use the index in *Maps*, you must also have the `read` and `view_index_metadata` index privileges for destination indices -To upload CSV files in {kib} with the *{file-data-viz}*, you must have privileges to upload GeoJSON files and: +To upload delimited files (such as CSV, TSV, or JSON files) on the {kib} home page, you must also have: -* The `manage_pipeline` cluster privilege. -* The `read` {kib} privilege for *Machine Learning*. -* The `machine_learning_admin` or `machine_learning_user` role. +* The `all` {kib} privilege for *Discover* +* The `manage_pipeline` or `manage_ingest_pipelines` cluster privilege +* The `manage` index privilege for destination indices [discrete] -=== Upload CSV with latitude and longitude columns +=== Upload delimited files with latitude and longitude columns -*File Data Visualizer* indexes CSV files with latitude and longitude columns as a geo_point. +On the {kib} home page, you can upload a file and import it into an {es} index with latitude and longitude columns combined into a `geo_point` field. -. Open the main menu, then click *Machine Learning*. -. Select the *Data Visualizer* tab, then click *Upload file*. -. Use the file chooser to select a CSV file. +. Go to the {kib} home page and click *Upload a file*. +. Select a file in one of the supported file formats. . Click *Import*. . Select the *Advanced* tab. . Set *Index name*. -. Click *Add combined field*, then click *Add geo point field*. +. If a combined `geo_point` field is not created automatically, click *Add combined field*, then click *Add geo point field*. . Fill out the form and click *Add*. . Click *Import*. [discrete] === Upload a GeoJSON file -*Upload GeoJSON* indexes GeoJSON features as a geo_point or geo_shape. +*Upload GeoJSON* indexes GeoJSON features as a `geo_point` or `geo_shape`. . <>. . Click *Add layer*. From c5499c65923272ac72380c24873ff2875f15c6f2 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 10 Aug 2021 21:06:35 +0200 Subject: [PATCH 038/104] [Lens] Redux selectors optimization (#107559) --- .../lens/public/app_plugin/app.test.tsx | 38 +-- x-pack/plugins/lens/public/app_plugin/app.tsx | 53 +--- .../visualization.test.tsx | 6 - .../config_panel/config_panel.tsx | 24 +- .../config_panel/layer_panel.test.tsx | 214 +++++++------- .../editor_frame/config_panel/layer_panel.tsx | 17 +- .../editor_frame/config_panel/types.ts | 22 +- .../editor_frame/data_panel_wrapper.tsx | 46 ++- .../editor_frame/editor_frame.test.tsx | 53 +--- .../editor_frame/editor_frame.tsx | 113 +++---- .../editor_frame/expression_helpers.ts | 17 +- .../editor_frame/frame_layout.tsx | 10 +- .../editor_frame/state_helpers.ts | 21 +- .../editor_frame/suggestion_helpers.test.ts | 22 +- .../editor_frame/suggestion_helpers.ts | 43 ++- .../editor_frame/suggestion_panel.test.tsx | 154 +++++----- .../editor_frame/suggestion_panel.tsx | 232 +++++++-------- .../workspace_panel/chart_switch.test.tsx | 10 +- .../workspace_panel/chart_switch.tsx | 18 +- .../workspace_panel/workspace_panel.test.tsx | 277 +++++++----------- .../workspace_panel/workspace_panel.tsx | 168 +++++------ .../workspace_panel_wrapper.tsx | 17 +- .../operations/definitions/index.ts | 6 +- .../operations/definitions/terms/index.tsx | 4 +- .../definitions/terms/terms.test.tsx | 4 +- .../operations/layer_helpers.ts | 15 +- x-pack/plugins/lens/public/mocks.tsx | 40 ++- .../lens/public/state_management/index.ts | 1 + .../lens/public/state_management/selectors.ts | 153 ++++++++++ .../state_management/time_range_middleware.ts | 9 +- .../lens/public/state_management/types.ts | 13 +- x-pack/plugins/lens/public/types.ts | 8 +- x-pack/plugins/lens/public/utils.ts | 61 +--- 33 files changed, 872 insertions(+), 1017 deletions(-) create mode 100644 x-pack/plugins/lens/public/state_management/selectors.ts diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index c1984991b04d8..c26cce3317cf6 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -13,13 +13,7 @@ import { App } from './app'; import { LensAppProps, LensAppServices } from './types'; import { EditorFrameInstance, EditorFrameProps } from '../types'; import { Document } from '../persistence'; -import { - createMockDatasource, - createMockVisualization, - DatasourceMock, - makeDefaultServices, - mountWithProvider, -} from '../mocks'; +import { visualizationMap, datasourceMap, makeDefaultServices, mountWithProvider } from '../mocks'; import { I18nProvider } from '@kbn/i18n/react'; import { SavedObjectSaveModal, @@ -71,29 +65,6 @@ describe('Lens App', () => { let defaultDoc: Document; let defaultSavedObjectId: string; - const mockDatasource: DatasourceMock = createMockDatasource('testDatasource'); - const mockDatasource2: DatasourceMock = createMockDatasource('testDatasource2'); - const datasourceMap = { - testDatasource2: mockDatasource2, - testDatasource: mockDatasource, - }; - - const mockVisualization = { - ...createMockVisualization(), - id: 'testVis', - visualizationTypes: [ - { - icon: 'empty', - id: 'testVis', - label: 'TEST1', - groupLabel: 'testVisGroup', - }, - ], - }; - const visualizationMap = { - testVis: mockVisualization, - }; - function createMockFrame(): jest.Mocked { return { EditorFrameContainer: jest.fn((props: EditorFrameProps) =>
), @@ -1082,11 +1053,12 @@ describe('Lens App', () => { }); it('updates the state if session id changes from the outside', async () => { - const services = makeDefaultServices(sessionIdSubject); + const sessionIdS = new Subject(); + const services = makeDefaultServices(sessionIdS); const { lensStore } = await mountWith({ props: undefined, services }); act(() => { - sessionIdSubject.next('new-session-id'); + sessionIdS.next('new-session-id'); }); await act(async () => { await new Promise((r) => setTimeout(r, 0)); @@ -1181,7 +1153,7 @@ describe('Lens App', () => { ...defaultDoc, state: { ...defaultDoc.state, - datasourceStates: { testDatasource: '' }, + datasourceStates: { testDatasource: {} }, visualization: {}, }, }, diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 8faee830d52bb..4b956768265e8 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -29,13 +29,13 @@ import { useLensDispatch, LensAppState, DispatchSetState, + selectSavedObjectFormat, } from '../state_management'; import { SaveModalContainer, getLastKnownDocWithoutPinnedFilters, runSaveLensVisualization, } from './save_modal_container'; -import { getSavedObjectFormat } from '../utils'; export type SaveProps = Omit & { returnToOrigin: boolean; @@ -79,11 +79,6 @@ export function App({ ); const { - datasourceStates, - visualization, - filters, - query, - activeDatasourceId, persistedDoc, isLinkedToOriginatingApp, searchSessionId, @@ -91,52 +86,20 @@ export function App({ isSaveable, } = useLensSelector((state) => state.lens); + const currentDoc = useLensSelector((state) => + selectSavedObjectFormat(state, datasourceMap, visualizationMap) + ); + // Used to show a popover that guides the user towards changing the date range when no data is available. const [indicateNoData, setIndicateNoData] = useState(false); const [isSaveModalVisible, setIsSaveModalVisible] = useState(false); const [lastKnownDoc, setLastKnownDoc] = useState(undefined); useEffect(() => { - const activeVisualization = visualization.activeId && visualizationMap[visualization.activeId]; - const activeDatasource = - activeDatasourceId && !datasourceStates[activeDatasourceId].isLoading - ? datasourceMap[activeDatasourceId] - : undefined; - - if (!activeDatasource || !activeVisualization || !visualization.state) { - return; + if (currentDoc) { + setLastKnownDoc(currentDoc); } - setLastKnownDoc( - // todo: that should be redux store selector - getSavedObjectFormat({ - activeDatasources: Object.keys(datasourceStates).reduce( - (acc, datasourceId) => ({ - ...acc, - [datasourceId]: datasourceMap[datasourceId], - }), - {} - ), - datasourceStates, - visualization, - filters, - query, - title: persistedDoc?.title || '', - description: persistedDoc?.description, - persistedId: persistedDoc?.savedObjectId, - }) - ); - }, [ - persistedDoc?.title, - persistedDoc?.description, - persistedDoc?.savedObjectId, - datasourceStates, - visualization, - filters, - query, - activeDatasourceId, - datasourceMap, - visualizationMap, - ]); + }, [currentDoc]); const showNoDataPopover = useCallback(() => { setIndicateNoData(true); diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx index 552f0f94a67de..1e4b1cfa6069d 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -22,12 +22,6 @@ function mockFrame(): FramePublicAPI { return { ...createMockFramePublicAPI(), datasourceLayers: {}, - query: { query: '', language: 'lucene' }, - dateRange: { - fromDate: 'now-7d', - toDate: 'now', - }, - filters: [], }; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx index 8a5d385b5be0f..804f73b5d5fec 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -24,24 +24,32 @@ import { updateDatasourceState, updateVisualizationState, setToggleFullscreen, + useLensSelector, + selectVisualization, } from '../../../state_management'; export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) { - return props.activeVisualization && props.visualizationState ? ( - + const visualization = useLensSelector(selectVisualization); + const activeVisualization = visualization.activeId + ? props.visualizationMap[visualization.activeId] + : null; + + return activeVisualization && visualization.state ? ( + ) : null; }); export function LayerPanels( props: ConfigPanelWrapperProps & { - activeDatasourceId: string; activeVisualization: Visualization; } ) { - const { activeVisualization, visualizationState, activeDatasourceId, datasourceMap } = props; + const { activeVisualization, datasourceMap } = props; + const { activeDatasourceId, visualization } = useLensSelector((state) => state.lens); + const dispatchLens = useLensDispatch(); - const layerIds = activeVisualization.getLayerIds(visualizationState); + const layerIds = activeVisualization.getLayerIds(visualization.state); const { setNextFocusedId: setNextFocusedLayerId, removeRef: removeLayerRef, @@ -139,7 +147,7 @@ export function LayerPanels( key={layerId} layerId={layerId} layerIndex={layerIndex} - visualizationState={visualizationState} + visualizationState={visualization.state} updateVisualization={setVisualizationState} updateDatasource={updateDatasource} updateDatasourceAsync={updateDatasourceAsync} @@ -187,7 +195,7 @@ export function LayerPanels( /> ) : null )} - {activeVisualization.appendLayer && visualizationState && ( + {activeVisualization.appendLayer && visualization.state && ( id, trackUiEvent, - activeDatasource: datasourceMap[activeDatasourceId], + activeDatasource: datasourceMap[activeDatasourceId!], state, }), }) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index 3bb5fca2141a0..12f27b5bfba10 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { EuiFormRow } from '@elastic/eui'; -import { mountWithIntl } from '@kbn/test/jest'; import { Visualization } from '../../../types'; import { LayerPanel } from './layer_panel'; import { ChildDragDropProvider, DragDrop } from '../../../drag_drop'; @@ -19,6 +18,7 @@ import { createMockFramePublicAPI, createMockDatasource, DatasourceMock, + mountWithProvider, } from '../../../mocks'; jest.mock('../../../id_generator'); @@ -65,15 +65,8 @@ describe('LayerPanel', () => { return { layerId: 'first', activeVisualization: mockVisualization, - activeDatasourceId: 'ds1', datasourceMap: { - ds1: mockDatasource, - }, - datasourceStates: { - ds1: { - isLoading: false, - state: 'state', - }, + testDatasource: mockDatasource, }, visualizationState: 'state', updateVisualization: jest.fn(), @@ -120,45 +113,49 @@ describe('LayerPanel', () => { }; mockVisualization.getLayerIds.mockReturnValue(['first']); - mockDatasource = createMockDatasource('ds1'); + mockDatasource = createMockDatasource('testDatasource'); }); describe('layer reset and remove', () => { - it('should show the reset button when single layer', () => { - const component = mountWithIntl(); - expect(component.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain( + it('should show the reset button when single layer', async () => { + const { instance } = await mountWithProvider(); + expect(instance.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain( 'Reset layer' ); }); - it('should show the delete button when multiple layers', () => { - const component = mountWithIntl(); - expect(component.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain( + it('should show the delete button when multiple layers', async () => { + const { instance } = await mountWithProvider( + + ); + expect(instance.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain( 'Delete layer' ); }); - it('should show to reset visualization for visualizations only allowing a single layer', () => { + it('should show to reset visualization for visualizations only allowing a single layer', async () => { const layerPanelAttributes = getDefaultProps(); delete layerPanelAttributes.activeVisualization.removeLayer; - const component = mountWithIntl(); - expect(component.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain( + const { instance } = await mountWithProvider(); + expect(instance.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain( 'Reset visualization' ); }); - it('should call the clear callback', () => { + it('should call the clear callback', async () => { const cb = jest.fn(); - const component = mountWithIntl(); + const { instance } = await mountWithProvider( + + ); act(() => { - component.find('[data-test-subj="lnsLayerRemove"]').first().simulate('click'); + instance.find('[data-test-subj="lnsLayerRemove"]').first().simulate('click'); }); expect(cb).toHaveBeenCalled(); }); }); describe('single group', () => { - it('should render the non-editable state', () => { + it('should render the non-editable state', async () => { mockVisualization.getConfiguration.mockReturnValue({ groups: [ { @@ -172,12 +169,12 @@ describe('LayerPanel', () => { ], }); - const component = mountWithIntl(); - const group = component.find('.lnsLayerPanel__dimensionContainer[data-test-subj="lnsGroup"]'); + const { instance } = await mountWithProvider(); + const group = instance.find('.lnsLayerPanel__dimensionContainer[data-test-subj="lnsGroup"]'); expect(group).toHaveLength(1); }); - it('should render the group with a way to add a new column', () => { + it('should render the group with a way to add a new column', async () => { mockVisualization.getConfiguration.mockReturnValue({ groups: [ { @@ -191,12 +188,12 @@ describe('LayerPanel', () => { ], }); - const component = mountWithIntl(); - const group = component.find('.lnsLayerPanel__dimensionContainer[data-test-subj="lnsGroup"]'); + const { instance } = await mountWithProvider(); + const group = instance.find('.lnsLayerPanel__dimensionContainer[data-test-subj="lnsGroup"]'); expect(group).toHaveLength(1); }); - it('should render the required warning when only one group is configured', () => { + it('should render the required warning when only one group is configured', async () => { mockVisualization.getConfiguration.mockReturnValue({ groups: [ { @@ -219,16 +216,16 @@ describe('LayerPanel', () => { ], }); - const component = mountWithIntl(); + const { instance } = await mountWithProvider(); - const group = component + const group = instance .find(EuiFormRow) .findWhere((e) => e.prop('error')?.props?.children === 'Required dimension'); expect(group).toHaveLength(1); }); - it('should render the datasource and visualization panels inside the dimension container', () => { + it('should render the datasource and visualization panels inside the dimension container', async () => { mockVisualization.getConfiguration.mockReturnValueOnce({ groups: [ { @@ -244,18 +241,18 @@ describe('LayerPanel', () => { }); mockVisualization.renderDimensionEditor = jest.fn(); - const component = mountWithIntl(); + const { instance } = await mountWithProvider(); act(() => { - component.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); + instance.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); }); - component.update(); + instance.update(); - const group = component.find('DimensionContainer').first(); + const group = instance.find('DimensionContainer').first(); const panel: React.ReactElement = group.prop('panel'); expect(panel.props.children).toHaveLength(2); }); - it('should not update the visualization if the datasource is incomplete', () => { + it('should not update the visualization if the datasource is incomplete', async () => { (generateId as jest.Mock).mockReturnValue(`newid`); const updateAll = jest.fn(); const updateDatasourceAsync = jest.fn(); @@ -273,7 +270,7 @@ describe('LayerPanel', () => { ], }); - const component = mountWithIntl( + const { instance } = await mountWithProvider( { ); act(() => { - component.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); + instance.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); }); - component.update(); + instance.update(); expect(mockDatasource.renderDimensionEditor).toHaveBeenCalledWith( expect.any(Element), @@ -319,7 +316,7 @@ describe('LayerPanel', () => { expect(updateAll).toHaveBeenCalled(); }); - it('should remove the dimension when the datasource marks it as removed', () => { + it('should remove the dimension when the datasource marks it as removed', async () => { const updateAll = jest.fn(); const updateDatasource = jest.fn(); @@ -336,38 +333,42 @@ describe('LayerPanel', () => { ], }); - const component = mountWithIntl( + const { instance } = await mountWithProvider( , + { + preloadedState: { + datasourceStates: { + testDatasource: { + isLoading: false, + state: { + layers: [ + { + indexPatternId: '1', + columns: { + y: { + operationType: 'moving_average', + references: ['ref'], + }, }, + columnOrder: ['y'], + incompleteColumns: {}, }, - columnOrder: ['y'], - incompleteColumns: {}, - }, - ], + ], + }, }, }, - }} - updateDatasource={updateDatasource} - updateAll={updateAll} - /> + }, + } ); act(() => { - component.find('[data-test-subj="lnsLayerPanel-dimensionLink"]').first().simulate('click'); + instance.find('[data-test-subj="lnsLayerPanel-dimensionLink"]').first().simulate('click'); }); - component.update(); + instance.update(); expect(mockDatasource.renderDimensionEditor).toHaveBeenCalledWith( expect.any(Element), @@ -399,7 +400,7 @@ describe('LayerPanel', () => { ); }); - it('should keep the DimensionContainer open when configuring a new dimension', () => { + it('should keep the DimensionContainer open when configuring a new dimension', async () => { /** * The ID generation system for new dimensions has been messy before, so * this tests that the ID used in the first render is used to keep the container @@ -436,13 +437,13 @@ describe('LayerPanel', () => { ], }); - const component = mountWithIntl(); + const { instance } = await mountWithProvider(); act(() => { - component.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); + instance.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); }); - component.update(); + instance.update(); - expect(component.find('EuiFlyoutHeader').exists()).toBe(true); + expect(instance.find('EuiFlyoutHeader').exists()).toBe(true); const lastArgs = mockDatasource.renderDimensionEditor.mock.calls[ @@ -459,7 +460,7 @@ describe('LayerPanel', () => { expect(mockVisualization.renderDimensionEditor).toHaveBeenCalled(); }); - it('should close the DimensionContainer when the active visualization changes', () => { + it('should close the DimensionContainer when the active visualization changes', async () => { /** * The ID generation system for new dimensions has been messy before, so * this tests that the ID used in the first render is used to keep the container @@ -495,21 +496,21 @@ describe('LayerPanel', () => { ], }); - const component = mountWithIntl(); + const { instance } = await mountWithProvider(); act(() => { - component.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); + instance.find('[data-test-subj="lns-empty-dimension"]').first().simulate('click'); }); - component.update(); - expect(component.find('EuiFlyoutHeader').exists()).toBe(true); + instance.update(); + expect(instance.find('EuiFlyoutHeader').exists()).toBe(true); act(() => { - component.setProps({ activeVisualization: mockVisualization2 }); + instance.setProps({ activeVisualization: mockVisualization2 }); }); - component.update(); - expect(component.find('EuiFlyoutHeader').exists()).toBe(false); + instance.update(); + expect(instance.find('EuiFlyoutHeader').exists()).toBe(false); }); - it('should only update the state on close when needed', () => { + it('should only update the state on close when needed', async () => { const updateDatasource = jest.fn(); mockVisualization.getConfiguration.mockReturnValue({ groups: [ @@ -524,37 +525,37 @@ describe('LayerPanel', () => { ], }); - const component = mountWithIntl( + const { instance } = await mountWithProvider( ); // Close without a state update mockDatasource.updateStateOnCloseDimension = jest.fn(); - component.find('[data-test-subj="lnsLayerPanel-dimensionLink"]').first().simulate('click'); + instance.find('[data-test-subj="lnsLayerPanel-dimensionLink"]').first().simulate('click'); act(() => { - (component.find('DimensionContainer').first().prop('handleClose') as () => void)(); + (instance.find('DimensionContainer').first().prop('handleClose') as () => void)(); }); - component.update(); + instance.update(); expect(mockDatasource.updateStateOnCloseDimension).toHaveBeenCalled(); expect(updateDatasource).not.toHaveBeenCalled(); // Close with a state update mockDatasource.updateStateOnCloseDimension = jest.fn().mockReturnValue({ newState: true }); - component.find('[data-test-subj="lnsLayerPanel-dimensionLink"]').first().simulate('click'); + instance.find('[data-test-subj="lnsLayerPanel-dimensionLink"]').first().simulate('click'); act(() => { - (component.find('DimensionContainer').first().prop('handleClose') as () => void)(); + (instance.find('DimensionContainer').first().prop('handleClose') as () => void)(); }); - component.update(); + instance.update(); expect(mockDatasource.updateStateOnCloseDimension).toHaveBeenCalled(); - expect(updateDatasource).toHaveBeenCalledWith('ds1', { newState: true }); + expect(updateDatasource).toHaveBeenCalledWith('testDatasource', { newState: true }); }); }); // This test is more like an integration test, since the layer panel owns all // the coordination between drag and drop describe('drag and drop behavior', () => { - it('should determine if the datasource supports dropping of a field onto empty dimension', () => { + it('should determine if the datasource supports dropping of a field onto empty dimension', async () => { mockVisualization.getConfiguration.mockReturnValue({ groups: [ { @@ -584,7 +585,7 @@ describe('LayerPanel', () => { }, }; - const component = mountWithIntl( + const { instance } = await mountWithProvider( @@ -596,7 +597,7 @@ describe('LayerPanel', () => { }) ); - const dragDropElement = component + const dragDropElement = instance .find('[data-test-subj="lnsGroup"] DragDrop .lnsDragDrop') .first(); @@ -610,7 +611,7 @@ describe('LayerPanel', () => { ); }); - it('should determine if the datasource supports dropping of a field onto a pre-filled dimension', () => { + it('should determine if the datasource supports dropping of a field onto a pre-filled dimension', async () => { mockVisualization.getConfiguration.mockReturnValue({ groups: [ { @@ -639,7 +640,7 @@ describe('LayerPanel', () => { }, }; - const component = mountWithIntl( + const { instance } = await mountWithProvider( @@ -650,10 +651,10 @@ describe('LayerPanel', () => { ); expect( - component.find('[data-test-subj="lnsGroup"] DragDrop').first().prop('dropType') + instance.find('[data-test-subj="lnsGroup"] DragDrop').first().prop('dropType') ).toEqual(undefined); - const dragDropElement = component + const dragDropElement = instance .find('[data-test-subj="lnsGroup"] DragDrop') .first() .find('.lnsLayerPanel__dimension'); @@ -664,7 +665,7 @@ describe('LayerPanel', () => { expect(mockDatasource.onDrop).not.toHaveBeenCalled(); }); - it('should allow drag to move between groups', () => { + it('should allow drag to move between groups', async () => { (generateId as jest.Mock).mockReturnValue(`newid`); mockVisualization.getConfiguration.mockReturnValue({ @@ -705,7 +706,7 @@ describe('LayerPanel', () => { }, }; - const component = mountWithIntl( + const { instance } = await mountWithProvider( @@ -719,7 +720,7 @@ describe('LayerPanel', () => { // Simulate drop on the pre-populated dimension - const dragDropElement = component + const dragDropElement = instance .find('[data-test-subj="lnsGroupB"] DragDrop .lnsDragDrop') .at(0); dragDropElement.simulate('dragOver'); @@ -734,7 +735,7 @@ describe('LayerPanel', () => { // Simulate drop on the empty dimension - const updatedDragDropElement = component + const updatedDragDropElement = instance .find('[data-test-subj="lnsGroupB"] DragDrop .lnsDragDrop') .at(2); @@ -749,7 +750,7 @@ describe('LayerPanel', () => { ); }); - it('should reorder when dropping in the same group', () => { + it('should reorder when dropping in the same group', async () => { mockVisualization.getConfiguration.mockReturnValue({ groups: [ { @@ -775,14 +776,15 @@ describe('LayerPanel', () => { }, }; - const component = mountWithIntl( + const { instance } = await mountWithProvider( , + undefined, { attachTo: container } ); act(() => { - component.find(DragDrop).at(1).prop('onDrop')!(draggingOperation, 'reorder'); + instance.find(DragDrop).at(1).prop('onDrop')!(draggingOperation, 'reorder'); }); expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ @@ -790,7 +792,7 @@ describe('LayerPanel', () => { droppedItem: draggingOperation, }) ); - const secondButton = component + const secondButton = instance .find(DragDrop) .at(1) .find('[data-test-subj="lnsDragDrop-keyboardHandler"]') @@ -799,7 +801,7 @@ describe('LayerPanel', () => { expect(focusedEl).toEqual(secondButton); }); - it('should copy when dropping on empty slot in the same group', () => { + it('should copy when dropping on empty slot in the same group', async () => { (generateId as jest.Mock).mockReturnValue(`newid`); mockVisualization.getConfiguration.mockReturnValue({ groups: [ @@ -826,13 +828,13 @@ describe('LayerPanel', () => { }, }; - const component = mountWithIntl( + const { instance } = await mountWithProvider( ); act(() => { - component.find(DragDrop).at(2).prop('onDrop')!(draggingOperation, 'duplicate_compatible'); + instance.find(DragDrop).at(2).prop('onDrop')!(draggingOperation, 'duplicate_compatible'); }); expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ @@ -843,7 +845,7 @@ describe('LayerPanel', () => { ); }); - it('should call onDrop and update visualization when replacing between compatible groups', () => { + it('should call onDrop and update visualization when replacing between compatible groups', async () => { const mockVis = { ...mockVisualization, removeDimension: jest.fn(), @@ -881,7 +883,7 @@ describe('LayerPanel', () => { mockDatasource.onDrop.mockReturnValue({ deleted: 'a' }); const updateVisualization = jest.fn(); - const component = mountWithIntl( + const { instance } = await mountWithProvider( { ); act(() => { - component.find(DragDrop).at(3).prop('onDrop')!(draggingOperation, 'replace_compatible'); + instance.find(DragDrop).at(3).prop('onDrop')!(draggingOperation, 'replace_compatible'); }); expect(mockDatasource.onDrop).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 879a9ec7acedd..d0a6830aa178a 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -29,6 +29,12 @@ import { EmptyDimensionButton } from './buttons/empty_dimension_button'; import { DimensionButton } from './buttons/dimension_button'; import { DraggableDimensionButton } from './buttons/draggable_dimension_button'; import { useFocusUpdate } from './use_focus_update'; +import { + useLensSelector, + selectIsFullscreenDatasource, + selectResolvedDateRange, + selectDatasourceStates, +} from '../../../state_management'; const initialActiveDimensionState = { isNew: false, @@ -51,7 +57,6 @@ export function LayerPanel( onRemoveLayer: () => void; registerNewLayerRef: (layerId: string, instance: HTMLDivElement | null) => void; toggleFullscreen: () => void; - isFullscreen: boolean; } ) { const [activeDimension, setActiveDimension] = useState( @@ -69,12 +74,14 @@ export function LayerPanel( updateVisualization, updateDatasource, toggleFullscreen, - isFullscreen, updateAll, updateDatasourceAsync, visualizationState, } = props; const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; + const dateRange = useLensSelector(selectResolvedDateRange); + const datasourceStates = useLensSelector(selectDatasourceStates); + const isFullscreen = useLensSelector(selectIsFullscreenDatasource); useEffect(() => { setActiveDimension(initialActiveDimensionState); @@ -90,12 +97,12 @@ export function LayerPanel( layerId, state: props.visualizationState, frame: props.framePublicAPI, - dateRange: props.framePublicAPI.dateRange, + dateRange, activeData: props.framePublicAPI.activeData, }; const datasourceId = datasourcePublicAPI.datasourceId; - const layerDatasourceState = props.datasourceStates[datasourceId].state; + const layerDatasourceState = datasourceStates[datasourceId].state; const layerDatasourceDropProps = useMemo( () => ({ @@ -113,8 +120,8 @@ export function LayerPanel( const layerDatasourceConfigProps = { ...layerDatasourceDropProps, frame: props.framePublicAPI, - dateRange: props.framePublicAPI.dateRange, activeData: props.framePublicAPI.activeData, + dateRange, }; const { groups } = useMemo( diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts index d9bf678d1916a..66a30b0a405e8 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts @@ -11,39 +11,21 @@ import { DatasourceDimensionEditorProps, VisualizationDimensionGroupConfig, DatasourceMap, + VisualizationMap, } from '../../../types'; export interface ConfigPanelWrapperProps { - activeDatasourceId: string; - visualizationState: unknown; - activeVisualization: Visualization | null; framePublicAPI: FramePublicAPI; datasourceMap: DatasourceMap; - datasourceStates: Record< - string, - { - isLoading: boolean; - state: unknown; - } - >; + visualizationMap: VisualizationMap; core: DatasourceDimensionEditorProps['core']; - isFullscreen: boolean; } export interface LayerPanelProps { - activeDatasourceId: string; visualizationState: unknown; datasourceMap: DatasourceMap; activeVisualization: Visualization; framePublicAPI: FramePublicAPI; - datasourceStates: Record< - string, - { - isLoading: boolean; - state: unknown; - } - >; core: DatasourceDimensionEditorProps['core']; - isFullscreen: boolean; } export interface LayerDatasourceDropProps { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx index abfeb647186a7..b77d313973432 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/data_panel_wrapper.tsx @@ -10,7 +10,6 @@ import './data_panel_wrapper.scss'; import React, { useMemo, memo, useContext, useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiPopover, EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; -import { createSelector } from '@reduxjs/toolkit'; import { NativeRenderer } from '../../native_renderer'; import { DragContext, DragDropIdentifier } from '../../drag_drop'; import { StateSetter, DatasourceDataPanelProps, DatasourceMap } from '../../types'; @@ -19,17 +18,16 @@ import { switchDatasource, useLensDispatch, updateDatasourceState, - LensState, useLensSelector, setState, + selectExecutionContext, + selectActiveDatasourceId, + selectDatasourceStates, } from '../../state_management'; import { initializeDatasources } from './state_helpers'; interface DataPanelWrapperProps { - datasourceState: unknown; datasourceMap: DatasourceMap; - activeDatasource: string | null; - datasourceIsLoading: boolean; showNoDataPopover: () => void; core: DatasourceDataPanelProps['core']; dropOntoWorkspace: (field: DragDropIdentifier) => void; @@ -37,35 +35,27 @@ interface DataPanelWrapperProps { plugins: { uiActions: UiActionsStart }; } -const getExternals = createSelector( - (state: LensState) => state.lens, - ({ resolvedDateRange, query, filters, datasourceStates, activeDatasourceId }) => ({ - dateRange: resolvedDateRange, - query, - filters, - datasourceStates, - activeDatasourceId, - }) -); - export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { - const { activeDatasource } = props; + const externalContext = useLensSelector(selectExecutionContext); + const activeDatasourceId = useLensSelector(selectActiveDatasourceId); + const datasourceStates = useLensSelector(selectDatasourceStates); + + const datasourceIsLoading = activeDatasourceId + ? datasourceStates[activeDatasourceId].isLoading + : true; - const { filters, query, dateRange, datasourceStates, activeDatasourceId } = useLensSelector( - getExternals - ); const dispatchLens = useLensDispatch(); const setDatasourceState: StateSetter = useMemo(() => { return (updater) => { dispatchLens( updateDatasourceState({ updater, - datasourceId: activeDatasource!, + datasourceId: activeDatasourceId!, clearStagedPreview: true, }) ); }; - }, [activeDatasource, dispatchLens]); + }, [activeDatasourceId, dispatchLens]); useEffect(() => { if (activeDatasourceId && datasourceStates[activeDatasourceId].state === null) { @@ -88,13 +78,11 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { }, [datasourceStates, activeDatasourceId, props.datasourceMap, dispatchLens]); const datasourceProps: DatasourceDataPanelProps = { + ...externalContext, dragDropContext: useContext(DragContext), - state: props.datasourceState, + state: activeDatasourceId ? datasourceStates[activeDatasourceId].state : null, setState: setDatasourceState, core: props.core, - filters, - query, - dateRange, showNoDataPopover: props.showNoDataPopover, dropOntoWorkspace: props.dropOntoWorkspace, hasSuggestionForField: props.hasSuggestionForField, @@ -135,7 +123,7 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { { setDatasourceSwitcher(false); dispatchLens(switchDatasource({ newDatasourceId: datasourceId })); @@ -147,10 +135,10 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { /> )} - {props.activeDatasource && !props.datasourceIsLoading && ( + {activeDatasourceId && !datasourceIsLoading && ( )} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index 4ce68dc3bc70a..5d1652b8e1f7a 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { ReactElement } from 'react'; +import React from 'react'; import { ReactWrapper } from 'enzyme'; // Tests are executed in a jsdom environment who does not have sizing methods, @@ -45,8 +45,7 @@ import { uiActionsPluginMock } from '../../../../../../src/plugins/ui_actions/pu import { chartPluginMock } from '../../../../../../src/plugins/charts/public/mocks'; import { expressionsPluginMock } from '../../../../../../src/plugins/expressions/public/mocks'; import { mockDataPlugin, mountWithProvider } from '../../mocks'; -import { setState, setToggleFullscreen } from '../../state_management'; -import { FrameLayout } from './frame_layout'; +import { setState } from '../../state_management'; function generateSuggestion(state = {}): DatasourceSuggestion { return { @@ -213,7 +212,7 @@ describe('editor_frame', () => { const props = { ...getDefaultProps(), visualizationMap: { - testVis: { ...mockVisualization, toExpression: () => 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }, datasourceMap: { testDatasource: { @@ -246,7 +245,7 @@ describe('editor_frame', () => { expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` "kibana | lens_merge_tables layerIds=\\"first\\" tables={datasource} - | vis" + | testVis" `); }); @@ -263,7 +262,7 @@ describe('editor_frame', () => { const props = { ...getDefaultProps(), visualizationMap: { - testVis: { ...mockVisualization, toExpression: () => 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }, datasourceMap: { testDatasource: { @@ -368,7 +367,7 @@ describe('editor_frame', () => { }, Object { "arguments": Object {}, - "function": "vis", + "function": "testVis", "type": "function", }, ], @@ -1100,45 +1099,5 @@ describe('editor_frame', () => { }) ); }); - - it('should avoid completely to compute suggestion when in fullscreen mode', async () => { - const props = { - ...getDefaultProps(), - initialContext: { - indexPatternId: '1', - fieldName: 'test', - }, - visualizationMap: { - testVis: mockVisualization, - }, - datasourceMap: { - testDatasource: mockDatasource, - testDatasource2: mockDatasource2, - }, - - ExpressionRenderer: expressionRendererMock, - }; - - const { instance: el, lensStore } = await mountWithProvider(, { - data: props.plugins.data, - }); - instance = el; - - expect( - instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement - ).not.toBeUndefined(); - - lensStore.dispatch(setToggleFullscreen()); - instance.update(); - - expect(instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement).toBe(false); - - lensStore.dispatch(setToggleFullscreen()); - instance.update(); - - expect( - instance.find(FrameLayout).prop('suggestionsPanel') as ReactElement - ).not.toBeUndefined(); - }); }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 653ad2d27fe06..0813d0deef02f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -5,21 +5,28 @@ * 2.0. */ -import React, { useCallback, useRef, useMemo } from 'react'; +import React, { useCallback, useRef } from 'react'; import { CoreStart } from 'kibana/public'; import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; import { DatasourceMap, FramePublicAPI, VisualizationMap } from '../../types'; import { DataPanelWrapper } from './data_panel_wrapper'; import { ConfigPanelWrapper } from './config_panel'; import { FrameLayout } from './frame_layout'; -import { SuggestionPanel } from './suggestion_panel'; +import { SuggestionPanelWrapper } from './suggestion_panel'; import { WorkspacePanel } from './workspace_panel'; import { DragDropIdentifier, RootDragDropProvider } from '../../drag_drop'; import { EditorFrameStartPlugins } from '../service'; -import { createDatasourceLayers } from './state_helpers'; import { getTopSuggestionForField, switchToSuggestion, Suggestion } from './suggestion_helpers'; import { trackUiEvent } from '../../lens_ui_telemetry'; -import { useLensSelector, useLensDispatch } from '../../state_management'; +import { + useLensSelector, + useLensDispatch, + selectAreDatasourcesLoaded, + selectFramePublicAPI, + selectActiveDatasourceId, + selectDatasourceStates, + selectVisualization, +} from '../../state_management'; export interface EditorFrameProps { datasourceMap: DatasourceMap; @@ -31,58 +38,27 @@ export interface EditorFrameProps { } export function EditorFrame(props: EditorFrameProps) { - const { - activeData, - resolvedDateRange: dateRange, - query, - filters, - searchSessionId, - activeDatasourceId, - visualization, - datasourceStates, - stagedPreview, - isFullscreenDatasource, - } = useLensSelector((state) => state.lens); - + const { datasourceMap, visualizationMap } = props; const dispatchLens = useLensDispatch(); - - const allLoaded = Object.values(datasourceStates).every(({ isLoading }) => isLoading === false); - - const datasourceLayers = React.useMemo( - () => createDatasourceLayers(props.datasourceMap, datasourceStates), - [props.datasourceMap, datasourceStates] - ); - - const framePublicAPI: FramePublicAPI = useMemo( - () => ({ - datasourceLayers, - activeData, - dateRange, - query, - filters, - searchSessionId, - }), - [activeData, datasourceLayers, dateRange, query, filters, searchSessionId] + const activeDatasourceId = useLensSelector(selectActiveDatasourceId); + const datasourceStates = useLensSelector(selectDatasourceStates); + const visualization = useLensSelector(selectVisualization); + const allLoaded = useLensSelector(selectAreDatasourcesLoaded); + const framePublicAPI: FramePublicAPI = useLensSelector((state) => + selectFramePublicAPI(state, datasourceMap) ); - // Using a ref to prevent rerenders in the child components while keeping the latest state const getSuggestionForField = useRef<(field: DragDropIdentifier) => Suggestion | undefined>(); getSuggestionForField.current = (field: DragDropIdentifier) => { - const activeVisualizationId = visualization.activeId; - const visualizationState = visualization.state; - const { visualizationMap, datasourceMap } = props; - if (!field || !activeDatasourceId) { return; } - return getTopSuggestionForField( - datasourceLayers, - activeVisualizationId, + framePublicAPI.datasourceLayers, + visualization, + datasourceStates, visualizationMap, - visualizationState, datasourceMap[activeDatasourceId], - datasourceStates, field ); }; @@ -106,18 +82,12 @@ export function EditorFrame(props: EditorFrameProps) { return ( @@ -125,50 +95,33 @@ export function EditorFrame(props: EditorFrameProps) { configPanel={ allLoaded && ( ) } workspacePanel={ allLoaded && ( ) } suggestionsPanel={ - allLoaded && - !isFullscreenDatasource && ( - ) } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts index 5d1f99cf0dec3..4acc56f1dff3d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/expression_helpers.ts @@ -6,18 +6,13 @@ */ import { Ast, fromExpression, ExpressionFunctionAST } from '@kbn/interpreter/common'; +import { DatasourceStates } from '../../state_management'; import { Visualization, DatasourcePublicAPI, DatasourceMap } from '../../types'; export function prependDatasourceExpression( visualizationExpression: Ast | string | null, datasourceMap: DatasourceMap, - datasourceStates: Record< - string, - { - isLoading: boolean; - state: unknown; - } - > + datasourceStates: DatasourceStates ): Ast | null { const datasourceExpressions: Array<[string, Ast | string]> = []; @@ -81,13 +76,7 @@ export function buildExpression({ visualization: Visualization | null; visualizationState: unknown; datasourceMap: DatasourceMap; - datasourceStates: Record< - string, - { - isLoading: boolean; - state: unknown; - } - >; + datasourceStates: DatasourceStates; datasourceLayers: Record; }): Ast | null { if (visualization === null) { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx index f27e0f9c24d7b..5175158e077f0 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/frame_layout.tsx @@ -11,20 +11,22 @@ import React from 'react'; import { EuiPage, EuiPageBody, EuiScreenReaderOnly } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import classNames from 'classnames'; +import { useLensSelector, selectIsFullscreenDatasource } from '../../state_management'; export interface FrameLayoutProps { dataPanel: React.ReactNode; configPanel?: React.ReactNode; suggestionsPanel?: React.ReactNode; workspacePanel?: React.ReactNode; - isFullscreen?: boolean; } export function FrameLayout(props: FrameLayoutProps) { + const isFullscreen = useLensSelector(selectIsFullscreenDatasource); + return ( @@ -65,7 +67,7 @@ export function FrameLayout(props: FrameLayoutProps) {
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts index 2b0090554b7da..5e059a1e2e8b1 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts @@ -28,15 +28,16 @@ import { getMissingIndexPatterns, getMissingVisualizationTypeError, } from '../error_helper'; +import { DatasourceStates } from '../../state_management'; export async function initializeDatasources( datasourceMap: DatasourceMap, - datasourceStates: Record, + datasourceStates: DatasourceStates, references?: SavedObjectReference[], initialContext?: VisualizeFieldContext, options?: InitializationOptions ) { - const states: Record = {}; + const states: DatasourceStates = {}; await Promise.all( Object.entries(datasourceMap).map(([datasourceId, datasource]) => { if (datasourceStates[datasourceId]) { @@ -57,8 +58,8 @@ export async function initializeDatasources( } export const createDatasourceLayers = memoizeOne(function createDatasourceLayers( - datasourceMap: DatasourceMap, - datasourceStates: Record + datasourceStates: DatasourceStates, + datasourceMap: DatasourceMap ) { const datasourceLayers: Record = {}; Object.keys(datasourceMap) @@ -79,7 +80,7 @@ export const createDatasourceLayers = memoizeOne(function createDatasourceLayers }); export async function persistedStateToExpression( - datasources: Record, + datasourceMap: DatasourceMap, visualizations: VisualizationMap, doc: Document ): Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }> { @@ -98,7 +99,7 @@ export async function persistedStateToExpression( } const visualization = visualizations[visualizationType!]; const datasourceStates = await initializeDatasources( - datasources, + datasourceMap, Object.fromEntries( Object.entries(persistedDatasourceStates).map(([id, state]) => [ id, @@ -110,7 +111,7 @@ export async function persistedStateToExpression( { isFullEditor: false } ); - const datasourceLayers = createDatasourceLayers(datasources, datasourceStates); + const datasourceLayers = createDatasourceLayers(datasourceStates, datasourceMap); const datasourceId = getActiveDatasourceIdFromDoc(doc); if (datasourceId == null) { @@ -121,7 +122,7 @@ export async function persistedStateToExpression( } const indexPatternValidation = validateRequiredIndexPatterns( - datasources[datasourceId], + datasourceMap[datasourceId], datasourceStates[datasourceId] ); @@ -133,7 +134,7 @@ export async function persistedStateToExpression( } const validationResult = validateDatasourceAndVisualization( - datasources[datasourceId], + datasourceMap[datasourceId], datasourceStates[datasourceId].state, visualization, visualizationState, @@ -146,7 +147,7 @@ export async function persistedStateToExpression( description, visualization, visualizationState, - datasourceMap: datasources, + datasourceMap, datasourceStates, datasourceLayers, }), diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts index 6f33cc4b8aab8..632989057b488 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts @@ -9,6 +9,7 @@ import { getSuggestions, getTopSuggestionForField } from './suggestion_helpers'; import { createMockVisualization, createMockDatasource, DatasourceMock } from '../../mocks'; import { TableSuggestion, DatasourceSuggestion, Visualization } from '../../types'; import { PaletteOutput } from 'src/plugins/charts/public'; +import { DatasourceStates } from '../../state_management'; const generateSuggestion = (state = {}, layerId: string = 'first'): DatasourceSuggestion => ({ state, @@ -22,13 +23,7 @@ const generateSuggestion = (state = {}, layerId: string = 'first'): DatasourceSu }); let datasourceMap: Record; -let datasourceStates: Record< - string, - { - isLoading: boolean; - state: unknown; - } ->; +let datasourceStates: DatasourceStates; beforeEach(() => { datasourceMap = { @@ -525,13 +520,10 @@ describe('suggestion helpers', () => { getOperationForColumnId: jest.fn(), }, }, - 'vis1', + { activeId: 'vis1', state: {} }, + { mockindexpattern: { state: mockDatasourceState, isLoading: false } }, { vis1: mockVisualization1 }, - {}, datasourceMap.mock, - { - mockindexpattern: { state: mockDatasourceState, isLoading: false }, - }, { id: 'myfield', humanData: { label: 'myfieldLabel' } }, ]; }); @@ -558,7 +550,7 @@ describe('suggestion helpers', () => { it('should return nothing if datasource does not produce suggestions', () => { datasourceMap.mock.getDatasourceSuggestionsForField.mockReturnValue([]); - defaultParams[2] = { + defaultParams[3] = { vis1: { ...mockVisualization1, getSuggestions: () => [] }, vis2: mockVisualization2, }; @@ -567,7 +559,7 @@ describe('suggestion helpers', () => { }); it('should not consider suggestion from other visualization if there is data', () => { - defaultParams[2] = { + defaultParams[3] = { vis1: { ...mockVisualization1, getSuggestions: () => [] }, vis2: mockVisualization2, }; @@ -593,7 +585,7 @@ describe('suggestion helpers', () => { previewIcon: 'empty', }, ]); - defaultParams[2] = { + defaultParams[3] = { vis1: mockVisualization1, vis2: mockVisualization2, vis3: mockVisualization3, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts index 31d54d26c304b..65cd5ae35c6fe 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts @@ -22,7 +22,13 @@ import { VisualizationMap, } from '../../types'; import { DragDropIdentifier } from '../../drag_drop'; -import { LensDispatch, selectSuggestion, switchVisualization } from '../../state_management'; +import { + LensDispatch, + selectSuggestion, + switchVisualization, + DatasourceStates, + VisualizationState, +} from '../../state_management'; export interface Suggestion { visualizationId: string; @@ -60,13 +66,7 @@ export function getSuggestions({ mainPalette, }: { datasourceMap: DatasourceMap; - datasourceStates: Record< - string, - { - isLoading: boolean; - state: unknown; - } - >; + datasourceStates: DatasourceStates; visualizationMap: VisualizationMap; activeVisualizationId: string | null; subVisualizationId?: string; @@ -143,13 +143,7 @@ export function getVisualizeFieldSuggestions({ visualizeTriggerFieldContext, }: { datasourceMap: DatasourceMap; - datasourceStates: Record< - string, - { - isLoading: boolean; - state: unknown; - } - >; + datasourceStates: DatasourceStates; visualizationMap: VisualizationMap; activeVisualizationId: string | null; subVisualizationId?: string; @@ -228,11 +222,10 @@ export function switchToSuggestion( export function getTopSuggestionForField( datasourceLayers: Record, - activeVisualizationId: string | null, + visualization: VisualizationState, + datasourceStates: DatasourceStates, visualizationMap: Record>, - visualizationState: unknown, datasource: Datasource, - datasourceStates: Record, field: DragDropIdentifier ) { const hasData = Object.values(datasourceLayers).some( @@ -240,20 +233,20 @@ export function getTopSuggestionForField( ); const mainPalette = - activeVisualizationId && visualizationMap[activeVisualizationId]?.getMainPalette - ? visualizationMap[activeVisualizationId].getMainPalette?.(visualizationState) + visualization.activeId && visualizationMap[visualization.activeId]?.getMainPalette + ? visualizationMap[visualization.activeId].getMainPalette?.(visualization.state) : undefined; const suggestions = getSuggestions({ datasourceMap: { [datasource.id]: datasource }, datasourceStates, visualizationMap: - hasData && activeVisualizationId - ? { [activeVisualizationId]: visualizationMap[activeVisualizationId] } + hasData && visualization.activeId + ? { [visualization.activeId]: visualizationMap[visualization.activeId] } : visualizationMap, - activeVisualizationId, - visualizationState, + activeVisualizationId: visualization.activeId, + visualizationState: visualization.state, field, mainPalette, }); - return suggestions.find((s) => s.visualizationId === activeVisualizationId) || suggestions[0]; + return suggestions.find((s) => s.visualizationId === visualization.activeId) || suggestions[0]; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx index 44fb47001631e..b63d2956cfe6b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.test.tsx @@ -16,12 +16,12 @@ import { } from '../../mocks'; import { act } from 'react-dom/test-utils'; import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; -import { esFilters, IFieldType, IndexPattern } from '../../../../../../src/plugins/data/public'; -import { SuggestionPanel, SuggestionPanelProps } from './suggestion_panel'; +import { SuggestionPanel, SuggestionPanelProps, SuggestionPanelWrapper } from './suggestion_panel'; import { getSuggestions, Suggestion } from './suggestion_helpers'; import { EuiIcon, EuiPanel, EuiToolTip } from '@elastic/eui'; import { LensIconChartDatatable } from '../../assets/chart_datatable'; import { mountWithProvider } from '../../mocks'; +import { LensAppState, PreviewState, setState, setToggleFullscreen } from '../../state_management'; jest.mock('./suggestion_helpers'); @@ -38,6 +38,8 @@ describe('suggestion_panel', () => { let defaultProps: SuggestionPanelProps; + let preloadedState: Partial; + beforeEach(() => { mockVisualization = createMockVisualization(); mockDatasource = createMockDatasource('a'); @@ -49,7 +51,7 @@ describe('suggestion_panel', () => { previewIcon: 'empty', score: 0.5, visualizationState: suggestion1State, - visualizationId: 'vis', + visualizationId: 'testVis', title: 'Suggestion1', keptLayerIds: ['a'], }, @@ -58,36 +60,58 @@ describe('suggestion_panel', () => { previewIcon: 'empty', score: 0.5, visualizationState: suggestion2State, - visualizationId: 'vis', + visualizationId: 'testVis', title: 'Suggestion2', keptLayerIds: ['a'], }, ] as Suggestion[]); - defaultProps = { - activeDatasourceId: 'mock', - datasourceMap: { - mock: mockDatasource, - }, + preloadedState = { datasourceStates: { - mock: { + testDatasource: { isLoading: false, - state: {}, + state: '', }, }, - activeVisualizationId: 'vis', + visualization: { + activeId: 'testVis', + state: {}, + }, + activeDatasourceId: 'testDatasource', + }; + + defaultProps = { + datasourceMap: { + testDatasource: mockDatasource, + }, visualizationMap: { - vis: mockVisualization, + testVis: mockVisualization, vis2: createMockVisualization(), }, - visualizationState: {}, ExpressionRenderer: expressionRendererMock, frame: createMockFramePublicAPI(), }; }); + it('should avoid completely to render SuggestionPanel when in fullscreen mode', async () => { + const { instance, lensStore } = await mountWithProvider( + + ); + expect(instance.find(SuggestionPanel).exists()).toBe(true); + + lensStore.dispatch(setToggleFullscreen()); + instance.update(); + expect(instance.find(SuggestionPanel).exists()).toBe(false); + + lensStore.dispatch(setToggleFullscreen()); + instance.update(); + expect(instance.find(SuggestionPanel).exists()).toBe(true); + }); + it('should list passed in suggestions', async () => { - const { instance } = await mountWithProvider(); + const { instance } = await mountWithProvider(, { + preloadedState, + }); expect( instance @@ -98,62 +122,56 @@ describe('suggestion_panel', () => { }); describe('uncommitted suggestions', () => { - let suggestionState: Pick< - SuggestionPanelProps, - 'datasourceStates' | 'activeVisualizationId' | 'visualizationState' - >; - let stagedPreview: SuggestionPanelProps['stagedPreview']; + let suggestionState: Pick; + let stagedPreview: PreviewState; beforeEach(() => { suggestionState = { datasourceStates: { - mock: { + testDatasource: { isLoading: false, - state: {}, + state: '', }, }, - activeVisualizationId: 'vis2', - visualizationState: {}, + visualization: { + activeId: 'vis2', + state: {}, + }, }; stagedPreview = { - datasourceStates: defaultProps.datasourceStates, - visualization: { - state: defaultProps.visualizationState, - activeId: defaultProps.activeVisualizationId, - }, + datasourceStates: preloadedState.datasourceStates!, + visualization: preloadedState.visualization!, }; }); it('should not update suggestions if current state is moved to staged preview', async () => { - const { instance } = await mountWithProvider(); + const { instance, lensStore } = await mountWithProvider( + , + { preloadedState } + ); getSuggestionsMock.mockClear(); - instance.setProps({ - stagedPreview, - ...suggestionState, - }); + lensStore.dispatch(setState({ stagedPreview })); instance.update(); expect(getSuggestionsMock).not.toHaveBeenCalled(); }); it('should update suggestions if staged preview is removed', async () => { - const { instance } = await mountWithProvider(); + const { instance, lensStore } = await mountWithProvider( + , + { preloadedState } + ); getSuggestionsMock.mockClear(); - instance.setProps({ - stagedPreview, - ...suggestionState, - }); + lensStore.dispatch(setState({ stagedPreview, ...suggestionState })); instance.update(); - instance.setProps({ - stagedPreview: undefined, - ...suggestionState, - }); + lensStore.dispatch(setState({ stagedPreview: undefined, ...suggestionState })); instance.update(); expect(getSuggestionsMock).toHaveBeenCalledTimes(1); }); it('should highlight currently active suggestion', async () => { - const { instance } = await mountWithProvider(); - + const { instance } = await mountWithProvider(, { + preloadedState, + }); act(() => { instance.find('[data-test-subj="lnsSuggestion"]').at(2).simulate('click'); }); @@ -189,12 +207,14 @@ describe('suggestion_panel', () => { }); it('should dispatch visualization switch action if suggestion is clicked', async () => { - const { instance, lensStore } = await mountWithProvider(); + const { instance, lensStore } = await mountWithProvider(, { + preloadedState, + }); act(() => { instance.find('button[data-test-subj="lnsSuggestion"]').at(1).simulate('click'); }); - instance.update(); + // instance.update(); expect(lensStore.dispatch).toHaveBeenCalledWith( expect.objectContaining({ @@ -203,7 +223,7 @@ describe('suggestion_panel', () => { datasourceId: undefined, datasourceState: {}, initialState: { suggestion1: true }, - newVisualizationId: 'vis', + newVisualizationId: 'testVis', }, }) ); @@ -217,7 +237,7 @@ describe('suggestion_panel', () => { previewIcon: LensIconChartDatatable, score: 0.5, visualizationState: suggestion1State, - visualizationId: 'vis', + visualizationId: 'testVis', title: 'Suggestion1', }, { @@ -225,7 +245,7 @@ describe('suggestion_panel', () => { previewIcon: 'empty', score: 0.5, visualizationState: suggestion2State, - visualizationId: 'vis', + visualizationId: 'testVis', title: 'Suggestion2', previewExpression: 'test | expression', }, @@ -239,7 +259,9 @@ describe('suggestion_panel', () => { mockDatasource.toExpression.mockReturnValue('datasource_expression'); - const { instance } = await mountWithProvider(); + const { instance } = await mountWithProvider(, { + preloadedState, + }); expect(instance.find(EuiIcon)).toHaveLength(1); expect(instance.find(EuiIcon).prop('type')).toEqual(LensIconChartDatatable); @@ -252,17 +274,20 @@ describe('suggestion_panel', () => { indexPatterns: {}, }; mockDatasource.checkIntegrity.mockReturnValue(['a']); - const newProps = { - ...defaultProps, + + const newPreloadedState = { + ...preloadedState, datasourceStates: { - mock: { - ...defaultProps.datasourceStates.mock, + testDatasource: { + ...preloadedState.datasourceStates!.testDatasource, state: missingIndexPatternsState, }, }, }; - const { instance } = await mountWithProvider(); + const { instance } = await mountWithProvider(, { + preloadedState: newPreloadedState, + }); expect(instance.html()).toEqual(null); }); @@ -274,7 +299,7 @@ describe('suggestion_panel', () => { previewIcon: 'empty', score: 0.5, visualizationState: suggestion1State, - visualizationId: 'vis', + visualizationId: 'testVis', title: 'Suggestion1', }, { @@ -282,7 +307,7 @@ describe('suggestion_panel', () => { previewIcon: 'empty', score: 0.5, visualizationState: suggestion2State, - visualizationId: 'vis', + visualizationId: 'testVis', title: 'Suggestion2', }, ] as Suggestion[]); @@ -291,18 +316,7 @@ describe('suggestion_panel', () => { (mockVisualization.toPreviewExpression as jest.Mock).mockReturnValueOnce('test | expression'); mockDatasource.toExpression.mockReturnValue('datasource_expression'); - const indexPattern = ({ id: 'index1' } as unknown) as IndexPattern; - const field = ({ name: 'myfield' } as unknown) as IFieldType; - - mountWithProvider( - - ); + mountWithProvider(); expect(expressionRendererMock).toHaveBeenCalledTimes(1); const passedExpression = (expressionRendererMock as jest.Mock).mock.calls[0][0].expression; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index c6dae5b23b9d4..858fcedf215ef 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -42,30 +42,28 @@ import { prependDatasourceExpression } from './expression_helpers'; import { trackUiEvent, trackSuggestionEvent } from '../../lens_ui_telemetry'; import { getMissingIndexPattern, validateDatasourceAndVisualization } from './state_helpers'; import { - PreviewState, rollbackSuggestion, + selectExecutionContextSearch, submitSuggestion, useLensDispatch, + useLensSelector, + selectCurrentVisualization, + selectCurrentDatasourceStates, + DatasourceStates, + selectIsFullscreenDatasource, + selectSearchSessionId, + selectActiveDatasourceId, + selectActiveData, + selectDatasourceStates, } from '../../state_management'; const MAX_SUGGESTIONS_DISPLAYED = 5; export interface SuggestionPanelProps { - activeDatasourceId: string | null; datasourceMap: DatasourceMap; - datasourceStates: Record< - string, - { - isLoading: boolean; - state: unknown; - } - >; - activeVisualizationId: string | null; visualizationMap: VisualizationMap; - visualizationState: unknown; ExpressionRenderer: ReactExpressionRendererType; frame: FramePublicAPI; - stagedPreview?: PreviewState; } const PreviewRenderer = ({ @@ -173,128 +171,114 @@ const SuggestionPreview = ({ ); }; +export const SuggestionPanelWrapper = (props: SuggestionPanelProps) => { + const isFullscreenDatasource = useLensSelector(selectIsFullscreenDatasource); + return isFullscreenDatasource ? null : ; +}; + export function SuggestionPanel({ - activeDatasourceId, datasourceMap, - datasourceStates, - activeVisualizationId, visualizationMap, - visualizationState, frame, ExpressionRenderer: ExpressionRendererComponent, - stagedPreview, }: SuggestionPanelProps) { const dispatchLens = useLensDispatch(); - - const currentDatasourceStates = stagedPreview ? stagedPreview.datasourceStates : datasourceStates; - const currentVisualizationState = stagedPreview - ? stagedPreview.visualization.state - : visualizationState; - const currentVisualizationId = stagedPreview - ? stagedPreview.visualization.activeId - : activeVisualizationId; + const activeDatasourceId = useLensSelector(selectActiveDatasourceId); + const activeData = useLensSelector(selectActiveData); + const datasourceStates = useLensSelector(selectDatasourceStates); + const existsStagedPreview = useLensSelector((state) => Boolean(state.lens.stagedPreview)); + const currentVisualization = useLensSelector(selectCurrentVisualization); + const currentDatasourceStates = useLensSelector(selectCurrentDatasourceStates); const missingIndexPatterns = getMissingIndexPattern( activeDatasourceId ? datasourceMap[activeDatasourceId] : null, activeDatasourceId ? datasourceStates[activeDatasourceId] : null ); - const { suggestions, currentStateExpression, currentStateError } = useMemo( - () => { - const newSuggestions = missingIndexPatterns.length - ? [] - : getSuggestions({ - datasourceMap, - datasourceStates: currentDatasourceStates, - visualizationMap, - activeVisualizationId: currentVisualizationId, - visualizationState: currentVisualizationState, - activeData: frame.activeData, - }) - .filter( - ({ - hide, - visualizationId, - visualizationState: suggestionVisualizationState, - datasourceState: suggestionDatasourceState, - datasourceId: suggetionDatasourceId, - }) => { - return ( - !hide && - validateDatasourceAndVisualization( - suggetionDatasourceId ? datasourceMap[suggetionDatasourceId] : null, - suggestionDatasourceState, - visualizationMap[visualizationId], - suggestionVisualizationState, - frame - ) == null - ); - } - ) - .slice(0, MAX_SUGGESTIONS_DISPLAYED) - .map((suggestion) => ({ - ...suggestion, - previewExpression: preparePreviewExpression( - suggestion, - visualizationMap[suggestion.visualizationId], - datasourceMap, - currentDatasourceStates, - frame - ), - })); - - const validationErrors = validateDatasourceAndVisualization( - activeDatasourceId ? datasourceMap[activeDatasourceId] : null, - activeDatasourceId && currentDatasourceStates[activeDatasourceId]?.state, - currentVisualizationId ? visualizationMap[currentVisualizationId] : null, - currentVisualizationState, - frame - ); - - const newStateExpression = - currentVisualizationState && currentVisualizationId && !validationErrors - ? preparePreviewExpression( - { visualizationState: currentVisualizationState }, - visualizationMap[currentVisualizationId], + const { suggestions, currentStateExpression, currentStateError } = useMemo(() => { + const newSuggestions = missingIndexPatterns.length + ? [] + : getSuggestions({ + datasourceMap, + datasourceStates: currentDatasourceStates, + visualizationMap, + activeVisualizationId: currentVisualization.activeId, + visualizationState: currentVisualization.state, + activeData, + }) + .filter( + ({ + hide, + visualizationId, + visualizationState: suggestionVisualizationState, + datasourceState: suggestionDatasourceState, + datasourceId: suggetionDatasourceId, + }) => { + return ( + !hide && + validateDatasourceAndVisualization( + suggetionDatasourceId ? datasourceMap[suggetionDatasourceId] : null, + suggestionDatasourceState, + visualizationMap[visualizationId], + suggestionVisualizationState, + frame + ) == null + ); + } + ) + .slice(0, MAX_SUGGESTIONS_DISPLAYED) + .map((suggestion) => ({ + ...suggestion, + previewExpression: preparePreviewExpression( + suggestion, + visualizationMap[suggestion.visualizationId], datasourceMap, currentDatasourceStates, frame - ) - : undefined; - - return { - suggestions: newSuggestions, - currentStateExpression: newStateExpression, - currentStateError: validationErrors, - }; - }, + ), + })); + + const validationErrors = validateDatasourceAndVisualization( + activeDatasourceId ? datasourceMap[activeDatasourceId] : null, + activeDatasourceId && currentDatasourceStates[activeDatasourceId]?.state, + currentVisualization.activeId ? visualizationMap[currentVisualization.activeId] : null, + currentVisualization.state, + frame + ); + + const newStateExpression = + currentVisualization.state && currentVisualization.activeId && !validationErrors + ? preparePreviewExpression( + { visualizationState: currentVisualization.state }, + visualizationMap[currentVisualization.activeId], + datasourceMap, + currentDatasourceStates, + frame + ) + : undefined; + + return { + suggestions: newSuggestions, + currentStateExpression: newStateExpression, + currentStateError: validationErrors, + }; // eslint-disable-next-line react-hooks/exhaustive-deps - [ - currentDatasourceStates, - currentVisualizationState, - currentVisualizationId, - activeDatasourceId, - datasourceMap, - visualizationMap, - ] - ); + }, [ + currentDatasourceStates, + currentVisualization.state, + currentVisualization.activeId, + activeDatasourceId, + datasourceMap, + visualizationMap, + ]); - const context: ExecutionContextSearch = useMemo( - () => ({ - query: frame.query, - timeRange: { - from: frame.dateRange.fromDate, - to: frame.dateRange.toDate, - }, - filters: frame.filters, - }), - [frame.query, frame.dateRange.fromDate, frame.dateRange.toDate, frame.filters] - ); + const context: ExecutionContextSearch = useLensSelector(selectExecutionContextSearch); + const searchSessionId = useLensSelector(selectSearchSessionId); const contextRef = useRef(context); contextRef.current = context; - const sessionIdRef = useRef(frame.searchSessionId); - sessionIdRef.current = frame.searchSessionId; + const sessionIdRef = useRef(searchSessionId); + sessionIdRef.current = searchSessionId; const AutoRefreshExpressionRenderer = useMemo(() => { return (props: ReactExpressionRendererProps) => ( @@ -312,11 +296,11 @@ export function SuggestionPanel({ // if the staged preview is overwritten by a suggestion, // reset the selected index to "current visualization" because // we are not in transient suggestion state anymore - if (!stagedPreview && lastSelectedSuggestion !== -1) { + if (!existsStagedPreview && lastSelectedSuggestion !== -1) { setLastSelectedSuggestion(-1); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [stagedPreview]); + }, [existsStagedPreview]); if (!activeDatasourceId) { return null; @@ -347,7 +331,7 @@ export function SuggestionPanel({

- {stagedPreview && ( + {existsStagedPreview && (
- {currentVisualizationId && ( + {currentVisualization.activeId && ( >, - datasourceStates: Record, + datasourceMap: DatasourceMap, + datasourceStates: DatasourceStates, framePublicAPI: FramePublicAPI ) { const suggestionDatasourceId = visualizableState.datasourceId; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx index 9b5766c3e3bfa..9e80dcfc47420 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx @@ -11,6 +11,7 @@ import { createMockVisualization, createMockFramePublicAPI, createMockDatasource, + mockDatasourceStates, } from '../../../mocks'; import { mountWithProvider } from '../../../mocks'; @@ -163,15 +164,6 @@ describe('chart_switch', () => { }; } - function mockDatasourceStates() { - return { - testDatasource: { - state: {}, - isLoading: false, - }, - }; - } - function showFlyout(instance: ReactWrapper) { instance.find('[data-test-subj="lnsChartSwitchPopover"]').first().simulate('click'); } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx index 7db639536b8ac..e2036e556a551 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx @@ -35,6 +35,11 @@ import { updateVisualizationState, useLensDispatch, useLensSelector, + VisualizationState, + DatasourceStates, + selectActiveDatasourceId, + selectVisualization, + selectDatasourceStates, } from '../../../state_management'; import { generateId } from '../../../id_generator/id_generator'; @@ -111,9 +116,9 @@ function getCurrentVisualizationId( export const ChartSwitch = memo(function ChartSwitch(props: Props) { const [flyoutOpen, setFlyoutOpen] = useState(false); const dispatchLens = useLensDispatch(); - const activeDatasourceId = useLensSelector((state) => state.lens.activeDatasourceId); - const visualization = useLensSelector((state) => state.lens.visualization); - const datasourceStates = useLensSelector((state) => state.lens.datasourceStates); + const activeDatasourceId = useLensSelector(selectActiveDatasourceId); + const visualization = useLensSelector(selectVisualization); + const datasourceStates = useLensSelector(selectDatasourceStates); function removeLayers(layerIds: string[]) { const activeVisualization = @@ -498,11 +503,8 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) { function getTopSuggestion( props: Props, visualizationId: string, - datasourceStates: Record, - visualization: { - activeId: string | null; - state: unknown; - }, + datasourceStates: DatasourceStates, + visualization: VisualizationState, newVisualization: Visualization, subVisualizationId?: string ): Suggestion | undefined { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index 784455cc9f6d1..e687c3ea44422 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -33,6 +33,7 @@ import { UiActionsStart } from '../../../../../../../src/plugins/ui_actions/publ import { uiActionsPluginMock } from '../../../../../../../src/plugins/ui_actions/public/mocks'; import { TriggerContract } from '../../../../../../../src/plugins/ui_actions/public/triggers'; import { VIS_EVENT_TO_TRIGGER } from '../../../../../../../src/plugins/visualizations/public/embeddable'; +import { LensRootStore, setState } from '../../../state_management'; const defaultPermissions: Record>> = { navLinks: { management: true }, @@ -49,12 +50,8 @@ function createCoreStartWithPermissions(newCapabilities = defaultPermissions) { } const defaultProps = { - activeDatasourceId: 'mock', - datasourceStates: {}, datasourceMap: {}, framePublicAPI: createMockFramePublicAPI(), - activeVisualizationId: 'vis', - visualizationState: {}, ExpressionRenderer: createExpressionRendererMock(), core: createCoreStartWithPermissions(), plugins: { @@ -62,7 +59,6 @@ const defaultProps = { data: mockDataPlugin(), }, getSuggestionForField: () => undefined, - isFullscreen: false, toggleFullscreen: jest.fn(), }; @@ -84,7 +80,7 @@ describe('workspace_panel', () => { uiActionsMock.getTrigger.mockReturnValue(trigger); mockVisualization = createMockVisualization(); mockVisualization2 = createMockVisualization(); - mockDatasource = createMockDatasource('a'); + mockDatasource = createMockDatasource('testDatasource'); expressionRendererMock = createExpressionRendererMock(); }); @@ -96,14 +92,16 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( , - { data: defaultProps.plugins.data } + { + data: defaultProps.plugins.data, + preloadedState: { visualization: { activeId: null, state: {} }, datasourceStates: {} }, + } ); instance = mounted.instance; expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2); @@ -115,11 +113,11 @@ describe('workspace_panel', () => { null }, + testVis: { ...mockVisualization, toExpression: () => null }, }} />, - { data: defaultProps.plugins.data } + { data: defaultProps.plugins.data, preloadedState: { datasourceStates: {} } } ); instance = mounted.instance; @@ -132,11 +130,11 @@ describe('workspace_panel', () => { 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} />, - { data: defaultProps.plugins.data } + { data: defaultProps.plugins.data, preloadedState: { datasourceStates: {} } } ); instance = mounted.instance; @@ -155,18 +153,12 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} ExpressionRenderer={expressionRendererMock} />, @@ -178,7 +170,7 @@ describe('workspace_panel', () => { expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` "kibana | lens_merge_tables layerIds=\\"first\\" tables={datasource} - | vis" + | testVis" `); }); @@ -194,18 +186,12 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} ExpressionRenderer={expressionRendererMock} plugins={{ ...props.plugins, uiActions: uiActionsMock }} @@ -235,18 +221,12 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} ExpressionRenderer={expressionRendererMock} />, @@ -283,28 +263,32 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} ExpressionRenderer={expressionRendererMock} />, - { data: defaultProps.plugins.data } + { + data: defaultProps.plugins.data, + preloadedState: { + datasourceStates: { + testDatasource: { + state: {}, + isLoading: false, + }, + mock2: { + state: {}, + isLoading: false, + }, + }, + }, + } ); instance = mounted.instance; @@ -364,18 +348,12 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} ExpressionRenderer={expressionRendererMock} />, @@ -418,18 +396,12 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} ExpressionRenderer={expressionRendererMock} />, @@ -470,23 +442,27 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} />, - { data: defaultProps.plugins.data } + { + data: defaultProps.plugins.data, + preloadedState: { + datasourceStates: { + testDatasource: { + // define a layer with an indexpattern not available + state: { layers: { indexPatternId: 'a' }, indexPatterns: {} }, + isLoading: false, + }, + }, + }, + } ); instance = mounted.instance; @@ -504,20 +480,12 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} // Use cannot navigate to the management page core={createCoreStartWithPermissions({ @@ -526,7 +494,18 @@ describe('workspace_panel', () => { })} />, - { data: defaultProps.plugins.data } + { + data: defaultProps.plugins.data, + preloadedState: { + datasourceStates: { + testDatasource: { + // define a layer with an indexpattern not available + state: { layers: { indexPatternId: 'a' }, indexPatterns: {} }, + isLoading: false, + }, + }, + }, + } ); instance = mounted.instance; @@ -545,19 +524,12 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} // user can go to management, but indexPatterns management is not accessible core={createCoreStartWithPermissions({ @@ -566,7 +538,18 @@ describe('workspace_panel', () => { })} />, - { data: defaultProps.plugins.data } + { + data: defaultProps.plugins.data, + preloadedState: { + datasourceStates: { + testDatasource: { + // define a layer with an indexpattern not available + state: { layers: { indexPatternId: 'a' }, indexPatterns: {} }, + isLoading: false, + }, + }, + }, + } ); instance = mounted.instance; @@ -588,18 +571,12 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} />, @@ -617,7 +594,7 @@ describe('workspace_panel', () => { mockVisualization.getErrorMessages.mockReturnValue([ { shortMessage: 'Some error happened', longMessage: 'Some long description happened' }, ]); - mockVisualization.toExpression.mockReturnValue('vis'); + mockVisualization.toExpression.mockReturnValue('testVis'); const framePublicAPI = createMockFramePublicAPI(); framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, @@ -626,18 +603,12 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( , @@ -657,7 +628,7 @@ describe('workspace_panel', () => { mockVisualization.getErrorMessages.mockReturnValue([ { shortMessage: 'Some error happened', longMessage: 'Some long description happened' }, ]); - mockVisualization.toExpression.mockReturnValue('vis'); + mockVisualization.toExpression.mockReturnValue('testVis'); const framePublicAPI = createMockFramePublicAPI(); framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, @@ -666,18 +637,12 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( , @@ -703,18 +668,12 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} />, @@ -738,18 +697,12 @@ describe('workspace_panel', () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} ExpressionRenderer={expressionRendererMock} />, @@ -775,23 +728,17 @@ describe('workspace_panel', () => { framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, }; - + let lensStore: LensRootStore; await act(async () => { const mounted = await mountWithProvider( 'vis' }, + testVis: { ...mockVisualization, toExpression: () => 'testVis' }, }} ExpressionRenderer={expressionRendererMock} />, @@ -799,6 +746,7 @@ describe('workspace_panel', () => { { data: defaultProps.plugins.data } ); instance = mounted.instance; + lensStore = mounted.lensStore; }); instance.update(); @@ -809,7 +757,14 @@ describe('workspace_panel', () => { return ; }); - instance.setProps({ visualizationState: {} }); + lensStore!.dispatch( + setState({ + visualization: { + activeId: 'testVis', + state: {}, + }, + }) + ); instance.update(); expect(expressionRendererMock).toHaveBeenCalledTimes(2); @@ -843,18 +798,12 @@ describe('workspace_panel', () => { > { it('should immediately transition if exactly one suggestion is returned', async () => { mockGetSuggestionForField.mockReturnValue({ - visualizationId: 'vis', + visualizationId: 'testVis', datasourceState: {}, - datasourceId: 'mock', + datasourceId: 'testDatasource', visualizationState: {}, }); const { lensStore } = await initComponent(); @@ -879,19 +828,19 @@ describe('workspace_panel', () => { expect(lensStore.dispatch).toHaveBeenCalledWith({ type: 'lens/switchVisualization', payload: { - newVisualizationId: 'vis', + newVisualizationId: 'testVis', initialState: {}, datasourceState: {}, - datasourceId: 'mock', + datasourceId: 'testDatasource', }, }); }); it('should allow to drop if there are suggestions', async () => { mockGetSuggestionForField.mockReturnValue({ - visualizationId: 'vis', + visualizationId: 'testVis', datasourceState: {}, - datasourceId: 'mock', + datasourceId: 'testDatasource', visualizationState: {}, }); await initComponent(); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index 68243efc28d07..8b49c72b3cffa 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -40,6 +40,7 @@ import { isLensEditEvent, VisualizationMap, DatasourceMap, + DatasourceFixAction, } from '../../../types'; import { DragDrop, DragContext, DragDropIdentifier } from '../../../drag_drop'; import { Suggestion, switchToSuggestion } from '../suggestion_helpers'; @@ -58,34 +59,30 @@ import { updateVisualizationState, updateDatasourceState, setSaveable, + useLensSelector, + selectExecutionContext, + selectIsFullscreenDatasource, + selectVisualization, + selectDatasourceStates, + selectActiveDatasourceId, + selectSearchSessionId, } from '../../../state_management'; export interface WorkspacePanelProps { - activeVisualizationId: string | null; visualizationMap: VisualizationMap; - visualizationState: unknown; - activeDatasourceId: string | null; datasourceMap: DatasourceMap; - datasourceStates: Record< - string, - { - state: unknown; - isLoading: boolean; - } - >; framePublicAPI: FramePublicAPI; ExpressionRenderer: ReactExpressionRendererType; core: CoreStart; plugins: { uiActions?: UiActionsStart; data: DataPublicPluginStart }; getSuggestionForField: (field: DragDropIdentifier) => Suggestion | undefined; - isFullscreen: boolean; } interface WorkspaceState { expressionBuildError?: Array<{ shortMessage: string; longMessage: string; - fixAction?: { label: string; newState: (framePublicAPI: FramePublicAPI) => Promise }; + fixAction?: DatasourceFixAction; }>; expandError: boolean; } @@ -120,29 +117,30 @@ export const WorkspacePanel = React.memo(function WorkspacePanel(props: Workspac // Exported for testing purposes only. export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ - activeDatasourceId, - activeVisualizationId, + framePublicAPI, visualizationMap, - visualizationState, datasourceMap, - datasourceStates, - framePublicAPI, core, plugins, ExpressionRenderer: ExpressionRendererComponent, suggestionForDraggedField, - isFullscreen, }: Omit & { suggestionForDraggedField: Suggestion | undefined; }) { const dispatchLens = useLensDispatch(); + const isFullscreen = useLensSelector(selectIsFullscreenDatasource); + const visualization = useLensSelector(selectVisualization); + const activeDatasourceId = useLensSelector(selectActiveDatasourceId); + const datasourceStates = useLensSelector(selectDatasourceStates); + + const { datasourceLayers } = framePublicAPI; const [localState, setLocalState] = useState({ expressionBuildError: undefined, expandError: false, }); - const activeVisualization = activeVisualizationId - ? visualizationMap[activeVisualizationId] + const activeVisualization = visualization.activeId + ? visualizationMap[visualization.activeId] : null; const missingIndexPatterns = getMissingIndexPattern( @@ -175,64 +173,60 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ activeDatasourceId ? datasourceMap[activeDatasourceId] : null, activeDatasourceId && datasourceStates[activeDatasourceId]?.state, activeVisualization, - visualizationState, + visualization.state, framePublicAPI ), // eslint-disable-next-line react-hooks/exhaustive-deps - [activeVisualization, visualizationState, activeDatasourceId, datasourceMap, datasourceStates] + [activeVisualization, visualization.state, activeDatasourceId, datasourceMap, datasourceStates] ); - const expression = useMemo( - () => { - if (!configurationValidationError?.length && !missingRefsErrors.length) { - try { - const ast = buildExpression({ - visualization: activeVisualization, - visualizationState, - datasourceMap, - datasourceStates, - datasourceLayers: framePublicAPI.datasourceLayers, - }); - - if (ast) { - // expression has to be turned into a string for dirty checking - if the ast is rebuilt, - // turning it into a string will make sure the expression renderer only re-renders if the - // expression actually changed. - return toExpression(ast); - } else { - return null; - } - } catch (e) { - const buildMessages = activeVisualization?.getErrorMessages(visualizationState); - const defaultMessage = { - shortMessage: i18n.translate('xpack.lens.editorFrame.buildExpressionError', { - defaultMessage: 'An unexpected error occurred while preparing the chart', - }), - longMessage: e.toString(), - }; - // Most likely an error in the expression provided by a datasource or visualization - setLocalState((s) => ({ - ...s, - expressionBuildError: buildMessages ?? [defaultMessage], - })); + const expression = useMemo(() => { + if (!configurationValidationError?.length && !missingRefsErrors.length) { + try { + const ast = buildExpression({ + visualization: activeVisualization, + visualizationState: visualization.state, + datasourceMap, + datasourceStates, + datasourceLayers, + }); + + if (ast) { + // expression has to be turned into a string for dirty checking - if the ast is rebuilt, + // turning it into a string will make sure the expression renderer only re-renders if the + // expression actually changed. + return toExpression(ast); + } else { + return null; } + } catch (e) { + const buildMessages = activeVisualization?.getErrorMessages(visualization.state); + const defaultMessage = { + shortMessage: i18n.translate('xpack.lens.editorFrame.buildExpressionError', { + defaultMessage: 'An unexpected error occurred while preparing the chart', + }), + longMessage: e.toString(), + }; + // Most likely an error in the expression provided by a datasource or visualization + setLocalState((s) => ({ + ...s, + expressionBuildError: buildMessages ?? [defaultMessage], + })); } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - activeVisualization, - visualizationState, - datasourceMap, - datasourceStates, - framePublicAPI.dateRange, - framePublicAPI.query, - framePublicAPI.filters, - ] - ); + } + }, [ + activeVisualization, + visualization.state, + datasourceMap, + datasourceStates, + datasourceLayers, + configurationValidationError?.length, + missingRefsErrors.length, + ]); const expressionExists = Boolean(expression); const hasLoaded = Boolean( - activeVisualization && visualizationState && datasourceMap && datasourceStates + activeVisualization && visualization.state && datasourceMap && datasourceStates ); useEffect(() => { if (hasLoaded) { @@ -392,8 +386,8 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ return ( Promise }; + fixAction?: DatasourceFixAction; }>; missingRefsErrors?: Array<{ shortMessage: string; longMessage: string }>; }; @@ -432,22 +426,19 @@ export const VisualizationWrapper = ({ application: ApplicationStart; activeDatasourceId: string | null; }) => { - const context: ExecutionContextSearch = useMemo( + const context = useLensSelector(selectExecutionContext); + const searchContext: ExecutionContextSearch = useMemo( () => ({ - query: framePublicAPI.query, + query: context.query, timeRange: { - from: framePublicAPI.dateRange.fromDate, - to: framePublicAPI.dateRange.toDate, + from: context.dateRange.fromDate, + to: context.dateRange.toDate, }, - filters: framePublicAPI.filters, + filters: context.filters, }), - [ - framePublicAPI.query, - framePublicAPI.dateRange.fromDate, - framePublicAPI.dateRange.toDate, - framePublicAPI.filters, - ] + [context] ); + const searchSessionId = useLensSelector(selectSearchSessionId); const dispatchLens = useLensDispatch(); @@ -465,9 +456,7 @@ export const VisualizationWrapper = ({ | { shortMessage: string; longMessage: string; - fixAction?: - | { label: string; newState: (framePublicAPI: FramePublicAPI) => Promise } - | undefined; + fixAction?: DatasourceFixAction; } | undefined ) { @@ -480,7 +469,10 @@ export const VisualizationWrapper = ({ data-test-subj="errorFixAction" onClick={async () => { trackUiEvent('error_fix_action'); - const newState = await validationError.fixAction?.newState(framePublicAPI); + const newState = await validationError.fixAction?.newState({ + ...framePublicAPI, + ...context, + }); dispatchLens( updateDatasourceState({ updater: newState, @@ -638,8 +630,8 @@ export const VisualizationWrapper = ({ className="lnsExpressionRenderer__component" padding="m" expression={expression!} - searchContext={context} - searchSessionId={framePublicAPI.searchSessionId} + searchContext={searchContext} + searchSessionId={searchSessionId} onEvent={onEvent} onData$={onData$} renderMode="edit" diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx index 057d8c8baebfa..e1cb1aeb9f825 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel_wrapper.tsx @@ -14,23 +14,22 @@ import { DatasourceMap, FramePublicAPI, VisualizationMap } from '../../../types' import { NativeRenderer } from '../../../native_renderer'; import { ChartSwitch } from './chart_switch'; import { WarningsPopover } from './warnings_popover'; -import { useLensDispatch, updateVisualizationState } from '../../../state_management'; +import { + useLensDispatch, + updateVisualizationState, + DatasourceStates, + VisualizationState, +} from '../../../state_management'; import { WorkspaceTitle } from './title'; export interface WorkspacePanelWrapperProps { children: React.ReactNode | React.ReactNode[]; framePublicAPI: FramePublicAPI; - visualizationState: unknown; + visualizationState: VisualizationState['state']; visualizationMap: VisualizationMap; visualizationId: string | null; datasourceMap: DatasourceMap; - datasourceStates: Record< - string, - { - isLoading: boolean; - state: unknown; - } - >; + datasourceStates: DatasourceStates; isFullscreen: boolean; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index 897777a05efc6..a8ab6ef943b64 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -50,7 +50,7 @@ import { FormulaIndexPatternColumn, } from './formula'; import { lastValueOperation, LastValueIndexPatternColumn } from './last_value'; -import { FramePublicAPI, OperationMetadata } from '../../../types'; +import { FrameDatasourceAPI, OperationMetadata } from '../../../types'; import type { BaseIndexPatternColumn, ReferenceBasedIndexPatternColumn } from './column_types'; import { IndexPattern, IndexPatternField, IndexPatternLayer } from '../../types'; import { DateRange } from '../../../../common'; @@ -280,7 +280,7 @@ interface BaseOperationDefinitionProps { label: string; newState: ( core: CoreStart, - frame: FramePublicAPI, + frame: FrameDatasourceAPI, layerId: string ) => Promise; }; @@ -437,7 +437,7 @@ interface FieldBasedOperationDefinition { label: string; newState: ( core: CoreStart, - frame: FramePublicAPI, + frame: FrameDatasourceAPI, layerId: string ) => Promise; }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index 4e2f69c927a18..cfe190261b53b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -22,7 +22,7 @@ import { FieldStatsResponse } from '../../../../../common'; import { AggFunctionsMapping, esQuery } from '../../../../../../../../src/plugins/data/public'; import { buildExpressionFunction } from '../../../../../../../../src/plugins/expressions/public'; import { updateColumnParam, isReferenced } from '../../layer_helpers'; -import { DataType, FramePublicAPI } from '../../../../types'; +import { DataType, FrameDatasourceAPI } from '../../../../types'; import { FiltersIndexPatternColumn, OperationDefinition, operationDefinitionMap } from '../index'; import { FieldBasedIndexPatternColumn } from '../column_types'; import { ValuesInput } from './values_input'; @@ -77,7 +77,7 @@ function getDisallowedTermsMessage( label: i18n.translate('xpack.lens.indexPattern.termsWithMultipleShiftsFixActionLabel', { defaultMessage: 'Use filters', }), - newState: async (core: CoreStart, frame: FramePublicAPI, layerId: string) => { + newState: async (core: CoreStart, frame: FrameDatasourceAPI, layerId: string) => { const currentColumn = layer.columns[columnId] as TermsIndexPatternColumn; const fieldName = currentColumn.sourceField; const activeDataFieldNameMatch = diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index f326f3e3ed5f6..9d5959c807490 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -22,7 +22,7 @@ import { ValuesInput } from './values_input'; import type { TermsIndexPatternColumn } from '.'; import { termsOperation } from '../index'; import { IndexPattern, IndexPatternLayer } from '../../../types'; -import { FramePublicAPI } from '../../../../types'; +import { FrameDatasourceAPI } from '../../../../types'; const uiSettingsMock = {} as IUiSettingsClient; @@ -1110,7 +1110,7 @@ describe('terms', () => { fromDate: '2020', toDate: '2021', }, - } as unknown) as FramePublicAPI, + } as unknown) as FrameDatasourceAPI, 'first' ); expect(newLayer.columns.col1).toEqual( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index 1e0d0792e132a..232843171016a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -9,7 +9,8 @@ import { partition, mapValues, pickBy } from 'lodash'; import { CoreStart } from 'kibana/public'; import { Query } from 'src/plugins/data/common'; import type { - FramePublicAPI, + DatasourceFixAction, + FrameDatasourceAPI, OperationMetadata, VisualizationDimensionGroupConfig, } from '../../types'; @@ -1249,10 +1250,7 @@ export function getErrorMessages( | string | { message: string; - fixAction?: { - label: string; - newState: (frame: FramePublicAPI) => Promise; - }; + fixAction?: DatasourceFixAction; } > | undefined { @@ -1284,7 +1282,7 @@ export function getErrorMessages( fixAction: errorMessage.fixAction ? { ...errorMessage.fixAction, - newState: async (frame: FramePublicAPI) => ({ + newState: async (frame: FrameDatasourceAPI) => ({ ...state, layers: { ...state.layers, @@ -1300,10 +1298,7 @@ export function getErrorMessages( | string | { message: string; - fixAction?: { - label: string; - newState: (framePublicAPI: FramePublicAPI) => Promise; - }; + fixAction?: DatasourceFixAction; } >; diff --git a/x-pack/plugins/lens/public/mocks.tsx b/x-pack/plugins/lens/public/mocks.tsx index 3c598540cbb93..9258c94ad2ab4 100644 --- a/x-pack/plugins/lens/public/mocks.tsx +++ b/x-pack/plugins/lens/public/mocks.tsx @@ -39,7 +39,22 @@ import { EmbeddableStateTransfer } from '../../../../src/plugins/embeddable/publ import { makeConfigureStore, LensAppState, LensState } from './state_management/index'; import { getResolvedDateRange } from './utils'; import { presentationUtilPluginMock } from '../../../../src/plugins/presentation_util/public/mocks'; -import { DatasourcePublicAPI, Datasource, Visualization, FramePublicAPI } from './types'; +import { + DatasourcePublicAPI, + Datasource, + Visualization, + FramePublicAPI, + FrameDatasourceAPI, +} from './types'; + +export function mockDatasourceStates() { + return { + testDatasource: { + state: {}, + isLoading: false, + }, + }; +} export function createMockVisualization(): jest.Mocked { return { @@ -83,9 +98,9 @@ export function createMockVisualization(): jest.Mocked { }; } -const visualizationMap = { - vis: createMockVisualization(), - vis2: createMockVisualization(), +export const visualizationMap = { + testVis: createMockVisualization(), + testVis2: createMockVisualization(), }; export type DatasourceMock = jest.Mocked & { @@ -134,7 +149,7 @@ export function createMockDatasource(id: string): DatasourceMock { const mockDatasource: DatasourceMock = createMockDatasource('testDatasource'); const mockDatasource2: DatasourceMock = createMockDatasource('testDatasource2'); -const datasourceMap = { +export const datasourceMap = { testDatasource2: mockDatasource2, testDatasource: mockDatasource, }; @@ -148,12 +163,18 @@ export function createExpressionRendererMock(): jest.Mock< export type FrameMock = jest.Mocked; export function createMockFramePublicAPI(): FrameMock { + return { + datasourceLayers: {}, + }; +} + +export type FrameDatasourceMock = jest.Mocked; +export function createMockFrameDatasourceAPI(): FrameDatasourceMock { return { datasourceLayers: {}, dateRange: { fromDate: 'now-7d', toDate: 'now' }, query: { query: '', language: 'lucene' }, filters: [], - searchSessionId: 'sessionId', }; } @@ -393,12 +414,7 @@ export const defaultState = { state: {}, activeId: 'testVis', }, - datasourceStates: { - testDatasource: { - isLoading: false, - state: '', - }, - }, + datasourceStates: mockDatasourceStates(), }; export function makeLensStore({ diff --git a/x-pack/plugins/lens/public/state_management/index.ts b/x-pack/plugins/lens/public/state_management/index.ts index b06dc73857cff..a23a040de2361 100644 --- a/x-pack/plugins/lens/public/state_management/index.ts +++ b/x-pack/plugins/lens/public/state_management/index.ts @@ -14,6 +14,7 @@ import { optimizingMiddleware } from './optimizing_middleware'; import { LensState, LensStoreDeps } from './types'; import { initMiddleware } from './init_middleware'; export * from './types'; +export * from './selectors'; export const reducer = { lens: lensSlice.reducer, diff --git a/x-pack/plugins/lens/public/state_management/selectors.ts b/x-pack/plugins/lens/public/state_management/selectors.ts new file mode 100644 index 0000000000000..360ca48c2d279 --- /dev/null +++ b/x-pack/plugins/lens/public/state_management/selectors.ts @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createSelector } from '@reduxjs/toolkit'; +import { SavedObjectReference } from 'kibana/server'; +import { LensState } from './types'; +import { extractFilterReferences } from '../persistence'; +import { Datasource, DatasourceMap, VisualizationMap } from '../types'; +import { createDatasourceLayers } from '../editor_frame_service/editor_frame'; + +export const selectPersistedDoc = (state: LensState) => state.lens.persistedDoc; +export const selectQuery = (state: LensState) => state.lens.query; +export const selectSearchSessionId = (state: LensState) => state.lens.searchSessionId; +export const selectFilters = (state: LensState) => state.lens.filters; +export const selectResolvedDateRange = (state: LensState) => state.lens.resolvedDateRange; +export const selectVisualization = (state: LensState) => state.lens.visualization; +export const selectStagedPreview = (state: LensState) => state.lens.stagedPreview; +export const selectDatasourceStates = (state: LensState) => state.lens.datasourceStates; +export const selectActiveDatasourceId = (state: LensState) => state.lens.activeDatasourceId; +export const selectActiveData = (state: LensState) => state.lens.activeData; +export const selectIsFullscreenDatasource = (state: LensState) => + Boolean(state.lens.isFullscreenDatasource); + +export const selectExecutionContext = createSelector( + [selectQuery, selectFilters, selectResolvedDateRange], + (query, filters, dateRange) => ({ + dateRange, + query, + filters, + }) +); + +export const selectExecutionContextSearch = createSelector(selectExecutionContext, (res) => ({ + query: res.query, + timeRange: { + from: res.dateRange.fromDate, + to: res.dateRange.toDate, + }, + filters: res.filters, +})); + +const selectDatasourceMap = (state: LensState, datasourceMap: DatasourceMap) => datasourceMap; + +const selectVisualizationMap = ( + state: LensState, + datasourceMap: DatasourceMap, + visualizationMap: VisualizationMap +) => visualizationMap; + +export const selectSavedObjectFormat = createSelector( + [ + selectPersistedDoc, + selectVisualization, + selectDatasourceStates, + selectQuery, + selectFilters, + selectActiveDatasourceId, + selectDatasourceMap, + selectVisualizationMap, + ], + ( + persistedDoc, + visualization, + datasourceStates, + query, + filters, + activeDatasourceId, + datasourceMap, + visualizationMap + ) => { + const activeVisualization = + visualization.state && visualization.activeId && visualizationMap[visualization.activeId]; + const activeDatasource = + datasourceStates && activeDatasourceId && !datasourceStates[activeDatasourceId].isLoading + ? datasourceMap[activeDatasourceId] + : undefined; + + if (!activeDatasource || !activeVisualization) { + return; + } + + const activeDatasources: Record = Object.keys(datasourceStates).reduce( + (acc, datasourceId) => ({ + ...acc, + [datasourceId]: datasourceMap[datasourceId], + }), + {} + ); + + const persistibleDatasourceStates: Record = {}; + const references: SavedObjectReference[] = []; + Object.entries(activeDatasources).forEach(([id, datasource]) => { + const { state: persistableState, savedObjectReferences } = datasource.getPersistableState( + datasourceStates[id].state + ); + persistibleDatasourceStates[id] = persistableState; + references.push(...savedObjectReferences); + }); + + const { persistableFilters, references: filterReferences } = extractFilterReferences(filters); + + references.push(...filterReferences); + + return { + savedObjectId: persistedDoc?.savedObjectId, + title: persistedDoc?.title || '', + description: persistedDoc?.description, + visualizationType: visualization.activeId, + type: 'lens', + references, + state: { + visualization: visualization.state, + query, + filters: persistableFilters, + datasourceStates: persistibleDatasourceStates, + }, + }; + } +); + +export const selectCurrentVisualization = createSelector( + [selectVisualization, selectStagedPreview], + (visualization, stagedPreview) => (stagedPreview ? stagedPreview.visualization : visualization) +); + +export const selectCurrentDatasourceStates = createSelector( + [selectDatasourceStates, selectStagedPreview], + (datasourceStates, stagedPreview) => + stagedPreview ? stagedPreview.datasourceStates : datasourceStates +); + +export const selectAreDatasourcesLoaded = createSelector( + selectDatasourceStates, + (datasourceStates) => + Object.values(datasourceStates).every(({ isLoading }) => isLoading === false) +); + +export const selectDatasourceLayers = createSelector( + [selectDatasourceStates, selectDatasourceMap], + (datasourceStates, datasourceMap) => createDatasourceLayers(datasourceStates, datasourceMap) +); + +export const selectFramePublicAPI = createSelector( + [selectDatasourceStates, selectActiveData, selectDatasourceMap], + (datasourceStates, activeData, datasourceMap) => ({ + datasourceLayers: createDatasourceLayers(datasourceStates, datasourceMap), + activeData, + }) +); diff --git a/x-pack/plugins/lens/public/state_management/time_range_middleware.ts b/x-pack/plugins/lens/public/state_management/time_range_middleware.ts index cc3e46b71fbfc..386e0d88e1814 100644 --- a/x-pack/plugins/lens/public/state_management/time_range_middleware.ts +++ b/x-pack/plugins/lens/public/state_management/time_range_middleware.ts @@ -10,7 +10,10 @@ import moment from 'moment'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { setState, LensDispatch } from '.'; import { LensAppState } from './types'; -import { getResolvedDateRange, containsDynamicMath, TIME_LAG_PERCENTAGE_LIMIT } from '../utils'; +import { getResolvedDateRange, containsDynamicMath } from '../utils'; + +const TIME_LAG_PERCENTAGE_LIMIT = 0.02; +const TIME_LAG_MIN_LIMIT = 10000; // for a small timerange to avoid infinite data refresh timelag minimum is TIME_LAG_ABSOLUTE ms /** * checks if TIME_LAG_PERCENTAGE_LIMIT passed to renew searchSessionId @@ -48,8 +51,8 @@ function updateTimeRange(data: DataPublicPluginStart, dispatch: LensDispatch) { // calculate lag of managed "now" for date math const nowDiff = Date.now() - data.nowProvider.get().valueOf(); - // if the lag is signifcant, start a new session to clear the cache - if (nowDiff > timeRangeLength * TIME_LAG_PERCENTAGE_LIMIT) { + // if the lag is significant, start a new session to clear the cache + if (nowDiff > Math.max(timeRangeLength * TIME_LAG_PERCENTAGE_LIMIT, TIME_LAG_MIN_LIMIT)) { dispatch( setState({ searchSessionId: data.search.session.start(), diff --git a/x-pack/plugins/lens/public/state_management/types.ts b/x-pack/plugins/lens/public/state_management/types.ts index 8816ef27238c2..7321f72386b42 100644 --- a/x-pack/plugins/lens/public/state_management/types.ts +++ b/x-pack/plugins/lens/public/state_management/types.ts @@ -15,12 +15,15 @@ import { DateRange } from '../../common'; import { LensAppServices } from '../app_plugin/types'; import { DatasourceMap, VisualizationMap } from '../types'; +export interface VisualizationState { + activeId: string | null; + state: unknown; +} + +export type DatasourceStates = Record; export interface PreviewState { - visualization: { - activeId: string | null; - state: unknown; - }; - datasourceStates: Record; + visualization: VisualizationState; + datasourceStates: DatasourceStates; } export interface EditorFrameState extends PreviewState { activeDatasourceId: string | null; diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 91b16d2bcbc16..db17154e3bbd2 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -256,6 +256,11 @@ export interface Datasource { getWarningMessages?: (state: T, frame: FramePublicAPI) => React.ReactNode[] | undefined; } +export interface DatasourceFixAction { + label: string; + newState: (frame: FrameDatasourceAPI) => Promise; +} + /** * This is an API provided to visualizations by the frame, which calls the publicAPI on the datasource */ @@ -516,10 +521,11 @@ export interface FramePublicAPI { * If accessing, make sure to check whether expected columns actually exist. */ activeData?: Record; +} +export interface FrameDatasourceAPI extends FramePublicAPI { dateRange: DateRange; query: Query; filters: Filter[]; - searchSessionId: string; } /** diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index b8ee5e4a0c26b..b7dd3ed3733cf 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -10,11 +10,10 @@ import { IndexPattern, IndexPatternsContract, TimefilterContract } from 'src/plu import { IUiSettingsClient } from 'kibana/public'; import moment from 'moment-timezone'; import { SavedObjectReference } from 'kibana/public'; -import { Filter, Query } from 'src/plugins/data/public'; import { uniq } from 'lodash'; import { Document } from './persistence/saved_object_store'; import { Datasource, DatasourceMap } from './types'; -import { extractFilterReferences } from './persistence'; +import { DatasourceStates } from './state_management'; export function getVisualizeGeoFieldMessage(fieldType: string) { return i18n.translate('xpack.lens.visualizeGeoFieldMessage', { @@ -36,8 +35,6 @@ export function containsDynamicMath(dateMathString: string) { return dateMathString.includes('now'); } -export const TIME_LAG_PERCENTAGE_LIMIT = 0.02; - export function getTimeZone(uiSettings: IUiSettingsClient) { const configuredTimeZone = uiSettings.get('dateFormat:tz'); if (configuredTimeZone === 'Browser') { @@ -59,66 +56,12 @@ export const getInitialDatasourceId = (datasourceMap: DatasourceMap, doc?: Docum return (doc && getActiveDatasourceIdFromDoc(doc)) || Object.keys(datasourceMap)[0] || null; }; -export interface GetIndexPatternsObjects { - activeDatasources: Record; - datasourceStates: Record; - visualization: { - activeId: string | null; - state: unknown; - }; - filters: Filter[]; - query: Query; - title: string; - description?: string; - persistedId?: string; -} - -export function getSavedObjectFormat({ - activeDatasources, - datasourceStates, - visualization, - filters, - query, - title, - description, - persistedId, -}: GetIndexPatternsObjects): Document { - const persistibleDatasourceStates: Record = {}; - const references: SavedObjectReference[] = []; - Object.entries(activeDatasources).forEach(([id, datasource]) => { - const { state: persistableState, savedObjectReferences } = datasource.getPersistableState( - datasourceStates[id].state - ); - persistibleDatasourceStates[id] = persistableState; - references.push(...savedObjectReferences); - }); - - const { persistableFilters, references: filterReferences } = extractFilterReferences(filters); - - references.push(...filterReferences); - - return { - savedObjectId: persistedId, - title, - description, - type: 'lens', - visualizationType: visualization.activeId, - state: { - datasourceStates: persistibleDatasourceStates, - visualization: visualization.state, - query, - filters: persistableFilters, - }, - references, - }; -} - export function getIndexPatternsIds({ activeDatasources, datasourceStates, }: { activeDatasources: Record; - datasourceStates: Record; + datasourceStates: DatasourceStates; }): string[] { const references: SavedObjectReference[] = []; Object.entries(activeDatasources).forEach(([id, datasource]) => { From 541b19201a56dfb100d516a4371cac7c03c94b44 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Tue, 10 Aug 2021 13:12:07 -0600 Subject: [PATCH 039/104] [Security Solution][Detections] Updates MITRE Tactics, Techniques, and Subtechniques to v9.0 (#107708) ## Summary Detection rules updated the MITRE ATT&CK mappings to v9.0 in https://github.com/elastic/detection-rules/pull/1401, so updating on the kibana side to ensure compatibility with latest ruleset. To update, I modified https://github.com/elastic/kibana/blob/4584a8b570402aa07832cf3e5b520e5d2cfa7166/x-pack/plugins/security_solution/scripts/extract_tactics_techniques_mitre.js#L18-L19 to point to the `ATT&CK-v9.0` tag: ``` https://raw.githubusercontent.com/mitre/cti/ATT&CK-v9.0/enterprise-attack/enterprise-attack.json ``` Then ran `yarn extract-mitre-attacks` from the root `security_solution` plugin directory. ### Checklist Delete any items that are not applicable to this PR. - [X] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) --- .../mitre/mitre_tactics_techniques.ts | 834 +++++++++++++----- .../translations/translations/ja-JP.json | 5 - .../translations/translations/zh-CN.json | 5 - 3 files changed, 619 insertions(+), 225 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts b/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts index f28311d9c96e7..3009ad8cdd018 100644 --- a/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts +++ b/x-pack/plugins/security_solution/public/detections/mitre/mitre_tactics_techniques.ts @@ -340,6 +340,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1110', tactics: ['credential-access'], }, + { + name: 'Build Image on Host', + id: 'T1612', + reference: 'https://attack.mitre.org/techniques/T1612', + tactics: ['defense-evasion'], + }, { name: 'Clipboard Data', id: 'T1115', @@ -406,6 +412,18 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1584', tactics: ['resource-development'], }, + { + name: 'Container Administration Command', + id: 'T1609', + reference: 'https://attack.mitre.org/techniques/T1609', + tactics: ['execution'], + }, + { + name: 'Container and Resource Discovery', + id: 'T1613', + reference: 'https://attack.mitre.org/techniques/T1613', + tactics: ['discovery'], + }, { name: 'Create Account', id: 'T1136', @@ -514,6 +532,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1140', tactics: ['defense-evasion'], }, + { + name: 'Deploy Container', + id: 'T1610', + reference: 'https://attack.mitre.org/techniques/T1610', + tactics: ['defense-evasion', 'execution'], + }, { name: 'Develop Capabilities', id: 'T1587', @@ -532,6 +556,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1561', tactics: ['impact'], }, + { + name: 'Domain Policy Modification', + id: 'T1484', + reference: 'https://attack.mitre.org/techniques/T1484', + tactics: ['defense-evasion', 'privilege-escalation'], + }, { name: 'Domain Trust Discovery', id: 'T1482', @@ -568,6 +598,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1499', tactics: ['impact'], }, + { + name: 'Escape to Host', + id: 'T1611', + reference: 'https://attack.mitre.org/techniques/T1611', + tactics: ['privilege-escalation'], + }, { name: 'Establish Accounts', id: 'T1585', @@ -688,6 +724,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1187', tactics: ['credential-access'], }, + { + name: 'Forge Web Credentials', + id: 'T1606', + reference: 'https://attack.mitre.org/techniques/T1606', + tactics: ['credential-access'], + }, { name: 'Gather Victim Host Information', id: 'T1592', @@ -749,7 +791,7 @@ export const technique = [ tactics: ['defense-evasion'], }, { - name: 'Implant Container Image', + name: 'Implant Internal Image', id: 'T1525', reference: 'https://attack.mitre.org/techniques/T1525', tactics: ['persistence'], @@ -830,7 +872,7 @@ export const technique = [ name: 'Modify Authentication Process', id: 'T1556', reference: 'https://attack.mitre.org/techniques/T1556', - tactics: ['credential-access', 'defense-evasion'], + tactics: ['credential-access', 'defense-evasion', 'persistence'], }, { name: 'Modify Cloud Compute Infrastructure', @@ -1162,6 +1204,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1153', tactics: ['execution'], }, + { + name: 'Stage Capabilities', + id: 'T1608', + reference: 'https://attack.mitre.org/techniques/T1608', + tactics: ['resource-development'], + }, { name: 'Steal Application Access Token', id: 'T1528', @@ -1198,6 +1246,12 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1082', tactics: ['discovery'], }, + { + name: 'System Location Discovery', + id: 'T1614', + reference: 'https://attack.mitre.org/techniques/T1614', + tactics: ['discovery'], + }, { name: 'System Network Configuration Discovery', id: 'T1016', @@ -1348,18 +1402,6 @@ export const technique = [ reference: 'https://attack.mitre.org/techniques/T1220', tactics: ['defense-evasion'], }, - { - name: 'Domain Policy Modification', - id: 'T1484', - reference: 'https://attack.mitre.org/techniques/T1484', - tactics: ['defense-evasion', 'privilege-escalation'], - }, - { - name: 'Forge Web Credentials', - id: 'T1606', - reference: 'https://attack.mitre.org/techniques/T1606', - tactics: ['credential-access'], - }, ]; export const techniquesOptions: MitreTechniquesOptions[] = [ @@ -1572,6 +1614,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'credential-access', value: 'bruteForce', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.buildImageOnHostDescription', + { defaultMessage: 'Build Image on Host (T1612)' } + ), + id: 'T1612', + name: 'Build Image on Host', + reference: 'https://attack.mitre.org/techniques/T1612', + tactics: 'defense-evasion', + value: 'buildImageOnHost', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.clipboardDataDescription', @@ -1693,6 +1746,28 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'resource-development', value: 'compromiseInfrastructure', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.containerAdministrationCommandDescription', + { defaultMessage: 'Container Administration Command (T1609)' } + ), + id: 'T1609', + name: 'Container Administration Command', + reference: 'https://attack.mitre.org/techniques/T1609', + tactics: 'execution', + value: 'containerAdministrationCommand', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.containerAndResourceDiscoveryDescription', + { defaultMessage: 'Container and Resource Discovery (T1613)' } + ), + id: 'T1613', + name: 'Container and Resource Discovery', + reference: 'https://attack.mitre.org/techniques/T1613', + tactics: 'discovery', + value: 'containerAndResourceDiscovery', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.createAccountDescription', @@ -1891,6 +1966,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'defense-evasion', value: 'deobfuscateDecodeFilesOrInformation', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.deployContainerDescription', + { defaultMessage: 'Deploy Container (T1610)' } + ), + id: 'T1610', + name: 'Deploy Container', + reference: 'https://attack.mitre.org/techniques/T1610', + tactics: 'defense-evasion,execution', + value: 'deployContainer', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.developCapabilitiesDescription', @@ -1924,6 +2010,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'impact', value: 'diskWipe', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.domainPolicyModificationDescription', + { defaultMessage: 'Domain Policy Modification (T1484)' } + ), + id: 'T1484', + name: 'Domain Policy Modification', + reference: 'https://attack.mitre.org/techniques/T1484', + tactics: 'defense-evasion,privilege-escalation', + value: 'domainPolicyModification', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.domainTrustDiscoveryDescription', @@ -1990,6 +2087,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'impact', value: 'endpointDenialOfService', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.escapeToHostDescription', + { defaultMessage: 'Escape to Host (T1611)' } + ), + id: 'T1611', + name: 'Escape to Host', + reference: 'https://attack.mitre.org/techniques/T1611', + tactics: 'privilege-escalation', + value: 'escapeToHost', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.establishAccountsDescription', @@ -2210,6 +2318,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'credential-access', value: 'forcedAuthentication', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.forgeWebCredentialsDescription', + { defaultMessage: 'Forge Web Credentials (T1606)' } + ), + id: 'T1606', + name: 'Forge Web Credentials', + reference: 'https://attack.mitre.org/techniques/T1606', + tactics: 'credential-access', + value: 'forgeWebCredentials', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.gatherVictimHostInformationDescription', @@ -2322,14 +2441,14 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.implantContainerImageDescription', - { defaultMessage: 'Implant Container Image (T1525)' } + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.implantInternalImageDescription', + { defaultMessage: 'Implant Internal Image (T1525)' } ), id: 'T1525', - name: 'Implant Container Image', + name: 'Implant Internal Image', reference: 'https://attack.mitre.org/techniques/T1525', tactics: 'persistence', - value: 'implantContainerImage', + value: 'implantInternalImage', }, { label: i18n.translate( @@ -2471,7 +2590,7 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ id: 'T1556', name: 'Modify Authentication Process', reference: 'https://attack.mitre.org/techniques/T1556', - tactics: 'credential-access,defense-evasion', + tactics: 'credential-access,defense-evasion,persistence', value: 'modifyAuthenticationProcess', }, { @@ -3079,6 +3198,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'execution', value: 'source', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.stageCapabilitiesDescription', + { defaultMessage: 'Stage Capabilities (T1608)' } + ), + id: 'T1608', + name: 'Stage Capabilities', + reference: 'https://attack.mitre.org/techniques/T1608', + tactics: 'resource-development', + value: 'stageCapabilities', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.stealApplicationAccessTokenDescription', @@ -3145,6 +3275,17 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'discovery', value: 'systemInformationDiscovery', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemLocationDiscoveryDescription', + { defaultMessage: 'System Location Discovery (T1614)' } + ), + id: 'T1614', + name: 'System Location Discovery', + reference: 'https://attack.mitre.org/techniques/T1614', + tactics: 'discovery', + value: 'systemLocationDiscovery', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.systemNetworkConfigurationDiscoveryDescription', @@ -3420,38 +3561,9 @@ export const techniquesOptions: MitreTechniquesOptions[] = [ tactics: 'defense-evasion', value: 'xslScriptProcessing', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.domainPolicyModificationDescription', - { defaultMessage: 'Domain Policy Modification (T1484)' } - ), - id: 'T1484', - name: 'Domain Policy Modification', - reference: 'https://attack.mitre.org/techniques/T1484', - tactics: 'defense-evasion,privilege-escalation', - value: 'domainPolicyModification', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackTechniques.forgeWebCredentialsDescription', - { defaultMessage: 'Forge Web Credentials (T1606)' } - ), - id: 'T1606', - name: 'Forge Web Credentials', - reference: 'https://attack.mitre.org/techniques/T1606', - tactics: 'credential-access', - value: 'forgeWebCredentials', - }, ]; export const subtechniques = [ - { - name: '.bash_profile and .bashrc', - id: 'T1546.004', - reference: 'https://attack.mitre.org/techniques/T1546/004', - tactics: ['privilege-escalation', 'persistence'], - techniqueId: 'T1546', - }, { name: '/etc/passwd and /etc/shadow', id: 'T1003.008', @@ -3480,6 +3592,13 @@ export const subtechniques = [ tactics: ['privilege-escalation', 'persistence'], techniqueId: 'T1546', }, + { + name: 'Active Setup', + id: 'T1547.014', + reference: 'https://attack.mitre.org/techniques/T1547/014', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1547', + }, { name: 'Add Office 365 Global Administrator Role', id: 'T1098.003', @@ -3494,6 +3613,13 @@ export const subtechniques = [ tactics: ['persistence'], techniqueId: 'T1137', }, + { + name: 'Additional Cloud Credentials', + id: 'T1098.001', + reference: 'https://attack.mitre.org/techniques/T1098/001', + tactics: ['persistence'], + techniqueId: 'T1098', + }, { name: 'AppCert DLLs', id: 'T1546.009', @@ -3774,6 +3900,13 @@ export const subtechniques = [ tactics: ['resource-development'], techniqueId: 'T1588', }, + { + name: 'Code Signing Policy Modification', + id: 'T1553.006', + reference: 'https://attack.mitre.org/techniques/T1553/006', + tactics: ['defense-evasion'], + techniqueId: 'T1553', + }, { name: 'Compile After Delivery', id: 'T1027.004', @@ -3837,6 +3970,20 @@ export const subtechniques = [ tactics: ['collection'], techniqueId: 'T1213', }, + { + name: 'Container API', + id: 'T1552.007', + reference: 'https://attack.mitre.org/techniques/T1552/007', + tactics: ['credential-access'], + techniqueId: 'T1552', + }, + { + name: 'Container Orchestration Job', + id: 'T1053.007', + reference: 'https://attack.mitre.org/techniques/T1053/007', + tactics: ['execution', 'persistence', 'privilege-escalation'], + techniqueId: 'T1053', + }, { name: 'Control Panel', id: 'T1218.002', @@ -4121,7 +4268,7 @@ export const subtechniques = [ name: 'Domain Controller Authentication', id: 'T1556.001', reference: 'https://attack.mitre.org/techniques/T1556/001', - tactics: ['credential-access', 'defense-evasion'], + tactics: ['credential-access', 'defense-evasion', 'persistence'], techniqueId: 'T1556', }, { @@ -4152,6 +4299,13 @@ export const subtechniques = [ tactics: ['reconnaissance'], techniqueId: 'T1590', }, + { + name: 'Domain Trust Modification', + id: 'T1484.002', + reference: 'https://attack.mitre.org/techniques/T1484/002', + tactics: ['defense-evasion', 'privilege-escalation'], + techniqueId: 'T1484', + }, { name: 'Domains', id: 'T1583.001', @@ -4173,6 +4327,13 @@ export const subtechniques = [ tactics: ['defense-evasion'], techniqueId: 'T1601', }, + { + name: 'Drive-by Target', + id: 'T1608.004', + reference: 'https://attack.mitre.org/techniques/T1608/004', + tactics: ['resource-development'], + techniqueId: 'T1608', + }, { name: 'Dylib Hijacking', id: 'T1574.004', @@ -4187,6 +4348,13 @@ export const subtechniques = [ tactics: ['execution'], techniqueId: 'T1559', }, + { + name: 'Dynamic Linker Hijacking', + id: 'T1574.006', + reference: 'https://attack.mitre.org/techniques/T1574/006', + tactics: ['persistence', 'privilege-escalation', 'defense-evasion'], + techniqueId: 'T1574', + }, { name: 'Dynamic-link Library Injection', id: 'T1055.001', @@ -4404,6 +4572,13 @@ export const subtechniques = [ tactics: ['credential-access'], techniqueId: 'T1558', }, + { + name: 'Group Policy Modification', + id: 'T1484.001', + reference: 'https://attack.mitre.org/techniques/T1484/001', + tactics: ['defense-evasion', 'privilege-escalation'], + techniqueId: 'T1484', + }, { name: 'Group Policy Preferences', id: 'T1552.006', @@ -4495,6 +4670,13 @@ export const subtechniques = [ tactics: ['defense-evasion'], techniqueId: 'T1027', }, + { + name: 'Install Digital Certificate', + id: 'T1608.003', + reference: 'https://attack.mitre.org/techniques/T1608/003', + tactics: ['resource-development'], + techniqueId: 'T1608', + }, { name: 'Install Root Certificate', id: 'T1553.004', @@ -4523,6 +4705,13 @@ export const subtechniques = [ tactics: ['command-and-control'], techniqueId: 'T1090', }, + { + name: 'Internet Connection Discovery', + id: 'T1016.001', + reference: 'https://attack.mitre.org/techniques/T1016/001', + tactics: ['discovery'], + techniqueId: 'T1016', + }, { name: 'Invalid Code Signature', id: 'T1036.001', @@ -4531,7 +4720,7 @@ export const subtechniques = [ techniqueId: 'T1036', }, { - name: 'JavaScript/JScript', + name: 'JavaScript', id: 'T1059.007', reference: 'https://attack.mitre.org/techniques/T1059/007', tactics: ['execution'], @@ -4579,13 +4768,6 @@ export const subtechniques = [ tactics: ['privilege-escalation', 'persistence'], techniqueId: 'T1546', }, - { - name: 'LD_PRELOAD', - id: 'T1574.006', - reference: 'https://attack.mitre.org/techniques/T1574/006', - tactics: ['persistence', 'privilege-escalation', 'defense-evasion'], - techniqueId: 'T1574', - }, { name: 'LLMNR/NBT-NS Poisoning and SMB Relay', id: 'T1557.001', @@ -4642,6 +4824,13 @@ export const subtechniques = [ tactics: ['execution', 'persistence', 'privilege-escalation'], techniqueId: 'T1053', }, + { + name: 'Link Target', + id: 'T1608.005', + reference: 'https://attack.mitre.org/techniques/T1608/005', + tactics: ['resource-development'], + techniqueId: 'T1608', + }, { name: 'Linux and Mac File and Directory Permissions Modification', id: 'T1222.002', @@ -4733,6 +4922,13 @@ export const subtechniques = [ tactics: ['execution'], techniqueId: 'T1204', }, + { + name: 'Malicious Image', + id: 'T1204.003', + reference: 'https://attack.mitre.org/techniques/T1204/003', + tactics: ['execution'], + techniqueId: 'T1204', + }, { name: 'Malicious Link', id: 'T1204.001', @@ -4754,6 +4950,13 @@ export const subtechniques = [ tactics: ['resource-development'], techniqueId: 'T1588', }, + { + name: 'Mark-of-the-Web Bypass', + id: 'T1553.005', + reference: 'https://attack.mitre.org/techniques/T1553/005', + tactics: ['defense-evasion'], + techniqueId: 'T1553', + }, { name: 'Masquerade Task or Service', id: 'T1036.004', @@ -4821,7 +5024,7 @@ export const subtechniques = [ name: 'Network Device Authentication', id: 'T1556.004', reference: 'https://attack.mitre.org/techniques/T1556/004', - tactics: ['credential-access', 'defense-evasion'], + tactics: ['credential-access', 'defense-evasion', 'persistence'], techniqueId: 'T1556', }, { @@ -4968,7 +5171,7 @@ export const subtechniques = [ name: 'Password Filter DLL', id: 'T1556.002', reference: 'https://attack.mitre.org/techniques/T1556/002', - tactics: ['credential-access', 'defense-evasion'], + tactics: ['credential-access', 'defense-evasion', 'persistence'], techniqueId: 'T1556', }, { @@ -4978,6 +5181,13 @@ export const subtechniques = [ tactics: ['credential-access'], techniqueId: 'T1110', }, + { + name: 'Password Managers', + id: 'T1555.005', + reference: 'https://attack.mitre.org/techniques/T1555/005', + tactics: ['credential-access'], + techniqueId: 'T1555', + }, { name: 'Password Spraying', id: 'T1110.003', @@ -5024,7 +5234,7 @@ export const subtechniques = [ name: 'Pluggable Authentication Modules', id: 'T1556.003', reference: 'https://attack.mitre.org/techniques/T1556/003', - tactics: ['credential-access', 'defense-evasion'], + tactics: ['credential-access', 'defense-evasion', 'persistence'], techniqueId: 'T1556', }, { @@ -5139,6 +5349,13 @@ export const subtechniques = [ tactics: ['execution'], techniqueId: 'T1059', }, + { + name: 'RC Scripts', + id: 'T1037.004', + reference: 'https://attack.mitre.org/techniques/T1037/004', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1037', + }, { name: 'RDP Hijacking', id: 'T1563.002', @@ -5153,13 +5370,6 @@ export const subtechniques = [ tactics: ['defense-evasion', 'persistence'], techniqueId: 'T1542', }, - { - name: 'Rc.common', - id: 'T1037.004', - reference: 'https://attack.mitre.org/techniques/T1037/004', - tactics: ['persistence', 'privilege-escalation'], - techniqueId: 'T1037', - }, { name: 'Re-opened Applications', id: 'T1547.007', @@ -5265,6 +5475,13 @@ export const subtechniques = [ tactics: ['impact'], techniqueId: 'T1565', }, + { + name: 'SAML Tokens', + id: 'T1606.002', + reference: 'https://attack.mitre.org/techniques/T1606/002', + tactics: ['credential-access'], + techniqueId: 'T1606', + }, { name: 'SID-History Injection', id: 'T1134.005', @@ -5713,6 +5930,27 @@ export const subtechniques = [ tactics: ['execution'], techniqueId: 'T1059', }, + { + name: 'Unix Shell Configuration Modification', + id: 'T1546.004', + reference: 'https://attack.mitre.org/techniques/T1546/004', + tactics: ['privilege-escalation', 'persistence'], + techniqueId: 'T1546', + }, + { + name: 'Upload Malware', + id: 'T1608.001', + reference: 'https://attack.mitre.org/techniques/T1608/001', + tactics: ['resource-development'], + techniqueId: 'T1608', + }, + { + name: 'Upload Tool', + id: 'T1608.002', + reference: 'https://attack.mitre.org/techniques/T1608/002', + tactics: ['resource-development'], + techniqueId: 'T1608', + }, { name: 'User Activity Based Checks', id: 'T1497.002', @@ -5790,6 +6028,13 @@ export const subtechniques = [ tactics: ['reconnaissance'], techniqueId: 'T1596', }, + { + name: 'Web Cookies', + id: 'T1606.001', + reference: 'https://attack.mitre.org/techniques/T1606/001', + tactics: ['credential-access'], + techniqueId: 'T1606', + }, { name: 'Web Portal Capture', id: 'T1056.003', @@ -5839,6 +6084,13 @@ export const subtechniques = [ tactics: ['execution'], techniqueId: 'T1059', }, + { + name: 'Windows Credential Manager', + id: 'T1555.004', + reference: 'https://attack.mitre.org/techniques/T1555/004', + tactics: ['credential-access'], + techniqueId: 'T1555', + }, { name: 'Windows File and Directory Permissions Modification', id: 'T1222.001', @@ -5875,55 +6127,15 @@ export const subtechniques = [ techniqueId: 'T1547', }, { - name: 'Additional Cloud Credentials', - id: 'T1098.001', - reference: 'https://attack.mitre.org/techniques/T1098/001', - tactics: ['persistence'], - techniqueId: 'T1098', - }, - { - name: 'Group Policy Modification', - id: 'T1484.001', - reference: 'https://attack.mitre.org/techniques/T1484/001', - tactics: ['defense-evasion', 'privilege-escalation'], - techniqueId: 'T1484', - }, - { - name: 'Domain Trust Modification', - id: 'T1484.002', - reference: 'https://attack.mitre.org/techniques/T1484/002', - tactics: ['defense-evasion', 'privilege-escalation'], - techniqueId: 'T1484', - }, - { - name: 'Web Cookies', - id: 'T1606.001', - reference: 'https://attack.mitre.org/techniques/T1606/001', - tactics: ['credential-access'], - techniqueId: 'T1606', - }, - { - name: 'SAML Tokens', - id: 'T1606.002', - reference: 'https://attack.mitre.org/techniques/T1606/002', - tactics: ['credential-access'], - techniqueId: 'T1606', - }, -]; - -export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.bashProfileAndBashrcT1546Description', - { defaultMessage: '.bash_profile and .bashrc (T1546.004)' } - ), - id: 'T1546.004', - name: '.bash_profile and .bashrc', - reference: 'https://attack.mitre.org/techniques/T1546/004', - tactics: 'privilege-escalation,persistence', - techniqueId: 'T1546', - value: 'bashProfileAndBashrc', + name: 'XDG Autostart Entries', + id: 'T1547.013', + reference: 'https://attack.mitre.org/techniques/T1547/013', + tactics: ['persistence', 'privilege-escalation'], + techniqueId: 'T1547', }, +]; + +export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.etcPasswdAndEtcShadowT1003Description', @@ -5972,6 +6184,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1546', value: 'accessibilityFeatures', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.activeSetupT1547Description', + { defaultMessage: 'Active Setup (T1547.014)' } + ), + id: 'T1547.014', + name: 'Active Setup', + reference: 'https://attack.mitre.org/techniques/T1547/014', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1547', + value: 'activeSetup', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.addOffice365GlobalAdministratorRoleT1098Description', @@ -5996,6 +6220,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1137', value: 'addIns', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.additionalCloudCredentialsT1098Description', + { defaultMessage: 'Additional Cloud Credentials (T1098.001)' } + ), + id: 'T1098.001', + name: 'Additional Cloud Credentials', + reference: 'https://attack.mitre.org/techniques/T1098/001', + tactics: 'persistence', + techniqueId: 'T1098', + value: 'additionalCloudCredentials', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.appCertDlLsT1546Description', @@ -6476,6 +6712,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1588', value: 'codeSigningCertificates', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.codeSigningPolicyModificationT1553Description', + { defaultMessage: 'Code Signing Policy Modification (T1553.006)' } + ), + id: 'T1553.006', + name: 'Code Signing Policy Modification', + reference: 'https://attack.mitre.org/techniques/T1553/006', + tactics: 'defense-evasion', + techniqueId: 'T1553', + value: 'codeSigningPolicyModification', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.compileAfterDeliveryT1027Description', @@ -6584,6 +6832,30 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1213', value: 'confluence', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.containerApiT1552Description', + { defaultMessage: 'Container API (T1552.007)' } + ), + id: 'T1552.007', + name: 'Container API', + reference: 'https://attack.mitre.org/techniques/T1552/007', + tactics: 'credential-access', + techniqueId: 'T1552', + value: 'containerApi', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.containerOrchestrationJobT1053Description', + { defaultMessage: 'Container Orchestration Job (T1053.007)' } + ), + id: 'T1053.007', + name: 'Container Orchestration Job', + reference: 'https://attack.mitre.org/techniques/T1053/007', + tactics: 'execution,persistence,privilege-escalation', + techniqueId: 'T1053', + value: 'containerOrchestrationJob', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.controlPanelT1218Description', @@ -7072,7 +7344,7 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ id: 'T1556.001', name: 'Domain Controller Authentication', reference: 'https://attack.mitre.org/techniques/T1556/001', - tactics: 'credential-access,defense-evasion', + tactics: 'credential-access,defense-evasion,persistence', techniqueId: 'T1556', value: 'domainControllerAuthentication', }, @@ -7124,6 +7396,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1590', value: 'domainProperties', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.domainTrustModificationT1484Description', + { defaultMessage: 'Domain Trust Modification (T1484.002)' } + ), + id: 'T1484.002', + name: 'Domain Trust Modification', + reference: 'https://attack.mitre.org/techniques/T1484/002', + tactics: 'defense-evasion,privilege-escalation', + techniqueId: 'T1484', + value: 'domainTrustModification', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.domainsT1583Description', @@ -7160,6 +7444,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1601', value: 'downgradeSystemImage', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.driveByTargetT1608Description', + { defaultMessage: 'Drive-by Target (T1608.004)' } + ), + id: 'T1608.004', + name: 'Drive-by Target', + reference: 'https://attack.mitre.org/techniques/T1608/004', + tactics: 'resource-development', + techniqueId: 'T1608', + value: 'driveByTarget', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.dylibHijackingT1574Description', @@ -7184,6 +7480,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1559', value: 'dynamicDataExchange', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.dynamicLinkerHijackingT1574Description', + { defaultMessage: 'Dynamic Linker Hijacking (T1574.006)' } + ), + id: 'T1574.006', + name: 'Dynamic Linker Hijacking', + reference: 'https://attack.mitre.org/techniques/T1574/006', + tactics: 'persistence,privilege-escalation,defense-evasion', + techniqueId: 'T1574', + value: 'dynamicLinkerHijacking', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.dynamicLinkLibraryInjectionT1055Description', @@ -7556,6 +7864,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1558', value: 'goldenTicket', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.groupPolicyModificationT1484Description', + { defaultMessage: 'Group Policy Modification (T1484.001)' } + ), + id: 'T1484.001', + name: 'Group Policy Modification', + reference: 'https://attack.mitre.org/techniques/T1484/001', + tactics: 'defense-evasion,privilege-escalation', + techniqueId: 'T1484', + value: 'groupPolicyModification', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.groupPolicyPreferencesT1552Description', @@ -7712,6 +8032,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1027', value: 'indicatorRemovalFromTools', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.installDigitalCertificateT1608Description', + { defaultMessage: 'Install Digital Certificate (T1608.003)' } + ), + id: 'T1608.003', + name: 'Install Digital Certificate', + reference: 'https://attack.mitre.org/techniques/T1608/003', + tactics: 'resource-development', + techniqueId: 'T1608', + value: 'installDigitalCertificate', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.installRootCertificateT1553Description', @@ -7760,6 +8092,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1090', value: 'internalProxy', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.internetConnectionDiscoveryT1016Description', + { defaultMessage: 'Internet Connection Discovery (T1016.001)' } + ), + id: 'T1016.001', + name: 'Internet Connection Discovery', + reference: 'https://attack.mitre.org/techniques/T1016/001', + tactics: 'discovery', + techniqueId: 'T1016', + value: 'internetConnectionDiscovery', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.invalidCodeSignatureT1036Description', @@ -7774,15 +8118,15 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.javaScriptJScriptT1059Description', - { defaultMessage: 'JavaScript/JScript (T1059.007)' } + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.javaScriptT1059Description', + { defaultMessage: 'JavaScript (T1059.007)' } ), id: 'T1059.007', - name: 'JavaScript/JScript', + name: 'JavaScript', reference: 'https://attack.mitre.org/techniques/T1059/007', tactics: 'execution', techniqueId: 'T1059', - value: 'javaScriptJScript', + value: 'javaScript', }, { label: i18n.translate( @@ -7856,18 +8200,6 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1546', value: 'lcLoadDylibAddition', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.ldPreloadT1574Description', - { defaultMessage: 'LD_PRELOAD (T1574.006)' } - ), - id: 'T1574.006', - name: 'LD_PRELOAD', - reference: 'https://attack.mitre.org/techniques/T1574/006', - tactics: 'persistence,privilege-escalation,defense-evasion', - techniqueId: 'T1574', - value: 'ldPreload', - }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.llmnrNbtNsPoisoningAndSmbRelayT1557Description', @@ -7964,6 +8296,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1053', value: 'launchd', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.linkTargetT1608Description', + { defaultMessage: 'Link Target (T1608.005)' } + ), + id: 'T1608.005', + name: 'Link Target', + reference: 'https://attack.mitre.org/techniques/T1608/005', + tactics: 'resource-development', + techniqueId: 'T1608', + value: 'linkTarget', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.linuxAndMacFileAndDirectoryPermissionsModificationT1222Description', @@ -8120,6 +8464,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1204', value: 'maliciousFile', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.maliciousImageT1204Description', + { defaultMessage: 'Malicious Image (T1204.003)' } + ), + id: 'T1204.003', + name: 'Malicious Image', + reference: 'https://attack.mitre.org/techniques/T1204/003', + tactics: 'execution', + techniqueId: 'T1204', + value: 'maliciousImage', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.maliciousLinkT1204Description', @@ -8156,6 +8512,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1588', value: 'malware', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.markOfTheWebBypassT1553Description', + { defaultMessage: 'Mark-of-the-Web Bypass (T1553.005)' } + ), + id: 'T1553.005', + name: 'Mark-of-the-Web Bypass', + reference: 'https://attack.mitre.org/techniques/T1553/005', + tactics: 'defense-evasion', + techniqueId: 'T1553', + value: 'markOfTheWebBypass', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.masqueradeTaskOrServiceT1036Description', @@ -8272,7 +8640,7 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ id: 'T1556.004', name: 'Network Device Authentication', reference: 'https://attack.mitre.org/techniques/T1556/004', - tactics: 'credential-access,defense-evasion', + tactics: 'credential-access,defense-evasion,persistence', techniqueId: 'T1556', value: 'networkDeviceAuthentication', }, @@ -8524,7 +8892,7 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ id: 'T1556.002', name: 'Password Filter DLL', reference: 'https://attack.mitre.org/techniques/T1556/002', - tactics: 'credential-access,defense-evasion', + tactics: 'credential-access,defense-evasion,persistence', techniqueId: 'T1556', value: 'passwordFilterDll', }, @@ -8540,6 +8908,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1110', value: 'passwordGuessing', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.passwordManagersT1555Description', + { defaultMessage: 'Password Managers (T1555.005)' } + ), + id: 'T1555.005', + name: 'Password Managers', + reference: 'https://attack.mitre.org/techniques/T1555/005', + tactics: 'credential-access', + techniqueId: 'T1555', + value: 'passwordManagers', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.passwordSprayingT1110Description', @@ -8620,7 +9000,7 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ id: 'T1556.003', name: 'Pluggable Authentication Modules', reference: 'https://attack.mitre.org/techniques/T1556/003', - tactics: 'credential-access,defense-evasion', + tactics: 'credential-access,defense-evasion,persistence', techniqueId: 'T1556', value: 'pluggableAuthenticationModules', }, @@ -8816,6 +9196,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1059', value: 'python', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.rcScriptsT1037Description', + { defaultMessage: 'RC Scripts (T1037.004)' } + ), + id: 'T1037.004', + name: 'RC Scripts', + reference: 'https://attack.mitre.org/techniques/T1037/004', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1037', + value: 'rcScripts', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.rdpHijackingT1563Description', @@ -8840,18 +9232,6 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1542', value: 'rommoNkit', }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.rcCommonT1037Description', - { defaultMessage: 'Rc.common (T1037.004)' } - ), - id: 'T1037.004', - name: 'Rc.common', - reference: 'https://attack.mitre.org/techniques/T1037/004', - tactics: 'persistence,privilege-escalation', - techniqueId: 'T1037', - value: 'rcCommon', - }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.reOpenedApplicationsT1547Description', @@ -9032,6 +9412,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1565', value: 'runtimeDataManipulation', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.samlTokensT1606Description', + { defaultMessage: 'SAML Tokens (T1606.002)' } + ), + id: 'T1606.002', + name: 'SAML Tokens', + reference: 'https://attack.mitre.org/techniques/T1606/002', + tactics: 'credential-access', + techniqueId: 'T1606', + value: 'samlTokens', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.sidHistoryInjectionT1134Description', @@ -9800,6 +10192,42 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1059', value: 'unixShell', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.unixShellConfigurationModificationT1546Description', + { defaultMessage: 'Unix Shell Configuration Modification (T1546.004)' } + ), + id: 'T1546.004', + name: 'Unix Shell Configuration Modification', + reference: 'https://attack.mitre.org/techniques/T1546/004', + tactics: 'privilege-escalation,persistence', + techniqueId: 'T1546', + value: 'unixShellConfigurationModification', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.uploadMalwareT1608Description', + { defaultMessage: 'Upload Malware (T1608.001)' } + ), + id: 'T1608.001', + name: 'Upload Malware', + reference: 'https://attack.mitre.org/techniques/T1608/001', + tactics: 'resource-development', + techniqueId: 'T1608', + value: 'uploadMalware', + }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.uploadToolT1608Description', + { defaultMessage: 'Upload Tool (T1608.002)' } + ), + id: 'T1608.002', + name: 'Upload Tool', + reference: 'https://attack.mitre.org/techniques/T1608/002', + tactics: 'resource-development', + techniqueId: 'T1608', + value: 'uploadTool', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.userActivityBasedChecksT1497Description', @@ -9932,6 +10360,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1596', value: 'whois', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.webCookiesT1606Description', + { defaultMessage: 'Web Cookies (T1606.001)' } + ), + id: 'T1606.001', + name: 'Web Cookies', + reference: 'https://attack.mitre.org/techniques/T1606/001', + tactics: 'credential-access', + techniqueId: 'T1606', + value: 'webCookies', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.webPortalCaptureT1056Description', @@ -10016,6 +10456,18 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ techniqueId: 'T1059', value: 'windowsCommandShell', }, + { + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.windowsCredentialManagerT1555Description', + { defaultMessage: 'Windows Credential Manager (T1555.004)' } + ), + id: 'T1555.004', + name: 'Windows Credential Manager', + reference: 'https://attack.mitre.org/techniques/T1555/004', + tactics: 'credential-access', + techniqueId: 'T1555', + value: 'windowsCredentialManager', + }, { label: i18n.translate( 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.windowsFileAndDirectoryPermissionsModificationT1222Description', @@ -10078,63 +10530,15 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ }, { label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.additionalCloudCredentialsT1098Description', - { defaultMessage: 'Additional Cloud Credentials (T1098.001)' } - ), - id: 'T1098.001', - name: 'Additional Cloud Credentials', - reference: 'https://attack.mitre.org/techniques/T1098/001', - tactics: 'persistence', - techniqueId: 'T1098', - value: 'additionalCloudCredentials', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.groupPolicyModificationT1484Description', - { defaultMessage: 'Group Policy Modification (T1484.001)' } - ), - id: 'T1484.001', - name: 'Group Policy Modification', - reference: 'https://attack.mitre.org/techniques/T1484/001', - tactics: 'defense-evasion,privilege-escalation', - techniqueId: 'T1484', - value: 'groupPolicyModification', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.domainTrustModificationT1484Description', - { defaultMessage: 'Domain Trust Modification (T1484.002)' } - ), - id: 'T1484.002', - name: 'Domain Trust Modification', - reference: 'https://attack.mitre.org/techniques/T1484/002', - tactics: 'defense-evasion,privilege-escalation', - techniqueId: 'T1484', - value: 'domainTrustModification', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.webCookiesT1606Description', - { defaultMessage: 'Web Cookies (T1606.001)' } - ), - id: 'T1606.001', - name: 'Web Cookies', - reference: 'https://attack.mitre.org/techniques/T1606/001', - tactics: 'credential-access', - techniqueId: 'T1606', - value: 'webCookies', - }, - { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.samlTokensT1606Description', - { defaultMessage: 'SAML Tokens (T1606.002)' } + 'xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.xdgAutostartEntriesT1547Description', + { defaultMessage: 'XDG Autostart Entries (T1547.013)' } ), - id: 'T1606.002', - name: 'SAML Tokens', - reference: 'https://attack.mitre.org/techniques/T1606/002', - tactics: 'credential-access', - techniqueId: 'T1606', - value: 'samlTokens', + id: 'T1547.013', + name: 'XDG Autostart Entries', + reference: 'https://attack.mitre.org/techniques/T1547/013', + tactics: 'persistence,privilege-escalation', + techniqueId: 'T1547', + value: 'xdgAutostartEntries', }, ]; @@ -10145,21 +10549,21 @@ export const subtechniquesOptions: MitreSubtechniquesOptions[] = [ */ export const getMockThreatData = () => ({ tactic: { - name: 'Privilege Escalation', - id: 'TA0004', - reference: 'https://attack.mitre.org/tactics/TA0004', + name: 'Credential Access', + id: 'TA0006', + reference: 'https://attack.mitre.org/tactics/TA0006', }, technique: { - name: 'Event Triggered Execution', - id: 'T1546', - reference: 'https://attack.mitre.org/techniques/T1546', - tactics: ['privilege-escalation', 'persistence'], + name: 'OS Credential Dumping', + id: 'T1003', + reference: 'https://attack.mitre.org/techniques/T1003', + tactics: ['credential-access'], }, subtechnique: { - name: '.bash_profile and .bashrc', - id: 'T1546.004', - reference: 'https://attack.mitre.org/techniques/T1546/004', - tactics: ['privilege-escalation', 'persistence'], - techniqueId: 'T1546', + name: '/etc/passwd and /etc/shadow', + id: 'T1003.008', + reference: 'https://attack.mitre.org/techniques/T1003/008', + tactics: ['credential-access'], + techniqueId: 'T1003', }, }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c8e3526005963..db3285242cabd 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -20619,7 +20619,6 @@ "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.atWindowsT1053Description": " (Windows) (T1053.002) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.authenticationPackageT1547Description": "認証パッケージ (T1547.002) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.bashHistoryT1552Description": "Bash 履歴 (T1552.003) ", - "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.bashProfileAndBashrcT1546Description": ".bash_profile および .bashrc (T1546.004) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.bidirectionalCommunicationT1102Description": "双方向通信 (T1102.002) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.binaryPaddingT1027Description": "バイナリパディング (T1027.001) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.bootkitT1542Description": "ブートキット (T1542.003) ", @@ -20755,7 +20754,6 @@ "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.internalProxyT1090Description": "内部プロキシ (T1090.001) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.invalidCodeSignatureT1036Description": "無効なコード署名 (T1036.001) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.ipAddressesT1590Description": "IP アドレス (T1590.005) ", - "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.javaScriptJScriptT1059Description": "JavaScript/JScript (T1059.007) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.junkDataT1001Description": "ジャンクデータ (T1001.001) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.kerberoastingT1558Description": "Kerberoasting (T1558.003) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.kernelModulesAndExtensionsT1547Description": "カーネルモジュールおよび拡張 (T1547.006) ", @@ -20766,7 +20764,6 @@ "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.launchDaemonT1543Description": "デーモンの起動 (T1543.004) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.launchdT1053Description": "Launchd (T1053.004) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.lcLoadDylibAdditionT1546Description": "LC_LOAD_DYLIB 追加 (T1546.006) ", - "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.ldPreloadT1574Description": "LD_PRELOAD (T1574.006) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.linuxAndMacFileAndDirectoryPermissionsModificationT1222Description": "Linux および Mac ファイルおよびディレクトリアクセス権修正 (T1222.002) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.llmnrNbtNsPoisoningAndSmbRelayT1557Description": "LLMNR/NBT-NSポイズニングおよび SMB リレー (T1557.001) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.localAccountsT1078Description": "ローカルアカウント (T1078.003) ", @@ -20842,7 +20839,6 @@ "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.pubPrnT1216Description": "PubPrn (T1216.001) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.purchaseTechnicalDataT1597Description": "技術データの購入 (T1597.002) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.pythonT1059Description": "Python (T1059.006) ", - "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.rcCommonT1037Description": "Rc.common (T1037.004) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.rdpHijackingT1563Description": "RDP ハイジャック (T1563.002) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.reduceKeySpaceT1600Description": "キースペースの削減 (T1600.001) ", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.reflectionAmplificationT1498Description": "リフレクション (アンプ) 攻撃 (T1498.002) ", @@ -21052,7 +21048,6 @@ "xpack.securitySolution.detectionEngine.mitreAttackTechniques.hijackExecutionFlowDescription": "ハイジャック実行フロー (T1574) ", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.hypervisorDescription": "ハイパーバイザー (T1062) ", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.impairDefensesDescription": "防御の破損 (T1562) ", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.implantContainerImageDescription": "コンテナーイメージの挿入 (T1525) ", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.indicatorRemovalOnHostDescription": "ホストでのインジケーター削除 (T1070) ", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.indirectCommandExecutionDescription": "間接コマンド実行 (T1202) ", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.ingressToolTransferDescription": "Ingress Tool Transfer (T1105) ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index cf31ec8d8eceb..1e550ad8f7806 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -21080,7 +21080,6 @@ "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.atWindowsT1053Description": "At (Windows) (T1053.002)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.authenticationPackageT1547Description": "Authentication Package (T1547.002)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.bashHistoryT1552Description": "Bash History (T1552.003)", - "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.bashProfileAndBashrcT1546Description": ".bash_profile and .bashrc (T1546.004)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.bidirectionalCommunicationT1102Description": "Bidirectional Communication (T1102.002)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.binaryPaddingT1027Description": "Binary Padding (T1027.001)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.bootkitT1542Description": "Bootkit (T1542.003)", @@ -21216,7 +21215,6 @@ "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.internalProxyT1090Description": "Internal Proxy (T1090.001)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.invalidCodeSignatureT1036Description": "Invalid Code Signature (T1036.001)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.ipAddressesT1590Description": "IP Addresses (T1590.005)", - "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.javaScriptJScriptT1059Description": "JavaScript/JScript (T1059.007)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.junkDataT1001Description": "Junk Data (T1001.001)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.kerberoastingT1558Description": "Kerberoasting (T1558.003)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.kernelModulesAndExtensionsT1547Description": "Kernel Modules and Extensions (T1547.006)", @@ -21227,7 +21225,6 @@ "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.launchDaemonT1543Description": "Launch Daemon (T1543.004)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.launchdT1053Description": "Launchd (T1053.004)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.lcLoadDylibAdditionT1546Description": "LC_LOAD_DYLIB Addition (T1546.006)", - "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.ldPreloadT1574Description": "LD_PRELOAD (T1574.006)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.linuxAndMacFileAndDirectoryPermissionsModificationT1222Description": "Linux and Mac File and Directory Permissions Modification (T1222.002)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.llmnrNbtNsPoisoningAndSmbRelayT1557Description": "LLMNR/NBT-NS Poisoning and SMB Relay (T1557.001)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.localAccountsT1078Description": "Local Accounts (T1078.003)", @@ -21303,7 +21300,6 @@ "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.pubPrnT1216Description": "PubPrn (T1216.001)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.purchaseTechnicalDataT1597Description": "Purchase Technical Data (T1597.002)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.pythonT1059Description": "Python (T1059.006)", - "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.rcCommonT1037Description": "Rc.common (T1037.004)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.rdpHijackingT1563Description": "RDP Hijacking (T1563.002)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.reduceKeySpaceT1600Description": "Reduce Key Space (T1600.001)", "xpack.securitySolution.detectionEngine.mitreAttackSubtechniques.reflectionAmplificationT1498Description": "Reflection Amplification (T1498.002)", @@ -21513,7 +21509,6 @@ "xpack.securitySolution.detectionEngine.mitreAttackTechniques.hijackExecutionFlowDescription": "Hijack Execution Flow (T1574)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.hypervisorDescription": "Hypervisor (T1062)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.impairDefensesDescription": "Impair Defenses (T1562)", - "xpack.securitySolution.detectionEngine.mitreAttackTechniques.implantContainerImageDescription": "Implant Container Image (T1525)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.indicatorRemovalOnHostDescription": "Indicator Removal on Host (T1070)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.indirectCommandExecutionDescription": "Indirect Command Execution (T1202)", "xpack.securitySolution.detectionEngine.mitreAttackTechniques.ingressToolTransferDescription": "Ingress Tool Transfer (T1105)", From ff9611b13692d9a6f28ecf3ca6e04569b8e3e86d Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 10 Aug 2021 20:26:46 +0100 Subject: [PATCH 040/104] chore(NA): moving @kbn/storybook to babel transpiler (#107547) * chore(NA): moving @kbn/storybook to babel transpiler * chore(NA): fix import from kbn/storybook * chore(NA): fix public interface * chore(NA): fix kbn-storybook preset * chore(NA): update types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-storybook/.babelrc | 3 ++ packages/kbn-storybook/BUILD.bazel | 36 +++++++++++++------ packages/kbn-storybook/package.json | 4 +-- packages/kbn-storybook/preset.js | 4 +-- .../ignore_not_found_export_plugin.ts | 0 packages/kbn-storybook/{ => src}/index.ts | 1 + .../kbn-storybook/{ => src}/lib/constants.ts | 0 .../{ => src}/lib/default_config.ts | 0 .../kbn-storybook/{ => src}/lib/register.ts | 0 .../lib/register_theme_switcher_addon.ts | 0 .../{ => src}/lib/run_storybook_cli.ts | 0 .../{ => src}/lib/theme_switcher.tsx | 0 packages/kbn-storybook/{ => src}/typings.ts | 0 .../kbn-storybook/{ => src}/webpack.config.ts | 0 packages/kbn-storybook/tsconfig.json | 12 ++++--- .../presentation_util/storybook/main.ts | 5 ++- 16 files changed, 44 insertions(+), 21 deletions(-) create mode 100644 packages/kbn-storybook/.babelrc rename packages/kbn-storybook/{ => src}/ignore_not_found_export_plugin.ts (100%) rename packages/kbn-storybook/{ => src}/index.ts (88%) rename packages/kbn-storybook/{ => src}/lib/constants.ts (100%) rename packages/kbn-storybook/{ => src}/lib/default_config.ts (100%) rename packages/kbn-storybook/{ => src}/lib/register.ts (100%) rename packages/kbn-storybook/{ => src}/lib/register_theme_switcher_addon.ts (100%) rename packages/kbn-storybook/{ => src}/lib/run_storybook_cli.ts (100%) rename packages/kbn-storybook/{ => src}/lib/theme_switcher.tsx (100%) rename packages/kbn-storybook/{ => src}/typings.ts (100%) rename packages/kbn-storybook/{ => src}/webpack.config.ts (100%) diff --git a/packages/kbn-storybook/.babelrc b/packages/kbn-storybook/.babelrc new file mode 100644 index 0000000000000..7da72d1779128 --- /dev/null +++ b/packages/kbn-storybook/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@kbn/babel-preset/node_preset"] +} diff --git a/packages/kbn-storybook/BUILD.bazel b/packages/kbn-storybook/BUILD.bazel index e18256aeb8da4..ae09d79d58331 100644 --- a/packages/kbn-storybook/BUILD.bazel +++ b/packages/kbn-storybook/BUILD.bazel @@ -1,14 +1,14 @@ load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") +load("//src/dev/bazel:index.bzl", "jsts_transpiler") PKG_BASE_NAME = "kbn-storybook" PKG_REQUIRE_NAME = "@kbn/storybook" SOURCE_FILES = glob( [ - "lib/**/*.ts", - "lib/**/*.tsx", - "*.ts", + "src/**/*.ts", + "src/**/*.tsx", ], exclude = ["**/*.test.*"], ) @@ -28,7 +28,7 @@ NPM_MODULE_EXTRA_FILES = [ "preset.js", ] -SRC_DEPS = [ +RUNTIME_DEPS = [ "//packages/kbn-dev-utils", "//packages/kbn-ui-shared-deps", "@npm//@storybook/addons", @@ -45,13 +45,27 @@ SRC_DEPS = [ ] TYPES_DEPS = [ + "//packages/kbn-dev-utils", + "//packages/kbn-ui-shared-deps", + "@npm//@storybook/addons", + "@npm//@storybook/api", + "@npm//@storybook/components", + "@npm//@storybook/core", + "@npm//@storybook/node-logger", + "@npm//@storybook/react", + "@npm//@storybook/theming", "@npm//@types/loader-utils", "@npm//@types/node", + "@npm//@types/react", "@npm//@types/webpack", "@npm//@types/webpack-merge", ] -DEPS = SRC_DEPS + TYPES_DEPS +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) ts_config( name = "tsconfig", @@ -62,14 +76,16 @@ ts_config( ) ts_project( - name = "tsc", + name = "tsc_types", args = ['--pretty'], srcs = SRCS, - deps = DEPS, + deps = TYPES_DEPS, declaration = True, declaration_map = True, - incremental = True, - out_dir = "target", + emit_declaration_only = True, + incremental = False, + out_dir = "target_types", + root_dir = "src", source_map = True, tsconfig = ":tsconfig", ) @@ -77,7 +93,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = [":tsc"] + DEPS, + deps = RUNTIME_DEPS + [":target_node", ":tsc_types"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-storybook/package.json b/packages/kbn-storybook/package.json index f3c12f19a0793..5360e4a5c547b 100644 --- a/packages/kbn-storybook/package.json +++ b/packages/kbn-storybook/package.json @@ -3,8 +3,8 @@ "version": "1.0.0", "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", - "main": "./target/index.js", - "types": "./target/index.d.ts", + "main": "./target_node/index.js", + "types": "./target_types/index.d.ts", "kibana": { "devOnly": true } diff --git a/packages/kbn-storybook/preset.js b/packages/kbn-storybook/preset.js index be0012a3818b1..a0170dad1a315 100644 --- a/packages/kbn-storybook/preset.js +++ b/packages/kbn-storybook/preset.js @@ -7,11 +7,11 @@ */ // eslint-disable-next-line -const webpackConfig = require('./target/webpack.config').default; +const webpackConfig = require('./target_node/webpack.config'); module.exports = { managerEntries: (entry = []) => { - return [...entry, require.resolve('./target/lib/register')]; + return [...entry, require.resolve('./target_node/lib/register')]; }, webpackFinal: (config) => { return webpackConfig({ config }); diff --git a/packages/kbn-storybook/ignore_not_found_export_plugin.ts b/packages/kbn-storybook/src/ignore_not_found_export_plugin.ts similarity index 100% rename from packages/kbn-storybook/ignore_not_found_export_plugin.ts rename to packages/kbn-storybook/src/ignore_not_found_export_plugin.ts diff --git a/packages/kbn-storybook/index.ts b/packages/kbn-storybook/src/index.ts similarity index 88% rename from packages/kbn-storybook/index.ts rename to packages/kbn-storybook/src/index.ts index 16ac1c6e10c4b..1694c995c577f 100644 --- a/packages/kbn-storybook/index.ts +++ b/packages/kbn-storybook/src/index.ts @@ -8,3 +8,4 @@ export { defaultConfig } from './lib/default_config'; export { runStorybookCli } from './lib/run_storybook_cli'; +export { default as WebpackConfig } from './webpack.config'; diff --git a/packages/kbn-storybook/lib/constants.ts b/packages/kbn-storybook/src/lib/constants.ts similarity index 100% rename from packages/kbn-storybook/lib/constants.ts rename to packages/kbn-storybook/src/lib/constants.ts diff --git a/packages/kbn-storybook/lib/default_config.ts b/packages/kbn-storybook/src/lib/default_config.ts similarity index 100% rename from packages/kbn-storybook/lib/default_config.ts rename to packages/kbn-storybook/src/lib/default_config.ts diff --git a/packages/kbn-storybook/lib/register.ts b/packages/kbn-storybook/src/lib/register.ts similarity index 100% rename from packages/kbn-storybook/lib/register.ts rename to packages/kbn-storybook/src/lib/register.ts diff --git a/packages/kbn-storybook/lib/register_theme_switcher_addon.ts b/packages/kbn-storybook/src/lib/register_theme_switcher_addon.ts similarity index 100% rename from packages/kbn-storybook/lib/register_theme_switcher_addon.ts rename to packages/kbn-storybook/src/lib/register_theme_switcher_addon.ts diff --git a/packages/kbn-storybook/lib/run_storybook_cli.ts b/packages/kbn-storybook/src/lib/run_storybook_cli.ts similarity index 100% rename from packages/kbn-storybook/lib/run_storybook_cli.ts rename to packages/kbn-storybook/src/lib/run_storybook_cli.ts diff --git a/packages/kbn-storybook/lib/theme_switcher.tsx b/packages/kbn-storybook/src/lib/theme_switcher.tsx similarity index 100% rename from packages/kbn-storybook/lib/theme_switcher.tsx rename to packages/kbn-storybook/src/lib/theme_switcher.tsx diff --git a/packages/kbn-storybook/typings.ts b/packages/kbn-storybook/src/typings.ts similarity index 100% rename from packages/kbn-storybook/typings.ts rename to packages/kbn-storybook/src/typings.ts diff --git a/packages/kbn-storybook/webpack.config.ts b/packages/kbn-storybook/src/webpack.config.ts similarity index 100% rename from packages/kbn-storybook/webpack.config.ts rename to packages/kbn-storybook/src/webpack.config.ts diff --git a/packages/kbn-storybook/tsconfig.json b/packages/kbn-storybook/tsconfig.json index 1f6886c45c505..9e1ee54cd4899 100644 --- a/packages/kbn-storybook/tsconfig.json +++ b/packages/kbn-storybook/tsconfig.json @@ -1,15 +1,19 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": true, - "outDir": "target", - "skipLibCheck": true, "declaration": true, "declarationMap": true, + "emitDeclarationOnly": true, + "incremental": false, + "outDir": "target_types", + "rootDir": "src", + "skipLibCheck": true, "sourceMap": true, "sourceRoot": "../../../../packages/kbn-storybook", "target": "es2015", "types": ["node"] }, - "include": ["*.ts", "lib/**/*.ts", "lib/**/*.tsx"] + "include": [ + "src/**/*.ts", "src/**/*.tsx" + ] } diff --git a/src/plugins/presentation_util/storybook/main.ts b/src/plugins/presentation_util/storybook/main.ts index 17b05404d0e46..09de9240c1aee 100644 --- a/src/plugins/presentation_util/storybook/main.ts +++ b/src/plugins/presentation_util/storybook/main.ts @@ -7,13 +7,12 @@ */ import { Configuration } from 'webpack'; -import { defaultConfig } from '@kbn/storybook'; -import webpackConfig from '@kbn/storybook/target/webpack.config'; +import { defaultConfig, WebpackConfig } from '@kbn/storybook'; module.exports = { ...defaultConfig, addons: ['@storybook/addon-essentials'], webpackFinal: (config: Configuration) => { - return webpackConfig({ config }); + return WebpackConfig({ config }); }, }; From 97e345fa8dee90460d074a818a311d88f99c43ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 10 Aug 2021 15:36:50 -0400 Subject: [PATCH 041/104] [APM] Fixing service inventory responsive design (#107690) * fixing service inventory responsive design * truncate service name * adding unit test * addressing PR comments * fixing test * fixing merge problem Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../service_list/ServiceListMetric.tsx | 27 +++- .../service_inventory/service_list/index.tsx | 83 ++++++---- .../service_list/service_list.test.tsx | 146 ++++++++++++++++-- .../shared/truncate_with_tooltip/index.tsx | 5 +- .../apm/public/hooks/use_break_points.ts | 2 + 5 files changed, 207 insertions(+), 56 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/ServiceListMetric.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/ServiceListMetric.tsx index af2cde7f861cc..7a4721407e69b 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/ServiceListMetric.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/ServiceListMetric.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import React from 'react'; import { Coordinate } from '../../../../../typings/timeseries'; import { SparkPlot } from '../../../shared/charts/spark_plot'; @@ -14,18 +15,32 @@ export function ServiceListMetric({ series, valueLabel, comparisonSeries, + hideSeries = false, }: { color: 'euiColorVis1' | 'euiColorVis0' | 'euiColorVis7'; series?: Coordinate[]; comparisonSeries?: Coordinate[]; valueLabel: React.ReactNode; + hideSeries?: boolean; }) { + if (!hideSeries) { + return ( + + ); + } + return ( - + + + + {valueLabel} + + + ); } diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx index 39e65f7eb15d1..f50816809df07 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx @@ -17,7 +17,10 @@ import { TypeOf } from '@kbn/typed-react-router-config'; import { orderBy } from 'lodash'; import React, { useMemo } from 'react'; import { ValuesType } from 'utility-types'; -import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; +import { + BreakPoints, + useBreakPoints, +} from '../../../../hooks/use_break_points'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { ServiceHealthStatus } from '../../../../../common/service_health_status'; import { @@ -38,6 +41,7 @@ import { ITableColumn, ManagedTable } from '../../../shared/managed_table'; import { ServiceLink } from '../../../shared/service_link'; import { HealthBadge } from './HealthBadge'; import { ServiceListMetric } from './ServiceListMetric'; +import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; type ServiceListAPIResponse = APIReturnType<'GET /api/apm/services'>; type Items = ServiceListAPIResponse['items']; @@ -49,13 +53,6 @@ function formatString(value?: string | null) { return value || NOT_AVAILABLE_LABEL; } -const ToolTipWrapper = euiStyled.span` - width: 100%; - .apmServiceList__serviceNameTooltip { - width: 100%; - } -`; - const SERVICE_HEALTH_STATUS_ORDER = [ ServiceHealthStatus.unknown, ServiceHealthStatus.healthy, @@ -67,11 +64,16 @@ export function getServiceColumns({ query, showTransactionTypeColumn, comparisonData, + breakPoints, }: { query: TypeOf['query']; showTransactionTypeColumn: boolean; + breakPoints: BreakPoints; comparisonData?: ServicesDetailedStatisticsAPIResponse; }): Array> { + const { isSmall, isLarge, isXl } = breakPoints; + const showWhenSmallOrGreaterThanLarge = isSmall || !isLarge; + const showWhenSmallOrGreaterThanXL = isSmall || !isXl; return [ { field: 'healthStatus', @@ -96,34 +98,38 @@ export function getServiceColumns({ width: '40%', sortable: true, render: (_, { serviceName, agentName }) => ( - - + - - - ), - }, - { - field: 'environments', - name: i18n.translate('xpack.apm.servicesTable.environmentColumnLabel', { - defaultMessage: 'Environment', - }), - width: `${unit * 10}px`, - sortable: true, - render: (_, { environments }) => ( - + } + /> ), }, - ...(showTransactionTypeColumn + ...(showWhenSmallOrGreaterThanLarge + ? [ + { + field: 'environments', + name: i18n.translate( + 'xpack.apm.servicesTable.environmentColumnLabel', + { + defaultMessage: 'Environment', + } + ), + width: `${unit * 10}px`, + sortable: true, + render: (_, { environments }) => ( + + ), + } as ITableColumn, + ] + : []), + ...(showTransactionTypeColumn && showWhenSmallOrGreaterThanXL ? [ { field: 'transactionType', @@ -149,12 +155,13 @@ export function getServiceColumns({ comparisonSeries={ comparisonData?.previousPeriod[serviceName]?.latency } + hideSeries={!showWhenSmallOrGreaterThanLarge} color="euiColorVis1" valueLabel={asMillisecondDuration(latency || 0)} /> ), align: 'left', - width: `${unit * 10}px`, + width: showWhenSmallOrGreaterThanLarge ? `${unit * 10}px` : 'auto', }, { field: 'throughput', @@ -169,12 +176,13 @@ export function getServiceColumns({ comparisonSeries={ comparisonData?.previousPeriod[serviceName]?.throughput } + hideSeries={!showWhenSmallOrGreaterThanLarge} color="euiColorVis0" valueLabel={asTransactionRate(throughput)} /> ), align: 'left', - width: `${unit * 10}px`, + width: showWhenSmallOrGreaterThanLarge ? `${unit * 10}px` : 'auto', }, { field: 'transactionErrorRate', @@ -193,13 +201,14 @@ export function getServiceColumns({ comparisonSeries={ comparisonData?.previousPeriod[serviceName]?.transactionErrorRate } + hideSeries={!showWhenSmallOrGreaterThanLarge} color="euiColorVis7" valueLabel={valueLabel} /> ); }, align: 'left', - width: `${unit * 10}px`, + width: showWhenSmallOrGreaterThanLarge ? `${unit * 10}px` : 'auto', }, ]; } @@ -217,6 +226,7 @@ export function ServiceList({ comparisonData, isLoading, }: Props) { + const breakPoints = useBreakPoints(); const displayHealthStatus = items.some((item) => 'healthStatus' in item); const showTransactionTypeColumn = items.some( @@ -229,8 +239,13 @@ export function ServiceList({ const serviceColumns = useMemo( () => - getServiceColumns({ query, showTransactionTypeColumn, comparisonData }), - [query, showTransactionTypeColumn, comparisonData] + getServiceColumns({ + query, + showTransactionTypeColumn, + comparisonData, + breakPoints, + }), + [query, showTransactionTypeColumn, comparisonData, breakPoints] ); const columns = displayHealthStatus diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/service_list.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/service_list.test.tsx index d6cf7bf018d0f..51636e938f8cb 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/service_list.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/service_list.test.tsx @@ -7,6 +7,7 @@ import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; +import { BreakPoints } from '../../../../hooks/use_break_points'; import { ServiceHealthStatus } from '../../../../../common/service_health_status'; import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import { mockMoment, renderWithTheme } from '../../../../utils/testHelpers'; @@ -42,7 +43,12 @@ describe('ServiceList', () => { ).not.toThrowError(); }); - it('renders columns correctly', () => { + describe('responsive columns', () => { + const query = { + rangeFrom: 'now-15m', + rangeTo: 'now', + }; + const service: any = { serviceName: 'opbeans-python', agentName: 'python', @@ -59,20 +65,132 @@ describe('ServiceList', () => { timeseries: [], }, environments: ['test'], + transactionType: 'request', }; - const renderedColumns = getServiceColumns({ - query: { - rangeFrom: 'now-15m', - rangeTo: 'now', - }, - showTransactionTypeColumn: false, - }).map((c) => c.render!(service[c.field!], service)); - - expect(renderedColumns[0]).toMatchInlineSnapshot(` - - `); + describe('when small', () => { + it('shows environment, transaction type and sparklines', () => { + const renderedColumns = getServiceColumns({ + query, + showTransactionTypeColumn: true, + breakPoints: { + isSmall: true, + isLarge: true, + isXl: true, + } as BreakPoints, + }).map((c) => + c.render ? c.render!(service[c.field!], service) : service[c.field!] + ); + expect(renderedColumns.length).toEqual(7); + expect(renderedColumns[2]).toMatchInlineSnapshot(` + + `); + expect(renderedColumns[3]).toMatchInlineSnapshot(`"request"`); + expect(renderedColumns[4]).toMatchInlineSnapshot(` + + `); + }); + }); + + describe('when Large', () => { + it('hides environment, transaction type and sparklines', () => { + const renderedColumns = getServiceColumns({ + query, + showTransactionTypeColumn: true, + breakPoints: { + isSmall: false, + isLarge: true, + isXl: true, + } as BreakPoints, + }).map((c) => + c.render ? c.render!(service[c.field!], service) : service[c.field!] + ); + expect(renderedColumns.length).toEqual(5); + expect(renderedColumns[2]).toMatchInlineSnapshot(` + + `); + }); + + describe('when XL', () => { + it('hides transaction type', () => { + const renderedColumns = getServiceColumns({ + query, + showTransactionTypeColumn: true, + breakPoints: { + isSmall: false, + isLarge: false, + isXl: true, + } as BreakPoints, + }).map((c) => + c.render ? c.render!(service[c.field!], service) : service[c.field!] + ); + expect(renderedColumns.length).toEqual(6); + expect(renderedColumns[2]).toMatchInlineSnapshot(` + + `); + expect(renderedColumns[3]).toMatchInlineSnapshot(` + + `); + }); + }); + + describe('when XXL', () => { + it('hides transaction type', () => { + const renderedColumns = getServiceColumns({ + query, + showTransactionTypeColumn: true, + breakPoints: { + isSmall: false, + isLarge: false, + isXl: false, + } as BreakPoints, + }).map((c) => + c.render ? c.render!(service[c.field!], service) : service[c.field!] + ); + expect(renderedColumns.length).toEqual(7); + expect(renderedColumns[2]).toMatchInlineSnapshot(` + + `); + expect(renderedColumns[3]).toMatchInlineSnapshot(`"request"`); + expect(renderedColumns[4]).toMatchInlineSnapshot(` + + `); + }); + }); + }); }); describe('without ML data', () => { diff --git a/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx b/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx index d9268c14aa2ea..3b649d77172ee 100644 --- a/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx @@ -27,13 +27,14 @@ const ContentWrapper = euiStyled.div` interface Props { text: string; content?: React.ReactNode; + 'data-test-subj'?: string; } export function TruncateWithTooltip(props: Props) { - const { text, content } = props; + const { text, content, ...rest } = props; return ( - + ; + export function getScreenSizes(windowWidth: number) { return { isXSmall: isWithinMaxBreakpoint(windowWidth, 'xs'), From f596f89eac5bfffbe72c011c07fe96624c10f10a Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 10 Aug 2021 13:43:39 -0600 Subject: [PATCH 042/104] [Maps] filtered out docs with empty entity ids for tracks and top-hits layers (#107680) * [Maps] filtered out docs with empty entity ids for tracks and top-hits layers * eslint * add type check for string fields Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../es_geo_line_source/es_geo_line_source.tsx | 10 ++++++++++ .../sources/es_search_source/es_search_source.tsx | 15 ++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx index 82be83dad43f7..4755781147e5b 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/es_geo_line_source.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { GeoJsonProperties } from 'geojson'; import { i18n } from '@kbn/i18n'; +import type { Filter } from 'src/plugins/data/public'; import { EMPTY_FEATURE_COLLECTION, FIELD_ORIGIN, @@ -202,6 +203,15 @@ export class ESGeoLineSource extends AbstractESAggSource { terms: addFieldToDSL(termsAgg, splitField), }, }); + if (splitField.type === 'string') { + const entityIsNotEmptyFilter = esFilters.buildPhraseFilter(splitField, '', indexPattern); + entityIsNotEmptyFilter.meta.negate = true; + entitySearchSource.setField('filter', [ + ...(entitySearchSource.getField('filter') as Filter[]), + entityIsNotEmptyFilter, + ]); + } + const entityResp = await this._runEsQuery({ requestId: `${this.getId()}_entities`, requestName: i18n.translate('xpack.maps.source.esGeoLine.entityRequestName', { diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index 55eed588b8840..b63a821cdb0c7 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -9,8 +9,9 @@ import _ from 'lodash'; import React, { ReactElement } from 'react'; import rison from 'rison-node'; import { i18n } from '@kbn/i18n'; -import { IFieldType, IndexPattern } from 'src/plugins/data/public'; +import type { Filter, IFieldType, IndexPattern } from 'src/plugins/data/public'; import { GeoJsonProperties, Geometry, Position } from 'geojson'; +import { esFilters } from '../../../../../../../src/plugins/data/public'; import { AbstractESSource } from '../es_source'; import { getHttp, getSearchService, getTimeFilter } from '../../../kibana_services'; import { @@ -311,6 +312,18 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye }, }, }); + if (topHitsSplitField.type === 'string') { + const entityIsNotEmptyFilter = esFilters.buildPhraseFilter( + topHitsSplitField, + '', + indexPattern + ); + entityIsNotEmptyFilter.meta.negate = true; + searchSource.setField('filter', [ + ...(searchSource.getField('filter') as Filter[]), + entityIsNotEmptyFilter, + ]); + } const resp = await this._runEsQuery({ requestId: this.getId(), From faf6482e01d5b7ef1b31a58bb70a576a54e422dd Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Tue, 10 Aug 2021 15:48:07 -0400 Subject: [PATCH 043/104] [Input Controls] Options List Embeddable, Factory & Frame (#106877) Co-authored-by: Clint Andrew Hall Co-authored-by: andreadelrio --- .../public/components/fixtures/flights.ts | 1953 +++++++++++++++++ .../input_controls/__stories__/decorators.tsx | 51 + .../input_controls/__stories__/flights.ts | 60 + .../__stories__/input_controls.stories.tsx | 88 + .../control_frame/control_frame.scss | 14 + .../control_frame/control_frame.tsx | 58 + .../control_types/options_list/index.ts | 10 + .../options_list/options_list.scss | 46 + .../options_list/options_list_component.tsx | 173 ++ .../options_list/options_list_embeddable.tsx | 63 + .../options_list_embeddable_factory.ts | 32 + .../options_list_popover_component.tsx | 83 + .../options_list/options_list_strings.ts | 32 + .../input_controls/embeddable/types.ts | 23 + .../public/components/input_controls/index.ts | 7 + src/plugins/presentation_util/tsconfig.json | 3 + 16 files changed, 2696 insertions(+) create mode 100644 src/plugins/presentation_util/public/components/fixtures/flights.ts create mode 100644 src/plugins/presentation_util/public/components/input_controls/__stories__/decorators.tsx create mode 100644 src/plugins/presentation_util/public/components/input_controls/__stories__/flights.ts create mode 100644 src/plugins/presentation_util/public/components/input_controls/__stories__/input_controls.stories.tsx create mode 100644 src/plugins/presentation_util/public/components/input_controls/control_frame/control_frame.scss create mode 100644 src/plugins/presentation_util/public/components/input_controls/control_frame/control_frame.tsx create mode 100644 src/plugins/presentation_util/public/components/input_controls/control_types/options_list/index.ts create mode 100644 src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list.scss create mode 100644 src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_component.tsx create mode 100644 src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_embeddable.tsx create mode 100644 src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_embeddable_factory.ts create mode 100644 src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_popover_component.tsx create mode 100644 src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_strings.ts create mode 100644 src/plugins/presentation_util/public/components/input_controls/embeddable/types.ts create mode 100644 src/plugins/presentation_util/public/components/input_controls/index.ts diff --git a/src/plugins/presentation_util/public/components/fixtures/flights.ts b/src/plugins/presentation_util/public/components/fixtures/flights.ts new file mode 100644 index 0000000000000..19ac29226693b --- /dev/null +++ b/src/plugins/presentation_util/public/components/fixtures/flights.ts @@ -0,0 +1,1953 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const flights = [ + { + FlightNum: '9HY9SWR', + DestCountry: 'AU', + OriginWeather: 'Sunny', + OriginCityName: 'Frankfurt am Main', + AvgTicketPrice: 841.2656419677076, + DistanceMiles: 10247.856675613455, + FlightDelay: false, + DestWeather: 'Rain', + Dest: 'Sydney Kingsford Smith International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'DE', + dayOfWeek: 0, + DistanceKilometers: 16492.32665375846, + timestamp: '2018-01-01T00:00:00', + DestLocation: { lat: '-33.94609833', lon: '151.177002' }, + DestAirportID: 'SYD', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 1030.7704158599038, + Origin: 'Frankfurt am Main Airport', + OriginLocation: { lat: '50.033333', lon: '8.570556' }, + DestRegion: 'SE-BD', + OriginAirportID: 'FRA', + OriginRegion: 'DE-HE', + DestCityName: 'Sydney', + FlightTimeHour: 17.179506930998397, + FlightDelayMin: 0, + }, + { + FlightNum: 'X98CCZO', + DestCountry: 'IT', + OriginWeather: 'Clear', + OriginCityName: 'Cape Town', + AvgTicketPrice: 882.9826615595518, + DistanceMiles: 5482.606664853586, + FlightDelay: false, + DestWeather: 'Sunny', + Dest: 'Venice Marco Polo Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'ZA', + dayOfWeek: 0, + DistanceKilometers: 8823.40014044213, + timestamp: '2018-01-01T18:27:00', + DestLocation: { lat: '45.505299', lon: '12.3519' }, + DestAirportID: 'VE05', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 464.3894810759016, + Origin: 'Cape Town International Airport', + OriginLocation: { lat: '-33.96480179', lon: '18.60169983' }, + DestRegion: 'IT-34', + OriginAirportID: 'CPT', + OriginRegion: 'SE-BD', + DestCityName: 'Venice', + FlightTimeHour: 7.73982468459836, + FlightDelayMin: 0, + }, + { + FlightNum: 'UFK2WIZ', + DestCountry: 'IT', + OriginWeather: 'Rain', + OriginCityName: 'Venice', + AvgTicketPrice: 190.6369038508356, + DistanceMiles: 0.0, + FlightDelay: false, + DestWeather: 'Cloudy', + Dest: 'Venice Marco Polo Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 0.0, + timestamp: '2018-01-01T17:11:14', + DestLocation: { lat: '45.505299', lon: '12.3519' }, + DestAirportID: 'VE05', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 0.0, + Origin: 'Venice Marco Polo Airport', + OriginLocation: { lat: '45.505299', lon: '12.3519' }, + DestRegion: 'IT-34', + OriginAirportID: 'VE05', + OriginRegion: 'IT-34', + DestCityName: 'Venice', + FlightTimeHour: 0.0, + FlightDelayMin: 0, + }, + { + FlightNum: 'EAYQW69', + DestCountry: 'IT', + OriginWeather: 'Thunder & Lightning', + OriginCityName: 'Naples', + AvgTicketPrice: 181.69421554118, + DistanceMiles: 345.31943877289535, + FlightDelay: true, + DestWeather: 'Clear', + Dest: "Treviso-Sant'Angelo Airport", + FlightDelayType: 'Weather Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 555.7377668725265, + timestamp: '2018-01-01T10:33:28', + DestLocation: { lat: '45.648399', lon: '12.1944' }, + DestAirportID: 'TV01', + Carrier: 'Kibana Airlines', + Cancelled: true, + FlightTimeMin: 222.74905899019436, + Origin: 'Naples International Airport', + OriginLocation: { lat: '40.886002', lon: '14.2908' }, + DestRegion: 'IT-34', + OriginAirportID: 'NA01', + OriginRegion: 'IT-72', + DestCityName: 'Treviso', + FlightTimeHour: 3.712484316503239, + FlightDelayMin: 180, + }, + { + FlightNum: '58U013N', + DestCountry: 'CN', + OriginWeather: 'Damaging Wind', + OriginCityName: 'Mexico City', + AvgTicketPrice: 730.041778346198, + DistanceMiles: 8300.428124665925, + FlightDelay: false, + DestWeather: 'Clear', + Dest: "Xi'an Xianyang International Airport", + FlightDelayType: 'No Delay', + OriginCountry: 'MX', + dayOfWeek: 0, + DistanceKilometers: 13358.24419986236, + timestamp: '2018-01-01T05:13:00', + DestLocation: { lat: '34.447102', lon: '108.751999' }, + DestAirportID: 'XIY', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 785.7790705801389, + Origin: 'Licenciado Benito Juarez International Airport', + OriginLocation: { lat: '19.4363', lon: '-99.072098' }, + DestRegion: 'SE-BD', + OriginAirportID: 'AICM', + OriginRegion: 'MX-DIF', + DestCityName: "Xi'an", + FlightTimeHour: 13.096317843002314, + FlightDelayMin: 0, + }, + { + FlightNum: 'XEJ78I2', + DestCountry: 'IT', + OriginWeather: 'Rain', + OriginCityName: 'Edmonton', + AvgTicketPrice: 418.1520890531832, + DistanceMiles: 4891.315227492962, + FlightDelay: false, + DestWeather: 'Thunder & Lightning', + Dest: 'Genoa Cristoforo Colombo Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'CA', + dayOfWeek: 0, + DistanceKilometers: 7871.808813474433, + timestamp: '2018-01-01T01:43:03', + DestLocation: { lat: '44.4133', lon: '8.8375' }, + DestAirportID: 'GE01', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 393.5904406737217, + Origin: 'Edmonton International Airport', + OriginLocation: { lat: '53.30970001', lon: '-113.5800018' }, + DestRegion: 'IT-42', + OriginAirportID: 'CYEG', + OriginRegion: 'CA-AB', + DestCityName: 'Genova', + FlightTimeHour: 6.5598406778953615, + FlightDelayMin: 0, + }, + { + FlightNum: 'EVARI8I', + DestCountry: 'CH', + OriginWeather: 'Clear', + OriginCityName: 'Zurich', + AvgTicketPrice: 180.24681638061213, + DistanceMiles: 0.0, + FlightDelay: true, + DestWeather: 'Hail', + Dest: 'Zurich Airport', + FlightDelayType: 'Security Delay', + OriginCountry: 'CH', + dayOfWeek: 0, + DistanceKilometers: 0.0, + timestamp: '2018-01-01T13:49:53', + DestLocation: { lat: '47.464699', lon: '8.54917' }, + DestAirportID: 'ZRH', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 300.0, + Origin: 'Zurich Airport', + OriginLocation: { lat: '47.464699', lon: '8.54917' }, + DestRegion: 'CH-ZH', + OriginAirportID: 'ZRH', + OriginRegion: 'CH-ZH', + DestCityName: 'Zurich', + FlightTimeHour: 5.0, + FlightDelayMin: 300, + }, + { + FlightNum: '1IRBW25', + DestCountry: 'CA', + OriginWeather: 'Thunder & Lightning', + OriginCityName: 'Rome', + AvgTicketPrice: 585.1843103083941, + DistanceMiles: 4203.1829639346715, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'Ottawa Macdonald-Cartier International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 6764.367283910481, + timestamp: '2018-01-01T04:54:59', + DestLocation: { lat: '45.32249832', lon: '-75.66919708' }, + DestAirportID: 'YOW', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 614.9424803554983, + Origin: 'Ciampino___G. B. Pastine International Airport', + OriginLocation: { lat: '41.7994', lon: '12.5949' }, + DestRegion: 'CA-ON', + OriginAirportID: 'RM12', + OriginRegion: 'IT-62', + DestCityName: 'Ottawa', + FlightTimeHour: 10.249041339258305, + FlightDelayMin: 0, + }, + { + FlightNum: 'M05KE88', + DestCountry: 'IN', + OriginWeather: 'Heavy Fog', + OriginCityName: 'Milan', + AvgTicketPrice: 960.8697358054351, + DistanceMiles: 4377.166776556647, + FlightDelay: true, + DestWeather: 'Cloudy', + Dest: 'Rajiv Gandhi International Airport', + FlightDelayType: 'NAS Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 7044.367088850781, + timestamp: '2018-01-01T12:09:35', + DestLocation: { lat: '17.23131752', lon: '78.42985535' }, + DestAirportID: 'HYD', + Carrier: 'Kibana Airlines', + Cancelled: true, + FlightTimeMin: 602.0305907375651, + Origin: 'Milano Linate Airport', + OriginLocation: { lat: '45.445099', lon: '9.27674' }, + DestRegion: 'SE-BD', + OriginAirportID: 'MI11', + OriginRegion: 'IT-25', + DestCityName: 'Hyderabad', + FlightTimeHour: 10.033843178959419, + FlightDelayMin: 15, + }, + { + FlightNum: 'SNI3M1Z', + DestCountry: 'IT', + OriginWeather: 'Cloudy', + OriginCityName: 'Moscow', + AvgTicketPrice: 296.8777725965789, + DistanceMiles: 1303.5538675692512, + FlightDelay: false, + DestWeather: 'Rain', + Dest: "Treviso-Sant'Angelo Airport", + FlightDelayType: 'No Delay', + OriginCountry: 'RU', + dayOfWeek: 0, + DistanceKilometers: 2097.866595449369, + timestamp: '2018-01-01T12:09:35', + DestLocation: { lat: '45.648399', lon: '12.1944' }, + DestAirportID: 'TV01', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 174.82221628744742, + Origin: 'Sheremetyevo International Airport', + OriginLocation: { lat: '55.972599', lon: '37.4146' }, + DestRegion: 'IT-34', + OriginAirportID: 'SVO', + OriginRegion: 'RU-MOS', + DestCityName: 'Treviso', + FlightTimeHour: 2.9137036047907903, + FlightDelayMin: 0, + }, + { + FlightNum: 'JQ2XXQ5', + DestCountry: 'FI', + OriginWeather: 'Rain', + OriginCityName: 'Albuquerque', + AvgTicketPrice: 906.4379477399872, + DistanceMiles: 5313.8222112173335, + FlightDelay: false, + DestWeather: 'Rain', + Dest: 'Helsinki Vantaa Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 8551.76789268935, + timestamp: '2018-01-01T22:06:14', + DestLocation: { lat: '60.31719971', lon: '24.9633007' }, + DestAirportID: 'HEL', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 503.04517015819704, + Origin: 'Albuquerque International Sunport Airport', + OriginLocation: { lat: '35.040199', lon: '-106.609001' }, + DestRegion: 'FI-ES', + OriginAirportID: 'ABQ', + OriginRegion: 'US-NM', + DestCityName: 'Helsinki', + FlightTimeHour: 8.384086169303284, + FlightDelayMin: 0, + }, + { + FlightNum: 'V30ITD0', + DestCountry: 'AT', + OriginWeather: 'Rain', + OriginCityName: 'Venice', + AvgTicketPrice: 704.4637710312036, + DistanceMiles: 268.99172653633303, + FlightDelay: false, + DestWeather: 'Cloudy', + Dest: 'Vienna International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 432.90022115088834, + timestamp: '2018-01-01T11:52:34', + DestLocation: { lat: '48.11029816', lon: '16.56970024' }, + DestAirportID: 'VIE', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 36.07501842924069, + Origin: 'Venice Marco Polo Airport', + OriginLocation: { lat: '45.505299', lon: '12.3519' }, + DestRegion: 'AT-9', + OriginAirportID: 'VE05', + OriginRegion: 'IT-34', + DestCityName: 'Vienna', + FlightTimeHour: 0.6012503071540115, + FlightDelayMin: 0, + }, + { + FlightNum: 'P0WMFH7', + DestCountry: 'CN', + OriginWeather: 'Heavy Fog', + OriginCityName: 'Mexico City', + AvgTicketPrice: 922.499077027416, + DistanceMiles: 8025.381414737853, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'Shanghai Pudong International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'MX', + dayOfWeek: 0, + DistanceKilometers: 12915.599427519877, + timestamp: '2018-01-01T02:13:46', + DestLocation: { lat: '31.14340019', lon: '121.8050003' }, + DestAirportID: 'PVG', + Carrier: 'Logstash Airways', + Cancelled: true, + FlightTimeMin: 679.7683909220988, + Origin: 'Licenciado Benito Juarez International Airport', + OriginLocation: { lat: '19.4363', lon: '-99.072098' }, + DestRegion: 'SE-BD', + OriginAirportID: 'AICM', + OriginRegion: 'MX-DIF', + DestCityName: 'Shanghai', + FlightTimeHour: 11.32947318203498, + FlightDelayMin: 0, + }, + { + FlightNum: 'VT9O2KD', + DestCountry: 'CA', + OriginWeather: 'Rain', + OriginCityName: 'Naples', + AvgTicketPrice: 374.9592762864519, + DistanceMiles: 4311.560440686985, + FlightDelay: false, + DestWeather: 'Rain', + Dest: 'Ottawa Macdonald-Cartier International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 6938.783925856956, + timestamp: '2018-01-01T14:21:13', + DestLocation: { lat: '45.32249832', lon: '-75.66919708' }, + DestAirportID: 'YOW', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 330.41828218366453, + Origin: 'Naples International Airport', + OriginLocation: { lat: '40.886002', lon: '14.2908' }, + DestRegion: 'CA-ON', + OriginAirportID: 'NA01', + OriginRegion: 'IT-72', + DestCityName: 'Ottawa', + FlightTimeHour: 5.506971369727742, + FlightDelayMin: 0, + }, + { + FlightNum: 'NRHSVG8', + DestCountry: 'PR', + OriginWeather: 'Cloudy', + OriginCityName: 'Rome', + AvgTicketPrice: 552.9173708459598, + DistanceMiles: 4806.775668847457, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'Luis Munoz Marin International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 7735.755582005642, + timestamp: '2018-01-01T17:42:53', + DestLocation: { lat: '18.43939972', lon: '-66.00180054' }, + DestAirportID: 'SJU', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 407.1450306318759, + Origin: 'Ciampino___G. B. Pastine International Airport', + OriginLocation: { lat: '41.7994', lon: '12.5949' }, + DestRegion: 'PR-U-A', + OriginAirportID: 'RM12', + OriginRegion: 'IT-62', + DestCityName: 'San Juan', + FlightTimeHour: 6.7857505105312645, + FlightDelayMin: 0, + }, + { + FlightNum: 'YIPS2BZ', + DestCountry: 'DE', + OriginWeather: 'Thunder & Lightning', + OriginCityName: 'Chengdu', + AvgTicketPrice: 566.4875569256166, + DistanceMiles: 4896.74792596565, + FlightDelay: false, + DestWeather: 'Sunny', + Dest: 'Cologne Bonn Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'CN', + dayOfWeek: 0, + DistanceKilometers: 7880.551894165264, + timestamp: '2018-01-01T19:55:32', + DestLocation: { lat: '50.86589813', lon: '7.142739773' }, + DestAirportID: 'CGN', + Carrier: 'Kibana Airlines', + Cancelled: true, + FlightTimeMin: 656.7126578471053, + Origin: 'Chengdu Shuangliu International Airport', + OriginLocation: { lat: '30.57850075', lon: '103.9469986' }, + DestRegion: 'DE-NW', + OriginAirportID: 'CTU', + OriginRegion: 'SE-BD', + DestCityName: 'Cologne', + FlightTimeHour: 10.945210964118422, + FlightDelayMin: 0, + }, + { + FlightNum: 'C7IBZ42', + DestCountry: 'IT', + OriginWeather: 'Thunder & Lightning', + OriginCityName: 'Mexico City', + AvgTicketPrice: 989.9527866266118, + DistanceMiles: 6244.404143498341, + FlightDelay: false, + DestWeather: 'Damaging Wind', + Dest: 'Venice Marco Polo Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'MX', + dayOfWeek: 0, + DistanceKilometers: 10049.394341914194, + timestamp: '2018-01-01T07:49:27', + DestLocation: { lat: '45.505299', lon: '12.3519' }, + DestAirportID: 'VE05', + Carrier: 'Logstash Airways', + Cancelled: true, + FlightTimeMin: 773.0303339933996, + Origin: 'Licenciado Benito Juarez International Airport', + OriginLocation: { lat: '19.4363', lon: '-99.072098' }, + DestRegion: 'IT-34', + OriginAirportID: 'AICM', + OriginRegion: 'MX-DIF', + DestCityName: 'Venice', + FlightTimeHour: 12.883838899889993, + FlightDelayMin: 0, + }, + { + FlightNum: '7TTZM4I', + DestCountry: 'AR', + OriginWeather: 'Rain', + OriginCityName: 'Cleveland', + AvgTicketPrice: 569.6132552035547, + DistanceMiles: 5450.245542110189, + FlightDelay: true, + DestWeather: 'Cloudy', + Dest: 'Ministro Pistarini International Airport', + FlightDelayType: 'NAS Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 8771.31996172178, + timestamp: '2018-01-01T01:30:47', + DestLocation: { lat: '-34.8222', lon: '-58.5358' }, + DestAirportID: 'EZE', + Carrier: 'ES-Air', + Cancelled: false, + FlightTimeMin: 704.7169201324446, + Origin: 'Cleveland Hopkins International Airport', + OriginLocation: { lat: '41.4117012', lon: '-81.84980011' }, + DestRegion: 'SE-BD', + OriginAirportID: 'CLE', + OriginRegion: 'US-OH', + DestCityName: 'Buenos Aires', + FlightTimeHour: 11.745282002207409, + FlightDelayMin: 30, + }, + { + FlightNum: 'CSW89S3', + DestCountry: 'CN', + OriginWeather: 'Hail', + OriginCityName: 'Olenegorsk', + AvgTicketPrice: 277.4297073844173, + DistanceMiles: 4202.458848620224, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'Shanghai Pudong International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'RU', + dayOfWeek: 0, + DistanceKilometers: 6763.2019332738655, + timestamp: '2018-01-01T07:58:17', + DestLocation: { lat: '31.14340019', lon: '121.8050003' }, + DestAirportID: 'PVG', + Carrier: 'ES-Air', + Cancelled: false, + FlightTimeMin: 355.95799648809816, + Origin: 'Olenya Air Base', + OriginLocation: { lat: '68.15180206', lon: '33.46390152' }, + DestRegion: 'SE-BD', + OriginAirportID: 'XLMO', + OriginRegion: 'RU-MUR', + DestCityName: 'Shanghai', + FlightTimeHour: 5.932633274801636, + FlightDelayMin: 0, + }, + { + FlightNum: 'RBFKZBX', + DestCountry: 'IN', + OriginWeather: 'Cloudy', + OriginCityName: 'Casper', + AvgTicketPrice: 772.1008456460212, + DistanceMiles: 7507.304095087099, + FlightDelay: true, + DestWeather: 'Clear', + Dest: 'Indira Gandhi International Airport', + FlightDelayType: 'Late Aircraft Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 12081.834801603853, + timestamp: '2018-01-01T00:02:06', + DestLocation: { lat: '28.5665', lon: '77.103104' }, + DestAirportID: 'DEL', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 875.1146751002408, + Origin: 'Casper-Natrona County International Airport', + OriginLocation: { lat: '42.90800095', lon: '-106.4639969' }, + DestRegion: 'SE-BD', + OriginAirportID: 'CPR', + OriginRegion: 'US-WY', + DestCityName: 'New Delhi', + FlightTimeHour: 14.585244585004013, + FlightDelayMin: 120, + }, + { + FlightNum: 'R43CELD', + DestCountry: 'US', + OriginWeather: 'Cloudy', + OriginCityName: 'Erie', + AvgTicketPrice: 167.59992219374266, + DistanceMiles: 965.1786928006221, + FlightDelay: true, + DestWeather: 'Clear', + Dest: 'Wichita Mid Continent Airport', + FlightDelayType: 'Carrier Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 1553.3045381865245, + timestamp: '2018-01-01T01:08:20', + DestLocation: { lat: '37.64989853', lon: '-97.43309784' }, + DestAirportID: 'ICT', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 373.96688277078687, + Origin: 'Erie International Tom Ridge Field', + OriginLocation: { lat: '42.08312701', lon: '-80.17386675' }, + DestRegion: 'US-KS', + OriginAirportID: 'ERI', + OriginRegion: 'US-PA', + DestCityName: 'Wichita', + FlightTimeHour: 6.232781379513114, + FlightDelayMin: 300, + }, + { + FlightNum: '1TJWK8F', + DestCountry: 'CA', + OriginWeather: 'Clear', + OriginCityName: 'Newark', + AvgTicketPrice: 253.21006490337038, + DistanceMiles: 328.5065863946441, + FlightDelay: true, + DestWeather: 'Hail', + Dest: 'Ottawa Macdonald-Cartier International Airport', + FlightDelayType: 'NAS Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 528.6801037747022, + timestamp: '2018-01-01T01:08:20', + DestLocation: { lat: '45.32249832', lon: '-75.66919708' }, + DestAirportID: 'YOW', + Carrier: 'ES-Air', + Cancelled: false, + FlightTimeMin: 130.66770029036172, + Origin: 'Newark Liberty International Airport', + OriginLocation: { lat: '40.69250107', lon: '-74.16870117' }, + DestRegion: 'CA-ON', + OriginAirportID: 'EWR', + OriginRegion: 'US-NJ', + DestCityName: 'Ottawa', + FlightTimeHour: 2.177795004839362, + FlightDelayMin: 90, + }, + { + FlightNum: 'UDTSN13', + DestCountry: 'JP', + OriginWeather: 'Sunny', + OriginCityName: 'Copenhagen', + AvgTicketPrice: 917.2476203642742, + DistanceMiles: 5354.622537746889, + FlightDelay: false, + DestWeather: 'Damaging Wind', + Dest: 'Itami Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'DK', + dayOfWeek: 0, + DistanceKilometers: 8617.42965338773, + timestamp: '2018-01-01T07:48:35', + DestLocation: { lat: '34.78549957', lon: '135.4380035' }, + DestAirportID: 'ITM', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 574.4953102258486, + Origin: 'Copenhagen Kastrup Airport', + OriginLocation: { lat: '55.61790085', lon: '12.65600014' }, + DestRegion: 'SE-BD', + OriginAirportID: 'CPH', + OriginRegion: 'DK-84', + DestCityName: 'Osaka', + FlightTimeHour: 9.574921837097476, + FlightDelayMin: 0, + }, + { + FlightNum: '7Y08OAU', + DestCountry: 'AT', + OriginWeather: 'Heavy Fog', + OriginCityName: 'Seattle', + AvgTicketPrice: 451.5911757026697, + DistanceMiles: 5403.40296642641, + FlightDelay: false, + DestWeather: 'Heavy Fog', + Dest: 'Vienna International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 8695.934143600545, + timestamp: '2018-01-01T18:57:21', + DestLocation: { lat: '48.11029816', lon: '16.56970024' }, + DestAirportID: 'VIE', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 579.728942906703, + Origin: 'Seattle Tacoma International Airport', + OriginLocation: { lat: '47.44900131', lon: '-122.3089981' }, + DestRegion: 'AT-9', + OriginAirportID: 'SEA', + OriginRegion: 'US-WA', + DestCityName: 'Vienna', + FlightTimeHour: 9.66214904844505, + FlightDelayMin: 0, + }, + { + FlightNum: 'MH65PPZ', + DestCountry: 'FR', + OriginWeather: 'Rain', + OriginCityName: 'Berlin', + AvgTicketPrice: 307.0672008820741, + DistanceMiles: 529.8263707088325, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'Charles de Gaulle International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'DE', + dayOfWeek: 0, + DistanceKilometers: 852.6728907420354, + timestamp: '2018-01-01T13:18:25', + DestLocation: { lat: '49.01279831', lon: '2.549999952' }, + DestAirportID: 'CDG', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 50.15722886717855, + Origin: 'Berlin-Tegel Airport', + OriginLocation: { lat: '52.5597', lon: '13.2877' }, + DestRegion: 'FR-J', + OriginAirportID: 'TXL', + OriginRegion: 'DE-BE', + DestCityName: 'Paris', + FlightTimeHour: 0.8359538144529759, + FlightDelayMin: 0, + }, + { + FlightNum: '7SFSTEH', + DestCountry: 'JP', + OriginWeather: 'Thunder & Lightning', + OriginCityName: 'Manchester', + AvgTicketPrice: 268.24159591388866, + DistanceMiles: 5900.673562063734, + FlightDelay: false, + DestWeather: 'Rain', + Dest: 'Narita International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'GB', + dayOfWeek: 0, + DistanceKilometers: 9496.213593065899, + timestamp: '2018-01-01T08:20:35', + DestLocation: { lat: '35.76470184', lon: '140.3860016' }, + DestAirportID: 'NRT', + Carrier: 'ES-Air', + Cancelled: false, + FlightTimeMin: 527.5674218369944, + Origin: 'Manchester Airport', + OriginLocation: { lat: '53.35369873', lon: '-2.274950027' }, + DestRegion: 'SE-BD', + OriginAirportID: 'MAN', + OriginRegion: 'GB-ENG', + DestCityName: 'Tokyo', + FlightTimeHour: 8.792790363949907, + FlightDelayMin: 0, + }, + { + FlightNum: '430NKQO', + DestCountry: 'JP', + OriginWeather: 'Rain', + OriginCityName: 'Helsinki', + AvgTicketPrice: 975.8126321392289, + DistanceMiles: 4800.213800795046, + FlightDelay: false, + DestWeather: 'Hail', + Dest: 'Itami Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'FI', + dayOfWeek: 0, + DistanceKilometers: 7725.195279026703, + timestamp: '2018-01-01T15:38:32', + DestLocation: { lat: '34.78549957', lon: '135.4380035' }, + DestAirportID: 'ITM', + Carrier: 'Kibana Airlines', + Cancelled: true, + FlightTimeMin: 386.2597639513352, + Origin: 'Helsinki Vantaa Airport', + OriginLocation: { lat: '60.31719971', lon: '24.9633007' }, + DestRegion: 'SE-BD', + OriginAirportID: 'HEL', + OriginRegion: 'FI-ES', + DestCityName: 'Osaka', + FlightTimeHour: 6.437662732522253, + FlightDelayMin: 0, + }, + { + FlightNum: '3YAQM9U', + DestCountry: 'US', + OriginWeather: 'Clear', + OriginCityName: 'Phoenix', + AvgTicketPrice: 134.21454568995148, + DistanceMiles: 304.2189898030449, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'San Diego International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 489.59300592559157, + timestamp: '2018-01-01T03:08:45', + DestLocation: { lat: '32.73360062', lon: '-117.1900024' }, + DestAirportID: 'SAN', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 24.47965029627958, + Origin: 'Phoenix Sky Harbor International Airport', + OriginLocation: { lat: '33.43429947', lon: '-112.012001' }, + DestRegion: 'US-CA', + OriginAirportID: 'PHX', + OriginRegion: 'US-AZ', + DestCityName: 'San Diego', + FlightTimeHour: 0.4079941716046597, + FlightDelayMin: 0, + }, + { + FlightNum: 'HX0WBLI', + DestCountry: 'IT', + OriginWeather: 'Damaging Wind', + OriginCityName: 'Chitose / Tomakomai', + AvgTicketPrice: 988.8975638746068, + DistanceMiles: 5650.511340218511, + FlightDelay: false, + DestWeather: 'Sunny', + Dest: 'Verona Villafranca Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'JP', + dayOfWeek: 0, + DistanceKilometers: 9093.616522312619, + timestamp: '2018-01-01T01:16:59', + DestLocation: { lat: '45.395699', lon: '10.8885' }, + DestAirportID: 'VR10', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 568.3510326445387, + Origin: 'New Chitose Airport', + OriginLocation: { lat: '42.77519989', lon: '141.6920013' }, + DestRegion: 'IT-34', + OriginAirportID: 'CTS', + OriginRegion: 'SE-BD', + DestCityName: 'Verona', + FlightTimeHour: 9.472517210742312, + FlightDelayMin: 0, + }, + { + FlightNum: 'BSVTVSA', + DestCountry: 'CH', + OriginWeather: 'Rain', + OriginCityName: 'Tulsa', + AvgTicketPrice: 511.0672203518154, + DistanceMiles: 5028.070244660456, + FlightDelay: false, + DestWeather: 'Rain', + Dest: 'Zurich Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 8091.894679822838, + timestamp: '2018-01-01T18:00:59', + DestLocation: { lat: '47.464699', lon: '8.54917' }, + DestAirportID: 'ZRH', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 425.8891936748862, + Origin: 'Tulsa International Airport', + OriginLocation: { lat: '36.19839859', lon: '-95.88809967' }, + DestRegion: 'CH-ZH', + OriginAirportID: 'TUL', + OriginRegion: 'US-OK', + DestCityName: 'Zurich', + FlightTimeHour: 7.09815322791477, + FlightDelayMin: 0, + }, + { + FlightNum: 'OODIP58', + DestCountry: 'CN', + OriginWeather: 'Thunder & Lightning', + OriginCityName: 'Abu Dhabi', + AvgTicketPrice: 252.9119662217096, + DistanceMiles: 3032.4467769272865, + FlightDelay: true, + DestWeather: 'Sunny', + Dest: 'Chengdu Shuangliu International Airport', + FlightDelayType: 'Late Aircraft Delay', + OriginCountry: 'AE', + dayOfWeek: 0, + DistanceKilometers: 4880.250025767267, + timestamp: '2018-01-01T12:05:14', + DestLocation: { lat: '30.57850075', lon: '103.9469986' }, + DestAirportID: 'CTU', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 490.3500017178178, + Origin: 'Abu Dhabi International Airport', + OriginLocation: { lat: '24.43300056', lon: '54.65110016' }, + DestRegion: 'SE-BD', + OriginAirportID: 'AUH', + OriginRegion: 'SE-BD', + DestCityName: 'Chengdu', + FlightTimeHour: 8.172500028630298, + FlightDelayMin: 165, + }, + { + FlightNum: 'J6XATW0', + DestCountry: 'JP', + OriginWeather: 'Damaging Wind', + OriginCityName: 'Catania', + AvgTicketPrice: 572.072088442, + DistanceMiles: 6298.772997224989, + FlightDelay: true, + DestWeather: 'Damaging Wind', + Dest: 'Narita International Airport', + FlightDelayType: 'NAS Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 10136.892530446054, + timestamp: '2018-01-01T04:18:52', + DestLocation: { lat: '35.76470184', lon: '140.3860016' }, + DestAirportID: 'NRT', + Carrier: 'ES-Air', + Cancelled: false, + FlightTimeMin: 979.7410442038378, + Origin: 'Catania-Fontanarossa Airport', + OriginLocation: { lat: '37.466801', lon: '15.0664' }, + DestRegion: 'SE-BD', + OriginAirportID: 'CT03', + OriginRegion: 'IT-82', + DestCityName: 'Tokyo', + FlightTimeHour: 16.329017403397295, + FlightDelayMin: 135, + }, + { + FlightNum: 'JB2BVNL', + DestCountry: 'JP', + OriginWeather: 'Cloudy', + OriginCityName: 'Louisville', + AvgTicketPrice: 676.8834852133116, + DistanceMiles: 6553.342465155623, + FlightDelay: true, + DestWeather: 'Clear', + Dest: 'Narita International Airport', + FlightDelayType: 'Late Aircraft Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 10546.582376243412, + timestamp: '2018-01-01T08:31:08', + DestLocation: { lat: '35.76470184', lon: '140.3860016' }, + DestAirportID: 'NRT', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 963.3273125888152, + Origin: 'Louisville International Standiford Field', + OriginLocation: { lat: '38.1744', lon: '-85.736' }, + DestRegion: 'SE-BD', + OriginAirportID: 'SDF', + OriginRegion: 'US-KY', + DestCityName: 'Tokyo', + FlightTimeHour: 16.055455209813587, + FlightDelayMin: 210, + }, + { + FlightNum: '8SHQI41', + DestCountry: 'US', + OriginWeather: 'Cloudy', + OriginCityName: 'Spokane', + AvgTicketPrice: 131.81910243159925, + DistanceMiles: 1228.3139778743412, + FlightDelay: false, + DestWeather: 'Sunny', + Dest: 'Wichita Mid Continent Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 1976.7797304082037, + timestamp: '2018-01-01T10:36:25', + DestLocation: { lat: '37.64989853', lon: '-97.43309784' }, + DestAirportID: 'ICT', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 141.19855217201456, + Origin: 'Spokane International Airport', + OriginLocation: { lat: '47.61989975', lon: '-117.5339966' }, + DestRegion: 'US-KS', + OriginAirportID: 'GEG', + OriginRegion: 'US-WA', + DestCityName: 'Wichita', + FlightTimeHour: 2.3533092028669094, + FlightDelayMin: 0, + }, + { + FlightNum: 'IK1PCVN', + DestCountry: 'RU', + OriginWeather: 'Clear', + OriginCityName: 'Portland', + AvgTicketPrice: 470.6837477903439, + DistanceMiles: 5339.976001314777, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'Sheremetyevo International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 8593.858337859929, + timestamp: '2018-01-01T14:04:21', + DestLocation: { lat: '55.972599', lon: '37.4146' }, + DestAirportID: 'SVO', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 716.1548614883274, + Origin: 'Portland International Airport', + OriginLocation: { lat: '45.58869934', lon: '-122.5979996' }, + DestRegion: 'RU-MOS', + OriginAirportID: 'PDX', + OriginRegion: 'US-OR', + DestCityName: 'Moscow', + FlightTimeHour: 11.93591435813879, + FlightDelayMin: 0, + }, + { + FlightNum: 'U8SLHLH', + DestCountry: 'CO', + OriginWeather: 'Hail', + OriginCityName: 'Jebel Ali', + AvgTicketPrice: 547.6349165149929, + DistanceMiles: 8477.112527527572, + FlightDelay: true, + DestWeather: 'Cloudy', + Dest: 'El Dorado International Airport', + FlightDelayType: 'NAS Delay', + OriginCountry: 'AE', + dayOfWeek: 0, + DistanceKilometers: 13642.590183501334, + timestamp: '2018-01-01T04:49:05', + DestLocation: { lat: '4.70159', lon: '-74.1469' }, + DestAirportID: 'BOG', + Carrier: 'ES-Air', + Cancelled: false, + FlightTimeMin: 1064.4707273929525, + Origin: 'Al Maktoum International Airport', + OriginLocation: { lat: '24.896356', lon: '55.161389' }, + DestRegion: 'SE-BD', + OriginAirportID: 'DWC', + OriginRegion: 'SE-BD', + DestCityName: 'Bogota', + FlightTimeHour: 17.741178789882543, + FlightDelayMin: 90, + }, + { + FlightNum: 'LAEYSCF', + DestCountry: 'IT', + OriginWeather: 'Damaging Wind', + OriginCityName: 'Greensboro', + AvgTicketPrice: 741.0225650260804, + DistanceMiles: 4416.574730450814, + FlightDelay: false, + DestWeather: 'Sunny', + Dest: 'Turin Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 7107.788043002635, + timestamp: '2018-01-01T12:56:09', + DestLocation: { lat: '45.200802', lon: '7.64963' }, + DestAirportID: 'TO11', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 394.8771135001464, + Origin: 'Piedmont Triad International Airport', + OriginLocation: { lat: '36.09780121', lon: '-79.93730164' }, + DestRegion: 'IT-21', + OriginAirportID: 'GSO', + OriginRegion: 'US-NC', + DestCityName: 'Torino', + FlightTimeHour: 6.58128522500244, + FlightDelayMin: 0, + }, + { + FlightNum: '17NH0H3', + DestCountry: 'CA', + OriginWeather: 'Cloudy', + OriginCityName: 'Treviso', + AvgTicketPrice: 404.731389894892, + DistanceMiles: 4614.531201475585, + FlightDelay: false, + DestWeather: 'Hail', + Dest: 'Winnipeg / James Armstrong Richardson International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 7426.368101907524, + timestamp: '2018-01-01T18:14:14', + DestLocation: { lat: '49.90999985', lon: '-97.23989868' }, + DestAirportID: 'YWG', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 353.6365762813107, + Origin: "Treviso-Sant'Angelo Airport", + OriginLocation: { lat: '45.648399', lon: '12.1944' }, + DestRegion: 'CA-MB', + OriginAirportID: 'TV01', + OriginRegion: 'IT-34', + DestCityName: 'Winnipeg', + FlightTimeHour: 5.893942938021845, + FlightDelayMin: 0, + }, + { + FlightNum: 'EA3F2E4', + DestCountry: 'CN', + OriginWeather: 'Clear', + OriginCityName: 'Catania', + AvgTicketPrice: 954.2386926506058, + DistanceMiles: 4927.508301892961, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'Chengdu Shuangliu International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 7930.055920601625, + timestamp: '2018-01-01T21:30:40', + DestLocation: { lat: '30.57850075', lon: '103.9469986' }, + DestAirportID: 'CTU', + Carrier: 'ES-Air', + Cancelled: false, + FlightTimeMin: 566.4325657572589, + Origin: 'Catania-Fontanarossa Airport', + OriginLocation: { lat: '37.466801', lon: '15.0664' }, + DestRegion: 'SE-BD', + OriginAirportID: 'CT03', + OriginRegion: 'IT-82', + DestCityName: 'Chengdu', + FlightTimeHour: 9.440542762620982, + FlightDelayMin: 0, + }, + { + FlightNum: 'HF9AP10', + DestCountry: 'US', + OriginWeather: 'Sunny', + OriginCityName: 'New York', + AvgTicketPrice: 231.96273728774017, + DistanceMiles: 2445.8920569992383, + FlightDelay: false, + DestWeather: 'Heavy Fog', + Dest: 'San Diego International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 3936.2817065793824, + timestamp: '2018-01-01T01:50:27', + DestLocation: { lat: '32.73360062', lon: '-117.1900024' }, + DestAirportID: 'SAN', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 262.4187804386255, + Origin: 'John F Kennedy International Airport', + OriginLocation: { lat: '40.63980103', lon: '-73.77890015' }, + DestRegion: 'US-CA', + OriginAirportID: 'JFK', + OriginRegion: 'US-NY', + DestCityName: 'San Diego', + FlightTimeHour: 4.3736463406437585, + FlightDelayMin: 0, + }, + { + FlightNum: '1Y5O0VK', + DestCountry: 'AR', + OriginWeather: 'Rain', + OriginCityName: 'Quito', + AvgTicketPrice: 453.7551255012854, + DistanceMiles: 2708.548842821117, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'Ministro Pistarini International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'EC', + dayOfWeek: 0, + DistanceKilometers: 4358.986828901108, + timestamp: '2018-01-01T23:43:02', + DestLocation: { lat: '-34.8222', lon: '-58.5358' }, + DestAirportID: 'EZE', + Carrier: 'ES-Air', + Cancelled: false, + FlightTimeMin: 242.16593493895047, + Origin: 'Mariscal Sucre International Airport', + OriginLocation: { lat: '-0.129166667', lon: '-78.3575' }, + DestRegion: 'AR-B', + OriginAirportID: 'UIO', + OriginRegion: 'SE-BD', + DestCityName: 'Buenos Aires', + FlightTimeHour: 4.036098915649174, + FlightDelayMin: 0, + }, + { + FlightNum: 'SLJ4EVS', + DestCountry: 'AT', + OriginWeather: 'Damaging Wind', + OriginCityName: 'Toronto', + AvgTicketPrice: 334.7392896190951, + DistanceMiles: 4329.582240070009, + FlightDelay: true, + DestWeather: 'Thunder & Lightning', + Dest: 'Vienna International Airport', + FlightDelayType: 'Security Delay', + OriginCountry: 'CA', + dayOfWeek: 0, + DistanceKilometers: 6967.787200563229, + timestamp: '2018-01-01T05:24:19', + DestLocation: { lat: '48.11029816', lon: '16.56970024' }, + DestAirportID: 'VIE', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 604.8698353272488, + Origin: 'Lester B. Pearson International Airport', + OriginLocation: { lat: '43.67720032', lon: '-79.63059998' }, + DestRegion: 'AT-9', + OriginAirportID: 'YYZ', + OriginRegion: 'CA-ON', + DestCityName: 'Vienna', + FlightTimeHour: 10.081163922120814, + FlightDelayMin: 195, + }, + { + FlightNum: 'ZTL6FPB', + DestCountry: 'US', + OriginWeather: 'Heavy Fog', + OriginCityName: 'Bergamo', + AvgTicketPrice: 926.6665998573354, + DistanceMiles: 4643.872252383584, + FlightDelay: false, + DestWeather: 'Cloudy', + Dest: 'Louisville International Standiford Field', + FlightDelayType: 'No Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 7473.587946140007, + timestamp: '2018-01-01T00:47:09', + DestLocation: { lat: '38.1744', lon: '-85.736' }, + DestAirportID: 'SDF', + Carrier: 'ES-Air', + Cancelled: false, + FlightTimeMin: 679.417086012728, + Origin: 'Il Caravaggio International Airport', + OriginLocation: { lat: '45.673901', lon: '9.70417' }, + DestRegion: 'US-KY', + OriginAirportID: 'BG01', + OriginRegion: 'IT-25', + DestCityName: 'Louisville', + FlightTimeHour: 11.323618100212133, + FlightDelayMin: 0, + }, + { + FlightNum: '46J5N4Y', + DestCountry: 'CA', + OriginWeather: 'Hail', + OriginCityName: 'London', + AvgTicketPrice: 664.6811922084918, + DistanceMiles: 3351.640388335116, + FlightDelay: false, + DestWeather: 'Damaging Wind', + Dest: 'Ottawa Macdonald-Cartier International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'GB', + dayOfWeek: 0, + DistanceKilometers: 5393.942349124789, + timestamp: '2018-01-01T17:22:39', + DestLocation: { lat: '45.32249832', lon: '-75.66919708' }, + DestAirportID: 'YOW', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 283.8917025855152, + Origin: 'London Gatwick Airport', + OriginLocation: { lat: '51.14810181', lon: '-0.190277994' }, + DestRegion: 'CA-ON', + OriginAirportID: 'LGW', + OriginRegion: 'GB-ENG', + DestCityName: 'Ottawa', + FlightTimeHour: 4.731528376425254, + FlightDelayMin: 0, + }, + { + FlightNum: 'PVATFF1', + DestCountry: 'CN', + OriginWeather: 'Damaging Wind', + OriginCityName: 'Guangzhou', + AvgTicketPrice: 184.57886708644122, + DistanceMiles: 746.8977963958768, + FlightDelay: false, + DestWeather: 'Rain', + Dest: 'Shanghai Pudong International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'CN', + dayOfWeek: 0, + DistanceKilometers: 1202.015487242926, + timestamp: '2018-01-01T23:53:52', + DestLocation: { lat: '31.14340019', lon: '121.8050003' }, + DestAirportID: 'PVG', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 109.27413520390236, + Origin: 'Guangzhou Baiyun International Airport', + OriginLocation: { lat: '23.39240074', lon: '113.2990036' }, + DestRegion: 'SE-BD', + OriginAirportID: 'CAN', + OriginRegion: 'SE-BD', + DestCityName: 'Shanghai', + FlightTimeHour: 1.821235586731706, + FlightDelayMin: 0, + }, + { + FlightNum: 'NOFPUHM', + DestCountry: 'CH', + OriginWeather: 'Hail', + OriginCityName: 'Miami', + AvgTicketPrice: 650.3807641564442, + DistanceMiles: 4883.762668168842, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'Zurich Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'US', + dayOfWeek: 0, + DistanceKilometers: 7859.654147441516, + timestamp: '2018-01-01T16:25:52', + DestLocation: { lat: '47.464699', lon: '8.54917' }, + DestAirportID: 'ZRH', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 714.5140134037742, + Origin: 'Miami International Airport', + OriginLocation: { lat: '25.79319954', lon: '-80.29060364' }, + DestRegion: 'CH-ZH', + OriginAirportID: 'MIA', + OriginRegion: 'US-FL', + DestCityName: 'Zurich', + FlightTimeHour: 11.908566890062904, + FlightDelayMin: 0, + }, + { + FlightNum: 'S0SNYUV', + DestCountry: 'GB', + OriginWeather: 'Damaging Wind', + OriginCityName: 'Dubai', + AvgTicketPrice: 505.3645318454166, + DistanceMiles: 3420.471874540241, + FlightDelay: true, + DestWeather: 'Rain', + Dest: 'London Heathrow Airport', + FlightDelayType: 'Security Delay', + OriginCountry: 'AE', + dayOfWeek: 0, + DistanceKilometers: 5504.71588846009, + timestamp: '2018-01-01T00:43:34', + DestLocation: { lat: '51.4706', lon: '-0.461941' }, + DestAirportID: 'LHR', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 485.8175493588939, + Origin: 'Dubai International Airport', + OriginLocation: { lat: '25.25279999', lon: '55.36439896' }, + DestRegion: 'GB-ENG', + OriginAirportID: 'DXB', + OriginRegion: 'SE-BD', + DestCityName: 'London', + FlightTimeHour: 8.096959155981565, + FlightDelayMin: 180, + }, + { + FlightNum: 'FZ1FWP0', + DestCountry: 'CA', + OriginWeather: 'Rain', + OriginCityName: 'Mexico City', + AvgTicketPrice: 937.7339299906702, + DistanceMiles: 2230.671063160962, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'Ottawa Macdonald-Cartier International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'MX', + dayOfWeek: 0, + DistanceKilometers: 3589.917091471716, + timestamp: '2018-01-01T20:42:03', + DestLocation: { lat: '45.32249832', lon: '-75.66919708' }, + DestAirportID: 'YOW', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 211.17159361598328, + Origin: 'Licenciado Benito Juarez International Airport', + OriginLocation: { lat: '19.4363', lon: '-99.072098' }, + DestRegion: 'CA-ON', + OriginAirportID: 'AICM', + OriginRegion: 'MX-DIF', + DestCityName: 'Ottawa', + FlightTimeHour: 3.519526560266388, + FlightDelayMin: 0, + }, + { + FlightNum: 'SUXIZJA', + DestCountry: 'GB', + OriginWeather: 'Hail', + OriginCityName: 'Osaka', + AvgTicketPrice: 531.5562550465518, + DistanceMiles: 5873.817368388099, + FlightDelay: false, + DestWeather: 'Sunny', + Dest: 'Manchester Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'JP', + dayOfWeek: 0, + DistanceKilometers: 9452.992738911176, + timestamp: '2018-01-01T14:00:01', + DestLocation: { lat: '53.35369873', lon: '-2.274950027' }, + DestAirportID: 'MAN', + Carrier: 'ES-Air', + Cancelled: true, + FlightTimeMin: 556.0583964065398, + Origin: 'Kansai International Airport', + OriginLocation: { lat: '34.4272995', lon: '135.2440033' }, + DestRegion: 'GB-ENG', + OriginAirportID: 'KIX', + OriginRegion: 'SE-BD', + DestCityName: 'Manchester', + FlightTimeHour: 9.267639940108998, + FlightDelayMin: 0, + }, + { + FlightNum: '92JKFYR', + DestCountry: 'CL', + OriginWeather: 'Sunny', + OriginCityName: 'Tokoname', + AvgTicketPrice: 885.4770333086404, + DistanceMiles: 10864.104232263331, + FlightDelay: true, + DestWeather: 'Sunny', + Dest: 'Comodoro Arturo Merino Benitez International Airport', + FlightDelayType: 'Weather Delay', + OriginCountry: 'JP', + dayOfWeek: 0, + DistanceKilometers: 17484.0809615676, + timestamp: '2018-01-01T14:00:01', + DestLocation: { lat: '-33.39300156', lon: '-70.78579712' }, + DestAirportID: 'SCL', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 1404.9293047359693, + Origin: 'Chubu Centrair International Airport', + OriginLocation: { lat: '34.85839844', lon: '136.8049927' }, + DestRegion: 'SE-BD', + OriginAirportID: 'NGO', + OriginRegion: 'SE-BD', + DestCityName: 'Santiago', + FlightTimeHour: 23.415488412266154, + FlightDelayMin: 60, + }, + { + FlightNum: 'WG1N4RX', + DestCountry: 'RU', + OriginWeather: 'Clear', + OriginCityName: 'Brisbane', + AvgTicketPrice: 305.3721105628916, + DistanceMiles: 8710.169059970298, + FlightDelay: false, + DestWeather: 'Hail', + Dest: 'Olenya Air Base', + FlightDelayType: 'No Delay', + OriginCountry: 'AU', + dayOfWeek: 0, + DistanceKilometers: 14017.658315648841, + timestamp: '2018-01-01T06:32:21', + DestLocation: { lat: '68.15180206', lon: '33.46390152' }, + DestAirportID: 'XLMO', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 667.507538840421, + Origin: 'Brisbane International Airport', + OriginLocation: { lat: '-27.38419914', lon: '153.1170044' }, + DestRegion: 'RU-MUR', + OriginAirportID: 'BNE', + OriginRegion: 'SE-BD', + DestCityName: 'Olenegorsk', + FlightTimeHour: 11.12512564734035, + FlightDelayMin: 0, + }, + { + FlightNum: 'XKFZHUO', + DestCountry: 'RU', + OriginWeather: 'Sunny', + OriginCityName: 'Pisa', + AvgTicketPrice: 694.4927471612291, + DistanceMiles: 1461.43146405065, + FlightDelay: true, + DestWeather: 'Rain', + Dest: 'Sheremetyevo International Airport', + FlightDelayType: 'Carrier Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 2351.9459580811294, + timestamp: '2018-01-01T01:01:51', + DestLocation: { lat: '55.972599', lon: '37.4146' }, + DestAirportID: 'SVO', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 171.79639720540862, + Origin: 'Pisa International Airport', + OriginLocation: { lat: '43.683899', lon: '10.3927' }, + DestRegion: 'RU-MOS', + OriginAirportID: 'PI05', + OriginRegion: 'IT-52', + DestCityName: 'Moscow', + FlightTimeHour: 2.8632732867568103, + FlightDelayMin: 15, + }, + { + FlightNum: 'U4XV7HN', + DestCountry: 'IN', + OriginWeather: 'Clear', + OriginCityName: 'Cagliari', + AvgTicketPrice: 415.89453312482266, + DistanceMiles: 3888.394115635433, + FlightDelay: false, + DestWeather: 'Rain', + Dest: 'Indira Gandhi International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 6257.763739633191, + timestamp: '2018-01-01T10:44:44', + DestLocation: { lat: '28.5665', lon: '77.103104' }, + DestAirportID: 'DEL', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 568.8876126939265, + Origin: 'Cagliari Elmas Airport', + OriginLocation: { lat: '39.251499', lon: '9.05428' }, + DestRegion: 'SE-BD', + OriginAirportID: 'CA07', + OriginRegion: 'IT-88', + DestCityName: 'New Delhi', + FlightTimeHour: 9.481460211565441, + FlightDelayMin: 0, + }, + { + FlightNum: 'R0JFGVC', + DestCountry: 'SE', + OriginWeather: 'Heavy Fog', + OriginCityName: 'London', + AvgTicketPrice: 551.0601339103603, + DistanceMiles: 888.5278930418095, + FlightDelay: false, + DestWeather: 'Rain', + Dest: 'Stockholm-Arlanda Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'GB', + dayOfWeek: 0, + DistanceKilometers: 1429.947033499478, + timestamp: '2018-01-01T10:31:48', + DestLocation: { lat: '59.65190125', lon: '17.91860008' }, + DestAirportID: 'ARN', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 79.44150186108212, + Origin: 'London Luton Airport', + OriginLocation: { lat: '51.87469864', lon: '-0.368333012' }, + DestRegion: 'SE-AB', + OriginAirportID: 'LTN', + OriginRegion: 'GB-ENG', + DestCityName: 'Stockholm', + FlightTimeHour: 1.3240250310180353, + FlightDelayMin: 0, + }, + { + FlightNum: 'TF9BTQL', + DestCountry: 'US', + OriginWeather: 'Clear', + OriginCityName: 'Chitose / Tomakomai', + AvgTicketPrice: 798.7800550805481, + DistanceMiles: 5590.557988612903, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'Rochester International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'JP', + dayOfWeek: 0, + DistanceKilometers: 8997.130955626244, + timestamp: '2018-01-01T06:45:49', + DestLocation: { lat: '43.90829849', lon: '-92.5' }, + DestAirportID: 'RST', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 499.8406086459024, + Origin: 'New Chitose Airport', + OriginLocation: { lat: '42.77519989', lon: '141.6920013' }, + DestRegion: 'US-MN', + OriginAirportID: 'CTS', + OriginRegion: 'SE-BD', + DestCityName: 'Rochester', + FlightTimeHour: 8.33067681076504, + FlightDelayMin: 0, + }, + { + FlightNum: 'CKZBJJ8', + DestCountry: 'JP', + OriginWeather: 'Cloudy', + OriginCityName: 'Tokoname', + AvgTicketPrice: 117.01632685415828, + DistanceMiles: 606.1652997826461, + FlightDelay: true, + DestWeather: 'Clear', + Dest: 'New Chitose Airport', + FlightDelayType: 'Weather Delay', + OriginCountry: 'JP', + dayOfWeek: 0, + DistanceKilometers: 975.5284882134029, + timestamp: '2018-01-01T12:13:01', + DestLocation: { lat: '42.77519989', lon: '141.6920013' }, + DestAirportID: 'CTS', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 387.38402871843545, + Origin: 'Chubu Centrair International Airport', + OriginLocation: { lat: '34.85839844', lon: '136.8049927' }, + DestRegion: 'SE-BD', + OriginAirportID: 'NGO', + OriginRegion: 'SE-BD', + DestCityName: 'Chitose / Tomakomai', + FlightTimeHour: 6.456400478640591, + FlightDelayMin: 330, + }, + { + FlightNum: 'T9QK7GX', + DestCountry: 'US', + OriginWeather: 'Clear', + OriginCityName: 'Mumbai', + AvgTicketPrice: 841.2103842623986, + DistanceMiles: 9037.917857359074, + FlightDelay: false, + DestWeather: 'Hail', + Dest: 'San Antonio International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'IN', + dayOfWeek: 0, + DistanceKilometers: 14545.118876233682, + timestamp: '2018-01-01T12:13:01', + DestLocation: { lat: '29.53370094', lon: '-98.46980286' }, + DestAirportID: 'SAT', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 909.0699297646051, + Origin: 'Chhatrapati Shivaji International Airport', + OriginLocation: { lat: '19.08869934', lon: '72.86789703' }, + DestRegion: 'US-TX', + OriginAirportID: 'BOM', + OriginRegion: 'SE-BD', + DestCityName: 'San Antonio', + FlightTimeHour: 15.151165496076752, + FlightDelayMin: 0, + }, + { + FlightNum: 'B5PRMI8', + DestCountry: 'CH', + OriginWeather: 'Rain', + OriginCityName: 'Zurich', + AvgTicketPrice: 125.4083518148904, + DistanceMiles: 0.0, + FlightDelay: false, + DestWeather: 'Heavy Fog', + Dest: 'Zurich Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'CH', + dayOfWeek: 0, + DistanceKilometers: 0.0, + timestamp: '2018-01-01T10:21:08', + DestLocation: { lat: '47.464699', lon: '8.54917' }, + DestAirportID: 'ZRH', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 0.0, + Origin: 'Zurich Airport', + OriginLocation: { lat: '47.464699', lon: '8.54917' }, + DestRegion: 'CH-ZH', + OriginAirportID: 'ZRH', + OriginRegion: 'CH-ZH', + DestCityName: 'Zurich', + FlightTimeHour: 0.0, + FlightDelayMin: 0, + }, + { + FlightNum: 'OE6XDRI', + DestCountry: 'AT', + OriginWeather: 'Heavy Fog', + OriginCityName: 'Belogorsk', + AvgTicketPrice: 590.1173423147161, + DistanceMiles: 4498.903224225127, + FlightDelay: true, + DestWeather: 'Rain', + Dest: 'Vienna International Airport', + FlightDelayType: 'Carrier Delay', + OriginCountry: 'RU', + dayOfWeek: 0, + DistanceKilometers: 7240.282910487363, + timestamp: '2018-01-01T23:10:15', + DestLocation: { lat: '48.11029816', lon: '16.56970024' }, + DestAirportID: 'VIE', + Carrier: 'ES-Air', + Cancelled: true, + FlightTimeMin: 614.7753766898744, + Origin: 'Ukrainka Air Base', + OriginLocation: { lat: '51.169997', lon: '128.445007' }, + DestRegion: 'AT-9', + OriginAirportID: 'XHBU', + OriginRegion: 'RU-AMU', + DestCityName: 'Vienna', + FlightTimeHour: 10.246256278164573, + FlightDelayMin: 270, + }, + { + FlightNum: '0VH5EM7', + DestCountry: 'IN', + OriginWeather: 'Heavy Fog', + OriginCityName: 'Mumbai', + AvgTicketPrice: 223.59354684926342, + DistanceMiles: 387.3167656891642, + FlightDelay: false, + DestWeather: 'Cloudy', + Dest: 'Rajiv Gandhi International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'IN', + dayOfWeek: 0, + DistanceKilometers: 623.3259129612622, + timestamp: '2018-01-01T20:53:18', + DestLocation: { lat: '17.23131752', lon: '78.42985535' }, + DestAirportID: 'HYD', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 34.62921738673679, + Origin: 'Chhatrapati Shivaji International Airport', + OriginLocation: { lat: '19.08869934', lon: '72.86789703' }, + DestRegion: 'SE-BD', + OriginAirportID: 'BOM', + OriginRegion: 'SE-BD', + DestCityName: 'Hyderabad', + FlightTimeHour: 0.5771536231122798, + FlightDelayMin: 0, + }, + { + FlightNum: 'NHLMKLG', + DestCountry: 'CH', + OriginWeather: 'Rain', + OriginCityName: 'Zurich', + AvgTicketPrice: 178.41351273264587, + DistanceMiles: 0.0, + FlightDelay: false, + DestWeather: 'Heavy Fog', + Dest: 'Zurich Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'CH', + dayOfWeek: 0, + DistanceKilometers: 0.0, + timestamp: '2018-01-01T02:59:15', + DestLocation: { lat: '47.464699', lon: '8.54917' }, + DestAirportID: 'ZRH', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 0.0, + Origin: 'Zurich Airport', + OriginLocation: { lat: '47.464699', lon: '8.54917' }, + DestRegion: 'CH-ZH', + OriginAirportID: 'ZRH', + OriginRegion: 'CH-ZH', + DestCityName: 'Zurich', + FlightTimeHour: 0.0, + FlightDelayMin: 0, + }, + { + FlightNum: '0LT8Q4U', + DestCountry: 'PL', + OriginWeather: 'Thunder & Lightning', + OriginCityName: 'Beijing', + AvgTicketPrice: 416.9128422781053, + DistanceMiles: 4326.352348507669, + FlightDelay: false, + DestWeather: 'Clear', + Dest: 'Warsaw Chopin Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'CN', + dayOfWeek: 0, + DistanceKilometers: 6962.589193956727, + timestamp: '2018-01-01T03:09:37', + DestLocation: { lat: '52.16569901', lon: '20.96710014' }, + DestAirportID: 'WAW', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 580.2157661630606, + Origin: 'Beijing Capital International Airport', + OriginLocation: { lat: '40.08010101', lon: '116.5849991' }, + DestRegion: 'PL-MZ', + OriginAirportID: 'PEK', + OriginRegion: 'SE-BD', + DestCityName: 'Warsaw', + FlightTimeHour: 9.670262769384344, + FlightDelayMin: 0, + }, + { + FlightNum: 'E37OG3P', + DestCountry: 'GB', + OriginWeather: 'Hail', + OriginCityName: 'Cagliari', + AvgTicketPrice: 500.4828351053994, + DistanceMiles: 982.4372639008275, + FlightDelay: false, + DestWeather: 'Sunny', + Dest: 'London Luton Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 1581.0795160352134, + timestamp: '2018-01-01T04:14:28', + DestLocation: { lat: '51.87469864', lon: '-0.368333012' }, + DestAirportID: 'LTN', + Carrier: 'Logstash Airways', + Cancelled: true, + FlightTimeMin: 112.93425114537239, + Origin: 'Cagliari Elmas Airport', + OriginLocation: { lat: '39.251499', lon: '9.05428' }, + DestRegion: 'GB-ENG', + OriginAirportID: 'CA07', + OriginRegion: 'IT-88', + DestCityName: 'London', + FlightTimeHour: 1.8822375190895397, + FlightDelayMin: 0, + }, + { + FlightNum: 'VH5MDUD', + DestCountry: 'CA', + OriginWeather: 'Rain', + OriginCityName: 'Frankfurt am Main', + AvgTicketPrice: 346.7483794066166, + DistanceMiles: 3953.1867605000593, + FlightDelay: false, + DestWeather: 'Heavy Fog', + Dest: 'Lester B. Pearson International Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'DE', + dayOfWeek: 0, + DistanceKilometers: 6362.037393890208, + timestamp: '2018-01-01T23:45:30', + DestLocation: { lat: '43.67720032', lon: '-79.63059998' }, + DestAirportID: 'YYZ', + Carrier: 'Logstash Airways', + Cancelled: false, + FlightTimeMin: 353.4465218827893, + Origin: 'Frankfurt am Main Airport', + OriginLocation: { lat: '50.033333', lon: '8.570556' }, + DestRegion: 'CA-ON', + OriginAirportID: 'FRA', + OriginRegion: 'DE-HE', + DestCityName: 'Toronto', + FlightTimeHour: 5.890775364713154, + FlightDelayMin: 0, + }, + { + FlightNum: 'M21BD4I', + DestCountry: 'CN', + OriginWeather: 'Cloudy', + OriginCityName: 'Tokyo', + AvgTicketPrice: 969.1899314970427, + DistanceMiles: 1754.4121462676542, + FlightDelay: true, + DestWeather: 'Clear', + Dest: "Xi'an Xianyang International Airport", + FlightDelayType: 'NAS Delay', + OriginCountry: 'JP', + dayOfWeek: 0, + DistanceKilometers: 2823.4526611229717, + timestamp: '2018-01-01T20:17:26', + DestLocation: { lat: '34.447102', lon: '108.751999' }, + DestAirportID: 'XIY', + Carrier: 'JetBeats', + Cancelled: false, + FlightTimeMin: 261.1726330561486, + Origin: 'Tokyo Haneda International Airport', + OriginLocation: { lat: '35.552299', lon: '139.779999' }, + DestRegion: 'SE-BD', + OriginAirportID: 'HND', + OriginRegion: 'SE-BD', + DestCityName: "Xi'an", + FlightTimeHour: 4.352877217602477, + FlightDelayMin: 120, + }, + { + FlightNum: 'VDDH65N', + DestCountry: 'IT', + OriginWeather: 'Damaging Wind', + OriginCityName: 'Chengdu', + AvgTicketPrice: 438.00156663741143, + DistanceMiles: 4923.026978969943, + FlightDelay: true, + DestWeather: 'Sunny', + Dest: 'Ciampino___G. B. Pastine International Airport', + FlightDelayType: 'Carrier Delay', + OriginCountry: 'CN', + dayOfWeek: 0, + DistanceKilometers: 7922.843930443404, + timestamp: '2018-01-01T22:01:37', + DestLocation: { lat: '41.7994', lon: '12.5949' }, + DestAirportID: 'RM12', + Carrier: 'ES-Air', + Cancelled: false, + FlightTimeMin: 587.2782824020669, + Origin: 'Chengdu Shuangliu International Airport', + OriginLocation: { lat: '30.57850075', lon: '103.9469986' }, + DestRegion: 'IT-62', + OriginAirportID: 'CTU', + OriginRegion: 'SE-BD', + DestCityName: 'Rome', + FlightTimeHour: 9.787971373367782, + FlightDelayMin: 210, + }, + { + FlightNum: 'QY97TB0', + DestCountry: 'IT', + OriginWeather: 'Clear', + OriginCityName: 'Milan', + AvgTicketPrice: 173.5006794979574, + DistanceMiles: 149.4599064359537, + FlightDelay: false, + DestWeather: 'Cloudy', + Dest: 'Venice Marco Polo Airport', + FlightDelayType: 'No Delay', + OriginCountry: 'IT', + dayOfWeek: 0, + DistanceKilometers: 240.53240366326352, + timestamp: '2018-01-01T14:31:43', + DestLocation: { lat: '45.505299', lon: '12.3519' }, + DestAirportID: 'VE05', + Carrier: 'Kibana Airlines', + Cancelled: false, + FlightTimeMin: 15.03327522895397, + Origin: 'Milano Linate Airport', + OriginLocation: { lat: '45.445099', lon: '9.27674' }, + DestRegion: 'IT-34', + OriginAirportID: 'MI11', + OriginRegion: 'IT-25', + DestCityName: 'Venice', + FlightTimeHour: 0.25055458714923284, + FlightDelayMin: 0, + }, +]; diff --git a/src/plugins/presentation_util/public/components/input_controls/__stories__/decorators.tsx b/src/plugins/presentation_util/public/components/input_controls/__stories__/decorators.tsx new file mode 100644 index 0000000000000..0aaa0e7a8a533 --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/__stories__/decorators.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { Story } from '@storybook/react'; + +const bar = '#c5ced8'; +const panel = '#f7f9fa'; +const background = '#e0e6ec'; +const minHeight = 60; + +const panelStyle = { + height: 165, + width: 400, + background: panel, +}; + +const kqlBarStyle = { background: bar, padding: 16, minHeight, fontStyle: 'italic' }; + +const inputBarStyle = { background: '#fff', padding: 4, minHeight }; + +const layout = (OptionStory: Story) => ( + + KQL Bar + + + + + + + + + + + + + + + + + + +); + +export const decorators = [layout]; diff --git a/src/plugins/presentation_util/public/components/input_controls/__stories__/flights.ts b/src/plugins/presentation_util/public/components/input_controls/__stories__/flights.ts new file mode 100644 index 0000000000000..e405b704796ec --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/__stories__/flights.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { map, uniq } from 'lodash'; +import { EuiSelectableOption } from '@elastic/eui'; + +import { flights } from '../../fixtures/flights'; + +export type Flight = typeof flights[number]; +export type FlightField = keyof Flight; + +export const getOptions = (field: string) => uniq(map(flights, field)).sort(); + +export const getEuiSelectableOptions = (field: string, search?: string): EuiSelectableOption[] => { + const options = getOptions(field) + .map((option) => ({ + label: option + '', + searchableLabel: option + '', + })) + .filter((option) => !search || option.label.toLowerCase().includes(search.toLowerCase())); + if (options.length > 10) options.length = 10; + return options; +}; + +export const flightFieldLabels: Record = { + AvgTicketPrice: 'Average Ticket Price', + Cancelled: 'Cancelled', + Carrier: 'Carrier', + dayOfWeek: 'Day of Week', + Dest: 'Destination', + DestAirportID: 'Destination Airport ID', + DestCityName: 'Destination City', + DestCountry: 'Destination Country', + DestLocation: 'Destination Location', + DestRegion: 'Destination Region', + DestWeather: 'Destination Weather', + DistanceKilometers: 'Distance (km)', + DistanceMiles: 'Distance (mi)', + FlightDelay: 'Flight Delay', + FlightDelayMin: 'Flight Delay (min)', + FlightDelayType: 'Flight Delay Type', + FlightNum: 'Flight Number', + FlightTimeHour: 'Flight Time (hr)', + FlightTimeMin: 'Flight Time (min)', + Origin: 'Origin', + OriginAirportID: 'Origin Airport ID', + OriginCityName: 'Origin City', + OriginCountry: 'Origin Country', + OriginLocation: 'Origin Location', + OriginRegion: 'Origin Region', + OriginWeather: 'Origin Weather', + timestamp: 'Timestamp', +}; + +export const flightFields = Object.keys(flightFieldLabels) as FlightField[]; diff --git a/src/plugins/presentation_util/public/components/input_controls/__stories__/input_controls.stories.tsx b/src/plugins/presentation_util/public/components/input_controls/__stories__/input_controls.stories.tsx new file mode 100644 index 0000000000000..d1ad3af0daf44 --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/__stories__/input_controls.stories.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useEffect, useMemo, useState } from 'react'; + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { decorators } from './decorators'; +import { getEuiSelectableOptions, flightFields, flightFieldLabels, FlightField } from './flights'; +import { OptionsListEmbeddableFactory, OptionsListEmbeddable } from '../control_types/options_list'; +import { ControlFrame } from '../control_frame/control_frame'; + +export default { + title: 'Input Controls', + description: '', + decorators, +}; + +interface OptionsListStorybookArgs { + fields: string[]; + twoLine: boolean; +} + +const storybookArgs = { + twoLine: false, + fields: ['OriginCityName', 'OriginWeather', 'DestCityName', 'DestWeather'], +}; + +const storybookArgTypes = { + fields: { + twoLine: { + control: { type: 'bool' }, + }, + control: { + type: 'check', + options: flightFields, + }, + }, +}; + +const OptionsListStoryComponent = ({ fields, twoLine }: OptionsListStorybookArgs) => { + const [embeddables, setEmbeddables] = useState([]); + + const optionsListEmbeddableFactory = useMemo( + () => + new OptionsListEmbeddableFactory( + ({ field, search }) => + new Promise((r) => setTimeout(() => r(getEuiSelectableOptions(field, search)), 500)) + ), + [] + ); + + useEffect(() => { + const embeddableCreatePromises = fields.map((field) => { + return optionsListEmbeddableFactory.create({ + field, + id: '', + indexPattern: '', + multiSelect: true, + twoLineLayout: twoLine, + title: flightFieldLabels[field as FlightField], + }); + }); + Promise.all(embeddableCreatePromises).then((newEmbeddables) => setEmbeddables(newEmbeddables)); + }, [fields, optionsListEmbeddableFactory, twoLine]); + + return ( + + {embeddables.map((embeddable) => ( + + + + ))} + + ); +}; + +export const OptionsListStory = ({ fields, twoLine }: OptionsListStorybookArgs) => ( + +); + +OptionsListStory.args = storybookArgs; +OptionsListStory.argTypes = storybookArgTypes; diff --git a/src/plugins/presentation_util/public/components/input_controls/control_frame/control_frame.scss b/src/plugins/presentation_util/public/components/input_controls/control_frame/control_frame.scss new file mode 100644 index 0000000000000..ad054be022c32 --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/control_frame/control_frame.scss @@ -0,0 +1,14 @@ +.controlFrame--formControlLayout { + width: 100%; + min-width: $euiSize * 12.5; +} + +.controlFrame--control { + &.optionsList--filterBtnSingle { + height: 100%; + } +} + +.optionsList--filterBtnTwoLine { + width: 100%; +} \ No newline at end of file diff --git a/src/plugins/presentation_util/public/components/input_controls/control_frame/control_frame.tsx b/src/plugins/presentation_util/public/components/input_controls/control_frame/control_frame.tsx new file mode 100644 index 0000000000000..7fa8688ffb368 --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/control_frame/control_frame.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useMemo } from 'react'; +import useMount from 'react-use/lib/useMount'; +import classNames from 'classnames'; +import { EuiFormControlLayout, EuiFormLabel, EuiFormRow } from '@elastic/eui'; + +import { InputControlEmbeddable } from '../embeddable/types'; + +import './control_frame.scss'; + +interface ControlFrameProps { + embeddable: InputControlEmbeddable; + twoLine?: boolean; +} + +export const ControlFrame = ({ twoLine, embeddable }: ControlFrameProps) => { + const embeddableRoot: React.RefObject = useMemo(() => React.createRef(), []); + + useMount(() => { + if (embeddableRoot.current && embeddable) embeddable.render(embeddableRoot.current); + }); + + const form = ( + {embeddable.getInput().title} + ) + } + > +
+ + ); + + return twoLine ? ( + + {form} + + ) : ( + form + ); +}; diff --git a/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/index.ts b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/index.ts new file mode 100644 index 0000000000000..63275f12076ff --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { OptionsListEmbeddableFactory } from './options_list_embeddable_factory'; +export { OptionsListEmbeddable } from './options_list_embeddable'; diff --git a/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list.scss b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list.scss new file mode 100644 index 0000000000000..e9a4ef215733e --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list.scss @@ -0,0 +1,46 @@ +.optionsList--anchorOverride { + display:block; +} + +.optionsList--popoverOverride { + width: 100%; + height: 100%; +} + +.optionsList--items { + @include euiScrollBar; + + overflow-y: auto; + max-height: $euiSize * 30; + width: $euiSize * 25; + max-width: 100%; +} + +.optionsList--filterBtn { + .euiFilterButton__text-hasNotification { + flex-grow: 1; + justify-content: space-between; + width: 0; + } + &.optionsList--filterBtnSingle { + width: 100%; + } + &.optionsList--filterBtnPlaceholder { + .euiFilterButton__textShift { + color: $euiTextSubduedColor; + } + } +} + +.optionsList--filterGroupSingle { + box-shadow: none; + height: 100%; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-top-right-radius: $euiBorderRadius - 1px; + border-bottom-right-radius: $euiBorderRadius - 1px; +} + +.optionsList--filterGroup { + width: 100%; +} diff --git a/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_component.tsx b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_component.tsx new file mode 100644 index 0000000000000..2feb527ff9160 --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_component.tsx @@ -0,0 +1,173 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useMemo, useEffect, useState, useCallback, useRef } from 'react'; +import { debounceTime, tap } from 'rxjs/operators'; +import useMount from 'react-use/lib/useMount'; +import classNames from 'classnames'; +import { Subject } from 'rxjs'; +import { EuiFilterButton, EuiFilterGroup, EuiPopover, EuiSelectableOption } from '@elastic/eui'; + +import { + OptionsListDataFetcher, + OptionsListEmbeddable, + OptionsListEmbeddableInput, +} from './options_list_embeddable'; +import { OptionsListStrings } from './options_list_strings'; +import { InputControlOutput } from '../../embeddable/types'; +import { OptionsListPopover } from './options_list_popover_component'; +import { withEmbeddableSubscription } from '../../../../../../embeddable/public'; + +import './options_list.scss'; + +const toggleAvailableOptions = ( + indices: number[], + availableOptions: EuiSelectableOption[], + enabled: boolean +) => { + const newAvailableOptions = [...availableOptions]; + indices.forEach((index) => (newAvailableOptions[index].checked = enabled ? 'on' : undefined)); + return newAvailableOptions; +}; + +interface OptionsListProps { + input: OptionsListEmbeddableInput; + fetchData: OptionsListDataFetcher; +} + +export const OptionsListInner = ({ input, fetchData }: OptionsListProps) => { + const [availableOptions, setAvailableOptions] = useState([]); + const selectedOptions = useRef>(new Set()); + + // raw search string is stored here so it is remembered when popover is closed. + const [searchString, setSearchString] = useState(''); + const [debouncedSearchString, setDebouncedSearchString] = useState(); + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [loading, setIsLoading] = useState(false); + + const typeaheadSubject = useMemo(() => new Subject(), []); + + useMount(() => { + typeaheadSubject + .pipe( + tap((rawSearchText) => setSearchString(rawSearchText)), + debounceTime(100) + ) + .subscribe((search) => setDebouncedSearchString(search)); + // default selections can be applied here... + }); + + const { indexPattern, timeRange, filters, field, query } = input; + useEffect(() => { + let canceled = false; + setIsLoading(true); + fetchData({ + search: debouncedSearchString, + indexPattern, + timeRange, + filters, + field, + query, + }).then((newOptions) => { + if (canceled) return; + setIsLoading(false); + // We now have new 'availableOptions', we need to ensure the previously selected options are still selected. + const enabledIndices: number[] = []; + selectedOptions.current?.forEach((selectedOption) => { + const optionIndex = newOptions.findIndex( + (availableOption) => availableOption.label === selectedOption + ); + if (optionIndex >= 0) enabledIndices.push(optionIndex); + }); + newOptions = toggleAvailableOptions(enabledIndices, newOptions, true); + setAvailableOptions(newOptions); + }); + return () => { + canceled = true; + }; + }, [indexPattern, timeRange, filters, field, query, debouncedSearchString, fetchData]); + + const updateItem = useCallback( + (index: number) => { + const item = availableOptions?.[index]; + if (!item) return; + + const toggleOff = availableOptions[index].checked === 'on'; + + const newAvailableOptions = toggleAvailableOptions([index], availableOptions, !toggleOff); + setAvailableOptions(newAvailableOptions); + + if (toggleOff) { + selectedOptions.current.delete(item.label); + } else { + selectedOptions.current.add(item.label); + } + }, + [availableOptions] + ); + + const selectedOptionsString = Array.from(selectedOptions.current).join( + OptionsListStrings.summary.getSeparator() + ); + const selectedOptionsLength = Array.from(selectedOptions.current).length; + + const { twoLineLayout } = input; + + const button = ( + setIsPopoverOpen((openState) => !openState)} + isSelected={isPopoverOpen} + numFilters={availableOptions.length} + hasActiveFilters={selectedOptionsLength > 0} + numActiveFilters={selectedOptionsLength} + > + {!selectedOptionsLength ? OptionsListStrings.summary.getPlaceholder() : selectedOptionsString} + + ); + + return ( + + setIsPopoverOpen(false)} + panelPaddingSize="none" + anchorPosition="upLeft" + ownFocus + repositionOnScroll + > + + + + ); +}; + +export const OptionsListComponent = withEmbeddableSubscription< + OptionsListEmbeddableInput, + InputControlOutput, + OptionsListEmbeddable, + { fetchData: OptionsListDataFetcher } +>(OptionsListInner); diff --git a/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_embeddable.tsx b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_embeddable.tsx new file mode 100644 index 0000000000000..4dcc4a75dc1f0 --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_embeddable.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { EuiSelectableOption } from '@elastic/eui'; + +import { OptionsListComponent } from './options_list_component'; +import { Embeddable } from '../../../../../../embeddable/public'; +import { InputControlInput, InputControlOutput } from '../../embeddable/types'; + +interface OptionsListDataFetchProps { + field: string; + search?: string; + indexPattern: string; + query?: InputControlInput['query']; + filters?: InputControlInput['filters']; + timeRange?: InputControlInput['timeRange']; +} + +export type OptionsListDataFetcher = ( + props: OptionsListDataFetchProps +) => Promise; + +export const OPTIONS_LIST_CONTROL = 'optionsListControl'; +export interface OptionsListEmbeddableInput extends InputControlInput { + field: string; + indexPattern: string; + multiSelect: boolean; +} +export class OptionsListEmbeddable extends Embeddable< + OptionsListEmbeddableInput, + InputControlOutput +> { + public readonly type = OPTIONS_LIST_CONTROL; + + private node?: HTMLElement; + private fetchData: OptionsListDataFetcher; + + constructor( + input: OptionsListEmbeddableInput, + output: InputControlOutput, + fetchData: OptionsListDataFetcher + ) { + super(input, output); + this.fetchData = fetchData; + } + + reload = () => {}; + + public render = (node: HTMLElement) => { + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + this.node = node; + ReactDOM.render(, node); + }; +} diff --git a/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_embeddable_factory.ts b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_embeddable_factory.ts new file mode 100644 index 0000000000000..e1850e6715e34 --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_embeddable_factory.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EmbeddableFactoryDefinition } from '../../../../../../embeddable/public'; +import { + OptionsListDataFetcher, + OptionsListEmbeddable, + OptionsListEmbeddableInput, + OPTIONS_LIST_CONTROL, +} from './options_list_embeddable'; + +export class OptionsListEmbeddableFactory implements EmbeddableFactoryDefinition { + public type = OPTIONS_LIST_CONTROL; + private fetchData: OptionsListDataFetcher; + + constructor(fetchData: OptionsListDataFetcher) { + this.fetchData = fetchData; + } + + public create(initialInput: OptionsListEmbeddableInput) { + return Promise.resolve(new OptionsListEmbeddable(initialInput, {}, this.fetchData)); + } + + public isEditable = () => Promise.resolve(false); + + public getDisplayName = () => 'Options List Control'; +} diff --git a/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_popover_component.tsx b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_popover_component.tsx new file mode 100644 index 0000000000000..cd558b99f9aa1 --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_popover_component.tsx @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { + EuiFieldSearch, + EuiFilterSelectItem, + EuiIcon, + EuiLoadingChart, + EuiPopoverTitle, + EuiSelectableOption, + EuiSpacer, +} from '@elastic/eui'; + +import { Subject } from 'rxjs'; +import { OptionsListStrings } from './options_list_strings'; + +interface OptionsListPopoverProps { + loading: boolean; + typeaheadSubject: Subject; + searchString: string; + updateItem: (index: number) => void; + availableOptions: EuiSelectableOption[]; +} + +export const OptionsListPopover = ({ + loading, + updateItem, + searchString, + typeaheadSubject, + availableOptions, +}: OptionsListPopoverProps) => { + return ( + <> + + { + typeaheadSubject.next(event.target.value); + }} + value={searchString} + /> + +
+ {!loading && + availableOptions && + availableOptions.map((item, index) => ( + updateItem(index)} + > + {item.label} + + ))} + {loading && ( +
+
+ + +

{OptionsListStrings.popover.getLoadingMessage()}

+
+
+ )} + + {!loading && (!availableOptions || availableOptions.length === 0) && ( +
+
+ + +

{OptionsListStrings.popover.getEmptyMessage()}

+
+
+ )} +
+ + ); +}; diff --git a/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_strings.ts b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_strings.ts new file mode 100644 index 0000000000000..2211ae14cb9bd --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_strings.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +export const OptionsListStrings = { + summary: { + getSeparator: () => + i18n.translate('presentationUtil.inputControls.optionsList.summary.separator', { + defaultMessage: ', ', + }), + getPlaceholder: () => + i18n.translate('presentationUtil.inputControls.optionsList.summary.placeholder', { + defaultMessage: 'Select...', + }), + }, + popover: { + getLoadingMessage: () => + i18n.translate('presentationUtil.inputControls.optionsList.popover.loading', { + defaultMessage: 'Loading filters', + }), + getEmptyMessage: () => + i18n.translate('presentationUtil.inputControls.optionsList.popover.empty', { + defaultMessage: 'No filters found', + }), + }, +}; diff --git a/src/plugins/presentation_util/public/components/input_controls/embeddable/types.ts b/src/plugins/presentation_util/public/components/input_controls/embeddable/types.ts new file mode 100644 index 0000000000000..00be17932ba1f --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/embeddable/types.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Filter, Query, TimeRange } from '../../../../../data/public'; +import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from '../../../../../embeddable/public'; + +export type InputControlInput = EmbeddableInput & { + filters?: Filter[]; + query?: Query; + timeRange?: TimeRange; + twoLineLayout?: boolean; +}; + +export type InputControlOutput = EmbeddableOutput & { + filters?: Filter[]; +}; + +export type InputControlEmbeddable = IEmbeddable; diff --git a/src/plugins/presentation_util/public/components/input_controls/index.ts b/src/plugins/presentation_util/public/components/input_controls/index.ts new file mode 100644 index 0000000000000..5c2d5b68ae2e0 --- /dev/null +++ b/src/plugins/presentation_util/public/components/input_controls/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ diff --git a/src/plugins/presentation_util/tsconfig.json b/src/plugins/presentation_util/tsconfig.json index b389d94b19413..e0b9bbeb4917d 100644 --- a/src/plugins/presentation_util/tsconfig.json +++ b/src/plugins/presentation_util/tsconfig.json @@ -13,6 +13,7 @@ "include": [ "common/**/*", "public/**/*", + "public/**/*.json", "server/**/*", "storybook/**/*", "../../../typings/**/*" @@ -21,5 +22,7 @@ { "path": "../../core/tsconfig.json" }, { "path": "../saved_objects/tsconfig.json" }, { "path": "../kibana_react/tsconfig.json" }, + { "path": "../embeddable/tsconfig.json" }, + { "path": "../data/tsconfig.json" } ] } From bc25c0fca95ace715d8ed026941ae8bd68ccf056 Mon Sep 17 00:00:00 2001 From: Sandra G Date: Tue, 10 Aug 2021 16:02:14 -0400 Subject: [PATCH 044/104] [Stack Monitoring] Rename alerts to rules (#107654) * rename constants and alert types to rules * update test language * update BaseRule properties to rule * change rawAlert to sanitizedRule Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/monitoring/common/constants.ts | 162 +++++++++--------- .../plugins/monitoring/common/types/alerts.ts | 2 +- .../monitoring/public/alerts/badge.tsx | 2 +- .../monitoring/public/alerts/callout.tsx | 2 +- .../ccr_read_exceptions_alert/index.tsx | 14 +- .../cpu_usage_alert/cpu_usage_alert.tsx | 14 +- .../public/alerts/disk_usage_alert/index.tsx | 14 +- .../alerts/large_shard_size_alert/index.tsx | 14 +- .../alerts/legacy_alert/legacy_alert.tsx | 12 +- .../lib/get_alert_panels_by_category.test.tsx | 46 ++--- .../lib/get_alert_panels_by_category.tsx | 35 ++-- .../lib/get_alert_panels_by_node.test.tsx | 38 ++-- .../alerts/lib/get_alert_panels_by_node.tsx | 6 +- .../alerts/memory_usage_alert/index.tsx | 14 +- .../missing_monitoring_data_alert.tsx | 14 +- .../thread_pool_rejections_alert/index.tsx | 4 +- .../cluster/overview/elasticsearch_panel.js | 64 +++---- .../cluster/overview/kibana_panel.js | 4 +- .../cluster/overview/logstash_panel.js | 8 +- x-pack/plugins/monitoring/public/plugin.ts | 14 +- .../public/views/elasticsearch/ccr/index.js | 4 +- .../views/elasticsearch/ccr/shard/index.js | 4 +- .../elasticsearch/index/advanced/index.js | 4 +- .../public/views/elasticsearch/index/index.js | 4 +- .../views/elasticsearch/indices/index.js | 4 +- .../elasticsearch/node/advanced/index.js | 24 +-- .../public/views/elasticsearch/node/index.js | 24 +-- .../public/views/elasticsearch/nodes/index.js | 24 +-- .../public/views/kibana/instance/index.js | 4 +- .../public/views/kibana/instances/index.js | 4 +- .../views/logstash/node/advanced/index.js | 4 +- .../public/views/logstash/node/index.js | 4 +- .../public/views/logstash/nodes/index.js | 4 +- .../server/alerts/alerts_factory.test.ts | 8 +- .../server/alerts/alerts_factory.ts | 90 +++++----- .../{base_alert.test.ts => base_rule.test.ts} | 24 +-- .../alerts/{base_alert.ts => base_rule.ts} | 48 +++--- ...st.ts => ccr_read_exceptions_rule.test.ts} | 32 ++-- ...s_alert.ts => ccr_read_exceptions_rule.ts} | 16 +- ...rt.test.ts => cluster_health_rule.test.ts} | 24 +-- ...health_alert.ts => cluster_health_rule.ts} | 16 +- ...e_alert.test.ts => cpu_usage_rule.test.ts} | 36 ++-- .../{cpu_usage_alert.ts => cpu_usage_rule.ts} | 18 +- ..._alert.test.ts => disk_usage_rule.test.ts} | 32 ++-- ...disk_usage_alert.ts => disk_usage_rule.ts} | 18 +- ...asticsearch_version_mismatch_rule.test.ts} | 26 +-- ...=> elasticsearch_version_mismatch_rule.ts} | 16 +- .../plugins/monitoring/server/alerts/index.ts | 30 ++-- ...s => kibana_version_mismatch_rule.test.ts} | 28 +-- ...ert.ts => kibana_version_mismatch_rule.ts} | 16 +- ....test.ts => large_shard_size_rule.test.ts} | 32 ++-- ...size_alert.ts => large_shard_size_rule.ts} | 16 +- ...est.ts => license_expiration_rule.test.ts} | 40 ++--- ...on_alert.ts => license_expiration_rule.ts} | 16 +- ...=> logstash_version_mismatch_rule.test.ts} | 28 +-- ...t.ts => logstash_version_mismatch_rule.ts} | 16 +- ...lert.test.ts => memory_usage_rule.test.ts} | 36 ++-- ...ry_usage_alert.ts => memory_usage_rule.ts} | 16 +- ...s => missing_monitoring_data_rule.test.ts} | 36 ++-- ...ert.ts => missing_monitoring_data_rule.ts} | 18 +- ...ert.test.ts => nodes_changed_rule.test.ts} | 32 ++-- ...changed_alert.ts => nodes_changed_rule.ts} | 16 +- ...ts => thread_pool_rejections_rule_base.ts} | 8 +- .../thread_pool_search_rejections_alert.ts | 27 --- ...hread_pool_search_rejections_rule.test.ts} | 36 ++-- .../thread_pool_search_rejections_rule.ts | 27 +++ .../thread_pool_write_rejections_alert.ts | 27 --- ...thread_pool_write_rejections_rule.test.ts} | 34 ++-- .../thread_pool_write_rejections_rule.ts | 27 +++ .../server/lib/alerts/fetch_status.test.ts | 28 ++- .../server/lib/alerts/fetch_status.ts | 16 +- x-pack/plugins/monitoring/server/plugin.ts | 10 +- 72 files changed, 798 insertions(+), 817 deletions(-) rename x-pack/plugins/monitoring/server/alerts/{base_alert.test.ts => base_rule.test.ts} (80%) rename x-pack/plugins/monitoring/server/alerts/{base_alert.ts => base_rule.ts} (90%) rename x-pack/plugins/monitoring/server/alerts/{ccr_read_exceptions_alert.test.ts => ccr_read_exceptions_rule.test.ts} (86%) rename x-pack/plugins/monitoring/server/alerts/{ccr_read_exceptions_alert.ts => ccr_read_exceptions_rule.ts} (96%) rename x-pack/plugins/monitoring/server/alerts/{cluster_health_alert.test.ts => cluster_health_rule.test.ts} (89%) rename x-pack/plugins/monitoring/server/alerts/{cluster_health_alert.ts => cluster_health_rule.ts} (94%) rename x-pack/plugins/monitoring/server/alerts/{cpu_usage_alert.test.ts => cpu_usage_rule.test.ts} (89%) rename x-pack/plugins/monitoring/server/alerts/{cpu_usage_alert.ts => cpu_usage_rule.ts} (95%) rename x-pack/plugins/monitoring/server/alerts/{disk_usage_alert.test.ts => disk_usage_rule.test.ts} (85%) rename x-pack/plugins/monitoring/server/alerts/{disk_usage_alert.ts => disk_usage_rule.ts} (95%) rename x-pack/plugins/monitoring/server/alerts/{elasticsearch_version_mismatch_alert.test.ts => elasticsearch_version_mismatch_rule.test.ts} (86%) rename x-pack/plugins/monitoring/server/alerts/{elasticsearch_version_mismatch_alert.ts => elasticsearch_version_mismatch_rule.ts} (93%) rename x-pack/plugins/monitoring/server/alerts/{kibana_version_mismatch_alert.test.ts => kibana_version_mismatch_rule.test.ts} (86%) rename x-pack/plugins/monitoring/server/alerts/{kibana_version_mismatch_alert.ts => kibana_version_mismatch_rule.ts} (94%) rename x-pack/plugins/monitoring/server/alerts/{large_shard_size_alert.test.ts => large_shard_size_rule.test.ts} (85%) rename x-pack/plugins/monitoring/server/alerts/{large_shard_size_alert.ts => large_shard_size_rule.ts} (96%) rename x-pack/plugins/monitoring/server/alerts/{license_expiration_alert.test.ts => license_expiration_rule.test.ts} (87%) rename x-pack/plugins/monitoring/server/alerts/{license_expiration_alert.ts => license_expiration_rule.ts} (95%) rename x-pack/plugins/monitoring/server/alerts/{logstash_version_mismatch_alert.test.ts => logstash_version_mismatch_rule.test.ts} (86%) rename x-pack/plugins/monitoring/server/alerts/{logstash_version_mismatch_alert.ts => logstash_version_mismatch_rule.ts} (93%) rename x-pack/plugins/monitoring/server/alerts/{memory_usage_alert.test.ts => memory_usage_rule.test.ts} (90%) rename x-pack/plugins/monitoring/server/alerts/{memory_usage_alert.ts => memory_usage_rule.ts} (96%) rename x-pack/plugins/monitoring/server/alerts/{missing_monitoring_data_alert.test.ts => missing_monitoring_data_rule.test.ts} (88%) rename x-pack/plugins/monitoring/server/alerts/{missing_monitoring_data_alert.ts => missing_monitoring_data_rule.ts} (94%) rename x-pack/plugins/monitoring/server/alerts/{nodes_changed_alert.test.ts => nodes_changed_rule.test.ts} (92%) rename x-pack/plugins/monitoring/server/alerts/{nodes_changed_alert.ts => nodes_changed_rule.ts} (96%) rename x-pack/plugins/monitoring/server/alerts/{thread_pool_rejections_alert_base.ts => thread_pool_rejections_rule_base.ts} (97%) delete mode 100644 x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.ts rename x-pack/plugins/monitoring/server/alerts/{thread_pool_search_rejections_alert.test.ts => thread_pool_search_rejections_rule.test.ts} (90%) create mode 100644 x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.ts delete mode 100644 x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.ts rename x-pack/plugins/monitoring/server/alerts/{thread_pool_write_rejections_alert.test.ts => thread_pool_write_rejections_rule.test.ts} (90%) create mode 100644 x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.ts diff --git a/x-pack/plugins/monitoring/common/constants.ts b/x-pack/plugins/monitoring/common/constants.ts index 8be0ca23d4d6e..3f03680e687b1 100644 --- a/x-pack/plugins/monitoring/common/constants.ts +++ b/x-pack/plugins/monitoring/common/constants.ts @@ -220,29 +220,29 @@ export const CLUSTER_DETAILS_FETCH_INTERVAL = 10800000; export const USAGE_FETCH_INTERVAL = 1200000; /** - * The prefix for all alert types used by monitoring - */ -export const ALERT_PREFIX = 'monitoring_'; -export const ALERT_LICENSE_EXPIRATION = `${ALERT_PREFIX}alert_license_expiration`; -export const ALERT_CLUSTER_HEALTH = `${ALERT_PREFIX}alert_cluster_health`; -export const ALERT_CPU_USAGE = `${ALERT_PREFIX}alert_cpu_usage`; -export const ALERT_DISK_USAGE = `${ALERT_PREFIX}alert_disk_usage`; -export const ALERT_NODES_CHANGED = `${ALERT_PREFIX}alert_nodes_changed`; -export const ALERT_ELASTICSEARCH_VERSION_MISMATCH = `${ALERT_PREFIX}alert_elasticsearch_version_mismatch`; -export const ALERT_KIBANA_VERSION_MISMATCH = `${ALERT_PREFIX}alert_kibana_version_mismatch`; -export const ALERT_LOGSTASH_VERSION_MISMATCH = `${ALERT_PREFIX}alert_logstash_version_mismatch`; -export const ALERT_MEMORY_USAGE = `${ALERT_PREFIX}alert_jvm_memory_usage`; -export const ALERT_MISSING_MONITORING_DATA = `${ALERT_PREFIX}alert_missing_monitoring_data`; -export const ALERT_THREAD_POOL_SEARCH_REJECTIONS = `${ALERT_PREFIX}alert_thread_pool_search_rejections`; -export const ALERT_THREAD_POOL_WRITE_REJECTIONS = `${ALERT_PREFIX}alert_thread_pool_write_rejections`; -export const ALERT_CCR_READ_EXCEPTIONS = `${ALERT_PREFIX}ccr_read_exceptions`; -export const ALERT_LARGE_SHARD_SIZE = `${ALERT_PREFIX}shard_size`; + * The prefix for all rule types used by monitoring + */ +export const RULE_PREFIX = 'monitoring_'; +export const RULE_LICENSE_EXPIRATION = `${RULE_PREFIX}alert_license_expiration`; +export const RULE_CLUSTER_HEALTH = `${RULE_PREFIX}alert_cluster_health`; +export const RULE_CPU_USAGE = `${RULE_PREFIX}alert_cpu_usage`; +export const RULE_DISK_USAGE = `${RULE_PREFIX}alert_disk_usage`; +export const RULE_NODES_CHANGED = `${RULE_PREFIX}alert_nodes_changed`; +export const RULE_ELASTICSEARCH_VERSION_MISMATCH = `${RULE_PREFIX}alert_elasticsearch_version_mismatch`; +export const RULE_KIBANA_VERSION_MISMATCH = `${RULE_PREFIX}alert_kibana_version_mismatch`; +export const RULE_LOGSTASH_VERSION_MISMATCH = `${RULE_PREFIX}alert_logstash_version_mismatch`; +export const RULE_MEMORY_USAGE = `${RULE_PREFIX}alert_jvm_memory_usage`; +export const RULE_MISSING_MONITORING_DATA = `${RULE_PREFIX}alert_missing_monitoring_data`; +export const RULE_THREAD_POOL_SEARCH_REJECTIONS = `${RULE_PREFIX}alert_thread_pool_search_rejections`; +export const RULE_THREAD_POOL_WRITE_REJECTIONS = `${RULE_PREFIX}alert_thread_pool_write_rejections`; +export const RULE_CCR_READ_EXCEPTIONS = `${RULE_PREFIX}ccr_read_exceptions`; +export const RULE_LARGE_SHARD_SIZE = `${RULE_PREFIX}shard_size`; /** - * Legacy alerts details/label for server and public use + * Legacy rules details/label for server and public use */ -export const LEGACY_ALERT_DETAILS = { - [ALERT_CLUSTER_HEALTH]: { +export const LEGACY_RULE_DETAILS = { + [RULE_CLUSTER_HEALTH]: { label: i18n.translate('xpack.monitoring.alerts.clusterHealth.label', { defaultMessage: 'Cluster health', }), @@ -250,7 +250,7 @@ export const LEGACY_ALERT_DETAILS = { defaultMessage: 'Alert when the health of the cluster changes.', }), }, - [ALERT_ELASTICSEARCH_VERSION_MISMATCH]: { + [RULE_ELASTICSEARCH_VERSION_MISMATCH]: { label: i18n.translate('xpack.monitoring.alerts.elasticsearchVersionMismatch.label', { defaultMessage: 'Elasticsearch version mismatch', }), @@ -261,7 +261,7 @@ export const LEGACY_ALERT_DETAILS = { } ), }, - [ALERT_KIBANA_VERSION_MISMATCH]: { + [RULE_KIBANA_VERSION_MISMATCH]: { label: i18n.translate('xpack.monitoring.alerts.kibanaVersionMismatch.label', { defaultMessage: 'Kibana version mismatch', }), @@ -269,7 +269,7 @@ export const LEGACY_ALERT_DETAILS = { defaultMessage: 'Alert when the cluser has multiple versions of Kibana.', }), }, - [ALERT_LICENSE_EXPIRATION]: { + [RULE_LICENSE_EXPIRATION]: { label: i18n.translate('xpack.monitoring.alerts.licenseExpiration.label', { defaultMessage: 'License expiration', }), @@ -277,7 +277,7 @@ export const LEGACY_ALERT_DETAILS = { defaultMessage: 'Alert when the cluster license is about to expire.', }), }, - [ALERT_LOGSTASH_VERSION_MISMATCH]: { + [RULE_LOGSTASH_VERSION_MISMATCH]: { label: i18n.translate('xpack.monitoring.alerts.logstashVersionMismatch.label', { defaultMessage: 'Logstash version mismatch', }), @@ -285,7 +285,7 @@ export const LEGACY_ALERT_DETAILS = { defaultMessage: 'Alert when the cluster has multiple versions of Logstash.', }), }, - [ALERT_NODES_CHANGED]: { + [RULE_NODES_CHANGED]: { label: i18n.translate('xpack.monitoring.alerts.nodesChanged.label', { defaultMessage: 'Nodes changed', }), @@ -296,10 +296,10 @@ export const LEGACY_ALERT_DETAILS = { }; /** - * Alerts details/label for server and public use + * Rules details/label for server and public use */ -export const ALERT_DETAILS = { - [ALERT_CPU_USAGE]: { +export const RULE_DETAILS = { + [RULE_CPU_USAGE]: { label: i18n.translate('xpack.monitoring.alerts.cpuUsage.label', { defaultMessage: 'CPU Usage', }), @@ -321,7 +321,7 @@ export const ALERT_DETAILS = { } as CommonAlertParamDetail, }, }, - [ALERT_DISK_USAGE]: { + [RULE_DISK_USAGE]: { paramDetails: { threshold: { label: i18n.translate('xpack.monitoring.alerts.diskUsage.paramDetails.threshold.label', { @@ -343,7 +343,7 @@ export const ALERT_DETAILS = { defaultMessage: 'Alert when the disk usage for a node is consistently high.', }), }, - [ALERT_MEMORY_USAGE]: { + [RULE_MEMORY_USAGE]: { paramDetails: { threshold: { label: i18n.translate('xpack.monitoring.alerts.memoryUsage.paramDetails.threshold.label', { @@ -365,7 +365,7 @@ export const ALERT_DETAILS = { defaultMessage: 'Alert when a node reports high memory usage.', }), }, - [ALERT_MISSING_MONITORING_DATA]: { + [RULE_MISSING_MONITORING_DATA]: { paramDetails: { duration: { label: i18n.translate('xpack.monitoring.alerts.missingData.paramDetails.duration.label', { @@ -387,7 +387,7 @@ export const ALERT_DETAILS = { defaultMessage: 'Alert when monitoring data is missing.', }), }, - [ALERT_THREAD_POOL_SEARCH_REJECTIONS]: { + [RULE_THREAD_POOL_SEARCH_REJECTIONS]: { paramDetails: { threshold: { label: i18n.translate('xpack.monitoring.alerts.rejection.paramDetails.threshold.label', { @@ -412,7 +412,7 @@ export const ALERT_DETAILS = { 'Alert when the number of rejections in the search thread pool exceeds the threshold.', }), }, - [ALERT_THREAD_POOL_WRITE_REJECTIONS]: { + [RULE_THREAD_POOL_WRITE_REJECTIONS]: { paramDetails: { threshold: { label: i18n.translate('xpack.monitoring.alerts.rejection.paramDetails.threshold.label', { @@ -437,7 +437,7 @@ export const ALERT_DETAILS = { 'Alert when the number of rejections in the write thread pool exceeds the threshold.', }), }, - [ALERT_CCR_READ_EXCEPTIONS]: { + [RULE_CCR_READ_EXCEPTIONS]: { paramDetails: { duration: { label: i18n.translate( @@ -456,7 +456,7 @@ export const ALERT_DETAILS = { defaultMessage: 'Alert if any CCR read exceptions have been detected.', }), }, - [ALERT_LARGE_SHARD_SIZE]: { + [RULE_LARGE_SHARD_SIZE]: { paramDetails: { threshold: { label: i18n.translate('xpack.monitoring.alerts.shardSize.paramDetails.threshold.label', { @@ -482,74 +482,74 @@ export const ALERT_DETAILS = { }, }; -export const ALERT_PANEL_MENU = [ +export const RULE_PANEL_MENU = [ { label: i18n.translate('xpack.monitoring.alerts.badge.panelCategory.clusterHealth', { defaultMessage: 'Cluster health', }), - alerts: [ - { alertName: ALERT_NODES_CHANGED }, - { alertName: ALERT_CLUSTER_HEALTH }, - { alertName: ALERT_ELASTICSEARCH_VERSION_MISMATCH }, - { alertName: ALERT_KIBANA_VERSION_MISMATCH }, - { alertName: ALERT_LOGSTASH_VERSION_MISMATCH }, + rules: [ + { ruleName: RULE_NODES_CHANGED }, + { ruleName: RULE_CLUSTER_HEALTH }, + { ruleName: RULE_ELASTICSEARCH_VERSION_MISMATCH }, + { ruleName: RULE_KIBANA_VERSION_MISMATCH }, + { ruleName: RULE_LOGSTASH_VERSION_MISMATCH }, ], }, { label: i18n.translate('xpack.monitoring.alerts.badge.panelCategory.resourceUtilization', { defaultMessage: 'Resource utilization', }), - alerts: [ - { alertName: ALERT_CPU_USAGE }, - { alertName: ALERT_DISK_USAGE }, - { alertName: ALERT_MEMORY_USAGE }, - { alertName: ALERT_LARGE_SHARD_SIZE }, + rules: [ + { ruleName: RULE_CPU_USAGE }, + { ruleName: RULE_DISK_USAGE }, + { ruleName: RULE_MEMORY_USAGE }, + { ruleName: RULE_LARGE_SHARD_SIZE }, ], }, { label: i18n.translate('xpack.monitoring.alerts.badge.panelCategory.errors', { defaultMessage: 'Errors and exceptions', }), - alerts: [ - { alertName: ALERT_MISSING_MONITORING_DATA }, - { alertName: ALERT_LICENSE_EXPIRATION }, - { alertName: ALERT_THREAD_POOL_SEARCH_REJECTIONS }, - { alertName: ALERT_THREAD_POOL_WRITE_REJECTIONS }, - { alertName: ALERT_CCR_READ_EXCEPTIONS }, + rules: [ + { ruleName: RULE_MISSING_MONITORING_DATA }, + { ruleName: RULE_LICENSE_EXPIRATION }, + { ruleName: RULE_THREAD_POOL_SEARCH_REJECTIONS }, + { ruleName: RULE_THREAD_POOL_WRITE_REJECTIONS }, + { ruleName: RULE_CCR_READ_EXCEPTIONS }, ], }, ]; /** - * A listing of all alert types - */ -export const ALERTS = [ - ALERT_LICENSE_EXPIRATION, - ALERT_CLUSTER_HEALTH, - ALERT_CPU_USAGE, - ALERT_DISK_USAGE, - ALERT_NODES_CHANGED, - ALERT_ELASTICSEARCH_VERSION_MISMATCH, - ALERT_KIBANA_VERSION_MISMATCH, - ALERT_LOGSTASH_VERSION_MISMATCH, - ALERT_MEMORY_USAGE, - ALERT_MISSING_MONITORING_DATA, - ALERT_THREAD_POOL_SEARCH_REJECTIONS, - ALERT_THREAD_POOL_WRITE_REJECTIONS, - ALERT_CCR_READ_EXCEPTIONS, - ALERT_LARGE_SHARD_SIZE, + * A listing of all rule types + */ +export const RULES = [ + RULE_LICENSE_EXPIRATION, + RULE_CLUSTER_HEALTH, + RULE_CPU_USAGE, + RULE_DISK_USAGE, + RULE_NODES_CHANGED, + RULE_ELASTICSEARCH_VERSION_MISMATCH, + RULE_KIBANA_VERSION_MISMATCH, + RULE_LOGSTASH_VERSION_MISMATCH, + RULE_MEMORY_USAGE, + RULE_MISSING_MONITORING_DATA, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_CCR_READ_EXCEPTIONS, + RULE_LARGE_SHARD_SIZE, ]; /** - * A list of all legacy alerts, which means they are powered by watcher - */ -export const LEGACY_ALERTS = [ - ALERT_LICENSE_EXPIRATION, - ALERT_CLUSTER_HEALTH, - ALERT_NODES_CHANGED, - ALERT_ELASTICSEARCH_VERSION_MISMATCH, - ALERT_KIBANA_VERSION_MISMATCH, - ALERT_LOGSTASH_VERSION_MISMATCH, + * A list of all legacy rules, which means they are powered by watcher + */ +export const LEGACY_RULES = [ + RULE_LICENSE_EXPIRATION, + RULE_CLUSTER_HEALTH, + RULE_NODES_CHANGED, + RULE_ELASTICSEARCH_VERSION_MISMATCH, + RULE_KIBANA_VERSION_MISMATCH, + RULE_LOGSTASH_VERSION_MISMATCH, ]; /** @@ -564,9 +564,9 @@ export const ALERT_ACTION_TYPE_EMAIL = '.email'; export const ALERT_ACTION_TYPE_LOG = '.server-log'; /** - * To enable modifing of alerts in under actions + * To enable modifing of rules in under actions */ -export const ALERT_REQUIRES_APP_CONTEXT = false; +export const RULE_REQUIRES_APP_CONTEXT = false; export const ALERT_EMAIL_SERVICES = ['gmail', 'hotmail', 'icloud', 'outlook365', 'ses', 'yahoo']; diff --git a/x-pack/plugins/monitoring/common/types/alerts.ts b/x-pack/plugins/monitoring/common/types/alerts.ts index 39323e0d7b27c..17bbffce19a18 100644 --- a/x-pack/plugins/monitoring/common/types/alerts.ts +++ b/x-pack/plugins/monitoring/common/types/alerts.ts @@ -20,7 +20,7 @@ export interface RulesByType { } export interface CommonAlertStatus { states: CommonAlertState[]; - rawAlert: Alert | SanitizedAlert; + sanitizedRule: Alert | SanitizedAlert; } export interface CommonAlertState { diff --git a/x-pack/plugins/monitoring/public/alerts/badge.tsx b/x-pack/plugins/monitoring/public/alerts/badge.tsx index 4f6572d25b05f..6b1c8c5085565 100644 --- a/x-pack/plugins/monitoring/public/alerts/badge.tsx +++ b/x-pack/plugins/monitoring/public/alerts/badge.tsx @@ -46,7 +46,7 @@ export const AlertsBadge: React.FC = (props: Props) => { // We do not always have the alerts that each consumer wants due to licensing const { stateFilter = () => true } = props; const alertsList = Object.values(props.alerts).flat(); - const alerts = alertsList.filter((alertItem) => Boolean(alertItem?.rawAlert)); + const alerts = alertsList.filter((alertItem) => Boolean(alertItem?.sanitizedRule)); const [showPopover, setShowPopover] = React.useState(null); const inSetupMode = isInSetupMode(React.useContext(SetupModeContext)); const alertCount = inSetupMode diff --git a/x-pack/plugins/monitoring/public/alerts/callout.tsx b/x-pack/plugins/monitoring/public/alerts/callout.tsx index eaf45856864cc..b4e14ca0b0ac0 100644 --- a/x-pack/plugins/monitoring/public/alerts/callout.tsx +++ b/x-pack/plugins/monitoring/public/alerts/callout.tsx @@ -103,7 +103,7 @@ export const AlertsCallout: React.FC = (props: Props) => { } )} } + label={} /> diff --git a/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx index b660bdcfdce8f..1de9a175026a6 100644 --- a/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx @@ -10,9 +10,9 @@ import { i18n } from '@kbn/i18n'; import { Expression, Props } from '../components/param_details_form/expression'; import { AlertTypeModel, ValidationResult } from '../../../../triggers_actions_ui/public'; import { - ALERT_CCR_READ_EXCEPTIONS, - ALERT_DETAILS, - ALERT_REQUIRES_APP_CONTEXT, + RULE_CCR_READ_EXCEPTIONS, + RULE_DETAILS, + RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; import { AlertTypeParams } from '../../../../alerting/common'; @@ -38,17 +38,17 @@ const validate = (inputValues: ValidateOptions): ValidationResult => { export function createCCRReadExceptionsAlertType(): AlertTypeModel { return { - id: ALERT_CCR_READ_EXCEPTIONS, - description: ALERT_DETAILS[ALERT_CCR_READ_EXCEPTIONS].description, + id: RULE_CCR_READ_EXCEPTIONS, + description: RULE_DETAILS[RULE_CCR_READ_EXCEPTIONS].description, iconClass: 'bell', documentationUrl(docLinks) { return `${docLinks.links.monitoring.alertsKibanaCCRReadExceptions}`; }, alertParamsExpression: (props: Props) => ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', - requiresAppContext: ALERT_REQUIRES_APP_CONTEXT, + requiresAppContext: RULE_REQUIRES_APP_CONTEXT, }; } diff --git a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx index cad2cbdf613ab..ec7a583ec2ba1 100644 --- a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx @@ -8,27 +8,23 @@ import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; -import { - ALERT_CPU_USAGE, - ALERT_DETAILS, - ALERT_REQUIRES_APP_CONTEXT, -} from '../../../common/constants'; +import { RULE_CPU_USAGE, RULE_DETAILS, RULE_REQUIRES_APP_CONTEXT } from '../../../common/constants'; import { validate, MonitoringAlertTypeParams } from '../components/param_details_form/validation'; import { Expression, Props } from '../components/param_details_form/expression'; export function createCpuUsageAlertType(): AlertTypeModel { return { - id: ALERT_CPU_USAGE, - description: ALERT_DETAILS[ALERT_CPU_USAGE].description, + id: RULE_CPU_USAGE, + description: RULE_DETAILS[RULE_CPU_USAGE].description, iconClass: 'bell', documentationUrl(docLinks) { return `${docLinks.links.monitoring.alertsKibanaCpuThreshold}`; }, alertParamsExpression: (props: Props) => ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', - requiresAppContext: ALERT_REQUIRES_APP_CONTEXT, + requiresAppContext: RULE_REQUIRES_APP_CONTEXT, }; } diff --git a/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx index 2cacb9d3a3b71..779945da0c9e0 100644 --- a/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx @@ -12,24 +12,24 @@ import { Expression, Props } from '../components/param_details_form/expression'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; import { - ALERT_DISK_USAGE, - ALERT_DETAILS, - ALERT_REQUIRES_APP_CONTEXT, + RULE_DISK_USAGE, + RULE_DETAILS, + RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; export function createDiskUsageAlertType(): AlertTypeModel { return { - id: ALERT_DISK_USAGE, - description: ALERT_DETAILS[ALERT_DISK_USAGE].description, + id: RULE_DISK_USAGE, + description: RULE_DETAILS[RULE_DISK_USAGE].description, iconClass: 'bell', documentationUrl(docLinks) { return `${docLinks.links.monitoring.alertsKibanaDiskThreshold}`; }, alertParamsExpression: (props: Props) => ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', - requiresAppContext: ALERT_REQUIRES_APP_CONTEXT, + requiresAppContext: RULE_REQUIRES_APP_CONTEXT, }; } diff --git a/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx index 89139f12dacca..e0f595abe7602 100644 --- a/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx @@ -10,9 +10,9 @@ import { i18n } from '@kbn/i18n'; import { Expression, Props } from '../components/param_details_form/expression'; import { AlertTypeModel, ValidationResult } from '../../../../triggers_actions_ui/public'; import { - ALERT_LARGE_SHARD_SIZE, - ALERT_DETAILS, - ALERT_REQUIRES_APP_CONTEXT, + RULE_LARGE_SHARD_SIZE, + RULE_DETAILS, + RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; import { AlertTypeParams } from '../../../../alerting/common'; @@ -38,17 +38,17 @@ const validate = (inputValues: ValidateOptions): ValidationResult => { export function createLargeShardSizeAlertType(): AlertTypeModel { return { - id: ALERT_LARGE_SHARD_SIZE, - description: ALERT_DETAILS[ALERT_LARGE_SHARD_SIZE].description, + id: RULE_LARGE_SHARD_SIZE, + description: RULE_DETAILS[RULE_LARGE_SHARD_SIZE].description, iconClass: 'bell', documentationUrl(docLinks) { return `${docLinks.links.monitoring.alertsKibanaLargeShardSize}`; }, alertParamsExpression: (props: Props) => ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', - requiresAppContext: ALERT_REQUIRES_APP_CONTEXT, + requiresAppContext: RULE_REQUIRES_APP_CONTEXT, }; } diff --git a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx index 82bdf2786f89e..cac4873bc0c79 100644 --- a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx @@ -11,16 +11,16 @@ import { EuiTextColor, EuiSpacer } from '@elastic/eui'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; import { - LEGACY_ALERTS, - LEGACY_ALERT_DETAILS, - ALERT_REQUIRES_APP_CONTEXT, + LEGACY_RULES, + LEGACY_RULE_DETAILS, + RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; export function createLegacyAlertTypes(): AlertTypeModel[] { - return LEGACY_ALERTS.map((legacyAlert) => { + return LEGACY_RULES.map((legacyAlert) => { return { id: legacyAlert, - description: LEGACY_ALERT_DETAILS[legacyAlert].description, + description: LEGACY_RULE_DETAILS[legacyAlert].description, iconClass: 'bell', documentationUrl(docLinks) { return `${docLinks.links.monitoring.alertsKibanaClusterAlerts}`; @@ -38,7 +38,7 @@ export function createLegacyAlertTypes(): AlertTypeModel[] { ), defaultActionMessage: '{{context.internalFullMessage}}', validate: () => ({ errors: {} }), - requiresAppContext: ALERT_REQUIRES_APP_CONTEXT, + requiresAppContext: RULE_REQUIRES_APP_CONTEXT, }; }); } diff --git a/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.test.tsx b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.test.tsx index 63ae489677c78..0ba35969d8d17 100644 --- a/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.test.tsx +++ b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.test.tsx @@ -6,17 +6,17 @@ */ import { - ALERT_CPU_USAGE, - ALERT_LOGSTASH_VERSION_MISMATCH, - ALERT_THREAD_POOL_WRITE_REJECTIONS, + RULE_CPU_USAGE, + RULE_LOGSTASH_VERSION_MISMATCH, + RULE_THREAD_POOL_WRITE_REJECTIONS, } from '../../../common/constants'; import { AlertSeverity } from '../../../common/enums'; import { getAlertPanelsByCategory } from './get_alert_panels_by_category'; import { - ALERT_LICENSE_EXPIRATION, - ALERT_NODES_CHANGED, - ALERT_DISK_USAGE, - ALERT_MEMORY_USAGE, + RULE_LICENSE_EXPIRATION, + RULE_NODES_CHANGED, + RULE_DISK_USAGE, + RULE_MEMORY_USAGE, } from '../../../common/constants'; import { AlertExecutionStatusValues } from '../../../../alerting/common'; import { AlertState } from '../../../common/types/alerts'; @@ -93,7 +93,7 @@ describe('getAlertPanelsByCategory', () => { return { states, - rawAlert: { + sanitizedRule: { alertTypeId: type, name: `${type}_label`, ...mockAlert, @@ -107,32 +107,32 @@ describe('getAlertPanelsByCategory', () => { describe('non setup mode', () => { it('should properly group for alerts in each category', () => { const alerts = [ - getAlert(ALERT_NODES_CHANGED, 2), - getAlert(ALERT_DISK_USAGE, 1), - getAlert(ALERT_LICENSE_EXPIRATION, 2), + getAlert(RULE_NODES_CHANGED, 2), + getAlert(RULE_DISK_USAGE, 1), + getAlert(RULE_LICENSE_EXPIRATION, 2), ]; const result = getAlertPanelsByCategory(panelTitle, false, alerts, stateFilter); expect(result).toMatchSnapshot(); }); it('should properly group for alerts in a single category', () => { - const alerts = [getAlert(ALERT_MEMORY_USAGE, 2)]; + const alerts = [getAlert(RULE_MEMORY_USAGE, 2)]; const result = getAlertPanelsByCategory(panelTitle, false, alerts, stateFilter); expect(result).toMatchSnapshot(); }); it('should not show any alert if none are firing', () => { const alerts = [ - getAlert(ALERT_LOGSTASH_VERSION_MISMATCH, 0), - getAlert(ALERT_CPU_USAGE, 0), - getAlert(ALERT_THREAD_POOL_WRITE_REJECTIONS, 0), + getAlert(RULE_LOGSTASH_VERSION_MISMATCH, 0), + getAlert(RULE_CPU_USAGE, 0), + getAlert(RULE_THREAD_POOL_WRITE_REJECTIONS, 0), ]; const result = getAlertPanelsByCategory(panelTitle, false, alerts, stateFilter); expect(result).toMatchSnapshot(); }); it('should allow for state filtering', () => { - const alerts = [getAlert(ALERT_CPU_USAGE, 2)]; + const alerts = [getAlert(RULE_CPU_USAGE, 2)]; const customStateFilter = (state: AlertState) => state.nodeName === 'es_name_0'; const result = getAlertPanelsByCategory(panelTitle, false, alerts, customStateFilter); expect(result).toMatchSnapshot(); @@ -142,25 +142,25 @@ describe('getAlertPanelsByCategory', () => { describe('setup mode', () => { it('should properly group for alerts in each category', () => { const alerts = [ - getAlert(ALERT_NODES_CHANGED, 2), - getAlert(ALERT_DISK_USAGE, 1), - getAlert(ALERT_LICENSE_EXPIRATION, 2), + getAlert(RULE_NODES_CHANGED, 2), + getAlert(RULE_DISK_USAGE, 1), + getAlert(RULE_LICENSE_EXPIRATION, 2), ]; const result = getAlertPanelsByCategory(panelTitle, true, alerts, stateFilter); expect(result).toMatchSnapshot(); }); it('should properly group for alerts in a single category', () => { - const alerts = [getAlert(ALERT_MEMORY_USAGE, 2)]; + const alerts = [getAlert(RULE_MEMORY_USAGE, 2)]; const result = getAlertPanelsByCategory(panelTitle, true, alerts, stateFilter); expect(result).toMatchSnapshot(); }); it('should still show alerts if none are firing', () => { const alerts = [ - getAlert(ALERT_LOGSTASH_VERSION_MISMATCH, 0), - getAlert(ALERT_CPU_USAGE, 0), - getAlert(ALERT_THREAD_POOL_WRITE_REJECTIONS, 0), + getAlert(RULE_LOGSTASH_VERSION_MISMATCH, 0), + getAlert(RULE_CPU_USAGE, 0), + getAlert(RULE_THREAD_POOL_WRITE_REJECTIONS, 0), ]; const result = getAlertPanelsByCategory(panelTitle, true, alerts, stateFilter); expect(result).toMatchSnapshot(); diff --git a/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.tsx b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.tsx index 26dbe25cdd5a9..f455e75f688c3 100644 --- a/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.tsx +++ b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.tsx @@ -8,7 +8,7 @@ import React, { Fragment } from 'react'; import { EuiText, EuiToolTip } from '@elastic/eui'; import { AlertPanel } from '../panel'; -import { ALERT_PANEL_MENU } from '../../../common/constants'; +import { RULE_PANEL_MENU } from '../../../common/constants'; import { getDateFromNow, getCalendar } from '../../../common/formatting'; import { AlertState, @@ -22,7 +22,7 @@ import { Legacy } from '../../legacy_shims'; interface MenuAlert { alert: CommonAlert; - alertName: string; + ruleName: string; states: CommonAlertState[]; } interface MenuItem { @@ -36,13 +36,13 @@ export function getAlertPanelsByCategory( alerts: CommonAlertStatus[], stateFilter: (state: AlertState) => boolean ) { - // return items organized by categories in ALERT_PANEL_MENU + // return items organized by categories in RULE_PANEL_MENU // only show rules in setup mode const menu = inSetupMode - ? ALERT_PANEL_MENU.reduce((acc, category) => { + ? RULE_PANEL_MENU.reduce((acc, category) => { // check if we have any rules with that match this category - const alertsInCategory = category.alerts.filter((alert) => - alerts.find(({ rawAlert }) => rawAlert.alertTypeId === alert.alertName) + const alertsInCategory = category.rules.filter((rule) => + alerts.find(({ sanitizedRule }) => sanitizedRule.alertTypeId === rule.ruleName) ); // return all the categories that have rules and the rules if (alertsInCategory.length > 0) { @@ -51,14 +51,14 @@ export function getAlertPanelsByCategory( ...category, // add the corresponding rules that belong to this category alerts: alertsInCategory - .map(({ alertName }) => { + .map(({ ruleName }) => { return alerts - .filter(({ rawAlert }) => rawAlert.alertTypeId === alertName) + .filter(({ sanitizedRule }) => sanitizedRule.alertTypeId === ruleName) .map((alert) => { return { - alert: alert.rawAlert, + alert: alert.sanitizedRule, states: [], - alertName, + ruleName, }; }); }) @@ -68,13 +68,14 @@ export function getAlertPanelsByCategory( } return acc; }, []) - : ALERT_PANEL_MENU.reduce((acc, category) => { - // return items organized by categories in ALERT_PANEL_MENU, then rule name, then the actual alerts + : RULE_PANEL_MENU.reduce((acc, category) => { + // return items organized by categories in RULE_PANEL_MENU, then rule name, then the actual alerts const firingAlertsInCategory: MenuAlert[] = []; let categoryFiringAlertCount = 0; - for (const { alertName } of category.alerts) { + for (const { ruleName } of category.rules) { const foundAlerts = alerts.filter( - ({ rawAlert, states }) => alertName === rawAlert.alertTypeId && states.length > 0 + ({ sanitizedRule, states }) => + ruleName === sanitizedRule.alertTypeId && states.length > 0 ); if (foundAlerts.length > 0) { foundAlerts.forEach((foundAlert) => { @@ -82,9 +83,9 @@ export function getAlertPanelsByCategory( const states = foundAlert.states.filter(({ state }) => stateFilter(state)); if (states.length > 0) { firingAlertsInCategory.push({ - alert: foundAlert.rawAlert, + alert: foundAlert.sanitizedRule, states, - alertName, + ruleName, }); categoryFiringAlertCount += states.length; } @@ -169,7 +170,7 @@ export function getAlertPanelsByCategory( panels.push({ id: nodeIndex + 1, title: `${category.label}`, - items: category.alerts.map(({ alert, alertName, states }) => { + items: category.alerts.map(({ alert, ruleName, states }) => { const filteredStates = states.filter(({ state }) => stateFilter(state)); const name = inSetupMode ? ( {alert.name} diff --git a/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.test.tsx b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.test.tsx index 69179fb1d07d8..5f9e8ba8b64fc 100644 --- a/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.test.tsx +++ b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.test.tsx @@ -6,17 +6,17 @@ */ import { - ALERT_CPU_USAGE, - ALERT_LOGSTASH_VERSION_MISMATCH, - ALERT_THREAD_POOL_WRITE_REJECTIONS, + RULE_CPU_USAGE, + RULE_LOGSTASH_VERSION_MISMATCH, + RULE_THREAD_POOL_WRITE_REJECTIONS, } from '../../../common/constants'; import { AlertSeverity } from '../../../common/enums'; import { getAlertPanelsByNode } from './get_alert_panels_by_node'; import { - ALERT_LICENSE_EXPIRATION, - ALERT_NODES_CHANGED, - ALERT_DISK_USAGE, - ALERT_MEMORY_USAGE, + RULE_LICENSE_EXPIRATION, + RULE_NODES_CHANGED, + RULE_DISK_USAGE, + RULE_MEMORY_USAGE, } from '../../../common/constants'; import { AlertExecutionStatusValues } from '../../../../alerting/common'; import { AlertState } from '../../../common/types/alerts'; @@ -88,7 +88,7 @@ describe('getAlertPanelsByNode', () => { } return { - rawAlert: { + sanitizedRule: { id: `${type}_${firingCount}`, alertTypeId: type, name: `${type}_label`, @@ -103,17 +103,17 @@ describe('getAlertPanelsByNode', () => { it('should properly group for alerts in each category', () => { const alerts = [ - getAlert(ALERT_NODES_CHANGED, 2), - getAlert(ALERT_DISK_USAGE, 1), - getAlert(ALERT_LICENSE_EXPIRATION, 2), + getAlert(RULE_NODES_CHANGED, 2), + getAlert(RULE_DISK_USAGE, 1), + getAlert(RULE_LICENSE_EXPIRATION, 2), { states: [ { firing: true, meta: {}, state: { cluster, ui, nodeId: 'es1', nodeName: 'es_name_1' } }, ], - rawAlert: { - id: `${ALERT_NODES_CHANGED}_3`, - alertTypeId: ALERT_NODES_CHANGED, - name: `${ALERT_NODES_CHANGED}_label_2`, + sanitizedRule: { + id: `${RULE_NODES_CHANGED}_3`, + alertTypeId: RULE_NODES_CHANGED, + name: `${RULE_NODES_CHANGED}_label_2`, ...mockAlert, }, }, @@ -123,16 +123,16 @@ describe('getAlertPanelsByNode', () => { }); it('should properly group for alerts in a single category', () => { - const alerts = [getAlert(ALERT_MEMORY_USAGE, 2)]; + const alerts = [getAlert(RULE_MEMORY_USAGE, 2)]; const result = getAlertPanelsByNode(panelTitle, alerts, stateFilter); expect(result).toMatchSnapshot(); }); it('should not show any alert if none are firing', () => { const alerts = [ - getAlert(ALERT_LOGSTASH_VERSION_MISMATCH, 0), - getAlert(ALERT_CPU_USAGE, 0), - getAlert(ALERT_THREAD_POOL_WRITE_REJECTIONS, 0), + getAlert(RULE_LOGSTASH_VERSION_MISMATCH, 0), + getAlert(RULE_CPU_USAGE, 0), + getAlert(RULE_THREAD_POOL_WRITE_REJECTIONS, 0), ]; const result = getAlertPanelsByNode(panelTitle, alerts, stateFilter); expect(result).toMatchSnapshot(); diff --git a/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.tsx b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.tsx index 75eb03b9bad29..b8b4397c98b20 100644 --- a/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.tsx +++ b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.tsx @@ -37,8 +37,8 @@ export function getAlertPanelsByNode( [uuid: string]: CommonAlertState[]; } = {}; - for (const { states, rawAlert } of alerts) { - const { id: alertId } = rawAlert; + for (const { states, sanitizedRule } of alerts) { + const { id: alertId } = sanitizedRule; for (const alertState of states.filter(({ state: _state }) => stateFilter(_state))) { const { state } = alertState; statesByNodes[state.nodeId] = statesByNodes[state.nodeId] || []; @@ -46,7 +46,7 @@ export function getAlertPanelsByNode( alertsByNodes[state.nodeId] = alertsByNodes[state.nodeId] || {}; alertsByNodes[state.nodeId][alertId] = alertsByNodes[alertState.state.nodeId][alertId] || { - alert: rawAlert, + alert: sanitizedRule, states: [], count: 0, }; diff --git a/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx index 6639b4f2f6a48..3e55b6d5454ff 100644 --- a/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx @@ -12,24 +12,24 @@ import { Expression, Props } from '../components/param_details_form/expression'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; import { - ALERT_MEMORY_USAGE, - ALERT_DETAILS, - ALERT_REQUIRES_APP_CONTEXT, + RULE_MEMORY_USAGE, + RULE_DETAILS, + RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; export function createMemoryUsageAlertType(): AlertTypeModel { return { - id: ALERT_MEMORY_USAGE, - description: ALERT_DETAILS[ALERT_MEMORY_USAGE].description, + id: RULE_MEMORY_USAGE, + description: RULE_DETAILS[RULE_MEMORY_USAGE].description, iconClass: 'bell', documentationUrl(docLinks) { return `${docLinks.links.monitoring.alertsKibanaJvmThreshold}`; }, alertParamsExpression: (props: Props) => ( - + ), validate, defaultActionMessage: '{{context.internalFullMessage}}', - requiresAppContext: ALERT_REQUIRES_APP_CONTEXT, + requiresAppContext: RULE_REQUIRES_APP_CONTEXT, }; } diff --git a/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx index 4c833c93e0701..4c90f067d47c0 100644 --- a/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/missing_monitoring_data_alert/missing_monitoring_data_alert.tsx @@ -10,16 +10,16 @@ import React from 'react'; import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; import { validate } from './validation'; import { - ALERT_MISSING_MONITORING_DATA, - ALERT_DETAILS, - ALERT_REQUIRES_APP_CONTEXT, + RULE_MISSING_MONITORING_DATA, + RULE_DETAILS, + RULE_REQUIRES_APP_CONTEXT, } from '../../../common/constants'; import { Expression } from './expression'; export function createMissingMonitoringDataAlertType(): AlertTypeModel { return { - id: ALERT_MISSING_MONITORING_DATA, - description: ALERT_DETAILS[ALERT_MISSING_MONITORING_DATA].description, + id: RULE_MISSING_MONITORING_DATA, + description: RULE_DETAILS[RULE_MISSING_MONITORING_DATA].description, iconClass: 'bell', documentationUrl(docLinks) { return `${docLinks.links.monitoring.alertsKibanaMissingData}`; @@ -27,11 +27,11 @@ export function createMissingMonitoringDataAlertType(): AlertTypeModel { alertParamsExpression: (props: any) => ( ), validate, defaultActionMessage: '{{context.internalFullMessage}}', - requiresAppContext: ALERT_REQUIRES_APP_CONTEXT, + requiresAppContext: RULE_REQUIRES_APP_CONTEXT, }; } diff --git a/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx index f3e697bd270e0..7fd9438e1cea3 100644 --- a/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx @@ -12,7 +12,7 @@ import { Expression, Props } from '../components/param_details_form/expression'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; import { CommonAlertParamDetails } from '../../../common/types/alerts'; -import { ALERT_REQUIRES_APP_CONTEXT } from '../../../common/constants'; +import { RULE_REQUIRES_APP_CONTEXT } from '../../../common/constants'; interface ThreadPoolTypes { [key: string]: unknown; @@ -61,6 +61,6 @@ export function createThreadPoolRejectionsAlertType( return { errors }; }, defaultActionMessage: '{{context.internalFullMessage}}', - requiresAppContext: ALERT_REQUIRES_APP_CONTEXT, + requiresAppContext: RULE_REQUIRES_APP_CONTEXT, }; } diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js index 8f64656339083..85211008ff7d8 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js @@ -38,18 +38,18 @@ import { SetupModeTooltip } from '../../setup_mode/tooltip'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { ELASTICSEARCH_SYSTEM_ID, - ALERT_LICENSE_EXPIRATION, - ALERT_CLUSTER_HEALTH, - ALERT_CPU_USAGE, - ALERT_DISK_USAGE, - ALERT_THREAD_POOL_SEARCH_REJECTIONS, - ALERT_THREAD_POOL_WRITE_REJECTIONS, - ALERT_MEMORY_USAGE, - ALERT_NODES_CHANGED, - ALERT_ELASTICSEARCH_VERSION_MISMATCH, - ALERT_MISSING_MONITORING_DATA, - ALERT_CCR_READ_EXCEPTIONS, - ALERT_LARGE_SHARD_SIZE, + RULE_LICENSE_EXPIRATION, + RULE_CLUSTER_HEALTH, + RULE_CPU_USAGE, + RULE_DISK_USAGE, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_MEMORY_USAGE, + RULE_NODES_CHANGED, + RULE_ELASTICSEARCH_VERSION_MISMATCH, + RULE_MISSING_MONITORING_DATA, + RULE_CCR_READ_EXCEPTIONS, + RULE_LARGE_SHARD_SIZE, } from '../../../../common/constants'; import { AlertsBadge } from '../../../alerts/badge'; import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; @@ -162,24 +162,24 @@ function renderLog(log) { ); } -const OVERVIEW_PANEL_ALERTS = [ - ALERT_CLUSTER_HEALTH, - ALERT_LICENSE_EXPIRATION, - ALERT_CCR_READ_EXCEPTIONS, +const OVERVIEW_PANEL_RULES = [ + RULE_CLUSTER_HEALTH, + RULE_LICENSE_EXPIRATION, + RULE_CCR_READ_EXCEPTIONS, ]; -const NODES_PANEL_ALERTS = [ - ALERT_CPU_USAGE, - ALERT_DISK_USAGE, - ALERT_THREAD_POOL_SEARCH_REJECTIONS, - ALERT_THREAD_POOL_WRITE_REJECTIONS, - ALERT_MEMORY_USAGE, - ALERT_NODES_CHANGED, - ALERT_ELASTICSEARCH_VERSION_MISMATCH, - ALERT_MISSING_MONITORING_DATA, +const NODES_PANEL_RULES = [ + RULE_CPU_USAGE, + RULE_DISK_USAGE, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_MEMORY_USAGE, + RULE_NODES_CHANGED, + RULE_ELASTICSEARCH_VERSION_MISMATCH, + RULE_MISSING_MONITORING_DATA, ]; -const INDICES_PANEL_ALERTS = [ALERT_LARGE_SHARD_SIZE]; +const INDICES_PANEL_RULES = [RULE_LARGE_SHARD_SIZE]; export function ElasticsearchPanel(props) { const clusterStats = props.cluster_stats || {}; @@ -286,8 +286,8 @@ export function ElasticsearchPanel(props) { }; let nodesAlertStatus = null; - if (shouldShowAlertBadge(alerts, NODES_PANEL_ALERTS, setupModeContext)) { - const alertsList = NODES_PANEL_ALERTS.map((alertType) => alerts[alertType]); + if (shouldShowAlertBadge(alerts, NODES_PANEL_RULES, setupModeContext)) { + const alertsList = NODES_PANEL_RULES.map((alertType) => alerts[alertType]); nodesAlertStatus = ( @@ -296,8 +296,8 @@ export function ElasticsearchPanel(props) { } let overviewAlertStatus = null; - if (shouldShowAlertBadge(alerts, OVERVIEW_PANEL_ALERTS, setupModeContext)) { - const alertsList = OVERVIEW_PANEL_ALERTS.map((alertType) => alerts[alertType]); + if (shouldShowAlertBadge(alerts, OVERVIEW_PANEL_RULES, setupModeContext)) { + const alertsList = OVERVIEW_PANEL_RULES.map((alertType) => alerts[alertType]); overviewAlertStatus = ( @@ -306,8 +306,8 @@ export function ElasticsearchPanel(props) { } let indicesAlertStatus = null; - if (shouldShowAlertBadge(alerts, INDICES_PANEL_ALERTS, setupModeContext)) { - const alertsList = INDICES_PANEL_ALERTS.map((alertType) => alerts[alertType]); + if (shouldShowAlertBadge(alerts, INDICES_PANEL_RULES, setupModeContext)) { + const alertsList = INDICES_PANEL_RULES.map((alertType) => alerts[alertType]); indicesAlertStatus = ( diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js index ce09621b61df3..654ef6590a064 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js @@ -29,7 +29,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { SetupModeTooltip } from '../../setup_mode/tooltip'; -import { KIBANA_SYSTEM_ID, ALERT_KIBANA_VERSION_MISMATCH } from '../../../../common/constants'; +import { KIBANA_SYSTEM_ID, RULE_KIBANA_VERSION_MISMATCH } from '../../../../common/constants'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { AlertsBadge } from '../../../alerts/badge'; import { shouldShowAlertBadge } from '../../../alerts/lib/should_show_alert_badge'; @@ -37,7 +37,7 @@ import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; import { SetupModeFeature } from '../../../../common/enums'; import { SetupModeContext } from '../../setup_mode/setup_mode_context'; -const INSTANCES_PANEL_ALERTS = [ALERT_KIBANA_VERSION_MISMATCH]; +const INSTANCES_PANEL_ALERTS = [RULE_KIBANA_VERSION_MISMATCH]; export function KibanaPanel(props) { const setupMode = props.setupMode; diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js index b9c73f67e9f49..1afe75cda4027 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js @@ -15,7 +15,7 @@ import { import { LOGSTASH, LOGSTASH_SYSTEM_ID, - ALERT_LOGSTASH_VERSION_MISMATCH, + RULE_LOGSTASH_VERSION_MISMATCH, } from '../../../../common/constants'; import { @@ -42,7 +42,7 @@ import { isSetupModeFeatureEnabled } from '../../../lib/setup_mode'; import { SetupModeFeature } from '../../../../common/enums'; import { SetupModeContext } from '../../setup_mode/setup_mode_context'; -const NODES_PANEL_ALERTS = [ALERT_LOGSTASH_VERSION_MISMATCH]; +const NODES_PANEL_RULES = [RULE_LOGSTASH_VERSION_MISMATCH]; export function LogstashPanel(props) { const { setupMode } = props; @@ -72,8 +72,8 @@ export function LogstashPanel(props) { ) : null; let nodesAlertStatus = null; - if (shouldShowAlertBadge(alerts, NODES_PANEL_ALERTS, setupModeContext)) { - const alertsList = NODES_PANEL_ALERTS.map((alertType) => alerts[alertType]); + if (shouldShowAlertBadge(alerts, NODES_PANEL_RULES, setupModeContext)) { + const alertsList = NODES_PANEL_RULES.map((alertType) => alerts[alertType]); nodesAlertStatus = ( diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index 15b7290fbeaf7..710b453e7f21e 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -23,9 +23,9 @@ import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { MonitoringStartPluginDependencies, MonitoringConfig } from './types'; import { TriggersAndActionsUIPublicPluginSetup } from '../../triggers_actions_ui/public'; import { - ALERT_THREAD_POOL_SEARCH_REJECTIONS, - ALERT_THREAD_POOL_WRITE_REJECTIONS, - ALERT_DETAILS, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_DETAILS, } from '../common/constants'; import { createCpuUsageAlertType } from './alerts/cpu_usage_alert'; import { createMissingMonitoringDataAlertType } from './alerts/missing_monitoring_data_alert'; @@ -176,14 +176,14 @@ export class MonitoringPlugin ruleTypeRegistry.register(createMissingMonitoringDataAlertType()); ruleTypeRegistry.register( createThreadPoolRejectionsAlertType( - ALERT_THREAD_POOL_SEARCH_REJECTIONS, - ALERT_DETAILS[ALERT_THREAD_POOL_SEARCH_REJECTIONS] + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_DETAILS[RULE_THREAD_POOL_SEARCH_REJECTIONS] ) ); ruleTypeRegistry.register( createThreadPoolRejectionsAlertType( - ALERT_THREAD_POOL_WRITE_REJECTIONS, - ALERT_DETAILS[ALERT_THREAD_POOL_WRITE_REJECTIONS] + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_DETAILS[RULE_THREAD_POOL_WRITE_REJECTIONS] ) ); ruleTypeRegistry.register(createCCRReadExceptionsAlertType()); diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.js index fb9e1ae9a400e..91cc9c8782b22 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.js @@ -15,7 +15,7 @@ import { Ccr } from '../../../components/elasticsearch/ccr'; import { MonitoringViewBaseController } from '../../base_controller'; import { CODE_PATH_ELASTICSEARCH, - ALERT_CCR_READ_EXCEPTIONS, + RULE_CCR_READ_EXCEPTIONS, ELASTICSEARCH_SYSTEM_ID, } from '../../../../common/constants'; import { SetupModeRenderer } from '../../../components/renderers'; @@ -47,7 +47,7 @@ uiRoutes.when('/elasticsearch/ccr', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_CCR_READ_EXCEPTIONS], + alertTypeIds: [RULE_CCR_READ_EXCEPTIONS], }, }, }); diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js index 21633bd036227..767fb18685633 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js @@ -16,7 +16,7 @@ import { MonitoringViewBaseController } from '../../../base_controller'; import { CcrShard } from '../../../../components/elasticsearch/ccr_shard'; import { CODE_PATH_ELASTICSEARCH, - ALERT_CCR_READ_EXCEPTIONS, + RULE_CCR_READ_EXCEPTIONS, ELASTICSEARCH_SYSTEM_ID, } from '../../../../../common/constants'; import { SetupModeRenderer } from '../../../../components/renderers'; @@ -46,7 +46,7 @@ uiRoutes.when('/elasticsearch/ccr/:index/shard/:shardId', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_CCR_READ_EXCEPTIONS], + alertTypeIds: [RULE_CCR_READ_EXCEPTIONS], filters: [ { shardId: $route.current.pathParams.shardId, diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js index d975f523b7524..9276527951612 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js @@ -19,7 +19,7 @@ import { AdvancedIndex } from '../../../../components/elasticsearch/index/advanc import { MonitoringViewBaseController } from '../../../base_controller'; import { CODE_PATH_ELASTICSEARCH, - ALERT_LARGE_SHARD_SIZE, + RULE_LARGE_SHARD_SIZE, ELASTICSEARCH_SYSTEM_ID, } from '../../../../../common/constants'; import { SetupModeContext } from '../../../../components/setup_mode/setup_mode_context'; @@ -80,7 +80,7 @@ uiRoutes.when('/elasticsearch/indices/:index/advanced', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_LARGE_SHARD_SIZE], + alertTypeIds: [RULE_LARGE_SHARD_SIZE], filters: [ { shardIndex: $route.current.pathParams.index, diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js index e610e7c0b66b3..c9efb622ff9d1 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js @@ -21,7 +21,7 @@ import { Index } from '../../../components/elasticsearch/index/index'; import { MonitoringViewBaseController } from '../../base_controller'; import { CODE_PATH_ELASTICSEARCH, - ALERT_LARGE_SHARD_SIZE, + RULE_LARGE_SHARD_SIZE, ELASTICSEARCH_SYSTEM_ID, } from '../../../../common/constants'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; @@ -88,7 +88,7 @@ uiRoutes.when('/elasticsearch/indices/:index', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_LARGE_SHARD_SIZE], + alertTypeIds: [RULE_LARGE_SHARD_SIZE], filters: [ { shardIndex: $route.current.pathParams.index, diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js index 0d8fe49e4e50d..5acff8be20dcf 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js @@ -16,7 +16,7 @@ import template from './index.html'; import { CODE_PATH_ELASTICSEARCH, ELASTICSEARCH_SYSTEM_ID, - ALERT_LARGE_SHARD_SIZE, + RULE_LARGE_SHARD_SIZE, } from '../../../../common/constants'; import { SetupModeRenderer } from '../../../components/renderers'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; @@ -60,7 +60,7 @@ uiRoutes.when('/elasticsearch/indices', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_LARGE_SHARD_SIZE], + alertTypeIds: [RULE_LARGE_SHARD_SIZE], }, }, }); diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js index 9fa96adabeea7..dc0456178fbff 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js @@ -20,12 +20,12 @@ import { AdvancedNode } from '../../../../components/elasticsearch/node/advanced import { MonitoringViewBaseController } from '../../../base_controller'; import { CODE_PATH_ELASTICSEARCH, - ALERT_CPU_USAGE, - ALERT_THREAD_POOL_SEARCH_REJECTIONS, - ALERT_THREAD_POOL_WRITE_REJECTIONS, - ALERT_MISSING_MONITORING_DATA, - ALERT_DISK_USAGE, - ALERT_MEMORY_USAGE, + RULE_CPU_USAGE, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_MISSING_MONITORING_DATA, + RULE_DISK_USAGE, + RULE_MEMORY_USAGE, } from '../../../../../common/constants'; function getPageData($injector) { @@ -77,12 +77,12 @@ uiRoutes.when('/elasticsearch/nodes/:node/advanced', { shouldFetch: true, options: { alertTypeIds: [ - ALERT_CPU_USAGE, - ALERT_DISK_USAGE, - ALERT_THREAD_POOL_SEARCH_REJECTIONS, - ALERT_THREAD_POOL_WRITE_REJECTIONS, - ALERT_MEMORY_USAGE, - ALERT_MISSING_MONITORING_DATA, + RULE_CPU_USAGE, + RULE_DISK_USAGE, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_MEMORY_USAGE, + RULE_MISSING_MONITORING_DATA, ], filters: [ { diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js index cb99a3c519969..3ec10aa9d4a4c 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js @@ -22,12 +22,12 @@ import { nodesByIndices } from '../../../components/elasticsearch/shard_allocati import { MonitoringViewBaseController } from '../../base_controller'; import { CODE_PATH_ELASTICSEARCH, - ALERT_CPU_USAGE, - ALERT_THREAD_POOL_SEARCH_REJECTIONS, - ALERT_THREAD_POOL_WRITE_REJECTIONS, - ALERT_MISSING_MONITORING_DATA, - ALERT_DISK_USAGE, - ALERT_MEMORY_USAGE, + RULE_CPU_USAGE, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_MISSING_MONITORING_DATA, + RULE_DISK_USAGE, + RULE_MEMORY_USAGE, ELASTICSEARCH_SYSTEM_ID, } from '../../../../common/constants'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; @@ -64,12 +64,12 @@ uiRoutes.when('/elasticsearch/nodes/:node', { shouldFetch: true, options: { alertTypeIds: [ - ALERT_CPU_USAGE, - ALERT_DISK_USAGE, - ALERT_THREAD_POOL_SEARCH_REJECTIONS, - ALERT_THREAD_POOL_WRITE_REJECTIONS, - ALERT_MEMORY_USAGE, - ALERT_MISSING_MONITORING_DATA, + RULE_CPU_USAGE, + RULE_DISK_USAGE, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_MEMORY_USAGE, + RULE_MISSING_MONITORING_DATA, ], filters: [ { diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js index fd5ababcb4936..5bc546e8590ad 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js @@ -19,12 +19,12 @@ import { SetupModeRenderer } from '../../../components/renderers'; import { ELASTICSEARCH_SYSTEM_ID, CODE_PATH_ELASTICSEARCH, - ALERT_CPU_USAGE, - ALERT_THREAD_POOL_SEARCH_REJECTIONS, - ALERT_THREAD_POOL_WRITE_REJECTIONS, - ALERT_MISSING_MONITORING_DATA, - ALERT_DISK_USAGE, - ALERT_MEMORY_USAGE, + RULE_CPU_USAGE, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_MISSING_MONITORING_DATA, + RULE_DISK_USAGE, + RULE_MEMORY_USAGE, } from '../../../../common/constants'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; @@ -94,12 +94,12 @@ uiRoutes.when('/elasticsearch/nodes', { shouldFetch: true, options: { alertTypeIds: [ - ALERT_CPU_USAGE, - ALERT_DISK_USAGE, - ALERT_THREAD_POOL_SEARCH_REJECTIONS, - ALERT_THREAD_POOL_WRITE_REJECTIONS, - ALERT_MEMORY_USAGE, - ALERT_MISSING_MONITORING_DATA, + RULE_CPU_USAGE, + RULE_DISK_USAGE, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_MEMORY_USAGE, + RULE_MISSING_MONITORING_DATA, ], }, }, diff --git a/x-pack/plugins/monitoring/public/views/kibana/instance/index.js b/x-pack/plugins/monitoring/public/views/kibana/instance/index.js index a318044330fe1..a71289b084516 100644 --- a/x-pack/plugins/monitoring/public/views/kibana/instance/index.js +++ b/x-pack/plugins/monitoring/public/views/kibana/instance/index.js @@ -28,7 +28,7 @@ import { import { MonitoringTimeseriesContainer } from '../../../components/chart'; import { DetailStatus } from '../../../components/kibana/detail_status'; import { MonitoringViewBaseController } from '../../base_controller'; -import { CODE_PATH_KIBANA, ALERT_KIBANA_VERSION_MISMATCH } from '../../../../common/constants'; +import { CODE_PATH_KIBANA, RULE_KIBANA_VERSION_MISMATCH } from '../../../../common/constants'; import { AlertsCallout } from '../../../alerts/callout'; function getPageData($injector) { @@ -77,7 +77,7 @@ uiRoutes.when('/kibana/instances/:uuid', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH], + alertTypeIds: [RULE_KIBANA_VERSION_MISMATCH], }, }, }); diff --git a/x-pack/plugins/monitoring/public/views/kibana/instances/index.js b/x-pack/plugins/monitoring/public/views/kibana/instances/index.js index 66fec51688cef..2601a366e6843 100644 --- a/x-pack/plugins/monitoring/public/views/kibana/instances/index.js +++ b/x-pack/plugins/monitoring/public/views/kibana/instances/index.js @@ -18,7 +18,7 @@ import { SetupModeContext } from '../../../components/setup_mode/setup_mode_cont import { KIBANA_SYSTEM_ID, CODE_PATH_KIBANA, - ALERT_KIBANA_VERSION_MISMATCH, + RULE_KIBANA_VERSION_MISMATCH, } from '../../../../common/constants'; uiRoutes.when('/kibana/instances', { @@ -48,7 +48,7 @@ uiRoutes.when('/kibana/instances', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_KIBANA_VERSION_MISMATCH], + alertTypeIds: [RULE_KIBANA_VERSION_MISMATCH], }, }, }); diff --git a/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.js b/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.js index 15d97a7dd52aa..9acfd81d186fd 100644 --- a/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.js +++ b/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.js @@ -29,7 +29,7 @@ import { import { MonitoringTimeseriesContainer } from '../../../../components/chart'; import { CODE_PATH_LOGSTASH, - ALERT_LOGSTASH_VERSION_MISMATCH, + RULE_LOGSTASH_VERSION_MISMATCH, } from '../../../../../common/constants'; import { AlertsCallout } from '../../../../alerts/callout'; @@ -77,7 +77,7 @@ uiRoutes.when('/logstash/node/:uuid/advanced', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH], + alertTypeIds: [RULE_LOGSTASH_VERSION_MISMATCH], }, }, telemetryPageViewTitle: 'logstash_node_advanced', diff --git a/x-pack/plugins/monitoring/public/views/logstash/node/index.js b/x-pack/plugins/monitoring/public/views/logstash/node/index.js index 80136c8aa9a24..b23875ba1a3bb 100644 --- a/x-pack/plugins/monitoring/public/views/logstash/node/index.js +++ b/x-pack/plugins/monitoring/public/views/logstash/node/index.js @@ -27,7 +27,7 @@ import { } from '@elastic/eui'; import { MonitoringTimeseriesContainer } from '../../../components/chart'; import { MonitoringViewBaseController } from '../../base_controller'; -import { CODE_PATH_LOGSTASH, ALERT_LOGSTASH_VERSION_MISMATCH } from '../../../../common/constants'; +import { CODE_PATH_LOGSTASH, RULE_LOGSTASH_VERSION_MISMATCH } from '../../../../common/constants'; import { AlertsCallout } from '../../../alerts/callout'; function getPageData($injector) { @@ -74,7 +74,7 @@ uiRoutes.when('/logstash/node/:uuid', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH], + alertTypeIds: [RULE_LOGSTASH_VERSION_MISMATCH], }, }, telemetryPageViewTitle: 'logstash_node', diff --git a/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js b/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js index b802af05be9a2..56b5d0ec6c82a 100644 --- a/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js +++ b/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js @@ -18,7 +18,7 @@ import { SetupModeContext } from '../../../components/setup_mode/setup_mode_cont import { CODE_PATH_LOGSTASH, LOGSTASH_SYSTEM_ID, - ALERT_LOGSTASH_VERSION_MISMATCH, + RULE_LOGSTASH_VERSION_MISMATCH, } from '../../../../common/constants'; uiRoutes.when('/logstash/nodes', { @@ -48,7 +48,7 @@ uiRoutes.when('/logstash/nodes', { alerts: { shouldFetch: true, options: { - alertTypeIds: [ALERT_LOGSTASH_VERSION_MISMATCH], + alertTypeIds: [RULE_LOGSTASH_VERSION_MISMATCH], }, }, }); diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts b/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts index a008170ac472f..c86a5264b204b 100644 --- a/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/alerts_factory.test.ts @@ -6,7 +6,7 @@ */ import { AlertsFactory } from './alerts_factory'; -import { ALERT_CPU_USAGE } from '../../common/constants'; +import { RULE_CPU_USAGE } from '../../common/constants'; jest.mock('../static_globals', () => ({ Globals: { @@ -39,7 +39,7 @@ describe('AlertsFactory', () => { ], }; }); - const alerts = await AlertsFactory.getByType(ALERT_CPU_USAGE, rulesClient as any); + const alerts = await AlertsFactory.getByType(RULE_CPU_USAGE, rulesClient as any); expect(alerts).not.toBeNull(); expect(alerts.length).toBe(2); expect(alerts[0].getId()).toBe(1); @@ -54,7 +54,7 @@ describe('AlertsFactory', () => { total: 0, }; }); - await AlertsFactory.getByType(ALERT_CPU_USAGE, rulesClient as any); - expect(filter).toBe(`alert.attributes.alertTypeId:${ALERT_CPU_USAGE}`); + await AlertsFactory.getByType(RULE_CPU_USAGE, rulesClient as any); + expect(filter).toBe(`alert.attributes.alertTypeId:${RULE_CPU_USAGE}`); }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts b/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts index 24dd57d4f2562..a276f96df009f 100644 --- a/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts +++ b/x-pack/plugins/monitoring/server/alerts/alerts_factory.ts @@ -6,64 +6,64 @@ */ import { - LargeShardSizeAlert, - CCRReadExceptionsAlert, - CpuUsageAlert, - MissingMonitoringDataAlert, - DiskUsageAlert, - ThreadPoolSearchRejectionsAlert, - ThreadPoolWriteRejectionsAlert, - MemoryUsageAlert, - NodesChangedAlert, - ClusterHealthAlert, - LicenseExpirationAlert, - LogstashVersionMismatchAlert, - KibanaVersionMismatchAlert, - ElasticsearchVersionMismatchAlert, - BaseAlert, + LargeShardSizeRule, + CCRReadExceptionsRule, + CpuUsageRule, + MissingMonitoringDataRule, + DiskUsageRule, + ThreadPoolSearchRejectionsRule, + ThreadPoolWriteRejectionsRule, + MemoryUsageRule, + NodesChangedRule, + ClusterHealthRule, + LicenseExpirationRule, + LogstashVersionMismatchRule, + KibanaVersionMismatchRule, + ElasticsearchVersionMismatchRule, + BaseRule, } from './'; import { - ALERT_CLUSTER_HEALTH, - ALERT_LICENSE_EXPIRATION, - ALERT_CPU_USAGE, - ALERT_MISSING_MONITORING_DATA, - ALERT_DISK_USAGE, - ALERT_THREAD_POOL_SEARCH_REJECTIONS, - ALERT_THREAD_POOL_WRITE_REJECTIONS, - ALERT_MEMORY_USAGE, - ALERT_NODES_CHANGED, - ALERT_LOGSTASH_VERSION_MISMATCH, - ALERT_KIBANA_VERSION_MISMATCH, - ALERT_ELASTICSEARCH_VERSION_MISMATCH, - ALERT_CCR_READ_EXCEPTIONS, - ALERT_LARGE_SHARD_SIZE, + RULE_CLUSTER_HEALTH, + RULE_LICENSE_EXPIRATION, + RULE_CPU_USAGE, + RULE_MISSING_MONITORING_DATA, + RULE_DISK_USAGE, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_MEMORY_USAGE, + RULE_NODES_CHANGED, + RULE_LOGSTASH_VERSION_MISMATCH, + RULE_KIBANA_VERSION_MISMATCH, + RULE_ELASTICSEARCH_VERSION_MISMATCH, + RULE_CCR_READ_EXCEPTIONS, + RULE_LARGE_SHARD_SIZE, } from '../../common/constants'; import { RulesClient } from '../../../alerting/server'; import { Alert } from '../../../alerting/common'; import { CommonAlertParams } from '../../common/types/alerts'; const BY_TYPE = { - [ALERT_CLUSTER_HEALTH]: ClusterHealthAlert, - [ALERT_LICENSE_EXPIRATION]: LicenseExpirationAlert, - [ALERT_CPU_USAGE]: CpuUsageAlert, - [ALERT_MISSING_MONITORING_DATA]: MissingMonitoringDataAlert, - [ALERT_DISK_USAGE]: DiskUsageAlert, - [ALERT_THREAD_POOL_SEARCH_REJECTIONS]: ThreadPoolSearchRejectionsAlert, - [ALERT_THREAD_POOL_WRITE_REJECTIONS]: ThreadPoolWriteRejectionsAlert, - [ALERT_MEMORY_USAGE]: MemoryUsageAlert, - [ALERT_NODES_CHANGED]: NodesChangedAlert, - [ALERT_LOGSTASH_VERSION_MISMATCH]: LogstashVersionMismatchAlert, - [ALERT_KIBANA_VERSION_MISMATCH]: KibanaVersionMismatchAlert, - [ALERT_ELASTICSEARCH_VERSION_MISMATCH]: ElasticsearchVersionMismatchAlert, - [ALERT_CCR_READ_EXCEPTIONS]: CCRReadExceptionsAlert, - [ALERT_LARGE_SHARD_SIZE]: LargeShardSizeAlert, + [RULE_CLUSTER_HEALTH]: ClusterHealthRule, + [RULE_LICENSE_EXPIRATION]: LicenseExpirationRule, + [RULE_CPU_USAGE]: CpuUsageRule, + [RULE_MISSING_MONITORING_DATA]: MissingMonitoringDataRule, + [RULE_DISK_USAGE]: DiskUsageRule, + [RULE_THREAD_POOL_SEARCH_REJECTIONS]: ThreadPoolSearchRejectionsRule, + [RULE_THREAD_POOL_WRITE_REJECTIONS]: ThreadPoolWriteRejectionsRule, + [RULE_MEMORY_USAGE]: MemoryUsageRule, + [RULE_NODES_CHANGED]: NodesChangedRule, + [RULE_LOGSTASH_VERSION_MISMATCH]: LogstashVersionMismatchRule, + [RULE_KIBANA_VERSION_MISMATCH]: KibanaVersionMismatchRule, + [RULE_ELASTICSEARCH_VERSION_MISMATCH]: ElasticsearchVersionMismatchRule, + [RULE_CCR_READ_EXCEPTIONS]: CCRReadExceptionsRule, + [RULE_LARGE_SHARD_SIZE]: LargeShardSizeRule, }; export class AlertsFactory { public static async getByType( type: string, alertsClient: RulesClient | undefined - ): Promise { + ): Promise { const alertCls = BY_TYPE[type]; if (!alertCls || !alertsClient) { return []; @@ -77,7 +77,7 @@ export class AlertsFactory { if (!alertClientAlerts.total || !alertClientAlerts.data?.length) { return []; } - return alertClientAlerts.data.map((alert) => new alertCls(alert as Alert) as BaseAlert); + return alertClientAlerts.data.map((alert) => new alertCls(alert as Alert) as BaseRule); } public static getAll() { diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/base_rule.test.ts similarity index 80% rename from x-pack/plugins/monitoring/server/alerts/base_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/base_rule.test.ts index 3fe4eac712487..5234fcfce5cbf 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_rule.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; jest.mock('../static_globals', () => ({ Globals: { @@ -15,10 +15,10 @@ jest.mock('../static_globals', () => ({ }, })); -describe('BaseAlert', () => { +describe('BaseRule', () => { describe('create', () => { - it('should create an alert if it does not exist', async () => { - const alert = new BaseAlert(); + it('should create a rule if it does not exist', async () => { + const rule = new BaseRule(); const rulesClient = { create: jest.fn(), find: jest.fn().mockImplementation(() => { @@ -41,7 +41,7 @@ describe('BaseAlert', () => { }, ]; - await alert.createIfDoesNotExist(rulesClient as any, actionsClient as any, actions); + await rule.createIfDoesNotExist(rulesClient as any, actionsClient as any, actions); expect(rulesClient.create).toHaveBeenCalledWith({ data: { actions: [ @@ -71,8 +71,8 @@ describe('BaseAlert', () => { }); }); - it('should not create an alert if it exists', async () => { - const alert = new BaseAlert(); + it('should not create a rule if it exists', async () => { + const rule = new BaseRule(); const rulesClient = { create: jest.fn(), find: jest.fn().mockImplementation(() => { @@ -96,7 +96,7 @@ describe('BaseAlert', () => { }, ]; - await alert.createIfDoesNotExist(rulesClient as any, actionsClient as any, actions); + await rule.createIfDoesNotExist(rulesClient as any, actionsClient as any, actions); expect(rulesClient.create).not.toHaveBeenCalled(); }); }); @@ -116,8 +116,8 @@ describe('BaseAlert', () => { }; const id = '456def'; const filters: any[] = []; - const alert = new BaseAlert(); - const states = await alert.getStates(rulesClient as any, id, filters); + const rule = new BaseRule(); + const states = await rule.getStates(rulesClient as any, id, filters); expect(states).toStrictEqual({ abc123: { id: 'foobar', @@ -133,8 +133,8 @@ describe('BaseAlert', () => { }; const id = '456def'; const filters: any[] = []; - const alert = new BaseAlert(); - const states = await alert.getStates(rulesClient as any, id, filters); + const rule = new BaseRule(); + const states = await rule.getStates(rulesClient as any, id, filters); expect(states).toStrictEqual({}); }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_rule.ts similarity index 90% rename from x-pack/plugins/monitoring/server/alerts/base_alert.ts rename to x-pack/plugins/monitoring/server/alerts/base_rule.ts index 7bc5d4242d0bd..62fb24560ddd6 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_rule.ts @@ -44,7 +44,7 @@ type ExecutedState = } | Record; -interface AlertOptions { +interface RuleOptions { id: string; name: string; throttle?: string | null; @@ -55,7 +55,7 @@ interface AlertOptions { accessorKey?: string; } -const defaultAlertOptions = (): AlertOptions => { +const defaultRuleOptions = (): RuleOptions => { return { id: '', name: '', @@ -65,24 +65,24 @@ const defaultAlertOptions = (): AlertOptions => { actionVariables: [], }; }; -export class BaseAlert { +export class BaseRule { protected scopedLogger: Logger; constructor( - public rawAlert?: SanitizedAlert, - public alertOptions: AlertOptions = defaultAlertOptions() + public sanitizedRule?: SanitizedAlert, + public ruleOptions: RuleOptions = defaultRuleOptions() ) { - const defaultOptions = defaultAlertOptions(); + const defaultOptions = defaultRuleOptions(); defaultOptions.defaultParams = { ...defaultOptions.defaultParams, - ...this.alertOptions.defaultParams, + ...this.ruleOptions.defaultParams, }; - this.alertOptions = { ...defaultOptions, ...this.alertOptions }; - this.scopedLogger = Globals.app.getLogger(alertOptions.id); + this.ruleOptions = { ...defaultOptions, ...this.ruleOptions }; + this.scopedLogger = Globals.app.getLogger(ruleOptions.id); } - public getAlertType(): AlertType { - const { id, name, actionVariables } = this.alertOptions; + public getRuleType(): AlertType { + const { id, name, actionVariables } = this.ruleOptions; return { id, name, @@ -110,7 +110,7 @@ export class BaseAlert { } public getId() { - return this.rawAlert?.id; + return this.sanitizedRule?.id; } public async createIfDoesNotExist( @@ -118,24 +118,24 @@ export class BaseAlert { actionsClient: ActionsClient, actions: AlertEnableAction[] ): Promise> { - const existingAlertData = await rulesClient.find({ + const existingRuleData = await rulesClient.find({ options: { - search: this.alertOptions.id, + search: this.ruleOptions.id, }, }); - if (existingAlertData.total > 0) { - const existingAlert = existingAlertData.data[0] as Alert; - return existingAlert; + if (existingRuleData.total > 0) { + const existingRule = existingRuleData.data[0] as Alert; + return existingRule; } - const alertActions = []; + const ruleActions = []; for (const actionData of actions) { const action = await actionsClient.get({ id: actionData.id }); if (!action) { continue; } - alertActions.push({ + ruleActions.push({ group: 'default', id: actionData.id, params: { @@ -151,7 +151,7 @@ export class BaseAlert { id: alertTypeId, throttle = '1d', interval = '1m', - } = this.alertOptions; + } = this.ruleOptions; return await rulesClient.create({ data: { enabled: true, @@ -163,7 +163,7 @@ export class BaseAlert { throttle, notifyWhen: null, schedule: { interval }, - actions: alertActions, + actions: ruleActions, }, }); } @@ -247,11 +247,11 @@ export class BaseAlert { return await fetchClusters(esClient, esIndexPattern); } const limit = parseDuration(params.limit); - const rangeFilter = this.alertOptions.fetchClustersRange + const rangeFilter = this.ruleOptions.fetchClustersRange ? { timestamp: { format: 'epoch_millis', - gte: +new Date() - limit - this.alertOptions.fetchClustersRange, + gte: +new Date() - limit - this.ruleOptions.fetchClustersRange, }, } : undefined; @@ -281,7 +281,7 @@ export class BaseAlert { continue; } - const key = this.alertOptions.accessorKey; + const key = this.ruleOptions.accessorKey; // for each node, update the alert's state with node state for (const node of nodes) { diff --git a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts similarity index 86% rename from x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts index 5c8ef7abbbf51..8dd4623bfd7e4 100644 --- a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.test.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { CCRReadExceptionsAlert } from './ccr_read_exceptions_alert'; -import { ALERT_CCR_READ_EXCEPTIONS } from '../../common/constants'; +import { CCRReadExceptionsRule } from './ccr_read_exceptions_rule'; +import { RULE_CCR_READ_EXCEPTIONS } from '../../common/constants'; import { fetchCCRReadExceptions } from '../lib/alerts/fetch_ccr_read_exceptions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; -type ICCRReadExceptionsAlertMock = CCRReadExceptionsAlert & { +type ICCRReadExceptionsRuleMock = CCRReadExceptionsRule & { defaultParams: { duration: string; }; @@ -47,16 +47,16 @@ jest.mock('../static_globals', () => ({ }, })); -describe('CCRReadExceptionsAlert', () => { +describe('CCRReadExceptionsRule', () => { it('should have defaults', () => { - const alert = new CCRReadExceptionsAlert() as ICCRReadExceptionsAlertMock; - expect(alert.alertOptions.id).toBe(ALERT_CCR_READ_EXCEPTIONS); - expect(alert.alertOptions.name).toBe('CCR read exceptions'); - expect(alert.alertOptions.throttle).toBe('6h'); - expect(alert.alertOptions.defaultParams).toStrictEqual({ + const rule = new CCRReadExceptionsRule() as ICCRReadExceptionsRuleMock; + expect(rule.ruleOptions.id).toBe(RULE_CCR_READ_EXCEPTIONS); + expect(rule.ruleOptions.name).toBe('CCR read exceptions'); + expect(rule.ruleOptions.throttle).toBe('6h'); + expect(rule.ruleOptions.defaultParams).toStrictEqual({ duration: '1h', }); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'remoteCluster', description: 'The remote cluster experiencing CCR read exceptions.', @@ -146,11 +146,11 @@ describe('CCRReadExceptionsAlert', () => { }); it('should fire actions', async () => { - const alert = new CCRReadExceptionsAlert() as ICCRReadExceptionsAlertMock; - const type = alert.getAlertType(); + const rule = new CCRReadExceptionsRule() as ICCRReadExceptionsRuleMock; + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(scheduleActions).toHaveBeenCalledWith('default', { internalFullMessage: `CCR read exceptions alert is firing for the following remote cluster: ${remoteCluster}. Current 'follower_index' index affected: ${followerIndex}. [View CCR stats](http://localhost:5601/app/monitoring#/elasticsearch/ccr?_g=(cluster_uuid:${clusterUuid}))`, @@ -177,11 +177,11 @@ describe('CCRReadExceptionsAlert', () => { }, ]; }); - const alert = new CCRReadExceptionsAlert() as ICCRReadExceptionsAlertMock; - const type = alert.getAlertType(); + const rule = new CCRReadExceptionsRule() as ICCRReadExceptionsRuleMock; + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(scheduleActions).toHaveBeenCalledWith('default', { internalFullMessage: `CCR read exceptions alert is firing for the following remote cluster: ${remoteCluster}. Current 'follower_index' index affected: ${followerIndex}. [View CCR stats](http://localhost:5601/app/monitoring#/elasticsearch/ccr?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`, diff --git a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_alert.ts b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts similarity index 96% rename from x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_alert.ts rename to x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts index 28f562b2cb131..587b3a69fb768 100644 --- a/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/ccr_read_exceptions_rule.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -24,8 +24,8 @@ import { import { AlertInstance } from '../../../alerting/server'; import { INDEX_PATTERN_ELASTICSEARCH, - ALERT_CCR_READ_EXCEPTIONS, - ALERT_DETAILS, + RULE_CCR_READ_EXCEPTIONS, + RULE_DETAILS, } from '../../common/constants'; import { fetchCCRReadExceptions } from '../lib/alerts/fetch_ccr_read_exceptions'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; @@ -36,11 +36,11 @@ import { AlertingDefaults, createLink } from './alert_helpers'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { Globals } from '../static_globals'; -export class CCRReadExceptionsAlert extends BaseAlert { - constructor(public rawAlert?: SanitizedAlert) { - super(rawAlert, { - id: ALERT_CCR_READ_EXCEPTIONS, - name: ALERT_DETAILS[ALERT_CCR_READ_EXCEPTIONS].label, +export class CCRReadExceptionsRule extends BaseRule { + constructor(public sanitizedRule?: SanitizedAlert) { + super(sanitizedRule, { + id: RULE_CCR_READ_EXCEPTIONS, + name: RULE_DETAILS[RULE_CCR_READ_EXCEPTIONS].label, throttle: '6h', defaultParams: { duration: '1h', diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.test.ts similarity index 89% rename from x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/cluster_health_rule.test.ts index 68d0f7b1e0ce7..5d209f7fc4a81 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { ClusterHealthAlert } from './cluster_health_alert'; -import { ALERT_CLUSTER_HEALTH } from '../../common/constants'; +import { ClusterHealthRule } from './cluster_health_rule'; +import { RULE_CLUSTER_HEALTH } from '../../common/constants'; import { AlertClusterHealthType, AlertSeverity } from '../../common/enums'; import { fetchClusterHealth } from '../lib/alerts/fetch_cluster_health'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; @@ -35,13 +35,13 @@ jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), })); -describe('ClusterHealthAlert', () => { +describe('ClusterHealthRule', () => { it('should have defaults', () => { - const alert = new ClusterHealthAlert(); - expect(alert.alertOptions.id).toBe(ALERT_CLUSTER_HEALTH); - expect(alert.alertOptions.name).toBe('Cluster health'); - expect(alert.alertOptions.throttle).toBe('1d'); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + const rule = new ClusterHealthRule(); + expect(rule.ruleOptions.id).toBe(RULE_CLUSTER_HEALTH); + expect(rule.ruleOptions.name).toBe('Cluster health'); + expect(rule.ruleOptions.throttle).toBe('1d'); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'clusterHealth', description: 'The health of the cluster.' }, { name: 'internalShortMessage', @@ -112,8 +112,8 @@ describe('ClusterHealthAlert', () => { }); it('should fire actions', async () => { - const alert = new ClusterHealthAlert(); - const type = alert.getAlertType(); + const rule = new ClusterHealthRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, params: {}, @@ -179,8 +179,8 @@ describe('ClusterHealthAlert', () => { }, ]; }); - const alert = new ClusterHealthAlert(); - const type = alert.getAlertType(); + const rule = new ClusterHealthRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, params: {}, diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts similarity index 94% rename from x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts rename to x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts index c5983ae9897fe..7fac3b74a1b66 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_rule.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -20,8 +20,8 @@ import { } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; import { - ALERT_CLUSTER_HEALTH, - LEGACY_ALERT_DETAILS, + RULE_CLUSTER_HEALTH, + LEGACY_RULE_DETAILS, INDEX_PATTERN_ELASTICSEARCH, } from '../../common/constants'; import { AlertMessageTokenType, AlertClusterHealthType, AlertSeverity } from '../../common/enums'; @@ -43,11 +43,11 @@ const YELLOW_STATUS_MESSAGE = i18n.translate( } ); -export class ClusterHealthAlert extends BaseAlert { - constructor(public rawAlert?: SanitizedAlert) { - super(rawAlert, { - id: ALERT_CLUSTER_HEALTH, - name: LEGACY_ALERT_DETAILS[ALERT_CLUSTER_HEALTH].label, +export class ClusterHealthRule extends BaseRule { + constructor(public sanitizedRule?: SanitizedAlert) { + super(sanitizedRule, { + id: RULE_CLUSTER_HEALTH, + name: LEGACY_RULE_DETAILS[RULE_CLUSTER_HEALTH].label, actionVariables: [ { name: 'clusterHealth', diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts similarity index 89% rename from x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts index 81c51366cb8aa..9b19c1ddeb7d1 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { CpuUsageAlert } from './cpu_usage_alert'; -import { ALERT_CPU_USAGE } from '../../common/constants'; +import { CpuUsageRule } from './cpu_usage_rule'; +import { RULE_CPU_USAGE } from '../../common/constants'; import { fetchCpuUsageNodeStats } from '../lib/alerts/fetch_cpu_usage_node_stats'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; @@ -35,14 +35,14 @@ jest.mock('../static_globals', () => ({ }, })); -describe('CpuUsageAlert', () => { +describe('CpuUsageRule', () => { it('should have defaults', () => { - const alert = new CpuUsageAlert(); - expect(alert.alertOptions.id).toBe(ALERT_CPU_USAGE); - expect(alert.alertOptions.name).toBe('CPU Usage'); - expect(alert.alertOptions.throttle).toBe('1d'); - expect(alert.alertOptions.defaultParams).toStrictEqual({ threshold: 85, duration: '5m' }); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + const rule = new CpuUsageRule(); + expect(rule.ruleOptions.id).toBe(RULE_CPU_USAGE); + expect(rule.ruleOptions.name).toBe('CPU Usage'); + expect(rule.ruleOptions.throttle).toBe('1d'); + expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 85, duration: '5m' }); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'node', description: 'The node reporting high cpu usage.' }, { name: 'internalShortMessage', @@ -114,11 +114,11 @@ describe('CpuUsageAlert', () => { }); it('should fire actions', async () => { - const alert = new CpuUsageAlert(); - const type = alert.getAlertType(); + const rule = new CpuUsageRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); const count = 1; expect(replaceState).toHaveBeenCalledWith({ @@ -211,11 +211,11 @@ describe('CpuUsageAlert', () => { }, ]; }); - const alert = new CpuUsageAlert(); - const type = alert.getAlertType(); + const rule = new CpuUsageRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [], @@ -233,11 +233,11 @@ describe('CpuUsageAlert', () => { }, ]; }); - const alert = new CpuUsageAlert(); - const type = alert.getAlertType(); + const rule = new CpuUsageRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); const count = 1; expect(scheduleActions).toHaveBeenCalledWith('default', { diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts similarity index 95% rename from x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts rename to x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts index 44b3cb306bd9f..2e57a3c22de1b 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_rule.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import numeral from '@elastic/numeral'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -23,11 +23,7 @@ import { CommonAlertFilter, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; -import { - INDEX_PATTERN_ELASTICSEARCH, - ALERT_CPU_USAGE, - ALERT_DETAILS, -} from '../../common/constants'; +import { INDEX_PATTERN_ELASTICSEARCH, RULE_CPU_USAGE, RULE_DETAILS } from '../../common/constants'; // @ts-ignore import { ROUNDED_FLOAT } from '../../common/formatting'; import { fetchCpuUsageNodeStats } from '../lib/alerts/fetch_cpu_usage_node_stats'; @@ -39,11 +35,11 @@ import { AlertingDefaults, createLink } from './alert_helpers'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { Globals } from '../static_globals'; -export class CpuUsageAlert extends BaseAlert { - constructor(public rawAlert?: SanitizedAlert) { - super(rawAlert, { - id: ALERT_CPU_USAGE, - name: ALERT_DETAILS[ALERT_CPU_USAGE].label, +export class CpuUsageRule extends BaseRule { + constructor(public sanitizedRule?: SanitizedAlert) { + super(sanitizedRule, { + id: RULE_CPU_USAGE, + name: RULE_DETAILS[RULE_CPU_USAGE].label, accessorKey: 'cpuUsage', defaultParams: { threshold: 85, diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.test.ts similarity index 85% rename from x-pack/plugins/monitoring/server/alerts/disk_usage_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/disk_usage_rule.test.ts index 0514e7b3cb078..63ff6a7ccab93 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.test.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { DiskUsageAlert } from './disk_usage_alert'; -import { ALERT_DISK_USAGE } from '../../common/constants'; +import { DiskUsageRule } from './disk_usage_rule'; +import { RULE_DISK_USAGE } from '../../common/constants'; import { fetchDiskUsageNodeStats } from '../lib/alerts/fetch_disk_usage_node_stats'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; -type IDiskUsageAlertMock = DiskUsageAlert & { +type IDiskUsageAlertMock = DiskUsageRule & { defaultParams: { threshold: number; duration: string; @@ -48,14 +48,14 @@ jest.mock('../static_globals', () => ({ }, })); -describe('DiskUsageAlert', () => { +describe('DiskUsageRule', () => { it('should have defaults', () => { - const alert = new DiskUsageAlert() as IDiskUsageAlertMock; - expect(alert.alertOptions.id).toBe(ALERT_DISK_USAGE); - expect(alert.alertOptions.name).toBe('Disk Usage'); - expect(alert.alertOptions.throttle).toBe('1d'); - expect(alert.alertOptions.defaultParams).toStrictEqual({ threshold: 80, duration: '5m' }); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + const alert = new DiskUsageRule() as IDiskUsageAlertMock; + expect(alert.ruleOptions.id).toBe(RULE_DISK_USAGE); + expect(alert.ruleOptions.name).toBe('Disk Usage'); + expect(alert.ruleOptions.throttle).toBe('1d'); + expect(alert.ruleOptions.defaultParams).toStrictEqual({ threshold: 80, duration: '5m' }); + expect(alert.ruleOptions.actionVariables).toStrictEqual([ { name: 'node', description: 'The node reporting high disk usage.' }, { name: 'internalShortMessage', @@ -126,11 +126,11 @@ describe('DiskUsageAlert', () => { }); it('should fire actions', async () => { - const alert = new DiskUsageAlert() as IDiskUsageAlertMock; - const type = alert.getAlertType(); + const rule = new DiskUsageRule() as IDiskUsageAlertMock; + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); const count = 1; expect(scheduleActions).toHaveBeenCalledWith('default', { @@ -156,11 +156,11 @@ describe('DiskUsageAlert', () => { }, ]; }); - const alert = new DiskUsageAlert() as IDiskUsageAlertMock; - const type = alert.getAlertType(); + const rule = new DiskUsageRule() as IDiskUsageAlertMock; + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); const count = 1; expect(scheduleActions).toHaveBeenCalledWith('default', { diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts similarity index 95% rename from x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts rename to x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts index ac7829d121a3a..ae3025c1db92c 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_rule.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import numeral from '@elastic/numeral'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -23,11 +23,7 @@ import { CommonAlertFilter, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; -import { - INDEX_PATTERN_ELASTICSEARCH, - ALERT_DISK_USAGE, - ALERT_DETAILS, -} from '../../common/constants'; +import { INDEX_PATTERN_ELASTICSEARCH, RULE_DISK_USAGE, RULE_DETAILS } from '../../common/constants'; // @ts-ignore import { ROUNDED_FLOAT } from '../../common/formatting'; import { fetchDiskUsageNodeStats } from '../lib/alerts/fetch_disk_usage_node_stats'; @@ -38,11 +34,11 @@ import { AlertingDefaults, createLink } from './alert_helpers'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { Globals } from '../static_globals'; -export class DiskUsageAlert extends BaseAlert { - constructor(public rawAlert?: SanitizedAlert) { - super(rawAlert, { - id: ALERT_DISK_USAGE, - name: ALERT_DETAILS[ALERT_DISK_USAGE].label, +export class DiskUsageRule extends BaseRule { + constructor(public sanitizedRule?: SanitizedAlert) { + super(sanitizedRule, { + id: RULE_DISK_USAGE, + name: RULE_DETAILS[RULE_DISK_USAGE].label, accessorKey: 'diskUsage', defaultParams: { threshold: 80, diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.test.ts similarity index 86% rename from x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.test.ts index 3ac15d795d027..12fa54f34e3c4 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { ElasticsearchVersionMismatchAlert } from './elasticsearch_version_mismatch_alert'; -import { ALERT_ELASTICSEARCH_VERSION_MISMATCH } from '../../common/constants'; +import { ElasticsearchVersionMismatchRule } from './elasticsearch_version_mismatch_rule'; +import { RULE_ELASTICSEARCH_VERSION_MISMATCH } from '../../common/constants'; import { fetchElasticsearchVersions } from '../lib/alerts/fetch_elasticsearch_versions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; @@ -38,11 +38,11 @@ jest.mock('../static_globals', () => ({ describe('ElasticsearchVersionMismatchAlert', () => { it('should have defaults', () => { - const alert = new ElasticsearchVersionMismatchAlert(); - expect(alert.alertOptions.id).toBe(ALERT_ELASTICSEARCH_VERSION_MISMATCH); - expect(alert.alertOptions.name).toBe('Elasticsearch version mismatch'); - expect(alert.alertOptions.throttle).toBe('1d'); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + const rule = new ElasticsearchVersionMismatchRule(); + expect(rule.ruleOptions.id).toBe(RULE_ELASTICSEARCH_VERSION_MISMATCH); + expect(rule.ruleOptions.name).toBe('Elasticsearch version mismatch'); + expect(rule.ruleOptions.throttle).toBe('1d'); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'versionList', description: 'The versions of Elasticsearch running in this cluster.', @@ -116,12 +116,12 @@ describe('ElasticsearchVersionMismatchAlert', () => { }); it('should fire actions', async () => { - const alert = new ElasticsearchVersionMismatchAlert(); - const type = alert.getAlertType(); + const rule = new ElasticsearchVersionMismatchRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [ @@ -170,12 +170,12 @@ describe('ElasticsearchVersionMismatchAlert', () => { }, ]; }); - const alert = new ElasticsearchVersionMismatchAlert(); - const type = alert.getAlertType(); + const rule = new ElasticsearchVersionMismatchRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).not.toHaveBeenCalledWith({}); expect(scheduleActions).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts similarity index 93% rename from x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts rename to x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts index d51eb99e3a47d..6a5abcb4975f4 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_rule.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -19,8 +19,8 @@ import { } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; import { - ALERT_ELASTICSEARCH_VERSION_MISMATCH, - LEGACY_ALERT_DETAILS, + RULE_ELASTICSEARCH_VERSION_MISMATCH, + LEGACY_RULE_DETAILS, INDEX_PATTERN_ELASTICSEARCH, } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; @@ -31,11 +31,11 @@ import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { fetchElasticsearchVersions } from '../lib/alerts/fetch_elasticsearch_versions'; -export class ElasticsearchVersionMismatchAlert extends BaseAlert { - constructor(public rawAlert?: SanitizedAlert) { - super(rawAlert, { - id: ALERT_ELASTICSEARCH_VERSION_MISMATCH, - name: LEGACY_ALERT_DETAILS[ALERT_ELASTICSEARCH_VERSION_MISMATCH].label, +export class ElasticsearchVersionMismatchRule extends BaseRule { + constructor(public sanitizedRule?: SanitizedAlert) { + super(sanitizedRule, { + id: RULE_ELASTICSEARCH_VERSION_MISMATCH, + name: LEGACY_RULE_DETAILS[RULE_ELASTICSEARCH_VERSION_MISMATCH].label, interval: '1d', actionVariables: [ { diff --git a/x-pack/plugins/monitoring/server/alerts/index.ts b/x-pack/plugins/monitoring/server/alerts/index.ts index e6398273b46ba..d5c59c802b681 100644 --- a/x-pack/plugins/monitoring/server/alerts/index.ts +++ b/x-pack/plugins/monitoring/server/alerts/index.ts @@ -5,19 +5,19 @@ * 2.0. */ -export { LargeShardSizeAlert } from './large_shard_size_alert'; -export { CCRReadExceptionsAlert } from './ccr_read_exceptions_alert'; -export { BaseAlert } from './base_alert'; -export { CpuUsageAlert } from './cpu_usage_alert'; -export { MissingMonitoringDataAlert } from './missing_monitoring_data_alert'; -export { DiskUsageAlert } from './disk_usage_alert'; -export { ThreadPoolSearchRejectionsAlert } from './thread_pool_search_rejections_alert'; -export { ThreadPoolWriteRejectionsAlert } from './thread_pool_write_rejections_alert'; -export { MemoryUsageAlert } from './memory_usage_alert'; -export { ClusterHealthAlert } from './cluster_health_alert'; -export { LicenseExpirationAlert } from './license_expiration_alert'; -export { NodesChangedAlert } from './nodes_changed_alert'; -export { ElasticsearchVersionMismatchAlert } from './elasticsearch_version_mismatch_alert'; -export { KibanaVersionMismatchAlert } from './kibana_version_mismatch_alert'; -export { LogstashVersionMismatchAlert } from './logstash_version_mismatch_alert'; +export { LargeShardSizeRule } from './large_shard_size_rule'; +export { CCRReadExceptionsRule } from './ccr_read_exceptions_rule'; +export { BaseRule } from './base_rule'; +export { CpuUsageRule } from './cpu_usage_rule'; +export { MissingMonitoringDataRule } from './missing_monitoring_data_rule'; +export { DiskUsageRule } from './disk_usage_rule'; +export { ThreadPoolSearchRejectionsRule } from './thread_pool_search_rejections_rule'; +export { ThreadPoolWriteRejectionsRule } from './thread_pool_write_rejections_rule'; +export { MemoryUsageRule } from './memory_usage_rule'; +export { ClusterHealthRule } from './cluster_health_rule'; +export { LicenseExpirationRule } from './license_expiration_rule'; +export { NodesChangedRule } from './nodes_changed_rule'; +export { ElasticsearchVersionMismatchRule } from './elasticsearch_version_mismatch_rule'; +export { KibanaVersionMismatchRule } from './kibana_version_mismatch_rule'; +export { LogstashVersionMismatchRule } from './logstash_version_mismatch_rule'; export { AlertsFactory } from './alerts_factory'; diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.test.ts similarity index 86% rename from x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.test.ts index 02a8f59aecfbd..01016a7c02ae2 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { KibanaVersionMismatchAlert } from './kibana_version_mismatch_alert'; -import { ALERT_KIBANA_VERSION_MISMATCH } from '../../common/constants'; +import { KibanaVersionMismatchRule } from './kibana_version_mismatch_rule'; +import { RULE_KIBANA_VERSION_MISMATCH } from '../../common/constants'; import { fetchKibanaVersions } from '../lib/alerts/fetch_kibana_versions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; @@ -36,13 +36,13 @@ jest.mock('../static_globals', () => ({ }, })); -describe('KibanaVersionMismatchAlert', () => { +describe('KibanaVersionMismatchRule', () => { it('should have defaults', () => { - const alert = new KibanaVersionMismatchAlert(); - expect(alert.alertOptions.id).toBe(ALERT_KIBANA_VERSION_MISMATCH); - expect(alert.alertOptions.name).toBe('Kibana version mismatch'); - expect(alert.alertOptions.throttle).toBe('1d'); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + const rule = new KibanaVersionMismatchRule(); + expect(rule.ruleOptions.id).toBe(RULE_KIBANA_VERSION_MISMATCH); + expect(rule.ruleOptions.name).toBe('Kibana version mismatch'); + expect(rule.ruleOptions.throttle).toBe('1d'); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'versionList', description: 'The versions of Kibana running in this cluster.', @@ -119,11 +119,11 @@ describe('KibanaVersionMismatchAlert', () => { }); it('should fire actions', async () => { - const alert = new KibanaVersionMismatchAlert(); - const type = alert.getAlertType(); + const rule = new KibanaVersionMismatchRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [ @@ -172,11 +172,11 @@ describe('KibanaVersionMismatchAlert', () => { }, ]; }); - const alert = new KibanaVersionMismatchAlert(); - const type = alert.getAlertType(); + const rule = new KibanaVersionMismatchRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).not.toHaveBeenCalledWith({}); expect(scheduleActions).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts similarity index 94% rename from x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts rename to x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts index 3d6417e8fd64c..90275ea4d23a8 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_rule.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -19,8 +19,8 @@ import { } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; import { - ALERT_KIBANA_VERSION_MISMATCH, - LEGACY_ALERT_DETAILS, + RULE_KIBANA_VERSION_MISMATCH, + LEGACY_RULE_DETAILS, INDEX_PATTERN_KIBANA, } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; @@ -31,11 +31,11 @@ import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { fetchKibanaVersions } from '../lib/alerts/fetch_kibana_versions'; -export class KibanaVersionMismatchAlert extends BaseAlert { - constructor(public rawAlert?: SanitizedAlert) { - super(rawAlert, { - id: ALERT_KIBANA_VERSION_MISMATCH, - name: LEGACY_ALERT_DETAILS[ALERT_KIBANA_VERSION_MISMATCH].label, +export class KibanaVersionMismatchRule extends BaseRule { + constructor(public sanitizedRule?: SanitizedAlert) { + super(sanitizedRule, { + id: RULE_KIBANA_VERSION_MISMATCH, + name: LEGACY_RULE_DETAILS[RULE_KIBANA_VERSION_MISMATCH].label, interval: '1d', actionVariables: [ { diff --git a/x-pack/plugins/monitoring/server/alerts/large_shard_size_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.test.ts similarity index 85% rename from x-pack/plugins/monitoring/server/alerts/large_shard_size_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.test.ts index 18987a24e5524..0b8509c4fa56a 100644 --- a/x-pack/plugins/monitoring/server/alerts/large_shard_size_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.test.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { LargeShardSizeAlert } from './large_shard_size_alert'; -import { ALERT_LARGE_SHARD_SIZE } from '../../common/constants'; +import { LargeShardSizeRule } from './large_shard_size_rule'; +import { RULE_LARGE_SHARD_SIZE } from '../../common/constants'; import { fetchIndexShardSize } from '../lib/alerts/fetch_index_shard_size'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; -type ILargeShardSizeAlertMock = LargeShardSizeAlert & { +type ILargeShardSizeRuleMock = LargeShardSizeRule & { defaultParams: { threshold: number; duration: string; @@ -48,17 +48,17 @@ jest.mock('../static_globals', () => ({ }, })); -describe('LargeShardSizeAlert', () => { +describe('LargeShardSizeRule', () => { it('should have defaults', () => { - const alert = new LargeShardSizeAlert() as ILargeShardSizeAlertMock; - expect(alert.alertOptions.id).toBe(ALERT_LARGE_SHARD_SIZE); - expect(alert.alertOptions.name).toBe('Shard size'); - expect(alert.alertOptions.throttle).toBe('12h'); - expect(alert.alertOptions.defaultParams).toStrictEqual({ + const rule = new LargeShardSizeRule() as ILargeShardSizeRuleMock; + expect(rule.ruleOptions.id).toBe(RULE_LARGE_SHARD_SIZE); + expect(rule.ruleOptions.name).toBe('Shard size'); + expect(rule.ruleOptions.throttle).toBe('12h'); + expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 55, indexPattern: '-.*', }); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'shardIndex', description: 'The index experiencing large average shard size.' }, { name: 'internalShortMessage', @@ -130,11 +130,11 @@ describe('LargeShardSizeAlert', () => { }); it('should fire actions', async () => { - const alert = new LargeShardSizeAlert() as ILargeShardSizeAlertMock; - const type = alert.getAlertType(); + const rule = new LargeShardSizeRule() as ILargeShardSizeRuleMock; + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(scheduleActions).toHaveBeenCalledWith('default', { internalFullMessage: `Large shard size alert is firing for the following index: ${shardIndex}. [View index shard size stats](http://localhost:5601/app/monitoring#/elasticsearch/indices/${shardIndex}?_g=(cluster_uuid:${clusterUuid}))`, @@ -158,11 +158,11 @@ describe('LargeShardSizeAlert', () => { }, ]; }); - const alert = new LargeShardSizeAlert() as ILargeShardSizeAlertMock; - const type = alert.getAlertType(); + const rule = new LargeShardSizeRule() as ILargeShardSizeRuleMock; + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(scheduleActions).toHaveBeenCalledWith('default', { internalFullMessage: `Large shard size alert is firing for the following index: ${shardIndex}. [View index shard size stats](http://localhost:5601/app/monitoring#/elasticsearch/indices/${shardIndex}?_g=(cluster_uuid:${clusterUuid},ccs:testCluster))`, diff --git a/x-pack/plugins/monitoring/server/alerts/large_shard_size_alert.ts b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts similarity index 96% rename from x-pack/plugins/monitoring/server/alerts/large_shard_size_alert.ts rename to x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts index a365e530cbd05..86f96daa3b21d 100644 --- a/x-pack/plugins/monitoring/server/alerts/large_shard_size_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/large_shard_size_rule.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -24,8 +24,8 @@ import { import { AlertInstance } from '../../../alerting/server'; import { INDEX_PATTERN_ELASTICSEARCH, - ALERT_LARGE_SHARD_SIZE, - ALERT_DETAILS, + RULE_LARGE_SHARD_SIZE, + RULE_DETAILS, } from '../../common/constants'; import { fetchIndexShardSize } from '../lib/alerts/fetch_index_shard_size'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; @@ -35,11 +35,11 @@ import { AlertingDefaults, createLink } from './alert_helpers'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { Globals } from '../static_globals'; -export class LargeShardSizeAlert extends BaseAlert { - constructor(public rawAlert?: SanitizedAlert) { - super(rawAlert, { - id: ALERT_LARGE_SHARD_SIZE, - name: ALERT_DETAILS[ALERT_LARGE_SHARD_SIZE].label, +export class LargeShardSizeRule extends BaseRule { + constructor(public sanitizedRule?: SanitizedAlert) { + super(sanitizedRule, { + id: RULE_LARGE_SHARD_SIZE, + name: RULE_DETAILS[RULE_LARGE_SHARD_SIZE].label, throttle: '12h', defaultParams: { indexPattern: '-.*', threshold: 55 }, actionVariables: [ diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts similarity index 87% rename from x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts index 0bb8ba23cd490..b8d00cac5c888 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { LicenseExpirationAlert } from './license_expiration_alert'; -import { ALERT_LICENSE_EXPIRATION } from '../../common/constants'; +import { LicenseExpirationRule } from './license_expiration_rule'; +import { RULE_LICENSE_EXPIRATION } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { fetchLicenses } from '../lib/alerts/fetch_licenses'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; @@ -42,13 +42,13 @@ jest.mock('../static_globals', () => ({ }, })); -describe('LicenseExpirationAlert', () => { +describe('LicenseExpirationRule', () => { it('should have defaults', () => { - const alert = new LicenseExpirationAlert(); - expect(alert.alertOptions.id).toBe(ALERT_LICENSE_EXPIRATION); - expect(alert.alertOptions.name).toBe('License expiration'); - expect(alert.alertOptions.throttle).toBe('1d'); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + const rule = new LicenseExpirationRule(); + expect(rule.ruleOptions.id).toBe(RULE_LICENSE_EXPIRATION); + expect(rule.ruleOptions.name).toBe('License expiration'); + expect(rule.ruleOptions.throttle).toBe('1d'); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'expiredDate', description: 'The date when the license expires.' }, { name: 'clusterName', description: 'The cluster to which the license belong.' }, { @@ -117,11 +117,11 @@ describe('LicenseExpirationAlert', () => { }); it('should fire actions', async () => { - const alert = new LicenseExpirationAlert(); - const type = alert.getAlertType(); + const alert = new LicenseExpirationRule(); + const type = alert.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: alert.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [ @@ -196,12 +196,12 @@ describe('LicenseExpirationAlert', () => { }, ]; }); - const alert = new LicenseExpirationAlert(); - const type = alert.getAlertType(); + const rule = new LicenseExpirationRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).not.toHaveBeenCalledWith({}); expect(scheduleActions).not.toHaveBeenCalled(); @@ -218,12 +218,12 @@ describe('LicenseExpirationAlert', () => { }, ]; }); - const alert = new LicenseExpirationAlert(); - const type = alert.getAlertType(); + const rule = new LicenseExpirationRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState.mock.calls[0][0].alertStates[0].ui.severity).toBe(AlertSeverity.Danger); }); @@ -239,12 +239,12 @@ describe('LicenseExpirationAlert', () => { }, ]; }); - const alert = new LicenseExpirationAlert(); - const type = alert.getAlertType(); + const rule = new LicenseExpirationRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState.mock.calls[0][0].alertStates[0].ui.severity).toBe(AlertSeverity.Warning); }); diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts similarity index 95% rename from x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts rename to x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts index f5a6f2f7c7e1d..67ea8bd57b491 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_rule.ts @@ -7,7 +7,7 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -22,8 +22,8 @@ import { } from '../../common/types/alerts'; import { AlertExecutorOptions, AlertInstance } from '../../../alerting/server'; import { - ALERT_LICENSE_EXPIRATION, - LEGACY_ALERT_DETAILS, + RULE_LICENSE_EXPIRATION, + LEGACY_RULE_DETAILS, INDEX_PATTERN_ELASTICSEARCH, } from '../../common/constants'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; @@ -36,11 +36,11 @@ import { fetchLicenses } from '../lib/alerts/fetch_licenses'; const EXPIRES_DAYS = [60, 30, 14, 7]; -export class LicenseExpirationAlert extends BaseAlert { - constructor(public rawAlert?: SanitizedAlert) { - super(rawAlert, { - id: ALERT_LICENSE_EXPIRATION, - name: LEGACY_ALERT_DETAILS[ALERT_LICENSE_EXPIRATION].label, +export class LicenseExpirationRule extends BaseRule { + constructor(public sanitizedRule?: SanitizedAlert) { + super(sanitizedRule, { + id: RULE_LICENSE_EXPIRATION, + name: LEGACY_RULE_DETAILS[RULE_LICENSE_EXPIRATION].label, interval: '1d', actionVariables: [ { diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.test.ts similarity index 86% rename from x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.test.ts index 9f0096a7e2981..20f64b65ba1f0 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { LogstashVersionMismatchAlert } from './logstash_version_mismatch_alert'; -import { ALERT_LOGSTASH_VERSION_MISMATCH } from '../../common/constants'; +import { LogstashVersionMismatchRule } from './logstash_version_mismatch_rule'; +import { RULE_LOGSTASH_VERSION_MISMATCH } from '../../common/constants'; import { fetchLogstashVersions } from '../lib/alerts/fetch_logstash_versions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; @@ -37,13 +37,13 @@ jest.mock('../static_globals', () => ({ }, })); -describe('LogstashVersionMismatchAlert', () => { +describe('LogstashVersionMismatchRule', () => { it('should have defaults', () => { - const alert = new LogstashVersionMismatchAlert(); - expect(alert.alertOptions.id).toBe(ALERT_LOGSTASH_VERSION_MISMATCH); - expect(alert.alertOptions.name).toBe('Logstash version mismatch'); - expect(alert.alertOptions.throttle).toBe('1d'); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + const rule = new LogstashVersionMismatchRule(); + expect(rule.ruleOptions.id).toBe(RULE_LOGSTASH_VERSION_MISMATCH); + expect(rule.ruleOptions.name).toBe('Logstash version mismatch'); + expect(rule.ruleOptions.throttle).toBe('1d'); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'versionList', description: 'The versions of Logstash running in this cluster.', @@ -117,12 +117,12 @@ describe('LogstashVersionMismatchAlert', () => { }); it('should fire actions', async () => { - const alert = new LogstashVersionMismatchAlert(); - const type = alert.getAlertType(); + const rule = new LogstashVersionMismatchRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [ @@ -171,12 +171,12 @@ describe('LogstashVersionMismatchAlert', () => { }, ]; }); - const alert = new LogstashVersionMismatchAlert(); - const type = alert.getAlertType(); + const rule = new LogstashVersionMismatchRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).not.toHaveBeenCalledWith({}); expect(scheduleActions).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts similarity index 93% rename from x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts rename to x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts index 7ee478b17fff8..0f9ad4dd4b117 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_rule.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -19,8 +19,8 @@ import { } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; import { - ALERT_LOGSTASH_VERSION_MISMATCH, - LEGACY_ALERT_DETAILS, + RULE_LOGSTASH_VERSION_MISMATCH, + LEGACY_RULE_DETAILS, INDEX_PATTERN_LOGSTASH, } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; @@ -31,11 +31,11 @@ import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { fetchLogstashVersions } from '../lib/alerts/fetch_logstash_versions'; -export class LogstashVersionMismatchAlert extends BaseAlert { - constructor(public rawAlert?: SanitizedAlert) { - super(rawAlert, { - id: ALERT_LOGSTASH_VERSION_MISMATCH, - name: LEGACY_ALERT_DETAILS[ALERT_LOGSTASH_VERSION_MISMATCH].label, +export class LogstashVersionMismatchRule extends BaseRule { + constructor(public sanitizedRule?: SanitizedAlert) { + super(sanitizedRule, { + id: RULE_LOGSTASH_VERSION_MISMATCH, + name: LEGACY_RULE_DETAILS[RULE_LOGSTASH_VERSION_MISMATCH].label, interval: '1d', actionVariables: [ { diff --git a/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts similarity index 90% rename from x-pack/plugins/monitoring/server/alerts/memory_usage_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts index 4076eff956ee9..8547f126a02d6 100644 --- a/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { MemoryUsageAlert } from './memory_usage_alert'; -import { ALERT_MEMORY_USAGE } from '../../common/constants'; +import { MemoryUsageRule } from './memory_usage_rule'; +import { RULE_MEMORY_USAGE } from '../../common/constants'; import { fetchMemoryUsageNodeStats } from '../lib/alerts/fetch_memory_usage_node_stats'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; @@ -35,14 +35,14 @@ jest.mock('../static_globals', () => ({ }, })); -describe('MemoryUsageAlert', () => { +describe('MemoryUsageRule', () => { it('should have defaults', () => { - const alert = new MemoryUsageAlert(); - expect(alert.alertOptions.id).toBe(ALERT_MEMORY_USAGE); - expect(alert.alertOptions.name).toBe('Memory Usage (JVM)'); - expect(alert.alertOptions.throttle).toBe('1d'); - expect(alert.alertOptions.defaultParams).toStrictEqual({ threshold: 85, duration: '5m' }); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + const rule = new MemoryUsageRule(); + expect(rule.ruleOptions.id).toBe(RULE_MEMORY_USAGE); + expect(rule.ruleOptions.name).toBe('Memory Usage (JVM)'); + expect(rule.ruleOptions.throttle).toBe('1d'); + expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 85, duration: '5m' }); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'node', description: 'The node reporting high memory usage.' }, { name: 'internalShortMessage', @@ -114,11 +114,11 @@ describe('MemoryUsageAlert', () => { }); it('should fire actions', async () => { - const alert = new MemoryUsageAlert(); - const type = alert.getAlertType(); + const rule = new MemoryUsageRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); const count = 1; expect(replaceState).toHaveBeenCalledWith({ @@ -245,11 +245,11 @@ describe('MemoryUsageAlert', () => { }, ]; }); - const alert = new MemoryUsageAlert(); - const type = alert.getAlertType(); + const rule = new MemoryUsageRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [], @@ -267,11 +267,11 @@ describe('MemoryUsageAlert', () => { }, ]; }); - const alert = new MemoryUsageAlert(); - const type = alert.getAlertType(); + const rule = new MemoryUsageRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); const count = 1; expect(scheduleActions).toHaveBeenCalledWith('default', { diff --git a/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts similarity index 96% rename from x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts rename to x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts index 86357e7b6f0ed..384610e659d47 100644 --- a/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/memory_usage_rule.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import numeral from '@elastic/numeral'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -25,8 +25,8 @@ import { import { AlertInstance } from '../../../alerting/server'; import { INDEX_PATTERN_ELASTICSEARCH, - ALERT_MEMORY_USAGE, - ALERT_DETAILS, + RULE_MEMORY_USAGE, + RULE_DETAILS, } from '../../common/constants'; // @ts-ignore import { ROUNDED_FLOAT } from '../../common/formatting'; @@ -39,11 +39,11 @@ import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { parseDuration } from '../../../alerting/common/parse_duration'; import { Globals } from '../static_globals'; -export class MemoryUsageAlert extends BaseAlert { - constructor(public rawAlert?: SanitizedAlert) { - super(rawAlert, { - id: ALERT_MEMORY_USAGE, - name: ALERT_DETAILS[ALERT_MEMORY_USAGE].label, +export class MemoryUsageRule extends BaseRule { + constructor(public sanitizedRule?: SanitizedAlert) { + super(sanitizedRule, { + id: RULE_MEMORY_USAGE, + name: RULE_DETAILS[RULE_MEMORY_USAGE].label, accessorKey: 'memoryUsage', defaultParams: { threshold: 85, diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.test.ts similarity index 88% rename from x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.test.ts index c6bf853b7787c..88ddc7c04884c 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { MissingMonitoringDataAlert } from './missing_monitoring_data_alert'; -import { ALERT_MISSING_MONITORING_DATA } from '../../common/constants'; +import { MissingMonitoringDataRule } from './missing_monitoring_data_rule'; +import { RULE_MISSING_MONITORING_DATA } from '../../common/constants'; import { fetchMissingMonitoringData } from '../lib/alerts/fetch_missing_monitoring_data'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; @@ -37,14 +37,14 @@ jest.mock('../static_globals', () => ({ }, })); -describe('MissingMonitoringDataAlert', () => { +describe('MissingMonitoringDataRule', () => { it('should have defaults', () => { - const alert = new MissingMonitoringDataAlert(); - expect(alert.alertOptions.id).toBe(ALERT_MISSING_MONITORING_DATA); - expect(alert.alertOptions.name).toBe('Missing monitoring data'); - expect(alert.alertOptions.throttle).toBe('6h'); - expect(alert.alertOptions.defaultParams).toStrictEqual({ limit: '1d', duration: '15m' }); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + const rule = new MissingMonitoringDataRule(); + expect(rule.ruleOptions.id).toBe(RULE_MISSING_MONITORING_DATA); + expect(rule.ruleOptions.name).toBe('Missing monitoring data'); + expect(rule.ruleOptions.throttle).toBe('6h'); + expect(rule.ruleOptions.defaultParams).toStrictEqual({ limit: '1d', duration: '15m' }); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'node', description: 'The node missing monitoring data.' }, { name: 'internalShortMessage', @@ -118,11 +118,11 @@ describe('MissingMonitoringDataAlert', () => { }); it('should fire actions', async () => { - const alert = new MissingMonitoringDataAlert(); - const type = alert.getAlertType(); + const rule = new MissingMonitoringDataRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); const count = 1; expect(replaceState).toHaveBeenCalledWith({ @@ -202,12 +202,12 @@ describe('MissingMonitoringDataAlert', () => { }, ]; }); - const alert = new MissingMonitoringDataAlert(); - const type = alert.getAlertType(); + const rule = new MissingMonitoringDataRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [], @@ -225,12 +225,12 @@ describe('MissingMonitoringDataAlert', () => { }, ]; }); - const alert = new MissingMonitoringDataAlert(); - const type = alert.getAlertType(); + const rule = new MissingMonitoringDataRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); const count = 1; expect(scheduleActions).toHaveBeenCalledWith('default', { diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts similarity index 94% rename from x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts rename to x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts index 77581df2303bb..32e4ff738c71b 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_rule.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -20,11 +20,7 @@ import { AlertNodeState, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; -import { - INDEX_PATTERN, - ALERT_MISSING_MONITORING_DATA, - ALERT_DETAILS, -} from '../../common/constants'; +import { INDEX_PATTERN, RULE_MISSING_MONITORING_DATA, RULE_DETAILS } from '../../common/constants'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { RawAlertInstance, SanitizedAlert } from '../../../alerting/common'; @@ -37,11 +33,11 @@ import { Globals } from '../static_globals'; // Go a bit farther back because we need to detect the difference between seeing the monitoring data versus just not looking far enough back const LIMIT_BUFFER = 3 * 60 * 1000; -export class MissingMonitoringDataAlert extends BaseAlert { - constructor(public rawAlert?: SanitizedAlert) { - super(rawAlert, { - id: ALERT_MISSING_MONITORING_DATA, - name: ALERT_DETAILS[ALERT_MISSING_MONITORING_DATA].label, +export class MissingMonitoringDataRule extends BaseRule { + constructor(public sanitizedRule?: SanitizedAlert) { + super(sanitizedRule, { + id: RULE_MISSING_MONITORING_DATA, + name: RULE_DETAILS[RULE_MISSING_MONITORING_DATA].label, accessorKey: 'gapDuration', fetchClustersRange: LIMIT_BUFFER, defaultParams: { diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.test.ts similarity index 92% rename from x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.test.ts index d628c1c30a7e1..199e50c80ef4c 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { NodesChangedAlert } from './nodes_changed_alert'; -import { ALERT_NODES_CHANGED } from '../../common/constants'; +import { NodesChangedRule } from './nodes_changed_rule'; +import { RULE_NODES_CHANGED } from '../../common/constants'; import { fetchNodesFromClusterStats } from '../lib/alerts/fetch_nodes_from_cluster_stats'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; @@ -44,11 +44,11 @@ jest.mock('../static_globals', () => ({ describe('NodesChangedAlert', () => { it('should have defaults', () => { - const alert = new NodesChangedAlert(); - expect(alert.alertOptions.id).toBe(ALERT_NODES_CHANGED); - expect(alert.alertOptions.name).toBe('Nodes changed'); - expect(alert.alertOptions.throttle).toBe('1d'); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + const rule = new NodesChangedRule(); + expect(rule.ruleOptions.id).toBe(RULE_NODES_CHANGED); + expect(rule.ruleOptions.name).toBe('Nodes changed'); + expect(rule.ruleOptions.throttle).toBe('1d'); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'added', description: 'The list of nodes added to the cluster.' }, { name: 'removed', description: 'The list of nodes removed from the cluster.' }, { name: 'restarted', description: 'The list of nodes restarted in the cluster.' }, @@ -168,12 +168,12 @@ describe('NodesChangedAlert', () => { (fetchNodesFromClusterStats as jest.Mock).mockImplementation(() => { return nodesChanged; }); - const alert = new NodesChangedAlert(); - const type = alert.getAlertType(); + const rule = new NodesChangedRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [ @@ -231,12 +231,12 @@ describe('NodesChangedAlert', () => { (fetchNodesFromClusterStats as jest.Mock).mockImplementation(() => { return nodesAddedChangedRemoved; }); - const alert = new NodesChangedAlert(); - const type = alert.getAlertType(); + const rule = new NodesChangedRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [ @@ -325,12 +325,12 @@ describe('NodesChangedAlert', () => { }, ]; }); - const alert = new NodesChangedAlert(); - const type = alert.getAlertType(); + const rule = new NodesChangedRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).not.toHaveBeenCalledWith({}); expect(scheduleActions).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts similarity index 96% rename from x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts rename to x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts index b26008ff3860d..90bd70f32c8cb 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_rule.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -21,8 +21,8 @@ import { } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerting/server'; import { - ALERT_NODES_CHANGED, - LEGACY_ALERT_DETAILS, + RULE_NODES_CHANGED, + LEGACY_RULE_DETAILS, INDEX_PATTERN_ELASTICSEARCH, } from '../../common/constants'; import { AlertingDefaults } from './alert_helpers'; @@ -63,11 +63,11 @@ function getNodeStates(nodes: AlertClusterStatsNodes): AlertNodesChangedStates { }; } -export class NodesChangedAlert extends BaseAlert { - constructor(public rawAlert?: SanitizedAlert) { - super(rawAlert, { - id: ALERT_NODES_CHANGED, - name: LEGACY_ALERT_DETAILS[ALERT_NODES_CHANGED].label, +export class NodesChangedRule extends BaseRule { + constructor(public sanitizedRule?: SanitizedAlert) { + super(sanitizedRule, { + id: RULE_NODES_CHANGED, + name: LEGACY_RULE_DETAILS[RULE_NODES_CHANGED].label, actionVariables: [ { name: 'added', diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts similarity index 97% rename from x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts rename to x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts index 3b11d3464a215..c478b2f687c02 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_rule_base.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ElasticsearchClient } from 'kibana/server'; -import { BaseAlert } from './base_alert'; +import { BaseRule } from './base_rule'; import { AlertData, AlertCluster, @@ -32,7 +32,7 @@ import { Globals } from '../static_globals'; type ActionVariables = Array<{ name: string; description: string }>; -export class ThreadPoolRejectionsAlertBase extends BaseAlert { +export class ThreadPoolRejectionsRuleBase extends BaseRule { protected static createActionVariables(type: string) { return [ { @@ -50,13 +50,13 @@ export class ThreadPoolRejectionsAlertBase extends BaseAlert { } constructor( - rawAlert: Alert | undefined = undefined, + sanitizedRule: Alert | undefined = undefined, public readonly id: string, public readonly threadPoolType: string, public readonly name: string, public readonly actionVariables: ActionVariables ) { - super(rawAlert, { + super(sanitizedRule, { id, name, defaultParams: { diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.ts deleted file mode 100644 index 38b99fb2c6ead..0000000000000 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.ts +++ /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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ThreadPoolRejectionsAlertBase } from './thread_pool_rejections_alert_base'; -import { ALERT_THREAD_POOL_SEARCH_REJECTIONS, ALERT_DETAILS } from '../../common/constants'; -import { Alert } from '../../../alerting/common'; - -export class ThreadPoolSearchRejectionsAlert extends ThreadPoolRejectionsAlertBase { - private static TYPE = ALERT_THREAD_POOL_SEARCH_REJECTIONS; - private static THREAD_POOL_TYPE = 'search'; - private static readonly LABEL = ALERT_DETAILS[ALERT_THREAD_POOL_SEARCH_REJECTIONS].label; - constructor(rawAlert?: Alert) { - super( - rawAlert, - ThreadPoolSearchRejectionsAlert.TYPE, - ThreadPoolSearchRejectionsAlert.THREAD_POOL_TYPE, - ThreadPoolSearchRejectionsAlert.LABEL, - ThreadPoolRejectionsAlertBase.createActionVariables( - ThreadPoolSearchRejectionsAlert.THREAD_POOL_TYPE - ) - ); - } -} diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.test.ts similarity index 90% rename from x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.test.ts index 78f3e937016a4..351980d3f385d 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { ThreadPoolSearchRejectionsAlert } from './thread_pool_search_rejections_alert'; -import { ALERT_THREAD_POOL_SEARCH_REJECTIONS } from '../../common/constants'; +import { ThreadPoolSearchRejectionsRule } from './thread_pool_search_rejections_rule'; +import { RULE_THREAD_POOL_SEARCH_REJECTIONS } from '../../common/constants'; import { fetchThreadPoolRejectionStats } from '../lib/alerts/fetch_thread_pool_rejections_stats'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; @@ -37,14 +37,14 @@ jest.mock('../static_globals', () => ({ }, })); -describe('ThreadpoolSearchRejectionsAlert', () => { +describe('ThreadpoolSearchRejectionsRule', () => { it('should have defaults', () => { - const alert = new ThreadPoolSearchRejectionsAlert(); - expect(alert.alertOptions.id).toBe(ALERT_THREAD_POOL_SEARCH_REJECTIONS); - expect(alert.alertOptions.name).toBe('Thread pool search rejections'); - expect(alert.alertOptions.throttle).toBe('1d'); - expect(alert.alertOptions.defaultParams).toStrictEqual({ threshold: 300, duration: '5m' }); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + const rule = new ThreadPoolSearchRejectionsRule(); + expect(rule.ruleOptions.id).toBe(RULE_THREAD_POOL_SEARCH_REJECTIONS); + expect(rule.ruleOptions.name).toBe('Thread pool search rejections'); + expect(rule.ruleOptions.throttle).toBe('1d'); + expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 300, duration: '5m' }); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'node', description: 'The node reporting high thread pool search rejections.' }, { name: 'internalShortMessage', @@ -120,11 +120,11 @@ describe('ThreadpoolSearchRejectionsAlert', () => { }); it('should fire actions', async () => { - const alert = new ThreadPoolSearchRejectionsAlert(); - const type = alert.getAlertType(); + const rule = new ThreadPoolSearchRejectionsRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [ @@ -251,12 +251,12 @@ describe('ThreadpoolSearchRejectionsAlert', () => { }, ]; }); - const alert = new ThreadPoolSearchRejectionsAlert(); - const type = alert.getAlertType(); + const rule = new ThreadPoolSearchRejectionsRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [], @@ -274,12 +274,12 @@ describe('ThreadpoolSearchRejectionsAlert', () => { }, ]; }); - const alert = new ThreadPoolSearchRejectionsAlert(); - const type = alert.getAlertType(); + const rule = new ThreadPoolSearchRejectionsRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); const count = 1; expect(scheduleActions).toHaveBeenCalledWith('default', { diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.ts new file mode 100644 index 0000000000000..509c50cda70bb --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_search_rejections_rule.ts @@ -0,0 +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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ThreadPoolRejectionsRuleBase } from './thread_pool_rejections_rule_base'; +import { RULE_THREAD_POOL_SEARCH_REJECTIONS, RULE_DETAILS } from '../../common/constants'; +import { Alert } from '../../../alerting/common'; + +export class ThreadPoolSearchRejectionsRule extends ThreadPoolRejectionsRuleBase { + private static TYPE = RULE_THREAD_POOL_SEARCH_REJECTIONS; + private static THREAD_POOL_TYPE = 'search'; + private static readonly LABEL = RULE_DETAILS[RULE_THREAD_POOL_SEARCH_REJECTIONS].label; + constructor(sanitizedRule?: Alert) { + super( + sanitizedRule, + ThreadPoolSearchRejectionsRule.TYPE, + ThreadPoolSearchRejectionsRule.THREAD_POOL_TYPE, + ThreadPoolSearchRejectionsRule.LABEL, + ThreadPoolRejectionsRuleBase.createActionVariables( + ThreadPoolSearchRejectionsRule.THREAD_POOL_TYPE + ) + ); + } +} diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.ts deleted file mode 100644 index 6df8411e1ec86..0000000000000 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.ts +++ /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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ThreadPoolRejectionsAlertBase } from './thread_pool_rejections_alert_base'; -import { ALERT_THREAD_POOL_WRITE_REJECTIONS, ALERT_DETAILS } from '../../common/constants'; -import { Alert } from '../../../alerting/common'; - -export class ThreadPoolWriteRejectionsAlert extends ThreadPoolRejectionsAlertBase { - private static TYPE = ALERT_THREAD_POOL_WRITE_REJECTIONS; - private static THREAD_POOL_TYPE = 'write'; - private static readonly LABEL = ALERT_DETAILS[ALERT_THREAD_POOL_WRITE_REJECTIONS].label; - constructor(rawAlert?: Alert) { - super( - rawAlert, - ThreadPoolWriteRejectionsAlert.TYPE, - ThreadPoolWriteRejectionsAlert.THREAD_POOL_TYPE, - ThreadPoolWriteRejectionsAlert.LABEL, - ThreadPoolRejectionsAlertBase.createActionVariables( - ThreadPoolWriteRejectionsAlert.THREAD_POOL_TYPE - ) - ); - } -} diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.test.ts similarity index 90% rename from x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.test.ts rename to x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.test.ts index 83ba6fc7532a3..79896d11da2c3 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { ThreadPoolWriteRejectionsAlert } from './thread_pool_write_rejections_alert'; -import { ALERT_THREAD_POOL_WRITE_REJECTIONS } from '../../common/constants'; +import { ThreadPoolWriteRejectionsRule } from './thread_pool_write_rejections_rule'; +import { RULE_THREAD_POOL_WRITE_REJECTIONS } from '../../common/constants'; import { fetchThreadPoolRejectionStats } from '../lib/alerts/fetch_thread_pool_rejections_stats'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; @@ -39,12 +39,12 @@ jest.mock('../static_globals', () => ({ describe('ThreadpoolWriteRejectionsAlert', () => { it('should have defaults', () => { - const alert = new ThreadPoolWriteRejectionsAlert(); - expect(alert.alertOptions.id).toBe(ALERT_THREAD_POOL_WRITE_REJECTIONS); - expect(alert.alertOptions.name).toBe(`Thread pool write rejections`); - expect(alert.alertOptions.throttle).toBe('1d'); - expect(alert.alertOptions.defaultParams).toStrictEqual({ threshold: 300, duration: '5m' }); - expect(alert.alertOptions.actionVariables).toStrictEqual([ + const rule = new ThreadPoolWriteRejectionsRule(); + expect(rule.ruleOptions.id).toBe(RULE_THREAD_POOL_WRITE_REJECTIONS); + expect(rule.ruleOptions.name).toBe(`Thread pool write rejections`); + expect(rule.ruleOptions.throttle).toBe('1d'); + expect(rule.ruleOptions.defaultParams).toStrictEqual({ threshold: 300, duration: '5m' }); + expect(rule.ruleOptions.actionVariables).toStrictEqual([ { name: 'node', description: 'The node reporting high thread pool write rejections.' }, { name: 'internalShortMessage', @@ -120,11 +120,11 @@ describe('ThreadpoolWriteRejectionsAlert', () => { }); it('should fire actions', async () => { - const alert = new ThreadPoolWriteRejectionsAlert(); - const type = alert.getAlertType(); + const rule = new ThreadPoolWriteRejectionsRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [ @@ -251,12 +251,12 @@ describe('ThreadpoolWriteRejectionsAlert', () => { }, ]; }); - const alert = new ThreadPoolWriteRejectionsAlert(); - const type = alert.getAlertType(); + const rule = new ThreadPoolWriteRejectionsRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); expect(replaceState).toHaveBeenCalledWith({ alertStates: [], @@ -274,12 +274,12 @@ describe('ThreadpoolWriteRejectionsAlert', () => { }, ]; }); - const alert = new ThreadPoolWriteRejectionsAlert(); - const type = alert.getAlertType(); + const rule = new ThreadPoolWriteRejectionsRule(); + const type = rule.getRuleType(); await type.executor({ ...executorOptions, // @ts-ignore - params: alert.alertOptions.defaultParams, + params: rule.ruleOptions.defaultParams, } as any); const count = 1; expect(scheduleActions).toHaveBeenCalledWith('default', { diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.ts new file mode 100644 index 0000000000000..ca7e1dc67d5b1 --- /dev/null +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_write_rejections_rule.ts @@ -0,0 +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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ThreadPoolRejectionsRuleBase } from './thread_pool_rejections_rule_base'; +import { RULE_THREAD_POOL_WRITE_REJECTIONS, RULE_DETAILS } from '../../common/constants'; +import { Alert } from '../../../alerting/common'; + +export class ThreadPoolWriteRejectionsRule extends ThreadPoolRejectionsRuleBase { + private static TYPE = RULE_THREAD_POOL_WRITE_REJECTIONS; + private static THREAD_POOL_TYPE = 'write'; + private static readonly LABEL = RULE_DETAILS[RULE_THREAD_POOL_WRITE_REJECTIONS].label; + constructor(sanitizedRule?: Alert) { + super( + sanitizedRule, + ThreadPoolWriteRejectionsRule.TYPE, + ThreadPoolWriteRejectionsRule.THREAD_POOL_TYPE, + ThreadPoolWriteRejectionsRule.LABEL, + ThreadPoolRejectionsRuleBase.createActionVariables( + ThreadPoolWriteRejectionsRule.THREAD_POOL_TYPE + ) + ); + } +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts index 3662b8252f0a5..6e6f9469164e9 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.test.ts @@ -9,10 +9,10 @@ import { fetchStatus } from './fetch_status'; import { AlertUiState, AlertState } from '../../../common/types/alerts'; import { AlertSeverity } from '../../../common/enums'; import { - ALERT_CPU_USAGE, - ALERT_CLUSTER_HEALTH, - ALERT_DISK_USAGE, - ALERT_MISSING_MONITORING_DATA, + RULE_CPU_USAGE, + RULE_CLUSTER_HEALTH, + RULE_DISK_USAGE, + RULE_MISSING_MONITORING_DATA, } from '../../../common/constants'; jest.mock('../../static_globals', () => ({ @@ -31,7 +31,7 @@ jest.mock('../../static_globals', () => ({ })); describe('fetchStatus', () => { - const alertType = ALERT_CPU_USAGE; + const alertType = RULE_CPU_USAGE; const alertTypes = [alertType]; const defaultClusterState = { clusterUuid: 'abc', @@ -81,11 +81,11 @@ describe('fetchStatus', () => { expect(status).toEqual({ monitoring_alert_cpu_usage: [ { - rawAlert: { id: 1 }, + sanitizedRule: { id: 1 }, states: [], }, { - rawAlert: { id: 2 }, + sanitizedRule: { id: 2 }, states: [], }, ], @@ -149,11 +149,7 @@ describe('fetchStatus', () => { isEnabled: true, })), }; - await fetchStatus( - rulesClient as any, - [ALERT_CLUSTER_HEALTH], - [defaultClusterState.clusterUuid] - ); + await fetchStatus(rulesClient as any, [RULE_CLUSTER_HEALTH], [defaultClusterState.clusterUuid]); expect(customLicenseService.getWatcherFeature).toHaveBeenCalled(); }); @@ -187,13 +183,13 @@ describe('fetchStatus', () => { }; const status = await fetchStatus( customRulesClient as any, - [ALERT_CPU_USAGE, ALERT_DISK_USAGE, ALERT_MISSING_MONITORING_DATA], + [RULE_CPU_USAGE, RULE_DISK_USAGE, RULE_MISSING_MONITORING_DATA], [defaultClusterState.clusterUuid] ); expect(Object.keys(status)).toEqual([ - ALERT_CPU_USAGE, - ALERT_DISK_USAGE, - ALERT_MISSING_MONITORING_DATA, + RULE_CPU_USAGE, + RULE_DISK_USAGE, + RULE_MISSING_MONITORING_DATA, ]); }); }); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts index 6355cb3eb26b9..257f0cec48600 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -9,7 +9,7 @@ import { AlertInstanceState } from '../../../common/types/alerts'; import { RulesClient } from '../../../../alerting/server'; import { AlertsFactory } from '../../alerts'; import { CommonAlertState, CommonAlertFilter, RulesByType } from '../../../common/types/alerts'; -import { ALERTS } from '../../../common/constants'; +import { RULES } from '../../../common/constants'; export async function fetchStatus( rulesClient: RulesClient, @@ -18,7 +18,7 @@ export async function fetchStatus( filters: CommonAlertFilter[] = [] ): Promise { const rulesByType = await Promise.all( - (alertTypes || ALERTS).map(async (type) => AlertsFactory.getByType(type, rulesClient)) + (alertTypes || RULES).map(async (type) => AlertsFactory.getByType(type, rulesClient)) ); if (!rulesByType.length) return {}; @@ -26,13 +26,13 @@ export async function fetchStatus( const rulesWithStates = await Promise.all( rulesFlattened.map(async (rule) => { // we should have a different class to distinguish between "alerts" where the rule exists - // and a BaseAlert created without an existing rule for better typing so we don't need to check here - if (!rule.rawAlert) { - throw new Error('alert missing rawAlert'); + // and a BaseRule created without an existing rule for better typing so we don't need to check here + if (!rule.sanitizedRule) { + throw new Error('alert missing sanitizedRule'); } const id = rule.getId(); if (!id) { - throw new Error('alert missing id'); + throw new Error('rule missing id'); } // Now that we have the id, we can get the state @@ -63,10 +63,10 @@ export async function fetchStatus( [] ); - const type = rule.alertOptions.id; + const type = rule.ruleOptions.id; const result = { states: alertStates, - rawAlert: rule.rawAlert, + sanitizedRule: rule.sanitizedRule, }; return { type, result }; }) diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 6bfa052a6fe8f..1d6af9f080dc0 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -24,9 +24,9 @@ import { import { get, has } from 'lodash'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; import { - ALERTS, KIBANA_MONITORING_LOGGING_TAG, KIBANA_STATS_TYPE_MONITORING, + RULES, LOGGING_TAG, SAVED_OBJECT_TELEMETRY, } from '../common/constants'; @@ -125,7 +125,7 @@ export class MonitoringPlugin const alerts = AlertsFactory.getAll(); for (const alert of alerts) { - plugins.alerting?.registerType(alert.getAlertType()); + plugins.alerting?.registerType(alert.getRuleType()); } const config = createConfig(this.initializerContext.config.get>()); @@ -275,7 +275,7 @@ export class MonitoringPlugin app: ['monitoring', 'kibana'], catalogue: ['monitoring'], privileges: null, - alerting: ALERTS, + alerting: RULES, reserved: { description: i18n.translate('xpack.monitoring.feature.reserved.description', { defaultMessage: 'To grant users access, you should also assign the monitoring_user role.', @@ -292,10 +292,10 @@ export class MonitoringPlugin }, alerting: { rule: { - all: ALERTS, + all: RULES, }, alert: { - all: ALERTS, + all: RULES, }, }, ui: [], From c21272cc5bc25da14fd3ab3c62627725b74a800f Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 10 Aug 2021 16:02:45 -0400 Subject: [PATCH 045/104] Sharing saved objects developer guide (#107099) --- dev_docs/key_concepts/saved_objects.mdx | 14 + dev_docs/tutorials/saved_objects.mdx | 6 +- docs/api/saved-objects/resolve.asciidoc | 5 +- .../sharing-saved-objects-dev-flowchart.png | Bin 0 -> 176397 bytes ...aved-objects-faq-changing-object-ids-1.png | Bin 0 -> 92840 bytes ...aved-objects-faq-changing-object-ids-2.png | Bin 0 -> 137194 bytes ...g-saved-objects-faq-resolve-outcomes-1.png | Bin 0 -> 38971 bytes ...g-saved-objects-faq-resolve-outcomes-2.png | Bin 0 -> 43869 bytes ...g-saved-objects-faq-resolve-outcomes-3.png | Bin 0 -> 52388 bytes .../images/sharing-saved-objects-overview.png | Bin 0 -> 67805 bytes .../images/sharing-saved-objects-q2.png | Bin 0 -> 46973 bytes .../images/sharing-saved-objects-step-1.png | Bin 0 -> 73929 bytes .../images/sharing-saved-objects-step-3.png | Bin 0 -> 127926 bytes .../images/sharing-saved-objects-step-4.png | Bin 0 -> 39126 bytes .../images/sharing-saved-objects-step-5.png | Bin 0 -> 65264 bytes docs/developer/advanced/index.asciidoc | 5 +- .../advanced/sharing-saved-objects.asciidoc | 472 ++++++++++++++++++ .../core/saved-objects-service.asciidoc | 5 +- ...olvedsimplesavedobject.alias_target_id.md} | 6 +- ...n-core-public.resolvedsimplesavedobject.md | 4 +- ...resolvedsimplesavedobject.saved_object.md} | 6 +- ...objectsresolveresponse.alias_target_id.md} | 6 +- ...core-public.savedobjectsresolveresponse.md | 2 +- ...objectsresolveresponse.alias_target_id.md} | 6 +- ...core-server.savedobjectsresolveresponse.md | 2 +- src/core/public/public.api.md | 6 +- .../saved_objects_client.test.ts | 10 +- .../saved_objects/saved_objects_client.ts | 10 +- src/core/public/saved_objects/types.ts | 4 +- .../service/lib/repository.test.js | 4 +- .../saved_objects/service/lib/repository.ts | 4 +- .../service/saved_objects_client.ts | 2 +- src/core/server/server.api.md | 2 +- src/plugins/spaces_oss/public/api.ts | 2 +- .../common/suites/resolve.ts | 4 +- 35 files changed, 545 insertions(+), 42 deletions(-) create mode 100644 docs/developer/advanced/images/sharing-saved-objects-dev-flowchart.png create mode 100644 docs/developer/advanced/images/sharing-saved-objects-faq-changing-object-ids-1.png create mode 100644 docs/developer/advanced/images/sharing-saved-objects-faq-changing-object-ids-2.png create mode 100644 docs/developer/advanced/images/sharing-saved-objects-faq-resolve-outcomes-1.png create mode 100644 docs/developer/advanced/images/sharing-saved-objects-faq-resolve-outcomes-2.png create mode 100644 docs/developer/advanced/images/sharing-saved-objects-faq-resolve-outcomes-3.png create mode 100644 docs/developer/advanced/images/sharing-saved-objects-overview.png create mode 100644 docs/developer/advanced/images/sharing-saved-objects-q2.png create mode 100644 docs/developer/advanced/images/sharing-saved-objects-step-1.png create mode 100644 docs/developer/advanced/images/sharing-saved-objects-step-3.png create mode 100644 docs/developer/advanced/images/sharing-saved-objects-step-4.png create mode 100644 docs/developer/advanced/images/sharing-saved-objects-step-5.png create mode 100644 docs/developer/advanced/sharing-saved-objects.asciidoc rename docs/development/core/public/{kibana-plugin-core-public.resolvedsimplesavedobject.aliastargetid.md => kibana-plugin-core-public.resolvedsimplesavedobject.alias_target_id.md} (59%) rename docs/development/core/public/{kibana-plugin-core-public.resolvedsimplesavedobject.savedobject.md => kibana-plugin-core-public.resolvedsimplesavedobject.saved_object.md} (55%) rename docs/development/core/public/{kibana-plugin-core-public.savedobjectsresolveresponse.aliastargetid.md => kibana-plugin-core-public.savedobjectsresolveresponse.alias_target_id.md} (62%) rename docs/development/core/server/{kibana-plugin-core-server.savedobjectsresolveresponse.aliastargetid.md => kibana-plugin-core-server.savedobjectsresolveresponse.alias_target_id.md} (62%) diff --git a/dev_docs/key_concepts/saved_objects.mdx b/dev_docs/key_concepts/saved_objects.mdx index d89342765c8f1..bef92bf028697 100644 --- a/dev_docs/key_concepts/saved_objects.mdx +++ b/dev_docs/key_concepts/saved_objects.mdx @@ -72,3 +72,17 @@ Sometimes Saved Objects end up persisted inside another Saved Object. We call th issues with edits propagating - since an entity can only exist in a single place. Note that from the end user stand point, we don’t use these terms “by reference” and “by value”. +## Sharing Saved Objects + +Starting in Kibana 7.12, saved objects can be shared to multiple spaces. The "space behavior" is determined for each object type depending +on how it is registered. + +If you are adding a **new** object type, when you register it: + +1. Use `namespaceType: 'multiple-isolated'` to make these objects exist in exactly one space +2. Use `namespaceType: 'multiple'` to make these objects exist in one *or more* spaces +3. Use `namespaceType: 'agnostic'` if you want these objects to always exist in all spaces + +If you have an **existing** "legacy" object type that is not shareable (using `namespaceType: 'single'`), see the [legacy developer guide +for Sharing Saved Objects](https://www.elastic.co/guide/en/kibana/master/sharing-saved-objects.html) for details on steps you need to take +to make sure this is converted to `namespaceType: 'multiple-isolated'` or `namespaceType: 'multiple'` in the 8.0 release. diff --git a/dev_docs/tutorials/saved_objects.mdx b/dev_docs/tutorials/saved_objects.mdx index 644afadb268cb..35efbb97a0a03 100644 --- a/dev_docs/tutorials/saved_objects.mdx +++ b/dev_docs/tutorials/saved_objects.mdx @@ -19,7 +19,7 @@ import { SavedObjectsType } from 'src/core/server'; export const dashboardVisualization: SavedObjectsType = { name: 'dashboard_visualization', [1] hidden: false, - namespaceType: 'single', + namespaceType: 'multiple-isolated', [2] mappings: { dynamic: false, properties: { @@ -41,6 +41,10 @@ export const dashboardVisualization: SavedObjectsType = { [1] Since the name of a Saved Object type forms part of the url path for the public Saved Objects HTTP API, these should follow our API URL path convention and always be written as snake case. +[2] This field determines "space behavior" -- whether these objects can exist in one space, multiple spaces, or all spaces. This value means +that objects of this type can only exist in a single space. See + for more information. + **src/plugins/my_plugin/server/saved_objects/index.ts** ```ts diff --git a/docs/api/saved-objects/resolve.asciidoc b/docs/api/saved-objects/resolve.asciidoc index f2bf31bc5d9e4..abfad6a0a8150 100644 --- a/docs/api/saved-objects/resolve.asciidoc +++ b/docs/api/saved-objects/resolve.asciidoc @@ -70,6 +70,8 @@ The `outcome` field may be any of the following: * `"aliasMatch"` -- One document with a legacy URL alias matched the given ID; in this case the `saved_object.id` field is different than the given ID. * `"conflict"` -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the `saved_object` object is the exact match, and the `saved_object.id` field is the same as the given ID. +If the outcome is `"aliasMatch"` or `"conflict"`, the response will also include an `alias_target_id` field. This means that an alias was found for another object, and it describes that other object's ID. + Retrieve a dashboard object in the `testspace` by ID: [source,sh] @@ -125,6 +127,7 @@ The API returns the following: "dashboard": "7.0.0" } }, - "outcome": "conflict" + "outcome": "conflict", + "alias_target_id": "05becb88-e214-439a-a2ac-15fc783b5d01" } -------------------------------------------------- diff --git a/docs/developer/advanced/images/sharing-saved-objects-dev-flowchart.png b/docs/developer/advanced/images/sharing-saved-objects-dev-flowchart.png new file mode 100644 index 0000000000000000000000000000000000000000..bc829059988db1fc4393b94430e272fc777a8e8f GIT binary patch literal 176397 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

)@J;N6BrJ#6j?PitHL= zsL`5DbZX)!siIJZbl^Ksv(2%CtuJ%?i|9x40NJqWnY>)nVw>)Av1!vYpIX%INA9_@ zxy*(yTV4@=>dPxzcOF0|t$OV_7vxWhgC3t(kOzvX5=ZuG-9Vyfa1^#3u6in=Xl!t0 z0yEkGpJFtfF9`b9V%XturG~@Y+%jgZ(RLn7XRS@s>5s<+{g22i@th9>KSwQ#&C92* z6??&gBJgH1Zl7BN?{+f(vkQ8JNO}g!5(kg|-pCwp?`Um|NE0Nw$68n`2L{H6~XrpN*Dc9P5mI$g)k} zV@SBLx?eb;wXya{WwU_|^PeF;B>i%pj*HF(*Yh=@>*4Ribe+W_A3rt9CD5;$>cI{F z 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�ULH6h< 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�H}sr@FA#7#f_@i4kjPx~v-d2f60`qgN$>L) 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 literal 0 HcmV?d00001 diff --git a/docs/developer/advanced/images/sharing-saved-objects-faq-changing-object-ids-2.png b/docs/developer/advanced/images/sharing-saved-objects-faq-changing-object-ids-2.png new file mode 100644 index 0000000000000000000000000000000000000000..50160d7a7b0ff3e9e77056eae15734de3e4435e7 GIT binary patch literal 137194 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 z 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 f`1g4Rp0+P-UdWRR92(ap*SygaCbVy7vJI06<=` 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%0DZ(wc1^2U$R~3{MP(y~gL-;|1`?~9Glyl#$ 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 CTSxu? literal 0 HcmV?d00001 diff --git a/docs/developer/advanced/images/sharing-saved-objects-faq-resolve-outcomes-2.png b/docs/developer/advanced/images/sharing-saved-objects-faq-resolve-outcomes-2.png new file mode 100644 index 0000000000000000000000000000000000000000..7c9dcb5fb9b05ef44d0d90502b3fb992a9a9b42c GIT binary patch literal 43869 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

F$VXz2g$AW~+JF7f3Gkg@g85G>Z;r z8{=)i7n4n2@0msm%PUwa5k?=lL>B1>v^W!eN!Q)q#yCryBE&R+yx3Qii;9*t-C;Jb zf|Ww5@yn>IInk#Zu8q;eve`l%eakL)rQBds)dyCm zWyGdH*@T%H)}>M_>?O;J@wNKW(le-aK1zjbAjR~|QuPmE*YQStPxi{uC0|f!*cZlC zTP7|qJASUxHRG1-X1(d9Byne~)!OZmJY^N<}j0LjR8(~_o) z*sU_oC4DLXL=@50jmzcIenD1p+f6J(=44sx@5W1Rg{5*tQf7w*{7*MaDfQ;lhD(N5 za>j!M!O{*+Omk|HAfd4b5uef-Q92o`h`iIB8d!6^@31{)5xskNb7q?nOg-RKPu8z_IsM2wTG)JX?~cTaBGG%1^X z1~8h3!qsb6+*$~UTyD#6&+k8!AxQbP$M9%p59$ntA2 zNDI}xyo~@|+ve%Av@V~x3KWMe6Y$Jn%1wANNxdh4Npjn$I=d~{=!m@JG z+Rk5;Ydr2iT$OxTG==gZ0%CTfX6I5F@qLL2Aul zIg_Kw%mj`fWsn4yIQz8jHHD?XbowUz#BFT46&Ltgb#Z!0oY}QkR*ym6ST>vdh7GE7a#8Ag_yvi5W(P#{!VsRjaxR3tldz6n@Q- z%0-0@3MW^YkvLuv&bI7sSyC-uJI|T->=1f#ezd3<-goi|I_bQcum_)|jYKeYYgBnX z{X`jJ2aGMxv?)uZvAVg>805DLzNB^c4G8p*Dds!4EDX28aNI7PdPW6;Ld#XgVbsPe zXGFbnR$US-VI#p?{Lveyq+;Ih-@Qx%kT_$DWKtGdK7C$)Uu&Tr@8W1?BSEvUmEE!# zj#EGK2`bi?DD|?hjL+v<`}{Xx zA&(P$vjx#Fo4?~wC*Q%?#$wA63r}zHv6#kKtB!ik-CR{G01JZ)4g#<`$f*S?n7`Fp zh|@}SLWqQeK$BwfgJ+OQrcZL#qV8@jeN6(vAmR{95I7}*(~SI5lU8_0egopU%7aKI z7x5-iWZ#e95X?_1M#8TSN0XM@7@V-8^FN|#-G&9Gsn3zapiX&cR`O(5C2?omD6Th_ zwZyOWgR|f5pVXx1Jn?28^A2Od!*UUfYv8@ReJ;I8-k0VsrP(IOy@OT}OR_0mk!mJn zz2-tL3`@DWUH#BXT=o5hlOF>TKOR2AVOeshNU6wOna0;#P%?kO=$6kYt`sk|4uK$^ zsux4Rrm4onynd;D^_`Y&z3Ce6bmaI=J|~WT3f_UKm{YDj3NKR+VmHrh!InEUuV|ld z1dC_W`0G`Zyaas+;>Fc}Gl@4uZ`O_`R?yU>+3?BPU{l7##+K4-E2?AvOBSK8LZwa# zQS|bf4w$a+n=~6=#Dp|e&Kvj+yJftW1u#Xf`)*wB8oVZnAE4$98+6ulFI*+#gffC* z+x@U76{FMXTK9ZY<0IEDU(A{Pe`q?(u&BPb3&SwLP|_gqqf5F$x;q7=yBnmtTR=J` zr6r`hyHiq1O1iuLXWr|2zpCi4IcM+vtaYzt!0W^1y3d*U;kEXc^?0eRHcKH<$K#0v znDmwE1x#K#40XoYohOx}6Hd4S$&;)aNxE1Q%czLv?z$Tp)Un*+?@aEX2YE4W#Xn0N4(R+V3 zjebk{ELIZp)7wKr=QU4G;dDx*l;smL9Va%lZGF?2bc?Utl`zJ2GBC>V)|~p#oUdo3 z#{hfVQoG(N;RqqcX{b|0d2b0WFUI@b)-|^^?v&@YvWl8aJw#Lx|VV~UjWnF$*&V| z(|>Zo@?JauulUc(5~1^D%UmWvnDyp5X=-oRbLe_;=RIxS-f2Cmn%hh*XLo^G#ci+E zU9UQTNzd2a;cYRYj=EZCU=bn`XFk*Y(!%-DBOW7N9eR3uTj*1j_`jr^PP;k8TX$ z6g;5Qy5}z*s`&&2Rjq3hhF@r$vu@2EMid$!Xu&6E6mFC;|XboZ6Q6LnGN_N{Kdo+zcHFHW9X>VME-G=aSW)ucZ-%UG?SFTPYrk30bpU$%ff0CwTEYz@iSyoidVJM*Rn)*9dIyzB8PFHshPO!U?jkUyah%w2k0CDaXvSSvYZu43+uA<6`1{ z3bVw0#6k`V#W9?b(Z!!Pzb}S2j0~$sa2Byej*%Th+U5*K!UpxAM=#Gm;cR&A6Z1ao zB<)U%qvO`k|9iD>_^qMT*01NYVqGGs6Gmk<_C!AOk>`(-I03s^Br2)UAAQ@5%%5x- z3shj(EoI5Qi7eXmAaXLZkwog?v-d&wwzF0T_)n(cMUEK%x`W`>F0M}iRLEoFBFVsc zg!+{KtsZb`Gtancjo0w&wS0%mAR!nlgPabD|G^Sv(hiltHXf!IsZ4x@Km5jUT2RK~ zBB>FRa-L~8-;n%qqZF18E0RVW!%|k6Ytf;&aAcgW{6sfUW#x>i_!EWHMTfcEJHCHp z(v13T?rwVv@+=+mZt&R5JscSH0|(?j^<12CVCMm|wh@}F=B@|CU=e;|K+_V76QY7GDm3R!h-KivCTO?_X>M zKp2TQ+1r8WXIk<7MSF7MO7p?|3tQ65C!#`v1(N~uD+!I)54GO4p$th`UYA6_+%%Q9 z?)F~h^?v@i`a3E09qn0V(}Sv@Ui&Kb^56DblA1D~0iVa|QnzL7hCpcGAg(@IRHw0A zW#t2IC90u~v9WPOlBhrG`+_lCvCWxJEq;&JUow9QWBd(g{qfD1s9NhWMwwG15_Ql~ zuHUKNXfO=pK)k|;BwTBIq&EzO7(tTFEm~e)o>FMrb7-KdZ$%{k4UfOXq&Udp5lKWBnT~*SCNZ!)LGC9tmJXLelxuM47@kF|hql`* z20VH?HT*2&!B6FCe-FYEFABm(Da_;9mO7<<&G-jRsrs%NLmse}?&Z#sCwRa+l2rp@ zBu8VFeE}AWrJe#vJ-PF>#NOaHE)BA7Ev)M&DhlPsAH0Luq)xbwy6tYLN)Rdv{m4?w zqmo*%;X_ku+>=c*Gvf|G*dSbT4A2ME5xsK%1_1XhbK4)jTy0v7HYyL8{Ma>G}Fy&dZ6|C7C ziE;Zdq^z(Km7o^4A#u`)X|=E}9;xn#P6{&r0GT%7K%&b)Hfwcn_1^=J8WM`lQDKx? zSC><}xO$^9W^$V<(;qMtDpIj~SX7w0n~0P3LDJC}`*_G2=ERH36Y#7=f+qFps;pjM zDYBKOeTPCg6Fq*>SMKna3v)$K74E#ZnU#GKmZ%m_n&8jY`dreuyI1a4PM3AG+Zrpt z;jM|}O5U2>jg@pwE<#-ZKwl%kWk&y}R2es?%ief$T*k*Q1z+`Oen*&~(;P^4b539X z1rmdmlxotY_E3pmmdTY}kx$vj`8kZRnQJ0ORh+|mL&24K02d2* z9r?)Vm}^93qi%Qq6Cx{b+?Sm%u6)Aw)VZ_|EMLKV%!BB;{CW1dEZHzJ%b7}gs*}SNzQ*JGNbb!v!=y~Z+Ww* zD;6Y^@|Hmox+LD8b-leZ69Rk%6Joj8guZeN&iLE)obsCYE4gM`FD7KscQXjFRfPId zPH`5<`dN8jj62z4l&`A_89*fHY3hl^D%D>c*03%s^nOGi(c^CsmCx&`3vJDoe_KTl z9!2W(}-_&5JfHF9Sgo#)u@>SN_kGX)OeZ=MQ0>f ztXuMom10q|KCau-wN+frx2*_Ly>iI!ATpdP^OKbZF*Mz1y7*Pu8821&^xm(8zve>* z6y%IOY$g*)dfT((KZId@aB%wlS#tymZMLX$iaU#|;nLHROOtq+?O;Sp#0&jJ!l^q7 zc)cQRBE||EFH`OJ>!A-+xn5U}lFBXZItuew>zyt%#)$Nu;~qz<_^m*hSGYTj63f!a z@|jCGK))l7+SKFgK59jyyElRaZx{-H{R^Sag%w*OS`yLg?_n-@uf z4Hd?jg(3Ks#$S|spLygkNc=8eGbAqu&YW1n`#;O=ydQio(H z-;FZei_RP|DhN(UwO&=B7_8|PP`Il7W=!GeJ9vYpQBY56p+7#59&3wLY;5{EOwhe3up^Y9A-taN@hHoUI0p>li^QzQJ^gB7fLvy_L=Ppnp{v!|N)+N3K>w;n7F} z7K=0tCKsSEKi#f*7?S^k826rby_`5!5iekW$6>kye$g!P=-*AlOI$7Q^);Ds?4!(RBq(?jXk4Or)=%1V%^p;I2 z8yx%>Nymd?f}g}|7U6YF?^4*L)tvrgQY)nqSjXe|SVo-e7i4qnM<}U|RXrYFGD`7B zp)3DKb&LRg98YDpNreD~D&~}l6=!Q6~_$*MZpAd`UVpkiIE3;22U;GPkw7! zTOa_2_>dG$LJ;DhYh zz!zwt(lLuHcbLtVsg;;z+5XW`Gxp3aAy}J+USZ^rP2_g5^()?hIlx1bP$o7Ck0L^| zQfC*LU!uC{EJGBSpg5ZM`?ZIZ)2RTGJry(8w|K9;hF$m|6(&TXOM;as;_g+_P|0;O>u0g4(Y)%5MS`z<3tx>sz_C>d>ZG)qW|b`H!5>COxD> zc!h-$Z6z&+%t&ZDraOm-_T}$bHCMb@4Wm}Gmc{sYYTypHFf36SVvA)3Zoef6{qP6) zpoRdLzl{JF@t?Cy!;{n_VoKz2RtzrkV3Nnkt-4dB=KJ*~S#qDou9w?Y{}hyIu_NfP zL3wm9_Ob?`=Ihx!IS=1UwHI=odXav|)d{nQ1X|~1^avhJwHoz^We9j>03{u z-490Hk_Oi|-Vr891+&`i2fF zYsnW_Nfb7IF{A2{jx2`b;R&dAYZ0T;3KkBD*+36~#lGK&XEgxv z9Vs-e^Kb^die<2yw`TUaj&dY*Q;3kE%muRf$YMO_MmgYwZJEA&jk>Ko@|Jru?q~t# zdi{KldZp(qNEthDHkL4SvO>|8R>!~nQA%HnBuFiEiYb0nF&`<`VgrZvaXSma3GVA; zM%}(uzI8T{Yj(1ARPBtRAJbPwl$&_4vn=qh`C|qhh3Eu(=BF!mC}VY*x`G)OQF>Gx ztA&CyQd-2(KW@S-+c&L!w>WsIRETv4A_i@H7c)xPe7|uUN?w^c%8gWu(_et;eJ!xK z*#DQKUB83k(9-QArIDz_%@C^-)ARM5r)3q|0l1)D;^91)fXAUA%f7&+1P$)R8SM{R zTUSy`GTm572Mu}4?+n8R_^Jpz_ck!-V6!&3j{Cr_xPFUW{|dim;rOzBqE>v z3TMP-bEw?#1n=TG4U@F3U=RJiOZ2>Q6Dna1%+$StOhp5IB!R2v8u0`VO}tyu+z zIlFj{MPuShK@hluRhdSpn=IWasvwqj-1GAF>5P^URasW}IpF}g@NlE63uE~R{*B$m zzu@3f(0c**QoOqPFEZVS4>De?reRxxwXto$X{Oii^4IR=@n#{xg=KN^%Qo!@w>ZMl zKfhne|Io8Z-QpE}c)x@YmL}aFMRo&MfS+wsXs($RHr$_#J^3=2FBRN-(nq&t?ctiI zYk>$%xu@cCD}n@2i~-v-@w=E@mL=X?)HrBPDmX9U{-}2M5yINTz9(m}&Ml)unrmke zBD++0fq8$UxO~6g$N??!!6VqjbKZr^87h9MROj!8gPxm@&+ph`{U|VBw2yyGh7-j_B3(p8# z1ZBQ))ZO(?=I{d#{@!FqZ4My_EgWV~mqK;oZP^Gzw+~jLyTA#~Q|%-XNRd-a|By!4 zbdM9;gC3OegurfJwq@o+01A?V`CEUTBC=u9){G$1tZrP!Zf0B ztW58ky3m5Gz~hL7vEhC^;7e54J)ba<40x(ic${>ln6l2llJC`2gw`K{0F3CU8?rFq zZV-TqgJ{Hu8VNwK(nia*<_#@bJ*FPBt$_oU!w>QJIa~w*v)dOXh-P-0eTj>|fjs^Ox~aS{)xHDPHbSJz zIu9z8^#vI-5rG<@Pep3r@@cq;3WipvZwtzQHowbLvd*|xtA7Fsj~w4i*_MaBth$b$ z3ta-xV@cVeUcZbNi#dy@H~~}&?@^*@LI(Sh;Sw97ua)v3I79rst6HF zNoOQ`bp*iuRM8NG z%BFP2Qg~6>e}XM$kf~ zZW{<$LqrJ0G0u&y1bUv8t)}P#)aUj444!}>M%`9Di>Vw2N~yTSnvFNAEW1C3kGBVv zL1c=~N5P=k)i`4QRt7ry+S~vUhcbPsaExDuD&c`s5p(|RHub(&CwHo?jq0gCzu8>; zrB+qZB17?5ljQ^ANLW@+zJcUNRcwo(h}w7E2&dCPj_kM~CgAaamiN3H&iv`5N!fd_ z*HK!oXQ*1%YL}gESc&hSz~8o08ZzFVc8#V43Egs1w$eYX;!Lh7P1glv}E?Tyc$Vc8)BaCU9^80 zecCx(ZM3G$LM4o!{Yvf}?o-s z;Chy#@4h^+W4i8v{hS^R(AKs&iI%r(Tp$pJpMGd?UsG(B2p6iU{ zEI_L^y#l!v%z6>dn~j%KkE*|zD`Zb`z~uZgQR}ilS*8E3fNxe!S&*8j+j(KB9|^Kx1By;@~ZH>!!qSYM{Z!c zgZfWHwTyWfj{jpZ&iMOkLVI+&jR&#>8tTs{pOfWh5T|6VCOONNTuap*YFr98XtP}t z+gND7JBgX;({!aHWXfK{`JcveA5!>6yCI+zHaE){3Wd!3u8JD^-u~mdJ)uPl1>$Cz zdZ=W<<1%f=LenEjw1BdIiI2FI&St7-PVsG@V@hDV6FQxrz#(P(-7cTDP2u|8{MKBn zw<;V^!8W8R z^gY(7f$&_%apS6km;|d%A^!Q^zy=_LbsD|2aHpJHR3PHT^%^UIqJ zsM0;(|1LcFRb!H648$vF;5TB+8yyQ8S?}-!?^vk!#S5L65>&4zv9WYm=ZJ}rHRYP+ z(dh^;5^d_UthaX^{9tL!sBdb;r{d40{EGe+J*_=DwZ*BjDb!eX1ZAe&l|6PEe=aKR zZOWZb7%-s|-ZVq9b#=Q~{U-gW$!eOYeoe%<+ZUJEyEP?abbpnVJhHt`!P-`isk{x2{MtdC}wg0hW#UTnu+xNZyUZ0UL|(oZeQo_?(XGr-PCAV zNe+FMeK9z{_v<4xQxO`j(E`<*VP(4fUqZPxtMur;3ErmL0U4i5;P=~1@ffwpI4s5^ zlX1beGA*5|{{OVgVdIj5bGxwMd#pXV8VFAR$oH1B4ayZX_J+wJ*<7M2zJBU`X-b}T zal}IUjOH#bm!1;Rm{L&UkW|Z2ol?L?+G@ejw4zw(6F~RP2ucNzy0sS*6GWiTnAo5E(hvuI~+B*+KVc1 zsq(6wpq8Sg{Tgn**X{ALqTIE)`RqM}QG=B8DnJacg(ltc!pH0HK3_X&n%fMaA^jr4 zg)zlV@Sco1J3ImFfp<@`&fN1444R?**h|q>^Y}yuI`0=_y7x~}iJx?ln#wn*{voZZ z&NA?C>+Ft;(fJ;Iq4U2!3)YAZ{jILMY{`LiM+nsOf2Nc~?wsq+T_j0x`Vtko>fwH5 z^K6-|y?j!#Uvb>v??%@cmfSAxy>5pKtsCTkJoBK9+j9+ zeI2k$nxbpNfnlwJABZ70x)f`38(sPXT8C{+@Bk@F3*e4EoQ7~mvvBGN-EYF}_r)4$ zuAb#zY{(2aM96z~8|}}j%l)*+PI$p?*e$RX_B5bxT$3MFzW;Hh8~rFqS+HWZESo0% zx+CAy?^x(Oe5%?q(nwJmKyA3(&kRn?(i&vYbKUW3@5d6EGyUl^di( zs`m|UJX9C`{CIjE<9(s1_=ffC{(`Q3eh+4)8vpRE90SP8Dt;Icww!0*klus0zPn8F zGQWpZo&#c}(|bV3`7-&9*ZbPak~@s`$1ae8bsyT&e4cRh-+sF#N5>-ivt)0PZ8qNW z&gocTt&1|9@L!L_(#l_tAH<7(h7B(}CFszz_LUE2D9|^p=)dIL$!Sr#AmJdVRLyi# za}gcY$n3@&t3F@CVL2?y(l@RQz{97Ide-7}rSdHXS;AUy>^XHjV~uAOlW&8N(ntE(Mx;=N zjAn}!jOz1JYTt<|eEx{oqg`YFMPOe^eiN@hRJKl*aQu8^#d($9KQ^Wk34DUxPrzIz z$D!?bSF73+LD)uo<iCM#>? zMR9|rfp?CZiJ(PvI}G%y4y zNyQT4@>0)qsmn*F0QdC~_>cO4WM7k|`pU1(<^gH}@?w0vm&26ibBa7}_WpooQ1vlP zxCbygPAIY-vVw@i7$EbHCyoI}9$(CHkdEaV(*1Y3QGJo9g0Q%SfayN}ly=4yC>+7S z*c_0VmUpcDJ_kh6WHKcb7x%7&U1|cw6c-2G4+}0IjtseOD3nxTdjt!M=A~Ob&Q4YJ z=($#szkf1#j?5*j60%?1)2d4NU^7bSz3e>3q`!VZ`g)G2X~X(EG3fPvh0$+1#ZZb= zKBbiZv|0SBqbUyK-wSER^4GGKVV~{YaTLLf_Hup0*afewvChU)fN z+?qU3II~D7{Ni9BZ}T8FZsiCG6|X^z#E!j;;h1zfg1q*kqAeHHTo9Wm)2N$?^@WfT z$cT*Bh;2bON|x*|UX|3xvs-)L?uUdI7_dif{s4#$bJ!sOp}0yiSf$acsC%sdHgmSc zw}&M;orsE*hB*!FCYTnp%M;AfR9aJD# z`3)G}d%j-dwmM09W$#ETiq1^HiiZFf=kng<@2!1)(T_@hfCxJMPrWhUTa8_IK2_Kv zg4o@uu1&-r6$S%&UYyS=)XLc3sgG*LDycbdnq+xHQ?&P4_8sE@**%FaPH*lMOW9-} zjzgiR3RswAODs~?t|m;lP)UA#IX#vxy2>p?(M>J>^Jr7_XSOTKSC%%E> zZ`!1yltqH$IWrcDPQe1?^-$Nw8L-Qw>Wd}z<>ym3gbh9+L@y69*aXoG@{i*e4|%+| zPrR`&uHE^ME5VNbglZKL1yQYy4M;H-Yp|^}w(YlA4Y-bmUGF%GSS-HruAm2>+p4)I zkdcp9{U1j0Vti^~;wK|XwodAQPoCI`Q@zADGaw)i?0tmkzsRLI^$@u4jb9`-)w@i@ zTtYkZ%1n@Y?4E(GtmVe4BqPquow%$|I0%`dcM)2zgAE!N+cJE;sPfs4<#C5)ECUk_ z@C&WCrZVYTORE}v2G38GUV(Xp!fR5JS6if z{V4fN$)1voGu|O_UmI&g$#Tz$G)z7n)jD`j{2&1IcTDb}By62o@W0mwu%@cOj_QCv zz{t3ftZQOoE)rbb#N6ABDqM_Tu75H9UUZ_By;_J+bh^~uZ5MFoc4*CwIc?>7%wPXj)@-g%p=rhet`pG*l|yr%rVjaIeU zv4{gcC;pL$bzUq|MfMoRusawsAE>q3V4J4eDo?*SC*h|F#WtfacV@cE>OZIXC0e^R z*rRvp30xKu`{p-X5YgH)7<8RKux(0H(oBRsPg-qtQWN5QVXog(rzQ}kSxvQh;yRIj zR1mn_X||(Ler%FJb=ih2&z8 z{sVwpD8r^Kvm-i4hF@hr5S?;V0dt|&6eF_zNAekbFBbo0dvFB6*!~{>zW@0iGs)M0^Xi!g7!vygRrK;C5VBklqR7 zyW6v|lvCv9LkGp7d7)X&ftSTHn?dk=&v~f(U<|QDiK508+)gZyzo06(14F^mZ?sFEgzxL98OJNOKF%O| zzJotizEpE+l{D{zCOc z4KNMr`6l(W+pNnPyI!eA${NL~smovHZ+K;H1R8V6Rq(;Z3fWb1>Xz0kz=eVOd0GM^ zn5EsQv&cr~`U8n7^)*rNF1*r#h>*cBIc35=@Gy60yu4%?qjF9KrDmKcOpED`zE^%%=#4my`rA3L zmj9E`)A4ri-v)KF*qqWS%!VF7e{A{-$GJHLooILx$dJKvM?l-k zVdoV(9Gt>bx+Pl~yJ!A!8Y#x~yCD1=0p9Q(k5iQWoBLX!42+(`zNh801Zk2l)Qbh* zL|3UBU^qVB!6|r=_#z2X81Z|Y9*;j8L9E%PLBiGg`SvbMJA+@ocR5sjC2Su!*TxYAF%ooW3%#^><{^k$_Zl6ZE1hBa|AHi4L7gSKfE5`*9#X@84Mgc7+8 z$=vS_h#P3uBVDCv8brlou3)o>ly^h1&r`{`sI>n$Sp17&-+1K99X7AdSv*!m<*A|w z-~N;E1nPV~vfm6nWFV7S8r1ts!F6Thv#+StW9)rHpQmN7i*+$hq+$F1%v#SiUOxl+ zB7Tn{yd!^;X(e0KmHvU8`Z+;a@Fw}I!FDDYZfFk#th~&L_53hNt3%mmxL=}6VToEm zON-9fKArYY^xaX}X0NLviD8#xEj5iqRffpBD&CyNaBg&2RnrXEcbObV?HGl1P;~9K z$8{WrqSbEY-^`CSH+GqVrHHOm&jwk)wi|7BnM*<|aSW@j&W>b(#lt#&VfNB8+W-O? z$*1F^l{jKcXj3AffR434pt$Y*Ady9yS7`oJ`r{-%9xaHynm9DtBBT*JyW$nl7Z))x zBg6#m*UyD73nU;KqC}#mA@67OrZcDY+C@y_aWj#{jBRDmu_(&Y@k!NK1+T_AFej4y zWE5_L-&hPjW65yx1)z#W9Je6UQx>q3PooPzPxI_fDnEfSq0?@{&8xiP3w1q7ZXX4_eh38q z06HMokkr}!6RD%NS^&9F)eQb_ zG1^MpgyYvm_iMh<=q;ijF$62*Cl^tJLGCSs;doU9<2|D&>0caV*fT|be4z>~$%jky z4fxA+!0`}^mhbm?m7w=J{X=<#@g}U%O0*%hpM@Ymp`H2y{&}$ZY+S5@@?yg#Yqaxjvxv zF{||D-;I%S=DUQ3ajc+RRFgsJL%$9q9C?*%%nkZ9hDp#!ap;X`QK+7!vbqh=fQcTvI2AO3pltpo??@Fs$p_m;FZ3wS^D^1!Fq%O1@+nVt@7k#DVn>URiT8JevWH{2~=) zx+bcl*dWjRcU6-EL-i=rS<0eCyr!YJVSC78VN&lrYsG3WzJkil`bqr18L!`v*L_vg@X#@O>bQ{W-B(9scAPAF1C& zVway6k5hkuLO?hE^K>*D|H6R4XdY6*e*5)E?$8}Au{?rQdi#$HIq9~Gm4C~1ZK%sO zC=X6MA2RstTa$!Phm4yvEpLR2m{R;Q8)5f4x$!7mmC`q#9)wdR#K=S- zbc^t;!Il6Yta%?*o&w`#^77w|QX27m_&Z zxSYr)`iTh0AX3cS)qjF(@uPjRdqD$3C%|TBJD98xIkwN7^vS--C8 zL+r&qMnXOGPhHCbIYegP&Wqgk=Kj9L5kNnmvbj2%UK;KRhU?r#+i{*-{# zyGfQaKW|viF!>pqTMz%EX+fOF$#vL~g;Rpe5+aiT0e4hf<%G8*-mcyaE~oNvGmWn* zFZxa#%_$SvPHM1lCi6Xa_;@;-u7U5an{}1{i{RaoHWPQ*1eTbWYy}3Y7O*8NkazQ2 z>yA|AALeJnZ!JR2CA$PWYr7pls)Cd$)xe|~dy$f$W)(^5(X%t}1v z^<2#EQY*YtJU!rG1=wBJ7QYtDz0=dIwG~e)msMRZDdtvPj%!RVpK30?O@#OS?P{mE zda-C)T>UkwrMMB>{%oQ0>v7jv&gH`D+1L}QKSkSdyS`lJhg4)hR!f3Z%zI3ieli@D zXec;t9eYRE{*l%EvT#ExFQeT@+u7l{1;1`R<7)nHlx8{V1= z2jk`OHr4NLT|B%?SS;!qZuv`ueUb86j!jXGrh>S!!^r}Akk=J{>E1)exZ$s1!D?N! zbv?Ox3Js!~k~VHDcvnN;=)yfCsY=;4)DPgGW<~_zLTO{Xq4Mda*{%T61{0job9AXu zpU3=70Tyai5Rg{16NQl`NZ8Kxdre7BO&N74_j0K5S}Pa^6pMBAWF{VZ%}3ZT+^KY& znye!!{!+qM7#}(K#KoZ$cUrFCIGlj$L#eMx)%INW0 z`*M3~n@irfs(v9uqmh(P!uzhgt%ynmEQbst#IaxmR*V#5Gh-+qRwS>G(LA#>vVVZ~ z5y-FZ5HZz@m^@Bx)5x+gJuP-TNfDaOSIsDnKb$izaScKdh+TFd(wiY*o~a}0N`c-m zY1cY3n0eLJQa{6x-8-4b+3?2_i^lXu=WP^C zlr^JOj$4k;j3`tLs;te+t;I4O$^uNI=$IY;G7?sDEw=t_~qLzQ_dJ-9N41Hzyd6;tmmP!V(eg<(#55uqA1Y+IOdSdzU6tx(vBR?9Xj>V4?{>AZc{P(&Wj0PzIH z`q7=os5xT=Sefnz>>;}^OaWbN&o0QZBI!AtUI9s~sIuPq++1|J&v*jHgRjibooFqn zR6*X|=h)_a6f<`Nso^5xD~xgaRBQFv19 zeV>dxU^O1B_|Ln5j4TosHSoPkjHO7h-_21QiRJ!8_}zN(a^+*^T6XrqLDj>Qf~Mxb z!)Uy`B7`Qam0b24!8)C!!O1ig&67PqdR?X_I$LN+lE2VEYEGWxx_`*!(_$=XD`*eJ zB-7+_ndq<>VOgO8)c~E!4k=Wje*p>Lj4|BKXQH<1MXC8k%>E|~6HYCz3QMqK{SJ?P zodZK%->vS*r7pwbK{pU!C*8frF^THx)i2xYWuj_~s`eR-Q67&WARRMVG8}pQ zo_)wDf{iXgU>HccTqo0NlV|SxgFc;0Be54bm^amsQ*n<9`@D~%Me1trw6AfI{^171 zJu+R2kh#;QX|%qP$qJC?WIjUxVL@lSHL(*mY@d+}sw@8eMheTvz(=ECTm&hYCi;{O zfwoG9!~bO)VE5}sd;kjJLqBFxiDBv3H@sD1>PAhKAWt9qzSbGs@LY9r<@)lCJ=4GQ zOQp+ZH_wcrUrnc}UHGB;KooGpu_}ej9^7#h|QHGDohvc#eT-k^9rf0rO*6$ zK%6E$JC!d;iI(C}?ktmRcx9I>PjGLH~6@D(2sv*w;1l>u6ZLk> zV!`k_k+POvO_`#f1LFg~_A5(9>?86b8EI>eQ-aKnFLa9Ds6B^lQz0s$+rag0Uk5GA zq)$}H%dG6MToqQwtx4bszR9+(%6iqCSTo)$Z>`mxd#%lldu(K^)FS)%9``30RPT1Q zkSUU{6`1ym_&I640-4CM^Pw2!URyMDC%WJhx80J;f`L9mWTa%ISbMd^6g@5iEN)&{ z57$gXc;6%ur&e!AXnVVF@+cgv2BFwjkuk_d_2s5gODQ@?qKmyN4K{I+aReJ0Wz>eW zw)mhEd^B#QMdlXplc>bJ5$(L)xA~^!Gk%RKnYRO(Zt=#}AIbQHa2NxFSpyM75C^2= ze6yFwUTU==&NIatDjKp+u$hIVj9c5G^EkSe|E`=1kf+Z6{p*=BTem6EISg%4?_I7x|P=VDEe!jMo%%g&^ zZe=sF3I*3vDOml`#!Tn;sM38VFdoT6%$LDoBpt6|3_e8^rRoNDS~l|YSDOxREgS;pr>N~<)8tnaK< zDl;8981S7tx4vr4$POEKZT|uVN zD!LR6d>w*+@O=w;*phyTw2*bs;`9Skk2#Cl)vZ&nbny&4%B)8w4zK+eJf=#=M%7TNQjwO@@7Cs3~aDPw9}qA&$kDWHR+_X!j;yHLD=n39Z*N6(g8wD{32H*rvX3rW0DmnT zfDnpfxOStw4O4zg2Li-n554X{UuPY_TehVUUa4NUMxFZrKF|bR$@j!rDb=^}*HhCv zjXpQqbT+mceSk3HV*bi$0j1VsUk28{0Hq3+~O;d>R^*6IK9*m4&Z0$`7vEbZrFa=haa$w=5s&>Za^z(#)o1@AXPPobw& zT?e+Ov#zcR;0rQ1trK~-Lb1mIpL4(dr$L8z*iYGKZp@m(jw>PA^&(&+ru`A-MrHB(lmLM(_WM zwd*WeU>DpL_4hfAH2!Sr2dx8qsS6Y%w?q?te7>4$djT@$mgnEwh>>2zlFZ_E>#gU; zkE**)5q>^TX7NlJ2O=y3fZiO?ia#fcfP=a-ax9+AsO^uDPWUTjI?GU`u?{HpHi~Iy z<;|BQ*MoG0d$xFe@|r$J4@KJO0XRjy9c}gZpJ-RMfUf*)lIe3V|0iG|cO%qaiU=eP z_Vy~xcRituf6sebKc`gnpTJcx2GS7X94h6P+lZ)l5w_6(Lm(S(h)qME_K;1%%6Wdf zdTf!18GB7h^A+d--~uWIemv|8zgYv0d~Wib_w~Om1J7R(z*q6i6-c6#kdvVToaHFa z$yWtxpW~W{xyTGx;tmH^5#3@_e_$2WCKaulW8N5_O@khWmgL9lS!greh~9ozw2CFw z|ADsbd%%6IhaWwMf%}gUtVJ?Se=yzG{{;ae)Kw-t$Pc9ad@`WX;j{7!pMFf~^vcGS zQ9+)W5G}i34(AGUr@*H=iDvcOPNFLKCbD$V-UYbUgXrJ?4K?Gf4`^Uff4z+|GW#_m zt#~NgI=0(mtAUn`ACC5r(^K#6F@b2~QMYmW5C z)t-PqriLq?|AeezxS@hF5TsG|OylSh1h7>H!{S@VjvEaZAWn|3?|!zvWZa8>Af$qbAXP5<@qs952TBzl|Jc{)X)>yrE1LrRoe` zANqR6vbUUWb~-d~g)^HZT5gTd=AtT@2ZjZLN?4)7YL82gKW8cc25e}fpMN6{oL=t@ zZE*vHQMXr)b?ouO*VnMI8b!-*gX4Z}z`AAQ;pFT;01SHu3UhO}wg5(faRIULFVwTU zRfx=ZEo$E1$nACxJ^oXWaQb!zgC(q7G=G&JkV`qu7>t?NBqB#9$2n;0P8N!K8s+Z| z%(p*Y5pf9Nk$2xkYGrz#FhILKoVpYpag{kYW{4WZH zihGWb%S+Za`-?7ZOcM>M(9FvB|D^O~y)z z9gzI4u+p*{OiZt=h!Hsf9Dl6;gbxanj;%|w_g-BlfFhhPPUDXMdH>751=5>$0auR< z*6kILyBN38z~1O@97Qqd|10V$!=ifLw!i|5NGvHREv0mq2uO#7NJ|PVh;%BobVx|< zA`Jr4rKEIs2nZt5tu)fSv;O>FFW=Z}=j=Ii=FFMro;z}jp;!%oKBd3+$6y2cG`jl| zV+y?y^jjm!r=7cDD4Oa#ESJ9Px(9)u5C zopf+AbS^e^1Twy@sbq_nAoYKULx@H_6mfFgK{`mOJyo^-9t!u@cI{I1F`zhM@p7jtGD`ma}7uvGt=?+9^B(ElUoQOsOc6UsZ z(nVdYP8_#|QixgFoO4YmFYM&M@VD)r&n3s-jl`$m<`Iti($jc7@nYN*ug>Frs+CwV z%68^$Ly`(tT}O{U>xpMG?H}SIObk zOf!~lbAHcVQ){=sCX*V3zk@~Eauw%q<|$O?l*@;UQb%?JPj3>B!iBW%Lm-lt2_Y= zVdkAiTZ5L+n)HWnaMsEZR0Ic_2?db{d)DS!ovA7c5KG$(MdO_<$KY{qv*=$SKV?Zx zzOr;_k$N%gm`Cst7*xzkX(3*=;1w*C?IYr%ja0}MJ(WVPujlS9L{QLjB~_~7QVFW( zk+CggWUTr*P;eT|i=3!t+pTe5Ow3-)@bFvw*5tw|ouJtB>#?n0hQ0LAc#f zZw`3Q;=-onzrLa@y{w2Q9gy8!WV``nv@A4}fp|&8@5ws;=YHsiHRD8@PBm4MHbhuY>$nMQ4-GWya z#jL92x4b(Xx#IbH=gIn;>WDv`F;#W9M3oEM#fKRt4o z2J^OZZ#JoVXZgqnlV3e#LlE!tQ%bg{=dOB<33W&|*xLb@$5`lhAI5kb)R+-ap8#BRtHu?u%oz?utYD%I}rm0v7FP z7ZnGon`N^DGTrXNqZksd*?1GvF}`m zeVpnuZN(Nn^{Kn+!iI(8?36yI4M;HA`&4IJ_XP8EEs?ocwIzhT>&6Y5S4y(d+I**y z*ju%q@ebppL-0R`iPQejjiAq#&N-_yWxx!1a=Wg^=W=N+$IkL1gELGK=Y>1)JacSi zWeh7u+CRUMTuT%1x;1`H3g?~<k3OHd&*6&;B z(^WPRp=TN*;4mmIe_*SHu38X5BH}W0c>o}V2p+xGfu}@4?S9QFRT#H5my#4+x z)WPqS{Kp7~8L{s2D%8JjN$;8TzNrC8RBd$*&<}{2rH_XHVsnsXj&y6=ldk5$Ob9zx zmGyFB_{t{ZS_CQAAMJZCp4|%2n5!QwXNhkZn$-SqwdLO&pw`%ohA@rXn8Pn!P0GZ$ zC_@G;j(s4HputSzLBV3RqJw&gns2wVA$y#&RJfasSG(Q$AJ%*D_J^w%avdH`9So?| z%vlnCD@(SS`t0`P8ON1Hh5lhTX4SUVwF=|AaV0mlATQn&4JM6CJoGyG5{y>DZ~I#1N^@n;MKq0tVbiPR~@e_D1~N z9{Xpu%s?0FKV@69W3I$VrDZ$zUsonPmXm^MfV^zE%yMYK4Ni_Gg9gS-C>v&kR4^mJ z8Dt!ajg$U;Ud((>wq6)D5|D3=NRYxp{X$QgmkszzHR`k> zWC&Y%a8iOlFELHt09dTwR*ZD8=Ael`j(vI{<&92f(S3j;WizDcg6|<2^ldHeR@g$x3>}~0J!y`vw6`FAkI}b-{8+Y(u&F^JF4yKbkdUL ziag5?G08FH=rzGzQv3Q#nYOA((8kW7+Bkn!IYwG= zf6;fZvs4Q`-a=U>>DNm>+o=2R3<)p^iZ!MkbKWM z&m@YckSkgGQOtyXI1tQjfJN&xpz2;V-v9AmBYaE~@4a9uQRm##7!KD=fZt*xI`B38 zM#XAIn6jGxUKl{|q@>mm6j+w89{+n57SRBmQ;jya;=d1WqaO~OzL}7J&q60dtS3fc z({RKamZJc0c2Y1!U|>O+nWD%J8%FVc{QCs`{gQ!`hwT3jLk~mWy^dE}EdP3^j{w~D z6?cUMx_^(5EPW=X0IWs{1;#S^sBFSu%q4xN(e(ZMFX&H%|IHD;2DM@iegIVgO)B%h zmiuykPl&_p)?XPBi3zFgIrL1%LX zR@#*Z{Qv^m_)7I_Fh%(@YS=;UTL~uFwH$3vk8@OXkUTNXT#&PA4Zz0vF2jt@YXy1O!21x`6GCT@sLW&ige9!Ph|7_v^aZY@Gr< zAi>O!#m-2J{$eojag4@bB2HJ_%@=W{ipGDqs|NV2K!qvnciL|J)+oc?N+t zaUfOH#rDS}&<5GwM!%iWR|tg^%KKx^0phS7VEgeO0NQ8~P7;z&*VxOl3RH8o7HCHo z{uMx9{vtgsIH}kEtG(mS6@3^VOy(4vwZO@5AW42WUZBY|Uh8B5LgU_(J+-PALoeW~ zfE!~Mt^HhM-N8f%ktE>XTm5jGa+_HJSd~Q*kfHjUeEy=L50DqN>#NJar44Wy}feH1TqK=xxPJH?|?f-e9KB@DzO6v-6Ix6%JQI-TV+3n6Th zpTW{!B_Yh5b<1&9@oGZ7&=VlsHZ%6# zldA%8a47(i9VZ@R;8wfmvc5O46sEKbY{2&_G&DO!7;) z@xbi0vzJ}EZ;4I-kC7J*od)6(2{|-he%_cW%W%kpH$(7CD*Nu|0awOPw($r|Z79A> z$?efLbhlT40Zo)39u^Yr*fQmHc{GI^5Y#h#dax!=AUK}(dnD^|-vZEwQwg-ZEUja) z^qZw)R91+hwmhDD67ADGE19xT4@A8@lodUzCfx@EFluyxT6s8(Ch@c}DF%KtgMn8RY z$%ztjp2R7}QT{u1kzpbV$D}ZJGXn{on}$Yn6mUJwn_i7(2Oqq{5wEhN_XjdNFYrYE z5ot~RP&=;u#p_$LoW{|{c=s~DRLF)fd=x|n);o0JwE>fxaX{4`x^)L6cB#`#_*4LZ zYV_42DJL1jI`2aZgDx${Z)Tw=G{Xk(y1u&iVW$k3PSFbm2I2xS4763B$oR*MrdBe% zRj$RHd&Frnk$s zza%tY9PwmVB02*e(<wo+SjU!WKJoZ(`XPjBr`^DI4PvGdO<`?PfT#iinM*w#{a1N@mQ-8(X8@;md<9nZKm*Z4YuLYm#Kg|2v`Rv36${6+2R`01)^NAiF z#vVKhS@X532eA#AkI_>(K|r=p(5ig77802s?r7m_%hJZG=sR2IvX(~{IoU0(S`5!X zBKH`6gV;n-B0{bEWlMD4A8t_qKieT{LE@t~o#eVAbk?6gK1x;XVi?$D&G2@Ps&}52 zR_<~g?XOEMv;6yF>Iss1CbtlRXu0)fuamOjdEEf{J(CCe&SGwx%NOY<G+s{u<(gPRCNGZ{nXvtt0! zd)PolhqMU=#4d7nH5JjoR4bWE;TNTN7Svx`7zN8{_K#T83NK1BKyVMGcf|>+j$-w5 z+~P3l%pb75vUrWte3BAgZFQywbnmzROrAFc%G785?LJ!K0t1W)DoTtX4F(y(JG?}3 z140@C%+;N!1_Sa0*KRZ0g-v)bHtA{q0n7un0F zgGsfmHOVn1V-)*lO)rheoF!)zb#bqBs-4vGVhOW0=Z4#Gm=vO2=~I2rHYNx{jD6CG z6&rCjS<_=?Sd!;T;5$VXV(-PUm9E*Ph-i zEXk-Rldydc&OX)%Qpgv^R#)?Ye@)U06Yn6WqnhR-Z1e@;sPmc0e+w&mVF*bXV$~fr zp?TBfzKc{i$rX^&ne7UiA6rVUfL*&+Xrm{}*rzn`^=F_k05h{5`h~bicZ>BoH%IZu zPo%$8cp}vqV_yq2UnBD)h&y)_-y?lc*Wpy;{Pwd(UoV_9Ke1z;KF+-Dl6=rzGO7Iv zy;SMM)$b8-MbX~Xd5(yfkPH}m@6zr~UUcdBzP<$dl^kJ?n>LZC{;=#dOoI-7H23C< z^S@=*YkG|%%mgpwz+N#CFDop=;-hNE(ZUzWwK&y)`CRABv;DT2d@$h}ec=kW?&TJH z(xQ_m*V2#pIb&qU;_IZ=2-0vXCI(YQ$7g|ovAx&B$yox0oO9By7`mNR0{&r?+*%?T zB9~rc0+Y;-e%c4gq)%55a*5OEd))++&#Iq2Spy--!Cc&qqPyBA#y#N!*=t7(&P3N> zGqW^e{E0ZHj^4>xKf+{Kg{#@=f{*C^Y5c31>hR|U{up?XgG@$yy;XgLr%~;)Z3K+% z)`KZ4l6LX_kCkAGn3&I&u%a5MTvJY1y?VDE7vm`#G}$ul55&DiZ-H=~TCN3BR?d8c_KD)QCa3DLboV-wY_oQU-Q`eq~@%r4CHw}joFOU(2(J|YaSU9&Fi?&8+HRlK9 zfXc%l4EW;nPmd>xKN$cV??_+1-p(Ubyr7~|Sy zf8_GsqvQ_Si2ZyK8jP78#Ig|+>Q^Bq^&-UF$&Il@{~6GfJGiYXCb?NW!pD*Q7<(~B zQG~r`#PoLL#8XnjX$QP3wyyT?bU_kIbWW!u-H+ESc#KShfng zeoF`I$cHu2tOw%k3TrX+R^(LbP!Z>jINjtIzo1)S3z-UnV#T?++wL0#Njn2lGGbL5 zGr`+pLi>k&4hbK|YpupG855S^V3UKZ&i6!*>%#n)sf;^?x-piCvUU03R!$w_4)PuJ z3LT#QU^G_a(etJS5o5(OKd@5_VR?3vJBQxaEqT{>4M+!~3{mr@EeGHoUL_u^r*Ooh zKhfofBn-moyeUd-e`B9$sR4`oa~}wzWQMQs|cXP{oCQHcNt*UHn>~p!CD#p8~5A|W*5bO&#b@T3_A5tz< zez4VB&&L^5~<&aEURba18f;&3Ds)gML&nt}y&Aqg^Uagg)uP z{TT%#f8y}%UbV=N!pbL(Bh@2tUbpN$0xG_H1_R8ZyW%$Zq+IV(kzBk{S<{QG=5%?S zszq?!^a45pAyvk(z~ePaWsLZG`joR6`yT#J%V{z9;jAvEYXfi^eu^x^jB(m6*nmvTZr)6Xq7= zkB~BQ8T8=~J0dzs34@MHZh>Za8&Y@g;p+eookaxC7N8+S{YFVW?z$hqPFaN2ot+T< z6JDo!?Um=m3;a~9MU`g_lk(cDBR6Ocd|7Ig4!tkSpb0k-g#rs>W+PdQ9N9q|QBT6c zq`f6hVt4PnCbg)QNK;8AO5}4jMZ%{HUUFLdc3v~3sGJ;9fA^gr&ZvNzh0`g?@0aE< zu`z8rWZP-S20viYz_OjG{s=4KKcP2jWFJh<56t26_DYCi3KB^(oON4kb1+IXs%c)W zUD}R(0M=GRh;CqSOr_fITi*VqnA0pz&J(B%8|6pHSQH1=plU&|@5TEFeTn)ug{;#~ zqk$@treV8-)JUC8t_DC);pz#8>Zfz!a5oaolaXgmpA2Fv;1oe+FRqC|p!#;7h=ju| zA$=Sd}Eik7E(UGZzUKeOcAH1Z3}p-VRm1jSz`F)u9xrM z!eYEt=)z@Z*C&8L+8zs+&{Zb+Cy(Wlu#wOYpLu zq7xgt`l z^OOMs>l(a?A$a5DDOla%ZJlo;^RP52e&JyRYA~6@=Gx!Y%RXzvPZLffWO=TdB)Ldg z5_hDyaW}jx_ZAJM0WcB7Z-xj}M6+pDE`~-3I$4j^yDTJcG2Wzz7no}G_n#;6yhSP# z^QO~nymxFb&;!Z0x7ZJB#kZd)Nek@pv83+^buU*wx%OqS$5BgFm*xBuvxbNf4kzIV zbY#uGjnR4vF|MEyL{J8rKtv^&E<8&|Lh$HwUa9XY(8B`npDa^l-3_eZS!r{q^>)3Z z3k^sy&E|2JY)eAi+o8g+Xk_(P%=4cJq2~u1+}kk4%5z<@P6J$6Z=>7;%@RROD({xS z##x=9IC8pF<^pK}9!ev|akc;#s4MJg8NTuUnw($fElbN>2e}812x*cmn-5Hco4eAo z=AWGr?E9H|yjl8t2iTTS5fbUO09z3*E3O9yf%x@!HLTQyT`9Nx9CN^K6DoVtk&za~0Qn)&57gNnG?^MNlcvAq^$(y~gw(~1KB;27n(-VDE|Y7Ge{ z)+SANpzz>@lffk*%{YcSyr;EZv zIRIMR3`Pmo(_07T|p#FONRW^Ao1NOj_gAEd2-M#`*it z^u{?RCn)@@EGi!Ze%y8qOVml3O`G03sNQgneSXggZ zN?DAq!9f!uaNes}awY$R-#5|lyIYMBx8A=hkq&x6sVzz$H(A+zegpg|$vu%Rl`#$Y EKW0%~9smFU literal 0 HcmV?d00001 diff --git a/docs/developer/advanced/images/sharing-saved-objects-faq-resolve-outcomes-3.png b/docs/developer/advanced/images/sharing-saved-objects-faq-resolve-outcomes-3.png new file mode 100644 index 0000000000000000000000000000000000000000..2d4c0c4399d8973066c63f1023b5213b44a9f603 GIT binary patch literal 52388 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

  • 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 literal 0 HcmV?d00001 diff --git a/docs/developer/advanced/images/sharing-saved-objects-overview.png b/docs/developer/advanced/images/sharing-saved-objects-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..5c08eaa286ae6fba889eab59542c5c36ce7dbe5a GIT binary patch literal 67805 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�q+!>)oc~wYuCKbIY!M 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

    TqDzU`uj4( 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 literal 0 HcmV?d00001 diff --git a/docs/developer/advanced/images/sharing-saved-objects-step-3.png b/docs/developer/advanced/images/sharing-saved-objects-step-3.png new file mode 100644 index 0000000000000000000000000000000000000000..92dd7ebfef88e03356c137611ba07a114ff1b594 GIT binary patch literal 127926 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 z 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

    +$STr}ywk?aR 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 rlXqzWIkIC6;7?OkPo+l5_R)U;ylBv5 literal 0 HcmV?d00001 diff --git a/docs/developer/advanced/images/sharing-saved-objects-step-4.png b/docs/developer/advanced/images/sharing-saved-objects-step-4.png new file mode 100644 index 0000000000000000000000000000000000000000..73c5848521a118c9855d59a9ead6f639b3a540d8 GIT binary patch literal 39126 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%0ACeGJb^+nV27I2^Z@jEuk>JO>D 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 LPpw+T_WAz+L|~kf literal 0 HcmV?d00001 diff --git a/docs/developer/advanced/images/sharing-saved-objects-step-5.png b/docs/developer/advanced/images/sharing-saved-objects-step-5.png new file mode 100644 index 0000000000000000000000000000000000000000..07e74e44d4e67642439f9048007bb18f113f1f9a GIT binary patch literal 65264 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

    # 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 literal 0 HcmV?d00001 diff --git a/docs/developer/advanced/index.asciidoc b/docs/developer/advanced/index.asciidoc index ea1cc810e8ff6..289b88cddd7a9 100644 --- a/docs/developer/advanced/index.asciidoc +++ b/docs/developer/advanced/index.asciidoc @@ -5,6 +5,7 @@ * <> * <> * <> +* <> include::development-es-snapshots.asciidoc[leveloffset=+1] @@ -12,4 +13,6 @@ include::running-elasticsearch.asciidoc[leveloffset=+1] include::development-basepath.asciidoc[leveloffset=+1] -include::upgrading-nodejs.asciidoc[leveloffset=+1] \ No newline at end of file +include::upgrading-nodejs.asciidoc[leveloffset=+1] + +include::sharing-saved-objects.asciidoc[leveloffset=+1] \ No newline at end of file diff --git a/docs/developer/advanced/sharing-saved-objects.asciidoc b/docs/developer/advanced/sharing-saved-objects.asciidoc new file mode 100644 index 0000000000000..19c1b80657281 --- /dev/null +++ b/docs/developer/advanced/sharing-saved-objects.asciidoc @@ -0,0 +1,472 @@ +[[sharing-saved-objects]] +== Sharing Saved Objects + +This guide describes the Sharing Saved Objects effort, and the breaking changes that plugin developers need to be aware of for the planned +8.0 release of {kib}. + +[[sharing-saved-objects-overview]] +=== Overview + +<> (hereinafter "objects") are used to store all sorts of things in {kib}, from Dashboards to Index +Patterns to Machine Learning Jobs. The effort to make objects shareable can be summarized in a single picture: + +image::images/sharing-saved-objects-overview.png["Sharing Saved Objects overview"] + +Each plugin can register different object types to be used in {kib}. Historically, objects could be _isolated_ (existing in a single +<>) or _global_ (existing in all spaces), there was no in-between. As of the 7.12 release, {kib} now supports two +additional types of objects: + +|====================================================================================================== +| | *Where it exists* | *Object IDs* | *Registered as:* +| Global | All spaces | Globally unique | `namespaceType: 'agnostic'` +| Isolated | 1 space | Unique in each space | `namespaceType: 'single'` +| (NEW) Share-capable | 1 space | Globally unique | `namespaceType: 'multiple-isolated'` +| (NEW) Shareable | 1 or more spaces | Globally unique | `namespaceType: 'multiple'` +|====================================================================================================== + +Ideally, most types of objects in {kib} will eventually be _shareable_; however, we have also introduced +<> as a stepping stone for plugin developers to fully support +this feature. + +[[sharing-saved-objects-breaking-changes]] +=== Breaking changes + +To implement this feature, we had to make a key change to how objects are serialized into raw {es} documents. As a result, +<>, and this will cause some breaking changes to +the way that consumers (plugin developers) interact with objects. We have implemented mitigations so that *these changes will not affect +end-users _if_ consumers implement the required steps below.* + +Existing, isolated object types will need to go through a special _conversion process_ to become share-capable upon upgrading {kib} to +version 8.0. Once objects are converted, they can easily be switched to become fully shareable in any future release. This conversion will +change the IDs of any existing objects that are not in the Default space. Changing object IDs itself has several knock-on effects: + +* Nonstandard links to other objects can break - _mitigated by <>_ +* "Deep link" pages (URLs) to objects can break - _mitigated by <> and <>_ +* Encrypted objects may not be able to be decrypted - _mitigated by <>_ + +*To be perfectly clear: these effects will all be mitigated _if and only if_ you follow the steps below!* + +TIP: External plugins can also convert their objects, but <>. + +[[sharing-saved-objects-dev-flowchart]] +=== Developer Flowchart + +If you're still reading this page, you're probably developing a {kib} plugin that registers an object type, and you want to know what steps +you need to take to prepare for the 8.0 release and mitigate any breaking changes! Depending on how you are using saved objects, you may +need to take up to 5 steps, which are detailed in separate sections below. Refer to this flowchart: + +image::images/sharing-saved-objects-dev-flowchart.png["Sharing Saved Objects developer flowchart"] + +TIP: There is a proof-of-concept (POC) pull request to demonstrate these changes. It first adds a simple test plugin that allows users to +create and view notes. Then, it goes through the steps of the flowchart to convert the isolated "note" objects to become share-capable. As +you read this guide, you can https://github.com/elastic/kibana/pull/107256[follow along in the POC] to see exactly how to take these steps. + +[[sharing-saved-objects-q1]] +=== Question 1 + +> *Do these objects contain links to other objects?* + +If your objects store _any_ links to other objects (with an object type/ID), you need to take specific steps to ensure that these links +continue functioning after the 8.0 upgrade. + +[[sharing-saved-objects-step-1]] +=== Step 1 + +⚠️ This step *must* be completed no later than the 7.16 release. ⚠️ + +> *Ensure all object links use the root-level `references` field* + +If you answered "Yes" to <>, you need to make sure that your object links are _only_ stored in the root-level +`references` field. When a given object's ID is changed, this field will be updated accordingly for other objects. + +The image below shows two different examples of object links from a "case" object to an "action" object. The top shows the incorrect way to +link to another object, and the bottom shows the correct way. + +image::images/sharing-saved-objects-step-1.png["Sharing Saved Objects step 1"] + +If your objects _do not_ use the root-level `references` field, you'll need to <> +_before the 8.0 release_ to fix that. Here's a migration function for the example above: + +```ts +function migrateCaseToV716( + doc: SavedObjectUnsanitizedDoc<{ connector: { type: string; id: string } }> +): SavedObjectSanitizedDoc { + const { + connector: { type: connectorType, id: connectorId, ...otherConnectorAttrs }, + } = doc.attributes; + const { references = [] } = doc; + return { + ...doc, + attributes: { + ...doc.attributes, + connector: otherConnectorAttrs, + }, + references: [...references, { type: connectorType, id: connectorId, name: 'connector' }], + }; +} + +... + +// Use this migration function where the "case" object type is registered +migrations: { + '7.16.0': migrateCaseToV716, +}, +``` + +NOTE: Reminder, don't forget to add unit tests and integration tests! + +[[sharing-saved-objects-q2]] +=== Question 2 + +> *Are there any "deep links" to these objects?* + +A deep link is a URL to a page that shows a specific object. End-users may bookmark these URLs or schedule reports with them, so it is +critical to ensure that these URLs continue working. The image below shows an example of a deep link to a Canvas workpad object: + +image::images/sharing-saved-objects-q2.png["Sharing Saved Objects deep link example"] + +Note that some URLs may contain <>, for example, a +Dashboard _and_ a filter for an Index Pattern. + +[[sharing-saved-objects-step-2]] +=== Step 2 + +⚠️ This step will preferably be completed in the 7.16 release; it *must* be completed no later than the 8.0 release. ⚠️ + +> *Update your code to use the new SavedObjectsClient `resolve()` method instead of `get()`* + +If you answered "Yes" to <>, you need to make sure that when you use the SavedObjectsClient to fetch an object +using its ID, you use a different API to do so. The existing `get()` function will only find an object using its current ID. To make sure +your existing deep link URLs don't break, you should use the new `resolve()` function; <>. + +In a nutshell, if your deep link page had something like this before: + +```ts +const savedObject = savedObjectsClient.get(objType, objId); +``` + +You'll need to change it to this: + +```ts +const resolveResult = savedObjectsClient.resolve(objType, objId); +const savedObject = resolveResult.saved_object; +``` + +TIP: See an example of this in https://github.com/elastic/kibana/pull/107256#user-content-example-steps[step 2 of the POC]! + +The +https://github.com/elastic/kibana/blob/master/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.md[SavedObjectsResolveResponse +interface] has three fields, summarized below: + +* `saved_object` - The saved object that was found. +* `outcome` - One of the following values: `'exactMatch' | 'aliasMatch' | 'conflict'` +* `alias_target_id` - This is defined if the outcome is `'aliasMatch'` or `'conflict'`. It means that a legacy URL alias with this ID points + to an object with a _different_ ID. + +The SavedObjectsClient is available both on the server-side and the client-side. You may be fetching the object on the server-side via a +custom HTTP route, or you may be fetching it on the client-side directly. Either way, the `outcome` and `alias_target_id` fields need to be +passed to your client-side code, and you should update your UI accordingly in the next step. + +NOTE: You don't need to use `resolve()` everywhere, <>! + +[[sharing-saved-objects-step-3]] +=== Step 3 + +⚠️ This step will preferably be completed in the 7.16 release; it *must* be completed no later than the 8.0 release. ⚠️ + +> *Update your _client-side code_ to correctly handle the three different `resolve()` outcomes* + +The Spaces plugin API exposes React components and functions that you should use to render your UI in a consistent manner for end-users. +Your UI will need to use the Core HTTP service and the Spaces plugin API to do this. + +Your page should change <>: + +image::images/sharing-saved-objects-step-3.png["Sharing Saved Objects resolve outcomes overview"] + +TIP: See an example of this in https://github.com/elastic/kibana/pull/107256#user-content-example-steps[step 3 of the POC]! + +1. Update your plugin's `kibana.json` to add a dependency on the Spaces plugin: ++ +```ts +... +"optionalPlugins": ["spaces"] +``` + +2. Update your plugin's `tsconfig.json` to add a dependency to the Space's plugin's type definitions: ++ +```ts +... +"references": [ + ... + { "path": "../spaces/tsconfig.json" }, +] +``` + +3. Update your Plugin class implementation to depend on the Core HTTP service and Spaces plugin API: ++ +```ts +interface PluginStartDeps { + spaces?: SpacesPluginStart; +} + +export class MyPlugin implements Plugin<{}, {}, {}, PluginStartDeps> { + public setup(core: CoreSetup) { + core.application.register({ + ... + async mount(appMountParams: AppMountParameters) { + const [coreStart, pluginStartDeps] = await core.getStartServices(); + const { http } = coreStart; + const { spaces: spacesApi } = pluginStartDeps; + ... + // pass `http` and `spacesApi` to your app when you render it + }, + }); + ... + } +} +``` + +4. In your deep link page, add a check for the `'aliasMatch'` outcome: ++ +```ts +if (spacesApi && resolveResult.outcome === 'aliasMatch') { + // We found this object by a legacy URL alias from its old ID; redirect the user to the page with its new ID, preserving any URL hash + const newObjectId = resolveResult.alias_target_id!; // This is always defined if outcome === 'aliasMatch' + const newPath = http.basePath.prepend( + `path/to/this/page/${newObjectId}${window.location.hash}` + ); + await spacesApi.ui.redirectLegacyUrl(newPath, OBJECT_NOUN); + return; +} +``` +_Note that `OBJECT_NOUN` is optional, it just changes "object" in the toast to whatever you specify -- you may want the toast to say +"dashboard" or "index pattern" instead!_ + +5. And finally, in your deep link page, add a function that will create a callout in the case of a `'conflict'` outcome: ++ +```tsx +const getLegacyUrlConflictCallout = () => { + // This function returns a callout component *if* we have encountered a "legacy URL conflict" scenario + if (spacesApi && resolveResult.outcome === 'conflict') { + // We have resolved to one object, but another object has a legacy URL alias associated with this ID/page. We should display a + // callout with a warning for the user, and provide a way for them to navigate to the other object. + const currentObjectId = savedObject.id; + const otherObjectId = resolveResult.alias_target_id!; // This is always defined if outcome === 'conflict' + const otherObjectPath = http.basePath.prepend( + `path/to/this/page/${otherObjectId}${window.location.hash}` + ); + return ( + <> + {spacesApi.ui.components.getLegacyUrlConflict({ + objectNoun: OBJECT_NOUN, + currentObjectId, + otherObjectId, + otherObjectPath, + })} + + + ); + } + return null; +}; +... +return ( + + + + {/* If we have a legacy URL conflict callout to display, show it at the top of the page */} + {getLegacyUrlConflictCallout()} + +... +); +``` + +6. https://github.com/elastic/kibana/pull/107099#issuecomment-891147792[Generate staging data and test your page's behavior with the +different outcomes.] + +NOTE: Reminder, don't forget to add unit tests and functional tests! + +[[sharing-saved-objects-step-4]] +=== Step 4 + +⚠️ This step *must* be completed in the 8.0 release (no earlier and no later). ⚠️ + +> *Update your _server-side code_ to convert these objects to become "share-capable"* + +After <> is complete, you can add the code to convert your objects. + +WARNING: The previous steps can be backported to the 7.x branch, but this step -- the conversion itself -- can only take place in 8.0! +You should use a separate pull request for this. + +When you register your object, you need to change the `namespaceType` and also add a `convertToMultiNamespaceTypeVersion` field. This +special field will trigger the actual conversion that will take place during the Core migration upgrade process when a user installs the +Kibana 8.0 release: + +image::images/sharing-saved-objects-step-4.png["Sharing Saved Objects conversion code"] + +TIP: See an example of this in https://github.com/elastic/kibana/pull/107256#user-content-example-steps[step 4 of the POC]! + +NOTE: Reminder, don't forget to add integration tests! + +[[sharing-saved-objects-q3]] +=== Question 3 + +> *Are these objects encrypted?* + +Saved objects can optionally be <> by using the Encrypted Saved Objects plugin. Very few +object types are encrypted, so most plugin developers will not be affected. + +[[sharing-saved-objects-step-5]] +=== Step 5 + +⚠️ This step *must* be completed in the 8.0 release (no earlier and no later). ⚠️ + +> *Update your _server-side code_ to add an Encrypted Saved Object (ESO) migration for these objects* + +If you answered "Yes" to <>, you need to take additional steps to make sure that your objects can still be +decrypted after the conversion process. Encrypted saved objects use some fields as part of "additionally authenticated data" (AAD) to defend +against different types of cryptographic attacks. The object ID is part of this AAD, and so it follows that the after the object's ID is +changed, the object will not be able to be decrypted with the standard process. + +To mitigate this, you need to add a "no-op" ESO migration that will be applied immediately after the object is converted during the 8.0 +upgrade process. This will decrypt the object using its old ID and then re-encrypt it using its new ID: + +image::images/sharing-saved-objects-step-5.png["Sharing Saved Objects ESO migration"] + +NOTE: Reminder, don't forget to add unit tests and integration tests! + +[[sharing-saved-objects-step-6]] +=== Step 6 + +> *Update your code to make your objects shareable* + +_This is not required for the 8.0 release; this additional information will be added in the near future!_ + +[[sharing-saved-objects-faq]] +=== Frequently asked questions (FAQ) + +[[sharing-saved-objects-faq-share-capable-vs-shareable]] +==== 1. Why are there both "share-capable" and "shareable" object types? + +We implemented the share-capable object type as an intermediate step for consumers who currently have isolated objects, but are not yet +ready to support fully shareable objects. This is primarily because we want to make sure all object types are converted at the same time in +the 8.0 release to minimize confusion and disruption for the end-user experience. + +We realize that the conversion process and all that it entails can be a not-insignificant amount of work for some Kibana teams to prepare +for by the 8.0 release. As long as an object is made share-capable, that ensures that its ID will be globally unique, so it will be trivial +to make that object shareable later on when the time is right. + +A developer can easily flip a switch to make a share-capable object into a shareable one, since these are both serialized the same way. +However, we envision that each consumer will need to enact their own plan and make additional UI changes when making an object shareable. +For example, some users may not have access to the Saved Objects Management page, but we still want those users to be able to see what +space(s) their objects exist in and share them to other spaces. Each application should add the appropriate UI controls to handle this. + + +[[sharing-saved-objects-faq-changing-object-ids]] +==== 2. Why do object IDs need to be changed? + +This is because of how isolated objects are serialized to raw Elasticsearch documents. Each raw document ID today contains its space ID +(_namespace_) as a prefix. When objects are copied or imported to other spaces, they keep the same object ID, they just have a different +prefix when they are serialized to Elasticsearch. This has resulted in a situation where many Kibana installations have saved objects in +different spaces with the same object ID: + +image::images/sharing-saved-objects-faq-changing-object-ids-1.png["Sharing Saved Objects object ID diagram (before conversion)"] + +Once an object is converted, we need to remove this prefix. Because of limitations with our migration process, we cannot actively check if +this would result in a conflict. Therefore, we decided to pre-emptively regenerate the object ID for every object in a non-Default space to +ensure that every object ID becomes globally unique: + +image::images/sharing-saved-objects-faq-changing-object-ids-2.png["Sharing Saved Objects object ID diagram (after conversion)"] + +[[sharing-saved-objects-faq-multiple-deep-link-objects]] +==== 3. What if one page has deep links to multiple objects? + +As mentioned in <>, some URLs may contain multiple object IDs, effectively deep linking to multiple objects. +These should be handled on a case-by-case basis at the plugin owner's discretion. A good rule of thumb is: + +* The "primary" object on the page should always handle the three `resolve()` outcomes as described in <>. +* Any "secondary" objects on the page may handle the outcomes differently. If the secondary object ID is not important (for example, it just + functions as a page anchor), it may make more sense to ignore the different outcomes. If the secondary object _is_ important but it is not + directly represented in the UI, it may make more sense to throw a descriptive error when a `'conflict'` outcome is encountered. + - If the secondary object is resolved by an external service (such as the index pattern service), the service should simply make the full + outcome available to consumers. + +Ideally, if a secondary object on a deep link page resolves to an `'aliasMatch'` outcome, the consumer should redirect the user to a URL +with the new ID and display a toast message. The reason for this is that we don't want users relying on legacy URL aliases more often than +necessary. However, such handling of secondary objects is not considered critical for the 8.0 release. + +[[sharing-saved-objects-faq-legacy-url-alias]] +==== 4. What is a "legacy URL alias"? + +As depicted above, when an object is converted to become share-capable, if it exists in a non-Default space, its ID gets changed. To +preserve its old ID, we also create a special object called a _legacy URL alias_ ("alias" for short); this alias retains the target object's +old ID (_sourceId_), and it contains a pointer to the target object's new ID (_targetId_). + +Aliases are meant to be mostly invisible to end-users by design. There is no UI to manage them directly. Our vision is that aliases will be +used as a stop-gap to help us through the 8.0 upgrade process, but we will nudge users away from relying on aliases so we can eventually +deprecate and remove them. + +[[sharing-saved-objects-faq-resolve-outcomes]] +==== 5. Why are there three different resolve outcomes? + +The `resolve()` function first checks if an object with the given ID exists, and then it checks if an object has an alias with the given ID. + +1. If only the former is true, the outcome is an `'exactMatch'` -- we found the exact object we were looking for. +2. If only the latter is true, the outcome is an `'aliasMatch'` -- we found an alias with this ID, that pointed us to an object with a +different ID. +3. Finally, if _both conditions_ are true, the outcome is a `'conflict'` -- we found two objects using this ID. Instead of returning an +error in this situation, in the interest of usability, we decided to return the _most correct match_, which is the exact match. By informing +the consumer that this is a conflict, the consumer can render an appropriate UI to the end-user explaining that this might not be the object +they are actually looking for. + +*Outcome 1* + +When you resolve an object with its current ID, the outcome is an `'exactMatch'`: + +image::images/sharing-saved-objects-faq-resolve-outcomes-1.png["Sharing Saved Objects resolve outcome 1 (exactMatch)"] + +This can happen in the Default space _and_ in non-Default spaces. + +*Outcome 2* + +When you resolve an object with its old ID (the ID of its alias), the outcome is an `'aliasMatch'`: + +image::images/sharing-saved-objects-faq-resolve-outcomes-2.png["Sharing Saved Objects resolve outcome 2 (aliasMatch)"] + +This outcome can only happen in non-Default spaces. + +*Outcome 3* + +The third outcome is an edge case that is a combination of the others. If you resolve an object ID and two objects are found -- one as an +exact match, the other as an alias match -- the outcome is a `'conflict'`: + +image::images/sharing-saved-objects-faq-resolve-outcomes-3.png["Sharing Saved Objects resolve outcome 3 (conflict)"] + +We actually have controls in place to prevent this scenario from happening when you share, import, or copy +objects. However, this scenario _could_ still happen in a few different situations, if objects are created a certain way or if a user +tampers with an object's raw ES document. Since we can't 100% rule out this scenario, we must handle it gracefully, but we do expect this +will be a rare occurrence. + +It is important to note that when a `'conflict'` occurs, the object that is returned is the "most correct" match -- the one with the ID that +exactly matches. + +[[sharing-saved-objects-faq-resolve-instead-of-get]] +==== 6. Should I always use resolve instead of get? + +Reading through this guide, you may think it is safer or better to use `resolve()` everywhere instead of `get()`. Actually, we made an +explicit design decision to add a separate `resolve()` function because we want to limit the affects of and reliance upon legacy URL +aliases. To that end, we collect anonymous usage data based on how many times `resolve()` is used and the different outcomes are +encountered. That usage data is less useful is `resolve()` is used more often than necessary. + +Ultimately, `resolve()` should _only_ be used for data flows that involve a user-controlled deep link to an object. There is no reason to +change any other data flows to use `resolve()`. + +[[sharing-saved-objects-faq-external-plugins]] +==== 7. What about external plugins? + +External plugins (those not shipped with {kib}) can use this guide to convert any isolated objects to become share-capable or fully +shareable! If you are an external plugin developer, the steps are the same, but you don't need to worry about getting anything done before a +specific release. The only thing you need to know is that your plugin cannot convert your objects until the 8.0 release. diff --git a/docs/developer/architecture/core/saved-objects-service.asciidoc b/docs/developer/architecture/core/saved-objects-service.asciidoc index fa7fc4233259d..a7ce86ea46359 100644 --- a/docs/developer/architecture/core/saved-objects-service.asciidoc +++ b/docs/developer/architecture/core/saved-objects-service.asciidoc @@ -45,7 +45,7 @@ import { SavedObjectsType } from 'src/core/server'; export const dashboardVisualization: SavedObjectsType = { name: 'dashboard_visualization', // <1> hidden: false, - namespaceType: 'single', + namespaceType: 'multiple-isolated', // <2> mappings: { dynamic: false, properties: { @@ -66,6 +66,8 @@ export const dashboardVisualization: SavedObjectsType = { <1> Since the name of a Saved Object type forms part of the url path for the public Saved Objects HTTP API, these should follow our API URL path convention and always be written as snake case. +<2> This field determines "space behavior" -- whether these objects can exist in one space, multiple spaces, or all spaces. This value means +that objects of this type can only exist in a single space. See <> for more information. .src/plugins/my_plugin/server/saved_objects/index.ts [source,typescript] @@ -153,6 +155,7 @@ should carefully consider the fields they add to the mappings. Similarly, Saved Object types should never use `dynamic: true` as this can cause an arbitrary amount of fields to be added to the `.kibana` index. +[[saved-objects-service-writing-migrations]] ==== Writing Migrations Saved Objects support schema changes between Kibana versions, which we call diff --git a/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.aliastargetid.md b/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.alias_target_id.md similarity index 59% rename from docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.aliastargetid.md rename to docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.alias_target_id.md index 415681b2bb0d3..0054f533a23d0 100644 --- a/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.aliastargetid.md +++ b/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.alias_target_id.md @@ -1,13 +1,13 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ResolvedSimpleSavedObject](./kibana-plugin-core-public.resolvedsimplesavedobject.md) > [aliasTargetId](./kibana-plugin-core-public.resolvedsimplesavedobject.aliastargetid.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ResolvedSimpleSavedObject](./kibana-plugin-core-public.resolvedsimplesavedobject.md) > [alias\_target\_id](./kibana-plugin-core-public.resolvedsimplesavedobject.alias_target_id.md) -## ResolvedSimpleSavedObject.aliasTargetId property +## ResolvedSimpleSavedObject.alias\_target\_id property The ID of the object that the legacy URL alias points to. This is only defined when the outcome is `'aliasMatch'` or `'conflict'`. Signature: ```typescript -aliasTargetId?: SavedObjectsResolveResponse['aliasTargetId']; +alias_target_id?: SavedObjectsResolveResponse['alias_target_id']; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.md b/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.md index 43727d86296a4..4936598c58799 100644 --- a/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.md +++ b/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.md @@ -16,7 +16,7 @@ export interface ResolvedSimpleSavedObject | Property | Type | Description | | --- | --- | --- | -| [aliasTargetId](./kibana-plugin-core-public.resolvedsimplesavedobject.aliastargetid.md) | SavedObjectsResolveResponse['aliasTargetId'] | The ID of the object that the legacy URL alias points to. This is only defined when the outcome is 'aliasMatch' or 'conflict'. | +| [alias\_target\_id](./kibana-plugin-core-public.resolvedsimplesavedobject.alias_target_id.md) | SavedObjectsResolveResponse['alias_target_id'] | The ID of the object that the legacy URL alias points to. This is only defined when the outcome is 'aliasMatch' or 'conflict'. | | [outcome](./kibana-plugin-core-public.resolvedsimplesavedobject.outcome.md) | SavedObjectsResolveResponse['outcome'] | The outcome for a successful resolve call is one of the following values:\* 'exactMatch' -- One document exactly matched the given ID. \* 'aliasMatch' -- One document with a legacy URL alias matched the given ID; in this case the saved_object.id field is different than the given ID. \* 'conflict' -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the saved_object object is the exact match, and the saved_object.id field is the same as the given ID. | -| [savedObject](./kibana-plugin-core-public.resolvedsimplesavedobject.savedobject.md) | SimpleSavedObject<T> | The saved object that was found. | +| [saved\_object](./kibana-plugin-core-public.resolvedsimplesavedobject.saved_object.md) | SimpleSavedObject<T> | The saved object that was found. | diff --git a/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.savedobject.md b/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.saved_object.md similarity index 55% rename from docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.savedobject.md rename to docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.saved_object.md index c05e8801768c9..7d90791a26fd8 100644 --- a/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.savedobject.md +++ b/docs/development/core/public/kibana-plugin-core-public.resolvedsimplesavedobject.saved_object.md @@ -1,13 +1,13 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ResolvedSimpleSavedObject](./kibana-plugin-core-public.resolvedsimplesavedobject.md) > [savedObject](./kibana-plugin-core-public.resolvedsimplesavedobject.savedobject.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ResolvedSimpleSavedObject](./kibana-plugin-core-public.resolvedsimplesavedobject.md) > [saved\_object](./kibana-plugin-core-public.resolvedsimplesavedobject.saved_object.md) -## ResolvedSimpleSavedObject.savedObject property +## ResolvedSimpleSavedObject.saved\_object property The saved object that was found. Signature: ```typescript -savedObject: SimpleSavedObject; +saved_object: SimpleSavedObject; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsresolveresponse.aliastargetid.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsresolveresponse.alias_target_id.md similarity index 62% rename from docs/development/core/public/kibana-plugin-core-public.savedobjectsresolveresponse.aliastargetid.md rename to docs/development/core/public/kibana-plugin-core-public.savedobjectsresolveresponse.alias_target_id.md index 02055da686880..07c55ae922363 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsresolveresponse.aliastargetid.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsresolveresponse.alias_target_id.md @@ -1,13 +1,13 @@ -[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsResolveResponse](./kibana-plugin-core-public.savedobjectsresolveresponse.md) > [aliasTargetId](./kibana-plugin-core-public.savedobjectsresolveresponse.aliastargetid.md) +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsResolveResponse](./kibana-plugin-core-public.savedobjectsresolveresponse.md) > [alias\_target\_id](./kibana-plugin-core-public.savedobjectsresolveresponse.alias_target_id.md) -## SavedObjectsResolveResponse.aliasTargetId property +## SavedObjectsResolveResponse.alias\_target\_id property The ID of the object that the legacy URL alias points to. This is only defined when the outcome is `'aliasMatch'` or `'conflict'`. Signature: ```typescript -aliasTargetId?: string; +alias_target_id?: string; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsresolveresponse.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsresolveresponse.md index 4345f2949d48e..cdc79d8ac363d 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsresolveresponse.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsresolveresponse.md @@ -15,7 +15,7 @@ export interface SavedObjectsResolveResponse | Property | Type | Description | | --- | --- | --- | -| [aliasTargetId](./kibana-plugin-core-public.savedobjectsresolveresponse.aliastargetid.md) | string | The ID of the object that the legacy URL alias points to. This is only defined when the outcome is 'aliasMatch' or 'conflict'. | +| [alias\_target\_id](./kibana-plugin-core-public.savedobjectsresolveresponse.alias_target_id.md) | string | The ID of the object that the legacy URL alias points to. This is only defined when the outcome is 'aliasMatch' or 'conflict'. | | [outcome](./kibana-plugin-core-public.savedobjectsresolveresponse.outcome.md) | 'exactMatch' | 'aliasMatch' | 'conflict' | The outcome for a successful resolve call is one of the following values:\* 'exactMatch' -- One document exactly matched the given ID. \* 'aliasMatch' -- One document with a legacy URL alias matched the given ID; in this case the saved_object.id field is different than the given ID. \* 'conflict' -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the saved_object object is the exact match, and the saved_object.id field is the same as the given ID. | | [saved\_object](./kibana-plugin-core-public.savedobjectsresolveresponse.saved_object.md) | SavedObject<T> | The saved object that was found. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.aliastargetid.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.alias_target_id.md similarity index 62% rename from docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.aliastargetid.md rename to docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.alias_target_id.md index 2e73d6ba2e1a9..4e8bc5e787ede 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.aliastargetid.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.alias_target_id.md @@ -1,13 +1,13 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsResolveResponse](./kibana-plugin-core-server.savedobjectsresolveresponse.md) > [aliasTargetId](./kibana-plugin-core-server.savedobjectsresolveresponse.aliastargetid.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsResolveResponse](./kibana-plugin-core-server.savedobjectsresolveresponse.md) > [alias\_target\_id](./kibana-plugin-core-server.savedobjectsresolveresponse.alias_target_id.md) -## SavedObjectsResolveResponse.aliasTargetId property +## SavedObjectsResolveResponse.alias\_target\_id property The ID of the object that the legacy URL alias points to. This is only defined when the outcome is `'aliasMatch'` or `'conflict'`. Signature: ```typescript -aliasTargetId?: string; +alias_target_id?: string; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.md index 8a2504ec7adcc..bbffd9902c0e7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsresolveresponse.md @@ -15,7 +15,7 @@ export interface SavedObjectsResolveResponse | Property | Type | Description | | --- | --- | --- | -| [aliasTargetId](./kibana-plugin-core-server.savedobjectsresolveresponse.aliastargetid.md) | string | The ID of the object that the legacy URL alias points to. This is only defined when the outcome is 'aliasMatch' or 'conflict'. | +| [alias\_target\_id](./kibana-plugin-core-server.savedobjectsresolveresponse.alias_target_id.md) | string | The ID of the object that the legacy URL alias points to. This is only defined when the outcome is 'aliasMatch' or 'conflict'. | | [outcome](./kibana-plugin-core-server.savedobjectsresolveresponse.outcome.md) | 'exactMatch' | 'aliasMatch' | 'conflict' | The outcome for a successful resolve call is one of the following values:\* 'exactMatch' -- One document exactly matched the given ID. \* 'aliasMatch' -- One document with a legacy URL alias matched the given ID; in this case the saved_object.id field is different than the given ID. \* 'conflict' -- Two documents matched the given ID, one was an exact match and another with a legacy URL alias; in this case the saved_object object is the exact match, and the saved_object.id field is the same as the given ID. | | [saved\_object](./kibana-plugin-core-server.savedobjectsresolveresponse.saved_object.md) | SavedObject<T> | The saved object that was found. | diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 9de10c3c88534..5ce12a1889c26 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1149,9 +1149,9 @@ export type ResolveDeprecationResponse = { // @public export interface ResolvedSimpleSavedObject { - aliasTargetId?: SavedObjectsResolveResponse['aliasTargetId']; + alias_target_id?: SavedObjectsResolveResponse['alias_target_id']; outcome: SavedObjectsResolveResponse['outcome']; - savedObject: SimpleSavedObject; + saved_object: SimpleSavedObject; } // Warning: (ae-missing-release-tag) "SavedObject" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1506,7 +1506,7 @@ export type SavedObjectsNamespaceType = 'single' | 'multiple' | 'multiple-isolat // @public (undocumented) export interface SavedObjectsResolveResponse { - aliasTargetId?: string; + alias_target_id?: string; outcome: 'exactMatch' | 'aliasMatch' | 'conflict'; saved_object: SavedObject; } diff --git a/src/core/public/saved_objects/saved_objects_client.test.ts b/src/core/public/saved_objects/saved_objects_client.test.ts index 85441b9841eaf..2eed9615430e9 100644 --- a/src/core/public/saved_objects/saved_objects_client.test.ts +++ b/src/core/public/saved_objects/saved_objects_client.test.ts @@ -155,7 +155,7 @@ describe('SavedObjectsClient', () => { http.fetch.mockResolvedValue({ saved_object: doc, outcome: 'conflict', - aliasTargetId: 'another-id', + alias_target_id: 'another-id', } as SavedObjectsResolveResponse); }); }); @@ -197,11 +197,11 @@ describe('SavedObjectsClient', () => { test('resolves with ResolvedSimpleSavedObject instance', async () => { const result = await savedObjectsClient.resolve(doc.type, doc.id); - expect(result.savedObject).toBeInstanceOf(SimpleSavedObject); - expect(result.savedObject.type).toBe(doc.type); - expect(result.savedObject.get('title')).toBe('Example title'); + expect(result.saved_object).toBeInstanceOf(SimpleSavedObject); + expect(result.saved_object.type).toBe(doc.type); + expect(result.saved_object.get('title')).toBe('Example title'); expect(result.outcome).toBe('conflict'); - expect(result.aliasTargetId).toBe('another-id'); + expect(result.alias_target_id).toBe('another-id'); }); }); diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index cb9fca8c8c757..2e4c25035dace 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -441,9 +441,13 @@ export class SavedObjectsClient { const path = `${this.getPath(['resolve'])}/${type}/${id}`; const request: Promise> = this.savedObjectsFetch(path, {}); - return request.then(({ saved_object: object, outcome, aliasTargetId }) => { - const savedObject = new SimpleSavedObject(this, object); - return { savedObject, outcome, aliasTargetId }; + return request.then((resolveResponse) => { + const simpleSavedObject = new SimpleSavedObject(this, resolveResponse.saved_object); + return { + saved_object: simpleSavedObject, + outcome: resolveResponse.outcome, + alias_target_id: resolveResponse.alias_target_id, + }; }); }; diff --git a/src/core/public/saved_objects/types.ts b/src/core/public/saved_objects/types.ts index ac3df16730125..1251e75b5d6e2 100644 --- a/src/core/public/saved_objects/types.ts +++ b/src/core/public/saved_objects/types.ts @@ -19,7 +19,7 @@ export interface ResolvedSimpleSavedObject { /** * The saved object that was found. */ - savedObject: SimpleSavedObject; + saved_object: SimpleSavedObject; /** * The outcome for a successful `resolve` call is one of the following values: * @@ -33,5 +33,5 @@ export interface ResolvedSimpleSavedObject { /** * The ID of the object that the legacy URL alias points to. This is only defined when the outcome is `'aliasMatch'` or `'conflict'`. */ - aliasTargetId?: SavedObjectsResolveResponse['aliasTargetId']; + alias_target_id?: SavedObjectsResolveResponse['alias_target_id']; } 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 c025adce29808..eead42db1ec58 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -3512,7 +3512,7 @@ describe('SavedObjectsRepository', () => { expect(result).toEqual({ saved_object: expect.objectContaining({ type, id: aliasTargetId }), outcome: 'aliasMatch', - aliasTargetId, + alias_target_id: aliasTargetId, }); }; @@ -3554,7 +3554,7 @@ describe('SavedObjectsRepository', () => { expect(result).toEqual({ saved_object: expect.objectContaining({ type, id }), outcome: 'conflict', - aliasTargetId, + alias_target_id: aliasTargetId, }); }); }); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 7ac4fe87bfc19..17b0f10ef67c8 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -1146,7 +1146,7 @@ export class SavedObjectsRepository { // @ts-expect-error MultiGetHit._source is optional saved_object: getSavedObjectFromSource(this._registry, type, id, exactMatchDoc), outcome: 'conflict', - aliasTargetId: legacyUrlAlias.targetId, + alias_target_id: legacyUrlAlias.targetId, }; outcomeStatString = REPOSITORY_RESOLVE_OUTCOME_STATS.CONFLICT; } else if (foundExactMatch) { @@ -1166,7 +1166,7 @@ export class SavedObjectsRepository { aliasMatchDoc ), outcome: 'aliasMatch', - aliasTargetId: legacyUrlAlias.targetId, + alias_target_id: legacyUrlAlias.targetId, }; outcomeStatString = REPOSITORY_RESOLVE_OUTCOME_STATS.ALIAS_MATCH; } 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 abb86d8120a9b..00d47d8d1fb03 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -328,7 +328,7 @@ export interface SavedObjectsResolveResponse { /** * The ID of the object that the legacy URL alias points to. This is only defined when the outcome is `'aliasMatch'` or `'conflict'`. */ - aliasTargetId?: string; + alias_target_id?: string; } /** diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 985e0e337d75c..b18479af23bb2 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -3029,7 +3029,7 @@ export interface SavedObjectsResolveImportErrorsOptions { // @public (undocumented) export interface SavedObjectsResolveResponse { - aliasTargetId?: string; + alias_target_id?: string; outcome: 'exactMatch' | 'aliasMatch' | 'conflict'; saved_object: SavedObject; } diff --git a/src/plugins/spaces_oss/public/api.ts b/src/plugins/spaces_oss/public/api.ts index b1b6a16958dbd..7492142f0d792 100644 --- a/src/plugins/spaces_oss/public/api.ts +++ b/src/plugins/spaces_oss/public/api.ts @@ -100,7 +100,7 @@ export interface SpacesApiUiComponent { * that there is a conflict, and it includes a button that will redirect the user to object B when clicked. * * Consumers need to determine the local path for the new URL on their own, based on the object ID that was used to call - * `SavedObjectsClient.resolve()` (A) and the `aliasTargetId` value in the response (B). For example... + * `SavedObjectsClient.resolve()` (A) and the `alias_target_id` value in the response (B). For example... * * A is `workpad-123` and B is `workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e`. * diff --git a/x-pack/test/saved_object_api_integration/common/suites/resolve.ts b/x-pack/test/saved_object_api_integration/common/suites/resolve.ts index 47aafc400ce76..bfaeff7f366a4 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/resolve.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/resolve.ts @@ -85,9 +85,9 @@ export function resolveTestSuiteFactory(esArchiver: any, supertest: SuperTest Date: Tue, 10 Aug 2021 22:07:36 +0200 Subject: [PATCH 046/104] [APM] Add telemetry to links into backend views (#107872) --- .../index.tsx | 12 +++++++++ .../service_map/Popover/backend_contents.tsx | 17 ++++++++++++- .../index.tsx | 11 ++++++++ .../span_flyout/sticky_span_properties.tsx | 13 ++++++++++ .../public/components/shared/backend_link.tsx | 3 +++ x-pack/plugins/apm/public/plugin.ts | 25 ++++++++++++++++--- .../shared/page_template/page_template.tsx | 4 +++ .../public/services/navigation_registry.ts | 2 ++ 8 files changed, 83 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx index c89526b332084..eff744fa45aee 100644 --- a/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; +import { METRIC_TYPE } from '@kbn/analytics'; import { useApmRouter } from '../../../../hooks/use_apm_router'; import { getNodeName, NodeType } from '../../../../../common/connections'; import { useApmParams } from '../../../../hooks/use_apm_params'; @@ -16,6 +17,7 @@ import { getTimeRangeComparison } from '../../../shared/time_comparison/get_time import { DependenciesTable } from '../../../shared/dependencies_table'; import { BackendLink } from '../../../shared/backend_link'; import { DependenciesTableServiceMapLink } from '../../../shared/dependencies_table/dependencies_table_service_map_link'; +import { useUiTracker } from '../../../../../../observability/public'; export function BackendInventoryDependenciesTable() { const { @@ -27,6 +29,9 @@ export function BackendInventoryDependenciesTable() { } = useApmParams('/backends'); const router = useApmRouter(); + + const trackEvent = useUiTracker(); + const serviceMapLink = router.link('/service-map', { query: { rangeFrom, @@ -79,6 +84,13 @@ export function BackendInventoryDependenciesTable() { rangeFrom, rangeTo, }} + onClick={() => { + trackEvent({ + app: 'apm', + metricType: METRIC_TYPE.CLICK, + metric: 'backend_inventory_to_backend_detail', + }); + }} /> ); diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx index e0fef269f3faf..43308cfdce9f1 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx @@ -8,7 +8,9 @@ import { EuiButton, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { TypeOf } from '@kbn/typed-react-router-config'; +import { METRIC_TYPE } from '@kbn/analytics'; import React from 'react'; +import { useUiTracker } from '../../../../../../observability/public'; import { ContentsProps } from '.'; import { NodeStats } from '../../../../../common/service_map'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; @@ -58,13 +60,26 @@ export function BackendContents({ nodeData }: ContentsProps) { >['query'], }); + const trackEvent = useUiTracker(); + return ( <> - + {/* eslint-disable-next-line @elastic/eui/href-or-on-click*/} + { + trackEvent({ + app: 'apm', + metricType: METRIC_TYPE.CLICK, + metric: 'service_map_to_backend_detail', + }); + }} + > {i18n.translate('xpack.apm.serviceMap.backendDetailsButtonText', { defaultMessage: 'Backend Details', })} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx index d9df9acf9ff65..085c69bb9eecf 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx @@ -8,6 +8,8 @@ import { EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; +import { METRIC_TYPE } from '@kbn/analytics'; +import { useUiTracker } from '../../../../../../observability/public'; import { useApmRouter } from '../../../../hooks/use_apm_router'; import { getNodeName, NodeType } from '../../../../../common/connections'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; @@ -54,6 +56,8 @@ export function ServiceOverviewDependenciesTable() { query, }); + const trackEvent = useUiTracker(); + const { data, status } = useFetcher( (callApmApi) => { if (!start || !end) { @@ -89,6 +93,13 @@ export function ServiceOverviewDependenciesTable() { rangeFrom, rangeTo, }} + onClick={() => { + trackEvent({ + app: 'apm', + metricType: METRIC_TYPE.CLICK, + metric: 'service_dependencies_to_backend_detail', + }); + }} /> ) : ( { + trackEvent({ + app: 'apm', + metricType: METRIC_TYPE.CLICK, + metric: 'span_flyout_to_backend_detail', + }); + }} /> ), width: '25%', diff --git a/x-pack/plugins/apm/public/components/shared/backend_link.tsx b/x-pack/plugins/apm/public/components/shared/backend_link.tsx index 84ce37391b369..caae47184510a 100644 --- a/x-pack/plugins/apm/public/components/shared/backend_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/backend_link.tsx @@ -21,6 +21,7 @@ interface BackendLinkProps { query: TypeOf['query']; subtype?: string; type?: string; + onClick?: React.ComponentProps['onClick']; } export function BackendLink({ @@ -28,6 +29,7 @@ export function BackendLink({ query, subtype, type, + onClick, }: BackendLinkProps) { const { link } = useApmRouter(); @@ -37,6 +39,7 @@ export function BackendLink({ path: { backendName }, query, })} + onClick={onClick} > diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 8175f46715b8f..0631dba7e2a34 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { from } from 'rxjs'; import { map } from 'rxjs/operators'; +import { UsageCollectionStart } from 'src/plugins/usage_collection/public'; import type { ConfigSchema } from '.'; import { AppMountParameters, @@ -32,9 +33,10 @@ import type { FeaturesPluginSetup } from '../../features/public'; import type { LicensingPluginSetup } from '../../licensing/public'; import type { MapsStartApi } from '../../maps/public'; import type { MlPluginSetup, MlPluginStart } from '../../ml/public'; -import type { +import { FetchDataParams, HasDataParams, + METRIC_TYPE, ObservabilityPublicSetup, ObservabilityPublicStart, } from '../../observability/public'; @@ -112,7 +114,7 @@ export class ApmPlugin implements Plugin { // register observability nav if user has access to plugin plugins.observability.navigation.registerSections( from(core.getStartServices()).pipe( - map(([coreStart]) => { + map(([coreStart, pluginsStart]) => { if (coreStart.application.capabilities.apm.show) { return [ // APM navigation @@ -123,7 +125,24 @@ export class ApmPlugin implements Plugin { { label: servicesTitle, app: 'apm', path: '/services' }, { label: tracesTitle, app: 'apm', path: '/traces' }, { label: serviceMapTitle, app: 'apm', path: '/service-map' }, - { label: backendsTitle, app: 'apm', path: '/backends' }, + { + label: backendsTitle, + app: 'apm', + path: '/backends', + onClick: () => { + const { usageCollection } = pluginsStart as { + usageCollection?: UsageCollectionStart; + }; + + if (usageCollection) { + usageCollection.reportUiCounter( + 'apm', + METRIC_TYPE.CLICK, + 'side_nav_backend' + ); + } + }, + }, ], }, diff --git a/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx b/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx index 896aca79114d7..61feba83431f5 100644 --- a/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx +++ b/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx @@ -78,6 +78,10 @@ export function ObservabilityPageTemplate({ href, isSelected, onClick: (event) => { + if (entry.onClick) { + entry.onClick(event); + } + if ( event.button !== 0 || event.defaultPrevented || diff --git a/x-pack/plugins/observability/public/services/navigation_registry.ts b/x-pack/plugins/observability/public/services/navigation_registry.ts index 79a36731f7ed1..4789e4c5ea574 100644 --- a/x-pack/plugins/observability/public/services/navigation_registry.ts +++ b/x-pack/plugins/observability/public/services/navigation_registry.ts @@ -28,6 +28,8 @@ export interface NavigationEntry { matchFullPath?: boolean; // whether to ignore trailing slashes, defaults to `true` ignoreTrailingSlash?: boolean; + // handler to be called when the item is clicked + onClick?: (event: React.MouseEvent) => void; } export interface NavigationRegistry { From baa903d539acfceb87ba0c71f09f6496719614a1 Mon Sep 17 00:00:00 2001 From: Sandra G Date: Tue, 10 Aug 2021 16:12:53 -0400 Subject: [PATCH 047/104] move rules creation out of closing popover (#107957) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/monitoring/public/alerts/alerts_dropdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/monitoring/public/alerts/alerts_dropdown.tsx b/x-pack/plugins/monitoring/public/alerts/alerts_dropdown.tsx index df0cbb43f8569..c7f81d999cd02 100644 --- a/x-pack/plugins/monitoring/public/alerts/alerts_dropdown.tsx +++ b/x-pack/plugins/monitoring/public/alerts/alerts_dropdown.tsx @@ -23,7 +23,6 @@ export const AlertsDropdown: React.FC<{}> = () => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const closePopover = () => { - alertsEnableModalProvider.enableAlerts(); setIsPopoverOpen(false); }; @@ -32,6 +31,7 @@ export const AlertsDropdown: React.FC<{}> = () => { }; const createDefaultRules = () => { + alertsEnableModalProvider.enableAlerts(); closePopover(); }; From 565e07104c8c8a812d9dd03c63121599d6fa4e5d Mon Sep 17 00:00:00 2001 From: ymao1 Date: Tue, 10 Aug 2021 16:18:10 -0400 Subject: [PATCH 048/104] [Alerting UI] Not showing edit button in rule management UI if rule is not editable in UI (#107801) * Should not show edit button on rule management page if rule not editable in stack * Disabling edit button in collapsed actions * Adding tests for collapsed item actions component * Cleanup Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/alerts_list.test.tsx | 35 +- .../alerts_list/components/alerts_list.tsx | 42 ++- .../collapsed_item_actions.test.tsx | 328 ++++++++++++++++++ .../components/collapsed_item_actions.tsx | 19 +- 4 files changed, 400 insertions(+), 24 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.test.tsx diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index 5a8a9000ea5b9..0654488939566 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -13,7 +13,7 @@ import { act } from 'react-dom/test-utils'; import { actionTypeRegistryMock } from '../../../action_type_registry.mock'; import { ruleTypeRegistryMock } from '../../../rule_type_registry.mock'; import { AlertsList } from './alerts_list'; -import { ValidationResult } from '../../../../types'; +import { AlertTypeModel, ValidationResult } from '../../../../types'; import { AlertExecutionStatusErrorReasons, ALERTS_FEATURE_ID, @@ -44,6 +44,12 @@ jest.mock('react-router-dom', () => ({ pathname: '/triggersActions/alerts/', }), })); +jest.mock('../../../lib/capabilities', () => ({ + hasAllPrivilege: jest.fn(() => true), + hasSaveAlertsCapability: jest.fn(() => true), + hasShowActionsCapability: jest.fn(() => true), + hasExecuteActionsCapability: jest.fn(() => true), +})); const { loadAlerts, loadAlertTypes } = jest.requireMock('../../../lib/alert_api'); const { loadActionTypes, loadAllActions } = jest.requireMock('../../../lib/action_connector_api'); const actionTypeRegistry = actionTypeRegistryMock.create(); @@ -264,7 +270,7 @@ describe('alerts_list component with items', () => { }, ]; - async function setup() { + async function setup(editable: boolean = true) { loadAlerts.mockResolvedValue({ page: 1, perPage: 10000, @@ -284,7 +290,20 @@ describe('alerts_list component with items', () => { loadAlertTypes.mockResolvedValue([alertTypeFromApi]); loadAllActions.mockResolvedValue([]); + const ruleTypeMock: AlertTypeModel = { + id: 'test_alert_type', + iconClass: 'test', + description: 'Alert when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + alertParamsExpression: jest.fn(), + requiresAppContext: !editable, + }; + ruleTypeRegistry.has.mockReturnValue(true); + ruleTypeRegistry.get.mockReturnValue(ruleTypeMock); // eslint-disable-next-line react-hooks/rules-of-hooks useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; @@ -408,6 +427,18 @@ describe('alerts_list component with items', () => { }) ); }); + + it('renders edit and delete buttons when user can manage rules', async () => { + await setup(); + expect(wrapper.find('[data-test-subj="alertSidebarEditAction"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="alertSidebarDeleteAction"]').exists()).toBeTruthy(); + }); + + it('does not render edit button when rule type does not allow editing in rules management', async () => { + await setup(false); + expect(wrapper.find('[data-test-subj="alertSidebarEditAction"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="alertSidebarDeleteAction"]').exists()).toBeTruthy(); + }); }); describe('alerts_list component empty with show only capability', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 3625dc07a1181..9b488922a3cf6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -50,7 +50,7 @@ import { deleteAlerts, } from '../../../lib/alert_api'; import { loadActionTypes } from '../../../lib/action_connector_api'; -import { hasExecuteActionsCapability } from '../../../lib/capabilities'; +import { hasAllPrivilege, hasExecuteActionsCapability } from '../../../lib/capabilities'; import { routeToRuleDetails, DEFAULT_SEARCH_PAGE_SIZE } from '../../../constants'; import { DeleteModalConfirmation } from '../../../components/delete_modal_confirmation'; import { EmptyPrompt } from '../../../components/prompts/empty_prompt'; @@ -60,7 +60,6 @@ import { ALERTS_FEATURE_ID, AlertExecutionStatusErrorReasons, } from '../../../../../../alerting/common'; -import { hasAllPrivilege } from '../../../lib/capabilities'; import { alertsStatusesTranslationsMapping, ALERT_STATUS_LICENSE_ERROR } from '../translations'; import { useKibana } from '../../../../common/lib/kibana'; import { DEFAULT_HIDDEN_ACTION_TYPES } from '../../../../common/constants'; @@ -143,6 +142,9 @@ export const AlertsList: React.FunctionComponent = () => { setCurrentRuleToEdit(ruleItem); }; + const isRuleTypeEditableInContext = (ruleTypeId: string) => + ruleTypeRegistry.has(ruleTypeId) ? !ruleTypeRegistry.get(ruleTypeId).requiresAppContext : false; + useEffect(() => { loadAlertsData(); }, [ @@ -466,23 +468,25 @@ export const AlertsList: React.FunctionComponent = () => { - - onRuleEdit(item)} - iconType={'pencil'} - aria-label={i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.editAriaLabel', - { defaultMessage: 'Edit' } - )} - /> - - + {item.isEditable && isRuleTypeEditableInContext(item.alertTypeId) && ( + + onRuleEdit(item)} + iconType={'pencil'} + aria-label={i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.editAriaLabel', + { defaultMessage: 'Edit' } + )} + /> + + )} + + new Promise((resolve) => { + setTimeout(resolve, ms); + }); + +describe('CollapsedItemActions', () => { + async function setup(editable: boolean = true) { + const ruleTypeRegistry = ruleTypeRegistryMock.create(); + ruleTypeRegistry.has.mockReturnValue(true); + const alertTypeR: AlertTypeModel = { + id: 'my-alert-type', + iconClass: 'test', + description: 'Alert when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + alertParamsExpression: jest.fn(), + requiresAppContext: !editable, + }; + ruleTypeRegistry.get.mockReturnValue(alertTypeR); + const useKibanaMock = useKibana as jest.Mocked; + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry; + } + + const getPropsWithRule = (overrides = {}, editable = false) => { + const rule: AlertTableItem = { + id: '1', + enabled: true, + name: 'test rule', + tags: ['tag1'], + alertTypeId: 'test_rule_type', + consumer: 'alerts', + schedule: { interval: '5d' }, + actions: [ + { id: 'test', actionTypeId: 'the_connector', group: 'rule', params: { message: 'test' } }, + ], + params: { name: 'test rule type name' }, + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + apiKeyOwner: null, + throttle: '1m', + notifyWhen: 'onActiveAlert', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'active', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, + actionsCount: 1, + tagsText: 'tag1', + alertType: 'Test Alert Type', + isEditable: true, + enabledInLicense: true, + ...overrides, + }; + + return { + item: rule, + onAlertChanged, + onEditAlert, + setAlertsToDelete, + disableAlert, + enableAlert, + unmuteAlert, + muteAlert, + }; + }; + + test('renders closed popover initially and opens on click with all actions enabled', async () => { + await setup(); + const wrapper = mountWithIntl(); + + expect(wrapper.find('[data-test-subj="selectActionButton"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="collapsedActionPanel"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="muteButton"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="disableButton"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="editAlert"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="deleteAlert"]').exists()).toBeFalsy(); + + wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="collapsedActionPanel"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="muteButton"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="disableButton"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="editAlert"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="deleteAlert"]').exists()).toBeTruthy(); + + expect(wrapper.find(`[data-test-subj="muteButton"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="muteButton"] button`).text()).toEqual('Mute'); + expect(wrapper.find(`[data-test-subj="disableButton"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="disableButton"] button`).text()).toEqual('Disable'); + expect(wrapper.find(`[data-test-subj="editAlert"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="editAlert"] button`).text()).toEqual('Edit rule'); + expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).text()).toEqual('Delete rule'); + }); + + test('handles case when rule is unmuted and enabled and mute is clicked', async () => { + await setup(); + const wrapper = mountWithIntl(); + wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + wrapper.find('button[data-test-subj="muteButton"]').simulate('click'); + await act(async () => { + await tick(10); + wrapper.update(); + }); + expect(muteAlert).toHaveBeenCalled(); + }); + + test('handles case when rule is unmuted and enabled and disable is clicked', async () => { + await setup(); + const wrapper = mountWithIntl(); + wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + wrapper.find('button[data-test-subj="disableButton"]').simulate('click'); + await act(async () => { + await tick(10); + wrapper.update(); + }); + expect(disableAlert).toHaveBeenCalled(); + }); + + test('handles case when rule is muted and enabled and unmute is clicked', async () => { + await setup(); + const wrapper = mountWithIntl( + + ); + wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + wrapper.find('button[data-test-subj="muteButton"]').simulate('click'); + await act(async () => { + await tick(10); + wrapper.update(); + }); + expect(unmuteAlert).toHaveBeenCalled(); + }); + + test('handles case when rule is unmuted and disabled and enable is clicked', async () => { + await setup(); + const wrapper = mountWithIntl( + + ); + wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + wrapper.find('button[data-test-subj="disableButton"]').simulate('click'); + await act(async () => { + await tick(10); + wrapper.update(); + }); + expect(enableAlert).toHaveBeenCalled(); + }); + + test('handles case when edit rule is clicked', async () => { + await setup(); + const wrapper = mountWithIntl(); + wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + wrapper.find('button[data-test-subj="editAlert"]').simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + expect(onEditAlert).toHaveBeenCalled(); + }); + + test('handles case when delete rule is clicked', async () => { + await setup(); + const wrapper = mountWithIntl(); + wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + wrapper.find('button[data-test-subj="deleteAlert"]').simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + expect(setAlertsToDelete).toHaveBeenCalled(); + }); + + test('renders actions correctly when rule is disabled', async () => { + await setup(); + const wrapper = mountWithIntl( + + ); + wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(`[data-test-subj="muteButton"] button`).prop('disabled')).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="muteButton"] button`).text()).toEqual('Mute'); + expect(wrapper.find(`[data-test-subj="disableButton"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="disableButton"] button`).text()).toEqual('Enable'); + expect(wrapper.find(`[data-test-subj="editAlert"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="editAlert"] button`).text()).toEqual('Edit rule'); + expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).text()).toEqual('Delete rule'); + }); + + test('renders actions correctly when rule is not editable', async () => { + await setup(); + const wrapper = mountWithIntl( + + ); + wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect( + wrapper.find(`[data-test-subj="selectActionButton"] button`).prop('disabled') + ).toBeTruthy(); + }); + + test('renders actions correctly when rule is not enabled due to license', async () => { + await setup(); + const wrapper = mountWithIntl( + + ); + wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(`[data-test-subj="muteButton"] button`).prop('disabled')).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="muteButton"] button`).text()).toEqual('Mute'); + expect(wrapper.find(`[data-test-subj="disableButton"] button`).prop('disabled')).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="disableButton"] button`).text()).toEqual('Disable'); + expect(wrapper.find(`[data-test-subj="editAlert"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="editAlert"] button`).text()).toEqual('Edit rule'); + expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).text()).toEqual('Delete rule'); + }); + + test('renders actions correctly when rule is muted', async () => { + await setup(); + const wrapper = mountWithIntl( + + ); + wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(`[data-test-subj="muteButton"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="muteButton"] button`).text()).toEqual('Unmute'); + expect(wrapper.find(`[data-test-subj="disableButton"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="disableButton"] button`).text()).toEqual('Disable'); + expect(wrapper.find(`[data-test-subj="editAlert"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="editAlert"] button`).text()).toEqual('Edit rule'); + expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).text()).toEqual('Delete rule'); + }); + + test('renders actions correctly when rule type is not editable in this context', async () => { + await setup(false); + const wrapper = mountWithIntl(); + wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(`[data-test-subj="muteButton"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="muteButton"] button`).text()).toEqual('Mute'); + expect(wrapper.find(`[data-test-subj="disableButton"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="disableButton"] button`).text()).toEqual('Disable'); + expect(wrapper.find(`[data-test-subj="editAlert"] button`).prop('disabled')).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="editAlert"] button`).text()).toEqual('Edit rule'); + expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).prop('disabled')).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="deleteAlert"] button`).text()).toEqual('Delete rule'); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx index b4bf4e786bca3..e2870e8097946 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.tsx @@ -10,6 +10,7 @@ import { asyncScheduler } from 'rxjs'; import React, { useEffect, useState } from 'react'; import { EuiButtonIcon, EuiPopover, EuiContextMenu } from '@elastic/eui'; +import { useKibana } from '../../../../common/lib/kibana'; import { AlertTableItem } from '../../../../types'; import { ComponentOpts as BulkOperationsComponentOpts, @@ -22,7 +23,7 @@ export type ComponentOpts = { onAlertChanged: () => void; setAlertsToDelete: React.Dispatch>; onEditAlert: (item: AlertTableItem) => void; -} & BulkOperationsComponentOpts; +} & Pick; export const CollapsedItemActions: React.FunctionComponent = ({ item, @@ -34,6 +35,8 @@ export const CollapsedItemActions: React.FunctionComponent = ({ setAlertsToDelete, onEditAlert, }: ComponentOpts) => { + const { ruleTypeRegistry } = useKibana().services; + const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isDisabled, setIsDisabled] = useState(!item.enabled); const [isMuted, setIsMuted] = useState(item.muteAll); @@ -42,9 +45,14 @@ export const CollapsedItemActions: React.FunctionComponent = ({ setIsMuted(item.muteAll); }, [item.enabled, item.muteAll]); + const isRuleTypeEditableInContext = ruleTypeRegistry.has(item.alertTypeId) + ? !ruleTypeRegistry.get(item.alertTypeId).requiresAppContext + : false; + const button = ( setIsPopoverOpen(!isPopoverOpen)} aria-label={i18n.translate( @@ -112,7 +120,7 @@ export const CollapsedItemActions: React.FunctionComponent = ({ ), }, { - disabled: !item.isEditable, + disabled: !item.isEditable || !isRuleTypeEditableInContext, 'data-test-subj': 'editAlert', onClick: () => { setIsPopoverOpen(!isPopoverOpen); @@ -148,7 +156,12 @@ export const CollapsedItemActions: React.FunctionComponent = ({ panelPaddingSize="none" data-test-subj="collapsedItemActions" > - + ); }; From ca2a591526498e8036191285e325b40cb1faa2b8 Mon Sep 17 00:00:00 2001 From: Ryan Keairns Date: Tue, 10 Aug 2021 16:02:58 -0500 Subject: [PATCH 049/104] Add monaco default style options (#107930) * Add monaco default style options * Update snapshot Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/code_editor/__snapshots__/code_editor.test.tsx.snap | 3 +++ src/plugins/kibana_react/public/code_editor/code_editor.tsx | 3 +++ .../public/components/expression_input/expression_input.tsx | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap b/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap index d4fb5a708e440..230d31ac9e0b6 100644 --- a/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap +++ b/src/plugins/kibana_react/public/code_editor/__snapshots__/code_editor.test.tsx.snap @@ -11,6 +11,9 @@ exports[`is rendered 1`] = ` onChange={[Function]} options={ Object { + "fontFamily": "Roboto Mono", + "fontSize": 12, + "lineHeight": 21, "matchBrackets": "never", "minimap": Object { "enabled": false, diff --git a/src/plugins/kibana_react/public/code_editor/code_editor.tsx b/src/plugins/kibana_react/public/code_editor/code_editor.tsx index 6b02d8d159617..e233b4468b0c6 100644 --- a/src/plugins/kibana_react/public/code_editor/code_editor.tsx +++ b/src/plugins/kibana_react/public/code_editor/code_editor.tsx @@ -193,6 +193,9 @@ export class CodeEditor extends React.Component { wordWrap: 'on', wrappingIndent: 'indent', matchBrackets: 'never', + fontFamily: 'Roboto Mono', + fontSize: 12, + lineHeight: 21, ...options, }} /> diff --git a/x-pack/plugins/canvas/public/components/expression_input/expression_input.tsx b/x-pack/plugins/canvas/public/components/expression_input/expression_input.tsx index 53ba7996eb641..a86d5c676166d 100644 --- a/x-pack/plugins/canvas/public/components/expression_input/expression_input.tsx +++ b/x-pack/plugins/canvas/public/components/expression_input/expression_input.tsx @@ -318,7 +318,7 @@ export class ExpressionInput extends React.Component { provideHover: this.providerHover, }} options={{ - fontSize: isCompact ? 12 : 16, + fontSize: isCompact ? 12 : 14, scrollBeyondLastLine: false, quickSuggestions: true, minimap: { From 2bf9ae8b290b3dc3557d15729dd6b326f222a8ae Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Tue, 10 Aug 2021 17:05:19 -0400 Subject: [PATCH 050/104] [Security Solution][Bug] - Disable alert table RBAC until fields sorted (#108034) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/search_strategy/timeline/index.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts index dfba32f8a238c..e419009354b42 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts @@ -9,7 +9,8 @@ import { ALERT_OWNER, RULE_ID, SPACE_IDS } from '@kbn/rule-data-utils'; import { map, mergeMap, catchError } from 'rxjs/operators'; import { from } from 'rxjs'; import { - isValidFeatureId, + // TODO: Undo comment in fix here https://github.com/elastic/kibana/pull/107857 + // isValidFeatureId, mapConsumerToIndexName, AlertConsumers, } from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; @@ -49,7 +50,9 @@ export const timelineSearchStrategyProvider = { const factoryQueryType = request.factoryQueryType; const entityType = request.entityType; - const alertConsumers = request.alertConsumers; + let alertConsumers = request.alertConsumers; + // TODO: Remove in fix here https://github.com/elastic/kibana/pull/107857 + alertConsumers = undefined; if (factoryQueryType == null) { throw new Error('factoryQueryType is required'); @@ -58,7 +61,9 @@ export const timelineSearchStrategyProvider = = timelineFactory[factoryQueryType]; if (alertConsumers != null && entityType != null && entityType === EntityType.ALERTS) { - const allFeatureIdsValid = alertConsumers.every((id) => isValidFeatureId(id)); + // TODO: Thist won't be hit since alertConsumers = undefined + // TODO: remove in fix here https://github.com/elastic/kibana/pull/107857 + const allFeatureIdsValid = null; // alertConsumers.every((id) => isValidFeatureId(id)); if (!allFeatureIdsValid) { throw new Error('An invalid alerts consumer feature id was provided'); From 422f64c498469f31b850796e305d1a7bd33abcde Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 10 Aug 2021 17:09:08 -0400 Subject: [PATCH 051/104] [Fleet] Fix all category count (#108089) --- .../sections/epm/screens/home/index.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx index 74572e74dd508..b260bba493e71 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx @@ -141,6 +141,9 @@ const AvailablePackages: React.FC = memo(() => { const queryParams = new URLSearchParams(useLocation().search); const initialCategory = queryParams.get('category') || ''; const [selectedCategory, setSelectedCategory] = useState(initialCategory); + const { data: allCategoryPackagesRes, isLoading: isLoadingAllPackages } = useGetPackages({ + category: '', + }); const { data: categoryPackagesRes, isLoading: isLoadingCategoryPackages } = useGetPackages({ category: selectedCategory, }); @@ -152,6 +155,11 @@ const AvailablePackages: React.FC = memo(() => { [categoryPackagesRes] ); + const allPackages = useMemo( + () => packageListToIntegrationsList(allCategoryPackagesRes?.response || []), + [allCategoryPackagesRes] + ); + const title = useMemo( () => i18n.translate('xpack.fleet.epmList.allTitle', { @@ -167,16 +175,16 @@ const AvailablePackages: React.FC = memo(() => { title: i18n.translate('xpack.fleet.epmList.allPackagesFilterLinkText', { defaultMessage: 'All', }), - count: packages?.length || 0, + count: allPackages?.length || 0, }, ...(categoriesRes ? categoriesRes.response : []), ], - [packages?.length, categoriesRes] + [allPackages?.length, categoriesRes] ); const controls = categories ? ( { From 1c9edebf9909710d29cd87d62e1319b6dd942dab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Tue, 10 Aug 2021 23:31:41 +0200 Subject: [PATCH 052/104] [APM] Display throughput as tps (instead of tpm) when bucket size < 60 seconds (#107850) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../apm/common/utils/formatters/duration.ts | 15 +- .../offset_previous_period_coordinate.ts | 5 +- .../cypress/integration/csm_dashboard.feature | 5 - .../service_overview_throughput_chart.tsx | 56 +++++- .../lib/helpers/calculate_throughput.ts | 5 + ...service_error_group_detailed_statistics.ts | 4 +- .../detailed_statistics.ts | 14 +- ...e_transaction_group_detailed_statistics.ts | 8 +- .../apm/server/lib/services/get_throughput.ts | 24 ++- .../lib/transaction_groups/get_error_rate.ts | 6 +- x-pack/plugins/apm/server/routes/services.ts | 15 ++ .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../services/__snapshots__/throughput.snap | 132 +------------- .../tests/services/throughput.ts | 171 ++++++++++++------ 15 files changed, 218 insertions(+), 244 deletions(-) diff --git a/x-pack/plugins/apm/common/utils/formatters/duration.ts b/x-pack/plugins/apm/common/utils/formatters/duration.ts index a068e6ca7f38a..b060f1aa6e005 100644 --- a/x-pack/plugins/apm/common/utils/formatters/duration.ts +++ b/x-pack/plugins/apm/common/utils/formatters/duration.ts @@ -13,6 +13,8 @@ import { asDecimalOrInteger, asInteger, asDecimal } from './formatters'; import { TimeUnit } from './datetime'; import { Maybe } from '../../../typings/common'; import { isFiniteNumber } from '../is_finite_number'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { ThroughputUnit } from '../../../server/lib/helpers/calculate_throughput'; interface FormatterOptions { defaultValue?: string; @@ -161,10 +163,15 @@ export function asTransactionRate(value: Maybe) { } return i18n.translate('xpack.apm.transactionRateLabel', { - defaultMessage: `{value} tpm`, - values: { - value: displayedValue, - }, + defaultMessage: `{displayedValue} tpm`, + values: { displayedValue }, + }); +} + +export function asExactTransactionRate(value: number, unit: ThroughputUnit) { + return i18n.translate('xpack.apm.exactTransactionRateLabel', { + defaultMessage: `{value} { unit, select, minute {tpm} other {tps} }`, + values: { value: asDecimalOrInteger(value), unit }, }); } diff --git a/x-pack/plugins/apm/common/utils/offset_previous_period_coordinate.ts b/x-pack/plugins/apm/common/utils/offset_previous_period_coordinate.ts index 4d095a79394a1..9300ec61fe42d 100644 --- a/x-pack/plugins/apm/common/utils/offset_previous_period_coordinate.ts +++ b/x-pack/plugins/apm/common/utils/offset_previous_period_coordinate.ts @@ -18,10 +18,7 @@ export function offsetPreviousPeriodCoordinates({ if (!previousPeriodTimeseries?.length) { return []; } - const currentPeriodStart = currentPeriodTimeseries?.length - ? currentPeriodTimeseries[0].x - : 0; - + const currentPeriodStart = currentPeriodTimeseries?.[0].x ?? 0; const dateDiff = currentPeriodStart - previousPeriodTimeseries[0].x; return previousPeriodTimeseries.map(({ x, y }) => { diff --git a/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature b/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature index 4ea7c72fbc9ad..4d68089417138 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature +++ b/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature @@ -30,11 +30,6 @@ Feature: CSM Dashboard Then should display percentile for page load chart And should display tooltip on hover - Scenario: Breakdown filter - Given a user clicks the page load breakdown filter - When the user selected the breakdown - Then breakdown series should appear in chart - Scenario: Search by url filter focus When a user clicks inside url search field Then it displays top pages in the suggestion popover diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx index 1d6c538570f8f..5eb130140ec90 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx @@ -5,10 +5,16 @@ * 2.0. */ -import { EuiPanel, EuiTitle } from '@elastic/eui'; +import { + EuiPanel, + EuiTitle, + EuiIconTip, + EuiFlexItem, + EuiFlexGroup, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { asTransactionRate } from '../../../../common/utils/formatters'; +import { asExactTransactionRate } from '../../../../common/utils/formatters'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useFetcher } from '../../../hooks/use_fetcher'; @@ -22,6 +28,7 @@ import { const INITIAL_STATE = { currentPeriod: [], previousPeriod: [], + throughputUnit: 'minute' as const, }; export function ServiceOverviewThroughputChart({ @@ -111,20 +118,49 @@ export function ServiceOverviewThroughputChart({ return ( - -

    - {i18n.translate('xpack.apm.serviceOverview.throughtputChartTitle', { - defaultMessage: 'Throughput', - })} -

    - + + + +

    + {i18n.translate( + 'xpack.apm.serviceOverview.throughtputChartTitle', + { defaultMessage: 'Throughput' } + )} + {data.throughputUnit === 'second' + ? i18n.translate( + 'xpack.apm.serviceOverview.throughtputPerSecondChartTitle', + { defaultMessage: '(per second)' } + ) + : ''} +

    +
    +
    + + + + +
    + asExactTransactionRate(y, data.throughputUnit)} customTheme={comparisonChartTheme} /> diff --git a/x-pack/plugins/apm/server/lib/helpers/calculate_throughput.ts b/x-pack/plugins/apm/server/lib/helpers/calculate_throughput.ts index d09f5d9d3f113..1b9e1b56830af 100644 --- a/x-pack/plugins/apm/server/lib/helpers/calculate_throughput.ts +++ b/x-pack/plugins/apm/server/lib/helpers/calculate_throughput.ts @@ -15,3 +15,8 @@ export function calculateThroughput({ const durationAsMinutes = (end - start) / 1000 / 60; return value / durationAsMinutes; } + +export type ThroughputUnit = 'minute' | 'second'; +export function getThroughputUnit(bucketSize: number): ThroughputUnit { + return bucketSize >= 60 ? 'minute' : 'second'; +} diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_detailed_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_detailed_statistics.ts index 1e0c8e6f60441..d277967dad774 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_detailed_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/get_service_error_group_detailed_statistics.ts @@ -159,7 +159,7 @@ export async function getServiceErrorGroupPeriods({ previousPeriodPromise, ]); - const firtCurrentPeriod = currentPeriod.length ? currentPeriod[0] : undefined; + const firstCurrentPeriod = currentPeriod?.[0]; return { currentPeriod: keyBy(currentPeriod, 'groupId'), @@ -167,7 +167,7 @@ export async function getServiceErrorGroupPeriods({ previousPeriod.map((errorRateGroup) => ({ ...errorRateGroup, timeseries: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: firtCurrentPeriod?.timeseries, + currentPeriodTimeseries: firstCurrentPeriod?.timeseries, previousPeriodTimeseries: errorRateGroup.timeseries, }), })), diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/detailed_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/detailed_statistics.ts index 804ed91c54a51..fc91efbc7c6e6 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_instances/detailed_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/detailed_statistics.ts @@ -123,9 +123,7 @@ export async function getServiceInstancesDetailedStatisticsPeriods({ previousPeriodPromise, ]); - const firtCurrentPeriod = currentPeriod.length - ? currentPeriod[0] - : undefined; + const firstCurrentPeriod = currentPeriod?.[0]; return { currentPeriod: keyBy(currentPeriod, 'serviceNodeName'), @@ -134,23 +132,23 @@ export async function getServiceInstancesDetailedStatisticsPeriods({ return { ...data, cpuUsage: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: firtCurrentPeriod?.cpuUsage, + currentPeriodTimeseries: firstCurrentPeriod?.cpuUsage, previousPeriodTimeseries: data.cpuUsage, }), errorRate: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: firtCurrentPeriod?.errorRate, + currentPeriodTimeseries: firstCurrentPeriod?.errorRate, previousPeriodTimeseries: data.errorRate, }), latency: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: firtCurrentPeriod?.latency, + currentPeriodTimeseries: firstCurrentPeriod?.latency, previousPeriodTimeseries: data.latency, }), memoryUsage: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: firtCurrentPeriod?.memoryUsage, + currentPeriodTimeseries: firstCurrentPeriod?.memoryUsage, previousPeriodTimeseries: data.memoryUsage, }), throughput: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: firtCurrentPeriod?.throughput, + currentPeriodTimeseries: firstCurrentPeriod?.throughput, previousPeriodTimeseries: data.throughput, }), }; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_detailed_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_detailed_statistics.ts index 6edec60b6f373..d39b521e6ae97 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_detailed_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_detailed_statistics.ts @@ -239,7 +239,7 @@ export async function getServiceTransactionGroupDetailedStatisticsPeriods({ previousPeriodPromise, ]); - const firtCurrentPeriod = currentPeriod.length ? currentPeriod[0] : undefined; + const firstCurrentPeriod = currentPeriod?.[0]; return { currentPeriod: keyBy(currentPeriod, 'transactionName'), @@ -248,15 +248,15 @@ export async function getServiceTransactionGroupDetailedStatisticsPeriods({ return { ...data, errorRate: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: firtCurrentPeriod?.errorRate, + currentPeriodTimeseries: firstCurrentPeriod?.errorRate, previousPeriodTimeseries: data.errorRate, }), throughput: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: firtCurrentPeriod?.throughput, + currentPeriodTimeseries: firstCurrentPeriod?.throughput, previousPeriodTimeseries: data.throughput, }), latency: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: firtCurrentPeriod?.latency, + currentPeriodTimeseries: firstCurrentPeriod?.latency, previousPeriodTimeseries: data.latency, }), }; diff --git a/x-pack/plugins/apm/server/lib/services/get_throughput.ts b/x-pack/plugins/apm/server/lib/services/get_throughput.ts index 4004d55da79f6..b7f265daeb465 100644 --- a/x-pack/plugins/apm/server/lib/services/get_throughput.ts +++ b/x-pack/plugins/apm/server/lib/services/get_throughput.ts @@ -16,7 +16,6 @@ import { getDocumentTypeFilterForAggregatedTransactions, getProcessorEventForAggregatedTransactions, } from '../helpers/aggregated_transactions'; -import { getBucketSizeForAggregatedTransactions } from '../helpers/get_bucket_size_for_aggregated_transactions'; import { Setup } from '../helpers/setup_request'; interface Options { @@ -28,9 +27,11 @@ interface Options { transactionType: string; start: number; end: number; + intervalString: string; + throughputUnit: 'minute' | 'second'; } -function fetcher({ +export async function getThroughput({ environment, kuery, searchAggregatedTransactions, @@ -39,13 +40,11 @@ function fetcher({ transactionType, start, end, + intervalString, + throughputUnit, }: Options) { const { apmEventClient } = setup; - const { intervalString } = getBucketSizeForAggregatedTransactions({ - start, - end, - searchAggregatedTransactions, - }); + const filter: ESFilter[] = [ { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: transactionType } }, @@ -79,7 +78,7 @@ function fetcher({ aggs: { throughput: { rate: { - unit: 'minute' as const, + unit: throughputUnit, }, }, }, @@ -88,11 +87,10 @@ function fetcher({ }, }; - return apmEventClient.search('get_throughput_for_service', params); -} - -export async function getThroughput(options: Options) { - const response = await fetcher(options); + const response = await apmEventClient.search( + 'get_throughput_for_service', + params + ); return ( response.aggregations?.timeseries.buckets.map((bucket) => { diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts index b27a54a983734..3184c58ce983a 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts @@ -183,16 +183,14 @@ export async function getErrorRatePeriods({ previousPeriodPromise, ]); - const firtCurrentPeriod = currentPeriod.transactionErrorRate.length - ? currentPeriod.transactionErrorRate - : undefined; + const firstCurrentPeriod = currentPeriod.transactionErrorRate; return { currentPeriod, previousPeriod: { ...previousPeriod, transactionErrorRate: offsetPreviousPeriodCoordinates({ - currentPeriodTimeseries: firtCurrentPeriod, + currentPeriodTimeseries: firstCurrentPeriod, previousPeriodTimeseries: previousPeriod.transactionErrorRate, }), }, diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index f5156fe85fbf5..6509c5764edb8 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -12,6 +12,7 @@ import { uniq } from 'lodash'; import { latencyAggregationTypeRt } from '../../common/latency_aggregation_types'; import { ProfilingValueType } from '../../common/profiling'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; +import { getThroughputUnit } from '../lib/helpers/calculate_throughput'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceAnnotations } from '../lib/services/annotations'; import { getServices } from '../lib/services/get_services'; @@ -43,6 +44,7 @@ import { import { offsetPreviousPeriodCoordinates } from '../../common/utils/offset_previous_period_coordinate'; import { getServicesDetailedStatistics } from '../lib/services/get_services_detailed_statistics'; import { getServiceDependenciesBreakdown } from '../lib/services/get_service_dependencies_breakdown'; +import { getBucketSizeForAggregatedTransactions } from '../lib/helpers/get_bucket_size_for_aggregated_transactions'; const servicesRoute = createApmServerRoute({ endpoint: 'GET /api/apm/services', @@ -451,6 +453,16 @@ const serviceThroughputRoute = createApmServerRoute({ }); const { start, end } = setup; + const { + bucketSize, + intervalString, + } = getBucketSizeForAggregatedTransactions({ + start, + end, + searchAggregatedTransactions, + }); + + const throughputUnit = getThroughputUnit(bucketSize); const commonProps = { environment, @@ -459,6 +471,8 @@ const serviceThroughputRoute = createApmServerRoute({ serviceName, setup, transactionType, + throughputUnit, + intervalString, }; const [currentPeriod, previousPeriod] = await Promise.all([ @@ -482,6 +496,7 @@ const serviceThroughputRoute = createApmServerRoute({ currentPeriodTimeseries: currentPeriod, previousPeriodTimeseries: previousPeriod, }), + throughputUnit, }; }, }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index db3285242cabd..00992a049cbc0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6107,7 +6107,6 @@ "xpack.apm.transactionDurationLabel": "期間", "xpack.apm.transactionErrorRateAlert.name": "トランザクションエラー率しきい値", "xpack.apm.transactionErrorRateAlertTrigger.isAbove": "より大きい", - "xpack.apm.transactionRateLabel": "{value} tpm", "xpack.apm.transactions.latency.chart.95thPercentileLabel": "95 パーセンタイル", "xpack.apm.transactions.latency.chart.99thPercentileLabel": "99 パーセンタイル", "xpack.apm.transactions.latency.chart.averageLabel": "平均", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 1e550ad8f7806..78cd7e41cc4ec 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6141,7 +6141,6 @@ "xpack.apm.transactionDurationLabel": "持续时间", "xpack.apm.transactionErrorRateAlert.name": "事务错误率阈值", "xpack.apm.transactionErrorRateAlertTrigger.isAbove": "高于", - "xpack.apm.transactionRateLabel": "{value} tpm", "xpack.apm.transactions.latency.chart.95thPercentileLabel": "第 95 个百分位", "xpack.apm.transactions.latency.chart.99thPercentileLabel": "第 99 个百分位", "xpack.apm.transactions.latency.chart.averageLabel": "平均值", diff --git a/x-pack/test/apm_api_integration/tests/services/__snapshots__/throughput.snap b/x-pack/test/apm_api_integration/tests/services/__snapshots__/throughput.snap index a52084ffef43b..a27f7047bb9b3 100644 --- a/x-pack/test/apm_api_integration/tests/services/__snapshots__/throughput.snap +++ b/x-pack/test/apm_api_integration/tests/services/__snapshots__/throughput.snap @@ -1,135 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`APM API tests basic apm_8.0.0 Throughput when data is loaded has the correct throughput 1`] = ` -Array [ - Object { - "x": 1627973400000, - "y": 4, - }, - Object { - "x": 1627973460000, - "y": 2, - }, - Object { - "x": 1627973520000, - "y": 3, - }, - Object { - "x": 1627973580000, - "y": 8, - }, - Object { - "x": 1627973640000, - "y": 4, - }, - Object { - "x": 1627973700000, - "y": 4, - }, - Object { - "x": 1627973760000, - "y": 6, - }, - Object { - "x": 1627973820000, - "y": 10, - }, - Object { - "x": 1627973880000, - "y": 8, - }, - Object { - "x": 1627973940000, - "y": 9, - }, - Object { - "x": 1627974000000, - "y": 10, - }, - Object { - "x": 1627974060000, - "y": 11, - }, - Object { - "x": 1627974120000, - "y": 7, - }, - Object { - "x": 1627974180000, - "y": 10, - }, - Object { - "x": 1627974240000, - "y": 12, - }, - Object { - "x": 1627974300000, - "y": 6, - }, - Object { - "x": 1627974360000, - "y": 0, - }, - Object { - "x": 1627974420000, - "y": 4, - }, - Object { - "x": 1627974480000, - "y": 3, - }, - Object { - "x": 1627974540000, - "y": 5, - }, - Object { - "x": 1627974600000, - "y": 5, - }, - Object { - "x": 1627974660000, - "y": 5, - }, - Object { - "x": 1627974720000, - "y": 4, - }, - Object { - "x": 1627974780000, - "y": 7, - }, - Object { - "x": 1627974840000, - "y": 2, - }, - Object { - "x": 1627974900000, - "y": 14, - }, - Object { - "x": 1627974960000, - "y": 3, - }, - Object { - "x": 1627975020000, - "y": 6, - }, - Object { - "x": 1627975080000, - "y": 12, - }, - Object { - "x": 1627975140000, - "y": 8, - }, - Object { - "x": 1627975200000, - "y": 0, - }, -] -`; - -exports[`APM API tests basic apm_8.0.0 Throughput when data is loaded with time comparison has the correct throughput 1`] = ` +exports[`APM API tests basic apm_8.0.0 Throughput when data is loaded with time comparison has the correct throughput in tpm 1`] = ` Object { "currentPeriod": Array [ Object { @@ -263,5 +134,6 @@ Object { "y": 0, }, ], + "throughputUnit": "minute", } `; diff --git a/x-pack/test/apm_api_integration/tests/services/throughput.ts b/x-pack/test/apm_api_integration/tests/services/throughput.ts index c9c7d43762e7e..2ecb9055baee4 100644 --- a/x-pack/test/apm_api_integration/tests/services/throughput.ts +++ b/x-pack/test/apm_api_integration/tests/services/throughput.ts @@ -6,32 +6,39 @@ */ import expect from '@kbn/expect'; -import qs from 'querystring'; -import { first, last } from 'lodash'; +import { first, last, mean } from 'lodash'; import moment from 'moment'; import { isFiniteNumber } from '../../../../plugins/apm/common/utils/is_finite_number'; import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi'; import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { registry } from '../../common/registry'; +import { createApmApiSupertest } from '../../common/apm_api_supertest'; type ThroughputReturn = APIReturnType<'GET /api/apm/services/{serviceName}/throughput'>; export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); + const apmApiSupertest = createApmApiSupertest(getService('supertest')); const archiveName = 'apm_8.0.0'; const metadata = archives_metadata[archiveName]; registry.when('Throughput when data is not loaded', { config: 'basic', archives: [] }, () => { it('handles the empty state', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-java/throughput?${qs.stringify({ - start: metadata.start, - end: metadata.end, - transactionType: 'request', - })}` - ); + const response = await apmApiSupertest({ + endpoint: 'GET /api/apm/services/{serviceName}/throughput', + params: { + path: { + serviceName: 'opbeans-java', + }, + query: { + start: metadata.start, + end: metadata.end, + transactionType: 'request', + }, + }, + }); + expect(response.status).to.be(200); expect(response.body.currentPeriod.length).to.be(0); expect(response.body.previousPeriod.length).to.be(0); @@ -43,46 +50,86 @@ export default function ApiTest({ getService }: FtrProviderContext) { 'Throughput when data is loaded', { config: 'basic', archives: [archiveName] }, () => { - before(async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-java/throughput?${qs.stringify({ - start: metadata.start, - end: metadata.end, - transactionType: 'request', - })}` - ); - throughputResponse = response.body; + describe('when querying without kql filter', () => { + before(async () => { + const response = await apmApiSupertest({ + endpoint: 'GET /api/apm/services/{serviceName}/throughput', + params: { + path: { + serviceName: 'opbeans-java', + }, + query: { + start: metadata.start, + end: metadata.end, + transactionType: 'request', + }, + }, + }); + throughputResponse = response.body; + }); + + it('returns some data', () => { + expect(throughputResponse.currentPeriod.length).to.be.greaterThan(0); + expect(throughputResponse.previousPeriod.length).not.to.be.greaterThan(0); + + const nonNullDataPoints = throughputResponse.currentPeriod.filter(({ y }) => + isFiniteNumber(y) + ); + + expect(nonNullDataPoints.length).to.be.greaterThan(0); + }); + + it('has the correct start date', () => { + expectSnapshot( + new Date(first(throughputResponse.currentPeriod)?.x ?? NaN).toISOString() + ).toMatchInline(`"2021-08-03T06:50:00.000Z"`); + }); + + it('has the correct end date', () => { + expectSnapshot( + new Date(last(throughputResponse.currentPeriod)?.x ?? NaN).toISOString() + ).toMatchInline(`"2021-08-03T07:20:00.000Z"`); + }); + + it('has the correct number of buckets', () => { + expectSnapshot(throughputResponse.currentPeriod.length).toMatchInline(`31`); + }); + + it('has the correct throughput in tpm', () => { + const avg = mean(throughputResponse.currentPeriod.map((d) => d.y)); + expectSnapshot(avg).toMatchInline(`6.19354838709677`); + expectSnapshot(throughputResponse.throughputUnit).toMatchInline(`"minute"`); + }); }); - it('returns some data', () => { - expect(throughputResponse.currentPeriod.length).to.be.greaterThan(0); - expect(throughputResponse.previousPeriod.length).not.to.be.greaterThan(0); - - const nonNullDataPoints = throughputResponse.currentPeriod.filter(({ y }) => - isFiniteNumber(y) - ); - - expect(nonNullDataPoints.length).to.be.greaterThan(0); - }); - - it('has the correct start date', () => { - expectSnapshot( - new Date(first(throughputResponse.currentPeriod)?.x ?? NaN).toISOString() - ).toMatchInline(`"2021-08-03T06:50:00.000Z"`); - }); - - it('has the correct end date', () => { - expectSnapshot( - new Date(last(throughputResponse.currentPeriod)?.x ?? NaN).toISOString() - ).toMatchInline(`"2021-08-03T07:20:00.000Z"`); - }); - - it('has the correct number of buckets', () => { - expectSnapshot(throughputResponse.currentPeriod.length).toMatchInline(`31`); - }); - - it('has the correct throughput', () => { - expectSnapshot(throughputResponse.currentPeriod).toMatch(); + describe('with kql filter to force transaction-based UI', () => { + before(async () => { + const response = await apmApiSupertest({ + endpoint: 'GET /api/apm/services/{serviceName}/throughput', + params: { + path: { + serviceName: 'opbeans-java', + }, + query: { + kuery: 'processor.event : "transaction"', + start: metadata.start, + end: metadata.end, + transactionType: 'request', + }, + }, + }); + throughputResponse = response.body; + }); + + it('has the correct throughput in tps', async () => { + const avgTps = mean(throughputResponse.currentPeriod.map((d) => d.y)); + expectSnapshot(avgTps).toMatchInline(`0.124043715846995`); + expectSnapshot(throughputResponse.throughputUnit).toMatchInline(`"second"`); + + // this tpm value must be similar tp tpm value calculated in the previous spec where metric docs were used + const avgTpm = avgTps * 60; + expectSnapshot(avgTpm).toMatchInline(`7.44262295081967`); + }); }); } ); @@ -92,15 +139,22 @@ export default function ApiTest({ getService }: FtrProviderContext) { { config: 'basic', archives: [archiveName] }, () => { before(async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-java/throughput?${qs.stringify({ - transactionType: 'request', - start: moment(metadata.end).subtract(15, 'minutes').toISOString(), - end: metadata.end, - comparisonStart: metadata.start, - comparisonEnd: moment(metadata.start).add(15, 'minutes').toISOString(), - })}` - ); + const response = await apmApiSupertest({ + endpoint: 'GET /api/apm/services/{serviceName}/throughput', + params: { + path: { + serviceName: 'opbeans-java', + }, + query: { + transactionType: 'request', + start: moment(metadata.end).subtract(15, 'minutes').toISOString(), + end: metadata.end, + comparisonStart: metadata.start, + comparisonEnd: moment(metadata.start).add(15, 'minutes').toISOString(), + }, + }, + }); + throughputResponse = response.body; }); @@ -144,8 +198,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(throughputResponse.previousPeriod.length).toMatchInline(`16`); }); - it('has the correct throughput', () => { + it('has the correct throughput in tpm', () => { expectSnapshot(throughputResponse).toMatch(); + expectSnapshot(throughputResponse.throughputUnit).toMatchInline(`"minute"`); }); } ); From c35215fb0b1188ecba3c00696ae35beb1ac93471 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Aug 2021 16:43:57 -0500 Subject: [PATCH 053/104] Update dependency @elastic/charts to v33.2.2 (#107939) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index ca64a4f9c3440..d7a072d1caef0 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "dependencies": { "@elastic/apm-rum": "^5.8.0", "@elastic/apm-rum-react": "^1.2.11", - "@elastic/charts": "33.2.0", + "@elastic/charts": "33.2.2", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.14", "@elastic/ems-client": "7.14.0", diff --git a/yarn.lock b/yarn.lock index c5f9de5dfe305..5ff06955b63cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1389,10 +1389,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@33.2.0": - version "33.2.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-33.2.0.tgz#2135e86bfdde9796b64c8e58d29b0b193ddf47a9" - integrity sha512-Wk7IbfbxncAznTgMO5crRut7cS2GW8e8k++sg23epa6sT4RAnWwsKsvBojQdx9IvTWYxa+YJt4e7wCs3m5MSfw== +"@elastic/charts@33.2.2": + version "33.2.2" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-33.2.2.tgz#403c46eebe71f4ca7e5c9c1a135eec66869961cc" + integrity sha512-g+z1T8s6m7eySaxcY7R6yqUHUstUtEIH0P4FineKWdZ5L6IkxBNrhM7r0FaddIurNxvBy/SGQorhmFZAksWhiQ== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" From 40766dcc08edc67b050d199c4e39f0c28f2c00c4 Mon Sep 17 00:00:00 2001 From: Constance Date: Tue, 10 Aug 2021 15:01:39 -0700 Subject: [PATCH 054/104] Do not render page header for loading domains (#108078) --- .../crawler/crawler_single_domain.test.tsx | 6 ++++-- .../crawler/crawler_single_domain.tsx | 20 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx index 52e3ca19e69e6..903068e28c39a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx @@ -57,15 +57,17 @@ describe('CrawlerSingleDomain', () => { expect(wrapper.prop('pageHeader').pageTitle).toEqual('https://elastic.co'); }); - it('uses a placeholder for the page title and page chrome if a domain has not been set', () => { + it('does not render a page header and uses placeholder chrome while loading', () => { setMockValues({ ...MOCK_VALUES, + dataLoading: true, domain: null, }); const wrapper = shallow(); - expect(wrapper.prop('pageHeader').pageTitle).toEqual('Loading...'); + expect(wrapper.prop('pageChrome')).toContain('...'); + expect(wrapper.prop('pageHeader')).toBeUndefined(); }); it('contains a crawler status banner', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx index aaa5f0d553983..6419c31cc16ca 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx @@ -32,23 +32,21 @@ export const CrawlerSingleDomain: React.FC = () => { const { fetchDomainData } = useActions(CrawlerSingleDomainLogic); - const displayDomainUrl = domain - ? domain.url - : i18n.translate('xpack.enterpriseSearch.appSearch.crawler.singleDomain.loadingTitle', { - defaultMessage: 'Loading...', - }); - useEffect(() => { fetchDomainData(domainId); }, []); return ( , ], - }} + pageChrome={getEngineBreadcrumbs([CRAWLER_TITLE, domain?.url || '...'])} + pageHeader={ + dataLoading + ? undefined + : { + pageTitle: domain!.url, + rightSideItems: [, ], + } + } isLoading={dataLoading} > From 118ef56c2e4a3593d72e727bc1f1aacb8e64abba Mon Sep 17 00:00:00 2001 From: Nick Peihl Date: Tue, 10 Aug 2021 15:23:59 -0700 Subject: [PATCH 055/104] Update EMS landing page url (#108102) --- src/plugins/maps_ems/common/ems_defaults.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/maps_ems/common/ems_defaults.ts b/src/plugins/maps_ems/common/ems_defaults.ts index a494386b100b7..5b5e8669cffa2 100644 --- a/src/plugins/maps_ems/common/ems_defaults.ts +++ b/src/plugins/maps_ems/common/ems_defaults.ts @@ -9,7 +9,7 @@ // Default config for the elastic hosted EMS endpoints export const DEFAULT_EMS_FILE_API_URL = 'https://vector.maps.elastic.co'; export const DEFAULT_EMS_TILE_API_URL = 'https://tiles.maps.elastic.co'; -export const DEFAULT_EMS_LANDING_PAGE_URL = 'https://maps.elastic.co/v7.13'; +export const DEFAULT_EMS_LANDING_PAGE_URL = 'https://maps.elastic.co/v7.14'; export const DEFAULT_EMS_FONT_LIBRARY_URL = 'https://tiles.maps.elastic.co/fonts/{fontstack}/{range}.pbf'; From 4ffa5cce468b5f6367a5502dc9f64143f043f8fc Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 10 Aug 2021 16:36:04 -0600 Subject: [PATCH 056/104] [Maps] fix code owners (#108106) --- .github/CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 000943bf62d8e..95c11b05a9783 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -158,6 +158,7 @@ # Maps #CC# /x-pack/plugins/maps/ @elastic/kibana-gis +/x-pack/plugins/maps/ @elastic/kibana-gis /x-pack/test/api_integration/apis/maps/ @elastic/kibana-gis /x-pack/test/functional/apps/maps/ @elastic/kibana-gis /x-pack/test/functional/es_archives/maps/ @elastic/kibana-gis @@ -165,7 +166,9 @@ /x-pack/plugins/stack_alerts/server/alert_types/geo_containment @elastic/kibana-gis /x-pack/plugins/stack_alerts/public/alert_types/geo_containment @elastic/kibana-gis #CC# /src/plugins/maps_legacy/ @elastic/kibana-gis +/src/plugins/maps_legacy/ @elastic/kibana-gis #CC# /x-pack/plugins/file_upload @elastic/kibana-gis +/x-pack/plugins/file_upload @elastic/kibana-gis /src/plugins/tile_map/ @elastic/kibana-gis /src/plugins/region_map/ @elastic/kibana-gis /packages/kbn-mapbox-gl @elastic/kibana-gis From e89d069f09d18726c00fbfd35bd946059ee7656f Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Tue, 10 Aug 2021 17:10:49 -0700 Subject: [PATCH 057/104] [Alerting UI] Fixed display permissions for edit/delete buttons when user has read only access. (#107996) * [Alerting UI] Fixed display permissions for Edit/delete buttons when user has read only access * fixed due to comments --- .../components/alerts_list.test.tsx | 13 ++- .../alerts_list/components/alerts_list.tsx | 41 ++++---- .../collapsed_item_actions.test.tsx | 19 +++- .../components/rule_enabled_switch.test.tsx | 95 +++++++++++++++++++ .../components/rule_enabled_switch.tsx | 2 +- 5 files changed, 145 insertions(+), 25 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.test.tsx diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index 0654488939566..958511128de04 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -434,10 +434,10 @@ describe('alerts_list component with items', () => { expect(wrapper.find('[data-test-subj="alertSidebarDeleteAction"]').exists()).toBeTruthy(); }); - it('does not render edit button when rule type does not allow editing in rules management', async () => { + it('does not render edit and delete button when rule type does not allow editing in rules management', async () => { await setup(false); expect(wrapper.find('[data-test-subj="alertSidebarEditAction"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="alertSidebarDeleteAction"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="alertSidebarDeleteAction"]').exists()).toBeFalsy(); }); }); @@ -567,11 +567,18 @@ describe('alerts_list with show only capability', () => { }); } + it('renders table of alerts with edit button disabled', async () => { + await setup(); + expect(wrapper.find('EuiBasicTable')).toHaveLength(1); + expect(wrapper.find('EuiTableRow')).toHaveLength(2); + expect(wrapper.find('[data-test-subj="editActionHoverButton"]')).toHaveLength(0); + }); + it('renders table of alerts with delete button disabled', async () => { await setup(); expect(wrapper.find('EuiBasicTable')).toHaveLength(1); expect(wrapper.find('EuiTableRow')).toHaveLength(2); - // TODO: check delete button + expect(wrapper.find('[data-test-subj="deleteActionHoverButton"]')).toHaveLength(0); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 9b488922a3cf6..941d400104082 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -464,28 +464,27 @@ export const AlertsList: React.FunctionComponent = () => { name: '', width: '10%', render(item: AlertTableItem) { - return ( + return item.isEditable && isRuleTypeEditableInContext(item.alertTypeId) ? ( - {item.isEditable && isRuleTypeEditableInContext(item.alertTypeId) && ( - - onRuleEdit(item)} - iconType={'pencil'} - aria-label={i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.editAriaLabel', - { defaultMessage: 'Edit' } - )} - /> - - )} + + onRuleEdit(item)} + iconType={'pencil'} + aria-label={i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.editAriaLabel', + { defaultMessage: 'Edit' } + )} + /> + { { defaultMessage: 'Delete' } )} className="alertSidebarItem__action" + data-test-subj="deleteActionHoverButton" onClick={() => setAlertsToDelete([item.id])} iconType={'trash'} aria-label={i18n.translate( @@ -504,6 +504,7 @@ export const AlertsList: React.FunctionComponent = () => { + { /> - ); + ) : null; }, }, ]; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.test.tsx index 5b7a87d0dfe27..5a06b03311cbe 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.test.tsx @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import * as React from 'react'; import { mountWithIntl, nextTick } from '@kbn/test/jest'; @@ -95,6 +94,20 @@ describe('CollapsedItemActions', () => { }; }; + test('renders panel items as disabled', async () => { + await setup(); + const wrapper = mountWithIntl( + + ); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + expect( + wrapper.find('[data-test-subj="selectActionButton"]').first().props().disabled + ).toBeTruthy(); + }); + test('renders closed popover initially and opens on click with all actions enabled', async () => { await setup(); const wrapper = mountWithIntl(); @@ -118,6 +131,10 @@ describe('CollapsedItemActions', () => { expect(wrapper.find('[data-test-subj="editAlert"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="deleteAlert"]').exists()).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="selectActionButton"]').first().props().disabled + ).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="muteButton"] button`).prop('disabled')).toBeFalsy(); expect(wrapper.find(`[data-test-subj="muteButton"] button`).text()).toEqual('Mute'); expect(wrapper.find(`[data-test-subj="disableButton"] button`).prop('disabled')).toBeFalsy(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.test.tsx new file mode 100644 index 0000000000000..bfa760b65ed4e --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.test.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { RuleEnabledSwitch, ComponentOpts } from './rule_enabled_switch'; + +describe('RuleEnabledSwitch', () => { + const enableAlert = jest.fn(); + const props: ComponentOpts = { + disableAlert: jest.fn(), + enableAlert, + item: { + id: '1', + name: 'test alert', + tags: ['tag1'], + enabled: true, + alertTypeId: 'test_alert_type', + schedule: { interval: '5d' }, + actions: [], + params: { name: 'test alert type name' }, + createdBy: null, + updatedBy: null, + apiKeyOwner: null, + throttle: '1m', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'active', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, + consumer: 'test', + actionsCount: 0, + alertType: 'test_alert_type', + createdAt: new Date('2020-08-20T19:23:38Z'), + enabledInLicense: true, + isEditable: false, + notifyWhen: null, + tagsText: 'test', + updatedAt: new Date('2020-08-20T19:23:38Z'), + }, + onAlertChanged: jest.fn(), + }; + + beforeEach(() => jest.resetAllMocks()); + + test('renders switch control as disabled when rule is not editable', () => { + const wrapper = mountWithIntl(); + expect(wrapper.find('[data-test-subj="enableSwitch"]').first().props().disabled).toBeTruthy(); + }); + + test('renders switch control', () => { + const wrapper = mountWithIntl( + + ); + expect(wrapper.find('[data-test-subj="enableSwitch"]').first().props().checked).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx index 49871549d2734..21652f1cce781 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.tsx @@ -10,7 +10,7 @@ import { EuiSwitch, EuiLoadingSpinner } from '@elastic/eui'; import { Alert, AlertTableItem } from '../../../../types'; -interface ComponentOpts { +export interface ComponentOpts { item: AlertTableItem; onAlertChanged: () => void; enableAlert: (alert: Alert) => Promise; From 538a6d928844fd28f56d59d6a67122d8aa3f58b8 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Tue, 10 Aug 2021 17:11:11 -0700 Subject: [PATCH 058/104] [Actions UI] Fixed Jira Api token label. (#107776) * [Actions UI] Fixed Jira Api token label. * fixed tests * fixed username Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/management/connectors/action-types/jira.asciidoc | 4 ++-- .../components/builtin_action_types/jira/jira.test.tsx | 2 +- .../components/builtin_action_types/jira/translations.ts | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/management/connectors/action-types/jira.asciidoc b/docs/management/connectors/action-types/jira.asciidoc index 368b11225654c..aa6e92f965e82 100644 --- a/docs/management/connectors/action-types/jira.asciidoc +++ b/docs/management/connectors/action-types/jira.asciidoc @@ -16,8 +16,8 @@ Jira connectors have the following configuration properties. Name:: The name of the connector. The name is used to identify a connector in the **Stack Management** UI connector listing, and in the connector list when configuring an action. URL:: Jira instance URL. Project key:: Jira project key. -Email (or username):: The account email (or username) for HTTP Basic authentication. -API token (or password):: Jira API authentication token (or password) for HTTP Basic authentication. +Email:: The account email for HTTP Basic authentication. +API token:: Jira API authentication token for HTTP Basic authentication. [float] [[jira-connector-networking-configuration]] diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx index 857582fa7cdaf..15d87605495fe 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx @@ -81,7 +81,7 @@ describe('jira connector validation', () => { }, secrets: { errors: { - apiToken: ['API token or password is required'], + apiToken: ['API token is required'], email: [], }, }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts index 5904eb05c31b6..1b9f3e2caa6ef 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts @@ -67,28 +67,28 @@ export const JIRA_REENTER_VALUES_LABEL = i18n.translate( export const JIRA_EMAIL_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.emailTextFieldLabel', { - defaultMessage: 'Username or email address', + defaultMessage: 'Email address', } ); export const JIRA_EMAIL_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredEmailTextField', { - defaultMessage: 'Username or email address is required', + defaultMessage: 'Email address is required', } ); export const JIRA_API_TOKEN_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.apiTokenTextFieldLabel', { - defaultMessage: 'API token or password', + defaultMessage: 'API token', } ); export const JIRA_API_TOKEN_REQUIRED = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiTokenTextField', { - defaultMessage: 'API token or password is required', + defaultMessage: 'API token is required', } ); From 978c44e3810d259a71ff400ebf1b67adcc5540af Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 10 Aug 2021 18:19:12 -0600 Subject: [PATCH 059/104] [Security Solutions][Detection Engine] Fixes "undefined" crash for author field by adding a migration for it (#107230) ## Summary Fixes https://github.com/elastic/kibana/issues/106233 During an earlier upgrade/fix to our system to add defaults to our types, we overlooked the "author" field which wasn't part of the original rules. Users upgrading might get errors such as: ``` params invalid: Invalid value "undefined" supplied to "author" ``` This fixes that issue by adding a migration for the `author` field for `7.14.1`. See https://github.com/elastic/kibana/issues/106233 for test instructions or manually remove your author field before upgrading your release and then upgrade and this should be fixed on upgrade. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../server/saved_objects/migrations.test.ts | 42 +++++++++++++++++++ .../server/saved_objects/migrations.ts | 35 ++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts index 9403c0c28c153..512f4618792fb 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts @@ -970,6 +970,48 @@ describe('successful migrations', () => { }); }); }); + + describe('7.14.1', () => { + test('security solution author field is migrated to array if it is undefined', () => { + const migration7141 = getMigrations(encryptedSavedObjectsSetup)['7.14.1']; + const alert = getMockData({ + alertTypeId: 'siem.signals', + params: {}, + }); + + expect(migration7141(alert, migrationContext)).toEqual({ + ...alert, + attributes: { + ...alert.attributes, + params: { + author: [], + }, + }, + }); + }); + + test('security solution author field does not override existing values if they exist', () => { + const migration7141 = getMigrations(encryptedSavedObjectsSetup)['7.14.1']; + const alert = getMockData({ + alertTypeId: 'siem.signals', + params: { + note: 'some note', + author: ['author 1'], + }, + }); + + expect(migration7141(alert, migrationContext)).toEqual({ + ...alert, + attributes: { + ...alert.attributes, + params: { + note: 'some note', + author: ['author 1'], + }, + }, + }); + }); + }); }); describe('handles errors during migrations', () => { diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.ts index 9f6adeb27083a..944acbdca0182 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.ts @@ -85,11 +85,18 @@ export function getMigrations( pipeMigrations(removeNullsFromSecurityRules) ); + const migrationSecurityRules714 = createEsoMigration( + encryptedSavedObjects, + (doc): doc is SavedObjectUnsanitizedDoc => isSecuritySolutionRule(doc), + pipeMigrations(removeNullAuthorFromSecurityRules) + ); + return { '7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced, '7.10.0'), '7.11.0': executeMigrationWithErrorHandling(migrationAlertUpdatedAtAndNotifyWhen, '7.11.0'), '7.11.2': executeMigrationWithErrorHandling(migrationActions7112, '7.11.2'), '7.13.0': executeMigrationWithErrorHandling(migrationSecurityRules713, '7.13.0'), + '7.14.1': executeMigrationWithErrorHandling(migrationSecurityRules714, '7.14.1'), }; } @@ -432,6 +439,34 @@ function removeNullsFromSecurityRules( }; } +/** + * The author field was introduced later and was not part of the original rules. We overlooked + * the filling in the author field as an empty array in an earlier upgrade routine from + * 'removeNullsFromSecurityRules' during the 7.13.0 upgrade. Since we don't change earlier migrations, + * but rather only move forward with the "arrow of time" we are going to upgrade and fix + * it if it is missing for anyone in 7.14.0 and above release. Earlier releases if we want to fix them, + * would have to be modified as a "7.13.1", etc... if we want to fix it there. + * @param doc The document that is not migrated and contains a "null" or "undefined" author field + * @returns The document with the author field fleshed in. + */ +function removeNullAuthorFromSecurityRules( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc { + const { + attributes: { params }, + } = doc; + return { + ...doc, + attributes: { + ...doc.attributes, + params: { + ...params, + author: params.author != null ? params.author : [], + }, + }, + }; +} + function pipeMigrations(...migrations: AlertMigration[]): AlertMigration { return (doc: SavedObjectUnsanitizedDoc) => migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc); From 4df34e1188f61677607a12c19ddab33445f5d7ad Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Tue, 10 Aug 2021 19:33:55 -0500 Subject: [PATCH 060/104] [ML] Enable Index data visualizer document count chart to update time range query (#106438) * Add brush listener * Fix back button not working * [ML] Remove api names in apidoc.json Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../document_count_chart.tsx | 42 ++++++++++++++++++- .../index_data_visualizer_view.tsx | 2 + x-pack/plugins/ml/server/routes/apidoc.json | 2 - 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_chart/document_count_chart.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_chart/document_count_chart.tsx index 34faed01f613e..b8df4defa18a2 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_chart/document_count_chart.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_chart/document_count_chart.tsx @@ -5,19 +5,24 @@ * 2.0. */ -import React, { FC, useMemo } from 'react'; +import React, { FC, useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { Axis, BarSeries, + BrushEndListener, Chart, + ElementClickListener, niceTimeFormatter, Position, ScaleType, Settings, + XYChartElementEvent, } from '@elastic/charts'; +import moment from 'moment'; +import { useDataVisualizerKibana } from '../../../../kibana_context'; export interface DocumentCountChartPoint { time: number | string; @@ -41,6 +46,10 @@ export const DocumentCountChart: FC = ({ timeRangeLatest, interval, }) => { + const { + services: { data }, + } = useDataVisualizerKibana(); + const seriesName = i18n.translate( 'xpack.dataVisualizer.dataGrid.field.documentCountChart.seriesLabel', { @@ -71,6 +80,35 @@ export const DocumentCountChart: FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [chartPoints, timeRangeEarliest, timeRangeLatest, interval]); + const timefilterUpdateHandler = useCallback( + (ranges: { from: number; to: number }) => { + data.query.timefilter.timefilter.setTime({ + from: moment(ranges.from).toISOString(), + to: moment(ranges.to).toISOString(), + mode: 'absolute', + }); + }, + [data] + ); + + const onBrushEnd: BrushEndListener = ({ x }) => { + if (!x) { + return; + } + const [from, to] = x; + timefilterUpdateHandler({ from, to }); + }; + + const onElementClick: ElementClickListener = ([elementData]) => { + const startRange = (elementData as XYChartElementEvent)[0].x; + + const range = { + from: startRange, + to: startRange + interval, + }; + timefilterUpdateHandler(range); + }; + return (
    = ({ height: 120, }} > - + = (dataVi from: globalState.time.from, to: globalState.time.to, }); + setLastRefresh(Date.now()); } }, [globalState, timefilter]); useEffect(() => { if (globalState?.refreshInterval !== undefined) { timefilter.setRefreshInterval(globalState.refreshInterval); + setLastRefresh(Date.now()); } }, [globalState, timefilter]); diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index 346bf510c6c0c..e238e690e7e97 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -22,8 +22,6 @@ "ValidateDataFrameAnalytics", "DataVisualizer", - "GetOverallStats", - "GetStatsForFields", "GetHistogramsForFields", "AnomalyDetectors", From bfad9e354f09eec58e2caa36e64943dd5db30708 Mon Sep 17 00:00:00 2001 From: Sandra G Date: Tue, 10 Aug 2021 21:03:14 -0400 Subject: [PATCH 061/104] add manage rules link to alerts dropdown (#107950) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../monitoring/public/alerts/alerts_dropdown.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/monitoring/public/alerts/alerts_dropdown.tsx b/x-pack/plugins/monitoring/public/alerts/alerts_dropdown.tsx index c7f81d999cd02..8ea073d6132d6 100644 --- a/x-pack/plugins/monitoring/public/alerts/alerts_dropdown.tsx +++ b/x-pack/plugins/monitoring/public/alerts/alerts_dropdown.tsx @@ -15,10 +15,15 @@ import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { Legacy } from '../legacy_shims'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { MonitoringStartPluginDependencies } from '../types'; export const AlertsDropdown: React.FC<{}> = () => { const $injector = Legacy.shims.getAngularInjector(); const alertsEnableModalProvider: any = $injector.get('enableAlertsModal'); + const { navigateToApp } = useKibana< + MonitoringStartPluginDependencies['core'] + >().services.application; const [isPopoverOpen, setIsPopoverOpen] = useState(false); @@ -44,13 +49,21 @@ export const AlertsDropdown: React.FC<{}> = () => { ); - const items = [ + const items: EuiContextMenuPanelDescriptor['items'] = [ { name: i18n.translate('xpack.monitoring.alerts.dropdown.createAlerts', { defaultMessage: 'Create default rules', }), onClick: createDefaultRules, }, + { + name: i18n.translate('xpack.monitoring.alerts.dropdown.manageRules', { + defaultMessage: 'Manage rules', + }), + icon: 'tableOfContents', + onClick: () => + navigateToApp('management', { path: '/insightsAndAlerting/triggersActions/rules' }), + }, ]; const panels: EuiContextMenuPanelDescriptor[] = [ From 6ed4b4f70ccc0eaff5d6c7758ab964af34bef4da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 10 Aug 2021 23:40:20 -0400 Subject: [PATCH 062/104] [APM] Add new ftr_e2e to kibana CI and remove current e2e tests. (#107593) --- test/scripts/jenkins_apm_cypress.sh | 12 + vars/tasks.groovy | 8 + .../cypress/integration/csm_dashboard.feature | 5 - .../read_only_user/deep_links.spec.ts | 2 +- .../integration/read_only_user/home.spec.ts | 54 ++- .../service_overview/header_filters.spec.ts | 76 ++-- .../service_overview/instances_table.spec.ts | 41 +- .../service_overview/service_overview.spec.ts | 3 +- .../service_overview/time_comparison.spec.ts | 50 +-- .../transactions_overview.spec.ts | 12 +- x-pack/plugins/apm/ftr_e2e/cypress_start.ts | 35 +- .../public/components/shared/service_link.tsx | 1 + .../scripts/kibana-security/call_kibana.ts | 51 +++ .../create_apm_users/create_role.ts | 86 ++++ .../kibana-security/create_apm_users/index.ts | 188 +++++++++ .../create_apm_users/power_user_role.ts | 46 ++ .../create_apm_users/read_only_user_role.ts | 46 ++ .../create_kibana_user_role.ts | 112 +++++ .../setup-custom-kibana-user-role.ts | 396 ++---------------- 19 files changed, 739 insertions(+), 485 deletions(-) create mode 100755 test/scripts/jenkins_apm_cypress.sh create mode 100644 x-pack/plugins/apm/scripts/kibana-security/call_kibana.ts create mode 100644 x-pack/plugins/apm/scripts/kibana-security/create_apm_users/create_role.ts create mode 100644 x-pack/plugins/apm/scripts/kibana-security/create_apm_users/index.ts create mode 100644 x-pack/plugins/apm/scripts/kibana-security/create_apm_users/power_user_role.ts create mode 100644 x-pack/plugins/apm/scripts/kibana-security/create_apm_users/read_only_user_role.ts create mode 100644 x-pack/plugins/apm/scripts/kibana-security/create_kibana_user_role.ts diff --git a/test/scripts/jenkins_apm_cypress.sh b/test/scripts/jenkins_apm_cypress.sh new file mode 100755 index 0000000000000..a1d2ab73b5552 --- /dev/null +++ b/test/scripts/jenkins_apm_cypress.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +source test/scripts/jenkins_test_setup_xpack.sh + +echo " -> Running APM cypress tests" +cd "$XPACK_DIR" + +checks-reporter-with-killswitch "APM Cypress Tests" \ + node plugins/apm/scripts/ftr_e2e/cypress_run + +echo "" +echo "" diff --git a/vars/tasks.groovy b/vars/tasks.groovy index e6ab3eaf92afd..7ae8be25c93ab 100644 --- a/vars/tasks.groovy +++ b/vars/tasks.groovy @@ -145,6 +145,14 @@ def functionalXpack(Map params = [:]) { // task(kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypressFirefox', './test/scripts/jenkins_security_solution_cypress_firefox.sh')) } } + + whenChanged([ + 'x-pack/plugins/apm/', + ]) { + if (githubPr.isPr()) { + task(kibanaPipeline.functionalTestProcess('xpack-APMCypress', './test/scripts/jenkins_apm_cypress.sh')) + } + } } } diff --git a/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature b/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature index 4d68089417138..0028f40a68d90 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature +++ b/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature @@ -1,10 +1,5 @@ Feature: CSM Dashboard - Scenario: Service name filter - Given a user browses the APM UI application for RUM Data - When the user changes the selected service name - Then it displays relevant client metrics - Scenario: Client metrics When a user browses the APM UI application for RUM Data Then should have correct client metrics diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/deep_links.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/deep_links.spec.ts index 106c380b43207..3f7e01be831f8 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/deep_links.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/deep_links.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -describe('APM depp links', () => { +describe('APM deep links', () => { before(() => { cy.loginAsReadOnlyUser(); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts index d25251f457e36..371850796fd24 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts @@ -11,18 +11,28 @@ import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; const { start, end } = archives_metadata['apm_8.0.0']; -const servicesPath = '/app/apm/services'; -const baseUrl = url.format({ - pathname: servicesPath, +const serviceInventoryHref = url.format({ + pathname: '/app/apm/services', query: { rangeFrom: start, rangeTo: end }, }); +const apisToIntercept = [ + { + endpoint: '/api/apm/service', + name: 'servicesMainStatistics', + }, + { + endpoint: '/api/apm/services/detailed_statistics', + name: 'servicesDetailedStatistics', + }, +]; + describe('Home page', () => { before(() => { - esArchiverLoad('apm_8.0.0'); + // esArchiverLoad('apm_8.0.0'); }); after(() => { - esArchiverUnload('apm_8.0.0'); + // esArchiverUnload('apm_8.0.0'); }); beforeEach(() => { cy.loginAsReadOnlyUser(); @@ -34,12 +44,12 @@ describe('Home page', () => { 'include', 'app/apm/services?rangeFrom=now-15m&rangeTo=now' ); - cy.get('.euiTabs .euiTab-isSelected').contains('Services'); }); - it('includes services with only metric documents', () => { + // Flaky + it.skip('includes services with only metric documents', () => { cy.visit( - `${baseUrl}&kuery=not%2520(processor.event%2520%253A%2522transaction%2522%2520)` + `${serviceInventoryHref}&kuery=not%2520(processor.event%2520%253A%2522transaction%2522%2520)` ); cy.contains('opbeans-python'); cy.contains('opbeans-java'); @@ -47,16 +57,28 @@ describe('Home page', () => { }); describe('navigations', () => { - it('navigates to service overview page with transaction type', () => { - const kuery = encodeURIComponent( - 'transaction.name : "taskManager markAvailableTasksAsClaimed"' - ); - cy.visit(`${baseUrl}&kuery=${kuery}`); - cy.contains('taskManager'); - cy.contains('kibana').click(); + /* + This test is flaky, there's a problem with EuiBasicTable, that it blocks any action while loading is enabled. + So it might fail to click on the service link. + */ + it.skip('navigates to service overview page with transaction type', () => { + apisToIntercept.map(({ endpoint, name }) => { + cy.intercept('GET', endpoint).as(name); + }); + + cy.visit(serviceInventoryHref); + + cy.contains('Services'); + + cy.wait('@servicesMainStatistics', { responseTimeout: 10000 }); + cy.wait('@servicesDetailedStatistics', { responseTimeout: 10000 }); + + cy.get('[data-test-subj="serviceLink_rum-js"]').then((element) => { + element[0].click(); + }); cy.get('[data-test-subj="headerFilterTransactionType"]').should( 'have.value', - 'taskManager' + 'page-load' ); }); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/header_filters.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/header_filters.spec.ts index d253a290f4a51..f124b3818c193 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/header_filters.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/header_filters.spec.ts @@ -10,51 +10,51 @@ import { esArchiverLoad, esArchiverUnload } from '../../../tasks/es_archiver'; const { start, end } = archives_metadata['apm_8.0.0']; -const serviceOverviewPath = '/app/apm/services/kibana/overview'; -const baseUrl = url.format({ - pathname: serviceOverviewPath, +const serviceOverviewHref = url.format({ + pathname: '/app/apm/services/opbeans-node/overview', query: { rangeFrom: start, rangeTo: end }, }); const apisToIntercept = [ { - endpoint: '/api/apm/services/kibana/transactions/charts/latency', - as: 'latencyChartRequest', + endpoint: '/api/apm/services/opbeans-node/transactions/charts/latency', + name: 'latencyChartRequest', }, { - endpoint: '/api/apm/services/kibana/throughput', - as: 'throughputChartRequest', + endpoint: '/api/apm/services/opbeans-node/throughput', + name: 'throughputChartRequest', }, { - endpoint: '/api/apm/services/kibana/transactions/charts/error_rate', - as: 'errorRateChartRequest', + endpoint: '/api/apm/services/opbeans-node/transactions/charts/error_rate', + name: 'errorRateChartRequest', }, { endpoint: - '/api/apm/services/kibana/transactions/groups/detailed_statistics', - as: 'transactionGroupsDetailedRequest', + '/api/apm/services/opbeans-node/transactions/groups/detailed_statistics', + name: 'transactionGroupsDetailedRequest', }, { endpoint: - '/api/apm/services/kibana/service_overview_instances/detailed_statistics', - as: 'instancesDetailedRequest', + '/api/apm/services/opbeans-node/service_overview_instances/detailed_statistics', + name: 'instancesDetailedRequest', }, { endpoint: - '/api/apm/services/kibana/service_overview_instances/main_statistics', - as: 'instancesMainStatisticsRequest', + '/api/apm/services/opbeans-node/service_overview_instances/main_statistics', + name: 'instancesMainStatisticsRequest', }, { - endpoint: '/api/apm/services/kibana/error_groups/main_statistics', - as: 'errorGroupsMainStatisticsRequest', + endpoint: '/api/apm/services/opbeans-node/error_groups/main_statistics', + name: 'errorGroupsMainStatisticsRequest', }, { - endpoint: '/api/apm/services/kibana/transaction/charts/breakdown', - as: 'transactonBreakdownRequest', + endpoint: '/api/apm/services/opbeans-node/transaction/charts/breakdown', + name: 'transactonBreakdownRequest', }, { - endpoint: '/api/apm/services/kibana/transactions/groups/main_statistics', - as: 'transactionsGroupsMainStatisticsRequest', + endpoint: + '/api/apm/services/opbeans-node/transactions/groups/main_statistics', + name: 'transactionsGroupsMainStatisticsRequest', }, ]; @@ -70,50 +70,46 @@ describe('Service overview - header filters', () => { }); describe('Filtering by transaction type', () => { it('changes url when selecting different value', () => { - cy.visit(baseUrl); - cy.contains('Kibana'); + cy.visit(serviceOverviewHref); + cy.contains('opbeans-node'); cy.url().should('not.include', 'transactionType'); cy.get('[data-test-subj="headerFilterTransactionType"]').should( 'have.value', 'request' ); - cy.get('[data-test-subj="headerFilterTransactionType"]').select( - 'taskManager' - ); - cy.url().should('include', 'transactionType=taskManager'); + cy.get('[data-test-subj="headerFilterTransactionType"]').select('Worker'); + cy.url().should('include', 'transactionType=Worker'); cy.get('[data-test-subj="headerFilterTransactionType"]').should( 'have.value', - 'taskManager' + 'Worker' ); }); it('calls APIs with correct transaction type', () => { - apisToIntercept.map(({ endpoint, as }) => { - cy.intercept('GET', endpoint).as(as); + apisToIntercept.map(({ endpoint, name }) => { + cy.intercept('GET', endpoint).as(name); }); - cy.visit(baseUrl); - cy.contains('Kibana'); + cy.visit(serviceOverviewHref); + cy.contains('opbeans-node'); cy.get('[data-test-subj="headerFilterTransactionType"]').should( 'have.value', 'request' ); cy.expectAPIsToHaveBeenCalledWith({ - apisIntercepted: apisToIntercept.map(({ as }) => `@${as}`), + apisIntercepted: apisToIntercept.map(({ name }) => `@${name}`), value: 'transactionType=request', }); - cy.get('[data-test-subj="headerFilterTransactionType"]').select( - 'taskManager' - ); - cy.url().should('include', 'transactionType=taskManager'); + cy.get('[data-test-subj="headerFilterTransactionType"]').select('Worker'); + cy.url().should('include', 'transactionType=Worker'); cy.get('[data-test-subj="headerFilterTransactionType"]').should( 'have.value', - 'taskManager' + 'Worker' ); cy.expectAPIsToHaveBeenCalledWith({ - apisIntercepted: apisToIntercept.map(({ as }) => `@${as}`), - value: 'transactionType=taskManager', + apisIntercepted: apisToIntercept.map(({ name }) => `@${name}`), + value: 'transactionType=Worker', }); }); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/instances_table.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/instances_table.spec.ts index 2d76dfe977ef7..40a08035f5213 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/instances_table.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/instances_table.spec.ts @@ -11,9 +11,8 @@ import { esArchiverLoad, esArchiverUnload } from '../../../tasks/es_archiver'; const { start, end } = archives_metadata['apm_8.0.0']; -const serviceOverviewPath = '/app/apm/services/opbeans-java/overview'; -const baseUrl = url.format({ - pathname: serviceOverviewPath, +const serviceOverviewHref = url.format({ + pathname: '/app/apm/services/opbeans-java/overview', query: { rangeFrom: start, rangeTo: end }, }); @@ -21,22 +20,22 @@ const apisToIntercept = [ { endpoint: '/api/apm/services/opbeans-java/service_overview_instances/main_statistics', - as: 'instancesMainRequest', + name: 'instancesMainRequest', }, { endpoint: '/api/apm/services/opbeans-java/service_overview_instances/detailed_statistics', - as: 'instancesDetailsRequest', + name: 'instancesDetailsRequest', }, { endpoint: - '/api/apm/services/opbeans-java/service_overview_instances/details/02950c4c5fbb0fda1cc98c47bf4024b473a8a17629db6530d95dcee68bd54c6c', - as: 'instanceDetailsRequest', + '/api/apm/services/opbeans-java/service_overview_instances/details/31651f3c624b81c55dd4633df0b5b9f9ab06b151121b0404ae796632cd1f87ad', + name: 'instanceDetailsRequest', }, { endpoint: - '/api/apm/services/opbeans-java/service_overview_instances/details/02950c4c5fbb0fda1cc98c47bf4024b473a8a17629db6530d95dcee68bd54c6c', - as: 'instanceDetailsRequest', + '/api/apm/services/opbeans-java/service_overview_instances/details/31651f3c624b81c55dd4633df0b5b9f9ab06b151121b0404ae796632cd1f87ad', + name: 'instanceDetailsRequest', }, ]; @@ -46,7 +45,7 @@ describe('Instances table', () => { }); describe('when data is not loaded', () => { it('shows empty message', () => { - cy.visit(baseUrl); + cy.visit(serviceOverviewHref); cy.contains('opbeans-java'); cy.get('[data-test-subj="serviceInstancesTableContainer"]').contains( 'No items found' @@ -62,18 +61,19 @@ describe('Instances table', () => { esArchiverUnload('apm_8.0.0'); }); const serviceNodeName = - '02950c4c5fbb0fda1cc98c47bf4024b473a8a17629db6530d95dcee68bd54c6c'; + '31651f3c624b81c55dd4633df0b5b9f9ab06b151121b0404ae796632cd1f87ad'; it('has data in the table', () => { - cy.visit(baseUrl); + cy.visit(serviceOverviewHref); cy.contains('opbeans-java'); cy.contains(serviceNodeName); }); - it('shows instance details', () => { - apisToIntercept.map(({ endpoint, as }) => { - cy.intercept('GET', endpoint).as(as); + // For some reason the details panel is not opening after clicking on the button. + it.skip('shows instance details', () => { + apisToIntercept.map(({ endpoint, name }) => { + cy.intercept('GET', endpoint).as(name); }); - cy.visit(baseUrl); + cy.visit(serviceOverviewHref); cy.contains('opbeans-java'); cy.wait('@instancesMainRequest'); @@ -88,12 +88,13 @@ describe('Instances table', () => { cy.contains('Service'); }); }); - it('shows actions available', () => { - apisToIntercept.map(({ endpoint, as }) => { - cy.intercept('GET', endpoint).as(as); + // For some reason the tooltip is not opening after clicking on the button. + it.skip('shows actions available', () => { + apisToIntercept.map(({ endpoint, name }) => { + cy.intercept('GET', endpoint).as(name); }); - cy.visit(baseUrl); + cy.visit(serviceOverviewHref); cy.contains('opbeans-java'); cy.wait('@instancesMainRequest'); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts index c3b4d979829fa..7c5d5988c9bf6 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts @@ -38,8 +38,7 @@ describe('Service Overview', () => { 'have.value', 'Worker' ); - - cy.get('[data-test-subj="tab_transactions"]').click(); + cy.contains('Transactions').click(); cy.get('[data-test-subj="headerFilterTransactionType"]').should( 'have.value', 'Worker' diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/time_comparison.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/time_comparison.spec.ts index 136328603a9d3..de05cc3abb927 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/time_comparison.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/time_comparison.spec.ts @@ -12,7 +12,7 @@ import { esArchiverLoad, esArchiverUnload } from '../../../tasks/es_archiver'; const { start, end } = archives_metadata['apm_8.0.0']; const serviceOverviewPath = '/app/apm/services/opbeans-java/overview'; -const baseUrl = url.format({ +const serviceOverviewHref = url.format({ pathname: serviceOverviewPath, query: { rangeFrom: start, rangeTo: end }, }); @@ -20,29 +20,29 @@ const baseUrl = url.format({ const apisToIntercept = [ { endpoint: '/api/apm/services/opbeans-java/transactions/charts/latency', - as: 'latencyChartRequest', + name: 'latencyChartRequest', }, { endpoint: '/api/apm/services/opbeans-java/throughput', - as: 'throughputChartRequest', + name: 'throughputChartRequest', }, { endpoint: '/api/apm/services/opbeans-java/transactions/charts/error_rate', - as: 'errorRateChartRequest', + name: 'errorRateChartRequest', }, { endpoint: '/api/apm/services/opbeans-java/transactions/groups/detailed_statistics', - as: 'transactionGroupsDetailedRequest', + name: 'transactionGroupsDetailedRequest', }, { endpoint: '/api/apm/services/opbeans-java/error_groups/detailed_statistics', - as: 'errorGroupsDetailedRequest', + name: 'errorGroupsDetailedRequest', }, { endpoint: '/api/apm/services/opbeans-java/service_overview_instances/detailed_statistics', - as: 'instancesDetailedRequest', + name: 'instancesDetailedRequest', }, ]; @@ -64,7 +64,7 @@ describe('Service overview: Time Comparison', () => { describe('when comparison is toggled off', () => { it('disables select box', () => { - cy.visit(baseUrl); + cy.visit(serviceOverviewHref); cy.contains('opbeans-java'); // Comparison is enabled by default @@ -76,17 +76,17 @@ describe('Service overview: Time Comparison', () => { }); it('calls APIs without comparison time range', () => { - apisToIntercept.map(({ endpoint, as }) => { - cy.intercept('GET', endpoint).as(as); + apisToIntercept.map(({ endpoint, name }) => { + cy.intercept('GET', endpoint).as(name); }); - cy.visit(baseUrl); + cy.visit(serviceOverviewHref); cy.contains('opbeans-java'); cy.get('[data-test-subj="comparisonSelect"]').should('be.enabled'); const comparisonStartEnd = - 'comparisonStart=2020-12-08T13%3A26%3A03.865Z&comparisonEnd=2020-12-08T13%3A57%3A00.000Z'; + 'comparisonStart=2021-08-02T06%3A50%3A00.000Z&comparisonEnd=2021-08-02T07%3A20%3A15.910Z'; // When the page loads it fetches all APIs with comparison time range - cy.wait(apisToIntercept.map(({ as }) => `@${as}`)).then( + cy.wait(apisToIntercept.map(({ name }) => `@${name}`)).then( (interceptions) => { interceptions.map((interception) => { expect(interception.request.url).include(comparisonStartEnd); @@ -98,7 +98,7 @@ describe('Service overview: Time Comparison', () => { cy.contains('Comparison').click(); cy.get('[data-test-subj="comparisonSelect"]').should('be.disabled'); // When comparison is disabled APIs are called withou comparison time range - cy.wait(apisToIntercept.map(({ as }) => `@${as}`)).then( + cy.wait(apisToIntercept.map(({ name }) => `@${name}`)).then( (interceptions) => { interceptions.map((interception) => { expect(interception.request.url).not.include(comparisonStartEnd); @@ -109,8 +109,8 @@ describe('Service overview: Time Comparison', () => { }); it('changes comparison type', () => { - apisToIntercept.map(({ endpoint, as }) => { - cy.intercept('GET', endpoint).as(as); + apisToIntercept.map(({ endpoint, name }) => { + cy.intercept('GET', endpoint).as(name); }); cy.visit(serviceOverviewPath); cy.contains('opbeans-java'); @@ -131,18 +131,8 @@ describe('Service overview: Time Comparison', () => { cy.contains('Week before'); cy.changeTimeRange('Today'); - cy.get('[data-test-subj="comparisonSelect"]').should( - 'have.value', - 'period' - ); - cy.get('[data-test-subj="comparisonSelect"]').should( - 'not.contain.text', - 'Day before' - ); - cy.get('[data-test-subj="comparisonSelect"]').should( - 'not.contain.text', - 'Week before' - ); + cy.contains('Day before'); + cy.contains('Week before'); cy.changeTimeRange('Last 24 hours'); cy.get('[data-test-subj="comparisonSelect"]').should('have.value', 'day'); @@ -177,8 +167,8 @@ describe('Service overview: Time Comparison', () => { }); it('hovers over throughput chart shows previous and current period', () => { - apisToIntercept.map(({ endpoint, as }) => { - cy.intercept('GET', endpoint).as(as); + apisToIntercept.map(({ endpoint, name }) => { + cy.intercept('GET', endpoint).as(name); }); cy.visit( url.format({ diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transactions_overview/transactions_overview.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transactions_overview/transactions_overview.spec.ts index fc17d1975d631..eaa0ee9e4d65a 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transactions_overview/transactions_overview.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/transactions_overview/transactions_overview.spec.ts @@ -11,9 +11,8 @@ import { esArchiverLoad, esArchiverUnload } from '../../../tasks/es_archiver'; const { start, end } = archives_metadata['apm_8.0.0']; -const serviceOverviewPath = '/app/apm/services/opbeans-node/transactions'; -const baseUrl = url.format({ - pathname: serviceOverviewPath, +const serviceOverviewHref = url.format({ + pathname: '/app/apm/services/opbeans-node/transactions', query: { rangeFrom: start, rangeTo: end }, }); @@ -27,8 +26,8 @@ describe('Transactions Overview', () => { beforeEach(() => { cy.loginAsReadOnlyUser(); }); - it('persists transaction type selected when clicking on Overview tab', () => { - cy.visit(baseUrl); + it('persists transaction type selected when navigating to Overview tab', () => { + cy.visit(serviceOverviewHref); cy.get('[data-test-subj="headerFilterTransactionType"]').should( 'have.value', 'request' @@ -38,8 +37,7 @@ describe('Transactions Overview', () => { 'have.value', 'Worker' ); - - cy.get('[data-test-subj="tab_overview"]').click(); + cy.get('a[href*="/app/apm/services/opbeans-node/overview"]').click(); cy.get('[data-test-subj="headerFilterTransactionType"]').should( 'have.value', 'Worker' diff --git a/x-pack/plugins/apm/ftr_e2e/cypress_start.ts b/x-pack/plugins/apm/ftr_e2e/cypress_start.ts index 0a13caa1a665b..a6027367d7868 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress_start.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress_start.ts @@ -7,12 +7,21 @@ import Url from 'url'; import cypress from 'cypress'; -import childProcess from 'child_process'; import { FtrProviderContext } from './ftr_provider_context'; import archives_metadata from './cypress/fixtures/es_archiver/archives_metadata'; +import { createKibanaUserRole } from '../scripts/kibana-security/create_kibana_user_role'; export async function cypressRunTests({ getService }: FtrProviderContext) { - await cypressStart(getService, cypress.run); + try { + const result = await cypressStart(getService, cypress.run); + + if (result && (result.status === 'failed' || result.totalFailed > 0)) { + process.exit(1); + } + } catch (error) { + console.error('errors: ', error); + process.exit(1); + } } export async function cypressOpenTests({ getService }: FtrProviderContext) { @@ -35,20 +44,22 @@ async function cypressStart( }); // Creates APM users - childProcess.execSync( - `node ../scripts/setup-kibana-security.js --role-suffix e2e_tests --username ${config.get( - 'servers.elasticsearch.username' - )} --password ${config.get( - 'servers.elasticsearch.password' - )} --kibana-url ${kibanaUrl}` - ); - - await cypressExecution({ + await createKibanaUserRole({ + elasticsearch: { + username: config.get('servers.elasticsearch.username'), + password: config.get('servers.elasticsearch.password'), + }, + kibana: { + hostname: kibanaUrl, + roleSuffix: 'e2e_tests', + }, + }); + + return cypressExecution({ config: { baseUrl: kibanaUrl }, env: { START_DATE: start, END_DATE: end, - ELASTICSEARCH_URL: Url.format(config.get('servers.elasticsearch')), KIBANA_URL: kibanaUrl, }, }); diff --git a/x-pack/plugins/apm/public/components/shared/service_link.tsx b/x-pack/plugins/apm/public/components/shared/service_link.tsx index d79243315c773..a09ce958fdcab 100644 --- a/x-pack/plugins/apm/public/components/shared/service_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/service_link.tsx @@ -32,6 +32,7 @@ export function ServiceLink({ return ( ({ + elasticsearch, + kibanaHostname, + options, +}: { + elasticsearch: Elasticsearch; + kibanaHostname: string; + options: AxiosRequestConfig; +}): Promise { + const kibanaBasePath = await getKibanaBasePath({ kibanaHostname }); + const { username, password } = elasticsearch; + + const { data } = await axios.request({ + ...options, + baseURL: kibanaHostname + kibanaBasePath, + auth: { username, password }, + headers: { 'kbn-xsrf': 'true', ...options.headers }, + }); + return data; +} + +const getKibanaBasePath = once( + async ({ kibanaHostname }: { kibanaHostname: string }) => { + try { + await axios.request({ url: kibanaHostname, maxRedirects: 0 }); + } catch (e) { + if (isAxiosError(e)) { + const location = e.response?.headers?.location; + const isBasePath = RegExp(/^\/\w{3}$/).test(location); + return isBasePath ? location : ''; + } + + throw e; + } + return ''; + } +); + +export function isAxiosError(e: AxiosError | Error): e is AxiosError { + return 'isAxiosError' in e; +} diff --git a/x-pack/plugins/apm/scripts/kibana-security/create_apm_users/create_role.ts b/x-pack/plugins/apm/scripts/kibana-security/create_apm_users/create_role.ts new file mode 100644 index 0000000000000..d4814e05029a0 --- /dev/null +++ b/x-pack/plugins/apm/scripts/kibana-security/create_apm_users/create_role.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +/* eslint-disable no-console */ + +import { Role } from '../../../../security/common/model'; +import { callKibana, isAxiosError } from '../call_kibana'; +import { Elasticsearch } from '../create_kibana_user_role'; + +type Privilege = [] | ['read'] | ['all']; +export interface KibanaPrivileges { + base?: Privilege; + feature?: Record; +} + +export type RoleType = Omit; + +export async function createRole({ + elasticsearch, + kibanaHostname, + roleName, + role, +}: { + elasticsearch: Elasticsearch; + kibanaHostname: string; + roleName: string; + role: RoleType; +}) { + const roleFound = await getRole({ + elasticsearch, + kibanaHostname, + roleName, + }); + if (roleFound) { + console.log(`Skipping: Role "${roleName}" already exists`); + return Promise.resolve(); + } + + await callKibana({ + elasticsearch, + kibanaHostname, + options: { + method: 'PUT', + url: `/api/security/role/${roleName}`, + data: { + metadata: { version: 1 }, + ...role, + }, + }, + }); + + console.log( + `Created role "${roleName}" with privilege "${JSON.stringify(role.kibana)}"` + ); +} + +async function getRole({ + elasticsearch, + kibanaHostname, + roleName, +}: { + elasticsearch: Elasticsearch; + kibanaHostname: string; + roleName: string; +}): Promise { + try { + return await callKibana({ + elasticsearch, + kibanaHostname, + options: { + method: 'GET', + url: `/api/security/role/${roleName}`, + }, + }); + } catch (e) { + // return empty if role doesn't exist + if (isAxiosError(e) && e.response?.status === 404) { + return null; + } + + throw e; + } +} diff --git a/x-pack/plugins/apm/scripts/kibana-security/create_apm_users/index.ts b/x-pack/plugins/apm/scripts/kibana-security/create_apm_users/index.ts new file mode 100644 index 0000000000000..fea09c7383603 --- /dev/null +++ b/x-pack/plugins/apm/scripts/kibana-security/create_apm_users/index.ts @@ -0,0 +1,188 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable no-console */ + +import { difference, union } from 'lodash'; +import { callKibana, isAxiosError } from '../call_kibana'; +import { Elasticsearch, Kibana } from '../create_kibana_user_role'; +import { createRole } from './create_role'; +import { powerUserRole } from './power_user_role'; +import { readOnlyUserRole } from './read_only_user_role'; + +export async function createAPMUsers({ + kibana: { roleSuffix, hostname }, + elasticsearch, +}: { + kibana: Kibana; + elasticsearch: Elasticsearch; +}) { + const KIBANA_READ_ROLE = `kibana_read_${roleSuffix}`; + const KIBANA_POWER_ROLE = `kibana_power_${roleSuffix}`; + const APM_USER_ROLE = 'apm_user'; + + // roles definition + const roles = [ + { + roleName: KIBANA_READ_ROLE, + role: readOnlyUserRole, + }, + { + roleName: KIBANA_POWER_ROLE, + role: powerUserRole, + }, + ]; + + // create roles + await Promise.all( + roles.map(async (role) => + createRole({ elasticsearch, kibanaHostname: hostname, ...role }) + ) + ); + + // users definition + const users = [ + { + username: 'apm_read_user', + roles: [APM_USER_ROLE, KIBANA_READ_ROLE], + }, + { + username: 'apm_power_user', + roles: [APM_USER_ROLE, KIBANA_POWER_ROLE], + }, + ]; + + // create users + await Promise.all( + users.map(async (user) => + createOrUpdateUser({ elasticsearch, kibanaHostname: hostname, user }) + ) + ); +} + +interface User { + username: string; + roles: string[]; + full_name?: string; + email?: string; + enabled?: boolean; +} + +async function createOrUpdateUser({ + elasticsearch, + kibanaHostname, + user, +}: { + elasticsearch: Elasticsearch; + kibanaHostname: string; + user: User; +}) { + const existingUser = await getUser({ + elasticsearch, + kibanaHostname, + username: user.username, + }); + if (!existingUser) { + return createUser({ elasticsearch, kibanaHostname, newUser: user }); + } + + return updateUser({ + elasticsearch, + kibanaHostname, + existingUser, + newUser: user, + }); +} + +async function createUser({ + elasticsearch, + kibanaHostname, + newUser, +}: { + elasticsearch: Elasticsearch; + kibanaHostname: string; + newUser: User; +}) { + const user = await callKibana({ + elasticsearch, + kibanaHostname, + options: { + method: 'POST', + url: `/internal/security/users/${newUser.username}`, + data: { + ...newUser, + enabled: true, + password: elasticsearch.password, + }, + }, + }); + + console.log(`User "${newUser.username}" was created`); + return user; +} + +async function updateUser({ + elasticsearch, + kibanaHostname, + existingUser, + newUser, +}: { + elasticsearch: Elasticsearch; + kibanaHostname: string; + existingUser: User; + newUser: User; +}) { + const { username } = newUser; + const allRoles = union(existingUser.roles, newUser.roles); + const hasAllRoles = difference(allRoles, existingUser.roles).length === 0; + if (hasAllRoles) { + console.log( + `Skipping: User "${username}" already has neccesarry roles: "${newUser.roles}"` + ); + return; + } + + // assign role to user + await callKibana({ + elasticsearch, + kibanaHostname, + options: { + method: 'POST', + url: `/internal/security/users/${username}`, + data: { ...existingUser, roles: allRoles }, + }, + }); + + console.log(`User "${username}" was updated`); +} + +async function getUser({ + elasticsearch, + kibanaHostname, + username, +}: { + elasticsearch: Elasticsearch; + kibanaHostname: string; + username: string; +}) { + try { + return await callKibana({ + elasticsearch, + kibanaHostname, + options: { + url: `/internal/security/users/${username}`, + }, + }); + } catch (e) { + // return empty if user doesn't exist + if (isAxiosError(e) && e.response?.status === 404) { + return null; + } + + throw e; + } +} diff --git a/x-pack/plugins/apm/scripts/kibana-security/create_apm_users/power_user_role.ts b/x-pack/plugins/apm/scripts/kibana-security/create_apm_users/power_user_role.ts new file mode 100644 index 0000000000000..e9d10509f7fce --- /dev/null +++ b/x-pack/plugins/apm/scripts/kibana-security/create_apm_users/power_user_role.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RoleType } from './create_role'; + +export const powerUserRole: RoleType = { + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [ + { + base: [], + feature: { + // core + discover: ['all'], + dashboard: ['all'], + canvas: ['all'], + ml: ['all'], + maps: ['all'], + graph: ['all'], + visualize: ['all'], + + // observability + logs: ['all'], + infrastructure: ['all'], + apm: ['all'], + uptime: ['all'], + + // security + siem: ['all'], + + // management + dev_tools: ['all'], + advancedSettings: ['all'], + indexPatterns: ['all'], + savedObjectsManagement: ['all'], + stackAlerts: ['all'], + fleet: ['all'], + actions: ['all'], + }, + spaces: ['*'], + }, + ], +}; diff --git a/x-pack/plugins/apm/scripts/kibana-security/create_apm_users/read_only_user_role.ts b/x-pack/plugins/apm/scripts/kibana-security/create_apm_users/read_only_user_role.ts new file mode 100644 index 0000000000000..794531da73a53 --- /dev/null +++ b/x-pack/plugins/apm/scripts/kibana-security/create_apm_users/read_only_user_role.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RoleType } from './create_role'; + +export const readOnlyUserRole: RoleType = { + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [ + { + base: [], + feature: { + // core + discover: ['read'], + dashboard: ['read'], + canvas: ['read'], + ml: ['read'], + maps: ['read'], + graph: ['read'], + visualize: ['read'], + + // observability + logs: ['read'], + infrastructure: ['read'], + apm: ['read'], + uptime: ['read'], + + // security + siem: ['read'], + + // management + dev_tools: ['read'], + advancedSettings: ['read'], + indexPatterns: ['read'], + savedObjectsManagement: ['read'], + stackAlerts: ['read'], + fleet: ['read'], + actions: ['read'], + }, + spaces: ['*'], + }, + ], +}; diff --git a/x-pack/plugins/apm/scripts/kibana-security/create_kibana_user_role.ts b/x-pack/plugins/apm/scripts/kibana-security/create_kibana_user_role.ts new file mode 100644 index 0000000000000..9520df8133bba --- /dev/null +++ b/x-pack/plugins/apm/scripts/kibana-security/create_kibana_user_role.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { callKibana, isAxiosError } from './call_kibana'; +import { createAPMUsers } from './create_apm_users'; + +/* eslint-disable no-console */ + +export interface Elasticsearch { + username: string; + password: string; +} + +export interface Kibana { + roleSuffix: string; + hostname: string; +} + +export async function createKibanaUserRole({ + kibana, + elasticsearch, +}: { + kibana: Kibana; + elasticsearch: Elasticsearch; +}) { + const version = await getKibanaVersion({ + elasticsearch, + kibanaHostname: kibana.hostname, + }); + console.log(`Connected to Kibana ${version}`); + + const isSecurityEnabled = await getIsSecurityEnabled({ + elasticsearch, + kibanaHostname: kibana.hostname, + }); + if (!isSecurityEnabled) { + throw new AbortError('Security must be enabled!'); + } + + await createAPMUsers({ kibana, elasticsearch }); +} + +async function getIsSecurityEnabled({ + elasticsearch, + kibanaHostname, +}: { + elasticsearch: Elasticsearch; + kibanaHostname: string; +}) { + try { + await callKibana({ + elasticsearch, + kibanaHostname, + options: { + url: `/internal/security/me`, + }, + }); + return true; + } catch (err) { + return false; + } +} + +async function getKibanaVersion({ + elasticsearch, + kibanaHostname, +}: { + elasticsearch: Elasticsearch; + kibanaHostname: string; +}) { + try { + const res: { version: { number: number } } = await callKibana({ + elasticsearch, + kibanaHostname, + options: { + method: 'GET', + url: `/api/status`, + }, + }); + return res.version.number; + } catch (e) { + if (isAxiosError(e)) { + switch (e.response?.status) { + case 401: + throw new AbortError( + `Could not access Kibana with the provided credentials. Username: "${e.config.auth?.username}". Password: "${e.config.auth?.password}"` + ); + + case 404: + throw new AbortError( + `Could not get version on ${e.config.url} (Code: 404)` + ); + + default: + throw new AbortError( + `Cannot access Kibana on ${e.config.baseURL}. Please specify Kibana with: "--kibana-url "` + ); + } + } + throw e; + } +} + +export class AbortError extends Error { + constructor(message: string) { + super(message); + } +} diff --git a/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts b/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts index 81d5fe50e0ad0..a0264f5211379 100644 --- a/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts +++ b/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts @@ -7,46 +7,59 @@ /* eslint-disable no-console */ -import axios, { AxiosRequestConfig, AxiosError } from 'axios'; -import { union, difference, once } from 'lodash'; import { argv } from 'yargs'; +import { isAxiosError } from './call_kibana'; +import { createKibanaUserRole, AbortError } from './create_kibana_user_role'; -const KIBANA_ROLE_SUFFIX = argv.roleSuffix as string | undefined; -const ELASTICSEARCH_USERNAME = (argv.username as string) || 'elastic'; -const ELASTICSEARCH_PASSWORD = argv.password as string | undefined; -const KIBANA_BASE_URL = argv.kibanaUrl as string | undefined; +const esUserName = (argv.username as string) || 'elastic'; +const esPassword = argv.password as string | undefined; +const kibanaBaseUrl = argv.kibanaUrl as string | undefined; +const kibanaRoleSuffix = argv.roleSuffix as string | undefined; -console.log({ - KIBANA_ROLE_SUFFIX, - ELASTICSEARCH_USERNAME, - ELASTICSEARCH_PASSWORD, - KIBANA_BASE_URL, -}); +if (!esPassword) { + throw new Error( + 'Please specify credentials for elasticsearch: `--username elastic --password abcd` ' + ); +} -interface User { - username: string; - roles: string[]; - full_name?: string; - email?: string; - enabled?: boolean; +if (!kibanaBaseUrl) { + throw new Error( + 'Please specify the url for Kibana: `--kibana-url http://localhost:5601` ' + ); } -const getKibanaBasePath = once(async () => { - try { - await axios.request({ url: KIBANA_BASE_URL, maxRedirects: 0 }); - } catch (e) { - if (isAxiosError(e)) { - const location = e.response?.headers?.location; - const isBasePath = RegExp(/^\/\w{3}$/).test(location); - return isBasePath ? location : ''; - } +if ( + !kibanaBaseUrl.startsWith('https://') && + !kibanaBaseUrl.startsWith('http://') +) { + throw new Error( + 'Kibana url must be prefixed with http(s):// `--kibana-url http://localhost:5601`' + ); +} - throw e; - } - return ''; +if (!kibanaRoleSuffix) { + throw new Error( + 'Please specify a unique suffix that will be added to your roles with `--role-suffix ` ' + ); +} + +console.log({ + kibanaRoleSuffix, + esUserName, + esPassword, + kibanaBaseUrl, }); -init().catch((e) => { +createKibanaUserRole({ + kibana: { + roleSuffix: kibanaRoleSuffix, + hostname: kibanaBaseUrl, + }, + elasticsearch: { + username: esUserName, + password: esPassword, + }, +}).catch((e) => { if (e instanceof AbortError) { console.error(e.message); } else if (isAxiosError(e)) { @@ -69,324 +82,3 @@ init().catch((e) => { console.error(e); } }); - -async function init() { - if (!ELASTICSEARCH_PASSWORD) { - console.log( - 'Please specify credentials for elasticsearch: `--username elastic --password abcd` ' - ); - return; - } - - if (!KIBANA_BASE_URL) { - console.log( - 'Please specify the url for Kibana: `--kibana-url http://localhost:5601` ' - ); - return; - } - - if ( - !KIBANA_BASE_URL.startsWith('https://') && - !KIBANA_BASE_URL.startsWith('http://') - ) { - console.log( - 'Kibana url must be prefixed with http(s):// `--kibana-url http://localhost:5601`' - ); - return; - } - - if (!KIBANA_ROLE_SUFFIX) { - console.log( - 'Please specify a unique suffix that will be added to your roles with `--role-suffix ` ' - ); - return; - } - - const version = await getKibanaVersion(); - console.log(`Connected to Kibana ${version}`); - - const isEnabled = await isSecurityEnabled(); - if (!isEnabled) { - console.log('Security must be enabled!'); - return; - } - - const APM_READ_ROLE = `apm_read_${KIBANA_ROLE_SUFFIX}`; - const KIBANA_READ_ROLE = `kibana_read_${KIBANA_ROLE_SUFFIX}`; - const KIBANA_WRITE_ROLE = `kibana_write_${KIBANA_ROLE_SUFFIX}`; - const APM_USER_ROLE = 'apm_user'; - - // create roles - await createRole({ - roleName: APM_READ_ROLE, - kibanaPrivileges: { feature: { apm: ['read'] } }, - }); - await createRole({ - roleName: KIBANA_READ_ROLE, - kibanaPrivileges: { - feature: { - // core - discover: ['read'], - dashboard: ['read'], - canvas: ['read'], - ml: ['read'], - maps: ['read'], - graph: ['read'], - visualize: ['read'], - - // observability - logs: ['read'], - infrastructure: ['read'], - apm: ['read'], - uptime: ['read'], - - // security - siem: ['read'], - - // management - dev_tools: ['read'], - advancedSettings: ['read'], - indexPatterns: ['read'], - savedObjectsManagement: ['read'], - stackAlerts: ['read'], - fleet: ['read'], - actions: ['read'], - }, - }, - }); - await createRole({ - roleName: KIBANA_WRITE_ROLE, - kibanaPrivileges: { - feature: { - // core - discover: ['all'], - dashboard: ['all'], - canvas: ['all'], - ml: ['all'], - maps: ['all'], - graph: ['all'], - visualize: ['all'], - - // observability - logs: ['all'], - infrastructure: ['all'], - apm: ['all'], - uptime: ['all'], - - // security - siem: ['all'], - - // management - dev_tools: ['all'], - advancedSettings: ['all'], - indexPatterns: ['all'], - savedObjectsManagement: ['all'], - stackAlerts: ['all'], - fleet: ['all'], - actions: ['all'], - }, - }, - }); - - // read access only to APM + apm index access - await createOrUpdateUser({ - username: 'apm_read_user', - roles: [APM_USER_ROLE, APM_READ_ROLE], - }); - - // read access to all apps + apm index access - await createOrUpdateUser({ - username: 'kibana_read_user', - roles: [APM_USER_ROLE, KIBANA_READ_ROLE], - }); - - // read/write access to all apps + apm index access - await createOrUpdateUser({ - username: 'kibana_write_user', - roles: [APM_USER_ROLE, KIBANA_WRITE_ROLE], - }); -} - -async function isSecurityEnabled() { - try { - await callKibana({ - url: `/internal/security/me`, - }); - return true; - } catch (err) { - return false; - } -} - -async function callKibana(options: AxiosRequestConfig): Promise { - const kibanaBasePath = await getKibanaBasePath(); - - if (!ELASTICSEARCH_PASSWORD) { - throw new Error('Missing `--password`'); - } - - const { data } = await axios.request({ - ...options, - baseURL: KIBANA_BASE_URL + kibanaBasePath, - auth: { - username: ELASTICSEARCH_USERNAME, - password: ELASTICSEARCH_PASSWORD, - }, - headers: { 'kbn-xsrf': 'true', ...options.headers }, - }); - return data; -} - -type Privilege = [] | ['read'] | ['all']; - -async function createRole({ - roleName, - kibanaPrivileges, -}: { - roleName: string; - kibanaPrivileges: { base?: Privilege; feature?: Record }; -}) { - const role = await getRole(roleName); - if (role) { - console.log(`Skipping: Role "${roleName}" already exists`); - return; - } - - await callKibana({ - method: 'PUT', - url: `/api/security/role/${roleName}`, - data: { - metadata: { version: 1 }, - elasticsearch: { cluster: [], indices: [] }, - kibana: [ - { - base: kibanaPrivileges.base ?? [], - feature: kibanaPrivileges.feature ?? {}, - spaces: ['*'], - }, - ], - }, - }); - - console.log( - `Created role "${roleName}" with privilege "${JSON.stringify( - kibanaPrivileges - )}"` - ); -} - -async function createOrUpdateUser(newUser: User) { - const existingUser = await getUser(newUser.username); - if (!existingUser) { - return createUser(newUser); - } - - return updateUser(existingUser, newUser); -} - -async function createUser(newUser: User) { - const user = await callKibana({ - method: 'POST', - url: `/internal/security/users/${newUser.username}`, - data: { - ...newUser, - enabled: true, - password: ELASTICSEARCH_PASSWORD, - }, - }); - - console.log(`User "${newUser.username}" was created`); - return user; -} - -async function updateUser(existingUser: User, newUser: User) { - const { username } = newUser; - const allRoles = union(existingUser.roles, newUser.roles); - const hasAllRoles = difference(allRoles, existingUser.roles).length === 0; - if (hasAllRoles) { - console.log( - `Skipping: User "${username}" already has neccesarry roles: "${newUser.roles}"` - ); - return; - } - - // assign role to user - await callKibana({ - method: 'POST', - url: `/internal/security/users/${username}`, - data: { ...existingUser, roles: allRoles }, - }); - - console.log(`User "${username}" was updated`); -} - -async function getUser(username: string) { - try { - return await callKibana({ - url: `/internal/security/users/${username}`, - }); - } catch (e) { - // return empty if user doesn't exist - if (isAxiosError(e) && e.response?.status === 404) { - return null; - } - - throw e; - } -} - -async function getRole(roleName: string) { - try { - return await callKibana({ - method: 'GET', - url: `/api/security/role/${roleName}`, - }); - } catch (e) { - // return empty if role doesn't exist - if (isAxiosError(e) && e.response?.status === 404) { - return null; - } - - throw e; - } -} - -async function getKibanaVersion() { - try { - const res: { version: { number: number } } = await callKibana({ - method: 'GET', - url: `/api/status`, - }); - return res.version.number; - } catch (e) { - if (isAxiosError(e)) { - switch (e.response?.status) { - case 401: - throw new AbortError( - `Could not access Kibana with the provided credentials. Username: "${e.config.auth?.username}". Password: "${e.config.auth?.password}"` - ); - - case 404: - throw new AbortError( - `Could not get version on ${e.config.url} (Code: 404)` - ); - - default: - throw new AbortError( - `Cannot access Kibana on ${e.config.baseURL}. Please specify Kibana with: "--kibana-url "` - ); - } - } - throw e; - } -} - -function isAxiosError(e: AxiosError | Error): e is AxiosError { - return 'isAxiosError' in e; -} - -class AbortError extends Error { - constructor(message: string) { - super(message); - } -} From c0395c9ef6182b4fced997ad371426e837af3fb6 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 10 Aug 2021 22:12:45 -0700 Subject: [PATCH 063/104] [build_ts_refs] improve caches, allow building a subset of projects (#107981) * [build_ts_refs] improve caches, allow building a subset of projects * cleanup project def script and update refs in type check script * rename browser_bazel config to avoid kebab-case * remove execInProjects() helper * list references for tsconfig.types.json for api-extractor workload * disable composite features of tsconfig.types.json for api-extractor * set declaration: true to avoid weird debug error * fix jest tests Co-authored-by: spalger --- .gitignore | 3 + BUILD.bazel | 2 + .../best-practices/typescript.asciidoc | 1 - examples/bfetch_explorer/tsconfig.json | 5 +- .../tsconfig.json | 10 +- examples/developer_examples/tsconfig.json | 3 +- examples/embeddable_examples/tsconfig.json | 9 +- examples/embeddable_explorer/tsconfig.json | 9 +- examples/expressions_explorer/tsconfig.json | 7 +- examples/hello_world/tsconfig.json | 6 +- .../tsconfig.json | 8 +- examples/locator_examples/tsconfig.json | 6 +- examples/locator_explorer/tsconfig.json | 8 +- examples/preboot_example/tsconfig.json | 1 - examples/routing_example/tsconfig.json | 6 +- .../screenshot_mode_example/tsconfig.json | 11 +- examples/search_examples/tsconfig.json | 11 +- .../state_containers_examples/tsconfig.json | 6 +- examples/ui_action_examples/tsconfig.json | 5 +- examples/ui_actions_explorer/tsconfig.json | 6 +- packages/elastic-datemath/BUILD.bazel | 2 +- packages/elastic-datemath/tsconfig.json | 3 +- .../elastic-safer-lodash-set/tsconfig.json | 6 +- packages/kbn-ace/BUILD.bazel | 2 +- packages/kbn-ace/tsconfig.json | 3 +- packages/kbn-analytics/BUILD.bazel | 3 +- packages/kbn-analytics/tsconfig.json | 3 +- packages/kbn-apm-config-loader/BUILD.bazel | 2 +- packages/kbn-apm-config-loader/tsconfig.json | 3 +- packages/kbn-apm-utils/BUILD.bazel | 2 +- packages/kbn-apm-utils/tsconfig.json | 3 +- packages/kbn-cli-dev-mode/BUILD.bazel | 2 +- packages/kbn-cli-dev-mode/tsconfig.json | 3 +- packages/kbn-config-schema/BUILD.bazel | 2 +- packages/kbn-config-schema/tsconfig.json | 3 +- packages/kbn-config/BUILD.bazel | 2 +- packages/kbn-config/tsconfig.json | 3 +- packages/kbn-crypto/BUILD.bazel | 2 +- packages/kbn-crypto/tsconfig.json | 5 +- packages/kbn-dev-utils/BUILD.bazel | 3 +- packages/kbn-dev-utils/src/extract.ts | 34 +++- .../src/proc_runner/proc_runner.ts | 9 +- packages/kbn-dev-utils/tsconfig.json | 4 +- packages/kbn-docs-utils/BUILD.bazel | 2 +- .../tests/__fixtures__/src/tsconfig.json | 2 +- packages/kbn-docs-utils/tsconfig.json | 6 +- packages/kbn-es-archiver/BUILD.bazel | 2 +- packages/kbn-es-archiver/tsconfig.json | 6 +- packages/kbn-es-query/BUILD.bazel | 2 +- packages/kbn-es-query/tsconfig.json | 3 +- packages/kbn-es/tsconfig.json | 4 +- packages/kbn-expect/tsconfig.json | 4 +- packages/kbn-field-types/BUILD.bazel | 2 +- packages/kbn-field-types/tsconfig.json | 3 +- packages/kbn-i18n/BUILD.bazel | 2 +- packages/kbn-i18n/tsconfig.json | 3 +- packages/kbn-interpreter/BUILD.bazel | 2 +- packages/kbn-interpreter/tsconfig.json | 5 +- packages/kbn-io-ts-utils/BUILD.bazel | 2 +- packages/kbn-io-ts-utils/tsconfig.json | 5 +- packages/kbn-legacy-logging/BUILD.bazel | 2 +- packages/kbn-legacy-logging/tsconfig.json | 3 +- packages/kbn-logging/BUILD.bazel | 2 +- packages/kbn-logging/tsconfig.json | 3 +- packages/kbn-mapbox-gl/BUILD.bazel | 2 +- packages/kbn-mapbox-gl/tsconfig.json | 5 +- packages/kbn-monaco/BUILD.bazel | 2 +- packages/kbn-monaco/tsconfig.json | 3 +- packages/kbn-optimizer/BUILD.bazel | 2 +- packages/kbn-optimizer/tsconfig.json | 5 +- packages/kbn-plugin-generator/BUILD.bazel | 2 +- .../template/tsconfig.json.ejs | 3 +- packages/kbn-plugin-generator/tsconfig.json | 3 +- packages/kbn-plugin-helpers/BUILD.bazel | 2 +- packages/kbn-plugin-helpers/tsconfig.json | 3 +- packages/kbn-pm/tsconfig.json | 4 +- packages/kbn-rule-data-utils/BUILD.bazel | 2 +- packages/kbn-rule-data-utils/tsconfig.json | 5 +- .../BUILD.bazel | 4 +- .../tsconfig.browser.json | 3 +- .../tsconfig.json | 3 +- .../kbn-securitysolution-es-utils/BUILD.bazel | 4 +- .../tsconfig.json | 3 +- .../BUILD.bazel | 4 +- .../tsconfig.json | 3 +- .../BUILD.bazel | 2 +- .../tsconfig.json | 3 +- .../BUILD.bazel | 2 +- .../tsconfig.json | 3 +- .../BUILD.bazel | 2 +- .../tsconfig.json | 3 +- .../BUILD.bazel | 2 +- .../tsconfig.json | 3 +- .../kbn-securitysolution-list-api/BUILD.bazel | 4 +- .../tsconfig.json | 3 +- .../BUILD.bazel | 4 +- .../tsconfig.json | 3 +- .../BUILD.bazel | 4 +- .../tsconfig.json | 3 +- .../BUILD.bazel | 4 +- .../tsconfig.json | 3 +- .../kbn-securitysolution-t-grid/BUILD.bazel | 6 +- .../tsconfig.browser.json | 3 +- .../kbn-securitysolution-t-grid/tsconfig.json | 3 +- .../kbn-securitysolution-utils/BUILD.bazel | 4 +- .../kbn-securitysolution-utils/tsconfig.json | 3 +- packages/kbn-server-http-tools/BUILD.bazel | 2 +- packages/kbn-server-http-tools/tsconfig.json | 5 +- .../kbn-server-route-repository/BUILD.bazel | 2 +- .../kbn-server-route-repository/tsconfig.json | 3 +- packages/kbn-std/BUILD.bazel | 2 +- packages/kbn-std/tsconfig.json | 3 +- packages/kbn-storybook/BUILD.bazel | 2 +- packages/kbn-storybook/tsconfig.json | 2 +- packages/kbn-telemetry-tools/BUILD.bazel | 2 +- packages/kbn-telemetry-tools/tsconfig.json | 3 +- packages/kbn-test-subj-selector/tsconfig.json | 4 +- packages/kbn-test/BUILD.bazel | 2 +- packages/kbn-test/tsconfig.json | 3 +- packages/kbn-tinymath/tsconfig.json | 2 +- .../kbn-typed-react-router-config/BUILD.bazel | 2 +- .../tsconfig.json | 3 +- packages/kbn-ui-shared-deps/BUILD.bazel | 2 +- packages/kbn-ui-shared-deps/tsconfig.json | 5 +- packages/kbn-utility-types/BUILD.bazel | 2 +- packages/kbn-utility-types/tsconfig.json | 3 +- packages/kbn-utils/BUILD.bazel | 2 +- .../kbn-utils/src/package_json/index.test.ts | 9 +- packages/kbn-utils/src/package_json/index.ts | 6 +- packages/kbn-utils/tsconfig.json | 3 +- scripts/convert_ts_projects.js | 10 + src/core/tsconfig.json | 1 - src/dev/build/lib/config.test.ts | 7 +- .../tasks/os_packages/docker_generator/run.ts | 5 +- src/dev/typescript/build_ts_refs.ts | 42 ++-- src/dev/typescript/build_ts_refs_cli.ts | 32 ++- src/dev/typescript/concurrent_map.ts | 4 + .../typescript/convert_all_to_composite.ts | 19 ++ src/dev/typescript/exec_in_projects.ts | 69 ------- src/dev/typescript/index.ts | 2 - src/dev/typescript/project.ts | 169 +++++++++++++--- src/dev/typescript/project_set.ts | 29 +++ src/dev/typescript/projects.ts | 84 ++++---- .../ref_output_cache.test.ts | 55 +++-- .../ref_output_cache/ref_output_cache.ts | 41 +++- .../typescript/ref_output_cache/repo_info.ts | 16 ++ src/dev/typescript/ref_output_cache/zip.ts | 7 - src/dev/typescript/root_refs_config.ts | 61 ++++++ src/dev/typescript/run_type_check_cli.ts | 190 ++++++++++-------- src/plugins/advanced_settings/tsconfig.json | 1 - src/plugins/apm_oss/tsconfig.json | 1 - src/plugins/bfetch/tsconfig.json | 1 - src/plugins/charts/tsconfig.json | 1 - src/plugins/console/tsconfig.json | 1 - src/plugins/dashboard/tsconfig.json | 1 - src/plugins/data/tsconfig.json | 1 - src/plugins/dev_tools/tsconfig.json | 1 - src/plugins/discover/tsconfig.json | 1 - src/plugins/embeddable/tsconfig.json | 1 - src/plugins/es_ui_shared/tsconfig.json | 1 - src/plugins/expression_error/tsconfig.json | 1 - src/plugins/expression_image/tsconfig.json | 1 - src/plugins/expression_metric/tsconfig.json | 1 - .../expression_repeat_image/tsconfig.json | 1 - .../expression_reveal_image/tsconfig.json | 1 - src/plugins/expression_shape/tsconfig.json | 1 - src/plugins/expressions/tsconfig.json | 1 - src/plugins/field_formats/tsconfig.json | 1 - src/plugins/home/tsconfig.json | 1 - .../index_pattern_field_editor/tsconfig.json | 1 - .../index_pattern_management/tsconfig.json | 1 - .../public/input_control_vis_renderer.tsx | 3 +- .../public/vis_controller.tsx | 37 ++-- src/plugins/input_control_vis/tsconfig.json | 1 - src/plugins/inspector/tsconfig.json | 1 - src/plugins/interactive_setup/tsconfig.json | 1 - src/plugins/kibana_legacy/tsconfig.json | 1 - src/plugins/kibana_overview/tsconfig.json | 1 - src/plugins/kibana_react/tsconfig.json | 1 - .../kibana_usage_collection/tsconfig.json | 1 - src/plugins/kibana_utils/tsconfig.json | 1 - src/plugins/legacy_export/tsconfig.json | 1 - src/plugins/management/tsconfig.json | 1 - src/plugins/maps_ems/tsconfig.json | 1 - src/plugins/maps_legacy/tsconfig.json | 1 - src/plugins/navigation/tsconfig.json | 1 - src/plugins/newsfeed/tsconfig.json | 1 - src/plugins/presentation_util/tsconfig.json | 1 - src/plugins/region_map/tsconfig.json | 1 - src/plugins/saved_objects/tsconfig.json | 1 - .../saved_objects_management/tsconfig.json | 1 - .../saved_objects_tagging_oss/tsconfig.json | 1 - src/plugins/screenshot_mode/tsconfig.json | 1 - src/plugins/security_oss/tsconfig.json | 1 - src/plugins/share/tsconfig.json | 1 - src/plugins/spaces_oss/tsconfig.json | 1 - src/plugins/telemetry/tsconfig.json | 1 - .../tsconfig.json | 1 - .../tsconfig.json | 1 - src/plugins/tile_map/tsconfig.json | 1 - src/plugins/timelion/tsconfig.json | 1 - src/plugins/ui_actions/tsconfig.json | 1 - src/plugins/url_forwarding/tsconfig.json | 1 - src/plugins/usage_collection/tsconfig.json | 1 - src/plugins/vis_default_editor/tsconfig.json | 1 - src/plugins/vis_type_markdown/tsconfig.json | 1 - src/plugins/vis_type_metric/tsconfig.json | 1 - src/plugins/vis_type_pie/tsconfig.json | 47 +++-- src/plugins/vis_type_table/tsconfig.json | 1 - src/plugins/vis_type_tagcloud/tsconfig.json | 1 - src/plugins/vis_type_timelion/tsconfig.json | 1 - src/plugins/vis_type_timeseries/tsconfig.json | 1 - src/plugins/vis_type_vega/tsconfig.json | 1 - src/plugins/vis_type_vislib/tsconfig.json | 1 - src/plugins/vis_type_xy/tsconfig.json | 1 - src/plugins/visualizations/tsconfig.json | 1 - src/plugins/visualize/tsconfig.json | 1 - .../plugins/kbn_tp_run_pipeline/tsconfig.json | 9 +- .../plugins/app_link_test/tsconfig.json | 5 +- .../plugins/core_app_status/tsconfig.json | 3 +- .../plugins/core_history_block/tsconfig.json | 3 +- .../plugins/core_http/tsconfig.json | 3 +- .../plugins/core_plugin_a/tsconfig.json | 3 +- .../core_plugin_appleave/tsconfig.json | 3 +- .../plugins/core_plugin_b/tsconfig.json | 6 +- .../core_plugin_chromeless/tsconfig.json | 5 +- .../core_plugin_deep_links/tsconfig.json | 3 +- .../core_plugin_deprecations/tsconfig.json | 3 +- .../tsconfig.json | 3 +- .../core_plugin_helpmenu/tsconfig.json | 3 +- .../core_plugin_route_timeouts/tsconfig.json | 3 +- .../core_plugin_static_assets/tsconfig.json | 3 +- .../core_provider_plugin/tsconfig.json | 3 +- .../plugins/data_search/tsconfig.json | 6 +- .../plugins/doc_views_plugin/tsconfig.json | 6 +- .../elasticsearch_client_plugin/tsconfig.json | 3 +- .../plugins/index_patterns/tsconfig.json | 6 +- .../kbn_sample_panel_action/tsconfig.json | 8 +- .../plugins/kbn_top_nav/tsconfig.json | 6 +- .../tsconfig.json | 14 +- .../management_test_plugin/tsconfig.json | 6 +- .../plugins/rendering_plugin/tsconfig.json | 3 +- .../tsconfig.json | 3 +- .../tsconfig.json | 3 +- .../saved_objects_hidden_type/tsconfig.json | 3 +- .../session_notifications/tsconfig.json | 7 +- .../plugins/telemetry/tsconfig.json | 8 +- .../plugins/ui_settings_plugin/tsconfig.json | 3 +- .../plugins/usage_collection/tsconfig.json | 6 +- .../type_check_plugin_public_api_docs.sh | 1 - .../plugins/status_plugin_a/tsconfig.json | 4 +- .../plugins/status_plugin_b/tsconfig.json | 4 +- test/tsconfig.json | 1 - tsconfig.base.json | 2 + tsconfig.bazel.json | 7 + tsconfig.browser_bazel.json | 7 + tsconfig.json | 119 +---------- tsconfig.refs.json | 126 ------------ tsconfig.types.json | 6 +- .../examples/alerting_example/tsconfig.json | 11 +- .../embedded_lens_example/tsconfig.json | 11 +- .../examples/reporting_example/tsconfig.json | 8 +- .../tsconfig.json | 11 +- x-pack/plugins/actions/tsconfig.json | 1 - x-pack/plugins/alerting/tsconfig.json | 1 - x-pack/plugins/apm/e2e/tsconfig.json | 5 +- x-pack/plugins/apm/ftr_e2e/config.ts | 2 - x-pack/plugins/apm/ftr_e2e/tsconfig.json | 18 +- x-pack/plugins/apm/tsconfig.json | 1 - x-pack/plugins/banners/tsconfig.json | 1 - x-pack/plugins/canvas/tsconfig.json | 1 - x-pack/plugins/cases/tsconfig.json | 1 - x-pack/plugins/cloud/tsconfig.json | 1 - .../cross_cluster_replication/tsconfig.json | 1 - .../plugins/dashboard_enhanced/tsconfig.json | 1 - x-pack/plugins/dashboard_mode/tsconfig.json | 1 - x-pack/plugins/data_enhanced/tsconfig.json | 1 - x-pack/plugins/data_visualizer/tsconfig.json | 1 - .../plugins/discover_enhanced/tsconfig.json | 1 - .../drilldowns/url_drilldown/tsconfig.json | 1 - .../plugins/embeddable_enhanced/tsconfig.json | 1 - .../encrypted_saved_objects/tsconfig.json | 1 - .../plugins/enterprise_search/tsconfig.json | 1 - x-pack/plugins/event_log/tsconfig.json | 1 - x-pack/plugins/features/tsconfig.json | 1 - x-pack/plugins/file_upload/tsconfig.json | 1 - x-pack/plugins/fleet/tsconfig.json | 1 - x-pack/plugins/global_search/tsconfig.json | 1 - .../plugins/global_search_bar/tsconfig.json | 1 - .../global_search_providers/tsconfig.json | 1 - x-pack/plugins/graph/tsconfig.json | 55 +++-- x-pack/plugins/grokdebugger/tsconfig.json | 1 - .../index_lifecycle_management/tsconfig.json | 1 - x-pack/plugins/index_management/tsconfig.json | 1 - x-pack/plugins/infra/tsconfig.json | 1 - x-pack/plugins/ingest_pipelines/tsconfig.json | 1 - x-pack/plugins/lens/tsconfig.json | 79 ++++---- .../plugins/license_api_guard/tsconfig.json | 1 - .../plugins/license_management/tsconfig.json | 1 - x-pack/plugins/licensing/tsconfig.json | 1 - x-pack/plugins/lists/tsconfig.json | 41 ++-- x-pack/plugins/logstash/tsconfig.json | 45 ++--- x-pack/plugins/maps/tsconfig.json | 1 - x-pack/plugins/metrics_entities/tsconfig.json | 1 - .../routes/apidoc_scripts/tsconfig.json | 4 +- x-pack/plugins/ml/tsconfig.json | 1 - x-pack/plugins/monitoring/tsconfig.json | 1 - x-pack/plugins/observability/tsconfig.json | 1 - x-pack/plugins/osquery/cypress/tsconfig.json | 8 +- x-pack/plugins/osquery/tsconfig.json | 1 - x-pack/plugins/painless_lab/tsconfig.json | 1 - x-pack/plugins/remote_clusters/tsconfig.json | 1 - x-pack/plugins/reporting/tsconfig.json | 1 - x-pack/plugins/rollup/tsconfig.json | 1 - x-pack/plugins/rule_registry/tsconfig.json | 1 - x-pack/plugins/runtime_fields/tsconfig.json | 1 - .../saved_objects_tagging/tsconfig.json | 1 - x-pack/plugins/searchprofiler/tsconfig.json | 1 - x-pack/plugins/security/tsconfig.json | 1 - .../security_solution/cypress/tsconfig.json | 12 +- .../plugins/security_solution/tsconfig.json | 89 ++++---- x-pack/plugins/snapshot_restore/tsconfig.json | 1 - x-pack/plugins/spaces/tsconfig.json | 1 - x-pack/plugins/stack_alerts/tsconfig.json | 1 - x-pack/plugins/task_manager/tsconfig.json | 1 - .../telemetry_collection_xpack/tsconfig.json | 1 - x-pack/plugins/timelines/tsconfig.json | 57 +++--- x-pack/plugins/transform/tsconfig.json | 1 - x-pack/plugins/translations/tsconfig.json | 1 - .../plugins/triggers_actions_ui/tsconfig.json | 1 - .../plugins/ui_actions_enhanced/tsconfig.json | 1 - .../plugins/upgrade_assistant/tsconfig.json | 1 - x-pack/plugins/uptime/tsconfig.json | 3 +- x-pack/plugins/watcher/tsconfig.json | 1 - x-pack/plugins/xpack_legacy/tsconfig.json | 1 - x-pack/test/tsconfig.json | 1 - .../application_usage_test/tsconfig.json | 3 +- .../stack_management_usage_test/tsconfig.json | 3 +- 338 files changed, 1165 insertions(+), 1284 deletions(-) create mode 100644 scripts/convert_ts_projects.js create mode 100644 src/dev/typescript/convert_all_to_composite.ts delete mode 100644 src/dev/typescript/exec_in_projects.ts create mode 100644 src/dev/typescript/project_set.ts create mode 100644 src/dev/typescript/root_refs_config.ts create mode 100644 tsconfig.bazel.json create mode 100644 tsconfig.browser_bazel.json delete mode 100644 tsconfig.refs.json diff --git a/.gitignore b/.gitignore index c0588d850062e..4f77a6e450c4b 100644 --- a/.gitignore +++ b/.gitignore @@ -75,6 +75,9 @@ report.asciidoc # TS incremental build cache *.tsbuildinfo +# Automatically generated and user-modifiable +/tsconfig.refs.json + # Yarn local mirror content .yarn-local-mirror diff --git a/BUILD.bazel b/BUILD.bazel index 1f6e3030e5d0c..e838d312d8762 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -3,7 +3,9 @@ exports_files( [ "tsconfig.base.json", + "tsconfig.bazel.json", "tsconfig.browser.json", + "tsconfig.browser_bazel.json", "tsconfig.json", "package.json" ], diff --git a/docs/developer/best-practices/typescript.asciidoc b/docs/developer/best-practices/typescript.asciidoc index f6db3fdffcb6a..6058cb4945e11 100644 --- a/docs/developer/best-practices/typescript.asciidoc +++ b/docs/developer/best-practices/typescript.asciidoc @@ -41,7 +41,6 @@ Additionally, in order to migrate into project refs, you also need to make sure { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/examples/bfetch_explorer/tsconfig.json b/examples/bfetch_explorer/tsconfig.json index 253fdd9ee6c89..fe909968bd8e2 100644 --- a/examples/bfetch_explorer/tsconfig.json +++ b/examples/bfetch_explorer/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types", }, "include": [ "index.ts", @@ -14,6 +13,8 @@ "exclude": [], "references": [ { "path": "../../src/core/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" }, + { "path": "../../src/plugins/bfetch/tsconfig.json" }, { "path": "../../src/plugins/kibana_react/tsconfig.json" }, ] } diff --git a/examples/dashboard_embeddable_examples/tsconfig.json b/examples/dashboard_embeddable_examples/tsconfig.json index 86b35c5e4943f..1c10d59de561f 100644 --- a/examples/dashboard_embeddable_examples/tsconfig.json +++ b/examples/dashboard_embeddable_examples/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -13,6 +12,11 @@ ], "exclude": [], "references": [ - { "path": "../../src/core/tsconfig.json" } + { "path": "../../src/core/tsconfig.json" }, + { "path": "../../src/plugins/dashboard/tsconfig.json" }, + { "path": "../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../src/plugins/es_ui_shared/tsconfig.json" }, + { "path": "../embeddable_examples/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" }, ] } diff --git a/examples/developer_examples/tsconfig.json b/examples/developer_examples/tsconfig.json index 86b35c5e4943f..23b24a38d1aef 100644 --- a/examples/developer_examples/tsconfig.json +++ b/examples/developer_examples/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/examples/embeddable_examples/tsconfig.json b/examples/embeddable_examples/tsconfig.json index a922c6defcd4c..34c7c8e04467e 100644 --- a/examples/embeddable_examples/tsconfig.json +++ b/examples/embeddable_examples/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -15,6 +14,12 @@ "exclude": [], "references": [ { "path": "../../src/core/tsconfig.json" }, + { "path": "../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../src/plugins/ui_actions/tsconfig.json" }, + { "path": "../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../src/plugins/dashboard/tsconfig.json" }, + { "path": "../../src/plugins/saved_objects/tsconfig.json" }, + { "path": "../../src/plugins/presentation_util/tsconfig.json" }, ] } diff --git a/examples/embeddable_explorer/tsconfig.json b/examples/embeddable_explorer/tsconfig.json index 4baebebcea42e..e5b19e2c1457a 100644 --- a/examples/embeddable_explorer/tsconfig.json +++ b/examples/embeddable_explorer/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -14,6 +13,10 @@ "exclude": [], "references": [ { "path": "../../src/core/tsconfig.json" }, - { "path": "../../src/plugins/inspector/tsconfig.json" } + { "path": "../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../src/plugins/ui_actions/tsconfig.json" }, + { "path": "../../src/plugins/inspector/tsconfig.json" }, + { "path": "../embeddable_examples/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" }, ] } diff --git a/examples/expressions_explorer/tsconfig.json b/examples/expressions_explorer/tsconfig.json index b4449819b25a6..f3451b496caa0 100644 --- a/examples/expressions_explorer/tsconfig.json +++ b/examples/expressions_explorer/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -14,5 +13,9 @@ "references": [ { "path": "../../src/core/tsconfig.json" }, { "path": "../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../src/plugins/expressions/tsconfig.json" }, + { "path": "../../src/plugins/ui_actions/tsconfig.json" }, + { "path": "../../src/plugins/inspector/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" }, ] } diff --git a/examples/hello_world/tsconfig.json b/examples/hello_world/tsconfig.json index 7fa03739119b4..06d3953e4a6bf 100644 --- a/examples/hello_world/tsconfig.json +++ b/examples/hello_world/tsconfig.json @@ -12,5 +12,9 @@ "server/**/*.ts", "../../typings/**/*" ], - "exclude": [] + "exclude": [], + "references": [ + { "path": "../../src/core/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" }, + ] } diff --git a/examples/index_pattern_field_editor_example/tsconfig.json b/examples/index_pattern_field_editor_example/tsconfig.json index 1f6d52ed5260e..f11ca18fc1a82 100644 --- a/examples/index_pattern_field_editor_example/tsconfig.json +++ b/examples/index_pattern_field_editor_example/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -14,5 +13,8 @@ "references": [ { "path": "../../src/core/tsconfig.json" }, { "path": "../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../src/plugins/data/tsconfig.json" }, + { "path": "../../src/plugins/index_pattern_field_editor/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" }, ] -} \ No newline at end of file +} diff --git a/examples/locator_examples/tsconfig.json b/examples/locator_examples/tsconfig.json index 8aef3328b4222..5010ad5a5fe0f 100644 --- a/examples/locator_examples/tsconfig.json +++ b/examples/locator_examples/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -13,6 +12,7 @@ ], "exclude": [], "references": [ - { "path": "../../src/core/tsconfig.json" } + { "path": "../../src/core/tsconfig.json" }, + { "path": "../../src/plugins/share/tsconfig.json" }, ] } diff --git a/examples/locator_explorer/tsconfig.json b/examples/locator_explorer/tsconfig.json index 8aef3328b4222..2fa75fc163fdd 100644 --- a/examples/locator_explorer/tsconfig.json +++ b/examples/locator_explorer/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -13,6 +12,9 @@ ], "exclude": [], "references": [ - { "path": "../../src/core/tsconfig.json" } + { "path": "../../src/core/tsconfig.json" }, + { "path": "../../src/plugins/share/tsconfig.json" }, + { "path": "../locator_examples/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" }, ] } diff --git a/examples/preboot_example/tsconfig.json b/examples/preboot_example/tsconfig.json index d18953eadf330..e5b5eda0c6478 100644 --- a/examples/preboot_example/tsconfig.json +++ b/examples/preboot_example/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/examples/routing_example/tsconfig.json b/examples/routing_example/tsconfig.json index 54ac800019f82..e47bf1c9bedb8 100644 --- a/examples/routing_example/tsconfig.json +++ b/examples/routing_example/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -14,6 +13,7 @@ ], "exclude": [], "references": [ - { "path": "../../src/core/tsconfig.json" } + { "path": "../../src/core/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" }, ] } diff --git a/examples/screenshot_mode_example/tsconfig.json b/examples/screenshot_mode_example/tsconfig.json index dfb436e7377ac..ef35abc699c66 100644 --- a/examples/screenshot_mode_example/tsconfig.json +++ b/examples/screenshot_mode_example/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -13,5 +12,11 @@ "../../typings/**/*" ], "exclude": [], - "references": [{ "path": "../../src/core/tsconfig.json" }] + "references": [ + { "path": "../../src/core/tsconfig.json" }, + { "path": "../../src/plugins/navigation/tsconfig.json" }, + { "path": "../../src/plugins/screenshot_mode/tsconfig.json" }, + { "path": "../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" }, + ] } diff --git a/examples/search_examples/tsconfig.json b/examples/search_examples/tsconfig.json index 2b7d86d76a8a5..547952b8dd3d8 100644 --- a/examples/search_examples/tsconfig.json +++ b/examples/search_examples/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -14,6 +13,12 @@ ], "exclude": [], "references": [ - { "path": "../../src/core/tsconfig.json" } + { "path": "../../src/core/tsconfig.json" }, + { "path": "../../src/plugins/data/tsconfig.json" }, + { "path": "../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../src/plugins/navigation/tsconfig.json" }, + { "path": "../../src/plugins/share/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" }, ] } diff --git a/examples/state_containers_examples/tsconfig.json b/examples/state_containers_examples/tsconfig.json index 442fa08022dc4..fc266cbe2c83f 100644 --- a/examples/state_containers_examples/tsconfig.json +++ b/examples/state_containers_examples/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -17,5 +16,8 @@ { "path": "../../src/core/tsconfig.json" }, { "path": "../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../src/plugins/navigation/tsconfig.json" }, + { "path": "../../src/plugins/data/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" }, ] } diff --git a/examples/ui_action_examples/tsconfig.json b/examples/ui_action_examples/tsconfig.json index 253fdd9ee6c89..41d91ac4b9be6 100644 --- a/examples/ui_action_examples/tsconfig.json +++ b/examples/ui_action_examples/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -13,7 +12,7 @@ ], "exclude": [], "references": [ - { "path": "../../src/core/tsconfig.json" }, { "path": "../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../src/plugins/ui_actions/tsconfig.json" }, ] } diff --git a/examples/ui_actions_explorer/tsconfig.json b/examples/ui_actions_explorer/tsconfig.json index b4449819b25a6..6debf7c0c650a 100644 --- a/examples/ui_actions_explorer/tsconfig.json +++ b/examples/ui_actions_explorer/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -14,5 +13,8 @@ "references": [ { "path": "../../src/core/tsconfig.json" }, { "path": "../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../src/plugins/ui_actions/tsconfig.json" }, + { "path": "../ui_action_examples/tsconfig.json" }, + { "path": "../developer_examples/tsconfig.json" }, ] } diff --git a/packages/elastic-datemath/BUILD.bazel b/packages/elastic-datemath/BUILD.bazel index 9244705c01550..04eb153bcc9e1 100644 --- a/packages/elastic-datemath/BUILD.bazel +++ b/packages/elastic-datemath/BUILD.bazel @@ -39,6 +39,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -50,7 +51,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/elastic-datemath/tsconfig.json b/packages/elastic-datemath/tsconfig.json index dcfadbb7cf262..02465bebe519a 100644 --- a/packages/elastic-datemath/tsconfig.json +++ b/packages/elastic-datemath/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "target_types", "rootDir": "src", "sourceMap": true, diff --git a/packages/elastic-safer-lodash-set/tsconfig.json b/packages/elastic-safer-lodash-set/tsconfig.json index 5a29c6ff2dd88..48aa6ed341d61 100644 --- a/packages/elastic-safer-lodash-set/tsconfig.json +++ b/packages/elastic-safer-lodash-set/tsconfig.json @@ -1,8 +1,6 @@ { - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "incremental": false, - }, + "extends": "../../tsconfig.bazel.json", + "compilerOptions": {}, "include": [ "**/*", ], diff --git a/packages/kbn-ace/BUILD.bazel b/packages/kbn-ace/BUILD.bazel index 49c9c6f783259..5b9c38b16b53a 100644 --- a/packages/kbn-ace/BUILD.bazel +++ b/packages/kbn-ace/BUILD.bazel @@ -55,6 +55,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -66,7 +67,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-ace/tsconfig.json b/packages/kbn-ace/tsconfig.json index 8fa6be6fb3fc3..4a132efa09118 100644 --- a/packages/kbn-ace/tsconfig.json +++ b/packages/kbn-ace/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "./target_types", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-ace/src", diff --git a/packages/kbn-analytics/BUILD.bazel b/packages/kbn-analytics/BUILD.bazel index 9f78d99b78710..ca8cdcbffbb52 100644 --- a/packages/kbn-analytics/BUILD.bazel +++ b/packages/kbn-analytics/BUILD.bazel @@ -50,6 +50,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -59,6 +60,7 @@ ts_config( deps = [ "//:tsconfig.base.json", "//:tsconfig.browser.json", + "//:tsconfig.browser_bazel.json", ], ) @@ -70,7 +72,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", root_dir = "src", source_map = True, diff --git a/packages/kbn-analytics/tsconfig.json b/packages/kbn-analytics/tsconfig.json index b24ed9ea137a5..dfae54ad91c8e 100644 --- a/packages/kbn-analytics/tsconfig.json +++ b/packages/kbn-analytics/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "isolatedModules": true, "outDir": "./target_types", "sourceMap": true, diff --git a/packages/kbn-apm-config-loader/BUILD.bazel b/packages/kbn-apm-config-loader/BUILD.bazel index 2ace393bcc4a3..81189171412b6 100644 --- a/packages/kbn-apm-config-loader/BUILD.bazel +++ b/packages/kbn-apm-config-loader/BUILD.bazel @@ -53,6 +53,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -64,7 +65,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-apm-config-loader/tsconfig.json b/packages/kbn-apm-config-loader/tsconfig.json index 314c7969b3f94..2f6da800d9dd2 100644 --- a/packages/kbn-apm-config-loader/tsconfig.json +++ b/packages/kbn-apm-config-loader/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "./target_types", "rootDir": "./src", "sourceMap": true, diff --git a/packages/kbn-apm-utils/BUILD.bazel b/packages/kbn-apm-utils/BUILD.bazel index c75ffb4207122..fdfd593476fe7 100644 --- a/packages/kbn-apm-utils/BUILD.bazel +++ b/packages/kbn-apm-utils/BUILD.bazel @@ -40,6 +40,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -51,7 +52,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-apm-utils/tsconfig.json b/packages/kbn-apm-utils/tsconfig.json index 08fa0e8a45088..d7056cda6d71a 100644 --- a/packages/kbn-apm-utils/tsconfig.json +++ b/packages/kbn-apm-utils/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "target_types", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-cli-dev-mode/BUILD.bazel b/packages/kbn-cli-dev-mode/BUILD.bazel index 608c781017dd6..e66a621781234 100644 --- a/packages/kbn-cli-dev-mode/BUILD.bazel +++ b/packages/kbn-cli-dev-mode/BUILD.bazel @@ -82,6 +82,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -93,7 +94,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", root_dir = "src", source_map = True, diff --git a/packages/kbn-cli-dev-mode/tsconfig.json b/packages/kbn-cli-dev-mode/tsconfig.json index 18fda8b978917..6ce2e21b84674 100644 --- a/packages/kbn-cli-dev-mode/tsconfig.json +++ b/packages/kbn-cli-dev-mode/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "./target_types", "rootDir": "./src", "sourceMap": true, diff --git a/packages/kbn-config-schema/BUILD.bazel b/packages/kbn-config-schema/BUILD.bazel index 2a4718b87df14..70de78b7617c9 100644 --- a/packages/kbn-config-schema/BUILD.bazel +++ b/packages/kbn-config-schema/BUILD.bazel @@ -51,6 +51,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -62,7 +63,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-config-schema/tsconfig.json b/packages/kbn-config-schema/tsconfig.json index de9476bbbb35c..79b652daf7ec1 100644 --- a/packages/kbn-config-schema/tsconfig.json +++ b/packages/kbn-config-schema/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "target_types", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-config/BUILD.bazel b/packages/kbn-config/BUILD.bazel index b11306be969b0..75e4428ed2d70 100644 --- a/packages/kbn-config/BUILD.bazel +++ b/packages/kbn-config/BUILD.bazel @@ -68,6 +68,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -79,7 +80,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-config/tsconfig.json b/packages/kbn-config/tsconfig.json index 20855c6073073..0971923a11a0f 100644 --- a/packages/kbn-config/tsconfig.json +++ b/packages/kbn-config/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "./target_types", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-crypto/BUILD.bazel b/packages/kbn-crypto/BUILD.bazel index 7419145f62888..36b61d0fb046b 100644 --- a/packages/kbn-crypto/BUILD.bazel +++ b/packages/kbn-crypto/BUILD.bazel @@ -48,6 +48,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -58,7 +59,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-crypto/tsconfig.json b/packages/kbn-crypto/tsconfig.json index 8b7be5e8a3c9b..af1a7c75c8e99 100644 --- a/packages/kbn-crypto/tsconfig.json +++ b/packages/kbn-crypto/tsconfig.json @@ -1,8 +1,7 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": true, - "outDir": "./target", + "outDir": "./target/types", "declaration": true, "declarationMap": true, "rootDir": "src", diff --git a/packages/kbn-dev-utils/BUILD.bazel b/packages/kbn-dev-utils/BUILD.bazel index 65935a0d73868..502bcd05b74f8 100644 --- a/packages/kbn-dev-utils/BUILD.bazel +++ b/packages/kbn-dev-utils/BUILD.bazel @@ -56,7 +56,6 @@ RUNTIME_DEPS = [ "@npm//globby", "@npm//load-json-file", "@npm//markdown-it", - "@npm//moment", "@npm//normalize-path", "@npm//rxjs", "@npm//tar", @@ -100,6 +99,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -111,7 +111,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-dev-utils/src/extract.ts b/packages/kbn-dev-utils/src/extract.ts index 05ad2b4bd99ec..138e6bd67a50c 100644 --- a/packages/kbn-dev-utils/src/extract.ts +++ b/packages/kbn-dev-utils/src/extract.ts @@ -12,12 +12,16 @@ import Path from 'path'; import { pipeline } from 'stream'; import { promisify } from 'util'; -import { lastValueFrom } from '@kbn/std'; import Tar from 'tar'; import Yauzl, { ZipFile, Entry } from 'yauzl'; import * as Rx from 'rxjs'; import { map, mergeMap, takeUntil } from 'rxjs/operators'; +const strComplete = (obs: Rx.Observable) => + new Promise((resolve, reject) => { + obs.subscribe({ complete: resolve, error: reject }); + }); + const asyncPipeline = promisify(pipeline); interface Options { @@ -36,6 +40,11 @@ interface Options { * Number of path segments to strip form paths in the archive, like --strip-components from tar */ stripComponents?: number; + + /** + * Write modified timestamps to extracted files + */ + setModifiedTimes?: Date; } /** @@ -43,7 +52,12 @@ interface Options { * for both archive types, only tested with familiar archives we create so might not * support some weird exotic zip features we don't use in our own snapshot/build tooling */ -export async function extract({ archivePath, targetDir, stripComponents = 0 }: Options) { +export async function extract({ + archivePath, + targetDir, + stripComponents = 0, + setModifiedTimes, +}: Options) { await Fs.mkdir(targetDir, { recursive: true }); if (archivePath.endsWith('.tar') || archivePath.endsWith('.tar.gz')) { @@ -98,7 +112,7 @@ export async function extract({ archivePath, targetDir, stripComponents = 0 }: O } // file entry - return openReadStream$(entry).pipe( + const writeFile$ = openReadStream$(entry).pipe( mergeMap(async (readStream) => { if (!readStream) { throw new Error('no readstream provided by yauzl'); @@ -106,15 +120,23 @@ export async function extract({ archivePath, targetDir, stripComponents = 0 }: O // write the file contents to disk await asyncPipeline(readStream, createWriteStream(fileName)); - // tell yauzl to read the next entry - zipFile.readEntry(); + + if (setModifiedTimes) { + // update the modified time of the file to match the zip entry + await Fs.utimes(fileName, setModifiedTimes, setModifiedTimes); + } }) ); + + // tell yauzl to read the next entry + zipFile.readEntry(); + + return writeFile$; }) ); // trigger the initial 'entry' event, happens async so the event will be delivered after the observable is subscribed zipFile.readEntry(); - await lastValueFrom(Rx.merge(entry$, error$)); + await strComplete(Rx.merge(entry$, error$)); } diff --git a/packages/kbn-dev-utils/src/proc_runner/proc_runner.ts b/packages/kbn-dev-utils/src/proc_runner/proc_runner.ts index efe6d30875512..35c910c911105 100644 --- a/packages/kbn-dev-utils/src/proc_runner/proc_runner.ts +++ b/packages/kbn-dev-utils/src/proc_runner/proc_runner.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import moment from 'moment'; import * as Rx from 'rxjs'; import { filter, first, catchError, map } from 'rxjs/operators'; import exitHook from 'exit-hook'; @@ -199,8 +198,12 @@ export class ProcRunner { // tie into proc outcome$, remove from _procs on compete proc.outcome$.subscribe({ next: (code) => { - const duration = moment.duration(Date.now() - startMs); - this.log.info('[%s] exited with %s after %s', name, code, duration.humanize()); + this.log.info( + '[%s] exited with %s after %s seconds', + name, + code, + ((Date.now() - startMs) / 1000).toFixed(1) + ); }, complete: () => { remove(); diff --git a/packages/kbn-dev-utils/tsconfig.json b/packages/kbn-dev-utils/tsconfig.json index 56843918d88b1..3c22642edfaa1 100644 --- a/packages/kbn-dev-utils/tsconfig.json +++ b/packages/kbn-dev-utils/tsconfig.json @@ -1,16 +1,14 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "target_types", "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-dev-utils/src", "stripInternal": false, - "target": "ES2019", "types": [ "jest", "node" diff --git a/packages/kbn-docs-utils/BUILD.bazel b/packages/kbn-docs-utils/BUILD.bazel index cf609f126d7db..498d2140b91fb 100644 --- a/packages/kbn-docs-utils/BUILD.bazel +++ b/packages/kbn-docs-utils/BUILD.bazel @@ -47,6 +47,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -57,7 +58,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/tsconfig.json b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/tsconfig.json index 57353d8847ae1..e80808c6181fe 100644 --- a/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/tsconfig.json +++ b/packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/tsconfig.json @@ -4,4 +4,4 @@ "strictNullChecks": true, }, "include": ["./**/*"] -} \ No newline at end of file +} diff --git a/packages/kbn-docs-utils/tsconfig.json b/packages/kbn-docs-utils/tsconfig.json index 9868c8b3d2bb4..9b1d6994c1b27 100644 --- a/packages/kbn-docs-utils/tsconfig.json +++ b/packages/kbn-docs-utils/tsconfig.json @@ -1,9 +1,7 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": true, - "outDir": "./target", - "target": "ES2019", + "outDir": "./target/types", "declaration": true, "declarationMap": true, "rootDir": "src", diff --git a/packages/kbn-es-archiver/BUILD.bazel b/packages/kbn-es-archiver/BUILD.bazel index 9b3db311afa24..b7040b584a318 100644 --- a/packages/kbn-es-archiver/BUILD.bazel +++ b/packages/kbn-es-archiver/BUILD.bazel @@ -59,6 +59,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -69,7 +70,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-es-archiver/tsconfig.json b/packages/kbn-es-archiver/tsconfig.json index 1bc93908a993e..dce71fd6cd4a1 100644 --- a/packages/kbn-es-archiver/tsconfig.json +++ b/packages/kbn-es-archiver/tsconfig.json @@ -1,9 +1,7 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": true, - "outDir": "./target", - "target": "ES2019", + "outDir": "./target/types", "declaration": true, "declarationMap": true, "sourceMap": true, diff --git a/packages/kbn-es-query/BUILD.bazel b/packages/kbn-es-query/BUILD.bazel index 1e40918e6b509..d4a531d308f6e 100644 --- a/packages/kbn-es-query/BUILD.bazel +++ b/packages/kbn-es-query/BUILD.bazel @@ -84,6 +84,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -95,7 +96,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-es-query/tsconfig.json b/packages/kbn-es-query/tsconfig.json index 78e722884594e..5b1f3be263138 100644 --- a/packages/kbn-es-query/tsconfig.json +++ b/packages/kbn-es-query/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "./target_types", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-es-query/src", diff --git a/packages/kbn-es/tsconfig.json b/packages/kbn-es/tsconfig.json index 9487a28232684..83035b6315e65 100644 --- a/packages/kbn-es/tsconfig.json +++ b/packages/kbn-es/tsconfig.json @@ -1,7 +1,7 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-es" + "outDir": "target/types" }, "include": [ "src/**/*" diff --git a/packages/kbn-expect/tsconfig.json b/packages/kbn-expect/tsconfig.json index 8c0d9f1e34bd0..5ad392a7d8d5a 100644 --- a/packages/kbn-expect/tsconfig.json +++ b/packages/kbn-expect/tsconfig.json @@ -1,7 +1,7 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": false, + "skipLibCheck": false }, "include": [ "expect.d.ts" diff --git a/packages/kbn-field-types/BUILD.bazel b/packages/kbn-field-types/BUILD.bazel index 829a1b11a0db7..1fb97a5914ee4 100644 --- a/packages/kbn-field-types/BUILD.bazel +++ b/packages/kbn-field-types/BUILD.bazel @@ -54,6 +54,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -65,7 +66,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-field-types/tsconfig.json b/packages/kbn-field-types/tsconfig.json index a7270134039be..150854076bbf8 100644 --- a/packages/kbn-field-types/tsconfig.json +++ b/packages/kbn-field-types/tsconfig.json @@ -1,7 +1,6 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": false, "outDir": "./target_types", "declaration": true, "declarationMap": true, diff --git a/packages/kbn-i18n/BUILD.bazel b/packages/kbn-i18n/BUILD.bazel index 1b3410c9f25e7..62d5fb1d75a46 100644 --- a/packages/kbn-i18n/BUILD.bazel +++ b/packages/kbn-i18n/BUILD.bazel @@ -73,6 +73,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -84,7 +85,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-i18n/tsconfig.json b/packages/kbn-i18n/tsconfig.json index 68c02adf9aaea..2ac0b081b76dd 100644 --- a/packages/kbn-i18n/tsconfig.json +++ b/packages/kbn-i18n/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "./target_types", "sourceMap": true, "sourceRoot": "../../../../../packages/kbn-i18n/src", diff --git a/packages/kbn-interpreter/BUILD.bazel b/packages/kbn-interpreter/BUILD.bazel index c29faf65638ca..903f892b64f3f 100644 --- a/packages/kbn-interpreter/BUILD.bazel +++ b/packages/kbn-interpreter/BUILD.bazel @@ -57,6 +57,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -68,7 +69,6 @@ ts_project( allow_js = True, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-interpreter/tsconfig.json b/packages/kbn-interpreter/tsconfig.json index 011ed877146e8..74ec484ea63e9 100644 --- a/packages/kbn-interpreter/tsconfig.json +++ b/packages/kbn-interpreter/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "allowJs": true, - "incremental": true, - "outDir": "./target", + "outDir": "./target/types", "declaration": true, "declarationMap": true, "rootDir": "src", diff --git a/packages/kbn-io-ts-utils/BUILD.bazel b/packages/kbn-io-ts-utils/BUILD.bazel index 053030a6f11a9..0e5210bcc38a5 100644 --- a/packages/kbn-io-ts-utils/BUILD.bazel +++ b/packages/kbn-io-ts-utils/BUILD.bazel @@ -45,6 +45,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -55,7 +56,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-io-ts-utils/tsconfig.json b/packages/kbn-io-ts-utils/tsconfig.json index 7b8f255275499..3ee769739dfc7 100644 --- a/packages/kbn-io-ts-utils/tsconfig.json +++ b/packages/kbn-io-ts-utils/tsconfig.json @@ -1,8 +1,7 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": true, - "outDir": "./target", + "outDir": "./target/types", "stripInternal": false, "declaration": true, "declarationMap": true, diff --git a/packages/kbn-legacy-logging/BUILD.bazel b/packages/kbn-legacy-logging/BUILD.bazel index 1fd04604dbd24..7a9b472ca9553 100644 --- a/packages/kbn-legacy-logging/BUILD.bazel +++ b/packages/kbn-legacy-logging/BUILD.bazel @@ -52,6 +52,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -62,7 +63,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-legacy-logging/tsconfig.json b/packages/kbn-legacy-logging/tsconfig.json index e3bcedd3de014..30a2e56602b6f 100644 --- a/packages/kbn-legacy-logging/tsconfig.json +++ b/packages/kbn-legacy-logging/tsconfig.json @@ -1,7 +1,6 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": true, "outDir": "target", "stripInternal": false, "declaration": true, diff --git a/packages/kbn-logging/BUILD.bazel b/packages/kbn-logging/BUILD.bazel index a5ef1581f41ad..1a3fa851a3957 100644 --- a/packages/kbn-logging/BUILD.bazel +++ b/packages/kbn-logging/BUILD.bazel @@ -41,6 +41,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -51,7 +52,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-logging/tsconfig.json b/packages/kbn-logging/tsconfig.json index 78985b823dd95..aaf79da229a86 100644 --- a/packages/kbn-logging/tsconfig.json +++ b/packages/kbn-logging/tsconfig.json @@ -1,7 +1,6 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": true, "outDir": "target", "stripInternal": false, "declaration": true, diff --git a/packages/kbn-mapbox-gl/BUILD.bazel b/packages/kbn-mapbox-gl/BUILD.bazel index 7d7186068832e..3cbf7c39421e2 100644 --- a/packages/kbn-mapbox-gl/BUILD.bazel +++ b/packages/kbn-mapbox-gl/BUILD.bazel @@ -43,6 +43,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -53,7 +54,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-mapbox-gl/tsconfig.json b/packages/kbn-mapbox-gl/tsconfig.json index cf1cca0f5a0fd..159f40c6a7ca6 100644 --- a/packages/kbn-mapbox-gl/tsconfig.json +++ b/packages/kbn-mapbox-gl/tsconfig.json @@ -1,8 +1,7 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": true, - "outDir": "./target", + "outDir": "./target/types", "declaration": true, "declarationMap": true, "rootDir": "src", diff --git a/packages/kbn-monaco/BUILD.bazel b/packages/kbn-monaco/BUILD.bazel index 5f4351c89ff1f..3656210cb6b1b 100644 --- a/packages/kbn-monaco/BUILD.bazel +++ b/packages/kbn-monaco/BUILD.bazel @@ -81,6 +81,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -92,7 +93,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-monaco/tsconfig.json b/packages/kbn-monaco/tsconfig.json index 03b017428beb2..959051b17b782 100644 --- a/packages/kbn-monaco/tsconfig.json +++ b/packages/kbn-monaco/tsconfig.json @@ -1,7 +1,6 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": false, "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, diff --git a/packages/kbn-optimizer/BUILD.bazel b/packages/kbn-optimizer/BUILD.bazel index 3809c2b33d500..ddf2a05519682 100644 --- a/packages/kbn-optimizer/BUILD.bazel +++ b/packages/kbn-optimizer/BUILD.bazel @@ -79,6 +79,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -89,7 +90,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-optimizer/tsconfig.json b/packages/kbn-optimizer/tsconfig.json index e7471846beca2..047c98db8a806 100644 --- a/packages/kbn-optimizer/tsconfig.json +++ b/packages/kbn-optimizer/tsconfig.json @@ -1,8 +1,7 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": true, - "outDir": "./target", + "outDir": "./target/types", "declaration": true, "declarationMap": true, "rootDir": "./src", diff --git a/packages/kbn-plugin-generator/BUILD.bazel b/packages/kbn-plugin-generator/BUILD.bazel index cd18f5e14a736..c16862ee4f3c2 100644 --- a/packages/kbn-plugin-generator/BUILD.bazel +++ b/packages/kbn-plugin-generator/BUILD.bazel @@ -65,6 +65,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -75,7 +76,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-plugin-generator/template/tsconfig.json.ejs b/packages/kbn-plugin-generator/template/tsconfig.json.ejs index 8a3ced743d0fa..06ef420ae55e0 100644 --- a/packages/kbn-plugin-generator/template/tsconfig.json.ejs +++ b/packages/kbn-plugin-generator/template/tsconfig.json.ejs @@ -1,8 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/packages/kbn-plugin-generator/tsconfig.json b/packages/kbn-plugin-generator/tsconfig.json index 9165fd21ebea0..6a25803c83940 100644 --- a/packages/kbn-plugin-generator/tsconfig.json +++ b/packages/kbn-plugin-generator/tsconfig.json @@ -1,7 +1,6 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": true, "outDir": "target", "target": "ES2019", "declaration": true, diff --git a/packages/kbn-plugin-helpers/BUILD.bazel b/packages/kbn-plugin-helpers/BUILD.bazel index 1a1f3453f768a..9242701770a86 100644 --- a/packages/kbn-plugin-helpers/BUILD.bazel +++ b/packages/kbn-plugin-helpers/BUILD.bazel @@ -56,6 +56,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -66,7 +67,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-plugin-helpers/tsconfig.json b/packages/kbn-plugin-helpers/tsconfig.json index 4348f1e1a7516..22adf020187ba 100644 --- a/packages/kbn-plugin-helpers/tsconfig.json +++ b/packages/kbn-plugin-helpers/tsconfig.json @@ -1,7 +1,6 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": true, "outDir": "target", "target": "ES2018", "declaration": true, diff --git a/packages/kbn-pm/tsconfig.json b/packages/kbn-pm/tsconfig.json index 558cff6556ff6..f12e49fe28d45 100644 --- a/packages/kbn-pm/tsconfig.json +++ b/packages/kbn-pm/tsconfig.json @@ -1,7 +1,7 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-pm", + "outDir": "target/types", "types": [ "jest", "node" diff --git a/packages/kbn-rule-data-utils/BUILD.bazel b/packages/kbn-rule-data-utils/BUILD.bazel index 62a48b66b4bce..1268daf54cd47 100644 --- a/packages/kbn-rule-data-utils/BUILD.bazel +++ b/packages/kbn-rule-data-utils/BUILD.bazel @@ -40,6 +40,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -50,7 +51,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-rule-data-utils/tsconfig.json b/packages/kbn-rule-data-utils/tsconfig.json index 852393f01e594..1c568530306c7 100644 --- a/packages/kbn-rule-data-utils/tsconfig.json +++ b/packages/kbn-rule-data-utils/tsconfig.json @@ -1,8 +1,7 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "incremental": true, - "outDir": "./target", + "outDir": "./target/types", "stripInternal": false, "declaration": true, "declarationMap": true, diff --git a/packages/kbn-securitysolution-autocomplete/BUILD.bazel b/packages/kbn-securitysolution-autocomplete/BUILD.bazel index c29f6d6badc6f..18c3b8f3ae3bb 100644 --- a/packages/kbn-securitysolution-autocomplete/BUILD.bazel +++ b/packages/kbn-securitysolution-autocomplete/BUILD.bazel @@ -60,6 +60,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -69,6 +70,7 @@ ts_config( deps = [ "//:tsconfig.base.json", "//:tsconfig.browser.json", + "//:tsconfig.browser_bazel.json", ], ) @@ -81,7 +83,6 @@ ts_project( declaration = True, declaration_dir = "target_types", declaration_map = True, - incremental = True, out_dir = "target_node", root_dir = "src", source_map = True, @@ -95,7 +96,6 @@ ts_project( deps = DEPS, allow_js = True, declaration = False, - incremental = True, out_dir = "target_web", source_map = True, root_dir = "src", diff --git a/packages/kbn-securitysolution-autocomplete/tsconfig.browser.json b/packages/kbn-securitysolution-autocomplete/tsconfig.browser.json index bab7b18c59cfd..404043569aa92 100644 --- a/packages/kbn-securitysolution-autocomplete/tsconfig.browser.json +++ b/packages/kbn-securitysolution-autocomplete/tsconfig.browser.json @@ -1,8 +1,7 @@ { - "extends": "../../tsconfig.browser.json", + "extends": "../../tsconfig.browser_bazel.json", "compilerOptions": { "allowJs": true, - "incremental": true, "outDir": "./target_web", "declaration": false, "isolatedModules": true, diff --git a/packages/kbn-securitysolution-autocomplete/tsconfig.json b/packages/kbn-securitysolution-autocomplete/tsconfig.json index bf402e93ffd69..484b639f94332 100644 --- a/packages/kbn-securitysolution-autocomplete/tsconfig.json +++ b/packages/kbn-securitysolution-autocomplete/tsconfig.json @@ -1,8 +1,7 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "allowJs": true, - "incremental": true, "declarationDir": "./target_types", "outDir": "target_node", "declaration": true, diff --git a/packages/kbn-securitysolution-es-utils/BUILD.bazel b/packages/kbn-securitysolution-es-utils/BUILD.bazel index b69aa3df8ecb5..97f2d9a41cd78 100644 --- a/packages/kbn-securitysolution-es-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-es-utils/BUILD.bazel @@ -44,7 +44,8 @@ ts_config( name = "tsconfig", src = "tsconfig.json", deps = [ - "//:tsconfig.base.json", + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -54,7 +55,6 @@ ts_project( args = ["--pretty"], declaration = True, declaration_map = True, - incremental = True, out_dir = "target", root_dir = "src", source_map = True, diff --git a/packages/kbn-securitysolution-es-utils/tsconfig.json b/packages/kbn-securitysolution-es-utils/tsconfig.json index be8848d781cae..906b01c943ab7 100644 --- a/packages/kbn-securitysolution-es-utils/tsconfig.json +++ b/packages/kbn-securitysolution-es-utils/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, - "incremental": true, "outDir": "target", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-securitysolution-hook-utils/BUILD.bazel b/packages/kbn-securitysolution-hook-utils/BUILD.bazel index 5bfe3d86867f6..b5f3e0df2e0a7 100644 --- a/packages/kbn-securitysolution-hook-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-hook-utils/BUILD.bazel @@ -45,7 +45,8 @@ ts_config( name = "tsconfig", src = "tsconfig.json", deps = [ - "//:tsconfig.base.json", + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -55,7 +56,6 @@ ts_project( args = ["--pretty"], declaration = True, declaration_map = True, - incremental = True, out_dir = "target", root_dir = "src", source_map = True, diff --git a/packages/kbn-securitysolution-hook-utils/tsconfig.json b/packages/kbn-securitysolution-hook-utils/tsconfig.json index 352dc086fec30..7e2cae223b14d 100644 --- a/packages/kbn-securitysolution-hook-utils/tsconfig.json +++ b/packages/kbn-securitysolution-hook-utils/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, - "incremental": true, "outDir": "target", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel b/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel index 8e4a464783fc6..3c9818a58408d 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel +++ b/packages/kbn-securitysolution-io-ts-alerting-types/BUILD.bazel @@ -53,6 +53,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -63,7 +64,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json b/packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json index 3411ce2c93d05..5f69f2bd0e2e4 100644 --- a/packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json +++ b/packages/kbn-securitysolution-io-ts-alerting-types/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, - "incremental": true, "outDir": "target", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel b/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel index 99df07c3d8ea8..bfdde4363985e 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel +++ b/packages/kbn-securitysolution-io-ts-list-types/BUILD.bazel @@ -54,6 +54,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -64,7 +65,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-securitysolution-io-ts-list-types/tsconfig.json b/packages/kbn-securitysolution-io-ts-list-types/tsconfig.json index d926653a4230b..fb979c23df12b 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/tsconfig.json +++ b/packages/kbn-securitysolution-io-ts-list-types/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, - "incremental": true, "outDir": "target", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-securitysolution-io-ts-types/BUILD.bazel b/packages/kbn-securitysolution-io-ts-types/BUILD.bazel index d30eb913b1a57..fdf3ef64460d9 100644 --- a/packages/kbn-securitysolution-io-ts-types/BUILD.bazel +++ b/packages/kbn-securitysolution-io-ts-types/BUILD.bazel @@ -52,6 +52,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -62,7 +63,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-securitysolution-io-ts-types/tsconfig.json b/packages/kbn-securitysolution-io-ts-types/tsconfig.json index 42a059439ecb5..12de4ed6ccb76 100644 --- a/packages/kbn-securitysolution-io-ts-types/tsconfig.json +++ b/packages/kbn-securitysolution-io-ts-types/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, - "incremental": true, "outDir": "target", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-securitysolution-io-ts-utils/BUILD.bazel b/packages/kbn-securitysolution-io-ts-utils/BUILD.bazel index b9326a5020717..f6c81a9adb9a3 100644 --- a/packages/kbn-securitysolution-io-ts-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-io-ts-utils/BUILD.bazel @@ -51,6 +51,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -61,7 +62,6 @@ ts_project( deps = DEPS, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-securitysolution-io-ts-utils/tsconfig.json b/packages/kbn-securitysolution-io-ts-utils/tsconfig.json index 22718276926f0..aa5aa4df550f2 100644 --- a/packages/kbn-securitysolution-io-ts-utils/tsconfig.json +++ b/packages/kbn-securitysolution-io-ts-utils/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, - "incremental": true, "outDir": "target", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-securitysolution-list-api/BUILD.bazel b/packages/kbn-securitysolution-list-api/BUILD.bazel index 9055cf0804e49..726d75d8c86b8 100644 --- a/packages/kbn-securitysolution-list-api/BUILD.bazel +++ b/packages/kbn-securitysolution-list-api/BUILD.bazel @@ -47,7 +47,8 @@ ts_config( name = "tsconfig", src = "tsconfig.json", deps = [ - "//:tsconfig.base.json", + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -57,7 +58,6 @@ ts_project( args = ["--pretty"], declaration = True, declaration_map = True, - incremental = True, out_dir = "target", root_dir = "src", source_map = True, diff --git a/packages/kbn-securitysolution-list-api/tsconfig.json b/packages/kbn-securitysolution-list-api/tsconfig.json index 4fad35e754f29..c2ac6d3d92286 100644 --- a/packages/kbn-securitysolution-list-api/tsconfig.json +++ b/packages/kbn-securitysolution-list-api/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, - "incremental": true, "outDir": "target", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-securitysolution-list-constants/BUILD.bazel b/packages/kbn-securitysolution-list-constants/BUILD.bazel index 8d6779bfa122e..e56c96e0b5da9 100644 --- a/packages/kbn-securitysolution-list-constants/BUILD.bazel +++ b/packages/kbn-securitysolution-list-constants/BUILD.bazel @@ -42,7 +42,8 @@ ts_config( name = "tsconfig", src = "tsconfig.json", deps = [ - "//:tsconfig.base.json", + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -52,7 +53,6 @@ ts_project( args = ["--pretty"], declaration = True, declaration_map = True, - incremental = True, out_dir = "target", root_dir = "src", source_map = True, diff --git a/packages/kbn-securitysolution-list-constants/tsconfig.json b/packages/kbn-securitysolution-list-constants/tsconfig.json index 84edcdd1d5794..769b5df990e6b 100644 --- a/packages/kbn-securitysolution-list-constants/tsconfig.json +++ b/packages/kbn-securitysolution-list-constants/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, - "incremental": true, "outDir": "target", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-securitysolution-list-hooks/BUILD.bazel b/packages/kbn-securitysolution-list-hooks/BUILD.bazel index 631e57958d5fa..87075604a75c3 100644 --- a/packages/kbn-securitysolution-list-hooks/BUILD.bazel +++ b/packages/kbn-securitysolution-list-hooks/BUILD.bazel @@ -54,7 +54,8 @@ ts_config( name = "tsconfig", src = "tsconfig.json", deps = [ - "//:tsconfig.base.json", + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -64,7 +65,6 @@ ts_project( args = ["--pretty"], declaration = True, declaration_map = True, - incremental = True, out_dir = "target", root_dir = "src", source_map = True, diff --git a/packages/kbn-securitysolution-list-hooks/tsconfig.json b/packages/kbn-securitysolution-list-hooks/tsconfig.json index 9e99c22ea9a07..9b09c02bd4aa1 100644 --- a/packages/kbn-securitysolution-list-hooks/tsconfig.json +++ b/packages/kbn-securitysolution-list-hooks/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, - "incremental": true, "outDir": "target", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-securitysolution-list-utils/BUILD.bazel b/packages/kbn-securitysolution-list-utils/BUILD.bazel index cb6ddbd7f91d5..b35b13004b1a8 100644 --- a/packages/kbn-securitysolution-list-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-list-utils/BUILD.bazel @@ -49,7 +49,8 @@ ts_config( name = "tsconfig", src = "tsconfig.json", deps = [ - "//:tsconfig.base.json", + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -59,7 +60,6 @@ ts_project( args = ["--pretty"], declaration = True, declaration_map = True, - incremental = True, out_dir = "target", root_dir = "src", source_map = True, diff --git a/packages/kbn-securitysolution-list-utils/tsconfig.json b/packages/kbn-securitysolution-list-utils/tsconfig.json index c462159445894..da48f4af29ead 100644 --- a/packages/kbn-securitysolution-list-utils/tsconfig.json +++ b/packages/kbn-securitysolution-list-utils/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, - "incremental": true, "outDir": "target", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-securitysolution-t-grid/BUILD.bazel b/packages/kbn-securitysolution-t-grid/BUILD.bazel index 5cf1081bdd32e..0c5ef200fc965 100644 --- a/packages/kbn-securitysolution-t-grid/BUILD.bazel +++ b/packages/kbn-securitysolution-t-grid/BUILD.bazel @@ -59,7 +59,8 @@ ts_config( name = "tsconfig", src = "tsconfig.json", deps = [ - "//:tsconfig.base.json", + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -69,6 +70,7 @@ ts_config( deps = [ "//:tsconfig.base.json", "//:tsconfig.browser.json", + "//:tsconfig.browser_bazel.json", ], ) @@ -80,7 +82,6 @@ ts_project( declaration = True, declaration_dir = "target_types", declaration_map = True, - incremental = True, out_dir = "target_node", root_dir = "src", source_map = True, @@ -94,7 +95,6 @@ ts_project( deps = DEPS, allow_js = True, declaration = False, - incremental = True, out_dir = "target_web", source_map = True, root_dir = "src", diff --git a/packages/kbn-securitysolution-t-grid/tsconfig.browser.json b/packages/kbn-securitysolution-t-grid/tsconfig.browser.json index a5183ba4fd457..e951765c4b991 100644 --- a/packages/kbn-securitysolution-t-grid/tsconfig.browser.json +++ b/packages/kbn-securitysolution-t-grid/tsconfig.browser.json @@ -1,8 +1,7 @@ { - "extends": "../../tsconfig.browser.json", + "extends": "../../tsconfig.browser_bazel.json", "compilerOptions": { "allowJs": true, - "incremental": true, "outDir": "./target_web", "declaration": false, "isolatedModules": true, diff --git a/packages/kbn-securitysolution-t-grid/tsconfig.json b/packages/kbn-securitysolution-t-grid/tsconfig.json index 8cda578edede4..1d5957e516e00 100644 --- a/packages/kbn-securitysolution-t-grid/tsconfig.json +++ b/packages/kbn-securitysolution-t-grid/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, - "incremental": true, "outDir": "target", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-securitysolution-utils/BUILD.bazel b/packages/kbn-securitysolution-utils/BUILD.bazel index 6084480ef6a15..41fb97bc6079e 100644 --- a/packages/kbn-securitysolution-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-utils/BUILD.bazel @@ -44,7 +44,8 @@ ts_config( name = "tsconfig", src = "tsconfig.json", deps = [ - "//:tsconfig.base.json", + "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -54,7 +55,6 @@ ts_project( args = ["--pretty"], declaration = True, declaration_map = True, - incremental = True, out_dir = "target", root_dir = "src", source_map = True, diff --git a/packages/kbn-securitysolution-utils/tsconfig.json b/packages/kbn-securitysolution-utils/tsconfig.json index 783e67666d8b8..3894b53d6cff3 100644 --- a/packages/kbn-securitysolution-utils/tsconfig.json +++ b/packages/kbn-securitysolution-utils/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, - "incremental": true, "outDir": "target", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-server-http-tools/BUILD.bazel b/packages/kbn-server-http-tools/BUILD.bazel index 2e1a33f647430..609fe6d00f173 100644 --- a/packages/kbn-server-http-tools/BUILD.bazel +++ b/packages/kbn-server-http-tools/BUILD.bazel @@ -61,6 +61,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -72,7 +73,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-server-http-tools/tsconfig.json b/packages/kbn-server-http-tools/tsconfig.json index 643b932073c9e..e378e41c3828b 100644 --- a/packages/kbn-server-http-tools/tsconfig.json +++ b/packages/kbn-server-http-tools/tsconfig.json @@ -1,11 +1,10 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, - "outDir": "./target", + "outDir": "./target/types", "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-server-http-tools/src", diff --git a/packages/kbn-server-route-repository/BUILD.bazel b/packages/kbn-server-route-repository/BUILD.bazel index 147b72c65f954..9f8a9f34061d2 100644 --- a/packages/kbn-server-route-repository/BUILD.bazel +++ b/packages/kbn-server-route-repository/BUILD.bazel @@ -57,6 +57,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -68,7 +69,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-server-route-repository/tsconfig.json b/packages/kbn-server-route-repository/tsconfig.json index 947744bc68d42..447a2084926c6 100644 --- a/packages/kbn-server-route-repository/tsconfig.json +++ b/packages/kbn-server-route-repository/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "./target_types", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-std/BUILD.bazel b/packages/kbn-std/BUILD.bazel index bcc5a87a1e795..571d3c061c138 100644 --- a/packages/kbn-std/BUILD.bazel +++ b/packages/kbn-std/BUILD.bazel @@ -53,6 +53,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -64,7 +65,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-std/tsconfig.json b/packages/kbn-std/tsconfig.json index 3cff7fbd258a6..2674ca26e96d5 100644 --- a/packages/kbn-std/tsconfig.json +++ b/packages/kbn-std/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "./target_types", "rootDir": "src", "sourceMap": true, diff --git a/packages/kbn-storybook/BUILD.bazel b/packages/kbn-storybook/BUILD.bazel index ae09d79d58331..dce2a6b3010f3 100644 --- a/packages/kbn-storybook/BUILD.bazel +++ b/packages/kbn-storybook/BUILD.bazel @@ -72,6 +72,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -83,7 +84,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", root_dir = "src", source_map = True, diff --git a/packages/kbn-storybook/tsconfig.json b/packages/kbn-storybook/tsconfig.json index 9e1ee54cd4899..0ccf3e78c8288 100644 --- a/packages/kbn-storybook/tsconfig.json +++ b/packages/kbn-storybook/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, diff --git a/packages/kbn-telemetry-tools/BUILD.bazel b/packages/kbn-telemetry-tools/BUILD.bazel index f90ca9b22dc92..1183de2586424 100644 --- a/packages/kbn-telemetry-tools/BUILD.bazel +++ b/packages/kbn-telemetry-tools/BUILD.bazel @@ -59,6 +59,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -70,7 +71,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-telemetry-tools/tsconfig.json b/packages/kbn-telemetry-tools/tsconfig.json index 1f1b4aea5d5c6..034d7c0c6e745 100644 --- a/packages/kbn-telemetry-tools/tsconfig.json +++ b/packages/kbn-telemetry-tools/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "isolatedModules": true, "outDir": "./target_types", "rootDir": "src", diff --git a/packages/kbn-test-subj-selector/tsconfig.json b/packages/kbn-test-subj-selector/tsconfig.json index a1e1c1af372c6..a590073946162 100644 --- a/packages/kbn-test-subj-selector/tsconfig.json +++ b/packages/kbn-test-subj-selector/tsconfig.json @@ -1,7 +1,7 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-test-subj-selector" + "outDir": "target/types" }, "include": [ "index.d.ts" diff --git a/packages/kbn-test/BUILD.bazel b/packages/kbn-test/BUILD.bazel index 34497ca141c5e..36e81df6d8c3c 100644 --- a/packages/kbn-test/BUILD.bazel +++ b/packages/kbn-test/BUILD.bazel @@ -110,6 +110,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -121,7 +122,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-test/tsconfig.json b/packages/kbn-test/tsconfig.json index 374139d965e3e..7ba83019b0075 100644 --- a/packages/kbn-test/tsconfig.json +++ b/packages/kbn-test/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "./target_types", "stripInternal": true, "rootDir": "src", diff --git a/packages/kbn-tinymath/tsconfig.json b/packages/kbn-tinymath/tsconfig.json index 73133b7318a0d..748eb53a69e3d 100644 --- a/packages/kbn-tinymath/tsconfig.json +++ b/packages/kbn-tinymath/tsconfig.json @@ -1,4 +1,4 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "include": ["index.d.ts"] } diff --git a/packages/kbn-typed-react-router-config/BUILD.bazel b/packages/kbn-typed-react-router-config/BUILD.bazel index fa89bc5029ccd..be346f8321fad 100644 --- a/packages/kbn-typed-react-router-config/BUILD.bazel +++ b/packages/kbn-typed-react-router-config/BUILD.bazel @@ -64,6 +64,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -75,7 +76,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-typed-react-router-config/tsconfig.json b/packages/kbn-typed-react-router-config/tsconfig.json index bee43cac2df0e..8e17781119ee9 100644 --- a/packages/kbn-typed-react-router-config/tsconfig.json +++ b/packages/kbn-typed-react-router-config/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "isolatedModules": true, "outDir": "./target_types", "sourceMap": true, diff --git a/packages/kbn-ui-shared-deps/BUILD.bazel b/packages/kbn-ui-shared-deps/BUILD.bazel index 426f8d0b7485a..352fd48907345 100644 --- a/packages/kbn-ui-shared-deps/BUILD.bazel +++ b/packages/kbn-ui-shared-deps/BUILD.bazel @@ -81,6 +81,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -92,7 +93,6 @@ ts_project( allow_js = True, declaration = True, declaration_map = True, - incremental = True, out_dir = "target", source_map = True, root_dir = "src", diff --git a/packages/kbn-ui-shared-deps/tsconfig.json b/packages/kbn-ui-shared-deps/tsconfig.json index 0fd49ede21830..90a89ac580a40 100644 --- a/packages/kbn-ui-shared-deps/tsconfig.json +++ b/packages/kbn-ui-shared-deps/tsconfig.json @@ -1,9 +1,8 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "allowJs": true, - "incremental": true, - "outDir": "./target", + "outDir": "./target/types", "declaration": true, "declarationMap": true, "rootDir": "src", diff --git a/packages/kbn-utility-types/BUILD.bazel b/packages/kbn-utility-types/BUILD.bazel index 70f814c30f415..8b63eea537aa6 100644 --- a/packages/kbn-utility-types/BUILD.bazel +++ b/packages/kbn-utility-types/BUILD.bazel @@ -45,6 +45,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -56,7 +57,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-utility-types/tsconfig.json b/packages/kbn-utility-types/tsconfig.json index 92fd810b389c8..997bcf9e0c45b 100644 --- a/packages/kbn-utility-types/tsconfig.json +++ b/packages/kbn-utility-types/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "./target_types", "rootDir": "./src", "sourceMap": true, diff --git a/packages/kbn-utils/BUILD.bazel b/packages/kbn-utils/BUILD.bazel index c4ded75eda637..c2f82d65d3318 100644 --- a/packages/kbn-utils/BUILD.bazel +++ b/packages/kbn-utils/BUILD.bazel @@ -49,6 +49,7 @@ ts_config( src = "tsconfig.json", deps = [ "//:tsconfig.base.json", + "//:tsconfig.bazel.json", ], ) @@ -60,7 +61,6 @@ ts_project( declaration = True, declaration_map = True, emit_declaration_only = True, - incremental = False, out_dir = "target_types", source_map = True, root_dir = "src", diff --git a/packages/kbn-utils/src/package_json/index.test.ts b/packages/kbn-utils/src/package_json/index.test.ts index f6d7e1f2f611b..f8d1b5d25fa1b 100644 --- a/packages/kbn-utils/src/package_json/index.test.ts +++ b/packages/kbn-utils/src/package_json/index.test.ts @@ -6,15 +6,8 @@ * Side Public License, v 1. */ -import path from 'path'; -import { kibanaPackageJson } from './'; +import { kibanaPackageJson } from './index'; it('parses package.json', () => { expect(kibanaPackageJson.name).toEqual('kibana'); }); - -it('includes __dirname and __filename', () => { - const root = path.resolve(__dirname, '../../../../'); - expect(kibanaPackageJson.__filename).toEqual(path.resolve(root, 'package.json')); - expect(kibanaPackageJson.__dirname).toEqual(root); -}); diff --git a/packages/kbn-utils/src/package_json/index.ts b/packages/kbn-utils/src/package_json/index.ts index d9304cee2ca38..fbd554c8c2c04 100644 --- a/packages/kbn-utils/src/package_json/index.ts +++ b/packages/kbn-utils/src/package_json/index.ts @@ -6,13 +6,11 @@ * Side Public License, v 1. */ -import { dirname, resolve } from 'path'; +import Path from 'path'; import { REPO_ROOT } from '../repo_root'; export const kibanaPackageJson = { - __filename: resolve(REPO_ROOT, 'package.json'), - __dirname: dirname(resolve(REPO_ROOT, 'package.json')), - ...require(resolve(REPO_ROOT, 'package.json')), + ...require(Path.resolve(REPO_ROOT, 'package.json')), }; export const isKibanaDistributable = () => { diff --git a/packages/kbn-utils/tsconfig.json b/packages/kbn-utils/tsconfig.json index cf546e2a1395e..85c26f42a695c 100644 --- a/packages/kbn-utils/tsconfig.json +++ b/packages/kbn-utils/tsconfig.json @@ -1,10 +1,9 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../tsconfig.bazel.json", "compilerOptions": { "declaration": true, "declarationMap": true, "emitDeclarationOnly": true, - "incremental": false, "outDir": "target_types", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-utils/src", diff --git a/scripts/convert_ts_projects.js b/scripts/convert_ts_projects.js new file mode 100644 index 0000000000000..65053db0d0bd1 --- /dev/null +++ b/scripts/convert_ts_projects.js @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +require('../src/setup_node_env'); +require('../src/dev/typescript/convert_all_to_composite'); diff --git a/src/core/tsconfig.json b/src/core/tsconfig.json index 855962070457e..9c042577cfe21 100644 --- a/src/core/tsconfig.json +++ b/src/core/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/dev/build/lib/config.test.ts b/src/dev/build/lib/config.test.ts index 8bae63629046c..8bc5eb70c9437 100644 --- a/src/dev/build/lib/config.test.ts +++ b/src/dev/build/lib/config.test.ts @@ -8,10 +8,9 @@ import { resolve } from 'path'; -import { REPO_ROOT } from '@kbn/utils'; +import { REPO_ROOT, kibanaPackageJson } from '@kbn/utils'; import { createAbsolutePathSerializer } from '@kbn/dev-utils'; -import pkg from '../../../../package.json'; import { Config } from './config'; jest.mock('./version_info', () => ({ @@ -36,14 +35,14 @@ const setup = async ({ targetAllPlatforms = true }: { targetAllPlatforms?: boole describe('#getKibanaPkg()', () => { it('returns the parsed package.json from the Kibana repo', async () => { const config = await setup(); - expect(config.getKibanaPkg()).toEqual(pkg); + expect(config.getKibanaPkg()).toEqual(kibanaPackageJson); }); }); describe('#getNodeVersion()', () => { it('returns the node version from the kibana package.json', async () => { const config = await setup(); - expect(config.getNodeVersion()).toEqual(pkg.engines.node); + expect(config.getNodeVersion()).toEqual(kibanaPackageJson.engines.node); }); }); diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts index 97fd740409741..cac02cae20c42 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -10,9 +10,8 @@ import { access, link, unlink, chmod } from 'fs'; import { resolve } from 'path'; import { promisify } from 'util'; -import { ToolingLog } from '@kbn/dev-utils'; +import { ToolingLog, kibanaPackageJson } from '@kbn/dev-utils'; -import { branch } from '../../../../../../package.json'; import { write, copyAll, mkdirp, exec, Config, Build } from '../../../lib'; import * as dockerTemplates from './templates'; import { TemplateContext } from './template_context'; @@ -64,7 +63,7 @@ export async function runDockerGenerator( artifactTarball, imageFlavor, version, - branch, + branch: kibanaPackageJson.branch, license, artifactsDir, imageTag, diff --git a/src/dev/typescript/build_ts_refs.ts b/src/dev/typescript/build_ts_refs.ts index 26425b7a3e61d..db51ff999ccc3 100644 --- a/src/dev/typescript/build_ts_refs.ts +++ b/src/dev/typescript/build_ts_refs.ts @@ -8,25 +8,31 @@ import Path from 'path'; -import execa from 'execa'; -import { ToolingLog, REPO_ROOT } from '@kbn/dev-utils'; +import { ToolingLog, REPO_ROOT, ProcRunner } from '@kbn/dev-utils'; -export const REF_CONFIG_PATHS = [Path.resolve(REPO_ROOT, 'tsconfig.refs.json')]; +import { ROOT_REFS_CONFIG_PATH } from './root_refs_config'; -export async function buildAllTsRefs(log: ToolingLog): Promise<{ failed: boolean }> { - for (const path of REF_CONFIG_PATHS) { - const relative = Path.relative(REPO_ROOT, path); - log.debug(`Building TypeScript projects refs for ${relative}...`); - const { failed, stdout } = await execa( - require.resolve('typescript/bin/tsc'), - ['-b', relative, '--pretty'], - { - cwd: REPO_ROOT, - reject: false, - } - ); - log.info(stdout); - if (failed) return { failed }; +export async function buildAllTsRefs({ + log, + procRunner, + verbose, +}: { + log: ToolingLog; + procRunner: ProcRunner; + verbose?: boolean; +}): Promise<{ failed: boolean }> { + const relative = Path.relative(REPO_ROOT, ROOT_REFS_CONFIG_PATH); + log.info(`Building TypeScript projects refs for ${relative}...`); + + try { + await procRunner.run('tsc', { + cmd: Path.relative(REPO_ROOT, require.resolve('typescript/bin/tsc')), + args: ['-b', relative, '--pretty', ...(verbose ? ['--verbose'] : [])], + cwd: REPO_ROOT, + wait: true, + }); + return { failed: false }; + } catch (error) { + return { failed: true }; } - return { failed: false }; } diff --git a/src/dev/typescript/build_ts_refs_cli.ts b/src/dev/typescript/build_ts_refs_cli.ts index ad7807eb87c66..6b3cb89a7b0ea 100644 --- a/src/dev/typescript/build_ts_refs_cli.ts +++ b/src/dev/typescript/build_ts_refs_cli.ts @@ -12,8 +12,10 @@ import { run, REPO_ROOT } from '@kbn/dev-utils'; import del from 'del'; import { RefOutputCache } from './ref_output_cache'; -import { buildAllTsRefs, REF_CONFIG_PATHS } from './build_ts_refs'; -import { getOutputsDeep } from './ts_configfile'; +import { buildAllTsRefs } from './build_ts_refs'; +import { updateRootRefsConfig, ROOT_REFS_CONFIG_PATH } from './root_refs_config'; +import { Project } from './project'; +import { PROJECT_CACHE } from './projects'; import { concurrentMap } from './concurrent_map'; const CACHE_WORKING_DIR = Path.resolve(REPO_ROOT, 'data/ts_refs_output_cache'); @@ -28,7 +30,7 @@ const isTypeFailure = (error: any) => export async function runBuildRefsCli() { run( - async ({ log, flags }) => { + async ({ log, flags, procRunner }) => { if (process.env.BUILD_TS_REFS_DISABLE === 'true' && !flags.force) { log.info( 'Building ts refs is disabled because the BUILD_TS_REFS_DISABLE environment variable is set to "true". Pass `--force` to run the build anyway.' @@ -36,7 +38,19 @@ export async function runBuildRefsCli() { return; } - const outDirs = getOutputsDeep(REF_CONFIG_PATHS); + // if the tsconfig.refs.json file is not self-managed then make sure it has + // a reference to every composite project in the repo + await updateRootRefsConfig(log); + + // load all the projects referenced from the root refs config deeply, so we know all + // the ts projects we are going to be cleaning or populating with caches + const projects = Project.load( + ROOT_REFS_CONFIG_PATH, + {}, + { + skipConfigValidation: true, + } + ).getProjectsDeep(PROJECT_CACHE); const cacheEnabled = process.env.BUILD_TS_REFS_CACHE_ENABLE !== 'false' && !!flags.cache; const doCapture = process.env.BUILD_TS_REFS_CACHE_CAPTURE === 'true'; @@ -44,15 +58,15 @@ export async function runBuildRefsCli() { const doInitCache = cacheEnabled && !doCapture; if (doClean) { - log.info('deleting', outDirs.length, 'ts output directories'); - await concurrentMap(100, outDirs, (outDir) => del(outDir)); + log.info('deleting', projects.outDirs.length, 'ts output directories'); + await concurrentMap(100, projects.outDirs, (outDir) => del(outDir)); } let outputCache; if (cacheEnabled) { outputCache = await RefOutputCache.create({ log, - outDirs, + projects, repoRoot: REPO_ROOT, workingDir: CACHE_WORKING_DIR, upstreamUrl: 'https://github.com/elastic/kibana.git', @@ -64,7 +78,7 @@ export async function runBuildRefsCli() { } try { - await buildAllTsRefs(log); + await buildAllTsRefs({ log, procRunner, verbose: !!flags.verbose }); log.success('ts refs build successfully'); } catch (error) { const typeFailure = isTypeFailure(error); @@ -97,7 +111,7 @@ export async function runBuildRefsCli() { --force Run the build even if the BUILD_TS_REFS_DISABLE is set to "true" --clean Delete outDirs for each ts project before building --no-cache Disable fetching/extracting outDir caches based on the mergeBase with upstream - --ignore-type-failures If tsc reports type errors, ignore them and just log a small warning. + --ignore-type-failures If tsc reports type errors, ignore them and just log a small warning `, }, log: { diff --git a/src/dev/typescript/concurrent_map.ts b/src/dev/typescript/concurrent_map.ts index 793630ab85a55..b40da14f4162b 100644 --- a/src/dev/typescript/concurrent_map.ts +++ b/src/dev/typescript/concurrent_map.ts @@ -15,6 +15,10 @@ export async function concurrentMap( arr: T[], fn: (item: T, i: number) => Promise ): Promise { + if (!arr.length) { + return []; + } + return await lastValueFrom( Rx.from(arr).pipe( // execute items in parallel based on concurrency diff --git a/src/dev/typescript/convert_all_to_composite.ts b/src/dev/typescript/convert_all_to_composite.ts new file mode 100644 index 0000000000000..9b9dd3468747b --- /dev/null +++ b/src/dev/typescript/convert_all_to_composite.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { run } from '@kbn/dev-utils'; + +import { PROJECTS } from './projects'; + +run(async ({ log }) => { + for (const project of PROJECTS) { + if (!project.config.compilerOptions?.composite) { + log.info(project.tsConfigPath); + } + } +}); diff --git a/src/dev/typescript/exec_in_projects.ts b/src/dev/typescript/exec_in_projects.ts deleted file mode 100644 index ed87aec4ec196..0000000000000 --- a/src/dev/typescript/exec_in_projects.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import os from 'os'; - -import { ToolingLog } from '@kbn/dev-utils'; -import chalk from 'chalk'; -import execa from 'execa'; -import Listr from 'listr'; - -import { Project } from './project'; - -class ProjectFailure { - constructor(public project: Project, public error: execa.ExecaError) {} -} - -export function execInProjects( - log: ToolingLog, - projects: Project[], - cmd: string, - getArgs: (project: Project) => string[] -) { - const list = new Listr( - projects.map((project) => ({ - task: () => - execa(cmd, getArgs(project), { - // execute in the current working directory so that relative paths in errors - // are relative from the right location - cwd: process.cwd(), - env: chalk.level > 0 ? { FORCE_COLOR: 'true' } : {}, - stdio: ['ignore', 'pipe', 'pipe'], - preferLocal: true, - }).catch((error) => { - throw new ProjectFailure(project, error); - }), - title: project.name, - })), - { - concurrent: Math.min(4, Math.round((os.cpus() || []).length / 2) || 1) || false, - exitOnError: false, - } - ); - - list.run().catch((error: any) => { - process.exitCode = 1; - - if (!error.errors) { - log.error('Unhandled exception!'); - log.error(error); - process.exit(); - } - - for (const e of error.errors) { - if (e instanceof ProjectFailure) { - log.write(''); - // stdout contains errors from tsc - // stderr conatins tsc crash report - log.error(`${e.project.name} failed\n${e.error.stdout || e.error.stderr}`); - } else { - log.error(e); - } - } - }); -} diff --git a/src/dev/typescript/index.ts b/src/dev/typescript/index.ts index 34ecd76a994db..d9ccc3975b4eb 100644 --- a/src/dev/typescript/index.ts +++ b/src/dev/typescript/index.ts @@ -7,8 +7,6 @@ */ export { Project } from './project'; -export { filterProjectsByFlag } from './projects'; export { getTsProjectForAbsolutePath } from './get_ts_project_for_absolute_path'; -export { execInProjects } from './exec_in_projects'; export { runTypeCheckCli } from './run_type_check_cli'; export * from './build_ts_refs_cli'; diff --git a/src/dev/typescript/project.ts b/src/dev/typescript/project.ts index 8d92284e49637..a0c196dd8e919 100644 --- a/src/dev/typescript/project.ts +++ b/src/dev/typescript/project.ts @@ -6,17 +6,18 @@ * Side Public License, v 1. */ -import { basename, dirname, relative, resolve } from 'path'; +import Path from 'path'; import { IMinimatch, Minimatch } from 'minimatch'; import { REPO_ROOT } from '@kbn/utils'; import { parseTsConfig } from './ts_configfile'; +import { ProjectSet } from './project_set'; function makeMatchers(directory: string, patterns: string[]) { return patterns.map( (pattern) => - new Minimatch(resolve(directory, pattern), { + new Minimatch(Path.resolve(directory, pattern), { dot: true, }) ); @@ -26,41 +27,151 @@ function testMatchers(matchers: IMinimatch[], path: string) { return matchers.some((matcher) => matcher.match(path)); } +export interface ProjectOptions { + name?: string; + disableTypeCheck?: boolean; +} + +interface LoadOptions { + history?: string[]; + cache?: Map; + skipConfigValidation?: boolean; +} + export class Project { - public directory: string; - public name: string; - public config: any; - public disableTypeCheck: boolean; + static load( + tsConfigPath: string, + projectOptions?: ProjectOptions, + loadOptions: LoadOptions = {} + ): Project { + const cache = loadOptions.cache ?? new Map(); + const cached = cache.get(tsConfigPath); + if (cached) { + return cached; + } - private readonly include: IMinimatch[]; - private readonly exclude: IMinimatch[]; + const config = parseTsConfig(tsConfigPath); - constructor( - public tsConfigPath: string, - options: { name?: string; disableTypeCheck?: boolean } = {} - ) { - this.config = parseTsConfig(tsConfigPath); - - const { files, include, exclude = [] } = this.config as { - files?: string[]; - include?: string[]; - exclude?: string[]; - }; - - if (files || !include) { - throw new Error( - 'tsconfig.json files in the Kibana repo must use "include" keys and not "files"' + if (!loadOptions?.skipConfigValidation) { + if (config.files) { + throw new Error(`${tsConfigPath} must not use "files" key`); + } + + if (!config.include) { + throw new Error(`${tsConfigPath} must have an "include" key`); + } + } + + const directory = Path.dirname(tsConfigPath); + const disableTypeCheck = projectOptions?.disableTypeCheck || false; + const name = + projectOptions?.name || Path.relative(REPO_ROOT, directory) || Path.basename(directory); + const include = config.include ? makeMatchers(directory, config.include) : undefined; + const exclude = config.exclude ? makeMatchers(directory, config.exclude) : undefined; + + let baseProject; + if (config.extends) { + const baseConfigPath = Path.resolve(directory, config.extends); + + // prevent circular deps + if (loadOptions.history?.includes(baseConfigPath)) { + throw new Error( + `circular "extends" are not supported in tsconfig files: ${loadOptions.history} => ${baseConfigPath}` + ); + } + + baseProject = Project.load( + baseConfigPath, + {}, + { + skipConfigValidation: true, + history: [...(loadOptions.history ?? []), tsConfigPath], + cache, + } ); } - this.directory = dirname(this.tsConfigPath); - this.disableTypeCheck = options.disableTypeCheck || false; - this.name = options.name || relative(REPO_ROOT, this.directory) || basename(this.directory); - this.include = makeMatchers(this.directory, include); - this.exclude = makeMatchers(this.directory, exclude); + const project = new Project( + tsConfigPath, + directory, + name, + config, + disableTypeCheck, + baseProject, + include, + exclude + ); + cache.set(tsConfigPath, project); + return project; + } + + constructor( + public readonly tsConfigPath: string, + public readonly directory: string, + public readonly name: string, + public readonly config: any, + public readonly disableTypeCheck: boolean, + + public readonly baseProject?: Project, + private readonly include?: IMinimatch[], + private readonly exclude?: IMinimatch[] + ) {} + + private getInclude(): IMinimatch[] { + return this.include ? this.include : this.baseProject?.getInclude() ?? []; + } + + private getExclude(): IMinimatch[] { + return this.exclude ? this.exclude : this.baseProject?.getExclude() ?? []; } public isAbsolutePathSelected(path: string) { - return testMatchers(this.exclude, path) ? false : testMatchers(this.include, path); + return testMatchers(this.getExclude(), path) ? false : testMatchers(this.getInclude(), path); + } + + public isCompositeProject(): boolean { + const own = this.config.compilerOptions?.composite; + return !!(own === undefined ? this.baseProject?.isCompositeProject() : own); + } + + public getOutDir(): string | undefined { + if (this.config.compilerOptions?.outDir) { + return Path.resolve(this.directory, this.config.compilerOptions.outDir); + } + if (this.baseProject) { + return this.baseProject.getOutDir(); + } + return undefined; + } + + public getRefdPaths(): string[] { + if (this.config.references) { + return (this.config.references as Array<{ path: string }>).map(({ path }) => + Path.resolve(this.directory, path) + ); + } + + return this.baseProject ? this.baseProject.getRefdPaths() : []; + } + + public getProjectsDeep(cache?: Map) { + const projects = new Set(); + const queue = new Set([this.tsConfigPath]); + + for (const path of queue) { + const project = Project.load(path, {}, { skipConfigValidation: true, cache }); + projects.add(project); + for (const refPath of project.getRefdPaths()) { + queue.add(refPath); + } + } + + return new ProjectSet(projects); + } + + public getConfigPaths(): string[] { + return this.baseProject + ? [this.tsConfigPath, ...this.baseProject.getConfigPaths()] + : [this.tsConfigPath]; } } diff --git a/src/dev/typescript/project_set.ts b/src/dev/typescript/project_set.ts new file mode 100644 index 0000000000000..4ef3693cf6d02 --- /dev/null +++ b/src/dev/typescript/project_set.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Project } from './project'; + +export class ProjectSet { + public readonly outDirs: string[]; + private readonly projects: Project[]; + + constructor(projects: Iterable) { + this.projects = [...projects]; + this.outDirs = this.projects + .map((p) => p.getOutDir()) + .filter((p): p is string => typeof p === 'string'); + } + + filterByPaths(paths: string[]) { + return new ProjectSet( + this.projects.filter((p) => + paths.some((f) => p.isAbsolutePathSelected(f) || p.getConfigPaths().includes(f)) + ) + ); + } +} diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts index 2c54bb8dba179..419d4f0854ecc 100644 --- a/src/dev/typescript/projects.ts +++ b/src/dev/typescript/projects.ts @@ -7,67 +7,57 @@ */ import glob from 'glob'; -import { resolve } from 'path'; +import Path from 'path'; import { REPO_ROOT } from '@kbn/utils'; -import { Project } from './project'; +import { Project, ProjectOptions } from './project'; + +/** + * Simple map of all projects defined in this file, to speed of some other operations + * which need to load files by path and to avoid re-parsing base config files hundreds of times + */ +export const PROJECT_CACHE = new Map(); + +const createProject = (rootRelativePath: string, options: ProjectOptions = {}) => + Project.load(Path.resolve(REPO_ROOT, rootRelativePath), options, { + cache: PROJECT_CACHE, + }); + +const findProjects = (pattern: string) => + // NOTE: using glob.sync rather than glob-all or globby + // because it takes less than 10 ms, while the other modules + // both took closer to 1000ms. + glob.sync(pattern, { cwd: REPO_ROOT }).map((path) => createProject(path)); export const PROJECTS = [ - new Project(resolve(REPO_ROOT, 'tsconfig.json')), - new Project(resolve(REPO_ROOT, 'test/tsconfig.json'), { name: 'kibana/test' }), - new Project(resolve(REPO_ROOT, 'x-pack/test/tsconfig.json'), { name: 'x-pack/test' }), - new Project(resolve(REPO_ROOT, 'src/core/tsconfig.json')), - new Project(resolve(REPO_ROOT, 'x-pack/plugins/drilldowns/url_drilldown/tsconfig.json'), { + createProject('tsconfig.json'), + createProject('test/tsconfig.json', { name: 'kibana/test' }), + createProject('x-pack/test/tsconfig.json', { name: 'x-pack/test' }), + createProject('src/core/tsconfig.json'), + + createProject('x-pack/plugins/drilldowns/url_drilldown/tsconfig.json', { name: 'security_solution/cypress', }), - new Project(resolve(REPO_ROOT, 'x-pack/plugins/security_solution/cypress/tsconfig.json'), { + createProject('x-pack/plugins/security_solution/cypress/tsconfig.json', { name: 'security_solution/cypress', }), - new Project(resolve(REPO_ROOT, 'x-pack/plugins/osquery/cypress/tsconfig.json'), { + createProject('x-pack/plugins/osquery/cypress/tsconfig.json', { name: 'osquery/cypress', }), - new Project(resolve(REPO_ROOT, 'x-pack/plugins/apm/e2e/tsconfig.json'), { + createProject('x-pack/plugins/apm/e2e/tsconfig.json', { name: 'apm/cypress', disableTypeCheck: true, }), - new Project(resolve(REPO_ROOT, 'x-pack/plugins/apm/ftr_e2e/tsconfig.json'), { + createProject('x-pack/plugins/apm/ftr_e2e/tsconfig.json', { name: 'apm/ftr_e2e', disableTypeCheck: true, }), - // NOTE: using glob.sync rather than glob-all or globby - // because it takes less than 10 ms, while the other modules - // both took closer to 1000ms. - ...glob - .sync('packages/*/tsconfig.json', { cwd: REPO_ROOT }) - .map((path) => new Project(resolve(REPO_ROOT, path))), - ...glob - .sync('src/plugins/*/tsconfig.json', { cwd: REPO_ROOT }) - .map((path) => new Project(resolve(REPO_ROOT, path))), - ...glob - .sync('x-pack/plugins/*/tsconfig.json', { cwd: REPO_ROOT }) - .map((path) => new Project(resolve(REPO_ROOT, path))), - ...glob - .sync('examples/*/tsconfig.json', { cwd: REPO_ROOT }) - .map((path) => new Project(resolve(REPO_ROOT, path))), - ...glob - .sync('x-pack/examples/*/tsconfig.json', { cwd: REPO_ROOT }) - .map((path) => new Project(resolve(REPO_ROOT, path))), - ...glob - .sync('test/plugin_functional/plugins/*/tsconfig.json', { cwd: REPO_ROOT }) - .map((path) => new Project(resolve(REPO_ROOT, path))), - ...glob - .sync('test/interpreter_functional/plugins/*/tsconfig.json', { cwd: REPO_ROOT }) - .map((path) => new Project(resolve(REPO_ROOT, path))), - ...glob - .sync('test/server_integration/__fixtures__/plugins/*/tsconfig.json', { cwd: REPO_ROOT }) - .map((path) => new Project(resolve(REPO_ROOT, path))), + ...findProjects('packages/*/tsconfig.json'), + ...findProjects('src/plugins/*/tsconfig.json'), + ...findProjects('x-pack/plugins/*/tsconfig.json'), + ...findProjects('examples/*/tsconfig.json'), + ...findProjects('x-pack/examples/*/tsconfig.json'), + ...findProjects('test/plugin_functional/plugins/*/tsconfig.json'), + ...findProjects('test/interpreter_functional/plugins/*/tsconfig.json'), + ...findProjects('test/server_integration/__fixtures__/plugins/*/tsconfig.json'), ]; - -export function filterProjectsByFlag(projectFlag?: string) { - if (!projectFlag) { - return PROJECTS; - } - - const tsConfigPath = resolve(projectFlag); - return PROJECTS.filter((project) => project.tsConfigPath === tsConfigPath); -} diff --git a/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts b/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts index 7347529239176..370ef9d17a04e 100644 --- a/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts +++ b/src/dev/typescript/ref_output_cache/integration_tests/ref_output_cache.test.ts @@ -23,6 +23,8 @@ import { import { RefOutputCache, OUTDIR_MERGE_BASE_FILENAME } from '../ref_output_cache'; import { Archives } from '../archives'; import type { RepoInfo } from '../repo_info'; +import { Project } from '../../project'; +import { ProjectSet } from '../../project_set'; jest.mock('../repo_info'); const { RepoInfo: MockRepoInfo } = jest.requireMock('../repo_info'); @@ -48,25 +50,41 @@ afterEach(async () => { logWriter.messages.length = 0; }); -it('creates and extracts caches, ingoring dirs with matching merge-base file and placing merge-base files', async () => { +function makeMockProject(path: string) { + Fs.mkdirSync(Path.resolve(path, 'target/test-types'), { recursive: true }); + Fs.writeFileSync( + Path.resolve(path, 'tsconfig.json'), + JSON.stringify({ + compilerOptions: { + outDir: './target/test-types', + }, + include: ['**/*'], + exclude: ['test/target/**/*'], + }) + ); + + return Project.load(Path.resolve(path, 'tsconfig.json')); +} + +it('creates and extracts caches, ingoring dirs with matching merge-base file, placing merge-base files, and overriding modified time for updated inputs', async () => { // setup repo mock const HEAD = 'abcdefg'; repo.getHeadSha.mockResolvedValue(HEAD); repo.getRelative.mockImplementation((path) => Path.relative(TMP, path)); repo.getRecentShasFrom.mockResolvedValue(['5678', '1234']); + repo.getFilesChangesSinceSha.mockResolvedValue([]); // create two fake outDirs - const outDirs = [Path.resolve(TMP, 'out/foo'), Path.resolve(TMP, 'out/bar')]; - for (const dir of outDirs) { - Fs.mkdirSync(dir, { recursive: true }); - Fs.writeFileSync(Path.resolve(dir, 'test'), 'hello world'); - } + const projects = new ProjectSet([ + makeMockProject(Path.resolve(TMP, 'test1')), + makeMockProject(Path.resolve(TMP, 'test2')), + ]); // init an archives instance using tmp const archives = await Archives.create(log, TMP); // init the RefOutputCache with our mock data - const refOutputCache = new RefOutputCache(log, repo, archives, outDirs, HEAD); + const refOutputCache = new RefOutputCache(log, repo, archives, projects, HEAD); // create the new cache right in the archives dir await refOutputCache.captureCache(Path.resolve(TMP)); @@ -88,19 +106,21 @@ it('creates and extracts caches, ingoring dirs with matching merge-base file and }); // modify the files in the outDirs so we can see which ones are restored from the cache - for (const dir of outDirs) { - Fs.writeFileSync(Path.resolve(dir, 'test'), 'not cleared by cache init'); + for (const dir of projects.outDirs) { + Fs.writeFileSync(Path.resolve(dir, 'no-cleared.txt'), 'not cleared by cache init'); } - // add the mergeBase to the first outDir so that it is ignored - Fs.writeFileSync(Path.resolve(outDirs[0], OUTDIR_MERGE_BASE_FILENAME), HEAD); + + // add the mergeBase to test1 outDir so that it is not cleared + Fs.writeFileSync(Path.resolve(projects.outDirs[0], OUTDIR_MERGE_BASE_FILENAME), HEAD); // rebuild the outDir from the refOutputCache await refOutputCache.initCaches(); + // verify that "test1" outdir is untouched and that "test2" is cleared out const files = Object.fromEntries( globby .sync( - outDirs.map((p) => normalize(p)), + projects.outDirs.map((p) => normalize(p)), { dot: true } ) .map((path) => [Path.relative(TMP, path), Fs.readFileSync(path, 'utf-8')]) @@ -108,10 +128,9 @@ it('creates and extracts caches, ingoring dirs with matching merge-base file and expect(files).toMatchInlineSnapshot(` Object { - "out/bar/.ts-ref-cache-merge-base": "abcdefg", - "out/bar/test": "hello world", - "out/foo/.ts-ref-cache-merge-base": "abcdefg", - "out/foo/test": "not cleared by cache init", + "test1/target/test-types/.ts-ref-cache-merge-base": "abcdefg", + "test1/target/test-types/no-cleared.txt": "not cleared by cache init", + "test2/target/test-types/.ts-ref-cache-merge-base": "abcdefg", } `); expect(logWriter.messages).toMatchInlineSnapshot(` @@ -124,7 +143,7 @@ it('creates and extracts caches, ingoring dirs with matching merge-base file and " debg download complete, renaming tmp", " debg download of cache for abcdefg complete", " debg extracting archives/abcdefg.zip to rebuild caches in 1 outDirs", - " debg [out/bar] clearing outDir and replacing with cache", + " debg [test2/target/test-types] clearing outDir and replacing with cache", ] `); }); @@ -138,7 +157,7 @@ it('cleans up oldest archives when there are more than 10', async () => { } const archives = await Archives.create(log, TMP); - const cache = new RefOutputCache(log, repo, archives, [], '1234'); + const cache = new RefOutputCache(log, repo, archives, new ProjectSet([]), '1234'); expect(cache.archives.size()).toBe(102); await cache.cleanup(); expect(cache.archives.size()).toBe(10); diff --git a/src/dev/typescript/ref_output_cache/ref_output_cache.ts b/src/dev/typescript/ref_output_cache/ref_output_cache.ts index ca69236a706d2..b7e641ceb33d5 100644 --- a/src/dev/typescript/ref_output_cache/ref_output_cache.ts +++ b/src/dev/typescript/ref_output_cache/ref_output_cache.ts @@ -9,14 +9,15 @@ import Path from 'path'; import Fs from 'fs/promises'; -import { ToolingLog, kibanaPackageJson } from '@kbn/dev-utils'; +import { ToolingLog, kibanaPackageJson, extract } from '@kbn/dev-utils'; import del from 'del'; import tempy from 'tempy'; import { Archives } from './archives'; -import { unzip, zip } from './zip'; +import { zip } from './zip'; import { concurrentMap } from '../concurrent_map'; import { RepoInfo } from './repo_info'; +import { ProjectSet } from '../project_set'; export const OUTDIR_MERGE_BASE_FILENAME = '.ts-ref-cache-merge-base'; @@ -49,7 +50,7 @@ export class RefOutputCache { static async create(options: { log: ToolingLog; workingDir: string; - outDirs: string[]; + projects: ProjectSet; repoRoot: string; upstreamUrl: string; }) { @@ -59,14 +60,14 @@ export class RefOutputCache { const upstreamBranch: string = kibanaPackageJson.branch; const mergeBase = await repoInfo.getMergeBase('HEAD', upstreamBranch); - return new RefOutputCache(options.log, repoInfo, archives, options.outDirs, mergeBase); + return new RefOutputCache(options.log, repoInfo, archives, options.projects, mergeBase); } constructor( private readonly log: ToolingLog, private readonly repo: RepoInfo, public readonly archives: Archives, - private readonly outDirs: string[], + private readonly projects: ProjectSet, private readonly mergeBase: string ) {} @@ -77,7 +78,7 @@ export class RefOutputCache { */ async initCaches() { const outdatedOutDirs = ( - await concurrentMap(100, this.outDirs, async (outDir) => ({ + await concurrentMap(100, this.projects.outDirs, async (outDir) => ({ path: outDir, outdated: !(await matchMergeBase(outDir, this.mergeBase)), })) @@ -101,6 +102,9 @@ export class RefOutputCache { return; } + const changedFiles = await this.repo.getFilesChangesSinceSha(archive.sha); + const outDirsForcingExtraCacheCheck = this.projects.filterByPaths(changedFiles).outDirs; + const tmpDir = tempy.directory(); this.log.debug( 'extracting', @@ -109,9 +113,13 @@ export class RefOutputCache { outdatedOutDirs.length, 'outDirs' ); - await unzip(archive.path, tmpDir); + await extract({ + archivePath: archive.path, + targetDir: tmpDir, + }); const cacheNames = await Fs.readdir(tmpDir); + const beginningOfTime = new Date(0); await concurrentMap(50, outdatedOutDirs, async (outDir) => { const relative = this.repo.getRelative(outDir); @@ -129,9 +137,22 @@ export class RefOutputCache { return; } - this.log.debug(`[${relative}] clearing outDir and replacing with cache`); + const setModifiedTimes = outDirsForcingExtraCacheCheck.includes(outDir) + ? beginningOfTime + : undefined; + + if (setModifiedTimes) { + this.log.debug(`[${relative}] replacing outDir with cache (forcing revalidation)`); + } else { + this.log.debug(`[${relative}] clearing outDir and replacing with cache`); + } + await del(outDir); - await unzip(Path.resolve(tmpDir, cacheName), outDir); + await extract({ + archivePath: Path.resolve(tmpDir, cacheName), + targetDir: outDir, + setModifiedTimes, + }); await Fs.writeFile(Path.resolve(outDir, OUTDIR_MERGE_BASE_FILENAME), this.mergeBase); }); } @@ -154,7 +175,7 @@ export class RefOutputCache { const subZips: Array<[string, string]> = []; await Promise.all( - this.outDirs.map(async (absolute) => { + this.projects.outDirs.map(async (absolute) => { const relative = this.repo.getRelative(absolute); const subZipName = `${relative.split(Path.sep).join('__')}.zip`; const subZipPath = Path.resolve(tmpDir, subZipName); diff --git a/src/dev/typescript/ref_output_cache/repo_info.ts b/src/dev/typescript/ref_output_cache/repo_info.ts index 9a51f3f75182b..0bba010bc185d 100644 --- a/src/dev/typescript/ref_output_cache/repo_info.ts +++ b/src/dev/typescript/ref_output_cache/repo_info.ts @@ -52,4 +52,20 @@ export class RepoInfo { return proc.stdout.trim(); } + + async getFilesChangesSinceSha(sha: string) { + this.log.debug('determining files changes since sha', sha); + + const proc = await execa('git', ['diff', '--name-only', sha], { + cwd: this.dir, + }); + const files = proc.stdout + .trim() + .split('\n') + .map((p) => Path.resolve(this.dir, p)); + + this.log.verbose('found the following changes compared to', sha, `\n - ${files.join('\n - ')}`); + + return files; + } } diff --git a/src/dev/typescript/ref_output_cache/zip.ts b/src/dev/typescript/ref_output_cache/zip.ts index b1bd8f514bb95..6b0ee053367de 100644 --- a/src/dev/typescript/ref_output_cache/zip.ts +++ b/src/dev/typescript/ref_output_cache/zip.ts @@ -12,7 +12,6 @@ import Path from 'path'; import { pipeline } from 'stream'; import { promisify } from 'util'; -import extractZip from 'extract-zip'; import archiver from 'archiver'; const asyncPipeline = promisify(pipeline); @@ -44,9 +43,3 @@ export async function zip( // await the promise from the pipeline and archive.finalize() await Promise.all([asyncPipeline(archive, createWriteStream(outputPath)), archive.finalize()]); } - -export async function unzip(path: string, outputDir: string) { - await extractZip(path, { - dir: outputDir, - }); -} diff --git a/src/dev/typescript/root_refs_config.ts b/src/dev/typescript/root_refs_config.ts new file mode 100644 index 0000000000000..c297a9288ddd5 --- /dev/null +++ b/src/dev/typescript/root_refs_config.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; +import Fs from 'fs/promises'; + +import dedent from 'dedent'; +import { REPO_ROOT, ToolingLog } from '@kbn/dev-utils'; + +import { PROJECTS } from './projects'; + +export const ROOT_REFS_CONFIG_PATH = Path.resolve(REPO_ROOT, 'tsconfig.refs.json'); +export const REF_CONFIG_PATHS = [ROOT_REFS_CONFIG_PATH]; + +async function isRootRefsConfigSelfManaged() { + try { + const currentRefsFile = await Fs.readFile(ROOT_REFS_CONFIG_PATH, 'utf-8'); + return currentRefsFile.trim().startsWith('// SELF MANAGED'); + } catch (error) { + if (error.code === 'ENOENT') { + return false; + } + + throw error; + } +} + +function generateTsConfig(refs: string[]) { + return dedent` + // This file is automatically updated when you run \`node scripts/build_ts_refs\`. If you start this + // file with the text "// SELF MANAGED" then you can comment out any projects that you like and only + // build types for specific projects and their dependencies + { + "include": [], + "references": [ +${refs.map((p) => ` { "path": ${JSON.stringify(p)} },`).join('\n')} + ] + } + `; +} + +export async function updateRootRefsConfig(log: ToolingLog) { + if (await isRootRefsConfigSelfManaged()) { + log.warning( + 'tsconfig.refs.json starts with "// SELF MANAGED" so not updating to include all projects' + ); + return; + } + + const refs = PROJECTS.filter((p) => p.isCompositeProject()) + .map((p) => `./${Path.relative(REPO_ROOT, p.tsConfigPath)}`) + .sort((a, b) => a.localeCompare(b)); + + log.debug('updating', ROOT_REFS_CONFIG_PATH); + await Fs.writeFile(ROOT_REFS_CONFIG_PATH, generateTsConfig(refs) + '\n'); +} diff --git a/src/dev/typescript/run_type_check_cli.ts b/src/dev/typescript/run_type_check_cli.ts index d9e9eb036fe0f..1bf31a6c5bac0 100644 --- a/src/dev/typescript/run_type_check_cli.ts +++ b/src/dev/typescript/run_type_check_cli.ts @@ -6,96 +6,116 @@ * Side Public License, v 1. */ -import { ToolingLog } from '@kbn/dev-utils'; -import chalk from 'chalk'; -import dedent from 'dedent'; -import getopts from 'getopts'; +import Path from 'path'; +import Os from 'os'; -import { execInProjects } from './exec_in_projects'; -import { filterProjectsByFlag } from './projects'; +import * as Rx from 'rxjs'; +import { mergeMap, reduce } from 'rxjs/operators'; +import execa from 'execa'; +import { run, createFailError } from '@kbn/dev-utils'; +import { lastValueFrom } from '@kbn/std'; + +import { PROJECTS } from './projects'; import { buildAllTsRefs } from './build_ts_refs'; +import { updateRootRefsConfig } from './root_refs_config'; export async function runTypeCheckCli() { - const extraFlags: string[] = []; - const opts = getopts(process.argv.slice(2), { - boolean: ['skip-lib-check', 'help'], - default: { - project: undefined, - }, - unknown(name) { - extraFlags.push(name); - return false; + run( + async ({ log, flags, procRunner }) => { + // if the tsconfig.refs.json file is not self-managed then make sure it has + // a reference to every composite project in the repo + await updateRootRefsConfig(log); + + const { failed } = await buildAllTsRefs({ log, procRunner, verbose: !!flags.verbose }); + if (failed) { + throw createFailError('Unable to build TS project refs'); + } + + const projectFilter = + flags.project && typeof flags.project === 'string' + ? Path.resolve(flags.project) + : undefined; + + const projects = PROJECTS.filter((p) => { + return ( + !p.disableTypeCheck && + (!projectFilter || p.tsConfigPath === projectFilter) && + !p.isCompositeProject() + ); + }); + + if (!projects.length) { + throw createFailError(`Unable to find project at ${flags.project}`); + } + + const concurrency = Math.min(4, Math.round((Os.cpus() || []).length / 2) || 1) || 1; + log.info('running type check in', projects.length, 'non-composite projects'); + + const tscArgs = [ + ...['--emitDeclarationOnly', 'false'], + '--noEmit', + '--pretty', + ...(flags['skip-lib-check'] + ? ['--skipLibCheck', flags['skip-lib-check'] as string] + : ['--skipLibCheck', 'false']), + ]; + + const failureCount = await lastValueFrom( + Rx.from(projects).pipe( + mergeMap(async (p) => { + const relativePath = Path.relative(process.cwd(), p.tsConfigPath); + + const result = await execa( + process.execPath, + [ + '--max-old-space-size=5120', + require.resolve('typescript/bin/tsc'), + ...['--project', p.tsConfigPath, ...(flags.verbose ? ['--verbose'] : [])], + ...tscArgs, + ], + { + reject: false, + all: true, + } + ); + + if (result.failed) { + log.error(`Type check failed in ${relativePath}:`); + log.error(result.all ?? ' - tsc produced no output - '); + return 1; + } else { + log.success(relativePath); + return 0; + } + }, concurrency), + reduce((acc, f) => acc + f, 0) + ) + ); + + if (failureCount > 0) { + throw createFailError(`${failureCount} type checks failed`); + } }, - }); - - const log = new ToolingLog({ - level: 'info', - writeTo: process.stdout, - }); - - if (extraFlags.length) { - for (const flag of extraFlags) { - log.error(`Unknown flag: ${flag}`); - } - - process.exitCode = 1; - opts.help = true; - } - - if (opts.help) { - process.stdout.write( - dedent(chalk` - {dim usage:} node scripts/type_check [...options] - - Run the TypeScript compiler without emitting files so that it can check - types during development. + { + description: ` + Run the TypeScript compiler without emitting files so that it can check types during development. Examples: - - {dim # check types in all projects} - {dim $} node scripts/type_check - - {dim # check types in a single project} - {dim $} node scripts/type_check --project packages/kbn-pm/tsconfig.json - - Options: - - --project [path] {dim Path to a tsconfig.json file determines the project to check} - --skip-lib-check {dim Skip type checking of all declaration files (*.d.ts). Default is false} - --help {dim Show this message} - `) - ); - process.stdout.write('\n'); - process.exit(); - } - - const { failed } = await buildAllTsRefs(log); - if (failed) { - log.error('Unable to build TS project refs'); - process.exit(1); - } - - const tscArgs = [ - // composite project cannot be used with --noEmit - ...['--composite', 'false'], - ...['--emitDeclarationOnly', 'false'], - '--noEmit', - '--pretty', - ...(opts['skip-lib-check'] - ? ['--skipLibCheck', opts['skip-lib-check']] - : ['--skipLibCheck', 'false']), - ]; - const projects = filterProjectsByFlag(opts.project).filter((p) => !p.disableTypeCheck); - - if (!projects.length) { - log.error(`Unable to find project at ${opts.project}`); - process.exit(1); - } - - execInProjects(log, projects, process.execPath, (project) => [ - '--max-old-space-size=5120', - require.resolve('typescript/bin/tsc'), - ...['--project', project.tsConfigPath], - ...tscArgs, - ]); + # check types in all projects + node scripts/type_check + + # check types in a single project + node scripts/type_check --project packages/kbn-pm/tsconfig.json + `, + flags: { + string: ['project'], + boolean: ['skip-lib-check'], + help: ` + --project [path] Path to a tsconfig.json file determines the project to check + --skip-lib-check Skip type checking of all declaration files (*.d.ts). Default is false + --help Show this message + `, + }, + } + ); } diff --git a/src/plugins/advanced_settings/tsconfig.json b/src/plugins/advanced_settings/tsconfig.json index 4d62e410326b6..b207f600cbd4e 100644 --- a/src/plugins/advanced_settings/tsconfig.json +++ b/src/plugins/advanced_settings/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/apm_oss/tsconfig.json b/src/plugins/apm_oss/tsconfig.json index aeb6837c69a99..08ed86d5da0a8 100644 --- a/src/plugins/apm_oss/tsconfig.json +++ b/src/plugins/apm_oss/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/bfetch/tsconfig.json b/src/plugins/bfetch/tsconfig.json index 173ff725d07d0..8fceb7d815ee9 100644 --- a/src/plugins/bfetch/tsconfig.json +++ b/src/plugins/bfetch/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/charts/tsconfig.json b/src/plugins/charts/tsconfig.json index a4f65d5937204..fc05a26068654 100644 --- a/src/plugins/charts/tsconfig.json +++ b/src/plugins/charts/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/console/tsconfig.json b/src/plugins/console/tsconfig.json index 34aca5021bac4..ee6fbfebc77a2 100644 --- a/src/plugins/console/tsconfig.json +++ b/src/plugins/console/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index 12820fc08310d..4febb8b5555cf 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/data/tsconfig.json b/src/plugins/data/tsconfig.json index 6e4e89f7538fd..3687604e05e2b 100644 --- a/src/plugins/data/tsconfig.json +++ b/src/plugins/data/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/dev_tools/tsconfig.json b/src/plugins/dev_tools/tsconfig.json index c17b2341fd42f..bbb33ac5b2f99 100644 --- a/src/plugins/dev_tools/tsconfig.json +++ b/src/plugins/dev_tools/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json index 8c71091e3ecf2..b3f1ad5d0bc1e 100644 --- a/src/plugins/discover/tsconfig.json +++ b/src/plugins/discover/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/embeddable/tsconfig.json b/src/plugins/embeddable/tsconfig.json index 27a887500fb68..b7a6ff3252d6a 100644 --- a/src/plugins/embeddable/tsconfig.json +++ b/src/plugins/embeddable/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/es_ui_shared/tsconfig.json b/src/plugins/es_ui_shared/tsconfig.json index 5f136d09b2ce4..38e6cf78f8f60 100644 --- a/src/plugins/es_ui_shared/tsconfig.json +++ b/src/plugins/es_ui_shared/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/expression_error/tsconfig.json b/src/plugins/expression_error/tsconfig.json index aa4562ec73576..111ff58935a35 100644 --- a/src/plugins/expression_error/tsconfig.json +++ b/src/plugins/expression_error/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/expression_image/tsconfig.json b/src/plugins/expression_image/tsconfig.json index 5fab51496c97e..9a7175a8d767b 100644 --- a/src/plugins/expression_image/tsconfig.json +++ b/src/plugins/expression_image/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/expression_metric/tsconfig.json b/src/plugins/expression_metric/tsconfig.json index 5fab51496c97e..9a7175a8d767b 100644 --- a/src/plugins/expression_metric/tsconfig.json +++ b/src/plugins/expression_metric/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/expression_repeat_image/tsconfig.json b/src/plugins/expression_repeat_image/tsconfig.json index aa4562ec73576..111ff58935a35 100644 --- a/src/plugins/expression_repeat_image/tsconfig.json +++ b/src/plugins/expression_repeat_image/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/expression_reveal_image/tsconfig.json b/src/plugins/expression_reveal_image/tsconfig.json index aa4562ec73576..111ff58935a35 100644 --- a/src/plugins/expression_reveal_image/tsconfig.json +++ b/src/plugins/expression_reveal_image/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/expression_shape/tsconfig.json b/src/plugins/expression_shape/tsconfig.json index 5fab51496c97e..9a7175a8d767b 100644 --- a/src/plugins/expression_shape/tsconfig.json +++ b/src/plugins/expression_shape/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/expressions/tsconfig.json b/src/plugins/expressions/tsconfig.json index cce71013cefa5..6716149d6b9c7 100644 --- a/src/plugins/expressions/tsconfig.json +++ b/src/plugins/expressions/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/field_formats/tsconfig.json b/src/plugins/field_formats/tsconfig.json index 4382ab1051c1d..9fb87bc5dd970 100644 --- a/src/plugins/field_formats/tsconfig.json +++ b/src/plugins/field_formats/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/home/tsconfig.json b/src/plugins/home/tsconfig.json index b15e1fc011b92..9324978b227d5 100644 --- a/src/plugins/home/tsconfig.json +++ b/src/plugins/home/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/index_pattern_field_editor/tsconfig.json b/src/plugins/index_pattern_field_editor/tsconfig.json index 5450ae74a91a0..e5caf463835d0 100644 --- a/src/plugins/index_pattern_field_editor/tsconfig.json +++ b/src/plugins/index_pattern_field_editor/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/index_pattern_management/tsconfig.json b/src/plugins/index_pattern_management/tsconfig.json index 16afcb3599fec..2a719e98bea31 100644 --- a/src/plugins/index_pattern_management/tsconfig.json +++ b/src/plugins/index_pattern_management/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/input_control_vis/public/input_control_vis_renderer.tsx b/src/plugins/input_control_vis/public/input_control_vis_renderer.tsx index 867fe13aee08f..81ff7a5aa47f1 100644 --- a/src/plugins/input_control_vis/public/input_control_vis_renderer.tsx +++ b/src/plugins/input_control_vis/public/input_control_vis_renderer.tsx @@ -24,8 +24,7 @@ export const getInputControlVisRenderer: ( if (!registeredController) { const { createInputControlVisController } = await import('./vis_controller'); - const Controller = createInputControlVisController(deps, handlers); - registeredController = new Controller(domNode); + registeredController = createInputControlVisController(deps, handlers, domNode); inputControlVisRegistry.set(domNode, registeredController); handlers.onDestroy(() => { diff --git a/src/plugins/input_control_vis/public/vis_controller.tsx b/src/plugins/input_control_vis/public/vis_controller.tsx index 566f9035fe77e..bb09a90bb9dd6 100644 --- a/src/plugins/input_control_vis/public/vis_controller.tsx +++ b/src/plugins/input_control_vis/public/vis_controller.tsx @@ -24,18 +24,17 @@ import { ListControl } from './control/list_control_factory'; import { InputControlVisDependencies } from './plugin'; import { InputControlVisParams } from './types'; -export type InputControlVisControllerType = InstanceType< - ReturnType ->; +export type InputControlVisControllerType = ReturnType; export const createInputControlVisController = ( deps: InputControlVisDependencies, - handlers: IInterpreterRenderHandlers + handlers: IInterpreterRenderHandlers, + el: Element ) => { - return class InputControlVisController { - private I18nContext?: I18nStart['Context']; - private _isLoaded = false; + let I18nContext: I18nStart['Context'] | undefined; + let isLoaded = false; + return new (class InputControlVisController { controls: Array; queryBarUpdateHandler: () => void; filterManager: FilterManager; @@ -43,7 +42,7 @@ export const createInputControlVisController = ( timeFilterSubscription: Subscription; visParams?: InputControlVisParams; - constructor(public el: Element) { + constructor() { this.controls = []; this.queryBarUpdateHandler = this.updateControlsFromKbn.bind(this); @@ -56,21 +55,21 @@ export const createInputControlVisController = ( .getTimeUpdate$() .subscribe(() => { if (this.visParams?.useTimeFilter) { - this._isLoaded = false; + isLoaded = false; } }); } async render(visParams: InputControlVisParams) { - if (!this.I18nContext) { + if (!I18nContext) { const [{ i18n }] = await deps.core.getStartServices(); - this.I18nContext = i18n.Context; + I18nContext = i18n.Context; } - if (!this._isLoaded || !isEqual(visParams, this.visParams)) { + if (!isLoaded || !isEqual(visParams, this.visParams)) { this.visParams = visParams; this.controls = []; this.controls = await this.initControls(visParams); - this._isLoaded = true; + isLoaded = true; } this.drawVis(); } @@ -78,17 +77,17 @@ export const createInputControlVisController = ( destroy() { this.updateSubsciption.unsubscribe(); this.timeFilterSubscription.unsubscribe(); - unmountComponentAtNode(this.el); + unmountComponentAtNode(el); this.controls.forEach((control) => control.destroy()); } drawVis = () => { - if (!this.I18nContext) { + if (!I18nContext) { throw new Error('no i18n context found'); } render( - + - , - this.el + , + el ); }; @@ -235,5 +234,5 @@ export const createInputControlVisController = ( await this.controls[controlIndex].fetch(query); this.drawVis(); }; - }; + })(); }; diff --git a/src/plugins/input_control_vis/tsconfig.json b/src/plugins/input_control_vis/tsconfig.json index bef7bc394a6cc..5e53199bb1e6e 100644 --- a/src/plugins/input_control_vis/tsconfig.json +++ b/src/plugins/input_control_vis/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/inspector/tsconfig.json b/src/plugins/inspector/tsconfig.json index 2a9c41464532c..4554a90821d48 100644 --- a/src/plugins/inspector/tsconfig.json +++ b/src/plugins/inspector/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/interactive_setup/tsconfig.json b/src/plugins/interactive_setup/tsconfig.json index 530e01a034b00..6ebeff836f69b 100644 --- a/src/plugins/interactive_setup/tsconfig.json +++ b/src/plugins/interactive_setup/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/kibana_legacy/tsconfig.json b/src/plugins/kibana_legacy/tsconfig.json index 709036c9e82f4..17f1a70838fd7 100644 --- a/src/plugins/kibana_legacy/tsconfig.json +++ b/src/plugins/kibana_legacy/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/kibana_overview/tsconfig.json b/src/plugins/kibana_overview/tsconfig.json index ac3ac109cb35f..fd9002d39ee8c 100644 --- a/src/plugins/kibana_overview/tsconfig.json +++ b/src/plugins/kibana_overview/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/kibana_react/tsconfig.json b/src/plugins/kibana_react/tsconfig.json index eb9a24ca141f6..3f6dd8fd280b6 100644 --- a/src/plugins/kibana_react/tsconfig.json +++ b/src/plugins/kibana_react/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/kibana_usage_collection/tsconfig.json b/src/plugins/kibana_usage_collection/tsconfig.json index ee07dfe589e4a..e57d6e25db8cd 100644 --- a/src/plugins/kibana_usage_collection/tsconfig.json +++ b/src/plugins/kibana_usage_collection/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/kibana_utils/tsconfig.json b/src/plugins/kibana_utils/tsconfig.json index ae5e9b90af807..0538b145a5d62 100644 --- a/src/plugins/kibana_utils/tsconfig.json +++ b/src/plugins/kibana_utils/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/legacy_export/tsconfig.json b/src/plugins/legacy_export/tsconfig.json index ec006d492499e..2f071b5ba6c56 100644 --- a/src/plugins/legacy_export/tsconfig.json +++ b/src/plugins/legacy_export/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/management/tsconfig.json b/src/plugins/management/tsconfig.json index ba3661666631a..beef79c9affd0 100644 --- a/src/plugins/management/tsconfig.json +++ b/src/plugins/management/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/maps_ems/tsconfig.json b/src/plugins/maps_ems/tsconfig.json index b85c3da66b83a..dd579b47f3109 100644 --- a/src/plugins/maps_ems/tsconfig.json +++ b/src/plugins/maps_ems/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/maps_legacy/tsconfig.json b/src/plugins/maps_legacy/tsconfig.json index f757e35f785af..b6fcb9345b1ce 100644 --- a/src/plugins/maps_legacy/tsconfig.json +++ b/src/plugins/maps_legacy/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/navigation/tsconfig.json b/src/plugins/navigation/tsconfig.json index 07cfe10d7d81f..1b7e87aaba41a 100644 --- a/src/plugins/navigation/tsconfig.json +++ b/src/plugins/navigation/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/newsfeed/tsconfig.json b/src/plugins/newsfeed/tsconfig.json index 18e6f2de1bc6f..1388e11087254 100644 --- a/src/plugins/newsfeed/tsconfig.json +++ b/src/plugins/newsfeed/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/presentation_util/tsconfig.json b/src/plugins/presentation_util/tsconfig.json index e0b9bbeb4917d..caff10a90e84c 100644 --- a/src/plugins/presentation_util/tsconfig.json +++ b/src/plugins/presentation_util/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/region_map/tsconfig.json b/src/plugins/region_map/tsconfig.json index 899611d027465..fec191402f2ab 100644 --- a/src/plugins/region_map/tsconfig.json +++ b/src/plugins/region_map/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/saved_objects/tsconfig.json b/src/plugins/saved_objects/tsconfig.json index d9045b91b9dfa..b8761ab81fa78 100644 --- a/src/plugins/saved_objects/tsconfig.json +++ b/src/plugins/saved_objects/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/saved_objects_management/tsconfig.json b/src/plugins/saved_objects_management/tsconfig.json index 99849dea38618..0f26da69acd17 100644 --- a/src/plugins/saved_objects_management/tsconfig.json +++ b/src/plugins/saved_objects_management/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/saved_objects_tagging_oss/tsconfig.json b/src/plugins/saved_objects_tagging_oss/tsconfig.json index b0059c71424bf..5a3f642a9d6ea 100644 --- a/src/plugins/saved_objects_tagging_oss/tsconfig.json +++ b/src/plugins/saved_objects_tagging_oss/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/screenshot_mode/tsconfig.json b/src/plugins/screenshot_mode/tsconfig.json index 58194b385448b..832972ae0baa0 100644 --- a/src/plugins/screenshot_mode/tsconfig.json +++ b/src/plugins/screenshot_mode/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/security_oss/tsconfig.json b/src/plugins/security_oss/tsconfig.json index 530e01a034b00..6ebeff836f69b 100644 --- a/src/plugins/security_oss/tsconfig.json +++ b/src/plugins/security_oss/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/share/tsconfig.json b/src/plugins/share/tsconfig.json index 985066915f1dd..c6afc06bc61c2 100644 --- a/src/plugins/share/tsconfig.json +++ b/src/plugins/share/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/spaces_oss/tsconfig.json b/src/plugins/spaces_oss/tsconfig.json index 0cc82d7e5d124..35942863c1f1b 100644 --- a/src/plugins/spaces_oss/tsconfig.json +++ b/src/plugins/spaces_oss/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/telemetry/tsconfig.json b/src/plugins/telemetry/tsconfig.json index 710e209537b8e..d50ccd563fe5a 100644 --- a/src/plugins/telemetry/tsconfig.json +++ b/src/plugins/telemetry/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/telemetry_collection_manager/tsconfig.json b/src/plugins/telemetry_collection_manager/tsconfig.json index 1329979860603..adfe3bba07963 100644 --- a/src/plugins/telemetry_collection_manager/tsconfig.json +++ b/src/plugins/telemetry_collection_manager/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/telemetry_management_section/tsconfig.json b/src/plugins/telemetry_management_section/tsconfig.json index 2daee868ac200..0f00f12e71c20 100644 --- a/src/plugins/telemetry_management_section/tsconfig.json +++ b/src/plugins/telemetry_management_section/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/tile_map/tsconfig.json b/src/plugins/tile_map/tsconfig.json index 899611d027465..fec191402f2ab 100644 --- a/src/plugins/tile_map/tsconfig.json +++ b/src/plugins/tile_map/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/timelion/tsconfig.json b/src/plugins/timelion/tsconfig.json index 5b96d69a878ea..594901c3cc1ed 100644 --- a/src/plugins/timelion/tsconfig.json +++ b/src/plugins/timelion/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/ui_actions/tsconfig.json b/src/plugins/ui_actions/tsconfig.json index a871d7215cdc5..d53d60ec73cd4 100644 --- a/src/plugins/ui_actions/tsconfig.json +++ b/src/plugins/ui_actions/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/url_forwarding/tsconfig.json b/src/plugins/url_forwarding/tsconfig.json index 8e867a6bad14f..c6ef2a0286da1 100644 --- a/src/plugins/url_forwarding/tsconfig.json +++ b/src/plugins/url_forwarding/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/usage_collection/tsconfig.json b/src/plugins/usage_collection/tsconfig.json index 68a0853994e80..7fac30a8048ea 100644 --- a/src/plugins/usage_collection/tsconfig.json +++ b/src/plugins/usage_collection/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/vis_default_editor/tsconfig.json b/src/plugins/vis_default_editor/tsconfig.json index 34003bced5ad0..3d8fb6778c85e 100644 --- a/src/plugins/vis_default_editor/tsconfig.json +++ b/src/plugins/vis_default_editor/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/vis_type_markdown/tsconfig.json b/src/plugins/vis_type_markdown/tsconfig.json index d5ab89b98081b..7c32f44935195 100644 --- a/src/plugins/vis_type_markdown/tsconfig.json +++ b/src/plugins/vis_type_markdown/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/vis_type_metric/tsconfig.json b/src/plugins/vis_type_metric/tsconfig.json index bee666a5906cd..e430ec2460796 100644 --- a/src/plugins/vis_type_metric/tsconfig.json +++ b/src/plugins/vis_type_metric/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/vis_type_pie/tsconfig.json b/src/plugins/vis_type_pie/tsconfig.json index 69bd2855b9843..9640447b35d98 100644 --- a/src/plugins/vis_type_pie/tsconfig.json +++ b/src/plugins/vis_type_pie/tsconfig.json @@ -1,25 +1,24 @@ { - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "composite": true, - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": [ - "common/**/*", - "public/**/*", - "server/**/*" - ], - "references": [ - { "path": "../../core/tsconfig.json" }, - { "path": "../charts/tsconfig.json" }, - { "path": "../data/tsconfig.json" }, - { "path": "../expressions/tsconfig.json" }, - { "path": "../visualizations/tsconfig.json" }, - { "path": "../usage_collection/tsconfig.json" }, - { "path": "../vis_default_editor/tsconfig.json" }, - { "path": "../field_formats/tsconfig.json" } - ] - } \ No newline at end of file + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*" + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../charts/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + { "path": "../expressions/tsconfig.json" }, + { "path": "../visualizations/tsconfig.json" }, + { "path": "../usage_collection/tsconfig.json" }, + { "path": "../vis_default_editor/tsconfig.json" }, + { "path": "../field_formats/tsconfig.json" } + ] +} diff --git a/src/plugins/vis_type_table/tsconfig.json b/src/plugins/vis_type_table/tsconfig.json index 50277d51e8748..16f2f809bde38 100644 --- a/src/plugins/vis_type_table/tsconfig.json +++ b/src/plugins/vis_type_table/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/vis_type_tagcloud/tsconfig.json b/src/plugins/vis_type_tagcloud/tsconfig.json index 18bbad2257466..021237dd7ad5b 100644 --- a/src/plugins/vis_type_tagcloud/tsconfig.json +++ b/src/plugins/vis_type_tagcloud/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/vis_type_timelion/tsconfig.json b/src/plugins/vis_type_timelion/tsconfig.json index 77f97de28366d..efeab8d73db1e 100644 --- a/src/plugins/vis_type_timelion/tsconfig.json +++ b/src/plugins/vis_type_timelion/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/vis_type_timeseries/tsconfig.json b/src/plugins/vis_type_timeseries/tsconfig.json index 7b2dd4b608c1c..68097d8cff786 100644 --- a/src/plugins/vis_type_timeseries/tsconfig.json +++ b/src/plugins/vis_type_timeseries/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/vis_type_vega/tsconfig.json b/src/plugins/vis_type_vega/tsconfig.json index 4091dafcbe357..e1b8b5d9d4bac 100644 --- a/src/plugins/vis_type_vega/tsconfig.json +++ b/src/plugins/vis_type_vega/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/vis_type_vislib/tsconfig.json b/src/plugins/vis_type_vislib/tsconfig.json index 5bf1af9ba75fe..0d2a4094f04be 100644 --- a/src/plugins/vis_type_vislib/tsconfig.json +++ b/src/plugins/vis_type_vislib/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/vis_type_xy/tsconfig.json b/src/plugins/vis_type_xy/tsconfig.json index 5cb0bc8d0bc8e..0e4e41c286bd2 100644 --- a/src/plugins/vis_type_xy/tsconfig.json +++ b/src/plugins/vis_type_xy/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/visualizations/tsconfig.json b/src/plugins/visualizations/tsconfig.json index 356448aa59771..65ab83d5e0bae 100644 --- a/src/plugins/visualizations/tsconfig.json +++ b/src/plugins/visualizations/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/src/plugins/visualize/tsconfig.json b/src/plugins/visualize/tsconfig.json index bc0891f391746..4dcf43dadf8ba 100644 --- a/src/plugins/visualize/tsconfig.json +++ b/src/plugins/visualize/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/tsconfig.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/tsconfig.json index 3d9d8ca9451d4..86170d3561408 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/tsconfig.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -13,6 +12,10 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../../../src/plugins/expressions/tsconfig.json" }, + { "path": "../../../../src/plugins/inspector/tsconfig.json" }, + { "path": "../../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../../src/plugins/kibana_utils/tsconfig.json" }, ] } diff --git a/test/plugin_functional/plugins/app_link_test/tsconfig.json b/test/plugin_functional/plugins/app_link_test/tsconfig.json index f77a5eaffc301..b53fafd70cf5d 100644 --- a/test/plugin_functional/plugins/app_link_test/tsconfig.json +++ b/test/plugin_functional/plugins/app_link_test/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -12,6 +11,6 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/plugins/kibana_react/tsconfig.json" } ] } diff --git a/test/plugin_functional/plugins/core_app_status/tsconfig.json b/test/plugin_functional/plugins/core_app_status/tsconfig.json index 0f0e7caf2b486..f380bcc8e8b36 100644 --- a/test/plugin_functional/plugins/core_app_status/tsconfig.json +++ b/test/plugin_functional/plugins/core_app_status/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "composite": true, - "outDir": "./target", + "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, "declarationMap": true diff --git a/test/plugin_functional/plugins/core_history_block/tsconfig.json b/test/plugin_functional/plugins/core_history_block/tsconfig.json index ffd2cd261ab23..a6882ecb3d1e0 100644 --- a/test/plugin_functional/plugins/core_history_block/tsconfig.json +++ b/test/plugin_functional/plugins/core_history_block/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": ["index.ts", "public/**/*.ts", "public/**/*.tsx", "../../../../typings/**/*"], "exclude": [], diff --git a/test/plugin_functional/plugins/core_http/tsconfig.json b/test/plugin_functional/plugins/core_http/tsconfig.json index 3d9d8ca9451d4..eab76d901e427 100644 --- a/test/plugin_functional/plugins/core_http/tsconfig.json +++ b/test/plugin_functional/plugins/core_http/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/plugin_functional/plugins/core_plugin_a/tsconfig.json b/test/plugin_functional/plugins/core_plugin_a/tsconfig.json index 3d9d8ca9451d4..eab76d901e427 100644 --- a/test/plugin_functional/plugins/core_plugin_a/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_a/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/plugin_functional/plugins/core_plugin_appleave/tsconfig.json b/test/plugin_functional/plugins/core_plugin_appleave/tsconfig.json index f77a5eaffc301..87e51c3eab37a 100644 --- a/test/plugin_functional/plugins/core_plugin_appleave/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_appleave/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/plugin_functional/plugins/core_plugin_b/tsconfig.json b/test/plugin_functional/plugins/core_plugin_b/tsconfig.json index 3d9d8ca9451d4..78476ce6697e1 100644 --- a/test/plugin_functional/plugins/core_plugin_b/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_b/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -13,6 +12,7 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../core_plugin_a/tsconfig.json" }, ] } diff --git a/test/plugin_functional/plugins/core_plugin_chromeless/tsconfig.json b/test/plugin_functional/plugins/core_plugin_chromeless/tsconfig.json index f77a5eaffc301..010574f0c3be0 100644 --- a/test/plugin_functional/plugins/core_plugin_chromeless/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_chromeless/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -12,6 +11,6 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/core/tsconfig.json" }, ] } diff --git a/test/plugin_functional/plugins/core_plugin_deep_links/tsconfig.json b/test/plugin_functional/plugins/core_plugin_deep_links/tsconfig.json index f77a5eaffc301..87e51c3eab37a 100644 --- a/test/plugin_functional/plugins/core_plugin_deep_links/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_deep_links/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/tsconfig.json b/test/plugin_functional/plugins/core_plugin_deprecations/tsconfig.json index 3d9d8ca9451d4..eab76d901e427 100644 --- a/test/plugin_functional/plugins/core_plugin_deprecations/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_deprecations/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/plugin_functional/plugins/core_plugin_execution_context/tsconfig.json b/test/plugin_functional/plugins/core_plugin_execution_context/tsconfig.json index 21662b2b64a18..4c00a35a3db77 100644 --- a/test/plugin_functional/plugins/core_plugin_execution_context/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_execution_context/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/plugin_functional/plugins/core_plugin_helpmenu/tsconfig.json b/test/plugin_functional/plugins/core_plugin_helpmenu/tsconfig.json index f9b0443e0a8bf..d346449e67c42 100644 --- a/test/plugin_functional/plugins/core_plugin_helpmenu/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_helpmenu/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/plugin_functional/plugins/core_plugin_route_timeouts/tsconfig.json b/test/plugin_functional/plugins/core_plugin_route_timeouts/tsconfig.json index 72793a327d97e..b0e6d4f5d84ce 100644 --- a/test/plugin_functional/plugins/core_plugin_route_timeouts/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_route_timeouts/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "server/**/*.ts", diff --git a/test/plugin_functional/plugins/core_plugin_static_assets/tsconfig.json b/test/plugin_functional/plugins/core_plugin_static_assets/tsconfig.json index f9b0443e0a8bf..d346449e67c42 100644 --- a/test/plugin_functional/plugins/core_plugin_static_assets/tsconfig.json +++ b/test/plugin_functional/plugins/core_plugin_static_assets/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json b/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json index d0d1f2d99295a..d18f6a63263bf 100644 --- a/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/core_provider_plugin/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "composite": true, - "outDir": "./target", + "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, "declarationMap": true diff --git a/test/plugin_functional/plugins/data_search/tsconfig.json b/test/plugin_functional/plugins/data_search/tsconfig.json index 72793a327d97e..e82f3fca8fb3c 100644 --- a/test/plugin_functional/plugins/data_search/tsconfig.json +++ b/test/plugin_functional/plugins/data_search/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "server/**/*.ts", @@ -10,6 +9,7 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../../../src/plugins/data/tsconfig.json" }, ] } diff --git a/test/plugin_functional/plugins/doc_views_plugin/tsconfig.json b/test/plugin_functional/plugins/doc_views_plugin/tsconfig.json index f9b0443e0a8bf..45babe3228965 100644 --- a/test/plugin_functional/plugins/doc_views_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/doc_views_plugin/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -12,6 +11,7 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../../../src/plugins/discover/tsconfig.json" }, ] } diff --git a/test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json b/test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json index 72793a327d97e..b0e6d4f5d84ce 100644 --- a/test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/elasticsearch_client_plugin/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "server/**/*.ts", diff --git a/test/plugin_functional/plugins/index_patterns/tsconfig.json b/test/plugin_functional/plugins/index_patterns/tsconfig.json index 9d11a9850f151..9ac0d7726c379 100644 --- a/test/plugin_functional/plugins/index_patterns/tsconfig.json +++ b/test/plugin_functional/plugins/index_patterns/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -12,6 +11,7 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../../../src/plugins/data/tsconfig.json" }, ] } diff --git a/test/plugin_functional/plugins/kbn_sample_panel_action/tsconfig.json b/test/plugin_functional/plugins/kbn_sample_panel_action/tsconfig.json index f77a5eaffc301..043ace6ce064d 100644 --- a/test/plugin_functional/plugins/kbn_sample_panel_action/tsconfig.json +++ b/test/plugin_functional/plugins/kbn_sample_panel_action/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -12,6 +11,9 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../../../src/plugins/ui_actions/tsconfig.json" }, + { "path": "../../../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../../../src/plugins/kibana_react/tsconfig.json" }, ] } diff --git a/test/plugin_functional/plugins/kbn_top_nav/tsconfig.json b/test/plugin_functional/plugins/kbn_top_nav/tsconfig.json index 3d9d8ca9451d4..adf3815905d1d 100644 --- a/test/plugin_functional/plugins/kbn_top_nav/tsconfig.json +++ b/test/plugin_functional/plugins/kbn_top_nav/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -13,6 +12,7 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../../../src/plugins/navigation/tsconfig.json" }, ] } diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json index e92dc717ae25e..8cbb8696409b6 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json @@ -1,14 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true, - "types": [ - "node", - "jest", - "react", - "@emotion/react/types/css-prop" - ] + "outDir": "./target/types" }, "include": [ "index.ts", @@ -18,6 +11,9 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../../src/plugins/visualizations/tsconfig.json" }, + { "path": "../../../../src/plugins/expressions/tsconfig.json" }, ] } diff --git a/test/plugin_functional/plugins/management_test_plugin/tsconfig.json b/test/plugin_functional/plugins/management_test_plugin/tsconfig.json index f77a5eaffc301..8222b8f011005 100644 --- a/test/plugin_functional/plugins/management_test_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/management_test_plugin/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -12,6 +11,7 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../../../src/plugins/management/tsconfig.json" }, ] } diff --git a/test/plugin_functional/plugins/rendering_plugin/tsconfig.json b/test/plugin_functional/plugins/rendering_plugin/tsconfig.json index 3d9d8ca9451d4..eab76d901e427 100644 --- a/test/plugin_functional/plugins/rendering_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/rendering_plugin/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/plugin_functional/plugins/saved_object_export_transforms/tsconfig.json b/test/plugin_functional/plugins/saved_object_export_transforms/tsconfig.json index da457c9ba32fc..f148b232e21fb 100644 --- a/test/plugin_functional/plugins/saved_object_export_transforms/tsconfig.json +++ b/test/plugin_functional/plugins/saved_object_export_transforms/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/plugin_functional/plugins/saved_object_import_warnings/tsconfig.json b/test/plugin_functional/plugins/saved_object_import_warnings/tsconfig.json index 3d9d8ca9451d4..eab76d901e427 100644 --- a/test/plugin_functional/plugins/saved_object_import_warnings/tsconfig.json +++ b/test/plugin_functional/plugins/saved_object_import_warnings/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/plugin_functional/plugins/saved_objects_hidden_type/tsconfig.json b/test/plugin_functional/plugins/saved_objects_hidden_type/tsconfig.json index da457c9ba32fc..f148b232e21fb 100644 --- a/test/plugin_functional/plugins/saved_objects_hidden_type/tsconfig.json +++ b/test/plugin_functional/plugins/saved_objects_hidden_type/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/plugin_functional/plugins/session_notifications/tsconfig.json b/test/plugin_functional/plugins/session_notifications/tsconfig.json index 3d9d8ca9451d4..c50f2e3f119c8 100644 --- a/test/plugin_functional/plugins/session_notifications/tsconfig.json +++ b/test/plugin_functional/plugins/session_notifications/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -13,6 +12,8 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../../../src/plugins/navigation/tsconfig.json" }, + { "path": "../../../../src/plugins/data/tsconfig.json" }, ] } diff --git a/test/plugin_functional/plugins/telemetry/tsconfig.json b/test/plugin_functional/plugins/telemetry/tsconfig.json index 947eb49a61b3a..b32ac67279f40 100644 --- a/test/plugin_functional/plugins/telemetry/tsconfig.json +++ b/test/plugin_functional/plugins/telemetry/tsconfig.json @@ -1,10 +1,12 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": ["public/**/*.ts", "types.ts", "../../../../typings/**/*"], "exclude": [], - "references": [{ "path": "../../../../src/core/tsconfig.json" }] + "references": [ + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../../../src/plugins/telemetry/tsconfig.json" }, + ] } diff --git a/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json b/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json index 05b1c2c11da03..6551c1496164f 100644 --- a/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json +++ b/test/plugin_functional/plugins/ui_settings_plugin/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types", }, "include": [ "server/**/*.ts", diff --git a/test/plugin_functional/plugins/usage_collection/tsconfig.json b/test/plugin_functional/plugins/usage_collection/tsconfig.json index 3d9d8ca9451d4..d6f7d08f18589 100644 --- a/test/plugin_functional/plugins/usage_collection/tsconfig.json +++ b/test/plugin_functional/plugins/usage_collection/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -13,6 +12,7 @@ ], "exclude": [], "references": [ - { "path": "../../../../src/core/tsconfig.json" } + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../../../src/plugins/usage_collection/tsconfig.json" }, ] } diff --git a/test/scripts/checks/type_check_plugin_public_api_docs.sh b/test/scripts/checks/type_check_plugin_public_api_docs.sh index 8acacb696c576..77fa76038f7c4 100755 --- a/test/scripts/checks/type_check_plugin_public_api_docs.sh +++ b/test/scripts/checks/type_check_plugin_public_api_docs.sh @@ -4,7 +4,6 @@ source src/dev/ci_setup/setup_env.sh checks-reporter-with-killswitch "Build TS Refs" \ node scripts/build_ts_refs \ - --ignore-type-failures \ --clean \ --no-cache \ --force diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_a/tsconfig.json b/test/server_integration/__fixtures__/plugins/status_plugin_a/tsconfig.json index 5069db62589c7..14ebb2e7d00c4 100644 --- a/test/server_integration/__fixtures__/plugins/status_plugin_a/tsconfig.json +++ b/test/server_integration/__fixtures__/plugins/status_plugin_a/tsconfig.json @@ -1,9 +1,7 @@ { "extends": "../../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true, - "composite": true + "outDir": "./target/types", }, "include": [ "index.ts", diff --git a/test/server_integration/__fixtures__/plugins/status_plugin_b/tsconfig.json b/test/server_integration/__fixtures__/plugins/status_plugin_b/tsconfig.json index 224aa42ef68d2..6b27e9b4b7a6c 100644 --- a/test/server_integration/__fixtures__/plugins/status_plugin_b/tsconfig.json +++ b/test/server_integration/__fixtures__/plugins/status_plugin_b/tsconfig.json @@ -1,9 +1,7 @@ { "extends": "../../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true, - "composite": true + "outDir": "./target/types" }, "include": [ "index.ts", diff --git a/test/tsconfig.json b/test/tsconfig.json index dccbe8d715c51..c94d4445dd246 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/tsconfig.base.json b/tsconfig.base.json index 0c8fec7c88cda..9de81f68110c1 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -21,6 +21,8 @@ "jsx": "react", // Enables all strict type checking options. "strict": true, + // All TS projects should be composite and only include the files they select, and ref the files outside of the project + "composite": true, // save information about the project graph on disk "incremental": true, // Do not check d.ts files by default diff --git a/tsconfig.bazel.json b/tsconfig.bazel.json new file mode 100644 index 0000000000000..892c727ef588e --- /dev/null +++ b/tsconfig.bazel.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "incremental": false, + "composite": false + } +} diff --git a/tsconfig.browser_bazel.json b/tsconfig.browser_bazel.json new file mode 100644 index 0000000000000..3a950a756dd08 --- /dev/null +++ b/tsconfig.browser_bazel.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.browser.json", + "compilerOptions": { + "incremental": false, + "composite": false + } +} diff --git a/tsconfig.json b/tsconfig.json index 3d6c29875c902..769dd5ecb6a55 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.base.json", "compilerOptions": { - "incremental": false + "outDir": "target/root_types" }, "include": [ "kibana.d.ts", @@ -16,122 +16,7 @@ "exclude": [], "references": [ { "path": "./src/core/tsconfig.json" }, - { "path": "./src/plugins/telemetry_management_section/tsconfig.json" }, - { "path": "./src/plugins/advanced_settings/tsconfig.json" }, - { "path": "./src/plugins/apm_oss/tsconfig.json" }, - { "path": "./src/plugins/bfetch/tsconfig.json" }, - { "path": "./src/plugins/charts/tsconfig.json" }, - { "path": "./src/plugins/console/tsconfig.json" }, - { "path": "./src/plugins/dashboard/tsconfig.json" }, - { "path": "./src/plugins/discover/tsconfig.json" }, - { "path": "./src/plugins/data/tsconfig.json" }, - { "path": "./src/plugins/dev_tools/tsconfig.json" }, - { "path": "./src/plugins/embeddable/tsconfig.json" }, - { "path": "./src/plugins/es_ui_shared/tsconfig.json" }, - { "path": "./src/plugins/expressions/tsconfig.json" }, - { "path": "./src/plugins/home/tsconfig.json" }, - { "path": "./src/plugins/inspector/tsconfig.json" }, - { "path": "./src/plugins/interactive_setup/tsconfig.json" }, - { "path": "./src/plugins/kibana_legacy/tsconfig.json" }, - { "path": "./src/plugins/kibana_overview/tsconfig.json" }, - { "path": "./src/plugins/kibana_react/tsconfig.json" }, - { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, - { "path": "./src/plugins/kibana_utils/tsconfig.json" }, - { "path": "./src/plugins/legacy_export/tsconfig.json" }, - { "path": "./src/plugins/management/tsconfig.json" }, - { "path": "./src/plugins/maps_legacy/tsconfig.json" }, - { "path": "./src/plugins/navigation/tsconfig.json" }, - { "path": "./src/plugins/newsfeed/tsconfig.json" }, - { "path": "./src/plugins/region_map/tsconfig.json" }, - { "path": "./src/plugins/saved_objects/tsconfig.json" }, - { "path": "./src/plugins/saved_objects_management/tsconfig.json" }, - { "path": "./src/plugins/saved_objects_tagging_oss/tsconfig.json" }, - { "path": "./src/plugins/security_oss/tsconfig.json" }, - { "path": "./src/plugins/share/tsconfig.json" }, - { "path": "./src/plugins/spaces_oss/tsconfig.json" }, - { "path": "./src/plugins/telemetry/tsconfig.json" }, - { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, - { "path": "./src/plugins/tile_map/tsconfig.json" }, - { "path": "./src/plugins/timelion/tsconfig.json" }, - { "path": "./src/plugins/ui_actions/tsconfig.json" }, - { "path": "./src/plugins/url_forwarding/tsconfig.json" }, { "path": "./src/plugins/usage_collection/tsconfig.json" }, - { "path": "./src/plugins/vis_default_editor/tsconfig.json" }, - { "path": "./src/plugins/vis_type_markdown/tsconfig.json" }, - { "path": "./src/plugins/vis_type_metric/tsconfig.json" }, - { "path": "./src/plugins/vis_type_table/tsconfig.json" }, - { "path": "./src/plugins/vis_type_tagcloud/tsconfig.json" }, - { "path": "./src/plugins/vis_type_timelion/tsconfig.json" }, - { "path": "./src/plugins/vis_type_timeseries/tsconfig.json" }, - { "path": "./src/plugins/vis_type_vislib/tsconfig.json" }, - { "path": "./src/plugins/vis_type_vega/tsconfig.json" }, - { "path": "./src/plugins/vis_type_xy/tsconfig.json" }, - { "path": "./src/plugins/vis_type_pie/tsconfig.json" }, - { "path": "./src/plugins/visualizations/tsconfig.json" }, - { "path": "./src/plugins/visualize/tsconfig.json" }, - { "path": "./src/plugins/index_pattern_management/tsconfig.json" }, - { "path": "./src/plugins/index_pattern_field_editor/tsconfig.json" }, - { "path": "./src/plugins/index_pattern_editor/tsconfig.json" }, - { "path": "./x-pack/plugins/actions/tsconfig.json" }, - { "path": "./x-pack/plugins/alerting/tsconfig.json" }, - { "path": "./x-pack/plugins/apm/tsconfig.json" }, - { "path": "./x-pack/plugins/canvas/tsconfig.json" }, - { "path": "./x-pack/plugins/cases/tsconfig.json" }, - { "path": "./x-pack/plugins/cloud/tsconfig.json" }, - { "path": "./x-pack/plugins/data_enhanced/tsconfig.json" }, - { "path": "./x-pack/plugins/dashboard_mode/tsconfig.json" }, - { "path": "./x-pack/plugins/discover_enhanced/tsconfig.json" }, - { "path": "./x-pack/plugins/drilldowns/url_drilldown/tsconfig.json" }, - { "path": "./x-pack/plugins/embeddable_enhanced/tsconfig.json" }, - { "path": "./x-pack/plugins/encrypted_saved_objects/tsconfig.json" }, - { "path": "./x-pack/plugins/enterprise_search/tsconfig.json" }, - { "path": "./x-pack/plugins/event_log/tsconfig.json" }, - { "path": "./x-pack/plugins/features/tsconfig.json" }, - { "path": "./x-pack/plugins/file_upload/tsconfig.json" }, - { "path": "./x-pack/plugins/fleet/tsconfig.json" }, - { "path": "./x-pack/plugins/global_search_bar/tsconfig.json" }, - { "path": "./x-pack/plugins/global_search_providers/tsconfig.json" }, - { "path": "./x-pack/plugins/global_search/tsconfig.json" }, - { "path": "./x-pack/plugins/graph/tsconfig.json" }, - { "path": "./x-pack/plugins/grokdebugger/tsconfig.json" }, - { "path": "./x-pack/plugins/infra/tsconfig.json" }, - { "path": "./x-pack/plugins/ingest_pipelines/tsconfig.json" }, - { "path": "./x-pack/plugins/lens/tsconfig.json" }, - { "path": "./x-pack/plugins/license_api_guard/tsconfig.json" }, - { "path": "./x-pack/plugins/license_management/tsconfig.json" }, - { "path": "./x-pack/plugins/licensing/tsconfig.json" }, - { "path": "./x-pack/plugins/lists/tsconfig.json" }, - { "path": "./x-pack/plugins/logstash/tsconfig.json" }, - { "path": "./x-pack/plugins/maps/tsconfig.json" }, - { "path": "./x-pack/plugins/metrics_entities/tsconfig.json" }, - { "path": "./x-pack/plugins/ml/tsconfig.json" }, - { "path": "./x-pack/plugins/monitoring/tsconfig.json" }, - { "path": "./x-pack/plugins/observability/tsconfig.json" }, - { "path": "./x-pack/plugins/osquery/tsconfig.json" }, - { "path": "./x-pack/plugins/painless_lab/tsconfig.json" }, - { "path": "./x-pack/plugins/saved_objects_tagging/tsconfig.json" }, - { "path": "./x-pack/plugins/searchprofiler/tsconfig.json" }, - { "path": "./x-pack/plugins/security/tsconfig.json" }, - { "path": "./x-pack/plugins/security_solution/tsconfig.json" }, - { "path": "./x-pack/plugins/snapshot_restore/tsconfig.json" }, - { "path": "./x-pack/plugins/spaces/tsconfig.json" }, - { "path": "./x-pack/plugins/stack_alerts/tsconfig.json" }, - { "path": "./x-pack/plugins/task_manager/tsconfig.json" }, - { "path": "./x-pack/plugins/telemetry_collection_xpack/tsconfig.json" }, - { "path": "./x-pack/plugins/timelines/tsconfig.json" }, - { "path": "./x-pack/plugins/transform/tsconfig.json" }, - { "path": "./x-pack/plugins/translations/tsconfig.json" }, - { "path": "./x-pack/plugins/triggers_actions_ui/tsconfig.json" }, - { "path": "./x-pack/plugins/ui_actions_enhanced/tsconfig.json" }, - { "path": "./x-pack/plugins/upgrade_assistant/tsconfig.json" }, - { "path": "./x-pack/plugins/runtime_fields/tsconfig.json" }, - { "path": "./x-pack/plugins/index_management/tsconfig.json" }, - { "path": "./x-pack/plugins/watcher/tsconfig.json" }, - { "path": "./x-pack/plugins/rollup/tsconfig.json" }, - { "path": "./x-pack/plugins/remote_clusters/tsconfig.json" }, - { "path": "./x-pack/plugins/cross_cluster_replication/tsconfig.json"}, - { "path": "./x-pack/plugins/index_lifecycle_management/tsconfig.json"}, - { "path": "./x-pack/plugins/uptime/tsconfig.json" }, - { "path": "./x-pack/plugins/xpack_legacy/tsconfig.json" } + { "path": "./x-pack/plugins/reporting/tsconfig.json" }, ] } diff --git a/tsconfig.refs.json b/tsconfig.refs.json deleted file mode 100644 index 1807a7014e389..0000000000000 --- a/tsconfig.refs.json +++ /dev/null @@ -1,126 +0,0 @@ -{ - "include": [], - "references": [ - { "path": "./src/core/tsconfig.json" }, - { "path": "./src/plugins/advanced_settings/tsconfig.json" }, - { "path": "./src/plugins/apm_oss/tsconfig.json" }, - { "path": "./src/plugins/bfetch/tsconfig.json" }, - { "path": "./src/plugins/charts/tsconfig.json" }, - { "path": "./src/plugins/console/tsconfig.json" }, - { "path": "./src/plugins/dashboard/tsconfig.json" }, - { "path": "./src/plugins/data/tsconfig.json" }, - { "path": "./src/plugins/dev_tools/tsconfig.json" }, - { "path": "./src/plugins/discover/tsconfig.json" }, - { "path": "./src/plugins/embeddable/tsconfig.json" }, - { "path": "./src/plugins/es_ui_shared/tsconfig.json" }, - { "path": "./src/plugins/expressions/tsconfig.json" }, - { "path": "./src/plugins/home/tsconfig.json" }, - { "path": "./src/plugins/inspector/tsconfig.json" }, - { "path": "./src/plugins/interactive_setup/tsconfig.json" }, - { "path": "./src/plugins/kibana_legacy/tsconfig.json" }, - { "path": "./src/plugins/kibana_overview/tsconfig.json" }, - { "path": "./src/plugins/kibana_react/tsconfig.json" }, - { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, - { "path": "./src/plugins/kibana_utils/tsconfig.json" }, - { "path": "./src/plugins/legacy_export/tsconfig.json" }, - { "path": "./src/plugins/management/tsconfig.json" }, - { "path": "./src/plugins/maps_legacy/tsconfig.json" }, - { "path": "./src/plugins/navigation/tsconfig.json" }, - { "path": "./src/plugins/newsfeed/tsconfig.json" }, - { "path": "./src/plugins/presentation_util/tsconfig.json" }, - { "path": "./src/plugins/region_map/tsconfig.json" }, - { "path": "./src/plugins/saved_objects_management/tsconfig.json" }, - { "path": "./src/plugins/saved_objects_tagging_oss/tsconfig.json" }, - { "path": "./src/plugins/saved_objects/tsconfig.json" }, - { "path": "./src/plugins/security_oss/tsconfig.json" }, - { "path": "./src/plugins/share/tsconfig.json" }, - { "path": "./src/plugins/spaces_oss/tsconfig.json" }, - { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, - { "path": "./src/plugins/telemetry_management_section/tsconfig.json" }, - { "path": "./src/plugins/telemetry/tsconfig.json" }, - { "path": "./src/plugins/tile_map/tsconfig.json" }, - { "path": "./src/plugins/timelion/tsconfig.json" }, - { "path": "./src/plugins/ui_actions/tsconfig.json" }, - { "path": "./src/plugins/url_forwarding/tsconfig.json" }, - { "path": "./src/plugins/usage_collection/tsconfig.json" }, - { "path": "./src/plugins/vis_default_editor/tsconfig.json" }, - { "path": "./src/plugins/vis_type_markdown/tsconfig.json" }, - { "path": "./src/plugins/vis_type_metric/tsconfig.json" }, - { "path": "./src/plugins/vis_type_table/tsconfig.json" }, - { "path": "./src/plugins/vis_type_tagcloud/tsconfig.json" }, - { "path": "./src/plugins/vis_type_timelion/tsconfig.json" }, - { "path": "./src/plugins/vis_type_timeseries/tsconfig.json" }, - { "path": "./src/plugins/vis_type_vislib/tsconfig.json" }, - { "path": "./src/plugins/vis_type_vega/tsconfig.json" }, - { "path": "./src/plugins/vis_type_xy/tsconfig.json" }, - { "path": "./src/plugins/vis_type_pie/tsconfig.json" }, - { "path": "./src/plugins/visualizations/tsconfig.json" }, - { "path": "./src/plugins/visualize/tsconfig.json" }, - { "path": "./src/plugins/index_pattern_management/tsconfig.json" }, - { "path": "./src/plugins/index_pattern_editor/tsconfig.json" }, - { "path": "./test/tsconfig.json" }, - { "path": "./x-pack/plugins/actions/tsconfig.json" }, - { "path": "./x-pack/plugins/alerting/tsconfig.json" }, - { "path": "./x-pack/plugins/apm/tsconfig.json" }, - { "path": "./x-pack/plugins/canvas/tsconfig.json" }, - { "path": "./x-pack/plugins/cases/tsconfig.json" }, - { "path": "./x-pack/plugins/cloud/tsconfig.json" }, - { "path": "./x-pack/plugins/dashboard_enhanced/tsconfig.json" }, - { "path": "./x-pack/plugins/data_enhanced/tsconfig.json" }, - { "path": "./x-pack/plugins/dashboard_mode/tsconfig.json" }, - { "path": "./x-pack/plugins/discover_enhanced/tsconfig.json" }, - { "path": "./x-pack/plugins/drilldowns/url_drilldown/tsconfig.json" }, - { "path": "./x-pack/plugins/embeddable_enhanced/tsconfig.json" }, - { "path": "./x-pack/plugins/encrypted_saved_objects/tsconfig.json" }, - { "path": "./x-pack/plugins/enterprise_search/tsconfig.json" }, - { "path": "./x-pack/plugins/event_log/tsconfig.json" }, - { "path": "./x-pack/plugins/features/tsconfig.json" }, - { "path": "./x-pack/plugins/file_upload/tsconfig.json" }, - { "path": "./x-pack/plugins/fleet/tsconfig.json" }, - { "path": "./x-pack/plugins/global_search_bar/tsconfig.json" }, - { "path": "./x-pack/plugins/global_search_providers/tsconfig.json" }, - { "path": "./x-pack/plugins/global_search/tsconfig.json" }, - { "path": "./x-pack/plugins/graph/tsconfig.json" }, - { "path": "./x-pack/plugins/grokdebugger/tsconfig.json" }, - { "path": "./x-pack/plugins/infra/tsconfig.json" }, - { "path": "./x-pack/plugins/ingest_pipelines/tsconfig.json" }, - { "path": "./x-pack/plugins/lens/tsconfig.json" }, - { "path": "./x-pack/plugins/license_management/tsconfig.json" }, - { "path": "./x-pack/plugins/licensing/tsconfig.json" }, - { "path": "./x-pack/plugins/lists/tsconfig.json" }, - { "path": "./x-pack/plugins/logstash/tsconfig.json" }, - { "path": "./x-pack/plugins/maps/tsconfig.json" }, - { "path": "./x-pack/plugins/metrics_entities/tsconfig.json" }, - { "path": "./x-pack/plugins/ml/tsconfig.json" }, - { "path": "./x-pack/plugins/monitoring/tsconfig.json" }, - { "path": "./x-pack/plugins/observability/tsconfig.json" }, - { "path": "./x-pack/plugins/osquery/tsconfig.json" }, - { "path": "./x-pack/plugins/painless_lab/tsconfig.json" }, - { "path": "./x-pack/plugins/reporting/tsconfig.json" }, - { "path": "./x-pack/plugins/saved_objects_tagging/tsconfig.json" }, - { "path": "./x-pack/plugins/searchprofiler/tsconfig.json" }, - { "path": "./x-pack/plugins/security/tsconfig.json" }, - { "path": "./x-pack/plugins/security_solution/tsconfig.json" }, - { "path": "./x-pack/plugins/snapshot_restore/tsconfig.json" }, - { "path": "./x-pack/plugins/spaces/tsconfig.json" }, - { "path": "./x-pack/plugins/stack_alerts/tsconfig.json" }, - { "path": "./x-pack/plugins/task_manager/tsconfig.json" }, - { "path": "./x-pack/plugins/telemetry_collection_xpack/tsconfig.json" }, - { "path": "./x-pack/plugins/timelines/tsconfig.json" }, - { "path": "./x-pack/plugins/transform/tsconfig.json" }, - { "path": "./x-pack/plugins/translations/tsconfig.json" }, - { "path": "./x-pack/plugins/triggers_actions_ui/tsconfig.json" }, - { "path": "./x-pack/plugins/ui_actions_enhanced/tsconfig.json" }, - { "path": "./x-pack/plugins/upgrade_assistant/tsconfig.json" }, - { "path": "./x-pack/plugins/runtime_fields/tsconfig.json" }, - { "path": "./x-pack/plugins/index_management/tsconfig.json" }, - { "path": "./x-pack/plugins/watcher/tsconfig.json" }, - { "path": "./x-pack/plugins/rollup/tsconfig.json" }, - { "path": "./x-pack/plugins/remote_clusters/tsconfig.json" }, - { "path": "./x-pack/plugins/cross_cluster_replication/tsconfig.json" }, - { "path": "./x-pack/plugins/index_lifecycle_management/tsconfig.json" }, - { "path": "./x-pack/plugins/uptime/tsconfig.json" }, - { "path": "./x-pack/plugins/xpack_legacy/tsconfig.json" }, - { "path": "./x-pack/test/tsconfig.json" }, - ] -} diff --git a/tsconfig.types.json b/tsconfig.types.json index 86a45f6db1697..54a26b6aca404 100644 --- a/tsconfig.types.json +++ b/tsconfig.types.json @@ -1,12 +1,12 @@ { "extends": "./tsconfig.base.json", "compilerOptions": { - "incremental": false, - "declaration": true, + "composite": false, "outDir": "./target/types", "stripInternal": false, + "declaration": true, "emitDeclarationOnly": true, - "declarationMap": true + "declarationMap": true, }, "include": [ "src/core/server/index.ts", diff --git a/x-pack/examples/alerting_example/tsconfig.json b/x-pack/examples/alerting_example/tsconfig.json index 95d42d40aceb3..881c48029031d 100644 --- a/x-pack/examples/alerting_example/tsconfig.json +++ b/x-pack/examples/alerting_example/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target" + "outDir": "./target/types" }, "include": [ "index.ts", @@ -13,6 +13,13 @@ ], "exclude": [], "references": [ - { "path": "../../../src/core/tsconfig.json" } + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/charts/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../plugins/alerting/tsconfig.json" }, + { "path": "../../plugins/triggers_actions_ui/tsconfig.json" }, + { "path": "../../plugins/features/tsconfig.json" }, + { "path": "../../../examples/developer_examples/tsconfig.json" }, ] } diff --git a/x-pack/examples/embedded_lens_example/tsconfig.json b/x-pack/examples/embedded_lens_example/tsconfig.json index 195db6effc5e6..e1016a6c011a1 100644 --- a/x-pack/examples/embedded_lens_example/tsconfig.json +++ b/x-pack/examples/embedded_lens_example/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -14,9 +13,9 @@ "exclude": [], "references": [ { "path": "../../../src/core/tsconfig.json" }, - { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, - { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, - { "path": "../../../src/plugins/share/tsconfig.json" }, - { "path": "../../../src/plugins/data/tsconfig.json" } + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../plugins/lens/tsconfig.json" }, + { "path": "../../../examples/developer_examples/tsconfig.json" }, ] } diff --git a/x-pack/examples/reporting_example/tsconfig.json b/x-pack/examples/reporting_example/tsconfig.json index ef727b3368b12..4c4016911e0c5 100644 --- a/x-pack/examples/reporting_example/tsconfig.json +++ b/x-pack/examples/reporting_example/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target" + "outDir": "./target/types" }, "include": [ "index.ts", @@ -13,7 +13,11 @@ ], "exclude": [], "references": [ - { "path": "../../../src/core/tsconfig.json" } + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/navigation/tsconfig.json" }, + { "path": "../../../src/plugins/screenshot_mode/tsconfig.json" }, + { "path": "../../../examples/developer_examples/tsconfig.json" }, + { "path": "../../plugins/reporting/tsconfig.json" }, ] } diff --git a/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json b/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json index 567baca039d76..0282ecbdd173a 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json +++ b/x-pack/examples/ui_actions_enhanced_examples/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "index.ts", @@ -16,6 +15,12 @@ { "path": "../../../src/core/tsconfig.json" }, { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, - { "path": "../../../src/plugins/share/tsconfig.json" } + { "path": "../../../src/plugins/share/tsconfig.json" }, + { "path": "../../../src/plugins/discover/tsconfig.json" }, + { "path": "../../../src/plugins/dashboard/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../../examples/developer_examples/tsconfig.json" }, + { "path": "../../plugins/dashboard_enhanced/tsconfig.json" }, + { "path": "../../plugins/ui_actions_enhanced/tsconfig.json" }, ] } diff --git a/x-pack/plugins/actions/tsconfig.json b/x-pack/plugins/actions/tsconfig.json index d5c1105c99ad0..b2526d84a3ce4 100644 --- a/x-pack/plugins/actions/tsconfig.json +++ b/x-pack/plugins/actions/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/alerting/tsconfig.json b/x-pack/plugins/alerting/tsconfig.json index 86ab00faeb5ad..a822bd776134b 100644 --- a/x-pack/plugins/alerting/tsconfig.json +++ b/x-pack/plugins/alerting/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/apm/e2e/tsconfig.json b/x-pack/plugins/apm/e2e/tsconfig.json index c4587349c7ad7..2560a15df9224 100644 --- a/x-pack/plugins/apm/e2e/tsconfig.json +++ b/x-pack/plugins/apm/e2e/tsconfig.json @@ -1,8 +1,9 @@ { "extends": "../../../../tsconfig.base.json", - "exclude": ["tmp"], - "include": ["./**/*"], + "include": ["**/*"], + "exclude": ["tmp", "target/**/*"], "compilerOptions": { + "outDir": "target/types", "types": ["cypress", "node"] } } diff --git a/x-pack/plugins/apm/ftr_e2e/config.ts b/x-pack/plugins/apm/ftr_e2e/config.ts index 38c196fbe70c2..4cb218fd24755 100644 --- a/x-pack/plugins/apm/ftr_e2e/config.ts +++ b/x-pack/plugins/apm/ftr_e2e/config.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { resolve } from 'path'; - import { FtrConfigProviderContext } from '@kbn/test'; import { CA_CERT_PATH } from '@kbn/dev-utils'; diff --git a/x-pack/plugins/apm/ftr_e2e/tsconfig.json b/x-pack/plugins/apm/ftr_e2e/tsconfig.json index 94b2b024ca4cc..84a66afe4588d 100644 --- a/x-pack/plugins/apm/ftr_e2e/tsconfig.json +++ b/x-pack/plugins/apm/ftr_e2e/tsconfig.json @@ -1,16 +1,22 @@ { "extends": "../../../../tsconfig.base.json", - "exclude": [ - "tmp" - ], "include": [ - "./**/*" + "**/*" + ], + "exclude": [ + "tmp", + "target/**/*" ], "compilerOptions": { + "outDir": "target/types", "types": [ "cypress", "node", "cypress-real-events" ] - } -} \ No newline at end of file + }, + "references": [ + { "path": "../../../test/tsconfig.json" }, + { "path": "../../../../test/tsconfig.json" }, + ] +} diff --git a/x-pack/plugins/apm/tsconfig.json b/x-pack/plugins/apm/tsconfig.json index 192b7f4fe8c2e..6eaf1a3bf1833 100644 --- a/x-pack/plugins/apm/tsconfig.json +++ b/x-pack/plugins/apm/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/banners/tsconfig.json b/x-pack/plugins/banners/tsconfig.json index 85608a8a78ad5..48767fb4525f5 100644 --- a/x-pack/plugins/banners/tsconfig.json +++ b/x-pack/plugins/banners/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/canvas/tsconfig.json b/x-pack/plugins/canvas/tsconfig.json index 384a7716657f5..87fabe2730c16 100644 --- a/x-pack/plugins/canvas/tsconfig.json +++ b/x-pack/plugins/canvas/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/cases/tsconfig.json b/x-pack/plugins/cases/tsconfig.json index 493fe6430efa7..99622df805ced 100644 --- a/x-pack/plugins/cases/tsconfig.json +++ b/x-pack/plugins/cases/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/cloud/tsconfig.json b/x-pack/plugins/cloud/tsconfig.json index 46e81aa7fa086..d1ff8c63e84cb 100644 --- a/x-pack/plugins/cloud/tsconfig.json +++ b/x-pack/plugins/cloud/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/cross_cluster_replication/tsconfig.json b/x-pack/plugins/cross_cluster_replication/tsconfig.json index e0923553beadc..4c1f3b20fa23e 100644 --- a/x-pack/plugins/cross_cluster_replication/tsconfig.json +++ b/x-pack/plugins/cross_cluster_replication/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/dashboard_enhanced/tsconfig.json b/x-pack/plugins/dashboard_enhanced/tsconfig.json index 567c390edfa5a..cebf306003aba 100644 --- a/x-pack/plugins/dashboard_enhanced/tsconfig.json +++ b/x-pack/plugins/dashboard_enhanced/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/dashboard_mode/tsconfig.json b/x-pack/plugins/dashboard_mode/tsconfig.json index 6e4ed11ffa7ff..8094e70e96b60 100644 --- a/x-pack/plugins/dashboard_mode/tsconfig.json +++ b/x-pack/plugins/dashboard_mode/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/data_enhanced/tsconfig.json b/x-pack/plugins/data_enhanced/tsconfig.json index 047b9b06516ba..544b50c21224f 100644 --- a/x-pack/plugins/data_enhanced/tsconfig.json +++ b/x-pack/plugins/data_enhanced/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/data_visualizer/tsconfig.json b/x-pack/plugins/data_visualizer/tsconfig.json index 7c60002c3c630..ee5f894305d5a 100644 --- a/x-pack/plugins/data_visualizer/tsconfig.json +++ b/x-pack/plugins/data_visualizer/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/discover_enhanced/tsconfig.json b/x-pack/plugins/discover_enhanced/tsconfig.json index 38a55e557909b..f372b7a7539ac 100644 --- a/x-pack/plugins/discover_enhanced/tsconfig.json +++ b/x-pack/plugins/discover_enhanced/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json b/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json index 50fe41c49b0c8..85a215254bb77 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json +++ b/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/embeddable_enhanced/tsconfig.json b/x-pack/plugins/embeddable_enhanced/tsconfig.json index 6e9eb69585cbc..f9ac369587473 100644 --- a/x-pack/plugins/embeddable_enhanced/tsconfig.json +++ b/x-pack/plugins/embeddable_enhanced/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/encrypted_saved_objects/tsconfig.json b/x-pack/plugins/encrypted_saved_objects/tsconfig.json index 2b51b313d34fc..4b06756a9cf2f 100644 --- a/x-pack/plugins/encrypted_saved_objects/tsconfig.json +++ b/x-pack/plugins/encrypted_saved_objects/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/enterprise_search/tsconfig.json b/x-pack/plugins/enterprise_search/tsconfig.json index 976cdfadca4b7..481c4527d5977 100644 --- a/x-pack/plugins/enterprise_search/tsconfig.json +++ b/x-pack/plugins/enterprise_search/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/event_log/tsconfig.json b/x-pack/plugins/event_log/tsconfig.json index 9b7cde10da3d6..e0e72fdbf6581 100644 --- a/x-pack/plugins/event_log/tsconfig.json +++ b/x-pack/plugins/event_log/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/features/tsconfig.json b/x-pack/plugins/features/tsconfig.json index 1260af55fbff6..b16d7b47bba5b 100644 --- a/x-pack/plugins/features/tsconfig.json +++ b/x-pack/plugins/features/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/file_upload/tsconfig.json b/x-pack/plugins/file_upload/tsconfig.json index 3e146d76fbb90..efea61e38b3e8 100644 --- a/x-pack/plugins/file_upload/tsconfig.json +++ b/x-pack/plugins/file_upload/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/fleet/tsconfig.json b/x-pack/plugins/fleet/tsconfig.json index a20d82de3c859..5002bf2893872 100644 --- a/x-pack/plugins/fleet/tsconfig.json +++ b/x-pack/plugins/fleet/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/global_search/tsconfig.json b/x-pack/plugins/global_search/tsconfig.json index 2d05328f445df..6a0385e5c080b 100644 --- a/x-pack/plugins/global_search/tsconfig.json +++ b/x-pack/plugins/global_search/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/global_search_bar/tsconfig.json b/x-pack/plugins/global_search_bar/tsconfig.json index 266eecc35c84b..04464a3c08200 100644 --- a/x-pack/plugins/global_search_bar/tsconfig.json +++ b/x-pack/plugins/global_search_bar/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/global_search_providers/tsconfig.json b/x-pack/plugins/global_search_providers/tsconfig.json index f2759954a6845..4ce15f6d44683 100644 --- a/x-pack/plugins/global_search_providers/tsconfig.json +++ b/x-pack/plugins/global_search_providers/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/graph/tsconfig.json b/x-pack/plugins/graph/tsconfig.json index 741c603e3aae4..d655f28c4e46e 100644 --- a/x-pack/plugins/graph/tsconfig.json +++ b/x-pack/plugins/graph/tsconfig.json @@ -1,30 +1,29 @@ { - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "composite": true, - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": [ - "*.ts", - "common/**/*", - "public/**/*", - "server/**/*", - "../../../typings/**/*", - ], - "references": [ - { "path": "../../../src/core/tsconfig.json" }, - { "path": "../licensing/tsconfig.json" }, - { "path": "../features/tsconfig.json"}, - { "path": "../../../src/plugins/data/tsconfig.json"}, - { "path": "../../../src/plugins/navigation/tsconfig.json" }, - { "path": "../../../src/plugins/saved_objects/tsconfig.json"}, - { "path": "../../../src/plugins/kibana_legacy/tsconfig.json"}, - { "path": "../../../src/plugins/home/tsconfig.json"}, - { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, - { "path": "../../../src/plugins/kibana_react/tsconfig.json" } - ] - } \ No newline at end of file + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "*.ts", + "common/**/*", + "public/**/*", + "server/**/*", + "../../../typings/**/*", + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../licensing/tsconfig.json" }, + { "path": "../features/tsconfig.json"}, + { "path": "../../../src/plugins/data/tsconfig.json"}, + { "path": "../../../src/plugins/navigation/tsconfig.json" }, + { "path": "../../../src/plugins/saved_objects/tsconfig.json"}, + { "path": "../../../src/plugins/kibana_legacy/tsconfig.json"}, + { "path": "../../../src/plugins/home/tsconfig.json"}, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" } + ] +} diff --git a/x-pack/plugins/grokdebugger/tsconfig.json b/x-pack/plugins/grokdebugger/tsconfig.json index 51d2d0b6db0ea..aefb15f74c7b6 100644 --- a/x-pack/plugins/grokdebugger/tsconfig.json +++ b/x-pack/plugins/grokdebugger/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/index_lifecycle_management/tsconfig.json b/x-pack/plugins/index_lifecycle_management/tsconfig.json index 75bd775a36749..d3a342e110211 100644 --- a/x-pack/plugins/index_lifecycle_management/tsconfig.json +++ b/x-pack/plugins/index_lifecycle_management/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/index_management/tsconfig.json b/x-pack/plugins/index_management/tsconfig.json index 81a96a77cef83..120e58c2850c5 100644 --- a/x-pack/plugins/index_management/tsconfig.json +++ b/x-pack/plugins/index_management/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/infra/tsconfig.json b/x-pack/plugins/infra/tsconfig.json index 765af7974a2f1..a9739bdfdedc7 100644 --- a/x-pack/plugins/infra/tsconfig.json +++ b/x-pack/plugins/infra/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/ingest_pipelines/tsconfig.json b/x-pack/plugins/ingest_pipelines/tsconfig.json index a248bc9f337fe..de9a8362e8c6b 100644 --- a/x-pack/plugins/ingest_pipelines/tsconfig.json +++ b/x-pack/plugins/ingest_pipelines/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/lens/tsconfig.json b/x-pack/plugins/lens/tsconfig.json index 6c4d3631a12f3..04d3838df2063 100644 --- a/x-pack/plugins/lens/tsconfig.json +++ b/x-pack/plugins/lens/tsconfig.json @@ -1,42 +1,41 @@ { - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "composite": true, - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": [ - "*.ts", - "common/**/*", - "public/**/*", - "server/**/*", - "../../../typings/**/*" - ], - "references": [ - { "path": "../../../src/core/tsconfig.json" }, - { "path": "../task_manager/tsconfig.json" }, - { "path": "../global_search/tsconfig.json"}, - { "path": "../saved_objects_tagging/tsconfig.json"}, - { "path": "../../../src/plugins/data/tsconfig.json"}, - { "path": "../../../src/plugins/index_pattern_field_editor/tsconfig.json"}, - { "path": "../../../src/plugins/charts/tsconfig.json"}, - { "path": "../../../src/plugins/expressions/tsconfig.json"}, - { "path": "../../../src/plugins/navigation/tsconfig.json" }, - { "path": "../../../src/plugins/url_forwarding/tsconfig.json" }, - { "path": "../../../src/plugins/visualizations/tsconfig.json" }, - { "path": "../../../src/plugins/dashboard/tsconfig.json" }, - { "path": "../../../src/plugins/ui_actions/tsconfig.json" }, - { "path": "../../../src/plugins/embeddable/tsconfig.json" }, - { "path": "../../../src/plugins/share/tsconfig.json" }, - { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, - { "path": "../../../src/plugins/saved_objects/tsconfig.json"}, - { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, - { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, - { "path": "../../../src/plugins/embeddable/tsconfig.json"}, - { "path": "../../../src/plugins/presentation_util/tsconfig.json"}, - { "path": "../../../src/plugins/field_formats/tsconfig.json"} - ] - } + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "*.ts", + "common/**/*", + "public/**/*", + "server/**/*", + "../../../typings/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../task_manager/tsconfig.json" }, + { "path": "../global_search/tsconfig.json"}, + { "path": "../saved_objects_tagging/tsconfig.json"}, + { "path": "../../../src/plugins/data/tsconfig.json"}, + { "path": "../../../src/plugins/index_pattern_field_editor/tsconfig.json"}, + { "path": "../../../src/plugins/charts/tsconfig.json"}, + { "path": "../../../src/plugins/expressions/tsconfig.json"}, + { "path": "../../../src/plugins/navigation/tsconfig.json" }, + { "path": "../../../src/plugins/url_forwarding/tsconfig.json" }, + { "path": "../../../src/plugins/visualizations/tsconfig.json" }, + { "path": "../../../src/plugins/dashboard/tsconfig.json" }, + { "path": "../../../src/plugins/ui_actions/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../../src/plugins/share/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../../../src/plugins/saved_objects/tsconfig.json"}, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json"}, + { "path": "../../../src/plugins/presentation_util/tsconfig.json"}, + { "path": "../../../src/plugins/field_formats/tsconfig.json"} + ] +} diff --git a/x-pack/plugins/license_api_guard/tsconfig.json b/x-pack/plugins/license_api_guard/tsconfig.json index 1b6ea789760d5..123e73a9e8163 100644 --- a/x-pack/plugins/license_api_guard/tsconfig.json +++ b/x-pack/plugins/license_api_guard/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/license_management/tsconfig.json b/x-pack/plugins/license_management/tsconfig.json index e6cb0101ee838..4384a9a0efd98 100644 --- a/x-pack/plugins/license_management/tsconfig.json +++ b/x-pack/plugins/license_management/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/licensing/tsconfig.json b/x-pack/plugins/licensing/tsconfig.json index 6118bcd81d342..d8855fcd65912 100644 --- a/x-pack/plugins/licensing/tsconfig.json +++ b/x-pack/plugins/licensing/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/lists/tsconfig.json b/x-pack/plugins/lists/tsconfig.json index 9025a1640f8bf..691c5243d9db8 100644 --- a/x-pack/plugins/lists/tsconfig.json +++ b/x-pack/plugins/lists/tsconfig.json @@ -1,23 +1,22 @@ { - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "composite": true, - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": [ - "common/**/*", - "public/**/*", - "server/**/*", - // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 - "server/**/*.json", - ], - "references": [ - { "path": "../../../src/core/tsconfig.json" }, - { "path": "../spaces/tsconfig.json" }, - { "path": "../security/tsconfig.json"}, - ] - } + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 + "server/**/*.json", + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" }, + { "path": "../security/tsconfig.json"}, + ] +} diff --git a/x-pack/plugins/logstash/tsconfig.json b/x-pack/plugins/logstash/tsconfig.json index 6f21cfdb0b191..5a13e8ca71599 100644 --- a/x-pack/plugins/logstash/tsconfig.json +++ b/x-pack/plugins/logstash/tsconfig.json @@ -1,26 +1,25 @@ { - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "composite": true, - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": [ - "common/**/*", - "public/**/*", - "server/**/*", - ], - "references": [ - { "path": "../../../src/core/tsconfig.json" }, - { "path": "../../../src/plugins/home/tsconfig.json"}, - { "path": "../../../src/plugins/management/tsconfig.json"}, + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json"}, + { "path": "../../../src/plugins/management/tsconfig.json"}, - { "path": "../features/tsconfig.json" }, - { "path": "../licensing/tsconfig.json"}, - { "path": "../monitoring/tsconfig.json"}, - { "path": "../security/tsconfig.json"}, - ] - } + { "path": "../features/tsconfig.json" }, + { "path": "../licensing/tsconfig.json"}, + { "path": "../monitoring/tsconfig.json"}, + { "path": "../security/tsconfig.json"}, + ] +} diff --git a/x-pack/plugins/maps/tsconfig.json b/x-pack/plugins/maps/tsconfig.json index 1b74b7ee7566a..5245b374b9fec 100644 --- a/x-pack/plugins/maps/tsconfig.json +++ b/x-pack/plugins/maps/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/metrics_entities/tsconfig.json b/x-pack/plugins/metrics_entities/tsconfig.json index 15e6aa1601627..402b327a2dbf2 100644 --- a/x-pack/plugins/metrics_entities/tsconfig.json +++ b/x-pack/plugins/metrics_entities/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json b/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json index 6d01a853698b8..4748911105e8c 100644 --- a/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json +++ b/x-pack/plugins/ml/server/routes/apidoc_scripts/tsconfig.json @@ -1,9 +1,7 @@ { "extends": "../../../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "target": "es6", - "moduleResolution": "node" + "outDir": "./target/types" }, "include": [ "schema_worker.ts", diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json index 8e859c35e3f85..db8fc463b0550 100644 --- a/x-pack/plugins/ml/tsconfig.json +++ b/x-pack/plugins/ml/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/monitoring/tsconfig.json b/x-pack/plugins/monitoring/tsconfig.json index d0fb7e1a88dcf..756b8528865ce 100644 --- a/x-pack/plugins/monitoring/tsconfig.json +++ b/x-pack/plugins/monitoring/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/observability/tsconfig.json b/x-pack/plugins/observability/tsconfig.json index 8aa184bca913f..4e912ee4535b8 100644 --- a/x-pack/plugins/observability/tsconfig.json +++ b/x-pack/plugins/observability/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/osquery/cypress/tsconfig.json b/x-pack/plugins/osquery/cypress/tsconfig.json index 467ea13fc4869..1adb067fe682e 100644 --- a/x-pack/plugins/osquery/cypress/tsconfig.json +++ b/x-pack/plugins/osquery/cypress/tsconfig.json @@ -1,11 +1,13 @@ { "extends": "../../../../tsconfig.base.json", - "exclude": [], "include": [ - "./**/*" + "**/*" + ], + "exclude": [ + "target/**/*" ], "compilerOptions": { - "tsBuildInfoFile": "../../../../build/tsbuildinfo/osquery/cypress", + "outDir": "target/types", "types": [ "cypress", "node" diff --git a/x-pack/plugins/osquery/tsconfig.json b/x-pack/plugins/osquery/tsconfig.json index 76e26c770cfe0..50b9807670954 100644 --- a/x-pack/plugins/osquery/tsconfig.json +++ b/x-pack/plugins/osquery/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/painless_lab/tsconfig.json b/x-pack/plugins/painless_lab/tsconfig.json index a869b21e06d4d..e0cf386193bb4 100644 --- a/x-pack/plugins/painless_lab/tsconfig.json +++ b/x-pack/plugins/painless_lab/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/remote_clusters/tsconfig.json b/x-pack/plugins/remote_clusters/tsconfig.json index 9dc7926bd62ea..006c3c53c1be4 100644 --- a/x-pack/plugins/remote_clusters/tsconfig.json +++ b/x-pack/plugins/remote_clusters/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/reporting/tsconfig.json b/x-pack/plugins/reporting/tsconfig.json index 406fe9965b8a0..3e58450565720 100644 --- a/x-pack/plugins/reporting/tsconfig.json +++ b/x-pack/plugins/reporting/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/rollup/tsconfig.json b/x-pack/plugins/rollup/tsconfig.json index 6885081ce4bdd..fbe323b2549ea 100644 --- a/x-pack/plugins/rollup/tsconfig.json +++ b/x-pack/plugins/rollup/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/rule_registry/tsconfig.json b/x-pack/plugins/rule_registry/tsconfig.json index f6253e441da31..769c9f81f8ce9 100644 --- a/x-pack/plugins/rule_registry/tsconfig.json +++ b/x-pack/plugins/rule_registry/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/runtime_fields/tsconfig.json b/x-pack/plugins/runtime_fields/tsconfig.json index a1efe4c9cf2dd..5dc704ec57693 100644 --- a/x-pack/plugins/runtime_fields/tsconfig.json +++ b/x-pack/plugins/runtime_fields/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/saved_objects_tagging/tsconfig.json b/x-pack/plugins/saved_objects_tagging/tsconfig.json index d00156ad1277c..608cdb2c793cd 100644 --- a/x-pack/plugins/saved_objects_tagging/tsconfig.json +++ b/x-pack/plugins/saved_objects_tagging/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/searchprofiler/tsconfig.json b/x-pack/plugins/searchprofiler/tsconfig.json index f8ac3a61f7812..c53c65b812a44 100644 --- a/x-pack/plugins/searchprofiler/tsconfig.json +++ b/x-pack/plugins/searchprofiler/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/security/tsconfig.json b/x-pack/plugins/security/tsconfig.json index 6c3fd1851a8cb..ea03b9dbb6471 100644 --- a/x-pack/plugins/security/tsconfig.json +++ b/x-pack/plugins/security/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/security_solution/cypress/tsconfig.json b/x-pack/plugins/security_solution/cypress/tsconfig.json index 270d877a362a6..f762e63c899ac 100644 --- a/x-pack/plugins/security_solution/cypress/tsconfig.json +++ b/x-pack/plugins/security_solution/cypress/tsconfig.json @@ -1,11 +1,14 @@ { "extends": "../../../../tsconfig.base.json", - "exclude": [], "include": [ - "./**/*" + "**/*", + "fixtures/**/*.json" + ], + "exclude": [ + "target/**/*" ], "compilerOptions": { - "tsBuildInfoFile": "../../../../build/tsbuildinfo/security_solution/cypress", + "outDir": "target/types", "types": [ "cypress", "cypress-pipe", @@ -13,4 +16,7 @@ ], "resolveJsonModule": true, }, + "references": [ + { "path": "../tsconfig.json" } + ] } diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index 0df41b9f988b7..5c87d58199df4 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -1,47 +1,46 @@ { - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "composite": true, - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": [ - "common/**/*", - "public/**/*", - "server/**/*", - "scripts/**/*", - // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 - "server/**/*.json", - "public/**/*.json", - "../../../typings/**/*" - ], - "references": [ - { "path": "../../../src/core/tsconfig.json" }, - { "path": "../../../src/plugins/data/tsconfig.json" }, - { "path": "../../../src/plugins/embeddable/tsconfig.json" }, - { "path": "../../../src/plugins/home/tsconfig.json" }, - { "path": "../../../src/plugins/inspector/tsconfig.json" }, - { "path": "../../../src/plugins/ui_actions/tsconfig.json" }, - { "path": "../../../src/plugins/newsfeed/tsconfig.json" }, - { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, - { "path": "../../../src/plugins/telemetry/tsconfig.json" }, - { "path": "../../../src/plugins/telemetry_management_section/tsconfig.json" }, - { "path": "../actions/tsconfig.json" }, - { "path": "../alerting/tsconfig.json" }, - { "path": "../cases/tsconfig.json" }, - { "path": "../data_enhanced/tsconfig.json" }, - { "path": "../encrypted_saved_objects/tsconfig.json" }, - { "path": "../features/tsconfig.json" }, - { "path": "../fleet/tsconfig.json" }, - { "path": "../licensing/tsconfig.json" }, - { "path": "../lists/tsconfig.json" }, - { "path": "../maps/tsconfig.json" }, - { "path": "../ml/tsconfig.json" }, - { "path": "../spaces/tsconfig.json" }, - { "path": "../security/tsconfig.json"}, - { "path": "../timelines/tsconfig.json"}, - ] - } + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + "scripts/**/*", + // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 + "server/**/*.json", + "public/**/*.json", + "../../../typings/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + { "path": "../../../src/plugins/inspector/tsconfig.json" }, + { "path": "../../../src/plugins/ui_actions/tsconfig.json" }, + { "path": "../../../src/plugins/newsfeed/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../../../src/plugins/telemetry/tsconfig.json" }, + { "path": "../../../src/plugins/telemetry_management_section/tsconfig.json" }, + { "path": "../actions/tsconfig.json" }, + { "path": "../alerting/tsconfig.json" }, + { "path": "../cases/tsconfig.json" }, + { "path": "../data_enhanced/tsconfig.json" }, + { "path": "../encrypted_saved_objects/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + { "path": "../fleet/tsconfig.json" }, + { "path": "../licensing/tsconfig.json" }, + { "path": "../lists/tsconfig.json" }, + { "path": "../maps/tsconfig.json" }, + { "path": "../ml/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" }, + { "path": "../security/tsconfig.json"}, + { "path": "../timelines/tsconfig.json"}, + ] +} diff --git a/x-pack/plugins/snapshot_restore/tsconfig.json b/x-pack/plugins/snapshot_restore/tsconfig.json index 39beda02977e1..82f0e86df3683 100644 --- a/x-pack/plugins/snapshot_restore/tsconfig.json +++ b/x-pack/plugins/snapshot_restore/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/spaces/tsconfig.json b/x-pack/plugins/spaces/tsconfig.json index 3888519555d22..4cc95504a158e 100644 --- a/x-pack/plugins/spaces/tsconfig.json +++ b/x-pack/plugins/spaces/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/stack_alerts/tsconfig.json b/x-pack/plugins/stack_alerts/tsconfig.json index c83935945c67b..f3ae4509f35be 100644 --- a/x-pack/plugins/stack_alerts/tsconfig.json +++ b/x-pack/plugins/stack_alerts/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/task_manager/tsconfig.json b/x-pack/plugins/task_manager/tsconfig.json index 4b53dcac72c8e..42ebd42b4f7a5 100644 --- a/x-pack/plugins/task_manager/tsconfig.json +++ b/x-pack/plugins/task_manager/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/telemetry_collection_xpack/tsconfig.json b/x-pack/plugins/telemetry_collection_xpack/tsconfig.json index f4c17c4317a9f..03ca7efad22a4 100644 --- a/x-pack/plugins/telemetry_collection_xpack/tsconfig.json +++ b/x-pack/plugins/telemetry_collection_xpack/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/timelines/tsconfig.json b/x-pack/plugins/timelines/tsconfig.json index 17a2a9b612ab1..9677c0e64dd88 100644 --- a/x-pack/plugins/timelines/tsconfig.json +++ b/x-pack/plugins/timelines/tsconfig.json @@ -1,31 +1,30 @@ { - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "composite": true, - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": [ - "common/**/*", - "public/**/*", - "server/**/*", - // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 - "server/**/*.json", - "public/**/*.json", - "../../../typings/**/*" - ], - "references": [ - { "path": "../../../src/core/tsconfig.json" }, - { "path": "../../../src/plugins/data/tsconfig.json" }, - { "path": "../../../src/plugins/home/tsconfig.json" }, - { "path": "../data_enhanced/tsconfig.json" }, - { "path": "../features/tsconfig.json" }, - { "path": "../cases/tsconfig.json" }, - { "path": "../licensing/tsconfig.json" }, - { "path": "../spaces/tsconfig.json" }, - { "path": "../alerting/tsconfig.json" } - ] - } + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 + "server/**/*.json", + "public/**/*.json", + "../../../typings/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + { "path": "../data_enhanced/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + { "path": "../cases/tsconfig.json" }, + { "path": "../licensing/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" }, + { "path": "../alerting/tsconfig.json" } + ] +} diff --git a/x-pack/plugins/transform/tsconfig.json b/x-pack/plugins/transform/tsconfig.json index 2717f92c7a4df..99e8baf3f92fc 100644 --- a/x-pack/plugins/transform/tsconfig.json +++ b/x-pack/plugins/transform/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/translations/tsconfig.json b/x-pack/plugins/translations/tsconfig.json index e48512742ed68..6b09de638f3f9 100644 --- a/x-pack/plugins/translations/tsconfig.json +++ b/x-pack/plugins/translations/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/triggers_actions_ui/tsconfig.json b/x-pack/plugins/triggers_actions_ui/tsconfig.json index 8202449b22298..6536206acf369 100644 --- a/x-pack/plugins/triggers_actions_ui/tsconfig.json +++ b/x-pack/plugins/triggers_actions_ui/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/ui_actions_enhanced/tsconfig.json b/x-pack/plugins/ui_actions_enhanced/tsconfig.json index 39318770126e5..100a1decd9427 100644 --- a/x-pack/plugins/ui_actions_enhanced/tsconfig.json +++ b/x-pack/plugins/ui_actions_enhanced/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/upgrade_assistant/tsconfig.json b/x-pack/plugins/upgrade_assistant/tsconfig.json index 750bea75c6656..33a1421fbb0c1 100644 --- a/x-pack/plugins/upgrade_assistant/tsconfig.json +++ b/x-pack/plugins/upgrade_assistant/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/uptime/tsconfig.json b/x-pack/plugins/uptime/tsconfig.json index 88099b57f0898..a41da4837f453 100644 --- a/x-pack/plugins/uptime/tsconfig.json +++ b/x-pack/plugins/uptime/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, @@ -32,4 +31,4 @@ "path": "../fleet/tsconfig.json" } ] -} \ No newline at end of file +} diff --git a/x-pack/plugins/watcher/tsconfig.json b/x-pack/plugins/watcher/tsconfig.json index 15a28d498f2bd..e17e7e753592a 100644 --- a/x-pack/plugins/watcher/tsconfig.json +++ b/x-pack/plugins/watcher/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/plugins/xpack_legacy/tsconfig.json b/x-pack/plugins/xpack_legacy/tsconfig.json index 3bfc78b72cb3e..57fccc031a0cf 100644 --- a/x-pack/plugins/xpack_legacy/tsconfig.json +++ b/x-pack/plugins/xpack_legacy/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index cd43e7108b06d..1bfd24d6ad29d 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "composite": true, "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, diff --git a/x-pack/test/usage_collection/plugins/application_usage_test/tsconfig.json b/x-pack/test/usage_collection/plugins/application_usage_test/tsconfig.json index f1bf94a38de8f..e625acbc569cf 100644 --- a/x-pack/test/usage_collection/plugins/application_usage_test/tsconfig.json +++ b/x-pack/test/usage_collection/plugins/application_usage_test/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "public/**/*.ts", diff --git a/x-pack/test/usage_collection/plugins/stack_management_usage_test/tsconfig.json b/x-pack/test/usage_collection/plugins/stack_management_usage_test/tsconfig.json index f1bf94a38de8f..e625acbc569cf 100644 --- a/x-pack/test/usage_collection/plugins/stack_management_usage_test/tsconfig.json +++ b/x-pack/test/usage_collection/plugins/stack_management_usage_test/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "../../../../../tsconfig.base.json", "compilerOptions": { - "outDir": "./target", - "skipLibCheck": true + "outDir": "./target/types" }, "include": [ "public/**/*.ts", From e4e22ab9284db140818a5ac59b12d6a50502ffe3 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 10 Aug 2021 22:34:42 -0700 Subject: [PATCH 064/104] [Reporting] server side code clean up (#106940) * clean up the enqueue job function * clean up the screenshots observable * clean up authorized user pre routing * clean up get_user * fix download job response handlers * clean up jobs query factory repetition * clean up setup deps made available from plugin.ts * update test for screenshots observable * Revert "clean up setup deps made available from plugin.ts" This reverts commit 91de680ebf2535968d1577d130ad1b17bc123b1a. * revert renames * minor rename * fix test after rename Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/reporting/server/core.ts | 7 - .../export_types/png/lib/generate_png.ts | 8 +- .../printable_pdf/lib/generate_pdf.ts | 6 +- .../reporting/server/lib/enqueue_job.test.ts | 20 +- .../reporting/server/lib/enqueue_job.ts | 87 ++++---- .../reporting/server/lib/screenshots/index.ts | 11 +- .../server/lib/screenshots/observable.test.ts | 94 ++++---- .../server/lib/screenshots/observable.ts | 202 ++++++++---------- .../routes/csv_searchsource_immediate.ts | 78 +++---- .../server/routes/diagnostic/browser.ts | 5 +- .../server/routes/diagnostic/config.ts | 5 +- .../server/routes/diagnostic/screenshot.ts | 5 +- .../server/routes/generate_from_jobparams.ts | 5 +- .../reporting/server/routes/generation.ts | 13 +- .../plugins/reporting/server/routes/jobs.ts | 29 +-- .../lib/authorized_user_pre_routing.test.ts | 17 +- .../routes/lib/authorized_user_pre_routing.ts | 64 +++--- .../reporting/server/routes/lib/get_user.ts | 6 +- .../server/routes/lib/job_response_handler.ts | 142 ++++++------ 19 files changed, 370 insertions(+), 434 deletions(-) diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index 708b9b1bdbea5..e09cee8c3c7c2 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -29,7 +29,6 @@ import { ReportingConfig, ReportingSetup } from './'; import { HeadlessChromiumDriverFactory } from './browsers/chromium/driver_factory'; import { ReportingConfigType } from './config'; import { checkLicense, getExportTypesRegistry, LevelLogger } from './lib'; -import { screenshotsObservableFactory, ScreenshotsObservableFn } from './lib/screenshots'; import { ReportingStore } from './lib/store'; import { ExecuteReportTask, MonitorReportsTask, ReportTaskParams } from './lib/tasks'; import { ReportingPluginRouter } from './types'; @@ -237,12 +236,6 @@ export class ReportingCore { .toPromise(); } - public async getScreenshotsObservable(): Promise { - const config = this.getConfig(); - const { browserDriverFactory } = await this.getPluginStartDeps(); - return screenshotsObservableFactory(config.get('capture'), browserDriverFactory); - } - public getEnableScreenshotMode() { const { screenshotMode } = this.getPluginSetupDeps(); return screenshotMode.setScreenshotModeEnabled; diff --git a/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts b/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts index 12815cf1b25a4..2af56ed9881ae 100644 --- a/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts +++ b/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts @@ -11,7 +11,7 @@ import { finalize, map, tap } from 'rxjs/operators'; import { ReportingCore } from '../../../'; import { LevelLogger } from '../../../lib'; import { LayoutParams, PreserveLayout } from '../../../lib/layouts'; -import { ScreenshotResults } from '../../../lib/screenshots'; +import { getScreenshots$, ScreenshotResults } from '../../../lib/screenshots'; import { ConditionalHeaders } from '../../common'; function getBase64DecodedSize(value: string) { @@ -24,7 +24,9 @@ function getBase64DecodedSize(value: string) { } export async function generatePngObservableFactory(reporting: ReportingCore) { - const getScreenshots = await reporting.getScreenshotsObservable(); + const config = reporting.getConfig(); + const captureConfig = config.get('capture'); + const { browserDriverFactory } = await reporting.getPluginStartDeps(); return function generatePngObservable( logger: LevelLogger, @@ -43,7 +45,7 @@ export async function generatePngObservableFactory(reporting: ReportingCore) { const apmScreenshots = apmTrans?.startSpan('screenshots_pipeline', 'setup'); let apmBuffer: typeof apm.currentSpan; - const screenshots$ = getScreenshots({ + const screenshots$ = getScreenshots$(captureConfig, browserDriverFactory, { logger, urls: [url], conditionalHeaders, diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts index 9b1a1820b002a..88b9f8dc95b94 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts @@ -11,7 +11,7 @@ import { mergeMap } from 'rxjs/operators'; import { ReportingCore } from '../../../'; import { LevelLogger } from '../../../lib'; import { createLayout, LayoutParams } from '../../../lib/layouts'; -import { ScreenshotResults } from '../../../lib/screenshots'; +import { getScreenshots$, ScreenshotResults } from '../../../lib/screenshots'; import { ConditionalHeaders } from '../../common'; import { PdfMaker } from './pdf'; import { getTracker } from './tracker'; @@ -29,7 +29,7 @@ const getTimeRange = (urlScreenshots: ScreenshotResults[]) => { export async function generatePdfObservableFactory(reporting: ReportingCore) { const config = reporting.getConfig(); const captureConfig = config.get('capture'); - const getScreenshots = await reporting.getScreenshotsObservable(); + const { browserDriverFactory } = await reporting.getPluginStartDeps(); return function generatePdfObservable( logger: LevelLogger, @@ -48,7 +48,7 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) { tracker.endLayout(); tracker.startScreenshots(); - const screenshots$ = getScreenshots({ + const screenshots$ = getScreenshots$(captureConfig, browserDriverFactory, { logger, urls, conditionalHeaders, diff --git a/x-pack/plugins/reporting/server/lib/enqueue_job.test.ts b/x-pack/plugins/reporting/server/lib/enqueue_job.test.ts index abfa2b88258fc..8cfea1b010dfe 100644 --- a/x-pack/plugins/reporting/server/lib/enqueue_job.test.ts +++ b/x-pack/plugins/reporting/server/lib/enqueue_job.test.ts @@ -16,7 +16,7 @@ import { } from '../test_helpers'; import { ReportingRequestHandlerContext } from '../types'; import { ExportTypesRegistry, ReportingStore } from './'; -import { enqueueJobFactory } from './enqueue_job'; +import { enqueueJob } from './enqueue_job'; import { Report } from './store'; describe('Enqueue Job', () => { @@ -72,13 +72,14 @@ describe('Enqueue Job', () => { }); it('returns a Report object', async () => { - const enqueueJob = enqueueJobFactory(mockReporting, logger); const report = await enqueueJob( + mockReporting, + ({} as unknown) as KibanaRequest, + ({} as unknown) as ReportingRequestHandlerContext, + false, 'printablePdf', mockBaseParams, - false, - ({} as unknown) as ReportingRequestHandlerContext, - ({} as unknown) as KibanaRequest + logger ); const { _id, created_at: _created_at, ...snapObj } = report; @@ -117,14 +118,15 @@ describe('Enqueue Job', () => { }); it('provides a default kibana version field for older POST URLs', async () => { - const enqueueJob = enqueueJobFactory(mockReporting, logger); mockBaseParams.version = undefined; const report = await enqueueJob( + mockReporting, + ({} as unknown) as KibanaRequest, + ({} as unknown) as ReportingRequestHandlerContext, + false, 'printablePdf', mockBaseParams, - false, - ({} as unknown) as ReportingRequestHandlerContext, - ({} as unknown) as KibanaRequest + logger ); const { _id, created_at: _created_at, ...snapObj } = report; diff --git a/x-pack/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/plugins/reporting/server/lib/enqueue_job.ts index 1c73b0d925ad0..998e4edf26a38 100644 --- a/x-pack/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/plugins/reporting/server/lib/enqueue_job.ts @@ -12,64 +12,53 @@ import { BaseParams, ReportingUser } from '../types'; import { checkParamsVersion, LevelLogger } from './'; import { Report } from './store'; -export type EnqueueJobFn = ( +export async function enqueueJob( + reporting: ReportingCore, + request: KibanaRequest, + context: ReportingRequestHandlerContext, + user: ReportingUser, exportTypeId: string, jobParams: BaseParams, - user: ReportingUser, - context: ReportingRequestHandlerContext, - request: KibanaRequest -) => Promise; - -export function enqueueJobFactory( - reporting: ReportingCore, parentLogger: LevelLogger -): EnqueueJobFn { +): Promise { const logger = parentLogger.clone(['createJob']); - return async function enqueueJob( - exportTypeId: string, - jobParams: BaseParams, - user: ReportingUser, - context: ReportingRequestHandlerContext, - request: KibanaRequest - ) { - const exportType = reporting.getExportTypesRegistry().getById(exportTypeId); + const exportType = reporting.getExportTypesRegistry().getById(exportTypeId); - if (exportType == null) { - throw new Error(`Export type ${exportTypeId} does not exist in the registry!`); - } + if (exportType == null) { + throw new Error(`Export type ${exportTypeId} does not exist in the registry!`); + } - if (!exportType.createJobFnFactory) { - throw new Error(`Export type ${exportTypeId} is not an async job type!`); - } + if (!exportType.createJobFnFactory) { + throw new Error(`Export type ${exportTypeId} is not an async job type!`); + } - const [createJob, store] = await Promise.all([ - exportType.createJobFnFactory(reporting, logger.clone([exportType.id])), - reporting.getStore(), - ]); + const [createJob, store] = await Promise.all([ + exportType.createJobFnFactory(reporting, logger.clone([exportType.id])), + reporting.getStore(), + ]); - jobParams.version = checkParamsVersion(jobParams, logger); - const job = await createJob!(jobParams, context, request); + jobParams.version = checkParamsVersion(jobParams, logger); + const job = await createJob!(jobParams, context, request); - // 1. Add the report to ReportingStore to show as pending - const report = await store.addReport( - new Report({ - jobtype: exportType.jobType, - created_by: user ? user.username : false, - payload: job, - meta: { - objectType: jobParams.objectType, - layout: jobParams.layout?.id, - }, - }) - ); - logger.debug(`Successfully stored pending job: ${report._index}/${report._id}`); + // 1. Add the report to ReportingStore to show as pending + const report = await store.addReport( + new Report({ + jobtype: exportType.jobType, + created_by: user ? user.username : false, + payload: job, + meta: { + objectType: jobParams.objectType, + layout: jobParams.layout?.id, + }, + }) + ); + logger.debug(`Successfully stored pending job: ${report._index}/${report._id}`); - // 2. Schedule the report with Task Manager - const task = await reporting.scheduleTask(report.toReportTaskJSON()); - logger.info( - `Scheduled ${exportType.name} reporting task. Task ID: task:${task.id}. Report ID: ${report._id}` - ); + // 2. Schedule the report with Task Manager + const task = await reporting.scheduleTask(report.toReportTaskJSON()); + logger.info( + `Scheduled ${exportType.name} reporting task. Task ID: task:${task.id}. Report ID: ${report._id}` + ); - return report; - }; + return report; } diff --git a/x-pack/plugins/reporting/server/lib/screenshots/index.ts b/x-pack/plugins/reporting/server/lib/screenshots/index.ts index d924b45c1e016..d5ef52d627c6b 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/index.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/index.ts @@ -5,12 +5,11 @@ * 2.0. */ -import * as Rx from 'rxjs'; import { LevelLogger } from '../'; import { ConditionalHeaders } from '../../export_types/common'; import { LayoutInstance } from '../layouts'; -export { screenshotsObservableFactory } from './observable'; +export { getScreenshots$ } from './observable'; export interface ScreenshotObservableOpts { logger: LevelLogger; @@ -55,11 +54,3 @@ export interface ScreenshotResults { error?: Error; elementsPositionAndAttributes?: ElementsPositionAndAttribute[]; // NOTE: for testing } - -export type ScreenshotsObservableFn = ({ - logger, - urls, - conditionalHeaders, - layout, - browserTimezone, -}: ScreenshotObservableOpts) => Rx.Observable; diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts index dd8aadb49a5ba..7458340a4a52f 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts @@ -32,7 +32,7 @@ import { } from '../../test_helpers'; import { ElementsPositionAndAttribute } from './'; import * as contexts from './constants'; -import { screenshotsObservableFactory } from './observable'; +import { getScreenshots$ } from './'; /* * Mocks @@ -67,8 +67,7 @@ describe('Screenshot Observable Pipeline', () => { }); it('pipelines a single url into screenshot and timeRange', async () => { - const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory); - const result = await getScreenshots$({ + const result = await getScreenshots$(captureConfig, mockBrowserDriverFactory, { logger, urls: ['/welcome/home/start/index.htm'], conditionalHeaders: {} as ConditionalHeaders, @@ -128,8 +127,7 @@ describe('Screenshot Observable Pipeline', () => { }); // test - const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory); - const result = await getScreenshots$({ + const result = await getScreenshots$(captureConfig, mockBrowserDriverFactory, { logger, urls: ['/welcome/home/start/index2.htm', '/welcome/home/start/index.php3?page=./home.php'], conditionalHeaders: {} as ConditionalHeaders, @@ -227,9 +225,8 @@ describe('Screenshot Observable Pipeline', () => { }); // test - const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory); const getScreenshot = async () => { - return await getScreenshots$({ + return await getScreenshots$(captureConfig, mockBrowserDriverFactory, { logger, urls: [ '/welcome/home/start/index2.htm', @@ -322,9 +319,8 @@ describe('Screenshot Observable Pipeline', () => { }); // test - const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory); const getScreenshot = async () => { - return await getScreenshots$({ + return await getScreenshots$(captureConfig, mockBrowserDriverFactory, { logger, urls: ['/welcome/home/start/index.php3?page=./home.php3'], conditionalHeaders: {} as ConditionalHeaders, @@ -354,50 +350,46 @@ describe('Screenshot Observable Pipeline', () => { }); mockLayout.getViewport = () => null; - // test - const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory); - const getScreenshot = async () => { - return await getScreenshots$({ - logger, - urls: ['/welcome/home/start/index.php3?page=./home.php3'], - conditionalHeaders: {} as ConditionalHeaders, - layout: mockLayout, - browserTimezone: 'UTC', - }).toPromise(); - }; + const screenshots = await getScreenshots$(captureConfig, mockBrowserDriverFactory, { + logger, + urls: ['/welcome/home/start/index.php3?page=./home.php3'], + conditionalHeaders: {} as ConditionalHeaders, + layout: mockLayout, + browserTimezone: 'UTC', + }).toPromise(); - await expect(getScreenshot()).resolves.toMatchInlineSnapshot(` - Array [ - Object { - "elementsPositionAndAttributes": Array [ - Object { - "attributes": Object {}, - "position": Object { - "boundingClientRect": Object { - "height": 1200, - "left": 0, - "top": 0, - "width": 1800, - }, - "scroll": Object { - "x": 0, - "y": 0, - }, - }, - }, - ], - "error": undefined, - "screenshots": Array [ - Object { - "base64EncodedData": "allyourBase64", - "description": undefined, - "title": undefined, - }, - ], - "timeRange": undefined, + expect(screenshots).toMatchInlineSnapshot(` + Array [ + Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object {}, + "position": Object { + "boundingClientRect": Object { + "height": 1200, + "left": 0, + "top": 0, + "width": 1800, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, }, - ] - `); + }, + ], + "error": undefined, + "screenshots": Array [ + Object { + "base64EncodedData": "allyourBase64", + "description": undefined, + "title": undefined, + }, + ], + "timeRange": undefined, + }, + ] + `); }); }); }); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts index 3692678064415..baaf8a4fb38ee 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts @@ -10,12 +10,7 @@ import * as Rx from 'rxjs'; import { catchError, concatMap, first, mergeMap, take, takeUntil, toArray } from 'rxjs/operators'; import { HeadlessChromiumDriverFactory } from '../../browsers'; import { CaptureConfig } from '../../types'; -import { - ElementsPositionAndAttribute, - ScreenshotObservableOpts, - ScreenshotResults, - ScreenshotsObservableFn, -} from './'; +import { ElementsPositionAndAttribute, ScreenshotObservableOpts, ScreenshotResults } from './'; import { checkPageIsOpen } from './check_browser_open'; import { DEFAULT_PAGELOAD_SELECTOR } from './constants'; import { getElementPositionAndAttributes } from './get_element_position_data'; @@ -36,117 +31,110 @@ interface ScreenSetupData { error?: Error; } -export function screenshotsObservableFactory( +export function getScreenshots$( captureConfig: CaptureConfig, - browserDriverFactory: HeadlessChromiumDriverFactory -): ScreenshotsObservableFn { - return function screenshotsObservable({ - logger, - urls, - conditionalHeaders, - layout, - browserTimezone, - }: ScreenshotObservableOpts): Rx.Observable { - const apmTrans = apm.startTransaction(`reporting screenshot pipeline`, 'reporting'); + browserDriverFactory: HeadlessChromiumDriverFactory, + { logger, urls, conditionalHeaders, layout, browserTimezone }: ScreenshotObservableOpts +): Rx.Observable { + const apmTrans = apm.startTransaction(`reporting screenshot pipeline`, 'reporting'); - const apmCreatePage = apmTrans?.startSpan('create_page', 'wait'); - const create$ = browserDriverFactory.createPage( - { viewport: layout.getBrowserViewport(), browserTimezone }, - logger - ); + const apmCreatePage = apmTrans?.startSpan('create_page', 'wait'); + const create$ = browserDriverFactory.createPage( + { viewport: layout.getBrowserViewport(), browserTimezone }, + logger + ); - return create$.pipe( - mergeMap(({ driver, exit$ }) => { - apmCreatePage?.end(); - exit$.subscribe({ error: () => apmTrans?.end() }); + return create$.pipe( + mergeMap(({ driver, exit$ }) => { + apmCreatePage?.end(); + exit$.subscribe({ error: () => apmTrans?.end() }); - return Rx.from(urls).pipe( - concatMap((url, index) => { - const setup$: Rx.Observable = Rx.of(1).pipe( - mergeMap(() => { - // If we're moving to another page in the app, we'll want to wait for the app to tell us - // it's loaded the next page. - const page = index + 1; - const pageLoadSelector = - page > 1 ? `[data-shared-page="${page}"]` : DEFAULT_PAGELOAD_SELECTOR; + return Rx.from(urls).pipe( + concatMap((url, index) => { + const setup$: Rx.Observable = Rx.of(1).pipe( + mergeMap(() => { + // If we're moving to another page in the app, we'll want to wait for the app to tell us + // it's loaded the next page. + const page = index + 1; + const pageLoadSelector = + page > 1 ? `[data-shared-page="${page}"]` : DEFAULT_PAGELOAD_SELECTOR; - return openUrl( - captureConfig, - driver, - url, - pageLoadSelector, - conditionalHeaders, - logger - ); - }), - mergeMap(() => getNumberOfItems(captureConfig, driver, layout, logger)), - mergeMap(async (itemsCount) => { - // set the viewport to the dimentions from the job, to allow elements to flow into the expected layout - const viewport = layout.getViewport(itemsCount) || getDefaultViewPort(); - await Promise.all([ - driver.setViewport(viewport, logger), - waitForVisualizations(captureConfig, driver, itemsCount, layout, logger), - ]); - }), - mergeMap(async () => { - // Waiting till _after_ elements have rendered before injecting our CSS - // allows for them to be displayed properly in many cases - await injectCustomCss(driver, layout, logger); + return openUrl( + captureConfig, + driver, + url, + pageLoadSelector, + conditionalHeaders, + logger + ); + }), + mergeMap(() => getNumberOfItems(captureConfig, driver, layout, logger)), + mergeMap(async (itemsCount) => { + // set the viewport to the dimentions from the job, to allow elements to flow into the expected layout + const viewport = layout.getViewport(itemsCount) || getDefaultViewPort(); + await Promise.all([ + driver.setViewport(viewport, logger), + waitForVisualizations(captureConfig, driver, itemsCount, layout, logger), + ]); + }), + mergeMap(async () => { + // Waiting till _after_ elements have rendered before injecting our CSS + // allows for them to be displayed properly in many cases + await injectCustomCss(driver, layout, logger); - const apmPositionElements = apmTrans?.startSpan('position_elements', 'correction'); - if (layout.positionElements) { - // position panel elements for print layout - await layout.positionElements(driver, logger); - } - if (apmPositionElements) apmPositionElements.end(); + const apmPositionElements = apmTrans?.startSpan('position_elements', 'correction'); + if (layout.positionElements) { + // position panel elements for print layout + await layout.positionElements(driver, logger); + } + if (apmPositionElements) apmPositionElements.end(); - await waitForRenderComplete(captureConfig, driver, layout, logger); - }), - mergeMap(async () => { - return await Promise.all([ - getTimeRange(driver, layout, logger), - getElementPositionAndAttributes(driver, layout, logger), - ]).then(([timeRange, elementsPositionAndAttributes]) => ({ - elementsPositionAndAttributes, - timeRange, - })); - }), - catchError((err) => { - checkPageIsOpen(driver); // if browser has closed, throw a relevant error about it + await waitForRenderComplete(captureConfig, driver, layout, logger); + }), + mergeMap(async () => { + return await Promise.all([ + getTimeRange(driver, layout, logger), + getElementPositionAndAttributes(driver, layout, logger), + ]).then(([timeRange, elementsPositionAndAttributes]) => ({ + elementsPositionAndAttributes, + timeRange, + })); + }), + catchError((err) => { + checkPageIsOpen(driver); // if browser has closed, throw a relevant error about it - logger.error(err); - return Rx.of({ elementsPositionAndAttributes: null, timeRange: null, error: err }); - }) - ); + logger.error(err); + return Rx.of({ elementsPositionAndAttributes: null, timeRange: null, error: err }); + }) + ); - return setup$.pipe( - takeUntil(exit$), - mergeMap( - async (data: ScreenSetupData): Promise => { - checkPageIsOpen(driver); // re-check that the browser has not closed + return setup$.pipe( + takeUntil(exit$), + mergeMap( + async (data: ScreenSetupData): Promise => { + checkPageIsOpen(driver); // re-check that the browser has not closed - const elements = data.elementsPositionAndAttributes - ? data.elementsPositionAndAttributes - : getDefaultElementPosition(layout.getViewport(1)); - const screenshots = await getScreenshots(driver, layout, elements, logger); - const { timeRange, error: setupError } = data; - return { - timeRange, - screenshots, - error: setupError, - elementsPositionAndAttributes: elements, - }; - } - ) - ); - }), - take(urls.length), - toArray() - ); - }), - first() - ); - }; + const elements = data.elementsPositionAndAttributes + ? data.elementsPositionAndAttributes + : getDefaultElementPosition(layout.getViewport(1)); + const screenshots = await getScreenshots(driver, layout, elements, logger); + const { timeRange, error: setupError } = data; + return { + timeRange, + screenshots, + error: setupError, + elementsPositionAndAttributes: elements, + }; + } + ) + ); + }), + take(urls.length), + toArray() + ); + }), + first() + ); } /* diff --git a/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts b/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts index 3d482d4f84d52..57324830af4a0 100644 --- a/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts @@ -13,7 +13,7 @@ import { runTaskFnFactory } from '../export_types/csv_searchsource_immediate/exe import { JobParamsDownloadCSV } from '../export_types/csv_searchsource_immediate/types'; import { LevelLogger as Logger } from '../lib'; import { TaskRunResult } from '../lib/tasks'; -import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; +import { authorizedUserPreRouting } from './lib/authorized_user_pre_routing'; import { HandlerErrorFunction } from './types'; const API_BASE_URL_V1 = '/api/reporting/v1'; @@ -36,7 +36,6 @@ export function registerGenerateCsvFromSavedObjectImmediate( parentLogger: Logger ) { const setupDeps = reporting.getPluginSetupDeps(); - const userHandler = authorizedUserPreRoutingFactory(reporting); const { router } = setupDeps; // TODO: find a way to abstract this using ExportTypeRegistry: it needs a new @@ -63,47 +62,50 @@ export function registerGenerateCsvFromSavedObjectImmediate( tags: kibanaAccessControlTags, }, }, - userHandler(async (_user, context, req: CsvFromSavedObjectRequest, res) => { - const logger = parentLogger.clone(['csv_searchsource_immediate']); - const runTaskFn = runTaskFnFactory(reporting, logger); + authorizedUserPreRouting( + reporting, + async (_user, context, req: CsvFromSavedObjectRequest, res) => { + const logger = parentLogger.clone(['csv_searchsource_immediate']); + const runTaskFn = runTaskFnFactory(reporting, logger); - try { - let buffer = Buffer.from(''); - const stream = new Writable({ - write(chunk, encoding, callback) { - buffer = Buffer.concat([ - buffer, - Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding), - ]); - callback(); - }, - }); + try { + let buffer = Buffer.from(''); + const stream = new Writable({ + write(chunk, encoding, callback) { + buffer = Buffer.concat([ + buffer, + Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding), + ]); + callback(); + }, + }); - const { - content_type: jobOutputContentType, - size: jobOutputSize, - }: TaskRunResult = await runTaskFn(null, req.body, context, stream, req); - stream.end(); - const jobOutputContent = buffer.toString(); + const { + content_type: jobOutputContentType, + size: jobOutputSize, + }: TaskRunResult = await runTaskFn(null, req.body, context, stream, req); + stream.end(); + const jobOutputContent = buffer.toString(); - logger.info(`Job output size: ${jobOutputSize} bytes`); + logger.info(`Job output size: ${jobOutputSize} bytes`); - // convert null to undefined so the value can be sent to h.response() - if (jobOutputContent === null) { - logger.warn('CSV Job Execution created empty content result'); - } + // convert null to undefined so the value can be sent to h.response() + if (jobOutputContent === null) { + logger.warn('CSV Job Execution created empty content result'); + } - return res.ok({ - body: jobOutputContent || '', - headers: { - 'content-type': jobOutputContentType ? jobOutputContentType : [], - 'accept-ranges': 'none', - }, - }); - } catch (err) { - logger.error(err); - return handleError(res, err); + return res.ok({ + body: jobOutputContent || '', + headers: { + 'content-type': jobOutputContentType ? jobOutputContentType : [], + 'accept-ranges': 'none', + }, + }); + } catch (err) { + logger.error(err); + return handleError(res, err); + } } - }) + ) ); } diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts b/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts index 25e60c65676d7..268cabd9be55f 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts @@ -10,7 +10,7 @@ import { ReportingCore } from '../..'; import { API_DIAGNOSE_URL } from '../../../common/constants'; import { browserStartLogs } from '../../browsers/chromium/driver_factory/start_logs'; import { LevelLogger as Logger } from '../../lib'; -import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_routing'; +import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing'; import { DiagnosticResponse } from './'; const logsToHelpMap = { @@ -47,14 +47,13 @@ const logsToHelpMap = { export const registerDiagnoseBrowser = (reporting: ReportingCore, logger: Logger) => { const { router } = reporting.getPluginSetupDeps(); - const userHandler = authorizedUserPreRoutingFactory(reporting); router.post( { path: `${API_DIAGNOSE_URL}/browser`, validate: {}, }, - userHandler(async (user, context, req, res) => { + authorizedUserPreRouting(reporting, async (_user, _context, _req, res) => { try { const logs = await browserStartLogs(reporting, logger).toPromise(); const knownIssues = Object.keys(logsToHelpMap) as Array; diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/config.ts b/x-pack/plugins/reporting/server/routes/diagnostic/config.ts index 109849aa302f2..93677409693f8 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/config.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/config.ts @@ -11,7 +11,7 @@ import { defaults, get } from 'lodash'; import { ReportingCore } from '../..'; import { API_DIAGNOSE_URL } from '../../../common/constants'; import { LevelLogger as Logger } from '../../lib'; -import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_routing'; +import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing'; import { DiagnosticResponse } from './'; const KIBANA_MAX_SIZE_BYTES_PATH = 'csv.maxSizeBytes'; @@ -27,7 +27,6 @@ const numberToByteSizeValue = (value: number | ByteSizeValue) => { export const registerDiagnoseConfig = (reporting: ReportingCore, logger: Logger) => { const setupDeps = reporting.getPluginSetupDeps(); - const userHandler = authorizedUserPreRoutingFactory(reporting); const { router } = setupDeps; router.post( @@ -35,7 +34,7 @@ export const registerDiagnoseConfig = (reporting: ReportingCore, logger: Logger) path: `${API_DIAGNOSE_URL}/config`, validate: {}, }, - userHandler(async (user, context, req, res) => { + authorizedUserPreRouting(reporting, async (_user, _context, _req, res) => { const warnings = []; const { asInternalUser: elasticsearchClient } = await reporting.getEsClient(); const config = reporting.getConfig(); diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts index 7405e8cff8975..765e0a2a4e8a2 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts @@ -13,12 +13,11 @@ import { omitBlockedHeaders } from '../../export_types/common'; import { getAbsoluteUrlFactory } from '../../export_types/common/get_absolute_url'; import { generatePngObservableFactory } from '../../export_types/png/lib/generate_png'; import { LevelLogger as Logger } from '../../lib'; -import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_routing'; +import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing'; import { DiagnosticResponse } from './'; export const registerDiagnoseScreenshot = (reporting: ReportingCore, logger: Logger) => { const setupDeps = reporting.getPluginSetupDeps(); - const userHandler = authorizedUserPreRoutingFactory(reporting); const { router } = setupDeps; router.post( @@ -26,7 +25,7 @@ export const registerDiagnoseScreenshot = (reporting: ReportingCore, logger: Log path: `${API_DIAGNOSE_URL}/screenshot`, validate: {}, }, - userHandler(async (user, context, req, res) => { + authorizedUserPreRouting(reporting, async (_user, _context, req, res) => { const generatePngObservable = await generatePngObservableFactory(reporting); const config = reporting.getConfig(); const decryptedHeaders = req.headers as Record; diff --git a/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts b/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts index 69b3f216886e6..c519616cda5fb 100644 --- a/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts +++ b/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts @@ -10,7 +10,7 @@ import rison from 'rison-node'; import { ReportingCore } from '../'; import { API_BASE_URL } from '../../common/constants'; import { BaseParams } from '../types'; -import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; +import { authorizedUserPreRouting } from './lib/authorized_user_pre_routing'; import { HandlerErrorFunction, HandlerFunction } from './types'; const BASE_GENERATE = `${API_BASE_URL}/generate`; @@ -21,7 +21,6 @@ export function registerGenerateFromJobParams( handleError: HandlerErrorFunction ) { const setupDeps = reporting.getPluginSetupDeps(); - const userHandler = authorizedUserPreRoutingFactory(reporting); const { router } = setupDeps; // TODO: find a way to abstract this using ExportTypeRegistry: it needs a new @@ -41,7 +40,7 @@ export function registerGenerateFromJobParams( }, options: { tags: kibanaAccessControlTags }, }, - userHandler(async (user, context, req, res) => { + authorizedUserPreRouting(reporting, async (user, context, req, res) => { let jobParamsRison: null | string = null; if (req.body) { diff --git a/x-pack/plugins/reporting/server/routes/generation.ts b/x-pack/plugins/reporting/server/routes/generation.ts index ce6d1a2f2641f..4082084c82fbc 100644 --- a/x-pack/plugins/reporting/server/routes/generation.ts +++ b/x-pack/plugins/reporting/server/routes/generation.ts @@ -10,7 +10,7 @@ import { kibanaResponseFactory } from 'src/core/server'; import { ReportingCore } from '../'; import { API_BASE_URL } from '../../common/constants'; import { LevelLogger as Logger } from '../lib'; -import { enqueueJobFactory } from '../lib/enqueue_job'; +import { enqueueJob } from '../lib/enqueue_job'; import { registerGenerateFromJobParams } from './generate_from_jobparams'; import { registerGenerateCsvFromSavedObjectImmediate } from './csv_searchsource_immediate'; import { HandlerFunction } from './types'; @@ -42,8 +42,15 @@ export function registerJobGenerationRoutes(reporting: ReportingCore, logger: Lo } try { - const enqueueJob = enqueueJobFactory(reporting, logger); - const report = await enqueueJob(exportTypeId, jobParams, user, context, req); + const report = await enqueueJob( + reporting, + req, + context, + user, + exportTypeId, + jobParams, + logger + ); // return task manager's task information and the download URL const downloadBaseUrl = getDownloadBaseUrl(reporting); diff --git a/x-pack/plugins/reporting/server/routes/jobs.ts b/x-pack/plugins/reporting/server/routes/jobs.ts index 37557c3afb0c7..ad0aac121106c 100644 --- a/x-pack/plugins/reporting/server/routes/jobs.ts +++ b/x-pack/plugins/reporting/server/routes/jobs.ts @@ -10,12 +10,9 @@ import Boom from '@hapi/boom'; import { ROUTE_TAG_CAN_REDIRECT } from '../../../security/server'; import { ReportingCore } from '../'; import { API_BASE_URL } from '../../common/constants'; -import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; +import { authorizedUserPreRouting } from './lib/authorized_user_pre_routing'; import { jobsQueryFactory } from './lib/jobs_query'; -import { - deleteJobResponseHandlerFactory, - downloadJobResponseHandlerFactory, -} from './lib/job_response_handler'; +import { deleteJobResponseHandler, downloadJobResponseHandler } from './lib/job_response_handler'; const MAIN_ENTRY = `${API_BASE_URL}/jobs`; @@ -25,8 +22,8 @@ const handleUnavailable = (res: any) => { export function registerJobInfoRoutes(reporting: ReportingCore) { const setupDeps = reporting.getPluginSetupDeps(); - const userHandler = authorizedUserPreRoutingFactory(reporting); const { router } = setupDeps; + const jobsQuery = jobsQueryFactory(reporting); // list jobs in the queue, paginated router.get( @@ -40,7 +37,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { }), }, }, - userHandler(async (user, context, req, res) => { + authorizedUserPreRouting(reporting, async (user, context, req, res) => { // ensure the async dependencies are loaded if (!context.reporting) { return handleUnavailable(res); @@ -53,7 +50,6 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { const page = parseInt(queryPage, 10) || 0; const size = Math.min(100, parseInt(querySize, 10) || 10); const jobIds = queryIds ? queryIds.split(',') : null; - const jobsQuery = jobsQueryFactory(reporting); const results = await jobsQuery.list(jobTypes, user, page, size, jobIds); return res.ok({ @@ -71,7 +67,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { path: `${MAIN_ENTRY}/count`, validate: false, }, - userHandler(async (user, context, _req, res) => { + authorizedUserPreRouting(reporting, async (user, context, _req, res) => { // ensure the async dependencies are loaded if (!context.reporting) { return handleUnavailable(res); @@ -81,7 +77,6 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { management: { jobTypes = [] }, } = await reporting.getLicenseInfo(); - const jobsQuery = jobsQueryFactory(reporting); const count = await jobsQuery.count(jobTypes, user); return res.ok({ @@ -103,7 +98,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { }), }, }, - userHandler(async (user, context, req, res) => { + authorizedUserPreRouting(reporting, async (user, context, req, res) => { // ensure the async dependencies are loaded if (!context.reporting) { return res.custom({ statusCode: 503 }); @@ -114,7 +109,6 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { management: { jobTypes = [] }, } = await reporting.getLicenseInfo(); - const jobsQuery = jobsQueryFactory(reporting); const result = await jobsQuery.get(user, docId); if (!result) { @@ -137,8 +131,6 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { ); // trigger a download of the output from a job - const downloadResponseHandler = downloadJobResponseHandlerFactory(reporting); - router.get( { path: `${MAIN_ENTRY}/download/{docId}`, @@ -149,7 +141,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { }, options: { tags: [ROUTE_TAG_CAN_REDIRECT] }, }, - userHandler(async (user, context, req, res) => { + authorizedUserPreRouting(reporting, async (user, context, req, res) => { // ensure the async dependencies are loaded if (!context.reporting) { return handleUnavailable(res); @@ -160,12 +152,11 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { management: { jobTypes = [] }, } = await reporting.getLicenseInfo(); - return downloadResponseHandler(res, jobTypes, user, { docId }); + return downloadJobResponseHandler(reporting, res, jobTypes, user, { docId }); }) ); // allow a report to be deleted - const deleteResponseHandler = deleteJobResponseHandlerFactory(reporting); router.delete( { path: `${MAIN_ENTRY}/delete/{docId}`, @@ -175,7 +166,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { }), }, }, - userHandler(async (user, context, req, res) => { + authorizedUserPreRouting(reporting, async (user, context, req, res) => { // ensure the async dependencies are loaded if (!context.reporting) { return handleUnavailable(res); @@ -186,7 +177,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { management: { jobTypes = [] }, } = await reporting.getLicenseInfo(); - return deleteResponseHandler(res, jobTypes, user, { docId }); + return deleteJobResponseHandler(reporting, res, jobTypes, user, { docId }); }) ); } diff --git a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts index 16ef9e6d5bc10..0cff81539e52e 100644 --- a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts +++ b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts @@ -11,7 +11,7 @@ import { ReportingCore } from '../../'; import { ReportingInternalSetup } from '../../core'; import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import type { ReportingRequestHandlerContext } from '../../types'; -import { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing'; +import { authorizedUserPreRouting } from './authorized_user_pre_routing'; let mockCore: ReportingCore; const mockReportingConfig = createMockConfigSchema({ roles: { enabled: false } }); @@ -46,11 +46,10 @@ describe('authorized_user_pre_routing', function () { ...mockCore.pluginSetupDeps, security: undefined, // disable security } as unknown) as ReportingInternalSetup); - const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); const mockResponseFactory = httpServerMock.createResponseFactory() as KibanaResponseFactory; let handlerCalled = false; - authorizedUserPreRouting((user: unknown) => { + authorizedUserPreRouting(mockCore, (user: unknown) => { expect(user).toBe(false); // verify the user is a false value handlerCalled = true; return Promise.resolve({ status: 200, options: {} }); @@ -70,11 +69,10 @@ describe('authorized_user_pre_routing', function () { }, }, // disable security } as unknown) as ReportingInternalSetup); - const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); const mockResponseFactory = httpServerMock.createResponseFactory() as KibanaResponseFactory; let handlerCalled = false; - authorizedUserPreRouting((user: unknown) => { + authorizedUserPreRouting(mockCore, (user: unknown) => { expect(user).toBe(false); // verify the user is a false value handlerCalled = true; return Promise.resolve({ status: 200, options: {} }); @@ -93,11 +91,10 @@ describe('authorized_user_pre_routing', function () { authc: { getCurrentUser: () => null }, }, } as unknown) as ReportingInternalSetup); - const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); const mockHandler = () => { throw new Error('Handler callback should not be called'); }; - const requestHandler = authorizedUserPreRouting(mockHandler); + const requestHandler = authorizedUserPreRouting(mockCore, mockHandler); const mockResponseFactory = getMockResponseFactory(); expect(requestHandler(getMockContext(), getMockRequest(), mockResponseFactory)).toMatchObject({ @@ -126,14 +123,13 @@ describe('authorized_user_pre_routing', function () { authc: { getCurrentUser: () => ({ username: 'friendlyuser', roles: ['cowboy'] }) }, }, } as unknown) as ReportingInternalSetup); - const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); const mockResponseFactory = getMockResponseFactory(); const mockHandler = () => { throw new Error('Handler callback should not be called'); }; expect( - authorizedUserPreRouting(mockHandler)( + authorizedUserPreRouting(mockCore, mockHandler)( getMockContext(), getMockRequest(), mockResponseFactory @@ -153,10 +149,9 @@ describe('authorized_user_pre_routing', function () { }, }, } as unknown) as ReportingInternalSetup); - const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); const mockResponseFactory = getMockResponseFactory(); - authorizedUserPreRouting((user) => { + authorizedUserPreRouting(mockCore, (user) => { expect(user).toMatchObject({ roles: ['reporting_user'], username: 'friendlyuser' }); done(); return Promise.resolve({ status: 200, options: {} }); diff --git a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts index 846d8c28a5378..2c6a01e615487 100644 --- a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts +++ b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts @@ -8,12 +8,13 @@ import { RequestHandler, RouteMethod } from 'src/core/server'; import { AuthenticatedUser } from '../../../../security/server'; import { ReportingCore } from '../../core'; -import { getUserFactory } from './get_user'; +import { getUser } from './get_user'; import type { ReportingRequestHandlerContext } from '../../types'; const superuserRole = 'superuser'; type ReportingRequestUser = AuthenticatedUser | false; + export type RequestHandlerUser = RequestHandler< P, Q, @@ -23,43 +24,40 @@ export type RequestHandlerUser = RequestHandler< ? (user: ReportingRequestUser, ...a: U) => R : never; -export const authorizedUserPreRoutingFactory = function authorizedUserPreRoutingFn( - reporting: ReportingCore -) { +export const authorizedUserPreRouting = ( + reporting: ReportingCore, + handler: RequestHandlerUser +): RequestHandler => { const { logger, security } = reporting.getPluginSetupDeps(); - const getUser = getUserFactory(security); - return ( - handler: RequestHandlerUser - ): RequestHandler => { - return (context, req, res) => { - try { - let user: ReportingRequestUser = false; - if (security && security.license.isEnabled()) { - // find the authenticated user, or null if security is not enabled - user = getUser(req); - if (!user) { - // security is enabled but the user is null - return res.unauthorized({ body: `Sorry, you aren't authenticated` }); - } + + return (context, req, res) => { + try { + let user: ReportingRequestUser = false; + if (security && security.license.isEnabled()) { + // find the authenticated user, or null if security is not enabled + user = getUser(req, security); + if (!user) { + // security is enabled but the user is null + return res.unauthorized({ body: `Sorry, you aren't authenticated` }); } + } - const deprecatedAllowedRoles = reporting.getDeprecatedAllowedRoles(); - if (user && deprecatedAllowedRoles !== false) { - // check allowance with the configured set of roleas + "superuser" - const allowedRoles = deprecatedAllowedRoles || []; - const authorizedRoles = [superuserRole, ...allowedRoles]; + const deprecatedAllowedRoles = reporting.getDeprecatedAllowedRoles(); + if (user && deprecatedAllowedRoles !== false) { + // check allowance with the configured set of roleas + "superuser" + const allowedRoles = deprecatedAllowedRoles || []; + const authorizedRoles = [superuserRole, ...allowedRoles]; - if (!user.roles.find((role) => authorizedRoles.includes(role))) { - // user's roles do not allow - return res.forbidden({ body: `Sorry, you don't have access to Reporting` }); - } + if (!user.roles.find((role) => authorizedRoles.includes(role))) { + // user's roles do not allow + return res.forbidden({ body: `Sorry, you don't have access to Reporting` }); } - - return handler(user, context, req, res); - } catch (err) { - logger.error(err); - return res.custom({ statusCode: 500 }); } - }; + + return handler(user, context, req, res); + } catch (err) { + logger.error(err); + return res.custom({ statusCode: 500 }); + } }; }; diff --git a/x-pack/plugins/reporting/server/routes/lib/get_user.ts b/x-pack/plugins/reporting/server/routes/lib/get_user.ts index 31d2009346b5e..cc5c97c7a3552 100644 --- a/x-pack/plugins/reporting/server/routes/lib/get_user.ts +++ b/x-pack/plugins/reporting/server/routes/lib/get_user.ts @@ -8,8 +8,6 @@ import { KibanaRequest } from 'kibana/server'; import { SecurityPluginSetup } from '../../../../security/server'; -export function getUserFactory(security?: SecurityPluginSetup) { - return (request: KibanaRequest) => { - return security?.authc.getCurrentUser(request) ?? false; - }; +export function getUser(request: KibanaRequest, security?: SecurityPluginSetup) { + return security?.authc.getCurrentUser(request) ?? false; } diff --git a/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts b/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts index 9f90be09bccbc..419f2e118f039 100644 --- a/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts +++ b/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts @@ -16,93 +16,85 @@ interface JobResponseHandlerParams { docId: string; } -interface JobResponseHandlerOpts { - excludeContent?: boolean; -} - -export function downloadJobResponseHandlerFactory(reporting: ReportingCore) { +export async function downloadJobResponseHandler( + reporting: ReportingCore, + res: typeof kibanaResponseFactory, + validJobTypes: string[], + user: ReportingUser, + params: JobResponseHandlerParams +) { const jobsQuery = jobsQueryFactory(reporting); const getDocumentPayload = getDocumentPayloadFactory(reporting); - - return async function jobResponseHandler( - res: typeof kibanaResponseFactory, - validJobTypes: string[], - user: ReportingUser, - params: JobResponseHandlerParams, - opts: JobResponseHandlerOpts = {} - ) { - try { - const { docId } = params; - - const doc = await jobsQuery.get(user, docId); - if (!doc) { - return res.notFound(); - } - - if (!validJobTypes.includes(doc.jobtype)) { - return res.unauthorized({ - body: `Sorry, you are not authorized to download ${doc.jobtype} reports`, - }); - } - - const payload = await getDocumentPayload(doc); - - if (!payload.contentType || !ALLOWED_JOB_CONTENT_TYPES.includes(payload.contentType)) { - return res.badRequest({ - body: `Unsupported content-type of ${payload.contentType} specified by job output`, - }); - } - - return res.custom({ - body: typeof payload.content === 'string' ? Buffer.from(payload.content) : payload.content, - statusCode: payload.statusCode, - headers: { - ...payload.headers, - 'content-type': payload.contentType || '', - }, - }); - } catch (err) { - const { logger } = reporting.getPluginSetupDeps(); - logger.error(err); - } - }; -} - -export function deleteJobResponseHandlerFactory(reporting: ReportingCore) { - const jobsQuery = jobsQueryFactory(reporting); - - return async function deleteJobResponseHander( - res: typeof kibanaResponseFactory, - validJobTypes: string[], - user: ReportingUser, - params: JobResponseHandlerParams - ) { + try { const { docId } = params; - const doc = await jobsQuery.get(user, docId); + const doc = await jobsQuery.get(user, docId); if (!doc) { return res.notFound(); } - const { jobtype: jobType } = doc; - - if (!validJobTypes.includes(jobType)) { + if (!validJobTypes.includes(doc.jobtype)) { return res.unauthorized({ - body: `Sorry, you are not authorized to delete ${jobType} reports`, + body: `Sorry, you are not authorized to download ${doc.jobtype} reports`, }); } - try { - const docIndex = doc.index; - await jobsQuery.delete(docIndex, docId); - return res.ok({ - body: { deleted: true }, - }); - } catch (error) { - return res.customError({ - statusCode: error.statusCode, - body: error.message, + const payload = await getDocumentPayload(doc); + + if (!payload.contentType || !ALLOWED_JOB_CONTENT_TYPES.includes(payload.contentType)) { + return res.badRequest({ + body: `Unsupported content-type of ${payload.contentType} specified by job output`, }); } - }; + + return res.custom({ + body: typeof payload.content === 'string' ? Buffer.from(payload.content) : payload.content, + statusCode: payload.statusCode, + headers: { + ...payload.headers, + 'content-type': payload.contentType || '', + }, + }); + } catch (err) { + const { logger } = reporting.getPluginSetupDeps(); + logger.error(err); + } +} + +export async function deleteJobResponseHandler( + reporting: ReportingCore, + res: typeof kibanaResponseFactory, + validJobTypes: string[], + user: ReportingUser, + params: JobResponseHandlerParams +) { + const jobsQuery = jobsQueryFactory(reporting); + + const { docId } = params; + const doc = await jobsQuery.get(user, docId); + + if (!doc) { + return res.notFound(); + } + + const { jobtype: jobType } = doc; + + if (!validJobTypes.includes(jobType)) { + return res.unauthorized({ + body: `Sorry, you are not authorized to delete ${jobType} reports`, + }); + } + + try { + const docIndex = doc.index; + await jobsQuery.delete(docIndex, docId); + return res.ok({ + body: { deleted: true }, + }); + } catch (error) { + return res.customError({ + statusCode: error.statusCode, + body: error.message, + }); + } } From 65a5cb1476ee09c628b77e84b44d8b118a2fbbc9 Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 10 Aug 2021 22:36:43 -0700 Subject: [PATCH 065/104] fix newly introduced type error (#107593) --- .../cypress/integration/read_only_user/home.spec.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts index 371850796fd24..76461d49ba012 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts @@ -7,7 +7,6 @@ import url from 'url'; import archives_metadata from '../../fixtures/es_archiver/archives_metadata'; -import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; const { start, end } = archives_metadata['apm_8.0.0']; @@ -28,12 +27,6 @@ const apisToIntercept = [ ]; describe('Home page', () => { - before(() => { - // esArchiverLoad('apm_8.0.0'); - }); - after(() => { - // esArchiverUnload('apm_8.0.0'); - }); beforeEach(() => { cy.loginAsReadOnlyUser(); }); From 441fb796e208592e6301aba023891ce1ba27b4f3 Mon Sep 17 00:00:00 2001 From: spalger Date: Tue, 10 Aug 2021 23:49:47 -0700 Subject: [PATCH 066/104] skip flaky test (#108043) --- .../src/lib/docs/index_doc_records_stream.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts b/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts index 2aa71fd23a711..91cec7dc49094 100644 --- a/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts +++ b/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts @@ -99,7 +99,8 @@ const testRecords = [ }, ]; -it('indexes documents using the bulk client helper', async () => { +// FLAKY: https://github.com/elastic/kibana/issues/108043 +it.skip('indexes documents using the bulk client helper', async () => { const client = new MockClient(); client.helpers.bulk.mockImplementation(async () => {}); From ee10819ef0833b69d03ad6b8c908658305145707 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Wed, 11 Aug 2021 10:56:44 +0300 Subject: [PATCH 067/104] [Lens] fix do not submit invalid query in filtered metric (#107542) * [Lens] Do not submit invalid query in filtered metric Closes: #95611 * fix CI * fix PR comments * fix PR comments * fix PR comment * move closePopover to useCallback * add filter validation to utils/isColumnInvalid Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../dimension_panel/dimension_panel.test.tsx | 23 +++-- .../dimension_panel/filtering.tsx | 91 +++++++++++++------ .../definitions/filters/filters.tsx | 33 ++++--- .../public/indexpattern_datasource/utils.ts | 9 +- 4 files changed, 105 insertions(+), 51 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 5318255792641..b6d3a230d06f5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -7,7 +7,7 @@ import { ReactWrapper, ShallowWrapper } from 'enzyme'; import 'jest-canvas-mock'; -import React, { ChangeEvent, MouseEvent, ReactElement } from 'react'; +import React, { ChangeEvent, MouseEvent } from 'react'; import { act } from 'react-dom/test-utils'; import { EuiComboBox, @@ -16,7 +16,6 @@ import { EuiRange, EuiSelect, EuiButtonIcon, - EuiPopover, } from '@elastic/eui'; import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; import { @@ -33,7 +32,7 @@ import { documentField } from '../document_field'; import { OperationMetadata } from '../../types'; import { DateHistogramIndexPatternColumn } from '../operations/definitions/date_histogram'; import { getFieldByNameFactory } from '../pure_helpers'; -import { Filtering } from './filtering'; +import { Filtering, setFilter } from './filtering'; import { TimeShift } from './time_shift'; import { DimensionEditor } from './dimension_editor'; import { AdvancedOptions } from './advanced_options'; @@ -1541,9 +1540,13 @@ describe('IndexPatternDimensionEditorPanel', () => { {...getProps({ filter: { language: 'kuery', query: 'a: b' } })} /> ); + expect( - (wrapper.find(Filtering).find(EuiPopover).prop('children') as ReactElement).props.value - ).toEqual({ language: 'kuery', query: 'a: b' }); + wrapper + .find(Filtering) + .find('button[data-test-subj="indexPattern-filters-existingFilterTrigger"]') + .text() + ).toBe(`a: b`); }); it('should allow to set filter initially', () => { @@ -1609,11 +1612,15 @@ describe('IndexPatternDimensionEditorPanel', () => { const props = getProps({ filter: { language: 'kuery', query: 'a: b' }, }); + wrapper = mount(); - (wrapper.find(Filtering).find(EuiPopover).prop('children') as ReactElement).props.onChange({ - language: 'kuery', - query: 'c: d', + + act(() => { + const { updateLayer, columnId, layer } = wrapper.find(Filtering).props(); + + updateLayer(setFilter(columnId, layer, { language: 'kuery', query: 'c: d' })); }); + expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); expect(setState.mock.calls[0][0](props.state)).toEqual({ ...props.state, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/filtering.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/filtering.tsx index 68705ebf2d157..ddd9718839649 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/filtering.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/filtering.tsx @@ -4,16 +4,28 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { EuiButtonIcon, EuiLink, EuiPanel, EuiPopover } from '@elastic/eui'; -import { EuiFormRow, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import React, { useState, useEffect, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; -import React, { useState } from 'react'; -import { Query } from 'src/plugins/data/public'; +import { isEqual } from 'lodash'; +import { + EuiButtonIcon, + EuiLink, + EuiPanel, + EuiPopover, + EuiFormRow, + EuiFlexItem, + EuiFlexGroup, + EuiPopoverProps, +} from '@elastic/eui'; +import type { Query } from 'src/plugins/data/public'; import { IndexPatternColumn, operationDefinitionMap } from '../operations'; -import { isQueryValid } from '../operations/definitions/filters'; +import { validateQuery } from '../operations/definitions/filters'; import { QueryInput } from '../query_input'; -import { IndexPattern, IndexPatternLayer } from '../types'; +import type { IndexPattern, IndexPatternLayer } from '../types'; + +const filterByLabel = i18n.translate('xpack.lens.indexPattern.filterBy.label', { + defaultMessage: 'Filter by', +}); // to do: get the language from uiSettings export const defaultFilter: Query = { @@ -49,29 +61,49 @@ export function Filtering({ updateLayer: (newLayer: IndexPatternLayer) => void; isInitiallyOpen: boolean; }) { + const inputFilter = selectedColumn.filter; + const [queryInput, setQueryInput] = useState(inputFilter ?? defaultFilter); const [filterPopoverOpen, setFilterPopoverOpen] = useState(isInitiallyOpen); + + useEffect(() => { + const { isValid } = validateQuery(queryInput, indexPattern); + + if (isValid && !isEqual(inputFilter, queryInput)) { + updateLayer(setFilter(columnId, layer, queryInput)); + } + }, [columnId, layer, queryInput, indexPattern, updateLayer, inputFilter]); + + const onClosePopup: EuiPopoverProps['closePopover'] = useCallback(() => { + setFilterPopoverOpen(false); + if (inputFilter) { + setQueryInput(inputFilter); + } + }, [inputFilter]); + const selectedOperation = operationDefinitionMap[selectedColumn.operationType]; - if (!selectedOperation.filterable || !selectedColumn.filter) { + + if (!selectedOperation.filterable || !inputFilter) { return null; } - const isInvalid = !isQueryValid(selectedColumn.filter, indexPattern); + const { isValid: isInputFilterValid } = validateQuery(inputFilter, indexPattern); + const { isValid: isQueryInputValid, error: queryInputError } = validateQuery( + queryInput, + indexPattern + ); return ( { - setFilterPopoverOpen(false); - }} + closePopover={onClosePopup} anchorClassName="eui-fullWidth" panelClassName="lnsIndexPatternDimensionEditor__filtersEditor" button={ @@ -85,12 +117,12 @@ export function Filtering({ onClick={() => { setFilterPopoverOpen(!filterPopoverOpen); }} - color={isInvalid ? 'danger' : 'text'} + color={isInputFilterValid ? 'text' : 'danger'} title={i18n.translate('xpack.lens.indexPattern.filterBy.clickToEdit', { defaultMessage: 'Click to edit', })} > - {selectedColumn.filter.query || + {inputFilter.query || i18n.translate('xpack.lens.indexPattern.filterBy.emptyFilterQuery', { defaultMessage: '(empty)', })} @@ -113,16 +145,21 @@ export function Filtering({ } > - { - updateLayer(setFilter(columnId, layer, newQuery)); - }} - isInvalid={false} - onSubmit={() => {}} - /> + + {}} + /> + diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx index 7f718498bd95b..84319d8f8e433 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx @@ -6,22 +6,17 @@ */ import './filters.scss'; - import React, { MouseEventHandler, useState } from 'react'; +import { fromKueryExpression, luceneStringToDsl, toElasticsearchQuery } from '@kbn/es-query'; import { omit } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiLink, htmlIdGenerator } from '@elastic/eui'; import { updateColumnParam } from '../../layer_helpers'; -import { OperationDefinition } from '../index'; -import { BaseIndexPatternColumn } from '../column_types'; +import type { OperationDefinition } from '../index'; +import type { BaseIndexPatternColumn } from '../column_types'; import { FilterPopover } from './filter_popover'; -import { IndexPattern } from '../../../types'; -import { - AggFunctionsMapping, - Query, - esKuery, - esQuery, -} from '../../../../../../../../src/plugins/data/public'; +import type { IndexPattern } from '../../../types'; +import type { AggFunctionsMapping, Query } from '../../../../../../../../src/plugins/data/public'; import { queryFilterToAst } from '../../../../../../../../src/plugins/data/common'; import { buildExpressionFunction } from '../../../../../../../../src/plugins/expressions/public'; import { NewBucketButton, DragDropBuckets, DraggableBucketContainer } from '../shared_components'; @@ -58,19 +53,27 @@ const defaultFilter: Filter = { label: '', }; -export const isQueryValid = (input: Query, indexPattern: IndexPattern) => { +export const validateQuery = (input: Query, indexPattern: IndexPattern) => { + let isValid = true; + let error: string | undefined; + try { if (input.language === 'kuery') { - esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(input.query), indexPattern); + toElasticsearchQuery(fromKueryExpression(input.query), indexPattern); } else { - esQuery.luceneStringToDsl(input.query); + luceneStringToDsl(input.query); } - return true; } catch (e) { - return false; + isValid = false; + error = e.message; } + + return { isValid, error }; }; +export const isQueryValid = (input: Query, indexPattern: IndexPattern) => + validateQuery(input, indexPattern).isValid; + export interface FiltersIndexPatternColumn extends BaseIndexPatternColumn { operationType: typeof OPERATION_NAME; params: { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts index a9e24c70ab8ac..7d225d730a757 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.ts @@ -15,6 +15,7 @@ import type { import { operationDefinitionMap, IndexPatternColumn } from './operations'; import { getInvalidFieldMessage } from './operations/definitions/helpers'; +import { isQueryValid } from './operations/definitions/filters'; /** * Normalizes the specified operation type. (e.g. document operations @@ -68,7 +69,13 @@ export function isColumnInvalid( operationDefinitionMap ); - return (operationErrorMessages && operationErrorMessages.length > 0) || referencesHaveErrors; + const filterHasError = column.filter ? !isQueryValid(column.filter, indexPattern) : false; + + return ( + (operationErrorMessages && operationErrorMessages.length > 0) || + referencesHaveErrors || + filterHasError + ); } function getReferencesErrors( From a444d8a4abe3481f2c163308982976bfd7bf7b8f Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Wed, 11 Aug 2021 01:15:38 -0700 Subject: [PATCH 068/104] [Reporting] Add lenience to a test on the order of asserted logs (#108135) --- .../server/routes/diagnostic/browser.test.ts | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts index 37361fc91392c..1652f91cd11ad 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts @@ -88,7 +88,7 @@ describe('POST /diagnose/browser', () => { await server.start(); mockedCreateInterface.mockImplementation(() => ({ - addEventListener: (e: string, cb: any) => setTimeout(() => cb(devtoolMessage), 0), + addEventListener: (_e: string, cb: any) => setTimeout(() => cb(devtoolMessage), 0), removeEventListener: jest.fn(), removeAllListeners: jest.fn(), close: jest.fn(), @@ -110,7 +110,7 @@ describe('POST /diagnose/browser', () => { await server.start(); mockedCreateInterface.mockImplementation(() => ({ - addEventListener: (e: string, cb: any) => setTimeout(() => cb(logs), 0), + addEventListener: (_e: string, cb: any) => setTimeout(() => cb(logs), 0), removeEventListener: jest.fn(), removeAllListeners: jest.fn(), close: jest.fn(), @@ -146,7 +146,7 @@ describe('POST /diagnose/browser', () => { await server.start(); mockedCreateInterface.mockImplementation(() => ({ - addEventListener: (e: string, cb: any) => { + addEventListener: (_e: string, cb: any) => { setTimeout(() => cb(devtoolMessage), 0); setTimeout(() => cb(fontNotFoundMessage), 0); }, @@ -186,7 +186,7 @@ describe('POST /diagnose/browser', () => { await server.start(); mockedCreateInterface.mockImplementation(() => ({ - addEventListener: (e: string, cb: any) => { + addEventListener: (_e: string, cb: any) => { setTimeout(() => cb(fontNotFoundMessage), 0); }, removeEventListener: jest.fn(), @@ -209,17 +209,16 @@ describe('POST /diagnose/browser', () => { .post('/api/reporting/diagnose/browser') .expect(200) .then(({ body }) => { - expect(body).toMatchInlineSnapshot(` - Object { - "help": Array [ - "The browser couldn't locate a default font. Please see https://www.elastic.co/guide/en/kibana/current/reporting-troubleshooting.html#reporting-troubleshooting-system-dependencies to fix this issue.", - ], - "logs": "Could not find the default font - Browser exited abnormally during startup - ", - "success": false, - } + const helpArray = [...body.help]; + helpArray.sort(); + expect(helpArray).toMatchInlineSnapshot(` + Array [ + "The browser couldn't locate a default font. Please see https://www.elastic.co/guide/en/kibana/current/reporting-troubleshooting.html#reporting-troubleshooting-system-dependencies to fix this issue.", + ] `); + expect(body.logs).toMatch(/Could not find the default font/); + expect(body.logs).toMatch(/Browser exited abnormally during startup/); + expect(body.success).toBe(false); }); }); @@ -242,7 +241,7 @@ describe('POST /diagnose/browser', () => { })); mockedCreateInterface.mockImplementation(() => ({ - addEventListener: (e: string, cb: any) => setTimeout(() => cb(devtoolMessage), 0), + addEventListener: (_e: string, cb: any) => setTimeout(() => cb(devtoolMessage), 0), removeEventListener: jest.fn(), removeAllListeners: createInterfaceListenersMock, close: createInterfaceCloseMock, From 86c17daec2899bb1a949f0e9dd2075c4bc02b1b0 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 11 Aug 2021 10:46:35 +0200 Subject: [PATCH 069/104] [ML] APM Latency Correlations: Field/value candidates prioritization (#107370) - Makes sure fields defined in `FIELDS_TO_ADD_AS_CANDIDATE` and prefixed with one of `FIELD_PREFIX_TO_ADD_AS_CANDIDATE` get queried first when retrieving the `correlation` and `ks-test` value. - Correctly consider the `includeFrozen` parameter. - The bulk of the PR is a refactor: - Moves `query_*` files to `queries` directory - Introduces `asyncSearchServiceStateProvider` to manage the state of the async search service in isolation so that we no longer mutate individual vars or plain objects. - Introduces `asyncSearchServiceLogProvider` and extends the log to not only store messages but original error messages retrieved from ES too. - Refactors some more functions in separate files and adds unit tests. - Removes some deprecated code no longer needed. --- .../search_strategies/correlations/types.ts | 2 +- .../correlations/async_search_service.ts | 250 +++++++----------- .../async_search_service_log.test.ts | 30 +++ .../correlations/async_search_service_log.ts | 36 +++ .../async_search_service_state.test.ts | 62 +++++ .../async_search_service_state.ts | 115 ++++++++ .../correlations/constants.ts | 3 + .../get_prioritized_field_value_pairs.test.ts | 70 +++++ .../get_prioritized_field_value_pairs.ts | 33 +++ .../get_query_with_params.test.ts | 15 +- .../{ => queries}/get_query_with_params.ts | 10 +- .../queries/get_request_base.test.ts | 35 +++ .../correlations/queries/get_request_base.ts | 18 ++ .../correlations/queries/index.ts | 15 ++ .../{ => queries}/query_correlation.test.ts | 7 +- .../{ => queries}/query_correlation.ts | 7 +- .../query_field_candidates.test.ts | 12 +- .../{ => queries}/query_field_candidates.ts | 22 +- .../query_field_value_pairs.test.ts | 22 +- .../queries/query_field_value_pairs.ts | 124 +++++++++ .../{ => queries}/query_fractions.test.ts | 7 +- .../{ => queries}/query_fractions.ts | 9 +- .../{ => queries}/query_histogram.test.ts | 11 +- .../{ => queries}/query_histogram.ts | 29 +- .../query_histogram_interval.test.ts | 9 +- .../{ => queries}/query_histogram_interval.ts | 7 +- .../query_histogram_range_steps.test.ts | 9 +- .../query_histogram_range_steps.ts | 7 +- .../query_histograms_generator.test.ts | 134 ++++++++++ .../queries/query_histograms_generator.ts | 97 +++++++ .../{ => queries}/query_percentiles.test.ts | 9 +- .../{ => queries}/query_percentiles.ts | 9 +- .../{ => queries}/query_ranges.test.ts | 9 +- .../{ => queries}/query_ranges.ts | 7 +- .../correlations/query_field_value_pairs.ts | 89 ------- ...> compute_expectations_and_ranges.test.ts} | 2 +- ....ts => compute_expectations_and_ranges.ts} | 0 .../utils/current_time_as_string.test.ts | 24 ++ .../utils/current_time_as_string.ts | 8 + .../utils/has_prefix_to_include.test.ts | 28 ++ .../utils/has_prefix_to_include.ts | 14 + .../correlations/utils/index.ts | 5 +- .../correlations/utils/math_utils.test.ts | 26 -- .../correlations/utils/math_utils.ts | 70 ----- 44 files changed, 1089 insertions(+), 418 deletions(-) create mode 100644 x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_log.test.ts create mode 100644 x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_log.ts create mode 100644 x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_state.test.ts create mode 100644 x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_state.ts create mode 100644 x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_prioritized_field_value_pairs.test.ts create mode 100644 x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_prioritized_field_value_pairs.ts rename x-pack/plugins/apm/server/lib/search_strategies/correlations/{ => queries}/get_query_with_params.test.ts (90%) rename x-pack/plugins/apm/server/lib/search_strategies/correlations/{ => queries}/get_query_with_params.ts (83%) create mode 100644 x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_request_base.test.ts create mode 100644 x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_request_base.ts create mode 100644 x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/index.ts rename x-pack/plugins/apm/server/lib/search_strategies/correlations/{ => queries}/query_correlation.test.ts (97%) rename x-pack/plugins/apm/server/lib/search_strategies/correlations/{ => queries}/query_correlation.ts (92%) rename x-pack/plugins/apm/server/lib/search_strategies/correlations/{ => queries}/query_field_candidates.test.ts (94%) rename x-pack/plugins/apm/server/lib/search_strategies/correlations/{ => queries}/query_field_candidates.ts (85%) rename x-pack/plugins/apm/server/lib/search_strategies/correlations/{ => queries}/query_field_value_pairs.test.ts (81%) create mode 100644 x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_value_pairs.ts rename x-pack/plugins/apm/server/lib/search_strategies/correlations/{ => queries}/query_fractions.test.ts (95%) rename x-pack/plugins/apm/server/lib/search_strategies/correlations/{ => queries}/query_fractions.ts (86%) rename x-pack/plugins/apm/server/lib/search_strategies/correlations/{ => queries}/query_histogram.test.ts (92%) rename x-pack/plugins/apm/server/lib/search_strategies/correlations/{ => queries}/query_histogram.ts (74%) rename x-pack/plugins/apm/server/lib/search_strategies/correlations/{ => queries}/query_histogram_interval.test.ts (93%) rename x-pack/plugins/apm/server/lib/search_strategies/correlations/{ => queries}/query_histogram_interval.ts (84%) rename x-pack/plugins/apm/server/lib/search_strategies/correlations/{ => queries}/query_histogram_range_steps.test.ts (94%) rename x-pack/plugins/apm/server/lib/search_strategies/correlations/{ => queries}/query_histogram_range_steps.ts (87%) create mode 100644 x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histograms_generator.test.ts create mode 100644 x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histograms_generator.ts rename x-pack/plugins/apm/server/lib/search_strategies/correlations/{ => queries}/query_percentiles.test.ts (94%) rename x-pack/plugins/apm/server/lib/search_strategies/correlations/{ => queries}/query_percentiles.ts (87%) rename x-pack/plugins/apm/server/lib/search_strategies/correlations/{ => queries}/query_ranges.test.ts (94%) rename x-pack/plugins/apm/server/lib/search_strategies/correlations/{ => queries}/query_ranges.ts (88%) delete mode 100644 x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.ts rename x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/{aggregation_utils.test.ts => compute_expectations_and_ranges.test.ts} (96%) rename x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/{aggregation_utils.ts => compute_expectations_and_ranges.ts} (100%) create mode 100644 x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/current_time_as_string.test.ts create mode 100644 x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/current_time_as_string.ts create mode 100644 x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/has_prefix_to_include.test.ts create mode 100644 x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/has_prefix_to_include.ts delete mode 100644 x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/math_utils.test.ts delete mode 100644 x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/math_utils.ts diff --git a/x-pack/plugins/apm/common/search_strategies/correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/correlations/types.ts index 6e1fd115aace1..bb697f0984335 100644 --- a/x-pack/plugins/apm/common/search_strategies/correlations/types.ts +++ b/x-pack/plugins/apm/common/search_strategies/correlations/types.ts @@ -32,6 +32,7 @@ export interface SearchServiceParams { export interface SearchServiceFetchParams extends SearchServiceParams { index: string; + includeFrozen?: boolean; } export interface SearchServiceValue { @@ -50,5 +51,4 @@ export interface AsyncSearchProviderProgress { loadedFieldCanditates: number; loadedFieldValuePairs: number; loadedHistograms: number; - getOverallProgress: () => number; } diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts index 90d24b6587f41..ae42a0c94fe9c 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service.ts @@ -5,28 +5,25 @@ * 2.0. */ -import { shuffle, range } from 'lodash'; +import { range } from 'lodash'; import type { ElasticsearchClient } from 'src/core/server'; -import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; -import { fetchTransactionDurationFieldCandidates } from './query_field_candidates'; -import { fetchTransactionDurationFieldValuePairs } from './query_field_value_pairs'; -import { fetchTransactionDurationPercentiles } from './query_percentiles'; -import { fetchTransactionDurationCorrelation } from './query_correlation'; -import { fetchTransactionDurationHistogramRangeSteps } from './query_histogram_range_steps'; -import { fetchTransactionDurationRanges, HistogramItem } from './query_ranges'; import type { - AsyncSearchProviderProgress, SearchServiceParams, SearchServiceFetchParams, - SearchServiceValue, } from '../../../../common/search_strategies/correlations/types'; -import { computeExpectationsAndRanges } from './utils/aggregation_utils'; -import { fetchTransactionDurationFractions } from './query_fractions'; - -const CORRELATION_THRESHOLD = 0.3; -const KS_TEST_THRESHOLD = 0.1; - -const currentTimeAsString = () => new Date().toISOString(); +import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; +import { + fetchTransactionDurationFieldCandidates, + fetchTransactionDurationFieldValuePairs, + fetchTransactionDurationFractions, + fetchTransactionDurationPercentiles, + fetchTransactionDurationHistograms, + fetchTransactionDurationHistogramRangeSteps, + fetchTransactionDurationRanges, +} from './queries'; +import { computeExpectationsAndRanges } from './utils'; +import { asyncSearchServiceLogProvider } from './async_search_service_log'; +import { asyncSearchServiceStateProvider } from './async_search_service_state'; export const asyncSearchServiceProvider = ( esClient: ElasticsearchClient, @@ -34,40 +31,11 @@ export const asyncSearchServiceProvider = ( searchServiceParams: SearchServiceParams, includeFrozen: boolean ) => { - let isCancelled = false; - let isRunning = true; - let error: Error; - let ccsWarning = false; - const log: string[] = []; - const logMessage = (message: string) => - log.push(`${currentTimeAsString()}: ${message}`); - - const progress: AsyncSearchProviderProgress = { - started: Date.now(), - loadedHistogramStepsize: 0, - loadedOverallHistogram: 0, - loadedFieldCanditates: 0, - loadedFieldValuePairs: 0, - loadedHistograms: 0, - getOverallProgress: () => - progress.loadedHistogramStepsize * 0.025 + - progress.loadedOverallHistogram * 0.025 + - progress.loadedFieldCanditates * 0.025 + - progress.loadedFieldValuePairs * 0.025 + - progress.loadedHistograms * 0.9, - }; + const { addLogMessage, getLogMessages } = asyncSearchServiceLogProvider(); - const values: SearchServiceValue[] = []; - let overallHistogram: HistogramItem[] | undefined; + const state = asyncSearchServiceStateProvider(); - let percentileThresholdValue: number; - - const cancel = () => { - logMessage(`Service cancelled.`); - isCancelled = true; - }; - - const fetchCorrelations = async () => { + async function fetchCorrelations() { let params: SearchServiceFetchParams | undefined; try { @@ -75,6 +43,7 @@ export const asyncSearchServiceProvider = ( params = { ...searchServiceParams, index: indices['apm_oss.transactionIndices'], + includeFrozen, }; // 95th percentile to be displayed as a marker in the log log chart @@ -86,24 +55,27 @@ export const asyncSearchServiceProvider = ( params, params.percentileThreshold ? [params.percentileThreshold] : undefined ); - percentileThresholdValue = + const percentileThresholdValue = percentileThreshold[`${params.percentileThreshold}.0`]; + state.setPercentileThresholdValue(percentileThresholdValue); - logMessage( + addLogMessage( `Fetched ${params.percentileThreshold}th percentile value of ${percentileThresholdValue} based on ${totalDocs} documents.` ); // finish early if we weren't able to identify the percentileThresholdValue. if (percentileThresholdValue === undefined) { - logMessage( + addLogMessage( `Abort service since percentileThresholdValue could not be determined.` ); - progress.loadedHistogramStepsize = 1; - progress.loadedOverallHistogram = 1; - progress.loadedFieldCanditates = 1; - progress.loadedFieldValuePairs = 1; - progress.loadedHistograms = 1; - isRunning = false; + state.setProgress({ + loadedHistogramStepsize: 1, + loadedOverallHistogram: 1, + loadedFieldCanditates: 1, + loadedFieldValuePairs: 1, + loadedHistograms: 1, + }); + state.setIsRunning(false); return; } @@ -111,12 +83,12 @@ export const asyncSearchServiceProvider = ( esClient, params ); - progress.loadedHistogramStepsize = 1; + state.setProgress({ loadedHistogramStepsize: 1 }); - logMessage(`Loaded histogram range steps.`); + addLogMessage(`Loaded histogram range steps.`); - if (isCancelled) { - isRunning = false; + if (state.getIsCancelled()) { + state.setIsRunning(false); return; } @@ -125,13 +97,13 @@ export const asyncSearchServiceProvider = ( params, histogramRangeSteps ); - progress.loadedOverallHistogram = 1; - overallHistogram = overallLogHistogramChartData; + state.setProgress({ loadedOverallHistogram: 1 }); + state.setOverallHistogram(overallLogHistogramChartData); - logMessage(`Loaded overall histogram chart data.`); + addLogMessage(`Loaded overall histogram chart data.`); - if (isCancelled) { - isRunning = false; + if (state.getIsCancelled()) { + state.setIsRunning(false); return; } @@ -142,10 +114,10 @@ export const asyncSearchServiceProvider = ( } = await fetchTransactionDurationPercentiles(esClient, params, percents); const percentiles = Object.values(percentilesRecords); - logMessage(`Loaded percentiles.`); + addLogMessage(`Loaded percentiles.`); - if (isCancelled) { - isRunning = false; + if (state.getIsCancelled()) { + state.setIsRunning(false); return; } @@ -154,21 +126,22 @@ export const asyncSearchServiceProvider = ( params ); - logMessage(`Identified ${fieldCandidates.length} fieldCandidates.`); + addLogMessage(`Identified ${fieldCandidates.length} fieldCandidates.`); - progress.loadedFieldCanditates = 1; + state.setProgress({ loadedFieldCanditates: 1 }); const fieldValuePairs = await fetchTransactionDurationFieldValuePairs( esClient, params, fieldCandidates, - progress + state, + addLogMessage ); - logMessage(`Identified ${fieldValuePairs.length} fieldValuePairs.`); + addLogMessage(`Identified ${fieldValuePairs.length} fieldValuePairs.`); - if (isCancelled) { - isRunning = false; + if (state.getIsCancelled()) { + state.setIsRunning(false); return; } @@ -181,114 +154,75 @@ export const asyncSearchServiceProvider = ( totalDocCount, } = await fetchTransactionDurationFractions(esClient, params, ranges); - logMessage(`Loaded fractions and totalDocCount of ${totalDocCount}.`); - - async function* fetchTransactionDurationHistograms() { - for (const item of shuffle(fieldValuePairs)) { - if (params === undefined || item === undefined || isCancelled) { - isRunning = false; - return; - } - - // If one of the fields have an error - // We don't want to stop the whole process - try { - const { - correlation, - ksTest, - } = await fetchTransactionDurationCorrelation( - esClient, - params, - expectations, - ranges, - fractions, - totalDocCount, - item.field, - item.value - ); - - if (isCancelled) { - isRunning = false; - return; - } - - if ( - correlation !== null && - correlation > CORRELATION_THRESHOLD && - ksTest !== null && - ksTest < KS_TEST_THRESHOLD - ) { - const logHistogram = await fetchTransactionDurationRanges( - esClient, - params, - histogramRangeSteps, - item.field, - item.value - ); - yield { - ...item, - correlation, - ksTest, - histogram: logHistogram, - }; - } else { - yield undefined; - } - } catch (e) { - // don't fail the whole process for individual correlation queries, - // just add the error to the internal log and check if we'd want to set the - // cross-cluster search compatibility warning to true. - logMessage( - `Failed to fetch correlation/kstest for '${item.field}/${item.value}'` - ); - if (params?.index.includes(':')) { - ccsWarning = true; - } - yield undefined; - } - } - } + addLogMessage(`Loaded fractions and totalDocCount of ${totalDocCount}.`); let loadedHistograms = 0; - for await (const item of fetchTransactionDurationHistograms()) { + for await (const item of fetchTransactionDurationHistograms( + esClient, + addLogMessage, + params, + state, + expectations, + ranges, + fractions, + histogramRangeSteps, + totalDocCount, + fieldValuePairs + )) { if (item !== undefined) { - values.push(item); + state.addValue(item); } loadedHistograms++; - progress.loadedHistograms = loadedHistograms / fieldValuePairs.length; + state.setProgress({ + loadedHistograms: loadedHistograms / fieldValuePairs.length, + }); } - logMessage( - `Identified ${values.length} significant correlations out of ${fieldValuePairs.length} field/value pairs.` + addLogMessage( + `Identified ${ + state.getState().values.length + } significant correlations out of ${ + fieldValuePairs.length + } field/value pairs.` ); } catch (e) { - error = e; + state.setError(e); } - if (error !== undefined && params?.index.includes(':')) { - ccsWarning = true; + if (state.getState().error !== undefined && params?.index.includes(':')) { + state.setCcsWarning(true); } - isRunning = false; - }; + state.setIsRunning(false); + } fetchCorrelations(); return () => { - const sortedValues = values.sort((a, b) => b.correlation - a.correlation); + const { + ccsWarning, + error, + isRunning, + overallHistogram, + percentileThresholdValue, + progress, + } = state.getState(); return { ccsWarning, error, - log, + log: getLogMessages(), isRunning, - loaded: Math.round(progress.getOverallProgress() * 100), + loaded: Math.round(state.getOverallProgress() * 100), overallHistogram, started: progress.started, total: 100, - values: sortedValues, + values: state.getValuesSortedByCorrelation(), percentileThresholdValue, - cancel, + cancel: () => { + addLogMessage(`Service cancelled.`); + state.setIsCancelled(true); + }, }; }; }; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_log.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_log.test.ts new file mode 100644 index 0000000000000..bbeb8435e61bf --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_log.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { asyncSearchServiceLogProvider } from './async_search_service_log'; + +describe('async search service', () => { + describe('asyncSearchServiceLogProvider', () => { + it('adds and retrieves messages from the log', async () => { + const { addLogMessage, getLogMessages } = asyncSearchServiceLogProvider(); + + const mockDate = new Date(1392202800000); + // @ts-ignore ignore the mockImplementation callback error + const spy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate); + + addLogMessage('the first message'); + addLogMessage('the second message'); + + expect(getLogMessages()).toEqual([ + '2014-02-12T11:00:00.000Z: the first message', + '2014-02-12T11:00:00.000Z: the second message', + ]); + + spy.mockRestore(); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_log.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_log.ts new file mode 100644 index 0000000000000..e69d2f55b6c56 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_log.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { currentTimeAsString } from './utils'; + +interface LogMessage { + timestamp: string; + message: string; + error?: string; +} + +export const asyncSearchServiceLogProvider = () => { + const log: LogMessage[] = []; + + function addLogMessage(message: string, error?: string) { + log.push({ + timestamp: currentTimeAsString(), + message, + ...(error !== undefined ? { error } : {}), + }); + } + + function getLogMessages() { + return log.map((l) => `${l.timestamp}: ${l.message}`); + } + + return { addLogMessage, getLogMessages }; +}; + +export type AsyncSearchServiceLog = ReturnType< + typeof asyncSearchServiceLogProvider +>; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_state.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_state.test.ts new file mode 100644 index 0000000000000..cfa1bf2a5ad71 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_state.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { asyncSearchServiceStateProvider } from './async_search_service_state'; + +describe('async search service', () => { + describe('asyncSearchServiceStateProvider', () => { + it('initializes with default state', () => { + const state = asyncSearchServiceStateProvider(); + const defaultState = state.getState(); + const defaultProgress = state.getOverallProgress(); + + expect(defaultState.ccsWarning).toBe(false); + expect(defaultState.error).toBe(undefined); + expect(defaultState.isCancelled).toBe(false); + expect(defaultState.isRunning).toBe(true); + expect(defaultState.overallHistogram).toBe(undefined); + expect(defaultState.progress.loadedFieldCanditates).toBe(0); + expect(defaultState.progress.loadedFieldValuePairs).toBe(0); + expect(defaultState.progress.loadedHistogramStepsize).toBe(0); + expect(defaultState.progress.loadedHistograms).toBe(0); + expect(defaultState.progress.loadedOverallHistogram).toBe(0); + expect(defaultState.progress.started > 0).toBe(true); + + expect(defaultProgress).toBe(0); + }); + + it('returns updated state', () => { + const state = asyncSearchServiceStateProvider(); + + state.setCcsWarning(true); + state.setError(new Error('the-error-message')); + state.setIsCancelled(true); + state.setIsRunning(false); + state.setOverallHistogram([{ key: 1392202800000, doc_count: 1234 }]); + state.setProgress({ loadedHistograms: 0.5 }); + + const updatedState = state.getState(); + const updatedProgress = state.getOverallProgress(); + + expect(updatedState.ccsWarning).toBe(true); + expect(updatedState.error?.message).toBe('the-error-message'); + expect(updatedState.isCancelled).toBe(true); + expect(updatedState.isRunning).toBe(false); + expect(updatedState.overallHistogram).toEqual([ + { key: 1392202800000, doc_count: 1234 }, + ]); + expect(updatedState.progress.loadedFieldCanditates).toBe(0); + expect(updatedState.progress.loadedFieldValuePairs).toBe(0); + expect(updatedState.progress.loadedHistogramStepsize).toBe(0); + expect(updatedState.progress.loadedHistograms).toBe(0.5); + expect(updatedState.progress.loadedOverallHistogram).toBe(0); + expect(updatedState.progress.started > 0).toBe(true); + + expect(updatedProgress).toBe(0.45); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_state.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_state.ts new file mode 100644 index 0000000000000..d0aac8987e070 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/async_search_service_state.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + AsyncSearchProviderProgress, + SearchServiceValue, +} from '../../../../common/search_strategies/correlations/types'; + +import { HistogramItem } from './queries'; + +export const asyncSearchServiceStateProvider = () => { + let ccsWarning = false; + function setCcsWarning(d: boolean) { + ccsWarning = d; + } + + let error: Error; + function setError(d: Error) { + error = d; + } + + let isCancelled = false; + function getIsCancelled() { + return isCancelled; + } + function setIsCancelled(d: boolean) { + isCancelled = d; + } + + let isRunning = true; + function setIsRunning(d: boolean) { + isRunning = d; + } + + let overallHistogram: HistogramItem[] | undefined; + function setOverallHistogram(d: HistogramItem[]) { + overallHistogram = d; + } + + let percentileThresholdValue: number; + function setPercentileThresholdValue(d: number) { + percentileThresholdValue = d; + } + + let progress: AsyncSearchProviderProgress = { + started: Date.now(), + loadedHistogramStepsize: 0, + loadedOverallHistogram: 0, + loadedFieldCanditates: 0, + loadedFieldValuePairs: 0, + loadedHistograms: 0, + }; + function getOverallProgress() { + return ( + progress.loadedHistogramStepsize * 0.025 + + progress.loadedOverallHistogram * 0.025 + + progress.loadedFieldCanditates * 0.025 + + progress.loadedFieldValuePairs * 0.025 + + progress.loadedHistograms * 0.9 + ); + } + function setProgress( + d: Partial> + ) { + progress = { + ...progress, + ...d, + }; + } + + const values: SearchServiceValue[] = []; + function addValue(d: SearchServiceValue) { + values.push(d); + } + + function getValuesSortedByCorrelation() { + return values.sort((a, b) => b.correlation - a.correlation); + } + + function getState() { + return { + ccsWarning, + error, + isCancelled, + isRunning, + overallHistogram, + percentileThresholdValue, + progress, + values, + }; + } + + return { + addValue, + getIsCancelled, + getOverallProgress, + getState, + getValuesSortedByCorrelation, + setCcsWarning, + setError, + setIsCancelled, + setIsRunning, + setOverallHistogram, + setPercentileThresholdValue, + setProgress, + }; +}; + +export type AsyncSearchServiceState = ReturnType< + typeof asyncSearchServiceStateProvider +>; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/constants.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/constants.ts index 5420479bfffb7..6b96b6b9d2131 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/constants.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/constants.ts @@ -76,3 +76,6 @@ export const PERCENTILES_STEP = 2; export const TERMS_SIZE = 20; export const SIGNIFICANT_FRACTION = 3; export const SIGNIFICANT_VALUE_DIGITS = 3; + +export const CORRELATION_THRESHOLD = 0.3; +export const KS_TEST_THRESHOLD = 0.1; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_prioritized_field_value_pairs.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_prioritized_field_value_pairs.test.ts new file mode 100644 index 0000000000000..dc11b4860a8b6 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_prioritized_field_value_pairs.test.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getPrioritizedFieldValuePairs } from './get_prioritized_field_value_pairs'; + +describe('correlations', () => { + describe('getPrioritizedFieldValuePairs', () => { + it('returns fields without prioritization in the same order', () => { + const fieldValuePairs = [ + { field: 'the-field-1', value: 'the-value-1' }, + { field: 'the-field-2', value: 'the-value-2' }, + ]; + const prioritziedFieldValuePairs = getPrioritizedFieldValuePairs( + fieldValuePairs + ); + expect(prioritziedFieldValuePairs.map((d) => d.field)).toEqual([ + 'the-field-1', + 'the-field-2', + ]); + }); + + it('returns fields with already sorted prioritization in the same order', () => { + const fieldValuePairs = [ + { field: 'service.version', value: 'the-value-1' }, + { field: 'the-field-2', value: 'the-value-2' }, + ]; + const prioritziedFieldValuePairs = getPrioritizedFieldValuePairs( + fieldValuePairs + ); + expect(prioritziedFieldValuePairs.map((d) => d.field)).toEqual([ + 'service.version', + 'the-field-2', + ]); + }); + + it('returns fields with unsorted prioritization in the corrected order', () => { + const fieldValuePairs = [ + { field: 'the-field-1', value: 'the-value-1' }, + { field: 'service.version', value: 'the-value-2' }, + ]; + const prioritziedFieldValuePairs = getPrioritizedFieldValuePairs( + fieldValuePairs + ); + expect(prioritziedFieldValuePairs.map((d) => d.field)).toEqual([ + 'service.version', + 'the-field-1', + ]); + }); + + it('considers prefixes when sorting', () => { + const fieldValuePairs = [ + { field: 'the-field-1', value: 'the-value-1' }, + { field: 'service.version', value: 'the-value-2' }, + { field: 'cloud.the-field-3', value: 'the-value-3' }, + ]; + const prioritziedFieldValuePairs = getPrioritizedFieldValuePairs( + fieldValuePairs + ); + expect(prioritziedFieldValuePairs.map((d) => d.field)).toEqual([ + 'service.version', + 'cloud.the-field-3', + 'the-field-1', + ]); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_prioritized_field_value_pairs.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_prioritized_field_value_pairs.ts new file mode 100644 index 0000000000000..ddfd87c83f9f3 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_prioritized_field_value_pairs.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FIELDS_TO_ADD_AS_CANDIDATE } from '../constants'; +import { hasPrefixToInclude } from '../utils'; + +import type { FieldValuePairs } from './query_field_value_pairs'; + +export const getPrioritizedFieldValuePairs = ( + fieldValuePairs: FieldValuePairs +) => { + const prioritizedFields = [...FIELDS_TO_ADD_AS_CANDIDATE]; + + return fieldValuePairs.sort((a, b) => { + const hasPrefixA = hasPrefixToInclude(a.field); + const hasPrefixB = hasPrefixToInclude(b.field); + + const includesA = prioritizedFields.includes(a.field); + const includesB = prioritizedFields.includes(b.field); + + if ((includesA || hasPrefixA) && !includesB && !hasPrefixB) { + return -1; + } else if (!includesA && !hasPrefixA && (includesB || hasPrefixB)) { + return 1; + } + + return 0; + }); +}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.test.ts similarity index 90% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.test.ts index 016355b3a6415..c27757b4fae69 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.test.ts @@ -11,7 +11,12 @@ describe('correlations', () => { describe('getQueryWithParams', () => { it('returns the most basic query filtering on processor.event=transaction', () => { const query = getQueryWithParams({ - params: { index: 'apm-*', start: '2020', end: '2021' }, + params: { + index: 'apm-*', + start: '2020', + end: '2021', + includeFrozen: false, + }, }); expect(query).toEqual({ bool: { @@ -41,6 +46,7 @@ describe('correlations', () => { end: '2021', environment: 'dev', percentileThresholdValue: 75, + includeFrozen: false, }, }); expect(query).toEqual({ @@ -89,7 +95,12 @@ describe('correlations', () => { it('returns a query considering a custom field/value pair', () => { const query = getQueryWithParams({ - params: { index: 'apm-*', start: '2020', end: '2021' }, + params: { + index: 'apm-*', + start: '2020', + end: '2021', + includeFrozen: false, + }, fieldName: 'actualFieldName', fieldValue: 'actualFieldValue', }); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.ts similarity index 83% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.ts index aeb76c37e526c..f28556f7a90b5 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/get_query_with_params.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_query_with_params.ts @@ -10,11 +10,11 @@ import { getOrElse } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import * as t from 'io-ts'; import { failure } from 'io-ts/lib/PathReporter'; -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; -import { rangeRt } from '../../../routes/default_api_types'; -import { getCorrelationsFilters } from '../../correlations/get_filters'; -import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; +import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; +import { rangeRt } from '../../../../routes/default_api_types'; +import { getCorrelationsFilters } from '../../../correlations/get_filters'; +import { Setup, SetupTimeRange } from '../../../helpers/setup_request'; const getPercentileThresholdValueQuery = ( percentileThresholdValue: number | undefined diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_request_base.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_request_base.test.ts new file mode 100644 index 0000000000000..e3f5c4a42d803 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_request_base.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getRequestBase } from './get_request_base'; + +describe('correlations', () => { + describe('getRequestBase', () => { + it('returns the request base parameters', () => { + const requestBase = getRequestBase({ + index: 'apm-*', + includeFrozen: true, + }); + expect(requestBase).toEqual({ + index: 'apm-*', + ignore_throttled: false, + ignore_unavailable: true, + }); + }); + + it('defaults ignore_throttled to true', () => { + const requestBase = getRequestBase({ + index: 'apm-*', + }); + expect(requestBase).toEqual({ + index: 'apm-*', + ignore_throttled: true, + ignore_unavailable: true, + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_request_base.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_request_base.ts new file mode 100644 index 0000000000000..e2cdbab830e0d --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/get_request_base.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; + +export const getRequestBase = ({ + index, + includeFrozen, +}: SearchServiceFetchParams) => ({ + index, + // matches APM's event client settings + ignore_throttled: includeFrozen === undefined ? true : !includeFrozen, + ignore_unavailable: true, +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/index.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/index.ts new file mode 100644 index 0000000000000..c33b131d9cbd7 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { fetchTransactionDurationFieldCandidates } from './query_field_candidates'; +export { fetchTransactionDurationFieldValuePairs } from './query_field_value_pairs'; +export { fetchTransactionDurationFractions } from './query_fractions'; +export { fetchTransactionDurationPercentiles } from './query_percentiles'; +export { fetchTransactionDurationCorrelation } from './query_correlation'; +export { fetchTransactionDurationHistograms } from './query_histograms_generator'; +export { fetchTransactionDurationHistogramRangeSteps } from './query_histogram_range_steps'; +export { fetchTransactionDurationRanges, HistogramItem } from './query_ranges'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_correlation.test.ts similarity index 97% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_correlation.test.ts index 678328dce1a19..b1ab4aad36249 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_correlation.test.ts @@ -15,7 +15,12 @@ import { BucketCorrelation, } from './query_correlation'; -const params = { index: 'apm-*', start: '2020', end: '2021' }; +const params = { + index: 'apm-*', + start: '2020', + end: '2021', + includeFrozen: false, +}; const expectations = [1, 3, 5]; const ranges = [{ to: 1 }, { from: 1, to: 3 }, { from: 3, to: 5 }, { from: 5 }]; const fractions = [1, 2, 4, 5]; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_correlation.ts similarity index 92% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_correlation.ts index 94a708f678600..823abe936e223 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_correlation.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_correlation.ts @@ -9,10 +9,11 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; +import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; +import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; +import { getRequestBase } from './get_request_base'; export interface HistogramItem { key: number; @@ -88,7 +89,7 @@ export const getTransactionDurationCorrelationRequest = ( }, }; return { - index: params.index, + ...getRequestBase(params), body, }; }; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_candidates.test.ts similarity index 94% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_candidates.test.ts index 8929b31b3ecb1..fb707231245d0 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_candidates.test.ts @@ -9,14 +9,20 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; +import { hasPrefixToInclude } from '../utils/has_prefix_to_include'; + import { fetchTransactionDurationFieldCandidates, getRandomDocsRequest, - hasPrefixToInclude, shouldBeExcluded, } from './query_field_candidates'; -const params = { index: 'apm-*', start: '2020', end: '2021' }; +const params = { + index: 'apm-*', + start: '2020', + end: '2021', + includeFrozen: false, +}; describe('query_field_candidates', () => { describe('shouldBeExcluded', () => { @@ -79,6 +85,8 @@ describe('query_field_candidates', () => { size: 1000, }, index: params.index, + ignore_throttled: !params.includeFrozen, + ignore_unavailable: true, }); }); }); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_candidates.ts similarity index 85% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_candidates.ts index 8aa54e243eec9..aeb67a4d6884b 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_candidates.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_candidates.ts @@ -9,17 +9,19 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; +import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; -import { getQueryWithParams } from './get_query_with_params'; -import { Field } from './query_field_value_pairs'; import { - FIELD_PREFIX_TO_ADD_AS_CANDIDATE, FIELD_PREFIX_TO_EXCLUDE_AS_CANDIDATE, FIELDS_TO_ADD_AS_CANDIDATE, FIELDS_TO_EXCLUDE_AS_CANDIDATE, POPULATED_DOC_COUNT_SAMPLE_SIZE, -} from './constants'; +} from '../constants'; +import { hasPrefixToInclude } from '../utils'; + +import { getQueryWithParams } from './get_query_with_params'; +import { getRequestBase } from './get_request_base'; +import type { FieldName } from './query_field_value_pairs'; export const shouldBeExcluded = (fieldName: string) => { return ( @@ -30,16 +32,10 @@ export const shouldBeExcluded = (fieldName: string) => { ); }; -export const hasPrefixToInclude = (fieldName: string) => { - return FIELD_PREFIX_TO_ADD_AS_CANDIDATE.some((prefix) => - fieldName.startsWith(prefix) - ); -}; - export const getRandomDocsRequest = ( params: SearchServiceFetchParams ): estypes.SearchRequest => ({ - index: params.index, + ...getRequestBase(params), body: { fields: ['*'], _source: false, @@ -57,7 +53,7 @@ export const getRandomDocsRequest = ( export const fetchTransactionDurationFieldCandidates = async ( esClient: ElasticsearchClient, params: SearchServiceFetchParams -): Promise<{ fieldCandidates: Field[] }> => { +): Promise<{ fieldCandidates: FieldName[] }> => { const { index } = params; // Get all fields with keyword mapping const respMapping = await esClient.fieldCaps({ diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_value_pairs.test.ts similarity index 81% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_value_pairs.test.ts index 7ffbc5208e41e..0ee57fd60cd68 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_value_pairs.test.ts @@ -9,14 +9,20 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import type { AsyncSearchProviderProgress } from '../../../../common/search_strategies/correlations/types'; +import { asyncSearchServiceLogProvider } from '../async_search_service_log'; +import { asyncSearchServiceStateProvider } from '../async_search_service_state'; import { fetchTransactionDurationFieldValuePairs, getTermsAggRequest, } from './query_field_value_pairs'; -const params = { index: 'apm-*', start: '2020', end: '2021' }; +const params = { + index: 'apm-*', + start: '2020', + end: '2021', + includeFrozen: false, +}; describe('query_field_value_pairs', () => { describe('getTermsAggRequest', () => { @@ -34,9 +40,6 @@ describe('query_field_value_pairs', () => { 'myFieldCandidate2', 'myFieldCandidate3', ]; - const progress = { - loadedFieldValuePairs: 0, - } as AsyncSearchProviderProgress; const esClientSearchMock = jest.fn((req: estypes.SearchRequest): { body: estypes.SearchResponse; @@ -56,13 +59,19 @@ describe('query_field_value_pairs', () => { search: esClientSearchMock, } as unknown) as ElasticsearchClient; + const { addLogMessage, getLogMessages } = asyncSearchServiceLogProvider(); + const state = asyncSearchServiceStateProvider(); + const resp = await fetchTransactionDurationFieldValuePairs( esClientMock, params, fieldCandidates, - progress + state, + addLogMessage ); + const { progress } = state.getState(); + expect(progress.loadedFieldValuePairs).toBe(1); expect(resp).toEqual([ { field: 'myFieldCandidate1', value: 'myValue1' }, @@ -73,6 +82,7 @@ describe('query_field_value_pairs', () => { { field: 'myFieldCandidate3', value: 'myValue2' }, ]); expect(esClientSearchMock).toHaveBeenCalledTimes(3); + expect(getLogMessages()).toEqual([]); }); }); }); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_value_pairs.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_value_pairs.ts new file mode 100644 index 0000000000000..33adff4af7a52 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_field_value_pairs.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ElasticsearchClient } from 'src/core/server'; + +import type { estypes } from '@elastic/elasticsearch'; + +import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; + +import type { AsyncSearchServiceLog } from '../async_search_service_log'; +import type { AsyncSearchServiceState } from '../async_search_service_state'; +import { TERMS_SIZE } from '../constants'; + +import { getQueryWithParams } from './get_query_with_params'; +import { getRequestBase } from './get_request_base'; + +export type FieldName = string; + +interface FieldValuePair { + field: FieldName; + value: string; +} +export type FieldValuePairs = FieldValuePair[]; + +export const getTermsAggRequest = ( + params: SearchServiceFetchParams, + fieldName: FieldName +): estypes.SearchRequest => ({ + ...getRequestBase(params), + body: { + query: getQueryWithParams({ params }), + size: 0, + aggs: { + attribute_terms: { + terms: { + field: fieldName, + size: TERMS_SIZE, + }, + }, + }, + }, +}); + +const fetchTransactionDurationFieldTerms = async ( + esClient: ElasticsearchClient, + params: SearchServiceFetchParams, + fieldName: string, + addLogMessage: AsyncSearchServiceLog['addLogMessage'] +): Promise => { + try { + const resp = await esClient.search(getTermsAggRequest(params, fieldName)); + + if (resp.body.aggregations === undefined) { + addLogMessage( + `Failed to fetch terms for field candidate ${fieldName} fieldValuePairs, no aggregations returned.`, + JSON.stringify(resp) + ); + return []; + } + const buckets = (resp.body.aggregations + .attribute_terms as estypes.AggregationsMultiBucketAggregate<{ + key: string; + }>)?.buckets; + if (buckets?.length >= 1) { + return buckets.map((d) => ({ + field: fieldName, + value: d.key, + })); + } + } catch (e) { + addLogMessage( + `Failed to fetch terms for field candidate ${fieldName} fieldValuePairs.`, + JSON.stringify(e) + ); + } + + return []; +}; + +async function fetchInSequence( + fieldCandidates: FieldName[], + fn: (fieldCandidate: string) => Promise +) { + const results = []; + + for (const fieldCandidate of fieldCandidates) { + results.push(...(await fn(fieldCandidate))); + } + + return results; +} + +export const fetchTransactionDurationFieldValuePairs = async ( + esClient: ElasticsearchClient, + params: SearchServiceFetchParams, + fieldCandidates: FieldName[], + state: AsyncSearchServiceState, + addLogMessage: AsyncSearchServiceLog['addLogMessage'] +): Promise => { + let fieldValuePairsProgress = 1; + + return await fetchInSequence( + fieldCandidates, + async function (fieldCandidate: string) { + const fieldTerms = await fetchTransactionDurationFieldTerms( + esClient, + params, + fieldCandidate, + addLogMessage + ); + + state.setProgress({ + loadedFieldValuePairs: fieldValuePairsProgress / fieldCandidates.length, + }); + fieldValuePairsProgress++; + + return fieldTerms; + } + ); +}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_fractions.test.ts similarity index 95% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_fractions.test.ts index 3e7d4a52e4de2..a44cc6131b4cc 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_fractions.test.ts @@ -14,7 +14,12 @@ import { getTransactionDurationRangesRequest, } from './query_fractions'; -const params = { index: 'apm-*', start: '2020', end: '2021' }; +const params = { + index: 'apm-*', + start: '2020', + end: '2021', + includeFrozen: false, +}; const ranges = [{ to: 1 }, { from: 1, to: 3 }, { from: 3, to: 5 }, { from: 5 }]; describe('query_fractions', () => { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_fractions.ts similarity index 86% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_fractions.ts index e9cec25673c6e..35e59054ad01f 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_fractions.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_fractions.ts @@ -7,15 +7,18 @@ import { ElasticsearchClient } from 'kibana/server'; import { estypes } from '@elastic/elasticsearch'; -import { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; + +import { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; +import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; + import { getQueryWithParams } from './get_query_with_params'; -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; +import { getRequestBase } from './get_request_base'; export const getTransactionDurationRangesRequest = ( params: SearchServiceFetchParams, ranges: estypes.AggregationsAggregationRange[] ): estypes.SearchRequest => ({ - index: params.index, + ...getRequestBase(params), body: { query: getQueryWithParams({ params }), size: 0, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram.test.ts similarity index 92% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram.test.ts index ace9177947960..daa40725b7307 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram.test.ts @@ -14,7 +14,12 @@ import { getTransactionDurationHistogramRequest, } from './query_histogram'; -const params = { index: 'apm-*', start: '2020', end: '2021' }; +const params = { + index: 'apm-*', + start: '2020', + end: '2021', + includeFrozen: false, +}; const interval = 100; describe('query_histogram', () => { @@ -54,7 +59,9 @@ describe('query_histogram', () => { }, size: 0, }, - index: 'apm-*', + index: params.index, + ignore_throttled: !params.includeFrozen, + ignore_unavailable: true, }); }); }); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram.ts similarity index 74% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram.ts index 045caabeab268..18fc18af1472e 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram.ts @@ -9,36 +9,33 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; +import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; import type { HistogramItem, ResponseHit, SearchServiceFetchParams, -} from '../../../../common/search_strategies/correlations/types'; +} from '../../../../../common/search_strategies/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; +import { getRequestBase } from './get_request_base'; export const getTransactionDurationHistogramRequest = ( params: SearchServiceFetchParams, interval: number, fieldName?: string, fieldValue?: string -): estypes.SearchRequest => { - const query = getQueryWithParams({ params, fieldName, fieldValue }); - - return { - index: params.index, - body: { - query, - size: 0, - aggs: { - transaction_duration_histogram: { - histogram: { field: TRANSACTION_DURATION, interval }, - }, +): estypes.SearchRequest => ({ + ...getRequestBase(params), + body: { + query: getQueryWithParams({ params, fieldName, fieldValue }), + size: 0, + aggs: { + transaction_duration_histogram: { + histogram: { field: TRANSACTION_DURATION, interval }, }, }, - }; -}; + }, +}); export const fetchTransactionDurationHistogram = async ( esClient: ElasticsearchClient, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_interval.test.ts similarity index 93% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_interval.test.ts index ebd78f1248510..b40f685645a2e 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_interval.test.ts @@ -14,7 +14,12 @@ import { getHistogramIntervalRequest, } from './query_histogram_interval'; -const params = { index: 'apm-*', start: '2020', end: '2021' }; +const params = { + index: 'apm-*', + start: '2020', + end: '2021', + includeFrozen: false, +}; describe('query_histogram_interval', () => { describe('getHistogramIntervalRequest', () => { @@ -58,6 +63,8 @@ describe('query_histogram_interval', () => { size: 0, }, index: params.index, + ignore_throttled: !params.includeFrozen, + ignore_unavailable: true, }); }); }); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_interval.ts similarity index 84% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_interval.ts index 0f897f2e9236e..cc50c8d4d860a 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_interval.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_interval.ts @@ -9,17 +9,18 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; +import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; +import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; +import { getRequestBase } from './get_request_base'; const HISTOGRAM_INTERVALS = 1000; export const getHistogramIntervalRequest = ( params: SearchServiceFetchParams ): estypes.SearchRequest => ({ - index: params.index, + ...getRequestBase(params), body: { query: getQueryWithParams({ params }), size: 0, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_range_steps.test.ts similarity index 94% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_range_steps.test.ts index 76aab1cd979c9..b909778e3e4ef 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_range_steps.test.ts @@ -14,7 +14,12 @@ import { getHistogramIntervalRequest, } from './query_histogram_range_steps'; -const params = { index: 'apm-*', start: '2020', end: '2021' }; +const params = { + index: 'apm-*', + start: '2020', + end: '2021', + includeFrozen: false, +}; describe('query_histogram_range_steps', () => { describe('getHistogramIntervalRequest', () => { @@ -58,6 +63,8 @@ describe('query_histogram_range_steps', () => { size: 0, }, index: params.index, + ignore_throttled: !params.includeFrozen, + ignore_unavailable: true, }); }); }); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_range_steps.ts similarity index 87% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_range_steps.ts index 405c84828db4c..116b5d1645601 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_histogram_range_steps.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histogram_range_steps.ts @@ -11,10 +11,11 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; +import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; +import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; +import { getRequestBase } from './get_request_base'; const getHistogramRangeSteps = (min: number, max: number, steps: number) => { // A d3 based scale function as a helper to get equally distributed bins on a log scale. @@ -27,7 +28,7 @@ const getHistogramRangeSteps = (min: number, max: number, steps: number) => { export const getHistogramIntervalRequest = ( params: SearchServiceFetchParams ): estypes.SearchRequest => ({ - index: params.index, + ...getRequestBase(params), body: { query: getQueryWithParams({ params }), size: 0, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histograms_generator.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histograms_generator.test.ts new file mode 100644 index 0000000000000..7f89aa52367a0 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histograms_generator.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { estypes } from '@elastic/elasticsearch'; + +import type { ElasticsearchClient } from 'src/core/server'; + +import { asyncSearchServiceLogProvider } from '../async_search_service_log'; +import { asyncSearchServiceStateProvider } from '../async_search_service_state'; + +import { fetchTransactionDurationHistograms } from './query_histograms_generator'; + +const params = { + index: 'apm-*', + start: '2020', + end: '2021', + includeFrozen: false, +}; +const expectations = [1, 3, 5]; +const ranges = [{ to: 1 }, { from: 1, to: 3 }, { from: 3, to: 5 }, { from: 5 }]; +const fractions = [1, 2, 4, 5]; +const totalDocCount = 1234; +const histogramRangeSteps = [1, 2, 4, 5]; + +const fieldValuePairs = [ + { field: 'the-field-name-1', value: 'the-field-value-1' }, + { field: 'the-field-name-2', value: 'the-field-value-2' }, + { field: 'the-field-name-2', value: 'the-field-value-3' }, +]; + +describe('query_histograms_generator', () => { + describe('fetchTransactionDurationHistograms', () => { + it(`doesn't break on failing ES queries and adds messages to the log`, async () => { + const esClientSearchMock = jest.fn((req: estypes.SearchRequest): { + body: estypes.SearchResponse; + } => { + return { + body: ({} as unknown) as estypes.SearchResponse, + }; + }); + + const esClientMock = ({ + search: esClientSearchMock, + } as unknown) as ElasticsearchClient; + + const state = asyncSearchServiceStateProvider(); + const { addLogMessage, getLogMessages } = asyncSearchServiceLogProvider(); + + let loadedHistograms = 0; + const items = []; + + for await (const item of fetchTransactionDurationHistograms( + esClientMock, + addLogMessage, + params, + state, + expectations, + ranges, + fractions, + histogramRangeSteps, + totalDocCount, + fieldValuePairs + )) { + if (item !== undefined) { + items.push(item); + } + loadedHistograms++; + } + + expect(items.length).toEqual(0); + expect(loadedHistograms).toEqual(3); + expect(esClientSearchMock).toHaveBeenCalledTimes(3); + expect(getLogMessages().map((d) => d.split(': ')[1])).toEqual([ + "Failed to fetch correlation/kstest for 'the-field-name-1/the-field-value-1'", + "Failed to fetch correlation/kstest for 'the-field-name-2/the-field-value-2'", + "Failed to fetch correlation/kstest for 'the-field-name-2/the-field-value-3'", + ]); + }); + + it('returns items with correlation and ks-test value', async () => { + const esClientSearchMock = jest.fn((req: estypes.SearchRequest): { + body: estypes.SearchResponse; + } => { + return { + body: ({ + aggregations: { + latency_ranges: { buckets: [] }, + transaction_duration_correlation: { value: 0.6 }, + ks_test: { less: 0.001 }, + logspace_ranges: { buckets: [] }, + }, + } as unknown) as estypes.SearchResponse, + }; + }); + + const esClientMock = ({ + search: esClientSearchMock, + } as unknown) as ElasticsearchClient; + + const state = asyncSearchServiceStateProvider(); + const { addLogMessage, getLogMessages } = asyncSearchServiceLogProvider(); + + let loadedHistograms = 0; + const items = []; + + for await (const item of fetchTransactionDurationHistograms( + esClientMock, + addLogMessage, + params, + state, + expectations, + ranges, + fractions, + histogramRangeSteps, + totalDocCount, + fieldValuePairs + )) { + if (item !== undefined) { + items.push(item); + } + loadedHistograms++; + } + + expect(items.length).toEqual(3); + expect(loadedHistograms).toEqual(3); + expect(esClientSearchMock).toHaveBeenCalledTimes(6); + expect(getLogMessages().length).toEqual(0); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histograms_generator.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histograms_generator.ts new file mode 100644 index 0000000000000..c4869aac187c6 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_histograms_generator.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { estypes } from '@elastic/elasticsearch'; + +import type { ElasticsearchClient } from 'src/core/server'; + +import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; + +import type { AsyncSearchServiceLog } from '../async_search_service_log'; +import type { AsyncSearchServiceState } from '../async_search_service_state'; +import { CORRELATION_THRESHOLD, KS_TEST_THRESHOLD } from '../constants'; + +import { getPrioritizedFieldValuePairs } from './get_prioritized_field_value_pairs'; +import { fetchTransactionDurationCorrelation } from './query_correlation'; +import { fetchTransactionDurationRanges } from './query_ranges'; + +import type { FieldValuePairs } from './query_field_value_pairs'; + +export async function* fetchTransactionDurationHistograms( + esClient: ElasticsearchClient, + addLogMessage: AsyncSearchServiceLog['addLogMessage'], + params: SearchServiceFetchParams, + state: AsyncSearchServiceState, + expectations: number[], + ranges: estypes.AggregationsAggregationRange[], + fractions: number[], + histogramRangeSteps: number[], + totalDocCount: number, + fieldValuePairs: FieldValuePairs +) { + for (const item of getPrioritizedFieldValuePairs(fieldValuePairs)) { + if (params === undefined || item === undefined || state.getIsCancelled()) { + state.setIsRunning(false); + return; + } + + // If one of the fields have an error + // We don't want to stop the whole process + try { + const { correlation, ksTest } = await fetchTransactionDurationCorrelation( + esClient, + params, + expectations, + ranges, + fractions, + totalDocCount, + item.field, + item.value + ); + + if (state.getIsCancelled()) { + state.setIsRunning(false); + return; + } + + if ( + correlation !== null && + correlation > CORRELATION_THRESHOLD && + ksTest !== null && + ksTest < KS_TEST_THRESHOLD + ) { + const logHistogram = await fetchTransactionDurationRanges( + esClient, + params, + histogramRangeSteps, + item.field, + item.value + ); + yield { + ...item, + correlation, + ksTest, + histogram: logHistogram, + }; + } else { + yield undefined; + } + } catch (e) { + // don't fail the whole process for individual correlation queries, + // just add the error to the internal log and check if we'd want to set the + // cross-cluster search compatibility warning to true. + addLogMessage( + `Failed to fetch correlation/kstest for '${item.field}/${item.value}'`, + JSON.stringify(e) + ); + if (params?.index.includes(':')) { + state.setCcsWarning(true); + } + yield undefined; + } + } +} diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_percentiles.test.ts similarity index 94% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_percentiles.test.ts index f0d01a4849f9f..2ca77f51be07c 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_percentiles.test.ts @@ -14,7 +14,12 @@ import { getTransactionDurationPercentilesRequest, } from './query_percentiles'; -const params = { index: 'apm-*', start: '2020', end: '2021' }; +const params = { + index: 'apm-*', + start: '2020', + end: '2021', + includeFrozen: false, +}; describe('query_percentiles', () => { describe('getTransactionDurationPercentilesRequest', () => { @@ -57,6 +62,8 @@ describe('query_percentiles', () => { track_total_hits: true, }, index: params.index, + ignore_throttled: !params.includeFrozen, + ignore_unavailable: true, }); }); }); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_percentiles.ts similarity index 87% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_percentiles.ts index cb302f19a000b..bd230687314e6 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_percentiles.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_percentiles.ts @@ -9,11 +9,12 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; +import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; +import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; -import { SIGNIFICANT_VALUE_DIGITS } from './constants'; +import { getRequestBase } from './get_request_base'; +import { SIGNIFICANT_VALUE_DIGITS } from '../constants'; export interface HistogramItem { key: number; @@ -36,7 +37,7 @@ export const getTransactionDurationPercentilesRequest = ( const query = getQueryWithParams({ params, fieldName, fieldValue }); return { - index: params.index, + ...getRequestBase(params), body: { track_total_hits: true, query, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_ranges.test.ts similarity index 94% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_ranges.test.ts index 7d18efc360563..fc995ae07b3d7 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_ranges.test.ts @@ -14,7 +14,12 @@ import { getTransactionDurationRangesRequest, } from './query_ranges'; -const params = { index: 'apm-*', start: '2020', end: '2021' }; +const params = { + index: 'apm-*', + start: '2020', + end: '2021', + includeFrozen: false, +}; const rangeSteps = [1, 3, 5]; describe('query_ranges', () => { @@ -74,6 +79,8 @@ describe('query_ranges', () => { size: 0, }, index: params.index, + ignore_throttled: !params.includeFrozen, + ignore_unavailable: true, }); }); }); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_ranges.ts similarity index 88% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_ranges.ts index 0e813a18fdf4a..6f662363d0c42 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_ranges.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/queries/query_ranges.ts @@ -9,10 +9,11 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'src/core/server'; -import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; -import type { SearchServiceFetchParams } from '../../../../common/search_strategies/correlations/types'; +import { TRANSACTION_DURATION } from '../../../../../common/elasticsearch_fieldnames'; +import type { SearchServiceFetchParams } from '../../../../../common/search_strategies/correlations/types'; import { getQueryWithParams } from './get_query_with_params'; +import { getRequestBase } from './get_request_base'; export interface HistogramItem { key: number; @@ -47,7 +48,7 @@ export const getTransactionDurationRangesRequest = ( } return { - index: params.index, + ...getRequestBase(params), body: { query, size: 0, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.ts deleted file mode 100644 index 23928565da084..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/query_field_value_pairs.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ElasticsearchClient } from 'src/core/server'; - -import type { estypes } from '@elastic/elasticsearch'; - -import type { - AsyncSearchProviderProgress, - SearchServiceFetchParams, -} from '../../../../common/search_strategies/correlations/types'; - -import { getQueryWithParams } from './get_query_with_params'; -import { TERMS_SIZE } from './constants'; - -interface FieldValuePair { - field: string; - value: string; -} -type FieldValuePairs = FieldValuePair[]; - -export type Field = string; - -export const getTermsAggRequest = ( - params: SearchServiceFetchParams, - fieldName: string -): estypes.SearchRequest => ({ - index: params.index, - body: { - query: getQueryWithParams({ params }), - size: 0, - aggs: { - attribute_terms: { - terms: { - field: fieldName, - size: TERMS_SIZE, - }, - }, - }, - }, -}); - -export const fetchTransactionDurationFieldValuePairs = async ( - esClient: ElasticsearchClient, - params: SearchServiceFetchParams, - fieldCandidates: Field[], - progress: AsyncSearchProviderProgress -): Promise => { - const fieldValuePairs: FieldValuePairs = []; - - let fieldValuePairsProgress = 1; - - for (let i = 0; i < fieldCandidates.length; i++) { - const fieldName = fieldCandidates[i]; - // mutate progress - progress.loadedFieldValuePairs = - fieldValuePairsProgress / fieldCandidates.length; - - try { - const resp = await esClient.search(getTermsAggRequest(params, fieldName)); - - if (resp.body.aggregations === undefined) { - fieldValuePairsProgress++; - continue; - } - const buckets = (resp.body.aggregations - .attribute_terms as estypes.AggregationsMultiBucketAggregate<{ - key: string; - }>)?.buckets; - if (buckets.length >= 1) { - fieldValuePairs.push( - ...buckets.map((d) => ({ - field: fieldName, - value: d.key, - })) - ); - } - - fieldValuePairsProgress++; - } catch (e) { - fieldValuePairsProgress++; - } - } - return fieldValuePairs; -}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/aggregation_utils.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/compute_expectations_and_ranges.test.ts similarity index 96% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/aggregation_utils.test.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/compute_expectations_and_ranges.test.ts index bdb2ffbfcd898..8fb4ecc57e7af 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/aggregation_utils.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/compute_expectations_and_ranges.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { computeExpectationsAndRanges } from './aggregation_utils'; +import { computeExpectationsAndRanges } from './compute_expectations_and_ranges'; describe('aggregation utils', () => { describe('computeExpectationsAndRanges', () => { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/aggregation_utils.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/compute_expectations_and_ranges.ts similarity index 100% rename from x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/aggregation_utils.ts rename to x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/compute_expectations_and_ranges.ts diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/current_time_as_string.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/current_time_as_string.test.ts new file mode 100644 index 0000000000000..2034c29b01d94 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/current_time_as_string.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { currentTimeAsString } from './current_time_as_string'; + +describe('aggregation utils', () => { + describe('currentTimeAsString', () => { + it('returns the current time as a string', () => { + const mockDate = new Date(1392202800000); + // @ts-ignore ignore the mockImplementation callback error + const spy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate); + + const timeString = currentTimeAsString(); + + expect(timeString).toEqual('2014-02-12T11:00:00.000Z'); + + spy.mockRestore(); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/current_time_as_string.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/current_time_as_string.ts new file mode 100644 index 0000000000000..f454b8c8274f1 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/current_time_as_string.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const currentTimeAsString = () => new Date().toISOString(); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/has_prefix_to_include.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/has_prefix_to_include.test.ts new file mode 100644 index 0000000000000..a951dc63caad9 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/has_prefix_to_include.test.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FIELD_PREFIX_TO_ADD_AS_CANDIDATE } from '../constants'; + +import { hasPrefixToInclude } from './has_prefix_to_include'; + +describe('aggregation utils', () => { + describe('hasPrefixToInclude', () => { + it('returns true if the prefix is included', async () => { + FIELD_PREFIX_TO_ADD_AS_CANDIDATE.forEach((prefix) => { + expect(hasPrefixToInclude(`${prefix}the-field-name`)).toBe(true); + }); + }); + it('returns false if the prefix is included', async () => { + FIELD_PREFIX_TO_ADD_AS_CANDIDATE.forEach((prefix) => { + expect( + hasPrefixToInclude(`unknown-prefix-.${prefix}the-field-name`) + ).toBe(false); + expect(hasPrefixToInclude('the-field-name')).toBe(false); + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/has_prefix_to_include.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/has_prefix_to_include.ts new file mode 100644 index 0000000000000..baf4d62af00fa --- /dev/null +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/has_prefix_to_include.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FIELD_PREFIX_TO_ADD_AS_CANDIDATE } from '../constants'; + +export const hasPrefixToInclude = (fieldName: string) => { + return FIELD_PREFIX_TO_ADD_AS_CANDIDATE.some((prefix) => + fieldName.startsWith(prefix) + ); +}; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/index.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/index.ts index ab6190fb288ad..000fd57c718b7 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/index.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/index.ts @@ -5,5 +5,6 @@ * 2.0. */ -export * from './math_utils'; -export * from './aggregation_utils'; +export { computeExpectationsAndRanges } from './compute_expectations_and_ranges'; +export { currentTimeAsString } from './current_time_as_string'; +export { hasPrefixToInclude } from './has_prefix_to_include'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/math_utils.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/math_utils.test.ts deleted file mode 100644 index ed4107b9d602a..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/math_utils.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getRandomInt } from './math_utils'; - -describe('math utils', () => { - describe('getRandomInt', () => { - it('returns a random integer within the given range', () => { - const min = 0.9; - const max = 11.1; - const randomInt = getRandomInt(min, max); - expect(Number.isInteger(randomInt)).toBe(true); - expect(randomInt > min).toBe(true); - expect(randomInt < max).toBe(true); - }); - - it('returns 1 if given range only allows this integer', () => { - const randomInt = getRandomInt(0.9, 1.1); - expect(randomInt).toBe(1); - }); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/math_utils.ts b/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/math_utils.ts deleted file mode 100644 index 01e856e511fc2..0000000000000 --- a/x-pack/plugins/apm/server/lib/search_strategies/correlations/utils/math_utils.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { range } from 'lodash'; -import { HistogramItem } from '../query_ranges'; -import { asPreciseDecimal } from '../../../../../common/utils/formatters'; - -// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random -export function getRandomInt(min: number, max: number) { - min = Math.ceil(min); - max = Math.floor(max); - return Math.floor(Math.random() * (max - min + 1) + min); // The maximum is inclusive and the minimum is inclusive -} - -// Roughly compare histograms by sampling random bins -// And rounding up histogram count to account for different floating points -export const isHistogramRoughlyEqual = ( - a: HistogramItem[], - b: HistogramItem[], - { numBinsToSample = 10, significantFraction = 3 } -) => { - if (a.length !== b.length) return false; - - const sampledIndices = Array.from(Array(numBinsToSample).keys()).map(() => - getRandomInt(0, a.length - 1) - ); - return !sampledIndices.some((idx) => { - return ( - asPreciseDecimal(a[idx].key, significantFraction) !== - asPreciseDecimal(b[idx].key, significantFraction) && - roundToNearest(a[idx].doc_count) !== roundToNearest(b[idx].doc_count) - ); - }); -}; - -/** Round numeric to the nearest 5 - * E.g. if roundBy = 5, results will be 11 -> 10, 14 -> 10, 16 -> 20 - */ -export const roundToNearest = (n: number, roundBy = 5) => { - return Math.ceil((n + 1) / roundBy) * roundBy; -}; - -/** - * Create a rough stringified version of the histogram - */ -export const hashHistogram = ( - histogram: HistogramItem[], - { significantFraction = 3, numBinsToSample = 10 } -) => { - // Generate bins to sample evenly - const sampledIndices = Array.from( - range( - 0, - histogram.length - 1, - Math.ceil(histogram.length / numBinsToSample) - ) - ); - return JSON.stringify( - sampledIndices.map((idx) => { - return `${asPreciseDecimal( - histogram[idx].key, - significantFraction - )}-${roundToNearest(histogram[idx].doc_count)}`; - }) - ); -}; From e86d909ae62deaa11447491c9c1352e302e4b022 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 11 Aug 2021 11:14:58 +0200 Subject: [PATCH 070/104] include and fix hello world example test (#108072) --- examples/hello_world/public/plugin.tsx | 2 +- test/examples/config.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/hello_world/public/plugin.tsx b/examples/hello_world/public/plugin.tsx index cb648bffbb57c..49acf07e6ae12 100755 --- a/examples/hello_world/public/plugin.tsx +++ b/examples/hello_world/public/plugin.tsx @@ -21,7 +21,7 @@ export class HelloWorldPlugin implements Plugin { id: 'helloWorld', title: 'Hello World', async mount({ element }: AppMountParameters) { - ReactDOM.render(
    Hello World!
    , element); + ReactDOM.render(
    Hello World!
    , element); return () => ReactDOM.unmountComponentAtNode(element); }, }); diff --git a/test/examples/config.js b/test/examples/config.js index d47748e5f22a9..85068740af463 100644 --- a/test/examples/config.js +++ b/test/examples/config.js @@ -22,6 +22,7 @@ export default async function ({ readConfigFile }) { return { testFiles: [ + require.resolve('./hello_world'), require.resolve('./embeddables'), require.resolve('./bfetch_explorer'), require.resolve('./ui_actions'), From cec5d3f27a9f58592a3868e13daf9fe94417e2eb Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Wed, 11 Aug 2021 03:25:46 -0700 Subject: [PATCH 071/104] [RAC] - Update field names (#107857) ### Summary ### Fields used moving forward `kibana.alert.rule.consumer` will refer to the context in which a rule instance is created. Rules created in: - stack --> `alerts` - security solution --> `siem` - apm --> `apm` `kibana.alert.rule.producer` will refer to the plugin that registered a rule type. Rules registered in: - stack --> `alerts` - security solution --> `siem` - apm --> `apm` So an `apm.error_rate` rule created in stack will have: - consumer: `alerts` and producer: `apm` An `apm.error_rate` rule created in apm will have: - consumer: `apm` and producer: `apm` `kibana.alert.rule.rule_type_id` will refer to a rule's rule type id. Examples: - `apm.error_rate` - `siem.signals` - `siem.threshold` Also renamed the following because `rule.*` fields are meant to be ecs fields pulled from the source/event document, not refer to our rule fields. `rule.name` --> `kibana.alert.rule.name` will refer to the rule's name. `rule.category` --> `kibana.alert.rule.category` will refer to the rule's category. `rule.id` --> `kibana.alert.rule.uuid` will refer to the rule's uuid. --- .../src/technical_field_names.ts | 47 ++++---- .../alerting_authorization.test.ts | 4 +- .../alerting_authorization_kuery.test.ts | 46 ++++---- .../Distribution/index.tsx | 4 +- .../helper/get_alert_annotations.test.tsx | 23 ++-- .../charts/helper/get_alert_annotations.tsx | 8 +- .../shared/charts/latency_chart/index.tsx | 8 +- .../latency_chart/latency_chart.stories.tsx | 35 +++--- .../transaction_error_rate_chart/index.tsx | 5 +- .../pages/alerts/alerts_flyout/index.tsx | 8 +- .../pages/alerts/alerts_table_t_grid.tsx | 4 +- .../alerts/alerts_table_t_grid_actions.tsx | 9 +- .../public/pages/alerts/decorate_response.ts | 8 +- .../public/pages/alerts/example_data.ts | 35 +++--- .../public/pages/alerts/render_cell_value.tsx | 4 +- x-pack/plugins/rule_registry/README.md | 14 +-- .../field_maps/technical_rule_field_map.ts | 33 +++--- .../server/alert_data_client/alerts_client.ts | 31 +++--- .../tests/bulk_update.test.ts | 39 ++++--- .../alert_data_client/tests/get.test.ts | 31 +++--- .../alert_data_client/tests/update.test.ts | 27 +++-- .../server/routes/get_alert_by_id.test.ts | 12 +-- .../utils/create_lifecycle_executor.test.ts | 22 ++-- .../server/utils/create_lifecycle_executor.ts | 18 ++-- .../utils/create_lifecycle_rule_type.test.ts | 50 ++++----- .../server/utils/get_rule_executor_data.ts | 30 +++--- .../navigation/breadcrumbs/index.test.ts | 6 +- .../alerts_table/default_config.tsx | 16 +-- .../get_signals_template.test.ts.snap | 16 +-- .../routes/index/get_signals_template.test.ts | 4 +- .../routes/index/get_signals_template.ts | 8 +- .../rule_execution_field_map.ts | 3 - .../rule_registry_log_client.ts | 34 +++--- .../factories/utils/build_alert.test.ts | 6 +- .../rule_types/factories/utils/build_alert.ts | 4 +- .../server/search_strategy/timeline/index.ts | 19 ++-- .../tests/alerts/rule_registry.ts | 102 +++++++++--------- .../rule_registry/alerts/data.json | 28 ++--- .../rule_registry/alerts/mappings.json | 4 +- .../applications/timelines_test/index.tsx | 4 +- .../security_and_spaces/tests/basic/events.ts | 10 +- .../security_and_spaces/tests/trial/events.ts | 10 +- .../security_only/tests/basic/events.ts | 6 +- .../security_only/tests/trial/events.ts | 6 +- .../test/timeline/spaces_only/tests/events.ts | 6 +- 45 files changed, 432 insertions(+), 415 deletions(-) diff --git a/packages/kbn-rule-data-utils/src/technical_field_names.ts b/packages/kbn-rule-data-utils/src/technical_field_names.ts index a29c1023caf67..4b3f3fbb6f370 100644 --- a/packages/kbn-rule-data-utils/src/technical_field_names.ts +++ b/packages/kbn-rule-data-utils/src/technical_field_names.ts @@ -17,25 +17,18 @@ const CONSUMERS = `${KIBANA_NAMESPACE}.consumers` as const; const ECS_VERSION = 'ecs.version' as const; const EVENT_ACTION = 'event.action' as const; const EVENT_KIND = 'event.kind' as const; -const RULE_CATEGORY = 'rule.category' as const; -const RULE_CONSUMERS = 'rule.consumers' as const; -const RULE_ID = 'rule.id' as const; -const RULE_NAME = 'rule.name' as const; -const RULE_UUID = 'rule.uuid' as const; const SPACE_IDS = `${KIBANA_NAMESPACE}.space_ids` as const; const TAGS = 'tags' as const; const TIMESTAMP = '@timestamp' as const; const VERSION = `${KIBANA_NAMESPACE}.version` as const; +// Fields pertaining to the alert const ALERT_ACTION_GROUP = `${ALERT_NAMESPACE}.action_group` as const; const ALERT_DURATION = `${ALERT_NAMESPACE}.duration.us` as const; const ALERT_END = `${ALERT_NAMESPACE}.end` as const; const ALERT_EVALUATION_THRESHOLD = `${ALERT_NAMESPACE}.evaluation.threshold` as const; const ALERT_EVALUATION_VALUE = `${ALERT_NAMESPACE}.evaluation.value` as const; const ALERT_ID = `${ALERT_NAMESPACE}.id` as const; -const ALERT_OWNER = `${ALERT_NAMESPACE}.owner` as const; -const ALERT_CONSUMERS = `${ALERT_NAMESPACE}.consumers` as const; -const ALERT_PRODUCER = `${ALERT_NAMESPACE}.producer` as const; const ALERT_REASON = `${ALERT_NAMESPACE}.reason` as const; const ALERT_RISK_SCORE = `${ALERT_NAMESPACE}.risk_score` as const; const ALERT_SEVERITY = `${ALERT_NAMESPACE}.severity` as const; @@ -49,8 +42,8 @@ const ALERT_WORKFLOW_REASON = `${ALERT_NAMESPACE}.workflow_reason` as const; const ALERT_WORKFLOW_STATUS = `${ALERT_NAMESPACE}.workflow_status` as const; const ALERT_WORKFLOW_USER = `${ALERT_NAMESPACE}.workflow_user` as const; +// Fields pertaining to the rule associated with the alert const ALERT_RULE_AUTHOR = `${ALERT_RULE_NAMESPACE}.author` as const; -const ALERT_RULE_CONSUMERS = `${ALERT_RULE_NAMESPACE}.consumers` as const; const ALERT_RULE_CREATED_AT = `${ALERT_RULE_NAMESPACE}.created_at` as const; const ALERT_RULE_CREATED_BY = `${ALERT_RULE_NAMESPACE}.created_by` as const; const ALERT_RULE_DESCRIPTION = `${ALERT_RULE_NAMESPACE}.description` as const; @@ -59,6 +52,7 @@ const ALERT_RULE_FROM = `${ALERT_RULE_NAMESPACE}.from` as const; const ALERT_RULE_ID = `${ALERT_RULE_NAMESPACE}.id` as const; const ALERT_RULE_INTERVAL = `${ALERT_RULE_NAMESPACE}.interval` as const; const ALERT_RULE_LICENSE = `${ALERT_RULE_NAMESPACE}.license` as const; +const ALERT_RULE_CATEGORY = `${ALERT_RULE_NAMESPACE}.category` as const; const ALERT_RULE_NAME = `${ALERT_RULE_NAMESPACE}.name` as const; const ALERT_RULE_NOTE = `${ALERT_RULE_NAMESPACE}.note` as const; const ALERT_RULE_REFERENCES = `${ALERT_RULE_NAMESPACE}.references` as const; @@ -75,6 +69,15 @@ const ALERT_RULE_TYPE_ID = `${ALERT_RULE_NAMESPACE}.rule_type_id` as const; const ALERT_RULE_UPDATED_AT = `${ALERT_RULE_NAMESPACE}.updated_at` as const; const ALERT_RULE_UPDATED_BY = `${ALERT_RULE_NAMESPACE}.updated_by` as const; const ALERT_RULE_VERSION = `${ALERT_RULE_NAMESPACE}.version` as const; +// the feature instantiating a rule type. +// Rule created in stack --> alerts +// Rule created in siem --> siem +const ALERT_RULE_CONSUMER = `${ALERT_RULE_NAMESPACE}.consumer` as const; +// the plugin that registered the rule type. +// Rule type apm.error_rate --> apm +// Rule type siem.signals --> siem +const ALERT_RULE_PRODUCER = `${ALERT_RULE_NAMESPACE}.producer` as const; +const ALERT_RULE_UUID = `${ALERT_RULE_NAMESPACE}.uuid` as const; const namespaces = { KIBANA_NAMESPACE, @@ -87,11 +90,6 @@ const fields = { ECS_VERSION, EVENT_KIND, EVENT_ACTION, - RULE_CATEGORY, - RULE_CONSUMERS, - RULE_ID, - RULE_NAME, - RULE_UUID, TAGS, TIMESTAMP, ALERT_ACTION_GROUP, @@ -100,13 +98,11 @@ const fields = { ALERT_EVALUATION_THRESHOLD, ALERT_EVALUATION_VALUE, ALERT_ID, - ALERT_OWNER, - ALERT_CONSUMERS, - ALERT_PRODUCER, + ALERT_RULE_CONSUMER, + ALERT_RULE_PRODUCER, ALERT_REASON, ALERT_RISK_SCORE, ALERT_RULE_AUTHOR, - ALERT_RULE_CONSUMERS, ALERT_RULE_CREATED_AT, ALERT_RULE_CREATED_BY, ALERT_RULE_DESCRIPTION, @@ -141,6 +137,8 @@ const fields = { ALERT_WORKFLOW_REASON, ALERT_WORKFLOW_STATUS, ALERT_WORKFLOW_USER, + ALERT_RULE_UUID, + ALERT_RULE_CATEGORY, SPACE_IDS, VERSION, }; @@ -154,9 +152,8 @@ export { ALERT_ID, ALERT_NAMESPACE, ALERT_RULE_NAMESPACE, - ALERT_OWNER, - ALERT_CONSUMERS, - ALERT_PRODUCER, + ALERT_RULE_CONSUMER, + ALERT_RULE_PRODUCER, ALERT_REASON, ALERT_RISK_SCORE, ALERT_STATUS, @@ -164,7 +161,6 @@ export { ALERT_WORKFLOW_STATUS, ALERT_WORKFLOW_USER, ALERT_RULE_AUTHOR, - ALERT_RULE_CONSUMERS, ALERT_RULE_CREATED_AT, ALERT_RULE_CREATED_BY, ALERT_RULE_DESCRIPTION, @@ -200,11 +196,8 @@ export { EVENT_ACTION, EVENT_KIND, KIBANA_NAMESPACE, - RULE_CATEGORY, - RULE_CONSUMERS, - RULE_ID, - RULE_NAME, - RULE_UUID, + ALERT_RULE_UUID, + ALERT_RULE_CATEGORY, TAGS, TIMESTAMP, SPACE_IDS, diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts index 71ac9e48c7297..6314488af88d7 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts @@ -1013,14 +1013,14 @@ describe('AlertingAuthorization', () => { await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationEntity.Rule, { type: AlertingAuthorizationFilterType.KQL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', }, }) ).filter ).toEqual( esKuery.fromKueryExpression( - `((path.to.rule.id:myAppAlertType and consumer-field:(myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule.id:myOtherAppAlertType and consumer-field:(myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule.id:mySecondAppAlertType and consumer-field:(myApp or myOtherApp or myAppWithSubFeature)))` + `((path.to.rule_type_id:myAppAlertType and consumer-field:(myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule_type_id:myOtherAppAlertType and consumer-field:(myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule_type_id:mySecondAppAlertType and consumer-field:(myApp or myOtherApp or myAppWithSubFeature)))` ) ); expect(auditLogger.logAuthorizationSuccess).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.test.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.test.ts index 5ea15c4818a21..4cb790a5dc818 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.test.ts @@ -37,14 +37,16 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { { type: AlertingAuthorizationFilterType.KQL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', }, }, 'space1' ) ).toEqual( - esKuery.fromKueryExpression(`((path.to.rule.id:myAppAlertType and consumer-field:(myApp)))`) + esKuery.fromKueryExpression( + `((path.to.rule_type_id:myAppAlertType and consumer-field:(myApp)))` + ) ); }); @@ -72,7 +74,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { { type: AlertingAuthorizationFilterType.KQL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', }, }, @@ -80,7 +82,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { ) ).toEqual( esKuery.fromKueryExpression( - `((path.to.rule.id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp)))` + `((path.to.rule_type_id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp)))` ) ); }); @@ -144,7 +146,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { { type: AlertingAuthorizationFilterType.KQL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', }, }, @@ -152,7 +154,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { ) ).toEqual( esKuery.fromKueryExpression( - `((path.to.rule.id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule.id:myOtherAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule.id:mySecondAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)))` + `((path.to.rule_type_id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule_type_id:myOtherAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule_type_id:mySecondAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)))` ) ); }); @@ -199,7 +201,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { { type: AlertingAuthorizationFilterType.KQL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', spaceIds: 'path.to.spaceIds', }, @@ -208,7 +210,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { ) ).toEqual( esKuery.fromKueryExpression( - `((path.to.rule.id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature) and path.to.spaceIds:space1) or (path.to.rule.id:myOtherAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature) and path.to.spaceIds:space1))` + `((path.to.rule_type_id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature) and path.to.spaceIds:space1) or (path.to.rule_type_id:myOtherAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature) and path.to.spaceIds:space1))` ) ); }); @@ -255,7 +257,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { { type: AlertingAuthorizationFilterType.KQL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', spaceIds: 'path.to.spaceIds', }, @@ -264,7 +266,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { ) ).toEqual( esKuery.fromKueryExpression( - `((path.to.rule.id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule.id:myOtherAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)))` + `((path.to.rule_type_id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule_type_id:myOtherAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)))` ) ); }); @@ -293,7 +295,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { { type: AlertingAuthorizationFilterType.ESDSL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', }, }, @@ -307,7 +309,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { should: [ { match: { - 'path.to.rule.id': 'myAppAlertType', + 'path.to.rule_type_id': 'myAppAlertType', }, }, ], @@ -355,7 +357,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { { type: AlertingAuthorizationFilterType.ESDSL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', }, }, @@ -366,7 +368,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { filter: [ { bool: { - should: [{ match: { 'path.to.rule.id': 'myAppAlertType' } }], + should: [{ match: { 'path.to.rule_type_id': 'myAppAlertType' } }], minimum_should_match: 1, }, }, @@ -459,7 +461,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { { type: AlertingAuthorizationFilterType.ESDSL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', }, }, @@ -473,7 +475,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { filter: [ { bool: { - should: [{ match: { 'path.to.rule.id': 'myAppAlertType' } }], + should: [{ match: { 'path.to.rule_type_id': 'myAppAlertType' } }], minimum_should_match: 1, }, }, @@ -516,7 +518,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { filter: [ { bool: { - should: [{ match: { 'path.to.rule.id': 'myOtherAppAlertType' } }], + should: [{ match: { 'path.to.rule_type_id': 'myOtherAppAlertType' } }], minimum_should_match: 1, }, }, @@ -559,7 +561,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { filter: [ { bool: { - should: [{ match: { 'path.to.rule.id': 'mySecondAppAlertType' } }], + should: [{ match: { 'path.to.rule_type_id': 'mySecondAppAlertType' } }], minimum_should_match: 1, }, }, @@ -611,7 +613,7 @@ describe('asFiltersBySpaceId', () => { { type: AlertingAuthorizationFilterType.ESDSL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', spaceIds: 'path.to.space.id', }, @@ -629,7 +631,7 @@ describe('asFiltersBySpaceId', () => { { type: AlertingAuthorizationFilterType.KQL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', spaceIds: 'path.to.space.id', }, @@ -645,7 +647,7 @@ describe('asFiltersBySpaceId', () => { { type: AlertingAuthorizationFilterType.ESDSL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', }, }, @@ -660,7 +662,7 @@ describe('asFiltersBySpaceId', () => { { type: AlertingAuthorizationFilterType.ESDSL, fieldNames: { - ruleTypeId: 'path.to.rule.id', + ruleTypeId: 'path.to.rule_type_id', consumer: 'consumer-field', spaceIds: 'path.to.space.id', }, diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx index 24257bcefa7f1..f969b5802e8b6 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx @@ -19,7 +19,7 @@ import { import { EuiTitle } from '@elastic/eui'; import d3 from 'd3'; import React, { Suspense, useState } from 'react'; -import { RULE_ID } from '@kbn/rule-data-utils/target/technical_field_names'; +import { ALERT_RULE_TYPE_ID } from '@kbn/rule-data-utils/target/technical_field_names'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { asRelativeDateTimeRange } from '../../../../../common/utils/formatters'; @@ -124,7 +124,7 @@ export function ErrorDistribution({ distribution, title }: Props) { /> {getAlertAnnotations({ alerts: alerts?.filter( - (alert) => alert[RULE_ID]?.[0] === AlertType.ErrorCount + (alert) => alert[ALERT_RULE_TYPE_ID]?.[0] === AlertType.ErrorCount ), chartStartTime: buckets[0]?.x0, getFormatter, diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.test.tsx index 81c4af44c90a3..0f09b042a587b 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.test.tsx @@ -8,15 +8,19 @@ import { ALERT_DURATION, ALERT_EVALUATION_THRESHOLD, + ALERT_RULE_TYPE_ID, ALERT_EVALUATION_VALUE, ALERT_ID, - ALERT_PRODUCER, - ALERT_OWNER, + ALERT_RULE_PRODUCER, + ALERT_RULE_CONSUMER, ALERT_SEVERITY_LEVEL, ALERT_START, ALERT_STATUS, ALERT_UUID, SPACE_IDS, + ALERT_RULE_UUID, + ALERT_RULE_NAME, + ALERT_RULE_CATEGORY, } from '@kbn/rule-data-utils'; import { ValuesType } from 'utility-types'; import { EuiTheme } from '../../../../../../../../src/plugins/kibana_react/common'; @@ -34,20 +38,19 @@ const theme = ({ eui: { euiColorDanger, euiColorWarning }, } as unknown) as EuiTheme; const alert: Alert = { - [SPACE_IDS]: ['space-id'], - 'rule.id': ['apm.transaction_duration'], + [ALERT_RULE_TYPE_ID]: ['apm.transaction_duration'], [ALERT_EVALUATION_VALUE]: [2057657.39], 'service.name': ['frontend-rum'], - 'rule.name': ['Latency threshold | frontend-rum'], + [ALERT_RULE_NAME]: ['Latency threshold | frontend-rum'], [ALERT_DURATION]: [62879000], [ALERT_STATUS]: ['open'], [SPACE_IDS]: ['myfakespaceid'], tags: ['apm', 'service.name:frontend-rum'], 'transaction.type': ['page-load'], - [ALERT_PRODUCER]: ['apm'], + [ALERT_RULE_PRODUCER]: ['apm'], [ALERT_UUID]: ['af2ae371-df79-4fca-b0eb-a2dbd9478180'], - [ALERT_OWNER]: ['apm'], - 'rule.uuid': ['82e0ee40-c2f4-11eb-9a42-a9da66a1722f'], + [ALERT_RULE_CONSUMER]: ['apm'], + [ALERT_RULE_UUID]: ['82e0ee40-c2f4-11eb-9a42-a9da66a1722f'], 'event.action': ['active'], '@timestamp': ['2021-06-01T16:16:05.183Z'], [ALERT_ID]: ['apm.transaction_duration_All'], @@ -55,7 +58,7 @@ const alert: Alert = { [ALERT_EVALUATION_THRESHOLD]: [500000], [ALERT_START]: ['2021-06-01T16:15:02.304Z'], 'event.kind': ['state'], - 'rule.category': ['Latency threshold'], + [ALERT_RULE_CATEGORY]: ['Latency threshold'], }; const chartStartTime = new Date(alert[ALERT_START]![0] as string).getTime(); const getFormatter: ObservabilityRuleTypeRegistry['getFormatter'] = () => () => ({ @@ -135,7 +138,7 @@ describe('getAlertAnnotations', () => { setSelectedAlertId, theme, })![0].props.dataValues[0].details - ).toEqual(alert['rule.name']![0]); + ).toEqual(alert[ALERT_RULE_NAME]![0]); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx index fa0725018f783..31a8cbf44ea27 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx @@ -17,8 +17,8 @@ import { ALERT_SEVERITY_LEVEL, ALERT_START, ALERT_UUID, - RULE_ID, - RULE_NAME, + ALERT_RULE_TYPE_ID, + ALERT_RULE_NAME, } from '@kbn/rule-data-utils/target/technical_field_names'; import React, { Dispatch, SetStateAction } from 'react'; import { EuiTheme } from 'src/plugins/kibana_react/common'; @@ -106,10 +106,10 @@ export function getAlertAnnotations({ const severityLevel = parsed[ALERT_SEVERITY_LEVEL]; const color = getAlertColor({ severityLevel, theme }); const header = getAlertHeader({ severityLevel }); - const formatter = getFormatter(parsed[RULE_ID]!); + const formatter = getFormatter(parsed[ALERT_RULE_TYPE_ID]!); const formatted = { link: undefined, - reason: parsed[RULE_NAME], + reason: parsed[ALERT_RULE_NAME], ...(formatter?.({ fields: parsed, formatters: { asDuration, asPercent }, diff --git a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx index 1a89f070bb5cd..d2df99ba29197 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx @@ -9,7 +9,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSelect, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { useHistory } from 'react-router-dom'; -import { RULE_ID } from '@kbn/rule-data-utils/target/technical_field_names'; +import { ALERT_RULE_TYPE_ID } from '@kbn/rule-data-utils/target/technical_field_names'; import { AlertType } from '../../../../../common/alert_types'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types'; @@ -128,8 +128,10 @@ export function LatencyChart({ height }: Props) { anomalyTimeseries={anomalyTimeseries} alerts={alerts.filter( (alert) => - alert[RULE_ID]?.[0] === AlertType.TransactionDuration || - alert[RULE_ID]?.[0] === AlertType.TransactionDurationAnomaly + alert[ALERT_RULE_TYPE_ID]?.[0] === + AlertType.TransactionDuration || + alert[ALERT_RULE_TYPE_ID]?.[0] === + AlertType.TransactionDurationAnomaly )} /> diff --git a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx index 71d517ad53871..39b7f488d68e6 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx @@ -8,12 +8,17 @@ import { ALERT_DURATION, ALERT_EVALUATION_THRESHOLD, + ALERT_RULE_TYPE_ID, ALERT_EVALUATION_VALUE, ALERT_ID, ALERT_SEVERITY_LEVEL, ALERT_START, ALERT_STATUS, ALERT_UUID, + ALERT_RULE_UUID, + ALERT_RULE_NAME, + ALERT_RULE_CATEGORY, + ALERT_RULE_PRODUCER, } from '@kbn/rule-data-utils'; import { StoryContext } from '@storybook/react'; import React, { ComponentType } from 'react'; @@ -120,17 +125,17 @@ Example.args = { alertsResponse: { alerts: [ { - 'rule.id': ['apm.transaction_duration'], + [ALERT_RULE_TYPE_ID]: ['apm.transaction_duration'], [ALERT_EVALUATION_VALUE]: [2001708.19], 'service.name': ['frontend-rum'], - 'rule.name': ['Latency threshold | frontend-rum'], + [ALERT_RULE_NAME]: ['Latency threshold | frontend-rum'], [ALERT_DURATION]: [10000000000], [ALERT_STATUS]: ['open'], tags: ['apm', 'service.name:frontend-rum'], 'transaction.type': ['page-load'], - 'kibana.alert.producer': ['apm'], + [ALERT_RULE_PRODUCER]: ['apm'], [ALERT_UUID]: ['af2ae371-df79-4fca-b0eb-a2dbd9478180'], - 'rule.uuid': ['82e0ee40-c2f4-11eb-9a42-a9da66a1722f'], + [ALERT_RULE_UUID]: ['82e0ee40-c2f4-11eb-9a42-a9da66a1722f'], 'event.action': ['active'], '@timestamp': ['2021-06-01T20:27:48.833Z'], [ALERT_ID]: ['apm.transaction_duration_All'], @@ -138,21 +143,21 @@ Example.args = { [ALERT_EVALUATION_THRESHOLD]: [500000], [ALERT_START]: ['2021-06-02T04:00:00.000Z'], 'event.kind': ['state'], - 'rule.category': ['Latency threshold'], + [ALERT_RULE_CATEGORY]: ['Latency threshold'], }, { - 'rule.id': ['apm.transaction_duration'], + [ALERT_RULE_TYPE_ID]: ['apm.transaction_duration'], [ALERT_EVALUATION_VALUE]: [2001708.19], 'service.name': ['frontend-rum'], - 'rule.name': ['Latency threshold | frontend-rum'], + [ALERT_RULE_NAME]: ['Latency threshold | frontend-rum'], [ALERT_DURATION]: [10000000000], [ALERT_STATUS]: ['open'], tags: ['apm', 'service.name:frontend-rum'], 'transaction.type': ['page-load'], - 'kibana.alert.producer': ['apm'], + [ALERT_RULE_PRODUCER]: ['apm'], [ALERT_SEVERITY_LEVEL]: ['warning'], [ALERT_UUID]: ['af2ae371-df79-4fca-b0eb-a2dbd9478181'], - 'rule.uuid': ['82e0ee40-c2f4-11eb-9a42-a9da66a1722f'], + [ALERT_RULE_UUID]: ['82e0ee40-c2f4-11eb-9a42-a9da66a1722f'], 'event.action': ['active'], '@timestamp': ['2021-06-01T20:27:48.833Z'], [ALERT_ID]: ['apm.transaction_duration_All'], @@ -160,21 +165,21 @@ Example.args = { [ALERT_EVALUATION_THRESHOLD]: [500000], [ALERT_START]: ['2021-06-02T10:45:00.000Z'], 'event.kind': ['state'], - 'rule.category': ['Latency threshold'], + [ALERT_RULE_CATEGORY]: ['Latency threshold'], }, { - 'rule.id': ['apm.transaction_duration'], + [ALERT_RULE_TYPE_ID]: ['apm.transaction_duration'], [ALERT_EVALUATION_VALUE]: [2001708.19], 'service.name': ['frontend-rum'], - 'rule.name': ['Latency threshold | frontend-rum'], + [ALERT_RULE_NAME]: ['Latency threshold | frontend-rum'], [ALERT_DURATION]: [1000000000], [ALERT_STATUS]: ['open'], tags: ['apm', 'service.name:frontend-rum'], 'transaction.type': ['page-load'], - 'kibana.alert.producer': ['apm'], + [ALERT_RULE_PRODUCER]: ['apm'], [ALERT_SEVERITY_LEVEL]: ['critical'], [ALERT_UUID]: ['af2ae371-df79-4fca-b0eb-a2dbd9478182'], - 'rule.uuid': ['82e0ee40-c2f4-11eb-9a42-a9da66a1722f'], + [ALERT_RULE_UUID]: ['82e0ee40-c2f4-11eb-9a42-a9da66a1722f'], 'event.action': ['active'], '@timestamp': ['2021-06-01T20:27:48.833Z'], [ALERT_ID]: ['apm.transaction_duration_All'], @@ -182,7 +187,7 @@ Example.args = { [ALERT_EVALUATION_THRESHOLD]: [500000], [ALERT_START]: ['2021-06-02T16:50:00.000Z'], 'event.kind': ['state'], - 'rule.category': ['Latency threshold'], + [ALERT_RULE_CATEGORY]: ['Latency threshold'], }, ], }, diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx index 18c765c50fbf7..226f9c095c2c3 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx @@ -8,7 +8,7 @@ import { EuiPanel, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { RULE_ID } from '../../../../../../rule_registry/common/technical_rule_data_field_names'; +import { ALERT_RULE_TYPE_ID } from '../../../../../../rule_registry/common/technical_rule_data_field_names'; import { AlertType } from '../../../../../common/alert_types'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { asPercent } from '../../../../../common/utils/formatters'; @@ -151,7 +151,8 @@ export function TransactionErrorRateChart({ yDomain={{ min: 0, max: 1 }} customTheme={comparisonChartThem} alerts={alerts.filter( - (alert) => alert[RULE_ID]?.[0] === AlertType.TransactionErrorRate + (alert) => + alert[ALERT_RULE_TYPE_ID]?.[0] === AlertType.TransactionErrorRate )} /> diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx index 53b5300e556c5..7c23aa8582ece 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx @@ -26,8 +26,8 @@ import { ALERT_EVALUATION_VALUE, ALERT_SEVERITY_LEVEL, ALERT_UUID, - RULE_CATEGORY, - RULE_NAME, + ALERT_RULE_CATEGORY, + ALERT_RULE_NAME, } from '@kbn/rule-data-utils/target/technical_field_names'; import moment from 'moment-timezone'; import React, { useMemo } from 'react'; @@ -113,7 +113,7 @@ export function AlertsFlyout({ title: i18n.translate('xpack.observability.alertsFlyout.ruleTypeLabel', { defaultMessage: 'Rule type', }), - description: alertData.fields[RULE_CATEGORY] ?? '-', + description: alertData.fields[ALERT_RULE_CATEGORY] ?? '-', }, ]; @@ -121,7 +121,7 @@ export function AlertsFlyout({ -

    {alertData.fields[RULE_NAME]}

    +

    {alertData.fields[ALERT_RULE_NAME]}

    {alertData.reason} diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx index 4bc9c40e6e917..798be42fce5cd 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx @@ -16,7 +16,7 @@ import { ALERT_SEVERITY_LEVEL, ALERT_STATUS, ALERT_START, - RULE_NAME, + ALERT_RULE_NAME, } from '@kbn/rule-data-utils/target/technical_field_names'; import type { TimelinesUIStart } from '../../../../timelines/public'; @@ -108,7 +108,7 @@ export const columns: Array< defaultMessage: 'Reason', }), linkField: '*', - id: RULE_NAME, + id: ALERT_RULE_NAME, }, ]; diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid_actions.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid_actions.tsx index 1f5372c8f2fea..2ba105113fec9 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid_actions.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid_actions.tsx @@ -14,7 +14,10 @@ import { EuiPopoverTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { RULE_ID, RULE_NAME } from '@kbn/rule-data-utils/target/technical_field_names'; +import { + ALERT_RULE_TYPE_ID, + ALERT_RULE_NAME, +} from '@kbn/rule-data-utils/target/technical_field_names'; import React, { useState } from 'react'; import { format, parse } from 'url'; @@ -29,10 +32,10 @@ export function RowCellActionsRender({ data }: ActionProps) { const { prepend } = core.http.basePath; const dataFieldEs = data.reduce((acc, d) => ({ ...acc, [d.field]: d.value }), {}); const parsedFields = parseTechnicalFields(dataFieldEs); - const formatter = observabilityRuleTypeRegistry.getFormatter(parsedFields[RULE_ID]!); + const formatter = observabilityRuleTypeRegistry.getFormatter(parsedFields[ALERT_RULE_TYPE_ID]!); const formatted = { link: undefined, - reason: parsedFields[RULE_NAME]!, + reason: parsedFields[ALERT_RULE_NAME]!, ...(formatter?.({ fields: parsedFields, formatters: { asDuration, asPercent } }) ?? {}), }; diff --git a/x-pack/plugins/observability/public/pages/alerts/decorate_response.ts b/x-pack/plugins/observability/public/pages/alerts/decorate_response.ts index e177bea6c6dac..f09a735de97be 100644 --- a/x-pack/plugins/observability/public/pages/alerts/decorate_response.ts +++ b/x-pack/plugins/observability/public/pages/alerts/decorate_response.ts @@ -6,8 +6,8 @@ */ import { - RULE_ID, - RULE_NAME, + ALERT_RULE_TYPE_ID, + ALERT_RULE_NAME, ALERT_STATUS, ALERT_START, } from '@kbn/rule-data-utils/target/technical_field_names'; @@ -22,10 +22,10 @@ export function decorateResponse( ): TopAlert[] { return alerts.map((alert) => { const parsedFields = parseTechnicalFields(alert); - const formatter = observabilityRuleTypeRegistry.getFormatter(parsedFields[RULE_ID]!); + const formatter = observabilityRuleTypeRegistry.getFormatter(parsedFields[ALERT_RULE_TYPE_ID]!); const formatted = { link: undefined, - reason: parsedFields[RULE_NAME]!, + reason: parsedFields[ALERT_RULE_NAME]!, ...(formatter?.({ fields: parsedFields, formatters: { asDuration, asPercent } }) ?? {}), }; diff --git a/x-pack/plugins/observability/public/pages/alerts/example_data.ts b/x-pack/plugins/observability/public/pages/alerts/example_data.ts index 8bb15682dc619..112932d49311c 100644 --- a/x-pack/plugins/observability/public/pages/alerts/example_data.ts +++ b/x-pack/plugins/observability/public/pages/alerts/example_data.ts @@ -11,49 +11,54 @@ import { ALERT_ID, ALERT_SEVERITY_LEVEL, ALERT_SEVERITY_VALUE, + ALERT_RULE_TYPE_ID, ALERT_START, ALERT_STATUS, ALERT_UUID, + ALERT_RULE_UUID, + ALERT_RULE_NAME, + ALERT_RULE_CATEGORY, + ALERT_RULE_PRODUCER, } from '@kbn/rule-data-utils'; export const apmAlertResponseExample = [ { - 'rule.id': ['apm.error_rate'], + [ALERT_RULE_TYPE_ID]: ['apm.error_rate'], 'service.name': ['opbeans-java'], - 'rule.name': ['Error count threshold | opbeans-java (smith test)'], + [ALERT_RULE_NAME]: ['Error count threshold | opbeans-java (smith test)'], [ALERT_DURATION]: [180057000], [ALERT_STATUS]: ['open'], [ALERT_SEVERITY_LEVEL]: ['warning'], tags: ['apm', 'service.name:opbeans-java'], [ALERT_UUID]: ['0175ec0a-a3b1-4d41-b557-e21c2d024352'], - 'rule.uuid': ['474920d0-93e9-11eb-ac86-0b455460de81'], + [ALERT_RULE_UUID]: ['474920d0-93e9-11eb-ac86-0b455460de81'], 'event.action': ['active'], '@timestamp': ['2021-04-12T13:53:49.550Z'], [ALERT_ID]: ['apm.error_rate_opbeans-java_production'], [ALERT_START]: ['2021-04-12T13:50:49.493Z'], - 'kibana.producer': ['apm'], + [ALERT_RULE_PRODUCER]: ['apm'], 'event.kind': ['state'], - 'rule.category': ['Error count threshold'], + [ALERT_RULE_CATEGORY]: ['Error count threshold'], 'service.environment': ['production'], 'processor.event': ['error'], }, { - 'rule.id': ['apm.error_rate'], + [ALERT_RULE_TYPE_ID]: ['apm.error_rate'], 'service.name': ['opbeans-java'], - 'rule.name': ['Error count threshold | opbeans-java (smith test)'], + [ALERT_RULE_NAME]: ['Error count threshold | opbeans-java (smith test)'], [ALERT_DURATION]: [2419005000], [ALERT_END]: ['2021-04-12T13:49:49.446Z'], [ALERT_STATUS]: ['closed'], tags: ['apm', 'service.name:opbeans-java'], [ALERT_UUID]: ['32b940e1-3809-4c12-8eee-f027cbb385e2'], - 'rule.uuid': ['474920d0-93e9-11eb-ac86-0b455460de81'], + [ALERT_RULE_UUID]: ['474920d0-93e9-11eb-ac86-0b455460de81'], 'event.action': ['close'], '@timestamp': ['2021-04-12T13:49:49.446Z'], [ALERT_ID]: ['apm.error_rate_opbeans-java_production'], [ALERT_START]: ['2021-04-12T13:09:30.441Z'], - 'kibana.producer': ['apm'], + [ALERT_RULE_PRODUCER]: ['apm'], 'event.kind': ['state'], - 'rule.category': ['Error count threshold'], + [ALERT_RULE_CATEGORY]: ['Error count threshold'], 'service.environment': ['production'], 'processor.event': ['error'], }, @@ -158,7 +163,7 @@ export const dynamicIndexPattern = { readFromDocValues: true, }, { - name: 'kibana.producer', + name: [ALERT_RULE_PRODUCER], type: 'string', esTypes: ['keyword'], searchable: true, @@ -174,7 +179,7 @@ export const dynamicIndexPattern = { readFromDocValues: true, }, { - name: 'rule.category', + name: [ALERT_RULE_CATEGORY], type: 'string', esTypes: ['keyword'], searchable: true, @@ -182,7 +187,7 @@ export const dynamicIndexPattern = { readFromDocValues: true, }, { - name: 'rule.id', + name: [ALERT_RULE_TYPE_ID], type: 'string', esTypes: ['keyword'], searchable: true, @@ -190,7 +195,7 @@ export const dynamicIndexPattern = { readFromDocValues: true, }, { - name: 'rule.name', + name: [ALERT_RULE_NAME], type: 'string', esTypes: ['keyword'], searchable: true, @@ -198,7 +203,7 @@ export const dynamicIndexPattern = { readFromDocValues: true, }, { - name: 'rule.uuid', + name: [ALERT_RULE_UUID], type: 'string', esTypes: ['keyword'], searchable: true, diff --git a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx index cac3240cd2004..560926bf20e87 100644 --- a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx @@ -12,7 +12,7 @@ import { ALERT_SEVERITY_LEVEL, ALERT_STATUS, ALERT_START, - RULE_NAME, + ALERT_RULE_NAME, } from '@kbn/rule-data-utils/target/technical_field_names'; import type { CellValueElementProps, TimelineNonEcsData } from '../../../../timelines/common'; @@ -93,7 +93,7 @@ export const getRenderCellValue = ({ return asDuration(Number(value)); case ALERT_SEVERITY_LEVEL: return ; - case RULE_NAME: + case ALERT_RULE_NAME: const dataFieldEs = data.reduce((acc, d) => ({ ...acc, [d.field]: d.value }), {}); const decoratedAlerts = decorateResponse( [dataFieldEs] ?? [], diff --git a/x-pack/plugins/rule_registry/README.md b/x-pack/plugins/rule_registry/README.md index 16e4b8f3e01e6..ef9a3252c41d7 100644 --- a/x-pack/plugins/rule_registry/README.md +++ b/x-pack/plugins/rule_registry/README.md @@ -11,9 +11,11 @@ It also exposes a rule data client that will create or update the index stream t By default, these indices will be prefixed with `.alerts`. To change this, for instance to support legacy multitenancy, set the following configuration option: ```yaml -xpack.ruleRegistry.index: '.kibana-alerts' +xpack.ruleRegistry.index: 'myAlerts' ``` +The above produces an alerts index prefixed `.alerts-myAlerts`. + To disable writing entirely: ```yaml @@ -120,11 +122,11 @@ The following fields are defined in the technical field component template and s - `event.kind`: signal (for the changeable alert document), state (for the state changes of the alert, e.g. when it opens, recovers, or changes in severity), or metric (individual evaluations that might be related to an alert). - `event.action`: the reason for the event. This might be `open`, `close`, `active`, or `evaluate`. - `tags`: tags attached to the alert. Right now they are copied over from the rule. -- `rule.id`: the identifier of the rule type, e.g. `apm.transaction_duration` -- `rule.uuid`: the saved objects id of the rule. -- `rule.name`: the name of the rule (as specified by the user). -- `rule.category`: the name of the rule type (as defined by the rule type producer) -- `kibana.alert.owner`: the feature which produced the alert. Usually a Kibana feature id like `apm`, `siem`... +- `kibana.alert.rule.rule_type_id`: the identifier of the rule type, e.g. `apm.transaction_duration` +- `kibana.alert.rule.uuid`: the saved objects id of the rule. +- `kibana.alert.rule.name`: the name of the rule (as specified by the user). +- `kibana.alert.rule.category`: the name of the rule type (as defined by the rule type producer) +- `kibana.alert.rule.consumer`: the feature which produced the alert (inherited from the rule producer field). Usually a Kibana feature id like `apm`, `siem`... - `kibana.alert.id`: the id of the alert, that is unique within the context of the rule execution it was created in. E.g., for a rule that monitors latency for all services in all environments, this might be `opbeans-java:production`. - `kibana.alert.uuid`: the unique identifier for the alert during its lifespan. If an alert recovers (or closes), this identifier is re-generated when it is opened again. - `kibana.alert.status`: the status of the alert. Can be `open` or `closed`. diff --git a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts index 11e572260d133..eb8d88cf697b9 100644 --- a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts +++ b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts @@ -14,14 +14,11 @@ export const technicalRuleFieldMap = { Fields.TIMESTAMP, Fields.EVENT_KIND, Fields.EVENT_ACTION, - Fields.RULE_UUID, - Fields.RULE_ID, - Fields.RULE_NAME, - Fields.RULE_CATEGORY, Fields.TAGS ), - [Fields.ALERT_OWNER]: { type: 'keyword', required: true }, - [Fields.ALERT_PRODUCER]: { type: 'keyword' }, + [Fields.ALERT_RULE_TYPE_ID]: { type: 'keyword', required: true }, + [Fields.ALERT_RULE_CONSUMER]: { type: 'keyword', required: true }, + [Fields.ALERT_RULE_PRODUCER]: { type: 'keyword' }, [Fields.SPACE_IDS]: { type: 'keyword', array: true, required: true }, [Fields.ALERT_UUID]: { type: 'keyword' }, [Fields.ALERT_ID]: { type: 'keyword' }, @@ -33,11 +30,6 @@ export const technicalRuleFieldMap = { [Fields.ALERT_STATUS]: { type: 'keyword' }, [Fields.ALERT_EVALUATION_THRESHOLD]: { type: 'scaled_float', scaling_factor: 100 }, [Fields.ALERT_EVALUATION_VALUE]: { type: 'scaled_float', scaling_factor: 100 }, - [Fields.CONSUMERS]: { - type: 'keyword', - array: true, - required: false, - }, [Fields.VERSION]: { type: 'keyword', array: false, @@ -93,9 +85,19 @@ export const technicalRuleFieldMap = { array: false, required: false, }, - [Fields.ALERT_RULE_CONSUMERS]: { + [Fields.ALERT_RULE_CATEGORY]: { type: 'keyword', - array: true, + array: false, + required: false, + }, + [Fields.ALERT_RULE_UUID]: { + type: 'keyword', + array: false, + required: false, + }, + [Fields.ALERT_RULE_ID]: { + type: 'keyword', + array: false, required: false, }, [Fields.ALERT_RULE_CREATED_AT]: { @@ -123,11 +125,6 @@ export const technicalRuleFieldMap = { array: false, required: false, }, - [Fields.ALERT_RULE_ID]: { - type: 'keyword', - array: false, - required: false, - }, [Fields.ALERT_RULE_INTERVAL]: { type: 'keyword', array: false, diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts index 2a7419b20570e..aaceb167b2e51 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts @@ -29,8 +29,8 @@ import { alertAuditEvent, operationAlertAuditActionMap } from './audit_events'; import { AuditLogger } from '../../../security/server'; import { ALERT_STATUS, - ALERT_OWNER, - RULE_ID, + ALERT_RULE_CONSUMER, + ALERT_RULE_TYPE_ID, SPACE_IDS, } from '../../common/technical_rule_data_field_names'; import { ParsedTechnicalFields } from '../../common/parse_technical_fields'; @@ -40,11 +40,15 @@ type NonNullableProps = Omit }; type AlertType = NonNullableProps< ParsedTechnicalFields, - typeof RULE_ID | typeof ALERT_OWNER | typeof SPACE_IDS + typeof ALERT_RULE_TYPE_ID | typeof ALERT_RULE_CONSUMER | typeof SPACE_IDS >; const isValidAlert = (source?: ParsedTechnicalFields): source is AlertType => { - return source?.[RULE_ID] != null && source?.[ALERT_OWNER] != null && source?.[SPACE_IDS] != null; + return ( + source?.[ALERT_RULE_TYPE_ID] != null && + source?.[ALERT_RULE_CONSUMER] != null && + source?.[SPACE_IDS] != null + ); }; export interface ConstructorOptions { logger: Logger; @@ -121,7 +125,10 @@ export class AlertsClient { _id: string; // this is typed kind of crazy to fit the output of es api response to this _source?: - | { [RULE_ID]?: string | null | undefined; [ALERT_OWNER]?: string | null | undefined } + | { + [ALERT_RULE_TYPE_ID]?: string | null | undefined; + [ALERT_RULE_CONSUMER]?: string | null | undefined; + } | null | undefined; }>, @@ -132,16 +139,16 @@ export class AlertsClient { hitIds: [hit._id, ...acc.hitIds], ownersAndRuleTypeIds: [ { - [RULE_ID]: hit?._source?.[RULE_ID], - [ALERT_OWNER]: hit?._source?.[ALERT_OWNER], + [ALERT_RULE_TYPE_ID]: hit?._source?.[ALERT_RULE_TYPE_ID], + [ALERT_RULE_CONSUMER]: hit?._source?.[ALERT_RULE_CONSUMER], }, ], }), { hitIds: [], ownersAndRuleTypeIds: [] } as { hitIds: string[]; ownersAndRuleTypeIds: Array<{ - [RULE_ID]: string | null | undefined; - [ALERT_OWNER]: string | null | undefined; + [ALERT_RULE_TYPE_ID]: string | null | undefined; + [ALERT_RULE_CONSUMER]: string | null | undefined; }>; } ); @@ -150,8 +157,8 @@ export class AlertsClient { return Promise.all( ownersAndRuleTypeIds.map((hit) => { - const alertOwner = hit?.[ALERT_OWNER]; - const ruleId = hit?.[RULE_ID]; + const alertOwner = hit?.[ALERT_RULE_CONSUMER]; + const ruleId = hit?.[ALERT_RULE_TYPE_ID]; if (hit != null && assertString(alertOwner) && assertString(ruleId)) { return this.authorization.ensureAuthorized({ ruleTypeId: ruleId, @@ -322,7 +329,7 @@ export class AlertsClient { AlertingAuthorizationEntity.Alert, { type: AlertingAuthorizationFilterType.ESDSL, - fieldNames: { consumer: ALERT_OWNER, ruleTypeId: RULE_ID }, + fieldNames: { consumer: ALERT_RULE_CONSUMER, ruleTypeId: ALERT_RULE_TYPE_ID }, }, operation ); diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts index 97a19935fa787..a6d42853531d7 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/bulk_update.test.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { ALERT_OWNER, ALERT_STATUS, SPACE_IDS, RULE_ID } from '@kbn/rule-data-utils'; +import { + ALERT_RULE_CONSUMER, + ALERT_STATUS, + SPACE_IDS, + ALERT_RULE_TYPE_ID, +} from '@kbn/rule-data-utils'; import { AlertsClient, ConstructorOptions } from '../alerts_client'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -77,8 +82,8 @@ describe('bulkUpdate()', () => { _id: fakeAlertId, _index: indexName, _source: { - [RULE_ID]: 'apm.error_rate', - [ALERT_OWNER]: 'apm', + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, @@ -134,8 +139,8 @@ describe('bulkUpdate()', () => { _id: fakeAlertId, _index: indexName, _source: { - [RULE_ID]: fakeRuleTypeId, - [ALERT_OWNER]: 'apm', + [ALERT_RULE_TYPE_ID]: fakeRuleTypeId, + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, @@ -180,8 +185,8 @@ describe('bulkUpdate()', () => { _id: successfulAuthzHit, _index: indexName, _source: { - [RULE_ID]: 'apm.error_rate', - [ALERT_OWNER]: 'apm', + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, @@ -190,8 +195,8 @@ describe('bulkUpdate()', () => { _id: unsuccessfulAuthzHit, _index: indexName, _source: { - [RULE_ID]: fakeRuleTypeId, - [ALERT_OWNER]: 'apm', + [ALERT_RULE_TYPE_ID]: fakeRuleTypeId, + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, @@ -267,8 +272,8 @@ describe('bulkUpdate()', () => { _id: fakeAlertId, _index: '.alerts-observability-apm.alerts', _source: { - [RULE_ID]: 'apm.error_rate', - [ALERT_OWNER]: 'apm', + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, @@ -327,8 +332,8 @@ describe('bulkUpdate()', () => { _id: fakeAlertId, _index: '.alerts-observability-apm.alerts', _source: { - [RULE_ID]: fakeRuleTypeId, - [ALERT_OWNER]: 'apm', + [ALERT_RULE_TYPE_ID]: fakeRuleTypeId, + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, @@ -388,8 +393,8 @@ describe('bulkUpdate()', () => { _id: successfulAuthzHit, _index: '.alerts-observability-apm.alerts', _source: { - [RULE_ID]: 'apm.error_rate', - [ALERT_OWNER]: 'apm', + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, @@ -398,8 +403,8 @@ describe('bulkUpdate()', () => { _id: unsuccessfulAuthzHit, _index: '.alerts-observability-apm.alerts', _source: { - [RULE_ID]: fakeRuleTypeId, - [ALERT_OWNER]: 'apm', + [ALERT_RULE_TYPE_ID]: fakeRuleTypeId, + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts index 651d728b1983c..c8d0d18dfd37e 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get.test.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { ALERT_OWNER, ALERT_STATUS, RULE_ID, SPACE_IDS } from '@kbn/rule-data-utils'; +import { + ALERT_RULE_CONSUMER, + ALERT_STATUS, + SPACE_IDS, + ALERT_RULE_TYPE_ID, +} from '@kbn/rule-data-utils'; import { AlertsClient, ConstructorOptions } from '../alerts_client'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -85,9 +90,9 @@ describe('get()', () => { _seq_no: 362, _primary_term: 2, _source: { - 'rule.id': 'apm.error_rate', + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', message: 'hello world 1', - [ALERT_OWNER]: 'apm', + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: ['test_default_space_id'], }, @@ -100,13 +105,13 @@ describe('get()', () => { const result = await alertsClient.get({ id: '1', index: '.alerts-observability-apm' }); expect(result).toMatchInlineSnapshot(` Object { - "kibana.alert.owner": "apm", + "kibana.alert.rule.consumer": "apm", + "kibana.alert.rule.rule_type_id": "apm.error_rate", "kibana.alert.status": "open", "kibana.space_ids": Array [ "test_default_space_id", ], "message": "hello world 1", - "rule.id": "apm.error_rate", } `); expect(esClientMock.search).toHaveBeenCalledTimes(1); @@ -184,9 +189,9 @@ describe('get()', () => { _seq_no: 362, _primary_term: 2, _source: { - 'rule.id': 'apm.error_rate', + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', message: 'hello world 1', - [ALERT_OWNER]: 'apm', + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: ['test_default_space_id'], }, @@ -235,8 +240,8 @@ describe('get()', () => { _id: fakeAlertId, _index: indexName, _source: { - [RULE_ID]: fakeRuleTypeId, - [ALERT_OWNER]: 'apm', + [ALERT_RULE_TYPE_ID]: fakeRuleTypeId, + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, @@ -307,9 +312,9 @@ describe('get()', () => { _seq_no: 362, _primary_term: 2, _source: { - 'rule.id': 'apm.error_rate', + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', message: 'hello world 1', - [ALERT_OWNER]: 'apm', + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: ['test_default_space_id'], }, @@ -330,13 +335,13 @@ describe('get()', () => { expect(result).toMatchInlineSnapshot(` Object { - "kibana.alert.owner": "apm", + "kibana.alert.rule.consumer": "apm", + "kibana.alert.rule.rule_type_id": "apm.error_rate", "kibana.alert.status": "open", "kibana.space_ids": Array [ "test_default_space_id", ], "message": "hello world 1", - "rule.id": "apm.error_rate", } `); }); diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts index 435b6e310ffdf..0aaab20052716 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/update.test.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { ALERT_OWNER, ALERT_STATUS, SPACE_IDS, RULE_ID } from '@kbn/rule-data-utils'; +import { + ALERT_RULE_CONSUMER, + ALERT_STATUS, + SPACE_IDS, + ALERT_RULE_TYPE_ID, +} from '@kbn/rule-data-utils'; import { AlertsClient, ConstructorOptions } from '../alerts_client'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -82,9 +87,9 @@ describe('update()', () => { _index: '.alerts-observability-apm', _id: 'NoxgpHkBqbdrfX07MqXV', _source: { - [RULE_ID]: 'apm.error_rate', + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', message: 'hello world 1', - [ALERT_OWNER]: 'apm', + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, @@ -168,9 +173,9 @@ describe('update()', () => { _index: '.alerts-observability-apm', _id: 'NoxgpHkBqbdrfX07MqXV', _source: { - 'rule.id': 'apm.error_rate', + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', message: 'hello world 1', - [ALERT_OWNER]: 'apm', + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, @@ -242,8 +247,8 @@ describe('update()', () => { _id: fakeAlertId, _index: indexName, _source: { - [RULE_ID]: fakeRuleTypeId, - [ALERT_OWNER]: 'apm', + [ALERT_RULE_TYPE_ID]: fakeRuleTypeId, + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, @@ -323,9 +328,9 @@ describe('update()', () => { _index: '.alerts-observability-apm', _id: 'NoxgpHkBqbdrfX07MqXV', _source: { - 'rule.id': 'apm.error_rate', + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', message: 'hello world 1', - [ALERT_OWNER]: 'apm', + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, @@ -383,9 +388,9 @@ describe('update()', () => { _seq_no: 362, _primary_term: 2, _source: { - 'rule.id': 'apm.error_rate', + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', message: 'hello world 1', - [ALERT_OWNER]: 'apm', + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [SPACE_IDS]: [DEFAULT_SPACE], }, diff --git a/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.test.ts b/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.test.ts index 073a48248f89a..372fb09661259 100644 --- a/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.test.ts +++ b/x-pack/plugins/rule_registry/server/routes/get_alert_by_id.test.ts @@ -6,13 +6,11 @@ */ import { - ALERT_OWNER, + ALERT_RULE_CONSUMER, ALERT_RULE_RISK_SCORE, - ALERT_RULE_SEVERITY, ALERT_STATUS, - CONSUMERS, ECS_VERSION, - RULE_ID, + ALERT_RULE_TYPE_ID, SPACE_IDS, TIMESTAMP, VERSION, @@ -28,14 +26,12 @@ import { requestMock, serverMock } from './__mocks__/server'; const getMockAlert = (): ParsedTechnicalFields => ({ [TIMESTAMP]: '2021-06-21T21:33:05.713Z', [ECS_VERSION]: '1.0.0', - [CONSUMERS]: [], [VERSION]: '7.13.0', - [RULE_ID]: 'apm.error_rate', - [ALERT_OWNER]: 'apm', + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', + [ALERT_RULE_CONSUMER]: 'apm', [ALERT_STATUS]: 'open', [ALERT_RULE_RISK_SCORE]: 20, [SPACE_IDS]: ['fake-space-id'], - [ALERT_RULE_SEVERITY]: 'warning', }); describe('getAlertByIdRoute', () => { diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts index 037efadabd8de..efcc56a1b9511 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts @@ -23,8 +23,8 @@ import { ALERT_STATUS, EVENT_ACTION, EVENT_KIND, - RULE_ID, - ALERT_OWNER, + ALERT_RULE_TYPE_ID, + ALERT_RULE_CONSUMER, SPACE_IDS, } from '../../common/technical_rule_data_field_names'; import { createRuleDataClientMock } from '../rule_data_client/create_rule_data_client_mock'; @@ -131,16 +131,16 @@ describe('createLifecycleExecutor', () => { { fields: { [ALERT_ID]: 'TEST_ALERT_0', - [ALERT_OWNER]: 'CONSUMER', - [RULE_ID]: 'RULE_TYPE_ID', + [ALERT_RULE_CONSUMER]: 'CONSUMER', + [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc }, }, { fields: { [ALERT_ID]: 'TEST_ALERT_1', - [ALERT_OWNER]: 'CONSUMER', - [RULE_ID]: 'RULE_TYPE_ID', + [ALERT_RULE_CONSUMER]: 'CONSUMER', + [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc }, }, @@ -229,8 +229,8 @@ describe('createLifecycleExecutor', () => { fields: { '@timestamp': '', [ALERT_ID]: 'TEST_ALERT_0', - [ALERT_OWNER]: 'CONSUMER', - [RULE_ID]: 'RULE_TYPE_ID', + [ALERT_RULE_CONSUMER]: 'CONSUMER', + [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc }, @@ -239,8 +239,8 @@ describe('createLifecycleExecutor', () => { fields: { '@timestamp': '', [ALERT_ID]: 'TEST_ALERT_1', - [ALERT_OWNER]: 'CONSUMER', - [RULE_ID]: 'RULE_TYPE_ID', + [ALERT_RULE_CONSUMER]: 'CONSUMER', + [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc }, @@ -335,7 +335,7 @@ const createDefaultAlertExecutorOptions = < ActionGroupIds extends string = '' >({ alertId = 'ALERT_ID', - ruleName = 'RULE_NAME', + ruleName = 'ALERT_RULE_NAME', params, state, createdAt = new Date(), diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts index 23ae24cb91bc4..7a00457f2c4e1 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts @@ -29,9 +29,9 @@ import { ALERT_UUID, EVENT_ACTION, EVENT_KIND, - ALERT_OWNER, - RULE_ID, - RULE_UUID, + ALERT_RULE_CONSUMER, + ALERT_RULE_TYPE_ID, + ALERT_RULE_UUID, TIMESTAMP, SPACE_IDS, } from '../../common/technical_rule_data_field_names'; @@ -155,8 +155,8 @@ export const createLifecycleExecutor = ( currentAlerts[id] = { ...fields, [ALERT_ID]: id, - [RULE_ID]: rule.ruleTypeId, - [ALERT_OWNER]: rule.consumer, + [ALERT_RULE_TYPE_ID]: rule.ruleTypeId, + [ALERT_RULE_CONSUMER]: rule.consumer, }; return alertInstanceFactory(id); }, @@ -197,7 +197,7 @@ export const createLifecycleExecutor = ( filter: [ { term: { - [RULE_UUID]: ruleExecutorData[RULE_UUID], + [ALERT_RULE_UUID]: ruleExecutorData[ALERT_RULE_UUID], }, }, { @@ -229,8 +229,8 @@ export const createLifecycleExecutor = ( alertsDataMap[alertId] = { ...fields, [ALERT_ID]: alertId, - [RULE_ID]: rule.ruleTypeId, - [ALERT_OWNER]: rule.consumer, + [ALERT_RULE_TYPE_ID]: rule.ruleTypeId, + [ALERT_RULE_CONSUMER]: rule.consumer, }; }); } @@ -247,7 +247,7 @@ export const createLifecycleExecutor = ( ...ruleExecutorData, [TIMESTAMP]: timestamp, [EVENT_KIND]: 'signal', - [ALERT_OWNER]: rule.consumer, + [ALERT_RULE_CONSUMER]: rule.consumer, [ALERT_ID]: alertId, } as ParsedTechnicalFields; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts index c1358da97e95a..3469187122127 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts @@ -6,15 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { - ALERT_DURATION, - ALERT_ID, - ALERT_OWNER, - ALERT_PRODUCER, - ALERT_START, - ALERT_STATUS, - ALERT_UUID, -} from '@kbn/rule-data-utils'; +import { ALERT_DURATION, ALERT_STATUS, ALERT_UUID } from '@kbn/rule-data-utils'; import { loggerMock } from '@kbn/logging/target/mocks'; import { castArray, omit, mapValues } from 'lodash'; import { RuleDataClient } from '../rule_data_client'; @@ -197,19 +189,19 @@ describe('createLifecycleRuleTypeFactory', () => { "@timestamp": "2021-06-16T09:01:00.000Z", "event.action": "open", "event.kind": "signal", - "${ALERT_DURATION}": 0, - "${ALERT_ID}": "opbeans-java", - "${ALERT_OWNER}": "consumer", - "${ALERT_PRODUCER}": "producer", - "${ALERT_START}": "2021-06-16T09:01:00.000Z", - "${ALERT_STATUS}": "open", + "kibana.alert.duration.us": 0, + "kibana.alert.id": "opbeans-java", + "kibana.alert.rule.category": "ruleTypeName", + "kibana.alert.rule.consumer": "consumer", + "kibana.alert.rule.name": "name", + "kibana.alert.rule.producer": "producer", + "kibana.alert.rule.rule_type_id": "ruleTypeId", + "kibana.alert.rule.uuid": "alertId", + "kibana.alert.start": "2021-06-16T09:01:00.000Z", + "kibana.alert.status": "open", "kibana.space_ids": Array [ "spaceId", ], - "rule.category": "ruleTypeName", - "rule.id": "ruleTypeId", - "rule.name": "name", - "rule.uuid": "alertId", "service.name": "opbeans-java", "tags": Array [ "tags", @@ -219,19 +211,19 @@ describe('createLifecycleRuleTypeFactory', () => { "@timestamp": "2021-06-16T09:01:00.000Z", "event.action": "open", "event.kind": "signal", - "${ALERT_DURATION}": 0, - "${ALERT_ID}": "opbeans-node", - "${ALERT_OWNER}": "consumer", - "${ALERT_PRODUCER}": "producer", - "${ALERT_START}": "2021-06-16T09:01:00.000Z", - "${ALERT_STATUS}": "open", + "kibana.alert.duration.us": 0, + "kibana.alert.id": "opbeans-node", + "kibana.alert.rule.category": "ruleTypeName", + "kibana.alert.rule.consumer": "consumer", + "kibana.alert.rule.name": "name", + "kibana.alert.rule.producer": "producer", + "kibana.alert.rule.rule_type_id": "ruleTypeId", + "kibana.alert.rule.uuid": "alertId", + "kibana.alert.start": "2021-06-16T09:01:00.000Z", + "kibana.alert.status": "open", "kibana.space_ids": Array [ "spaceId", ], - "rule.category": "ruleTypeName", - "rule.id": "ruleTypeId", - "rule.name": "name", - "rule.uuid": "alertId", "service.name": "opbeans-node", "tags": Array [ "tags", diff --git a/x-pack/plugins/rule_registry/server/utils/get_rule_executor_data.ts b/x-pack/plugins/rule_registry/server/utils/get_rule_executor_data.ts index 866eb5f882fe0..13f0b27e85c3b 100644 --- a/x-pack/plugins/rule_registry/server/utils/get_rule_executor_data.ts +++ b/x-pack/plugins/rule_registry/server/utils/get_rule_executor_data.ts @@ -7,30 +7,30 @@ import { AlertExecutorOptions } from '../../../alerting/server'; import { - ALERT_PRODUCER, - RULE_CATEGORY, - RULE_ID, - RULE_NAME, - RULE_UUID, + ALERT_RULE_PRODUCER, + ALERT_RULE_CATEGORY, + ALERT_RULE_TYPE_ID, + ALERT_RULE_NAME, + ALERT_RULE_UUID, TAGS, } from '../../common/technical_rule_data_field_names'; export interface RuleExecutorData { - [RULE_CATEGORY]: string; - [RULE_ID]: string; - [RULE_UUID]: string; - [RULE_NAME]: string; - [ALERT_PRODUCER]: string; + [ALERT_RULE_CATEGORY]: string; + [ALERT_RULE_TYPE_ID]: string; + [ALERT_RULE_UUID]: string; + [ALERT_RULE_NAME]: string; + [ALERT_RULE_PRODUCER]: string; [TAGS]: string[]; } export function getRuleData(options: AlertExecutorOptions) { return { - [RULE_ID]: options.rule.ruleTypeId, - [RULE_UUID]: options.alertId, - [RULE_CATEGORY]: options.rule.ruleTypeName, - [RULE_NAME]: options.rule.name, + [ALERT_RULE_TYPE_ID]: options.rule.ruleTypeId, + [ALERT_RULE_UUID]: options.alertId, + [ALERT_RULE_CATEGORY]: options.rule.ruleTypeName, + [ALERT_RULE_NAME]: options.rule.name, [TAGS]: options.tags, - [ALERT_PRODUCER]: options.rule.producer, + [ALERT_RULE_PRODUCER]: options.rule.producer, }; } diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts index f415dc287ca35..22916b90c084d 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts @@ -330,7 +330,7 @@ describe('Navigation Breadcrumbs', () => { test('should return Rules breadcrumbs when supplied rules Details pathname', () => { const mockDetailName = '5a4a0460-d822-11eb-8962-bfd4aff0a9b3'; - const mockRuleName = 'RULE_NAME'; + const mockRuleName = 'ALERT_RULE_NAME'; const breadcrumbs = getBreadcrumbsForRoute( { ...getMockObject('rules', `/rules/id/${mockDetailName}`, undefined), @@ -357,7 +357,7 @@ describe('Navigation Breadcrumbs', () => { test('should return Rules breadcrumbs when supplied rules Edit pathname', () => { const mockDetailName = '5a4a0460-d822-11eb-8962-bfd4aff0a9b3'; - const mockRuleName = 'RULE_NAME'; + const mockRuleName = 'ALERT_RULE_NAME'; const breadcrumbs = getBreadcrumbsForRoute( { ...getMockObject('rules', `/rules/id/${mockDetailName}/edit`, undefined), @@ -376,7 +376,7 @@ describe('Navigation Breadcrumbs', () => { "securitySolution/rules?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, { - text: 'RULE_NAME', + text: 'ALERT_RULE_NAME', href: `securitySolution/rules/id/${mockDetailName}?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, }, { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index 7ff6f82d40bdc..0519e3f2d4a75 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -8,10 +8,14 @@ import { ALERT_DURATION, ALERT_ID, - ALERT_PRODUCER, + ALERT_RULE_PRODUCER, ALERT_START, ALERT_STATUS, ALERT_UUID, + ALERT_RULE_UUID, + ALERT_RULE_ID, + ALERT_RULE_NAME, + ALERT_RULE_CATEGORY, } from '@kbn/rule-data-utils'; import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; @@ -185,11 +189,11 @@ export const requiredFieldMappingsForActionsRuleRegistry = { 'event.action': 'event.action', 'alert.status': ALERT_STATUS, 'alert.duration.us': ALERT_DURATION, - 'rule.uuid': 'rule.uuid', - 'rule.id': 'rule.id', - 'rule.name': 'rule.name', - 'rule.category': 'rule.category', - producer: ALERT_PRODUCER, + 'rule.uuid': ALERT_RULE_UUID, + 'rule.id': ALERT_RULE_ID, + 'rule.name': ALERT_RULE_NAME, + 'rule.category': ALERT_RULE_CATEGORY, + producer: ALERT_RULE_PRODUCER, tags: 'tags', }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap index 80ae8b9309f1f..9fd3e20f79b43 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap @@ -1583,10 +1583,6 @@ Object { "path": "signal.ancestors.type", "type": "alias", }, - "kibana.alert.consumers": Object { - "type": "constant_keyword", - "value": "siem", - }, "kibana.alert.depth": Object { "path": "signal.depth", "type": "alias", @@ -1675,10 +1671,6 @@ Object { "path": "signal.original_time", "type": "alias", }, - "kibana.alert.producer": Object { - "type": "constant_keyword", - "value": "siem", - }, "kibana.alert.risk_score": Object { "path": "signal.rule.risk_score", "type": "alias", @@ -1691,6 +1683,10 @@ Object { "path": "signal.rule.building_block_type", "type": "alias", }, + "kibana.alert.rule.consumer": Object { + "type": "constant_keyword", + "value": "siem", + }, "kibana.alert.rule.created_at": Object { "path": "signal.rule.created_at", "type": "alias", @@ -1751,6 +1747,10 @@ Object { "path": "signal.rule.note", "type": "alias", }, + "kibana.alert.rule.producer": Object { + "type": "constant_keyword", + "value": "siem", + }, "kibana.alert.rule.query": Object { "path": "signal.rule.query", "type": "alias", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.test.ts index 88c549cec5579..3355b0659f284 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.test.ts @@ -109,8 +109,8 @@ describe('get_signals_template', () => { const constantKeywordsFound = recursiveConstantKeywordFound('', template); expect(constantKeywordsFound).toEqual([ 'template.mappings.properties.kibana.space_ids', - 'template.mappings.properties.kibana.alert.consumers', - 'template.mappings.properties.kibana.alert.producer', + 'template.mappings.properties.kibana.alert.rule.consumer', + 'template.mappings.properties.kibana.alert.rule.producer', 'template.mappings.properties.kibana.alert.rule.rule_type_id', ]); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts index bc41441e1a117..989c73f97997b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts @@ -7,8 +7,8 @@ import { SPACE_IDS, - ALERT_CONSUMERS, - ALERT_PRODUCER, + ALERT_RULE_CONSUMER, + ALERT_RULE_PRODUCER, ALERT_RULE_TYPE_ID, } from '@kbn/rule-data-utils'; import signalsMapping from './signals_mapping.json'; @@ -116,11 +116,11 @@ export const getRbacRequiredFields = (spaceId: string) => { type: 'constant_keyword', value: spaceId, }, - [ALERT_CONSUMERS]: { + [ALERT_RULE_CONSUMER]: { type: 'constant_keyword', value: 'siem', }, - [ALERT_PRODUCER]: { + [ALERT_RULE_PRODUCER]: { type: 'constant_keyword', value: 'siem', }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_execution_field_map.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_execution_field_map.ts index 700ce66e2770f..b3c70cd56d9e6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_execution_field_map.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_execution_field_map.ts @@ -18,9 +18,6 @@ import { * @deprecated ruleExecutionFieldMap is kept here only as a reference. It will be superseded with EventLog implementation */ export const ruleExecutionFieldMap = { - // [ALERT_OWNER]: { type: 'keyword', required: true }, - // [SPACE_IDS]: { type: 'keyword', array: true, required: true }, - // [RULE_ID]: { type: 'keyword', required: true }, [MESSAGE]: { type: 'keyword' }, [EVENT_SEQUENCE]: { type: 'long' }, [EVENT_END]: { type: 'date' }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_registry_log_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_registry_log_client.ts index f5971475e2b16..1c19859eef432 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_registry_log_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_log_client/rule_registry_log_client.ts @@ -7,12 +7,13 @@ import { estypes } from '@elastic/elasticsearch'; import { - ALERT_OWNER, + ALERT_RULE_CONSUMER, + ALERT_RULE_TYPE_ID, EVENT_ACTION, EVENT_KIND, - RULE_ID, SPACE_IDS, TIMESTAMP, + ALERT_RULE_ID, } from '@kbn/rule-data-utils'; import { once } from 'lodash/fp'; import moment from 'moment'; @@ -95,7 +96,7 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient { } const filter: estypes.QueryDslQueryContainer[] = [ - { terms: { [RULE_ID]: ruleIds } }, + { terms: { [ALERT_RULE_ID]: ruleIds } }, { terms: { [SPACE_IDS]: [spaceId] } }, ]; @@ -114,7 +115,7 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient { aggs: { rules: { terms: { - field: RULE_ID, + field: ALERT_RULE_ID, size: ruleIds.length, }, aggs: { @@ -147,7 +148,10 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient { bucket.key, bucket.most_recent_logs.hits.hits.map((event) => { const logEntry = parseRuleExecutionLog(event._source); - invariant(logEntry['rule.id'], 'Malformed execution log entry: rule.id field not found'); + invariant( + logEntry[ALERT_RULE_ID] ?? '', + 'Malformed execution log entry: rule.id field not found' + ); const lastFailure = bucket.last_failure.event.hits.hits[0] ? parseRuleExecutionLog(bucket.last_failure.event.hits.hits[0]._source) @@ -179,7 +183,7 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient { ] : undefined; - const alertId = logEntry['rule.id']; + const alertId = logEntry[ALERT_RULE_ID] ?? ''; const statusDate = logEntry[TIMESTAMP]; const lastFailureAt = lastFailure?.[TIMESTAMP]; const lastFailureMessage = lastFailure?.[MESSAGE]; @@ -213,14 +217,6 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient { ); } - // { [x: string]: string | string[] | ExecutionMetricValue; - // [x: number]: string; - // "kibana.space_ids": string[]; - // "event.action": T; - // "event.kind": string; - // "rule.id": string; - // "@timestamp": string; } - public async logExecutionMetric({ ruleId, namespace, @@ -234,9 +230,10 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient { [EVENT_ACTION]: metric, [EVENT_KIND]: 'metric', [getMetricField(metric)]: value, - [RULE_ID]: ruleId, + [ALERT_RULE_ID]: ruleId ?? '', [TIMESTAMP]: new Date().toISOString(), - [ALERT_OWNER]: 'siem', + [ALERT_RULE_CONSUMER]: SERVER_APP_ID, + [ALERT_RULE_TYPE_ID]: SERVER_APP_ID, }, namespace ); @@ -256,11 +253,12 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient { [EVENT_KIND]: 'event', [EVENT_SEQUENCE]: this.sequence++, [MESSAGE]: message, - [RULE_ID]: ruleId, + [ALERT_RULE_ID]: ruleId ?? '', [RULE_STATUS_SEVERITY]: statusSeverityDict[newStatus], [RULE_STATUS]: newStatus, [TIMESTAMP]: new Date().toISOString(), - [ALERT_OWNER]: 'siem', + [ALERT_RULE_CONSUMER]: SERVER_APP_ID, + [ALERT_RULE_TYPE_ID]: SERVER_APP_ID, }, namespace ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts index f9874478e7a5d..4c59063d39e60 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts @@ -6,7 +6,7 @@ */ import { - ALERT_OWNER, + ALERT_RULE_CONSUMER, ALERT_RULE_NAMESPACE, ALERT_STATUS, ALERT_WORKFLOW_STATUS, @@ -58,7 +58,7 @@ describe('buildAlert', () => { const expected = { '@timestamp': timestamp, [SPACE_IDS]: [SPACE_ID], - [ALERT_OWNER]: SERVER_APP_ID, + [ALERT_RULE_CONSUMER]: SERVER_APP_ID, [ALERT_ANCESTORS]: [ { id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', @@ -127,7 +127,7 @@ describe('buildAlert', () => { const expected = { '@timestamp': timestamp, [SPACE_IDS]: [SPACE_ID], - [ALERT_OWNER]: SERVER_APP_ID, + [ALERT_RULE_CONSUMER]: SERVER_APP_ID, [ALERT_ANCESTORS]: [ { id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts index 641b37cb54bc4..ec667fa50934b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts @@ -6,7 +6,7 @@ */ import { - ALERT_OWNER, + ALERT_RULE_CONSUMER, ALERT_RULE_NAMESPACE, ALERT_STATUS, ALERT_WORKFLOW_STATUS, @@ -104,7 +104,7 @@ export const buildAlert = ( return ({ '@timestamp': new Date().toISOString(), - [ALERT_OWNER]: SERVER_APP_ID, + [ALERT_RULE_CONSUMER]: SERVER_APP_ID, [SPACE_IDS]: spaceId != null ? [spaceId] : [], [ALERT_ANCESTORS]: ancestors, [ALERT_STATUS]: 'open', diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts index e419009354b42..5b60443fc6f51 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts @@ -5,12 +5,11 @@ * 2.0. */ -import { ALERT_OWNER, RULE_ID, SPACE_IDS } from '@kbn/rule-data-utils'; +import { ALERT_RULE_CONSUMER, ALERT_RULE_TYPE_ID, SPACE_IDS } from '@kbn/rule-data-utils'; import { map, mergeMap, catchError } from 'rxjs/operators'; import { from } from 'rxjs'; import { - // TODO: Undo comment in fix here https://github.com/elastic/kibana/pull/107857 - // isValidFeatureId, + isValidFeatureId, mapConsumerToIndexName, AlertConsumers, } from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; @@ -50,9 +49,7 @@ export const timelineSearchStrategyProvider = { const factoryQueryType = request.factoryQueryType; const entityType = request.entityType; - let alertConsumers = request.alertConsumers; - // TODO: Remove in fix here https://github.com/elastic/kibana/pull/107857 - alertConsumers = undefined; + const alertConsumers = request.alertConsumers; if (factoryQueryType == null) { throw new Error('factoryQueryType is required'); @@ -61,9 +58,7 @@ export const timelineSearchStrategyProvider = = timelineFactory[factoryQueryType]; if (alertConsumers != null && entityType != null && entityType === EntityType.ALERTS) { - // TODO: Thist won't be hit since alertConsumers = undefined - // TODO: remove in fix here https://github.com/elastic/kibana/pull/107857 - const allFeatureIdsValid = null; // alertConsumers.every((id) => isValidFeatureId(id)); + const allFeatureIdsValid = alertConsumers.every((id) => isValidFeatureId(id)); if (!allFeatureIdsValid) { throw new Error('An invalid alerts consumer feature id was provided'); @@ -134,7 +129,7 @@ const timelineAlertsSearchStrategy = ({ }) => { // Based on what solution alerts you want to see, figures out what corresponding // index to query (ex: siem --> .alerts-security.alerts) - const indices = alertConsumers.flatMap((consumer) => mapConsumerToIndexName[consumer]); + const indices = alertConsumers.flatMap((consumer) => `${mapConsumerToIndexName[consumer]}*`); const requestWithAlertsIndices = { ...request, defaultIndex: indices, indexName: indices }; // Note: Alerts RBAC are built off of the alerting's authorization class, which @@ -145,8 +140,8 @@ const timelineAlertsSearchStrategy = ({ type: AlertingAuthorizationFilterType.ESDSL, // Not passing in values, these are the paths for these fields fieldNames: { - consumer: ALERT_OWNER, - ruleTypeId: RULE_ID, + consumer: ALERT_RULE_CONSUMER, + ruleTypeId: ALERT_RULE_TYPE_ID, spaceIds: SPACE_IDS, }, }); diff --git a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts index 8072064b2b1bf..6c83bba99abad 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts @@ -9,15 +9,11 @@ import expect from '@kbn/expect'; import { ALERT_DURATION, ALERT_END, - ALERT_EVALUATION_THRESHOLD, - ALERT_EVALUATION_VALUE, - ALERT_ID, - ALERT_OWNER, - ALERT_PRODUCER, ALERT_START, ALERT_STATUS, ALERT_UUID, EVENT_KIND, + ALERT_RULE_UUID, } from '@kbn/rule-data-utils'; import { merge, omit } from 'lodash'; import { format } from 'url'; @@ -350,7 +346,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { any >; - const exclude = ['@timestamp', ALERT_START, ALERT_UUID, 'rule.uuid']; + const exclude = ['@timestamp', ALERT_START, ALERT_UUID, ALERT_RULE_UUID]; const toCompare = omit(alertEvent, exclude); @@ -362,25 +358,34 @@ export default function ApiTest({ getService }: FtrProviderContext) { "event.kind": Array [ "signal", ], - "${ALERT_DURATION}": Array [ + "kibana.alert.duration.us": Array [ 0, ], - "${ALERT_EVALUATION_THRESHOLD}": Array [ + "kibana.alert.evaluation.threshold": Array [ 30, ], - "${ALERT_EVALUATION_VALUE}": Array [ + "kibana.alert.evaluation.value": Array [ 50, ], - "${ALERT_ID}": Array [ + "kibana.alert.id": Array [ "apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED", ], - "${ALERT_OWNER}": Array [ + "kibana.alert.rule.category": Array [ + "Transaction error rate threshold", + ], + "kibana.alert.rule.consumer": Array [ "apm", ], - "${ALERT_PRODUCER}": Array [ + "kibana.alert.rule.name": Array [ + "Transaction error rate threshold | opbeans-go", + ], + "kibana.alert.rule.producer": Array [ "apm", ], - "${ALERT_STATUS}": Array [ + "kibana.alert.rule.rule_type_id": Array [ + "apm.transaction_error_rate", + ], + "kibana.alert.status": Array [ "open", ], "kibana.space_ids": Array [ @@ -389,15 +394,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { "processor.event": Array [ "transaction", ], - "rule.category": Array [ - "Transaction error rate threshold", - ], - "rule.id": Array [ - "apm.transaction_error_rate", - ], - "rule.name": Array [ - "Transaction error rate threshold | opbeans-go", - ], "service.name": Array [ "opbeans-go", ], @@ -438,25 +434,34 @@ export default function ApiTest({ getService }: FtrProviderContext) { "event.kind": Array [ "signal", ], - "${ALERT_DURATION}": Array [ + "kibana.alert.duration.us": Array [ 0, ], - "${ALERT_EVALUATION_THRESHOLD}": Array [ + "kibana.alert.evaluation.threshold": Array [ 30, ], - "${ALERT_EVALUATION_VALUE}": Array [ + "kibana.alert.evaluation.value": Array [ 50, ], - "${ALERT_ID}": Array [ + "kibana.alert.id": Array [ "apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED", ], - "${ALERT_OWNER}": Array [ + "kibana.alert.rule.category": Array [ + "Transaction error rate threshold", + ], + "kibana.alert.rule.consumer": Array [ "apm", ], - "${ALERT_PRODUCER}": Array [ + "kibana.alert.rule.name": Array [ + "Transaction error rate threshold | opbeans-go", + ], + "kibana.alert.rule.producer": Array [ "apm", ], - "${ALERT_STATUS}": Array [ + "kibana.alert.rule.rule_type_id": Array [ + "apm.transaction_error_rate", + ], + "kibana.alert.status": Array [ "open", ], "kibana.space_ids": Array [ @@ -465,15 +470,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { "processor.event": Array [ "transaction", ], - "rule.category": Array [ - "Transaction error rate threshold", - ], - "rule.id": Array [ - "apm.transaction_error_rate", - ], - "rule.name": Array [ - "Transaction error rate threshold | opbeans-go", - ], "service.name": Array [ "opbeans-go", ], @@ -545,22 +541,31 @@ export default function ApiTest({ getService }: FtrProviderContext) { "event.kind": Array [ "signal", ], - "${ALERT_EVALUATION_THRESHOLD}": Array [ + "kibana.alert.evaluation.threshold": Array [ 30, ], - "${ALERT_EVALUATION_VALUE}": Array [ + "kibana.alert.evaluation.value": Array [ 50, ], - "${ALERT_ID}": Array [ + "kibana.alert.id": Array [ "apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED", ], - "${ALERT_OWNER}": Array [ + "kibana.alert.rule.category": Array [ + "Transaction error rate threshold", + ], + "kibana.alert.rule.consumer": Array [ "apm", ], - "${ALERT_PRODUCER}": Array [ + "kibana.alert.rule.name": Array [ + "Transaction error rate threshold | opbeans-go", + ], + "kibana.alert.rule.producer": Array [ "apm", ], - "${ALERT_STATUS}": Array [ + "kibana.alert.rule.rule_type_id": Array [ + "apm.transaction_error_rate", + ], + "kibana.alert.status": Array [ "closed", ], "kibana.space_ids": Array [ @@ -569,15 +574,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { "processor.event": Array [ "transaction", ], - "rule.category": Array [ - "Transaction error rate threshold", - ], - "rule.id": Array [ - "apm.transaction_error_rate", - ], - "rule.name": Array [ - "Transaction error rate threshold | opbeans-go", - ], "service.name": Array [ "opbeans-go", ], diff --git a/x-pack/test/functional/es_archives/rule_registry/alerts/data.json b/x-pack/test/functional/es_archives/rule_registry/alerts/data.json index 940ebe5321b9d..81ff007903368 100644 --- a/x-pack/test/functional/es_archives/rule_registry/alerts/data.json +++ b/x-pack/test/functional/es_archives/rule_registry/alerts/data.json @@ -6,9 +6,9 @@ "source": { "event.kind" : "signal", "@timestamp": "2020-12-16T15:16:18.570Z", - "rule.id": "apm.error_rate", + "kibana.alert.rule.rule_type_id": "apm.error_rate", "message": "hello world 1", - "kibana.alert.owner": "apm", + "kibana.alert.rule.consumer": "apm", "kibana.alert.status": "open", "kibana.space_ids": ["space1", "space2"] } @@ -23,9 +23,9 @@ "source": { "event.kind" : "signal", "@timestamp": "2020-12-16T15:16:18.570Z", - "rule.id": "apm.error_rate", + "kibana.alert.rule.rule_type_id": "apm.error_rate", "message": "hello world 1", - "kibana.alert.owner": "apm", + "kibana.alert.rule.consumer": "apm", "kibana.alert.status": "open", "kibana.space_ids": ["space1"] } @@ -40,9 +40,9 @@ "source": { "event.kind" : "signal", "@timestamp": "2020-12-16T15:16:18.570Z", - "rule.id": "apm.error_rate", + "kibana.alert.rule.rule_type_id": "apm.error_rate", "message": "hello world 1", - "kibana.alert.owner": "apm", + "kibana.alert.rule.consumer": "apm", "kibana.alert.status": "open", "kibana.space_ids": ["space2"] } @@ -57,9 +57,9 @@ "source": { "event.kind" : "signal", "@timestamp": "2020-12-16T15:16:18.570Z", - "rule.id": "siem.signals", + "kibana.alert.rule.rule_type_id": "siem.signals", "message": "hello world security", - "kibana.alert.owner": "siem", + "kibana.alert.rule.consumer": "siem", "kibana.alert.status": "open", "kibana.space_ids": ["space1", "space2"] } @@ -74,9 +74,9 @@ "source": { "event.kind" : "signal", "@timestamp": "2020-12-16T15:16:18.570Z", - "rule.id": "siem.customRule", + "kibana.alert.rule.rule_type_id": "siem.customRule", "message": "hello world security", - "kibana.alert.owner": "siem", + "kibana.alert.rule.consumer": "siem", "kibana.alert.status": "open", "kibana.space_ids": ["space1", "space2"] } @@ -90,9 +90,9 @@ "id": "space1securityalert", "source": { "@timestamp": "2020-12-16T15:16:18.570Z", - "rule.id": "siem.signals", + "kibana.alert.rule.rule_type_id": "siem.signals", "message": "hello world security", - "kibana.alert.owner": "siem", + "kibana.alert.rule.consumer": "siem", "kibana.alert.status": "open", "kibana.space_ids": ["space1"] } @@ -106,9 +106,9 @@ "id": "space2securityalert", "source": { "@timestamp": "2020-12-16T15:16:18.570Z", - "rule.id": "siem.signals", + "kibana.alert.rule.rule_type_id": "siem.signals", "message": "hello world security", - "kibana.alert.owner": "siem", + "kibana.alert.rule.consumer": "siem", "kibana.alert.status": "open", "kibana.space_ids": ["space2"] } diff --git a/x-pack/test/functional/es_archives/rule_registry/alerts/mappings.json b/x-pack/test/functional/es_archives/rule_registry/alerts/mappings.json index 74d50ca402e45..943457ad6cd85 100644 --- a/x-pack/test/functional/es_archives/rule_registry/alerts/mappings.json +++ b/x-pack/test/functional/es_archives/rule_registry/alerts/mappings.json @@ -13,7 +13,7 @@ } } }, - "kibana.alert.owner": { + "kibana.alert.rule.consumer": { "type": "keyword", "ignore_above": 256 } @@ -37,7 +37,7 @@ } } }, - "kibana.alert.owner": { + "kibana.alert.rule.consumer": { "type": "keyword", "ignore_above": 256 } diff --git a/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx b/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx index 317010aca24bd..585470aff23b2 100644 --- a/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx +++ b/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx @@ -38,7 +38,7 @@ export function renderApp( ReactDOM.unmountComponentAtNode(parameters.element); }; } -const ALERT_CONSUMER = [AlertConsumers.SIEM]; +const ALERT_RULE_CONSUMER = [AlertConsumers.SIEM]; const AppRoot = React.memo( ({ @@ -63,7 +63,7 @@ const AppRoot = React.memo( {(timelinesPluginSetup && timelinesPluginSetup.getTGrid && timelinesPluginSetup.getTGrid<'standalone'>({ - alertConsumers: ALERT_CONSUMER, + alertConsumers: ALERT_RULE_CONSUMER, type: 'standalone', columns: [], indexNames: [], diff --git a/x-pack/test/timeline/security_and_spaces/tests/basic/events.ts b/x-pack/test/timeline/security_and_spaces/tests/basic/events.ts index 12f5012b0b08c..67371338a925f 100644 --- a/x-pack/test/timeline/security_and_spaces/tests/basic/events.ts +++ b/x-pack/test/timeline/security_and_spaces/tests/basic/events.ts @@ -7,7 +7,7 @@ import { JsonObject } from '@kbn/utility-types'; import expect from '@kbn/expect'; -import { ALERT_ID, ALERT_OWNER } from '@kbn/rule-data-utils'; +import { ALERT_ID, ALERT_RULE_CONSUMER } from '@kbn/rule-data-utils'; import { User } from '../../../../rule_registry/common/lib/authentication/types'; import { TimelineEdges, TimelineNonEcsData } from '../../../../../plugins/timelines/common/'; @@ -74,7 +74,7 @@ export default ({ getService }: FtrProviderContext) => { field: '@timestamp', }, { - field: ALERT_OWNER, + field: ALERT_RULE_CONSUMER, }, { field: ALERT_ID, @@ -84,7 +84,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], factoryQueryType: TimelineEventsQueries.all, - fieldRequested: ['@timestamp', 'message', ALERT_OWNER, ALERT_ID, 'event.kind'], + fieldRequested: ['@timestamp', 'message', ALERT_RULE_CONSUMER, ALERT_ID, 'event.kind'], fields: [], filterQuery: { bool: { @@ -149,7 +149,9 @@ export default ({ getService }: FtrProviderContext) => { timeline.edges.every((hit: TimelineEdges) => { const data: TimelineNonEcsData[] = hit.node.data; return data.some(({ field, value }) => { - return field === ALERT_OWNER && featureIds.includes((value && value[0]) ?? ''); + return ( + field === ALERT_RULE_CONSUMER && featureIds.includes((value && value[0]) ?? '') + ); }); }) ).to.equal(true); diff --git a/x-pack/test/timeline/security_and_spaces/tests/trial/events.ts b/x-pack/test/timeline/security_and_spaces/tests/trial/events.ts index c51532967cd09..79fd9e6a4fb0b 100644 --- a/x-pack/test/timeline/security_and_spaces/tests/trial/events.ts +++ b/x-pack/test/timeline/security_and_spaces/tests/trial/events.ts @@ -7,7 +7,7 @@ import { JsonObject } from '@kbn/utility-types'; import expect from '@kbn/expect'; -import { ALERT_ID, ALERT_OWNER } from '@kbn/rule-data-utils'; +import { ALERT_ID, ALERT_RULE_CONSUMER } from '@kbn/rule-data-utils'; import { User } from '../../../../rule_registry/common/lib/authentication/types'; import { TimelineEdges, TimelineNonEcsData } from '../../../../../plugins/timelines/common/'; @@ -57,7 +57,7 @@ export default ({ getService }: FtrProviderContext) => { field: '@timestamp', }, { - field: ALERT_OWNER, + field: ALERT_RULE_CONSUMER, }, { field: ALERT_ID, @@ -67,7 +67,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], factoryQueryType: TimelineEventsQueries.all, - fieldRequested: ['@timestamp', 'message', ALERT_OWNER, ALERT_ID, 'event.kind'], + fieldRequested: ['@timestamp', 'message', ALERT_RULE_CONSUMER, ALERT_ID, 'event.kind'], fields: [], filterQuery: { bool: { @@ -131,7 +131,9 @@ export default ({ getService }: FtrProviderContext) => { timeline.edges.every((hit: TimelineEdges) => { const data: TimelineNonEcsData[] = hit.node.data; return data.some(({ field, value }) => { - return field === ALERT_OWNER && featureIds.includes((value && value[0]) ?? ''); + return ( + field === ALERT_RULE_CONSUMER && featureIds.includes((value && value[0]) ?? '') + ); }); }) ).to.equal(true); diff --git a/x-pack/test/timeline/security_only/tests/basic/events.ts b/x-pack/test/timeline/security_only/tests/basic/events.ts index 8c118de8f3287..e1ab3c47e1117 100644 --- a/x-pack/test/timeline/security_only/tests/basic/events.ts +++ b/x-pack/test/timeline/security_only/tests/basic/events.ts @@ -6,7 +6,7 @@ */ import { JsonObject } from '@kbn/utility-types'; -import { ALERT_ID, ALERT_OWNER } from '@kbn/rule-data-utils'; +import { ALERT_ID, ALERT_RULE_CONSUMER } from '@kbn/rule-data-utils'; import { getSpaceUrlPrefix } from '../../../../rule_registry/common/lib/authentication/spaces'; @@ -40,7 +40,7 @@ export default ({ getService }: FtrProviderContext) => { field: '@timestamp', }, { - field: ALERT_OWNER, + field: ALERT_RULE_CONSUMER, }, { field: ALERT_ID, @@ -50,7 +50,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], factoryQueryType: TimelineEventsQueries.all, - fieldRequested: ['@timestamp', 'message', ALERT_OWNER, ALERT_ID, 'event.kind'], + fieldRequested: ['@timestamp', 'message', ALERT_RULE_CONSUMER, ALERT_ID, 'event.kind'], fields: [], filterQuery: { bool: { diff --git a/x-pack/test/timeline/security_only/tests/trial/events.ts b/x-pack/test/timeline/security_only/tests/trial/events.ts index 8c118de8f3287..e1ab3c47e1117 100644 --- a/x-pack/test/timeline/security_only/tests/trial/events.ts +++ b/x-pack/test/timeline/security_only/tests/trial/events.ts @@ -6,7 +6,7 @@ */ import { JsonObject } from '@kbn/utility-types'; -import { ALERT_ID, ALERT_OWNER } from '@kbn/rule-data-utils'; +import { ALERT_ID, ALERT_RULE_CONSUMER } from '@kbn/rule-data-utils'; import { getSpaceUrlPrefix } from '../../../../rule_registry/common/lib/authentication/spaces'; @@ -40,7 +40,7 @@ export default ({ getService }: FtrProviderContext) => { field: '@timestamp', }, { - field: ALERT_OWNER, + field: ALERT_RULE_CONSUMER, }, { field: ALERT_ID, @@ -50,7 +50,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], factoryQueryType: TimelineEventsQueries.all, - fieldRequested: ['@timestamp', 'message', ALERT_OWNER, ALERT_ID, 'event.kind'], + fieldRequested: ['@timestamp', 'message', ALERT_RULE_CONSUMER, ALERT_ID, 'event.kind'], fields: [], filterQuery: { bool: { diff --git a/x-pack/test/timeline/spaces_only/tests/events.ts b/x-pack/test/timeline/spaces_only/tests/events.ts index 2c2d221129721..3867279fda7f2 100644 --- a/x-pack/test/timeline/spaces_only/tests/events.ts +++ b/x-pack/test/timeline/spaces_only/tests/events.ts @@ -7,7 +7,7 @@ import { JsonObject } from '@kbn/utility-types'; import expect from '@kbn/expect'; -import { ALERT_ID, ALERT_OWNER } from '@kbn/rule-data-utils'; +import { ALERT_ID, ALERT_RULE_CONSUMER } from '@kbn/rule-data-utils'; import { FtrProviderContext } from '../../../rule_registry/common/ftr_provider_context'; import { getSpaceUrlPrefix } from '../../../rule_registry/common/lib/authentication/spaces'; @@ -35,7 +35,7 @@ export default ({ getService }: FtrProviderContext) => { field: '@timestamp', }, { - field: ALERT_OWNER, + field: ALERT_RULE_CONSUMER, }, { field: ALERT_ID, @@ -45,7 +45,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], factoryQueryType: TimelineEventsQueries.all, - fieldRequested: ['@timestamp', 'message', ALERT_OWNER, ALERT_ID, 'event.kind'], + fieldRequested: ['@timestamp', 'message', ALERT_RULE_CONSUMER, ALERT_ID, 'event.kind'], fields: [], filterQuery: { bool: { From 46e0f0ba3ec6cb1f5a51fb12b0c6fb421054bd89 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Wed, 11 Aug 2021 12:43:13 +0200 Subject: [PATCH 072/104] [Reporting] Added docs about the new ILM `kibana-reporting` policy (#108018) * first iteration of ilm policy copy - in a callout * apply James' suggested change Co-authored-by: James Rodewig <40268737+jrodewig@users.noreply.github.com> Co-authored-by: James Rodewig <40268737+jrodewig@users.noreply.github.com> --- docs/user/reporting/index.asciidoc | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/user/reporting/index.asciidoc b/docs/user/reporting/index.asciidoc index cb999ed189d77..148e9f8ee14a5 100644 --- a/docs/user/reporting/index.asciidoc +++ b/docs/user/reporting/index.asciidoc @@ -7,9 +7,9 @@ -- :keywords: analyst, concept, task, reporting -:description: {kib} provides you with several options to share *Discover* saved searches, dashboards, *Visualize Library* visualizations, and *Canvas* workpads with others, or on a website. +:description: {kib} provides you with several options to share *Discover* saved searches, dashboards, *Visualize Library* visualizations, and *Canvas* workpads with others, or on a website. -{kib} provides you with several options to share *Discover* saved searches, dashboards, *Visualize Library* visualizations, and *Canvas* workpads. +{kib} provides you with several options to share *Discover* saved searches, dashboards, *Visualize Library* visualizations, and *Canvas* workpads. You access the options from the *Share* menu in the toolbar. The sharing options include the following: @@ -34,9 +34,9 @@ You access the options from the *Share* menu in the toolbar. The sharing options Create and download PDF, PNG, or CSV reports of saved searches, dashboards, visualizations, and workpads. [[reporting-layout-sizing]] -The layout and size of the report depends on what you are sharing. +The layout and size of the report depends on what you are sharing. For saved searches, dashboards, and visualizations, the layout depends on the size of the panels. -For workpads, the layout depends on the size of the worksheet dimensions. +For workpads, the layout depends on the size of the worksheet dimensions. To change the output size, change the size of the browser, which resizes the shareable container before the report generates. It might take some trial and error before you're satisfied. @@ -67,6 +67,11 @@ NOTE: When you create a dashboard report that includes a data table or saved sea . To view and manage reports, open the main menu, then click *Stack Management > Reporting*. +NOTE: Reports are stored in {es} and managed by the `kibana-reporting` {ilm} +({ilm-init}) policy. By default, the policy stores reports forever. To learn +more about {ilm-init} policies, refer to the {es} +{ref}/index-lifecycle-management.html[{ilm-init} documentation]. + [float] [[share-a-direct-link]] == Share a direct link @@ -102,7 +107,7 @@ Create a JSON file for a workpad. . Open the main menu, then click *Canvas*. -. Open the workpad you want to share. +. Open the workpad you want to share. . From the toolbar, click *Share*, then select *Download as JSON*. @@ -110,7 +115,7 @@ Create a JSON file for a workpad. [[add-workpad-website]] == Share workpads on a website -beta[] *Canvas* allows you to create _shareables_, which are workpads that you download and securely share on a website. +beta[] *Canvas* allows you to create _shareables_, which are workpads that you download and securely share on a website. To customize the behavior of the workpad on your website, you can choose to autoplay the pages or hide the workpad toolbar. . Open the main menu, then click *Canvas*. @@ -139,7 +144,7 @@ Some users might not have access to the dashboard or visualization. For more inf . Open the main menu, then open the dashboard or visualization you want to share. -. Click *Share > Embed code*. +. Click *Share > Embed code*. . Specify how you want to generate the code: From 82bec98cd72b740c44efbc26788eb8c8c12a1046 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Wed, 11 Aug 2021 13:59:49 +0300 Subject: [PATCH 073/104] [Lens] Register all expression functions to the server (#107836) Part of: #97134 Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/expressions/datatable/datatable.ts | 14 ++-- x-pack/plugins/lens/kibana.json | 1 + .../lens/public/app_plugin/lens_top_nav.tsx | 5 +- .../lens/public/app_plugin/mounter.tsx | 10 ++- .../plugins/lens/public/app_plugin/types.ts | 31 ++++---- .../expression.test.tsx | 8 +-- .../public/datatable_visualization/index.ts | 7 +- .../heatmap_visualization/expression.tsx | 5 +- .../public/heatmap_visualization/index.ts | 2 +- .../datapanel.test.tsx | 2 + .../indexpattern_datasource/datapanel.tsx | 18 +++-- .../field_item.test.tsx | 19 +++-- .../indexpattern_datasource/field_item.tsx | 6 +- .../fields_accordion.test.tsx | 7 +- .../fields_accordion.tsx | 4 +- .../public/indexpattern_datasource/index.ts | 37 ++++++---- .../indexpattern.test.ts | 3 + .../indexpattern_datasource/indexpattern.tsx | 18 +++-- .../metric_visualization/expression.tsx | 11 +-- .../lens/public/metric_visualization/index.ts | 2 +- x-pack/plugins/lens/public/mocks.tsx | 10 +-- .../public/pie_visualization/expression.tsx | 12 ++-- .../lens/public/pie_visualization/index.ts | 2 +- x-pack/plugins/lens/public/plugin.ts | 43 +++++++++-- .../public/xy_visualization/expression.tsx | 6 +- .../lens/public/xy_visualization/index.ts | 6 +- .../xy_visualization/to_expression.test.ts | 4 +- .../xy_visualization/visualization.test.ts | 6 +- .../public/xy_visualization/visualization.tsx | 10 +-- .../xy_visualization/xy_suggestions.test.ts | 8 +-- .../lens/server/expressions/expressions.ts | 71 +++++++++++++++++++ .../plugins/lens/server/expressions/index.ts | 8 +++ x-pack/plugins/lens/server/plugin.tsx | 5 ++ 33 files changed, 272 insertions(+), 129 deletions(-) create mode 100644 x-pack/plugins/lens/server/expressions/expressions.ts create mode 100644 x-pack/plugins/lens/server/expressions/index.ts diff --git a/x-pack/plugins/lens/common/expressions/datatable/datatable.ts b/x-pack/plugins/lens/common/expressions/datatable/datatable.ts index d2db63a01793e..32f6c1c089543 100644 --- a/x-pack/plugins/lens/common/expressions/datatable/datatable.ts +++ b/x-pack/plugins/lens/common/expressions/datatable/datatable.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { cloneDeep } from 'lodash'; import type { + ExecutionContext, DatatableColumnMeta, ExpressionFunctionDefinition, } from '../../../../../../src/plugins/expressions/common'; @@ -46,15 +47,13 @@ function isRange(meta: { params?: { id?: string } } | undefined) { return meta?.params?.id === 'range'; } -export const getDatatable = ({ - formatFactory, -}: { - formatFactory: FormatFactory; -}): ExpressionFunctionDefinition< +export const getDatatable = ( + getFormatFactory: (context: ExecutionContext) => FormatFactory | Promise +): ExpressionFunctionDefinition< 'lens_datatable', LensMultiTable, DatatableArgs, - DatatableRender + Promise > => ({ name: 'lens_datatable', type: 'render', @@ -87,12 +86,13 @@ export const getDatatable = ({ help: '', }, }, - fn(data, args, context) { + async fn(data, args, context) { let untransposedData: LensMultiTable | undefined; // do the sorting at this level to propagate it also at CSV download const [firstTable] = Object.values(data.tables); const [layerId] = Object.keys(context.inspectorAdapters.tables || {}); const formatters: Record> = {}; + const formatFactory = await getFormatFactory(context); firstTable.columns.forEach((column) => { formatters[column.id] = formatFactory(column.meta?.params); diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index 6a3e0f40c48f4..9565bf57a315f 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -8,6 +8,7 @@ "data", "charts", "expressions", + "fieldFormats", "inspector", "navigation", "urlForwarding", diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index 5f348d2a88ea0..06420051678ee 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -141,6 +141,7 @@ export const LensTopNavMenu = ({ }: LensTopNavMenuProps) => { const { data, + fieldFormats, navigation, uiSettings, application, @@ -255,7 +256,7 @@ export const LensTopNavMenu = ({ content: exporters.datatableToCSV(datatable, { csvSeparator: uiSettings.get('csv:separator', ','), quoteValues: uiSettings.get('csv:quoteValues', true), - formatFactory: data.fieldFormats.deserialize, + formatFactory: fieldFormats.deserialize, escapeFormulaValues: false, }), type: exporters.CSV_MIME_TYPE, @@ -305,7 +306,7 @@ export const LensTopNavMenu = ({ activeData, attributeService, dashboardFeatureFlag.allowByValueEmbeddables, - data.fieldFormats.deserialize, + fieldFormats.deserialize, getIsByValueMode, initialInput, isLinkedToOriginatingApp, diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 55f3e7463754e..5a783bc4180d3 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -46,7 +46,14 @@ export async function getLensServices( startDependencies: LensPluginStartDependencies, attributeService: () => Promise ): Promise { - const { data, navigation, embeddable, savedObjectsTagging, usageCollection } = startDependencies; + const { + data, + navigation, + embeddable, + savedObjectsTagging, + usageCollection, + fieldFormats, + } = startDependencies; const storage = new Storage(localStorage); const stateTransfer = embeddable?.getStateTransfer(); @@ -56,6 +63,7 @@ export async function getLensServices( data, storage, navigation, + fieldFormats, stateTransfer, usageCollection, savedObjectsTagging, diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index c482b56b70301..b6530b90ac3f5 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { History } from 'history'; -import { OnSaveProps } from 'src/plugins/saved_objects/public'; -import { +import type { History } from 'history'; +import type { OnSaveProps } from 'src/plugins/saved_objects/public'; +import type { ApplicationStart, AppMountParameters, ChromeStart, @@ -17,25 +17,27 @@ import { OverlayStart, SavedObjectsStart, } from '../../../../../src/core/public'; -import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; -import { UsageCollectionStart } from '../../../../../src/plugins/usage_collection/public'; -import { DashboardStart } from '../../../../../src/plugins/dashboard/public'; -import { LensEmbeddableInput } from '../embeddable/embeddable'; -import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public'; -import { LensAttributeService } from '../lens_attribute_service'; -import { IStorageWrapper } from '../../../../../src/plugins/kibana_utils/public'; -import { DashboardFeatureFlagConfig } from '../../../../../src/plugins/dashboard/public'; +import type { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; +import type { UsageCollectionStart } from '../../../../../src/plugins/usage_collection/public'; +import type { DashboardStart } from '../../../../../src/plugins/dashboard/public'; +import type { LensEmbeddableInput } from '../embeddable/embeddable'; +import type { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public'; +import type { LensAttributeService } from '../lens_attribute_service'; +import type { IStorageWrapper } from '../../../../../src/plugins/kibana_utils/public'; +import type { DashboardFeatureFlagConfig } from '../../../../../src/plugins/dashboard/public'; import type { SavedObjectTaggingPluginStart } from '../../../saved_objects_tagging/public'; import { VisualizeFieldContext, ACTION_VISUALIZE_LENS_FIELD, } from '../../../../../src/plugins/ui_actions/public'; -import { +import type { EmbeddableEditorState, EmbeddableStateTransfer, } from '../../../../../src/plugins/embeddable/public'; -import { DatasourceMap, EditorFrameInstance, VisualizationMap } from '../types'; -import { PresentationUtilPluginStart } from '../../../../../src/plugins/presentation_util/public'; +import type { DatasourceMap, EditorFrameInstance, VisualizationMap } from '../types'; +import type { PresentationUtilPluginStart } from '../../../../../src/plugins/presentation_util/public'; +import type { FieldFormatsStart } from '../../../../../src/plugins/field_formats/public'; + export interface RedirectToOriginProps { input?: LensEmbeddableInput; isCopied?: boolean; @@ -97,6 +99,7 @@ export interface LensAppServices { overlays: OverlayStart; storage: IStorageWrapper; dashboard: DashboardStart; + fieldFormats: FieldFormatsStart; data: DataPublicPluginStart; uiSettings: IUiSettingsClient; application: ApplicationStart; diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx index 163971c4ba9fb..b2a25cba329df 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx @@ -5,10 +5,10 @@ * 2.0. */ -import { DatatableProps } from '../../common/expressions'; +import type { DatatableProps } from '../../common/expressions'; import type { LensMultiTable } from '../../common'; import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks'; -import type { IFieldFormat } from '../../../../../src/plugins/field_formats/common'; +import type { FormatFactory } from '../../common'; import { getDatatable } from './expression'; function sampleArgs() { @@ -83,9 +83,9 @@ function sampleArgs() { describe('datatable_expression', () => { describe('datatable renders', () => { - test('it renders with the specified data and args', () => { + test('it renders with the specified data and args', async () => { const { data, args } = sampleArgs(); - const result = getDatatable({ formatFactory: (x) => x as IFieldFormat }).fn( + const result = await getDatatable(() => Promise.resolve((() => {}) as FormatFactory)).fn( data, args, createMockExecutionContext() diff --git a/x-pack/plugins/lens/public/datatable_visualization/index.ts b/x-pack/plugins/lens/public/datatable_visualization/index.ts index b4f37faf0bc00..3349f229a6048 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/index.ts +++ b/x-pack/plugins/lens/public/datatable_visualization/index.ts @@ -17,7 +17,7 @@ interface DatatableVisualizationPluginStartPlugins { } export interface DatatableVisualizationPluginSetupPlugins { expressions: ExpressionsSetup; - formatFactory: Promise; + formatFactory: FormatFactory; editorFrame: EditorFrameSetup; charts: ChartsPluginSetup; } @@ -37,13 +37,12 @@ export class DatatableVisualization { getDatatableVisualization, } = await import('../async_services'); const palettes = await charts.palettes.getPalettes(); - const resolvedFormatFactory = await formatFactory; expressions.registerFunction(() => datatableColumn); - expressions.registerFunction(() => getDatatable({ formatFactory: resolvedFormatFactory })); + expressions.registerFunction(() => getDatatable(() => formatFactory)); expressions.registerRenderer(() => getDatatableRenderer({ - formatFactory: resolvedFormatFactory, + formatFactory, getType: core .getStartServices() .then(([_, { data: dataStart }]) => dataStart.search.aggs.types.get), diff --git a/x-pack/plugins/lens/public/heatmap_visualization/expression.tsx b/x-pack/plugins/lens/public/heatmap_visualization/expression.tsx index 27be4b9ce7fe9..98ce4b399ae8d 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/heatmap_visualization/expression.tsx @@ -20,7 +20,7 @@ import type { HeatmapExpressionProps } from './types'; export { heatmapGridConfig, heatmapLegendConfig, heatmap } from '../../common/expressions'; export const getHeatmapRenderer = (dependencies: { - formatFactory: Promise; + formatFactory: FormatFactory; chartsThemeService: ChartsPluginSetup['theme']; paletteService: PaletteRegistry; timeZone: string; @@ -37,7 +37,6 @@ export const getHeatmapRenderer = (dependencies: { config: HeatmapExpressionProps, handlers: IInterpreterRenderHandlers ) => { - const formatFactory = await dependencies.formatFactory; const onClickValue = (data: LensFilterEvent['data']) => { handlers.event({ name: 'filter', data }); }; @@ -53,7 +52,7 @@ export const getHeatmapRenderer = (dependencies: { onClickValue={onClickValue} onSelectRange={onSelectRange} timeZone={dependencies.timeZone} - formatFactory={formatFactory} + formatFactory={dependencies.formatFactory} chartsThemeService={dependencies.chartsThemeService} paletteService={dependencies.paletteService} /> diff --git a/x-pack/plugins/lens/public/heatmap_visualization/index.ts b/x-pack/plugins/lens/public/heatmap_visualization/index.ts index 11f9b907eb929..5fb4524939f11 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/index.ts +++ b/x-pack/plugins/lens/public/heatmap_visualization/index.ts @@ -14,7 +14,7 @@ import type { FormatFactory } from '../../common'; export interface HeatmapVisualizationPluginSetupPlugins { expressions: ExpressionsSetup; - formatFactory: Promise; + formatFactory: FormatFactory; editorFrame: EditorFrameSetup; charts: ChartsPluginSetup; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index 03eb234d90766..59aebc517bf22 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -20,6 +20,7 @@ import { ChangeIndexPattern } from './change_indexpattern'; import { EuiProgress, EuiLoadingSpinner } from '@elastic/eui'; import { documentField } from './document_field'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; +import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; import { indexPatternFieldEditorPluginMock } from '../../../../../src/plugins/index_pattern_field_editor/public/mocks'; import { getFieldByNameFactory } from './pure_helpers'; import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/public/mocks'; @@ -251,6 +252,7 @@ describe('IndexPattern Data Panel', () => { indexPatternRefs: [], existingFields: {}, data: dataPluginMock.createStartContract(), + fieldFormats: fieldFormatsServiceMock.createStartContract(), indexPatternFieldEditor: indexPatternFieldEditorPluginMock.createStartContract(), onUpdateIndexPattern: jest.fn(), dragDropContext: createMockedDragDropContext(), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 0e53b0f3c8d44..5db5404f3d9da 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -23,14 +23,15 @@ import { EuiButtonIcon, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { EsQueryConfig, Query, Filter } from '@kbn/es-query'; +import type { EsQueryConfig, Query, Filter } from '@kbn/es-query'; import { FormattedMessage } from '@kbn/i18n/react'; -import { CoreStart } from 'kibana/public'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; +import type { CoreStart } from 'kibana/public'; +import type { DataPublicPluginStart } from 'src/plugins/data/public'; +import type { FieldFormatsStart } from 'src/plugins/field_formats/public'; import { htmlIdGenerator } from '@elastic/eui'; -import { DatasourceDataPanelProps, DataType, StateSetter } from '../types'; +import type { DatasourceDataPanelProps, DataType, StateSetter } from '../types'; import { ChildDragDropProvider, DragContextState } from '../drag_drop'; -import { +import type { IndexPattern, IndexPatternPrivateState, IndexPatternField, @@ -46,6 +47,7 @@ import { VISUALIZE_GEO_FIELD_TRIGGER } from '../../../../../src/plugins/ui_actio export type Props = Omit, 'core'> & { data: DataPublicPluginStart; + fieldFormats: FieldFormatsStart; changeIndexPattern: ( id: string, state: IndexPatternPrivateState, @@ -118,6 +120,7 @@ export function IndexPatternDataPanel({ dragDropContext, core, data, + fieldFormats, query, filters, dateRange, @@ -231,6 +234,7 @@ export function IndexPatternDataPanel({ dragDropContext={dragDropContext} core={core} data={data} + fieldFormats={fieldFormats} charts={charts} indexPatternFieldEditor={indexPatternFieldEditor} onChangeIndexPattern={onChangeIndexPattern} @@ -289,6 +293,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ onUpdateIndexPattern, core, data, + fieldFormats, indexPatternFieldEditor, existingFields, charts, @@ -297,6 +302,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ uiActions, }: Omit & { data: DataPublicPluginStart; + fieldFormats: FieldFormatsStart; core: CoreStart; currentIndexPatternId: string; indexPatternRefs: IndexPatternRef[]; @@ -565,6 +571,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ () => ({ core, data, + fieldFormats, indexPattern: currentIndexPattern, highlight: localState.nameFilter.toLowerCase(), dateRange, @@ -575,6 +582,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ [ core, data, + fieldFormats, currentIndexPattern, dateRange, query, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx index 2aa031959f5d7..7cfa957f8a855 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx @@ -12,12 +12,12 @@ import { EuiLoadingSpinner, EuiPopover } from '@elastic/eui'; import { InnerFieldItem, FieldItemProps } from './field_item'; import { coreMock } from 'src/core/public/mocks'; import { mountWithIntl } from '@kbn/test/jest'; -import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; -import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; +import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; import { IndexPattern } from './types'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; import { documentField } from './document_field'; import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/public/mocks'; +import { FieldFormatsStart } from '../../../../../src/plugins/field_formats/public'; const chartsThemeService = chartPluginMock.createSetupContract().theme; @@ -29,7 +29,6 @@ describe('IndexPattern Field Item', () => { let defaultProps: FieldItemProps; let indexPattern: IndexPattern; let core: ReturnType; - let data: DataPublicPluginStart; beforeEach(() => { indexPattern = { @@ -84,11 +83,15 @@ describe('IndexPattern Field Item', () => { } as IndexPattern; core = coreMock.createSetup(); - data = dataPluginMock.createStartContract(); core.http.post.mockClear(); defaultProps = { indexPattern, - data, + fieldFormats: ({ + ...fieldFormatsServiceMock.createStartContract(), + getDefaultInstance: jest.fn(() => ({ + convert: jest.fn((s: unknown) => JSON.stringify(s)), + })), + } as unknown) as FieldFormatsStart, core, highlight: '', dateRange: { @@ -112,12 +115,6 @@ describe('IndexPattern Field Item', () => { hasSuggestionForField: () => false, uiActions: uiActionsPluginMock.createStartContract(), }; - - data.fieldFormats = ({ - getDefaultInstance: jest.fn(() => ({ - convert: jest.fn((s: unknown) => JSON.stringify(s)), - })), - } as unknown) as DataPublicPluginStart['fieldFormats']; }); it('should display displayName of a field', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index 5ceb452038426..9c22ec9d4bb05 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -36,7 +36,7 @@ import { TooltipType, } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; +import type { FieldFormatsStart } from 'src/plugins/field_formats/public'; import { EuiHighlight } from '@elastic/eui'; import { Query, @@ -61,7 +61,7 @@ import { debouncedComponent } from '../debounced_component'; export interface FieldItemProps { core: DatasourceDataPanelProps['core']; - data: DataPublicPluginStart; + fieldFormats: FieldFormatsStart; field: IndexPatternField; indexPattern: IndexPattern; highlight?: string; @@ -395,7 +395,7 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { core, sampledValues, chartsThemeService, - data: { fieldFormats }, + fieldFormats, dropOntoWorkspace, editField, removeField, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx index 6270b94abf565..b3ade3ebc48b8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx @@ -9,8 +9,7 @@ import React from 'react'; import { EuiLoadingSpinner, EuiNotificationBadge } from '@elastic/eui'; import { coreMock } from 'src/core/public/mocks'; import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest'; -import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; -import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; +import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; import { IndexPattern } from './types'; import { FieldItem } from './field_item'; import { FieldsAccordion, FieldsAccordionProps, FieldItemSharedProps } from './fields_accordion'; @@ -21,7 +20,6 @@ describe('Fields Accordion', () => { let defaultProps: FieldsAccordionProps; let indexPattern: IndexPattern; let core: ReturnType; - let data: DataPublicPluginStart; let fieldProps: FieldItemSharedProps; beforeEach(() => { @@ -45,12 +43,11 @@ describe('Fields Accordion', () => { ], } as IndexPattern; core = coreMock.createSetup(); - data = dataPluginMock.createStartContract(); core.http.post.mockClear(); fieldProps = { indexPattern, - data, + fieldFormats: fieldFormatsServiceMock.createStartContract(), core, highlight: '', dateRange: { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx index 9f5409f9837f4..6a89d1770743c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx @@ -17,7 +17,7 @@ import { EuiIconTip, } from '@elastic/eui'; import classNames from 'classnames'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { FieldFormatsStart } from 'src/plugins/field_formats/public'; import { IndexPatternField } from './types'; import { FieldItem } from './field_item'; import { Query, Filter } from '../../../../../src/plugins/data/public'; @@ -28,7 +28,7 @@ import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; export interface FieldItemSharedProps { core: DatasourceDataPanelProps['core']; - data: DataPublicPluginStart; + fieldFormats: FieldFormatsStart; chartsThemeService: ChartsPluginSetup['theme']; indexPattern: IndexPattern; highlight?: string; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts index b5844fa5cf4d6..5ac9797d68db4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts @@ -5,20 +5,25 @@ * 2.0. */ -import { CoreSetup } from 'kibana/public'; +import type { CoreSetup } from 'kibana/public'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; -import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; -import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; -import { IndexPatternFieldEditorStart } from '../../../../../src/plugins/index_pattern_field_editor/public'; -import { +import type { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; +import type { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; +import type { IndexPatternFieldEditorStart } from '../../../../../src/plugins/index_pattern_field_editor/public'; +import type { DataPublicPluginSetup, DataPublicPluginStart, } from '../../../../../src/plugins/data/public'; -import { Datasource, EditorFrameSetup } from '../types'; -import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; +import type { Datasource, EditorFrameSetup } from '../types'; +import type { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; +import type { + FieldFormatsStart, + FieldFormatsSetup, +} from '../../../../../src/plugins/field_formats/public'; export interface IndexPatternDatasourceSetupPlugins { expressions: ExpressionsSetup; + fieldFormats: FieldFormatsSetup; data: DataPublicPluginSetup; editorFrame: EditorFrameSetup; charts: ChartsPluginSetup; @@ -26,6 +31,7 @@ export interface IndexPatternDatasourceSetupPlugins { export interface IndexPatternDatasourceStartPlugins { data: DataPublicPluginStart; + fieldFormats: FieldFormatsStart; indexPatternFieldEditor: IndexPatternFieldEditorStart; uiActions: UiActionsStart; } @@ -35,7 +41,12 @@ export class IndexPatternDatasource { setup( core: CoreSetup, - { expressions, editorFrame, charts, data: dataSetup }: IndexPatternDatasourceSetupPlugins + { + fieldFormats: fieldFormatsSetup, + expressions, + editorFrame, + charts, + }: IndexPatternDatasourceSetupPlugins ) { editorFrame.registerDatasource(async () => { const { @@ -48,10 +59,11 @@ export class IndexPatternDatasource { } = await import('../async_services'); return core .getStartServices() - .then(([coreStart, { indexPatternFieldEditor, uiActions, data }]) => { - const suffixFormatter = getSuffixFormatter(data.fieldFormats.deserialize); - if (!dataSetup.fieldFormats.has(suffixFormatter.id)) { - dataSetup.fieldFormats.register([suffixFormatter]); + .then(([coreStart, { indexPatternFieldEditor, uiActions, data, fieldFormats }]) => { + const suffixFormatter = getSuffixFormatter(fieldFormats.deserialize); + if (!fieldFormats.has(suffixFormatter.id)) { + // todo: this code should be executed on setup phase. + fieldFormatsSetup.register([suffixFormatter]); } expressions.registerFunction(timeScale); expressions.registerFunction(counterRate); @@ -59,6 +71,7 @@ export class IndexPatternDatasource { expressions.registerFunction(formatColumn); return getIndexPatternDatasource({ core: coreStart, + fieldFormats, storage: new Storage(localStorage), data, charts, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 261a73287dba9..b3176dbcfe409 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -20,6 +20,8 @@ import { operationDefinitionMap, getErrorMessages } from './operations'; import { createMockedFullReference } from './operations/mocks'; import { indexPatternFieldEditorPluginMock } from 'src/plugins/index_pattern_field_editor/public/mocks'; import { uiActionsPluginMock } from '../../../../../src/plugins/ui_actions/public/mocks'; +import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; + jest.mock('./loader'); jest.mock('../id_generator'); jest.mock('./operations'); @@ -172,6 +174,7 @@ describe('IndexPattern Data Source', () => { storage: {} as IStorageWrapper, core: coreMock.createStart(), data: dataPluginMock.createStartContract(), + fieldFormats: fieldFormatsServiceMock.createStartContract(), charts: chartPluginMock.createSetupContract(), indexPatternFieldEditor: indexPatternFieldEditorPluginMock.createStartContract(), uiActions: uiActionsPluginMock.createStartContract(), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 2cbe801a5b7b8..3a2d0df88a6cd 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -8,11 +8,12 @@ import React from 'react'; import { render } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; -import { CoreStart, SavedObjectReference } from 'kibana/public'; +import type { CoreStart, SavedObjectReference } from 'kibana/public'; import { i18n } from '@kbn/i18n'; -import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { IndexPatternFieldEditorStart } from '../../../../../src/plugins/index_pattern_field_editor/public'; -import { +import type { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import type { FieldFormatsStart } from 'src/plugins/field_formats/public'; +import type { IndexPatternFieldEditorStart } from '../../../../../src/plugins/index_pattern_field_editor/public'; +import type { DatasourceDimensionEditorProps, DatasourceDimensionTriggerProps, DatasourceDataPanelProps, @@ -83,6 +84,7 @@ export function getIndexPatternDatasource({ core, storage, data, + fieldFormats, charts, indexPatternFieldEditor, uiActions, @@ -90,6 +92,7 @@ export function getIndexPatternDatasource({ core: CoreStart; storage: IStorageWrapper; data: DataPublicPluginStart; + fieldFormats: FieldFormatsStart; charts: ChartsPluginSetup; indexPatternFieldEditor: IndexPatternFieldEditorStart; uiActions: UiActionsStart; @@ -202,6 +205,7 @@ export function getIndexPatternDatasource({ { + updateStateOnCloseDimension: ({ state, layerId }) => { const layer = state.layers[layerId]; if (!Object.values(layer.incompleteColumns || {}).length) { return; @@ -408,7 +414,7 @@ export function getIndexPatternDatasource({ getDatasourceSuggestionsFromCurrentState, getDatasourceSuggestionsForVisualizeField, - getErrorMessages(state, layersGroups) { + getErrorMessages(state) { if (!state) { return; } diff --git a/x-pack/plugins/lens/public/metric_visualization/expression.tsx b/x-pack/plugins/lens/public/metric_visualization/expression.tsx index 5a1e0d7fb5bdf..41b487e790a08 100644 --- a/x-pack/plugins/lens/public/metric_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/expression.tsx @@ -24,22 +24,17 @@ export { metricChart } from '../../common/expressions'; export type { MetricState, MetricConfig } from '../../common/expressions'; export const getMetricChartRenderer = ( - formatFactory: Promise + formatFactory: FormatFactory ): ExpressionRenderDefinition => ({ name: 'lens_metric_chart_renderer', displayName: 'Metric chart', help: 'Metric chart renderer', validate: () => undefined, reuseDomNode: true, - render: async ( - domNode: Element, - config: MetricChartProps, - handlers: IInterpreterRenderHandlers - ) => { - const resolvedFormatFactory = await formatFactory; + render: (domNode: Element, config: MetricChartProps, handlers: IInterpreterRenderHandlers) => { ReactDOM.render( - + , domNode, () => { diff --git a/x-pack/plugins/lens/public/metric_visualization/index.ts b/x-pack/plugins/lens/public/metric_visualization/index.ts index 484dc6140ecf2..29138979ab858 100644 --- a/x-pack/plugins/lens/public/metric_visualization/index.ts +++ b/x-pack/plugins/lens/public/metric_visualization/index.ts @@ -12,7 +12,7 @@ import type { FormatFactory } from '../../common'; export interface MetricVisualizationPluginSetupPlugins { expressions: ExpressionsSetup; - formatFactory: Promise; + formatFactory: FormatFactory; editorFrame: EditorFrameSetup; } diff --git a/x-pack/plugins/lens/public/mocks.tsx b/x-pack/plugins/lens/public/mocks.tsx index 9258c94ad2ab4..611b50b413b71 100644 --- a/x-pack/plugins/lens/public/mocks.tsx +++ b/x-pack/plugins/lens/public/mocks.tsx @@ -20,11 +20,11 @@ import { DeepPartial } from '@reduxjs/toolkit'; import { LensPublicStart } from '.'; import { visualizationTypes } from './xy_visualization/types'; import { navigationPluginMock } from '../../../../src/plugins/navigation/public/mocks'; -import { LensAppServices } from './app_plugin/types'; +import type { LensAppServices } from './app_plugin/types'; import { DOC_TYPE } from '../common'; import { DataPublicPluginStart, esFilters, UI_SETTINGS } from '../../../../src/plugins/data/public'; import { dashboardPluginMock } from '../../../../src/plugins/dashboard/public/mocks'; -import { +import type { LensByValueInput, LensSavedObjectAttributes, LensByReferenceInput, @@ -33,8 +33,9 @@ import { mockAttributeService, createEmbeddableStateTransferMock, } from '../../../../src/plugins/embeddable/public/mocks'; -import { LensAttributeService } from './lens_attribute_service'; -import { EmbeddableStateTransfer } from '../../../../src/plugins/embeddable/public'; +import { fieldFormatsServiceMock } from '../../../../src/plugins/field_formats/public/mocks'; +import type { LensAttributeService } from './lens_attribute_service'; +import type { EmbeddableStateTransfer } from '../../../../src/plugins/embeddable/public'; import { makeConfigureStore, LensAppState, LensState } from './state_management/index'; import { getResolvedDateRange } from './utils'; @@ -391,6 +392,7 @@ export function makeDefaultServices( getUrlForApp: jest.fn((appId: string) => `/testbasepath/app/${appId}#/`), }, data: mockDataPlugin(sessionIdSubject), + fieldFormats: fieldFormatsServiceMock.createStartContract(), storage: { get: jest.fn(), set: jest.fn(), diff --git a/x-pack/plugins/lens/public/pie_visualization/expression.tsx b/x-pack/plugins/lens/public/pie_visualization/expression.tsx index ce36f88b2805e..c1b9f4c799e64 100644 --- a/x-pack/plugins/lens/public/pie_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/expression.tsx @@ -22,7 +22,7 @@ import type { ChartsPluginSetup, PaletteRegistry } from '../../../../../src/plug export { pie } from '../../common/expressions'; export const getPieRenderer = (dependencies: { - formatFactory: Promise; + formatFactory: FormatFactory; chartsThemeService: ChartsPluginSetup['theme']; paletteService: PaletteRegistry; }): ExpressionRenderDefinition => ({ @@ -33,20 +33,16 @@ export const getPieRenderer = (dependencies: { help: '', validate: () => undefined, reuseDomNode: true, - render: async ( - domNode: Element, - config: PieExpressionProps, - handlers: IInterpreterRenderHandlers - ) => { + render: (domNode: Element, config: PieExpressionProps, handlers: IInterpreterRenderHandlers) => { const onClickValue = (data: LensFilterEvent['data']) => { handlers.event({ name: 'filter', data }); }; - const formatFactory = await dependencies.formatFactory; + ReactDOM.render( ; + formatFactory: FormatFactory; charts: ChartsPluginSetup; } diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 26608f9cc78be..e0a4848974237 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -7,6 +7,7 @@ import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public'; import type { Start as InspectorStartContract } from 'src/plugins/inspector/public'; +import type { FieldFormatsSetup, FieldFormatsStart } from 'src/plugins/field_formats/public'; import { UsageCollectionSetup, UsageCollectionStart } from 'src/plugins/usage_collection/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public'; @@ -57,7 +58,7 @@ import { ACTION_VISUALIZE_FIELD, VISUALIZE_FIELD_TRIGGER, } from '../../../../src/plugins/ui_actions/public'; -import { APP_ID, getEditPath, NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; +import { APP_ID, FormatFactory, getEditPath, NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; import type { EditorFrameStart, VisualizationType } from './types'; import { getLensAliasConfig } from './vis_type_alias'; import { visualizeFieldAction } from './trigger_actions/visualize_field_actions'; @@ -77,6 +78,7 @@ export interface LensPluginSetupDependencies { urlForwarding: UrlForwardingSetup; expressions: ExpressionsSetup; data: DataPublicPluginSetup; + fieldFormats: FieldFormatsSetup; embeddable?: EmbeddableSetup; visualizations: VisualizationsSetup; charts: ChartsPluginSetup; @@ -86,6 +88,7 @@ export interface LensPluginSetupDependencies { export interface LensPluginStartDependencies { data: DataPublicPluginStart; + fieldFormats: FieldFormatsStart; expressions: ExpressionsStart; navigation: NavigationPublicPluginStart; uiActions: UiActionsStart; @@ -158,6 +161,7 @@ export class LensPlugin { urlForwarding, expressions, data, + fieldFormats, embeddable, visualizations, charts, @@ -170,9 +174,21 @@ export class LensPlugin { const [coreStart, startDependencies] = await core.getStartServices(); return getLensAttributeService(coreStart, startDependencies); }; + const getStartServices = async (): Promise => { const [coreStart, deps] = await core.getStartServices(); - this.initParts(core, data, embeddable, charts, expressions, usageCollection); + + this.initParts( + core, + data, + embeddable, + charts, + expressions, + usageCollection, + fieldFormats, + deps.fieldFormats.deserialize + ); + return { attributeService: await this.attributeService!(), capabilities: coreStart.application.capabilities, @@ -210,7 +226,19 @@ export class LensPlugin { title: NOT_INTERNATIONALIZED_PRODUCT_NAME, navLinkStatus: AppNavLinkStatus.hidden, mount: async (params: AppMountParameters) => { - await this.initParts(core, data, embeddable, charts, expressions, usageCollection); + const [, deps] = await core.getStartServices(); + + await this.initParts( + core, + data, + embeddable, + charts, + expressions, + usageCollection, + fieldFormats, + deps.fieldFormats.deserialize + ); + const { mountApp, stopReportManager } = await import('./async_services'); this.stopReportManager = stopReportManager; await ensureDefaultIndexPattern(); @@ -245,7 +273,9 @@ export class LensPlugin { embeddable: EmbeddableSetup | undefined, charts: ChartsPluginSetup, expressions: ExpressionsServiceSetup, - usageCollection: UsageCollectionSetup | undefined + usageCollection: UsageCollectionSetup | undefined, + fieldFormats: FieldFormatsSetup, + formatFactory: FormatFactory ) { const { DatatableVisualization, @@ -277,11 +307,10 @@ export class LensPlugin { PieVisualizationPluginSetupPlugins = { expressions, data, + fieldFormats, charts, editorFrame: editorFrameSetupInterface, - formatFactory: core - .getStartServices() - .then(([_, { data: dataStart }]) => dataStart.fieldFormats.deserialize), + formatFactory, }; this.indexpatternDatasource.setup(core, dependencies); this.xyVisualization.setup(core, dependencies); diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 23b251b76e950..9ef87fe4f48d4 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -123,7 +123,7 @@ export function calculateMinInterval({ args: { layers }, data }: XYChartProps) { } export const getXyChartRenderer = (dependencies: { - formatFactory: Promise; + formatFactory: FormatFactory; chartsThemeService: ChartsPluginStart['theme']; chartsActiveCursorService: ChartsPluginStart['activeCursor']; paletteService: PaletteRegistry; @@ -148,12 +148,12 @@ export const getXyChartRenderer = (dependencies: { const onSelectRange = (data: LensBrushEvent['data']) => { handlers.event({ name: 'brush', data }); }; - const formatFactory = await dependencies.formatFactory; + ReactDOM.render( ; + formatFactory: FormatFactory; editorFrame: EditorFrameSetup; charts: ChartsPluginSetup; } @@ -41,7 +41,7 @@ export class XyVisualization { getXyChartRenderer, getXyVisualization, } = await import('../async_services'); - const [, { data, charts }] = await core.getStartServices(); + const [, { charts, fieldFormats }] = await core.getStartServices(); const palettes = await charts.palettes.getPalettes(); expressions.registerFunction(() => legendConfig); expressions.registerFunction(() => yAxisConfig); @@ -62,7 +62,7 @@ export class XyVisualization { timeZone: getTimeZone(core.uiSettings), }) ); - return getXyVisualization({ paletteService: palettes, data }); + return getXyVisualization({ paletteService: palettes, fieldFormats }); }); } } diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts index f43b633f4a716..621e2897a1059 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts @@ -11,12 +11,12 @@ import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks' import { getXyVisualization } from './xy_visualization'; import { Operation } from '../types'; import { createMockDatasource, createMockFramePublicAPI } from '../mocks'; -import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; +import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; describe('#toExpression', () => { const xyVisualization = getXyVisualization({ paletteService: chartPluginMock.createPaletteRegistry(), - data: dataPluginMock.createStartContract(), + fieldFormats: fieldFormatsServiceMock.createStartContract(), }); let mockDatasource: ReturnType; let frame: ReturnType; diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index fd80b9d96d30a..ef97e2622ee82 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -13,7 +13,7 @@ import type { SeriesType, XYLayerConfig } from '../../common/expressions'; import { createMockDatasource, createMockFramePublicAPI } from '../mocks'; import { LensIconChartBar } from '../assets/chart_bar'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; -import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; +import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; function exampleState(): State { return { @@ -32,11 +32,11 @@ function exampleState(): State { }; } const paletteServiceMock = chartPluginMock.createPaletteRegistry(); -const dataMock = dataPluginMock.createStartContract(); +const fieldFormatsMock = fieldFormatsServiceMock.createStartContract(); const xyVisualization = getXyVisualization({ paletteService: paletteServiceMock, - data: dataMock, + fieldFormats: fieldFormatsMock, }); describe('xy_visualization', () => { diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index 40caed7188190..799246ef26b80 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -12,7 +12,7 @@ import { Position } from '@elastic/charts'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { PaletteRegistry } from 'src/plugins/charts/public'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { FieldFormatsStart } from 'src/plugins/field_formats/public'; import { getSuggestions } from './xy_suggestions'; import { LayerContextMenu, XyToolbar, DimensionEditor } from './xy_config_panel'; import type { @@ -87,10 +87,10 @@ function getDescription(state?: State) { export const getXyVisualization = ({ paletteService, - data, + fieldFormats, }: { paletteService: PaletteRegistry; - data: DataPublicPluginStart; + fieldFormats: FieldFormatsStart; }): Visualization => ({ id: 'lnsXY', @@ -190,7 +190,7 @@ export const getXyVisualization = ({ const colorAssignments = getColorAssignments( state.layers, { tables: frame.activeData }, - data.fieldFormats.deserialize + fieldFormats.deserialize ); mappedAccessors = getAccessorColorConfig( colorAssignments, @@ -336,7 +336,7 @@ export const getXyVisualization = ({ , diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts index 87165d64625e7..924b87647fcee 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts @@ -6,19 +6,19 @@ */ import { getSuggestions } from './xy_suggestions'; -import { TableSuggestionColumn, VisualizationSuggestion, TableSuggestion } from '../types'; +import type { TableSuggestionColumn, VisualizationSuggestion, TableSuggestion } from '../types'; import { State, XYState, visualizationTypes } from './types'; import { generateId } from '../id_generator'; import { getXyVisualization } from './xy_visualization'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; -import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; -import { PaletteOutput } from 'src/plugins/charts/public'; +import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; +import type { PaletteOutput } from 'src/plugins/charts/public'; jest.mock('../id_generator'); const xyVisualization = getXyVisualization({ paletteService: chartPluginMock.createPaletteRegistry(), - data: dataPluginMock.createStartContract(), + fieldFormats: fieldFormatsServiceMock.createStartContract(), }); describe('xy_suggestions', () => { diff --git a/x-pack/plugins/lens/server/expressions/expressions.ts b/x-pack/plugins/lens/server/expressions/expressions.ts new file mode 100644 index 0000000000000..8f8e6131b0728 --- /dev/null +++ b/x-pack/plugins/lens/server/expressions/expressions.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CoreSetup, CoreStart, KibanaRequest } from 'kibana/server'; +import { + pie, + xyChart, + timeScale, + counterRate, + metricChart, + yAxisConfig, + layerConfig, + formatColumn, + legendConfig, + renameColumns, + gridlinesConfig, + datatableColumn, + tickLabelsConfig, + axisTitlesVisibilityConfig, + getDatatable, +} from '../../common/expressions'; +import type { PluginStartContract } from '../plugin'; +import type { + ExecutionContext, + ExpressionsServerSetup, +} from '../../../../../src/plugins/expressions/server'; + +const getUiSettings = (coreStart: CoreStart, kibanaRequest: KibanaRequest) => + coreStart.uiSettings.asScopedToClient(coreStart.savedObjects.getScopedClient(kibanaRequest)); + +export const setupExpressions = ( + core: CoreSetup, + expressions: ExpressionsServerSetup +) => { + const getFormatFactory = async (context: ExecutionContext) => { + const [coreStart, { fieldFormats: fieldFormatsStart }] = await core.getStartServices(); + const kibanaRequest = context.getKibanaRequest?.(); + + if (!kibanaRequest) { + throw new Error('"lens_datatable" expression function requires a KibanaRequest to execute'); + } + + const fieldFormats = await fieldFormatsStart.fieldFormatServiceFactory( + getUiSettings(coreStart, kibanaRequest) + ); + + return fieldFormats.deserialize; + }; + + [ + pie, + xyChart, + timeScale, + counterRate, + metricChart, + yAxisConfig, + layerConfig, + formatColumn, + legendConfig, + renameColumns, + gridlinesConfig, + datatableColumn, + tickLabelsConfig, + axisTitlesVisibilityConfig, + getDatatable(getFormatFactory), + ].forEach((expressionFn) => expressions.registerFunction(expressionFn)); +}; diff --git a/x-pack/plugins/lens/server/expressions/index.ts b/x-pack/plugins/lens/server/expressions/index.ts new file mode 100644 index 0000000000000..b726ba94ae5bc --- /dev/null +++ b/x-pack/plugins/lens/server/expressions/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { setupExpressions } from './expressions'; diff --git a/x-pack/plugins/lens/server/plugin.tsx b/x-pack/plugins/lens/server/plugin.tsx index b47019fa54ec0..f0ee801ece89b 100644 --- a/x-pack/plugins/lens/server/plugin.tsx +++ b/x-pack/plugins/lens/server/plugin.tsx @@ -10,6 +10,7 @@ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { Observable } from 'rxjs'; import { PluginStart as DataPluginStart } from 'src/plugins/data/server'; import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; +import { FieldFormatsStart } from 'src/plugins/field_formats/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { setupRoutes } from './routes'; import { @@ -20,6 +21,7 @@ import { import { setupSavedObjects } from './saved_objects'; import { EmbeddableSetup } from '../../../../src/plugins/embeddable/server'; import { lensEmbeddableFactory } from './embeddable/lens_embeddable_factory'; +import { setupExpressions } from './expressions'; export interface PluginSetupContract { usageCollection?: UsageCollectionSetup; @@ -30,6 +32,7 @@ export interface PluginSetupContract { export interface PluginStartContract { taskManager?: TaskManagerStartContract; + fieldFormats: FieldFormatsStart; data: DataPluginStart; } @@ -44,6 +47,8 @@ export class LensServerPlugin implements Plugin<{}, {}, {}, {}> { setup(core: CoreSetup, plugins: PluginSetupContract) { setupSavedObjects(core); setupRoutes(core, this.initializerContext.logger.get()); + setupExpressions(core, plugins.expressions); + if (plugins.usageCollection && plugins.taskManager) { registerLensUsageCollector( plugins.usageCollection, From 77d7d1730b65bf5ae31e4f75d33dfba30dd4d12b Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Wed, 11 Aug 2021 04:59:07 -0700 Subject: [PATCH 074/104] [Fleet] Add owner to kibana.json (#108136) --- x-pack/plugins/fleet/kibana.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/plugins/fleet/kibana.json b/x-pack/plugins/fleet/kibana.json index bbf60ef82ee6c..f9ad1b0b966a4 100644 --- a/x-pack/plugins/fleet/kibana.json +++ b/x-pack/plugins/fleet/kibana.json @@ -1,5 +1,9 @@ { "id": "fleet", + "owner": { + "name": "Fleet", + "githubTeam": "fleet" + }, "version": "kibana", "server": true, "ui": true, From 96ddfe6f7e05f88f12f2952f2a2cd19c3aafe864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Wed, 11 Aug 2021 09:20:59 -0400 Subject: [PATCH 075/104] [APM] Latency threshold incorrect value (#107963) * fixing threshold * removing console * adding unit test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...er_transaction_duration_alert_type.test.ts | 67 +++++++++++++++++++ ...egister_transaction_duration_alert_type.ts | 9 +-- 2 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.test.ts diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.test.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.test.ts new file mode 100644 index 0000000000000..ec0a986e2722a --- /dev/null +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { registerTransactionDurationAlertType } from './register_transaction_duration_alert_type'; +import { createRuleTypeMocks } from './test_utils'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; + +describe('registerTransactionDurationAlertType', () => { + it('sends alert when value is greater than threashold', async () => { + const { + services, + dependencies, + executor, + scheduleActions, + } = createRuleTypeMocks(); + + registerTransactionDurationAlertType(dependencies); + + services.scopedClusterClient.asCurrentUser.search.mockReturnValue( + elasticsearchClientMock.createSuccessTransportRequestPromise({ + hits: { + hits: [], + total: { + relation: 'eq', + value: 2, + }, + }, + aggregations: { + latency: { + value: 5500000, + }, + }, + took: 0, + timed_out: false, + _shards: { + failed: 0, + skipped: 0, + successful: 1, + total: 1, + }, + }) + ); + + const params = { + threshold: 3000, + windowSize: 5, + windowUnit: 'm', + transactionType: 'request', + serviceName: 'opbeans-java', + }; + await executor({ params }); + expect(scheduleActions).toHaveBeenCalledTimes(1); + expect(scheduleActions).toHaveBeenCalledWith('threshold_met', { + transactionType: 'request', + serviceName: 'opbeans-java', + environment: 'Not defined', + threshold: 3000, + triggerValue: '5,500 ms', + interval: `5m`, + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts index c14675cb93987..88394cb2c5988 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts @@ -149,9 +149,10 @@ export function registerTransactionDurationAlertType({ const transactionDuration = 'values' in latency ? Object.values(latency.values)[0] : latency?.value; - const threshold = alertParams.threshold * 1000; + // Converts threshold to microseconds because this is the unit used on transactionDuration + const thresholdMicroseconds = alertParams.threshold * 1000; - if (transactionDuration && transactionDuration > threshold) { + if (transactionDuration && transactionDuration > thresholdMicroseconds) { const durationFormatter = getDurationFormatter(transactionDuration); const transactionDurationFormatted = durationFormatter( transactionDuration @@ -168,14 +169,14 @@ export function registerTransactionDurationAlertType({ [TRANSACTION_TYPE]: alertParams.transactionType, [PROCESSOR_EVENT]: ProcessorEvent.transaction, [ALERT_EVALUATION_VALUE]: transactionDuration, - [ALERT_EVALUATION_THRESHOLD]: alertParams.threshold * 1000, + [ALERT_EVALUATION_THRESHOLD]: alertParams.threshold, }, }) .scheduleActions(alertTypeConfig.defaultActionGroupId, { transactionType: alertParams.transactionType, serviceName: alertParams.serviceName, environment: getEnvironmentLabel(alertParams.environment), - threshold, + threshold: alertParams.threshold, triggerValue: transactionDurationFormatted, interval: `${alertParams.windowSize}${alertParams.windowUnit}`, }); From 6054d7e352bcf83bb1d3d9cc5ce0dc25ac1af0cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Wed, 11 Aug 2021 16:19:32 +0200 Subject: [PATCH 076/104] [APM] Rename "Error rate" to "Failed transaction rate" (#107895) --- x-pack/plugins/apm/common/alert_types.ts | 2 +- .../components/alerting/register_apm_alerts.ts | 2 +- .../backend_error_rate_chart.tsx | 8 ++++++-- .../app/backend_detail_overview/index.tsx | 8 ++++---- .../public/components/app/correlations/index.tsx | 2 +- .../app/service_inventory/service_list/index.tsx | 2 +- .../app/service_map/Popover/stats_list.tsx | 2 +- .../get_columns.tsx | 2 +- .../service_overview_throughput_chart.tsx | 2 +- .../alerting_popover_flyout.tsx | 4 ++-- .../charts/transaction_error_rate_chart/index.tsx | 4 ++-- .../components/shared/dependencies_table/index.tsx | 2 +- .../shared/transactions_table/get_columns.tsx | 2 +- .../chart_preview/get_transaction_error_rate.ts | 7 ++----- .../get_correlations_for_failed_transactions.ts | 4 ++-- .../errors/get_overall_error_timeseries.ts | 4 ++-- .../server/lib/helpers/transaction_error_rate.ts | 6 +++--- ...ervice_transaction_group_detailed_statistics.ts | 4 ++-- .../lib/services/get_service_transaction_groups.ts | 6 ++---- .../get_services/get_service_transaction_stats.ts | 4 ++-- .../get_service_transaction_detailed_statistics.ts | 4 ++-- .../lib/transaction_groups/get_error_rate.ts | 10 ++++------ x-pack/plugins/apm/server/routes/backends.ts | 4 ++-- .../tests/alerts/rule_registry.ts | 14 +++++++------- 24 files changed, 53 insertions(+), 56 deletions(-) diff --git a/x-pack/plugins/apm/common/alert_types.ts b/x-pack/plugins/apm/common/alert_types.ts index 9476396a7aefa..fa706b7d8cb35 100644 --- a/x-pack/plugins/apm/common/alert_types.ts +++ b/x-pack/plugins/apm/common/alert_types.ts @@ -71,7 +71,7 @@ export const ALERT_TYPES_CONFIG: Record< }, [AlertType.TransactionErrorRate]: { name: i18n.translate('xpack.apm.transactionErrorRateAlert.name', { - defaultMessage: 'Transaction error rate threshold', + defaultMessage: 'Failed transaction rate threshold', }), actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: THRESHOLD_MET_GROUP_ID, diff --git a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts index ff06c845ba4d3..bcac3ad4e126e 100644 --- a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts +++ b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts @@ -154,7 +154,7 @@ export function registerApmAlerts( reason: i18n.translate( 'xpack.apm.alertTypes.transactionErrorRate.reason', { - defaultMessage: `Transaction error rate is greater than {threshold} (current value is {measured}) for {serviceName}`, + defaultMessage: `Failed transactions rate is greater than {threshold} (current value is {measured}) for {serviceName}`, values: { threshold: asPercent(fields[ALERT_EVALUATION_THRESHOLD], 100), measured: asPercent(fields[ALERT_EVALUATION_VALUE], 100), diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_error_rate_chart.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_error_rate_chart.tsx index bcdc4aae5ded1..e2c572c46a3da 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_error_rate_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/backend_error_rate_chart.tsx @@ -21,7 +21,11 @@ function yLabelFormat(y?: number | null) { return asPercent(y || 0, 1); } -export function BackendErrorRateChart({ height }: { height: number }) { +export function BackendFailedTransactionRateChart({ + height, +}: { + height: number; +}) { const { backendName } = useApmBackendContext(); const theme = useTheme(); @@ -72,7 +76,7 @@ export function BackendErrorRateChart({ height }: { height: number }) { type: 'linemark', color: theme.eui.euiColorVis7, title: i18n.translate('xpack.apm.backendErrorRateChart.chartTitle', { - defaultMessage: 'Error rate', + defaultMessage: 'Failed transaction rate', }), }); } diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx index fdcb6bdf38e42..39d13247bc5e4 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx @@ -21,7 +21,7 @@ import { BackendLatencyChart } from './backend_latency_chart'; import { BackendInventoryTitle } from '../../routing/home'; import { BackendDetailDependenciesTable } from './backend_detail_dependencies_table'; import { BackendThroughputChart } from './backend_throughput_chart'; -import { BackendErrorRateChart } from './backend_error_rate_chart'; +import { BackendFailedTransactionRateChart } from './backend_error_rate_chart'; import { BackendDetailTemplate } from '../../routing/templates/backend_detail_template'; export function BackendDetailOverview() { @@ -86,12 +86,12 @@ export function BackendDetailOverview() {

    {i18n.translate( - 'xpack.apm.backendDetailErrorRateChartTitle', - { defaultMessage: 'Error rate' } + 'xpack.apm.backendDetailFailedTransactionRateChartTitle', + { defaultMessage: 'Failed transaction rate' } )}

    - + diff --git a/x-pack/plugins/apm/public/components/app/correlations/index.tsx b/x-pack/plugins/apm/public/components/app/correlations/index.tsx index ad26996042792..0bb7ac3c17c42 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/index.tsx @@ -54,7 +54,7 @@ import { useApmServiceContext } from '../../../context/apm_service/use_apm_servi const errorRateTab = { key: 'errorRate', label: i18n.translate('xpack.apm.correlations.tabs.errorRateLabel', { - defaultMessage: 'Error rate', + defaultMessage: 'Failed transaction rate', }), component: ErrorCorrelations, }; diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx index f50816809df07..32b77c58c38fd 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx @@ -187,7 +187,7 @@ export function getServiceColumns({ { field: 'transactionErrorRate', name: i18n.translate('xpack.apm.servicesTable.transactionErrorRate', { - defaultMessage: 'Error rate %', + defaultMessage: 'Failed transaction rate', }), sortable: true, dataType: 'number', diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/stats_list.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/stats_list.tsx index 88915b9bc9f34..b46b7a0986179 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/stats_list.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/stats_list.tsx @@ -104,7 +104,7 @@ export function StatsList({ data, isLoading }: StatsListProps) { }, { title: i18n.translate('xpack.apm.serviceMap.errorRatePopoverStat', { - defaultMessage: 'Trans. error rate (avg.)', + defaultMessage: 'Failed transaction rate (avg.)', }), description: asPercent(avgErrorRate, 1, ''), }, diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx index 1ff896cff57f7..221b415326783 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx @@ -148,7 +148,7 @@ export function getColumns({ field: 'errorRate', name: i18n.translate( 'xpack.apm.serviceOverview.instancesTableColumnErrorRate', - { defaultMessage: 'Error rate' } + { defaultMessage: 'Failed transaction rate' } ), width: `${unit * 8}px`, render: (_, { serviceNodeName, errorRate }) => { diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx index 5eb130140ec90..741491f87f78c 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx @@ -129,7 +129,7 @@ export function ServiceOverviewThroughputChart({ {data.throughputUnit === 'second' ? i18n.translate( 'xpack.apm.serviceOverview.throughtputPerSecondChartTitle', - { defaultMessage: '(per second)' } + { defaultMessage: ' (per second)' } ) : ''} diff --git a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx index 4abd36a277311..4c593e80df0cb 100644 --- a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx +++ b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/alerting_popover_flyout.tsx @@ -26,7 +26,7 @@ const transactionDurationLabel = i18n.translate( ); const transactionErrorRateLabel = i18n.translate( 'xpack.apm.home.alertsMenu.transactionErrorRate', - { defaultMessage: 'Transaction error rate' } + { defaultMessage: 'Failed transaction rate' } ); const errorCountLabel = i18n.translate('xpack.apm.home.alertsMenu.errorCount', { defaultMessage: 'Error count', @@ -146,7 +146,7 @@ export function AlertingPopoverAndFlyout({ ], }, - // transaction error rate panel + // Failed transactions panel { id: CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID, title: transactionErrorRateLabel, diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx index 226f9c095c2c3..1685051bb44e8 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx @@ -114,7 +114,7 @@ export function TransactionErrorRateChart({ type: 'linemark', color: theme.eui.euiColorVis7, title: i18n.translate('xpack.apm.errorRate.chart.errorRate', { - defaultMessage: 'Error rate (avg.)', + defaultMessage: 'Failed transaction rate (avg.)', }), }, ...(comparisonEnabled @@ -137,7 +137,7 @@ export function TransactionErrorRateChart({

    {i18n.translate('xpack.apm.errorRate', { - defaultMessage: 'Error rate', + defaultMessage: 'Failed transaction rate', })}

    diff --git a/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx index a599a3bd0aa62..0b8fb787c8d8a 100644 --- a/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx @@ -113,7 +113,7 @@ export function DependenciesTable(props: Props) { { field: 'errorRateValue', name: i18n.translate('xpack.apm.dependenciesTable.columnErrorRate', { - defaultMessage: 'Error rate', + defaultMessage: 'Failed transaction rate', }), width: `${unit * 10}px`, render: (_, { currentStats, previousStats }) => { diff --git a/x-pack/plugins/apm/public/components/shared/transactions_table/get_columns.tsx b/x-pack/plugins/apm/public/components/shared/transactions_table/get_columns.tsx index 1103bfa93326f..a7574694647f0 100644 --- a/x-pack/plugins/apm/public/components/shared/transactions_table/get_columns.tsx +++ b/x-pack/plugins/apm/public/components/shared/transactions_table/get_columns.tsx @@ -122,7 +122,7 @@ export function getColumns({ sortable: true, name: i18n.translate( 'xpack.apm.serviceOverview.transactionsTableColumnErrorRate', - { defaultMessage: 'Error rate' } + { defaultMessage: 'Failed transaction rate' } ), width: `${unit * 8}px`, render: (_, { errorRate, name }) => { diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts index 888c929a2c721..f4d8ffc2749c3 100644 --- a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts @@ -17,7 +17,7 @@ import { environmentQuery } from '../../../../common/utils/environment_query'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { - calculateTransactionErrorPercentage, + calculateFailedTransactionRate, getOutcomeAggregation, } from '../../helpers/transaction_error_rate'; @@ -75,12 +75,9 @@ export async function getTransactionErrorRateChartPreview({ } return resp.aggregations.timeseries.buckets.map((bucket) => { - const errorPercentage = calculateTransactionErrorPercentage( - bucket.outcomes - ); return { x: bucket.key, - y: errorPercentage, + y: calculateFailedTransactionRate(bucket.outcomes), }; }); } diff --git a/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts b/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts index 081c66dc2c471..8cfaaa6021b1e 100644 --- a/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts +++ b/x-pack/plugins/apm/server/lib/correlations/errors/get_correlations_for_failed_transactions.ts @@ -19,7 +19,7 @@ import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { getTimeseriesAggregation, - getTransactionErrorRateTimeSeries, + getFailedTransactionRateTimeSeries, } from '../../helpers/transaction_error_rate'; import { CorrelationsOptions, getCorrelationsFilters } from '../get_filters'; @@ -145,7 +145,7 @@ export async function getErrorRateTimeSeries({ return { ...topSig, - timeseries: getTransactionErrorRateTimeSeries(agg.timeseries.buckets), + timeseries: getFailedTransactionRateTimeSeries(agg.timeseries.buckets), }; }), }; diff --git a/x-pack/plugins/apm/server/lib/correlations/errors/get_overall_error_timeseries.ts b/x-pack/plugins/apm/server/lib/correlations/errors/get_overall_error_timeseries.ts index f3477273806b6..0c36a2853746f 100644 --- a/x-pack/plugins/apm/server/lib/correlations/errors/get_overall_error_timeseries.ts +++ b/x-pack/plugins/apm/server/lib/correlations/errors/get_overall_error_timeseries.ts @@ -9,7 +9,7 @@ import { ProcessorEvent } from '../../../../common/processor_event'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { getTimeseriesAggregation, - getTransactionErrorRateTimeSeries, + getFailedTransactionRateTimeSeries, } from '../../helpers/transaction_error_rate'; import { CorrelationsOptions, getCorrelationsFilters } from '../get_filters'; @@ -43,7 +43,7 @@ export async function getOverallErrorTimeseries(options: CorrelationsOptions) { return { overall: { - timeseries: getTransactionErrorRateTimeSeries( + timeseries: getFailedTransactionRateTimeSeries( aggregations.timeseries.buckets ), }, diff --git a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts index 41d9c373710c1..240cbceb6c878 100644 --- a/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/helpers/transaction_error_rate.ts @@ -35,7 +35,7 @@ export const getTimeseriesAggregation = ( aggs: { outcomes: getOutcomeAggregation() }, }); -export function calculateTransactionErrorPercentage( +export function calculateFailedTransactionRate( outcomeResponse: AggregationResultOf ) { const outcomes = Object.fromEntries( @@ -48,7 +48,7 @@ export function calculateTransactionErrorPercentage( return failedTransactions / (successfulTransactions + failedTransactions); } -export function getTransactionErrorRateTimeSeries( +export function getFailedTransactionRateTimeSeries( buckets: AggregationResultOf< { date_histogram: AggregationOptionsByType['date_histogram']; @@ -60,7 +60,7 @@ export function getTransactionErrorRateTimeSeries( return buckets.map((dateBucket) => { return { x: dateBucket.key, - y: calculateTransactionErrorPercentage(dateBucket.outcomes), + y: calculateFailedTransactionRate(dateBucket.outcomes), }; }); } diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_detailed_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_detailed_statistics.ts index d39b521e6ae97..9d5b173a1c7e2 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_detailed_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_detailed_statistics.ts @@ -29,7 +29,7 @@ import { getLatencyValue, } from '../helpers/latency_aggregation_type'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { calculateTransactionErrorPercentage } from '../helpers/transaction_error_rate'; +import { calculateFailedTransactionRate } from '../helpers/transaction_error_rate'; export async function getServiceTransactionGroupDetailedStatistics({ environment, @@ -164,7 +164,7 @@ export async function getServiceTransactionGroupDetailedStatistics({ })); const errorRate = bucket.timeseries.buckets.map((timeseriesBucket) => ({ x: timeseriesBucket.key, - y: calculateTransactionErrorPercentage(timeseriesBucket[EVENT_OUTCOME]), + y: calculateFailedTransactionRate(timeseriesBucket[EVENT_OUTCOME]), })); const transactionGroupTotalDuration = bucket.transaction_group_total_duration.value || 0; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts index e3f2795eb38e8..c8ae36946a2f3 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts @@ -26,7 +26,7 @@ import { getLatencyValue, } from '../helpers/latency_aggregation_type'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { calculateTransactionErrorPercentage } from '../helpers/transaction_error_rate'; +import { calculateFailedTransactionRate } from '../helpers/transaction_error_rate'; export type ServiceOverviewTransactionGroupSortField = | 'name' @@ -115,9 +115,7 @@ export async function getServiceTransactionGroups({ const transactionGroups = response.aggregations?.transaction_groups.buckets.map((bucket) => { - const errorRate = calculateTransactionErrorPercentage( - bucket[EVENT_OUTCOME] - ); + const errorRate = calculateFailedTransactionRate(bucket[EVENT_OUTCOME]); const transactionGroupTotalDuration = bucket.transaction_group_total_duration.value || 0; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts index edc9e5cf90026..885d73b13bb93 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts @@ -25,7 +25,7 @@ import { } from '../../helpers/aggregated_transactions'; import { calculateThroughput } from '../../helpers/calculate_throughput'; import { - calculateTransactionErrorPercentage, + calculateFailedTransactionRate, getOutcomeAggregation, } from '../../helpers/transaction_error_rate'; import { ServicesItemsSetup } from './get_services_items'; @@ -137,7 +137,7 @@ export async function getServiceTransactionStats({ AGENT_NAME ] as AgentName, latency: topTransactionTypeBucket.avg_duration.value, - transactionErrorRate: calculateTransactionErrorPercentage( + transactionErrorRate: calculateFailedTransactionRate( topTransactionTypeBucket.outcomes ), throughput: calculateThroughput({ diff --git a/x-pack/plugins/apm/server/lib/services/get_services_detailed_statistics/get_service_transaction_detailed_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_services_detailed_statistics/get_service_transaction_detailed_statistics.ts index d339641069eb5..90fa54f31eb34 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services_detailed_statistics/get_service_transaction_detailed_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services_detailed_statistics/get_service_transaction_detailed_statistics.ts @@ -26,7 +26,7 @@ import { calculateThroughput } from '../../helpers/calculate_throughput'; import { getBucketSizeForAggregatedTransactions } from '../../helpers/get_bucket_size_for_aggregated_transactions'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { - calculateTransactionErrorPercentage, + calculateFailedTransactionRate, getOutcomeAggregation, } from '../../helpers/transaction_error_rate'; @@ -148,7 +148,7 @@ export async function getServiceTransactionDetailedStatistics({ transactionErrorRate: topTransactionTypeBucket.timeseries.buckets.map( (dateBucket) => ({ x: dateBucket.key + offsetInMs, - y: calculateTransactionErrorPercentage(dateBucket.outcomes), + y: calculateFailedTransactionRate(dateBucket.outcomes), }) ), throughput: topTransactionTypeBucket.timeseries.buckets.map( diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts index 3184c58ce983a..5c33959399104 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts @@ -23,9 +23,9 @@ import { import { getBucketSizeForAggregatedTransactions } from '../helpers/get_bucket_size_for_aggregated_transactions'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { - calculateTransactionErrorPercentage, + calculateFailedTransactionRate, getOutcomeAggregation, - getTransactionErrorRateTimeSeries, + getFailedTransactionRateTimeSeries, } from '../helpers/transaction_error_rate'; export async function getErrorRate({ @@ -124,13 +124,11 @@ export async function getErrorRate({ return { noHits, transactionErrorRate: [], average: null }; } - const transactionErrorRate = getTransactionErrorRateTimeSeries( + const transactionErrorRate = getFailedTransactionRateTimeSeries( resp.aggregations.timeseries.buckets ); - const average = calculateTransactionErrorPercentage( - resp.aggregations.outcomes - ); + const average = calculateFailedTransactionRate(resp.aggregations.outcomes); return { noHits, transactionErrorRate, average }; } diff --git a/x-pack/plugins/apm/server/routes/backends.ts b/x-pack/plugins/apm/server/routes/backends.ts index e673770dbbc1a..c5c2b3cac2283 100644 --- a/x-pack/plugins/apm/server/routes/backends.ts +++ b/x-pack/plugins/apm/server/routes/backends.ts @@ -237,7 +237,7 @@ const backendThroughputChartsRoute = createApmServerRoute({ }, }); -const backendErrorRateChartsRoute = createApmServerRoute({ +const backendFailedTransactionRateChartsRoute = createApmServerRoute({ endpoint: 'GET /api/apm/backends/{backendName}/charts/error_rate', params: t.type({ path: t.type({ @@ -288,4 +288,4 @@ export const backendsRouteRepository = createApmServerRouteRepository() .add(backendMetadataRoute) .add(backendLatencyChartsRoute) .add(backendThroughputChartsRoute) - .add(backendErrorRateChartsRoute); + .add(backendFailedTransactionRateChartsRoute); diff --git a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts index 6c83bba99abad..0d19d3fd1cda1 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.ts @@ -215,7 +215,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { actions: [], tags: ['apm', 'service.name:opbeans-go'], notifyWhen: 'onActionGroupChange', - name: 'Transaction error rate threshold | opbeans-go', + name: 'Failed transaction rate threshold | opbeans-go', }; const { body: response, status } = await supertest @@ -371,13 +371,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { "apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED", ], "kibana.alert.rule.category": Array [ - "Transaction error rate threshold", + "Failed transaction rate threshold", ], "kibana.alert.rule.consumer": Array [ "apm", ], "kibana.alert.rule.name": Array [ - "Transaction error rate threshold | opbeans-go", + "Failed transaction rate threshold | opbeans-go", ], "kibana.alert.rule.producer": Array [ "apm", @@ -447,13 +447,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { "apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED", ], "kibana.alert.rule.category": Array [ - "Transaction error rate threshold", + "Failed transaction rate threshold", ], "kibana.alert.rule.consumer": Array [ "apm", ], "kibana.alert.rule.name": Array [ - "Transaction error rate threshold | opbeans-go", + "Failed transaction rate threshold | opbeans-go", ], "kibana.alert.rule.producer": Array [ "apm", @@ -551,13 +551,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { "apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED", ], "kibana.alert.rule.category": Array [ - "Transaction error rate threshold", + "Failed transaction rate threshold", ], "kibana.alert.rule.consumer": Array [ "apm", ], "kibana.alert.rule.name": Array [ - "Transaction error rate threshold | opbeans-go", + "Failed transaction rate threshold | opbeans-go", ], "kibana.alert.rule.producer": Array [ "apm", From 35f55ce262cecf1650adc12cec2d06cc44594244 Mon Sep 17 00:00:00 2001 From: Kyle Pollich Date: Wed, 11 Aug 2021 10:32:41 -0400 Subject: [PATCH 077/104] [Fleet] Update Package Policy UI to Support Upgrading Package Policies (#107171) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove description column, replace w/ tooltip * Add upgrade tooltip to version column in package policy table * Add update policy action * Add inline upgrade package policy button * Clean up types + add upgrade CTA's to integrations policy table * Fix i18n * Fix button widths * Upgrade package policy when saving integration w/ upgrade param * Update edit policy page description for upgrades * Support setting vars on new package version before saving to upgrade * Add flyout for JSON of previous policy version * Compile package policy before displaying in flyout * Support different success redirects following package policy upgrades * Fix i18n * Fix more type errors * Fix even more type errors 🙃 * Fix type errors * Don't throw errors for missing vars, include them in missingVars response object * Update tests for new missingVars field * Fix failing tests * Address PR feedback * Fix missing i18n value * Fix types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/fleet/common/services/routes.ts | 4 + x-pack/plugins/fleet/common/types/index.ts | 1 + .../common/types/models/package_policy.ts | 1 + .../components/layout.tsx | 124 ++++++---- .../create_package_policy_page/index.tsx | 4 +- .../create_package_policy_page/types.ts | 8 +- .../package_policies_table.tsx | 104 +++++--- .../edit_package_policy_page/index.tsx | 227 ++++++++++++++++-- .../fleet/sections/agent_policy/index.tsx | 4 + .../upgrade_package_policy_page/index.tsx | 34 +++ .../detail/policies/package_policies.tsx | 90 ++++++- .../package_policy_actions_menu.tsx | 17 +- .../fleet/public/constants/page_paths.ts | 6 + x-pack/plugins/fleet/public/hooks/index.ts | 1 + .../hooks/use_package_installations.tsx | 18 +- .../hooks/use_request/package_policy.ts | 24 ++ .../public/types/in_memory_package_policy.ts | 16 ++ x-pack/plugins/fleet/public/types/index.ts | 5 +- .../fleet/server/services/package_policy.ts | 50 ++-- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - .../apis/package_policy/upgrade.ts | 12 +- 22 files changed, 618 insertions(+), 136 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/upgrade_package_policy_page/index.tsx create mode 100644 x-pack/plugins/fleet/public/types/in_memory_package_policy.ts diff --git a/x-pack/plugins/fleet/common/services/routes.ts b/x-pack/plugins/fleet/common/services/routes.ts index 1e51f2d6163cc..5294c31d6a289 100644 --- a/x-pack/plugins/fleet/common/services/routes.ts +++ b/x-pack/plugins/fleet/common/services/routes.ts @@ -81,6 +81,10 @@ export const packagePolicyRouteService = { getDeletePath: () => { return PACKAGE_POLICY_API_ROUTES.DELETE_PATTERN; }, + + getUpgradePath: () => { + return PACKAGE_POLICY_API_ROUTES.UPGRADE_PATTERN; + }, }; export const agentPolicyRouteService = { diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts index 59691bf32d099..0deda3bf32657 100644 --- a/x-pack/plugins/fleet/common/types/index.ts +++ b/x-pack/plugins/fleet/common/types/index.ts @@ -7,6 +7,7 @@ export * from './models'; export * from './rest_spec'; + import type { PreconfiguredAgentPolicy, PreconfiguredPackage } from './models/preconfiguration'; export interface FleetConfigType { diff --git a/x-pack/plugins/fleet/common/types/models/package_policy.ts b/x-pack/plugins/fleet/common/types/models/package_policy.ts index 2ff21961f1545..ab977c5d67d0d 100644 --- a/x-pack/plugins/fleet/common/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/package_policy.ts @@ -81,4 +81,5 @@ export type PackagePolicySOAttributes = Omit; export type DryRunPackagePolicy = NewPackagePolicy & { errors?: Array<{ key: string | undefined; message: string }>; + missingVars?: string[]; }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx index dd24f1091843a..59498325bf91f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/layout.tsx @@ -21,10 +21,10 @@ import { import { WithHeaderLayout } from '../../../../layouts'; import type { AgentPolicy, PackageInfo, RegistryPolicyTemplate } from '../../../../types'; import { PackageIcon } from '../../../../components'; -import type { CreatePackagePolicyFrom } from '../types'; +import type { EditPackagePolicyFrom } from '../types'; export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ - from: CreatePackagePolicyFrom; + from: EditPackagePolicyFrom; cancelUrl: string; onCancel?: React.ReactEventHandler; agentPolicy?: AgentPolicy; @@ -48,11 +48,48 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ 'data-test-subj': dataTestSubj, tabs = [], }) => { + const isAdd = useMemo(() => ['package'].includes(from), [from]); + const isEdit = useMemo(() => ['edit', 'package-edit'].includes(from), [from]); + const isUpgrade = useMemo( + () => + ['upgrade-from-fleet-policy-list', 'upgrade-from-integrations-policy-list'].includes(from), + [from] + ); + const pageTitle = useMemo(() => { - if ( - (from === 'package' || from === 'package-edit' || from === 'edit' || from === 'policy') && - packageInfo - ) { + if ((isAdd || isEdit || isUpgrade) && packageInfo) { + let pageTitleText = ( + + ); + + if (isEdit) { + pageTitleText = ( + + ); + } else if (isUpgrade) { + pageTitleText = ( + + ); + } + return ( @@ -66,32 +103,14 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ -

    - {from === 'edit' || from === 'package-edit' ? ( - - ) : ( - - )} -

    +

    {pageTitleText}

    ); } - return from === 'edit' || from === 'package-edit' ? ( + return isEdit ? (

    { - return from === 'edit' || from === 'package-edit' ? ( - - ) : from === 'policy' ? ( - - ) : ( - - ); - }, [from]); + if (isEdit) { + return ( + + ); + } else if (isAdd) { + return ( + + ); + } else if (isUpgrade) { + return ( + + ); + } else { + return ( + + ); + } + }, [isAdd, isEdit, isUpgrade]); const leftColumn = ( @@ -167,7 +201,7 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ ); const rightColumn = - agentPolicy && (from === 'policy' || from === 'edit') ? ( + agentPolicy && (isAdd || isEdit) ? ( { * We may want to deprecate the ability to pass in policyId from URL params since there is no package * creation possible if a user has not chosen one from the packages UI. */ - const from: CreatePackagePolicyFrom = + const from: EditPackagePolicyFrom = 'policyId' in params || queryParamsPolicyId ? 'policy' : 'package'; // Agent policy and package info states diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts index a4d7b5a952b66..ba48553edad9a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/types.ts @@ -5,5 +5,11 @@ * 2.0. */ -export type CreatePackagePolicyFrom = 'package' | 'package-edit' | 'policy' | 'edit'; +export type EditPackagePolicyFrom = + | 'package' + | 'package-edit' + | 'policy' + | 'edit' + | 'upgrade-from-fleet-policy-list' + | 'upgrade-from-integrations-policy-list'; export type PackagePolicyFormState = 'VALID' | 'INVALID' | 'CONFIRM' | 'LOADING' | 'SUBMITTED'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx index 0d2d8e1882183..4f5c61b76f4e1 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx @@ -16,19 +16,20 @@ import { EuiFlexGroup, EuiFlexItem, EuiText, + EuiIcon, + EuiToolTip, } from '@elastic/eui'; import { INTEGRATIONS_PLUGIN_ID } from '../../../../../../../../common'; import { pagePathGetters } from '../../../../../../../constants'; -import type { AgentPolicy, PackagePolicy } from '../../../../../types'; +import type { AgentPolicy, InMemoryPackagePolicy, PackagePolicy } from '../../../../../types'; import { PackageIcon, PackagePolicyActionsMenu } from '../../../../../components'; -import { useCapabilities, useStartServices } from '../../../../../hooks'; - -interface InMemoryPackagePolicy extends PackagePolicy { - packageName?: string; - packageTitle?: string; - packageVersion?: string; -} +import { + useCapabilities, + useLink, + usePackageInstallations, + useStartServices, +} from '../../../../../hooks'; interface Props { packagePolicies: PackagePolicy[]; @@ -53,6 +54,8 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ }) => { const { application } = useStartServices(); const hasWriteCapabilities = useCapabilities().write; + const { updatableIntegrations } = usePackageInstallations(); + const { getHref } = useLink(); // With the package policies provided on input, generate the list of package policies // used in the InMemoryTable (flattens some values for search) as well as @@ -66,11 +69,22 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ namespacesValues.push(packagePolicy.namespace); } + const updatableIntegrationRecord = updatableIntegrations.get( + packagePolicy.package?.name ?? '' + ); + + const hasUpgrade = + !!updatableIntegrationRecord && + updatableIntegrationRecord.policiesToUpgrade.some( + ({ id }) => id === packagePolicy.policy_id + ); + return { ...packagePolicy, packageName: packagePolicy.package?.name ?? '', packageTitle: packagePolicy.package?.title ?? '', packageVersion: packagePolicy.package?.version ?? '', + hasUpgrade, }; } ); @@ -79,7 +93,7 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ inputTypesValues.sort(stringSortAscending); return [mappedPackagePolicies, namespacesValues.map(toFilterOption)]; - }, [originalPackagePolicies]); + }, [originalPackagePolicies, updatableIntegrations]); const columns = useMemo( (): EuiInMemoryTableProps['columns'] => [ @@ -89,24 +103,20 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ name: i18n.translate('xpack.fleet.policyDetails.packagePoliciesTable.nameColumnTitle', { defaultMessage: 'Name', }), - render: (value: string) => ( - - {value} - - ), - }, - { - field: 'description', - name: i18n.translate( - 'xpack.fleet.policyDetails.packagePoliciesTable.descriptionColumnTitle', - { - defaultMessage: 'Description', - } - ), - render: (value: string) => ( - - {value} - + render: (value: string, { description }) => ( + <> + + {value} + + {description ? ( + +   + + + + + ) : null} + ), }, { @@ -143,6 +153,35 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ )} + {packagePolicy.hasUpgrade && ( + <> + + + + + + + + + + + + )} ); }, @@ -167,14 +206,21 @@ export const PackagePoliciesTable: React.FunctionComponent = ({ { render: (packagePolicy: InMemoryPackagePolicy) => { return ( - + ); }, }, ], }, ], - [agentPolicy] + [agentPolicy, getHref] ); return ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index 3dc88c7565e73..8a1ffe21a90e0 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -6,19 +6,28 @@ */ import React, { useState, useEffect, useCallback, useMemo, memo } from 'react'; -import { useRouteMatch, useHistory } from 'react-router-dom'; +import { useRouteMatch } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty, EuiButton, EuiBottomBar, + EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiSpacer, + EuiLink, + EuiFlyout, + EuiCodeBlock, + EuiPortal, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, } from '@elastic/eui'; +import styled from 'styled-components'; -import type { AgentPolicy, PackageInfo, UpdatePackagePolicy } from '../../../types'; +import type { AgentPolicy, PackageInfo, UpdatePackagePolicy, PackagePolicy } from '../../../types'; import { useLink, useBreadcrumbs, @@ -30,6 +39,8 @@ import { sendGetOneAgentPolicy, sendGetOnePackagePolicy, sendGetPackageInfoByKey, + sendUpgradePackagePolicy, + sendUpgradePackagePolicyDryRun, } from '../../../hooks'; import { useBreadcrumbs as useIntegrationsBreadcrumbs } from '../../../../integrations/hooks'; import { Loading, Error, ExtensionWrapper } from '../../../components'; @@ -39,13 +50,16 @@ import type { PackagePolicyValidationResults } from '../create_package_policy_pa import { validatePackagePolicy, validationHasErrors } from '../create_package_policy_page/services'; import type { PackagePolicyFormState, - CreatePackagePolicyFrom, + EditPackagePolicyFrom, } from '../create_package_policy_page/types'; import { StepConfigurePackagePolicy } from '../create_package_policy_page/step_configure_package'; import { StepDefinePackagePolicy } from '../create_package_policy_page/step_define_package_policy'; -import type { GetOnePackagePolicyResponse } from '../../../../../../common/types/rest_spec'; +import type { + GetOnePackagePolicyResponse, + UpgradePackagePolicyDryRunResponse, +} from '../../../../../../common/types/rest_spec'; import type { PackagePolicyEditExtensionComponentProps } from '../../../types'; -import { pkgKeyFromPackageInfo } from '../../../services'; +import { pkgKeyFromPackageInfo, storedPackagePoliciesToAgentInputs } from '../../../services'; export const EditPackagePolicyPage = memo(() => { const { @@ -57,14 +71,13 @@ export const EditPackagePolicyPage = memo(() => { export const EditPackagePolicyForm = memo<{ packagePolicyId: string; - from?: CreatePackagePolicyFrom; + from?: EditPackagePolicyFrom; }>(({ packagePolicyId, from = 'edit' }) => { - const { notifications } = useStartServices(); + const { application, notifications } = useStartServices(); const { agents: { enabled: isFleetEnabled }, } = useConfig(); - const history = useHistory(); - const { getHref, getPath } = useLink(); + const { getHref } = useLink(); // Agent policy, package info, and package policy states const [isLoadingData, setIsLoadingData] = useState(true); @@ -84,6 +97,10 @@ export const EditPackagePolicyForm = memo<{ const [originalPackagePolicy, setOriginalPackagePolicy] = useState< GetOnePackagePolicyResponse['item'] >(); + const [dryRunData, setDryRunData] = useState(); + + const isUpgrade = + from === 'upgrade-from-fleet-policy-list' || from === 'upgrade-from-integrations-policy-list'; const policyId = agentPolicy?.id ?? ''; @@ -113,8 +130,30 @@ export const EditPackagePolicyForm = memo<{ if (agentPolicyData?.item) { setAgentPolicy(agentPolicyData.item); } - if (packagePolicyData?.item) { - setOriginalPackagePolicy(packagePolicyData.item); + + const { data: upgradePackagePolicyDryRunData } = await sendUpgradePackagePolicyDryRun([ + packagePolicyId, + ]); + + if (upgradePackagePolicyDryRunData) { + setDryRunData(upgradePackagePolicyDryRunData); + } + + const basePolicy: PackagePolicy | undefined = packagePolicyData?.item; + let baseInputs: any = basePolicy?.inputs; + let basePackage: any = basePolicy?.package; + + const proposedUpgradePackagePolicy = upgradePackagePolicyDryRunData?.[0]?.diff?.[1]; + + // If we're upgrading the package, we need to "start from" the policy as it's returned from + // the dry run so we can allow the user to edit any new variables before saving + upgrading + if (isUpgrade && !!proposedUpgradePackagePolicy) { + baseInputs = proposedUpgradePackagePolicy.inputs; + basePackage = proposedUpgradePackagePolicy.package; + } + + if (basePolicy) { + setOriginalPackagePolicy(basePolicy); const { id, @@ -127,28 +166,41 @@ export const EditPackagePolicyForm = memo<{ updated_at, /* eslint-enable @typescript-eslint/naming-convention */ ...restOfPackagePolicy - } = packagePolicyData.item; + } = basePolicy as any; // Remove `compiled_stream` from all stream info, we assign this after saving const newPackagePolicy = { ...restOfPackagePolicy, - inputs: inputs.map((input) => { + inputs: baseInputs.map((input: any) => { // Remove `compiled_input` from all input info, we assign this after saving const { streams, compiled_input: compiledInput, ...restOfInput } = input; return { ...restOfInput, - streams: streams.map((stream) => { + streams: streams.map((stream: any) => { // eslint-disable-next-line @typescript-eslint/naming-convention const { compiled_stream, ...restOfStream } = stream; return restOfStream; }), }; }), + package: basePackage, }; + setPackagePolicy(newPackagePolicy); - if (packagePolicyData.item.package) { + + if (basePolicy.package) { + let _packageInfo = basePolicy.package; + + // When upgrading, we need to grab the `packageInfo` data from the new package version's + // proposed policy (comes from the dry run diff) to ensure we have the valid package key/version + // before saving + if (isUpgrade && !!upgradePackagePolicyDryRunData?.[0]?.diff?.[1]?.package) { + _packageInfo = upgradePackagePolicyDryRunData[0].diff?.[1]?.package; + } + const { data: packageData } = await sendGetPackageInfoByKey( - pkgKeyFromPackageInfo(packagePolicyData.item.package) + pkgKeyFromPackageInfo(_packageInfo!) ); + if (packageData?.response) { setPackageInfo(packageData.response); setValidationResults(validatePackagePolicy(newPackagePolicy, packageData.response)); @@ -162,7 +214,7 @@ export const EditPackagePolicyForm = memo<{ setIsLoadingData(false); }; getData(); - }, [policyId, packagePolicyId]); + }, [policyId, packagePolicyId, isUpgrade]); // Retrieve agent count const [agentCount, setAgentCount] = useState(0); @@ -240,7 +292,7 @@ export const EditPackagePolicyForm = memo<{ // Cancel url + Success redirect Path: // if `from === 'edit'` then it links back to Policy Details - // if `from === 'package-edit'` then it links back to the Integration Policy List + // if `from === 'package-edit'`, or `upgrade-from-integrations-policy-list` then it links back to the Integration Policy List const cancelUrl = useMemo((): string => { if (packageInfo && policyId) { return from === 'package-edit' @@ -254,14 +306,14 @@ export const EditPackagePolicyForm = memo<{ const successRedirectPath = useMemo(() => { if (packageInfo && policyId) { - return from === 'package-edit' - ? getPath('integration_details_policies', { + return from === 'package-edit' || from === 'upgrade-from-integrations-policy-list' + ? getHref('integration_details_policies', { pkgkey: pkgKeyFromPackageInfo(packageInfo!), }) - : getPath('policy_details', { policyId }); + : getHref('policy_details', { policyId }); } return '/'; - }, [from, getPath, packageInfo, policyId]); + }, [from, getHref, packageInfo, policyId]); // Save package policy const [formState, setFormState] = useState('INVALID'); @@ -281,9 +333,33 @@ export const EditPackagePolicyForm = memo<{ setFormState('CONFIRM'); return; } + const { error } = await savePackagePolicy(); if (!error) { - history.push(successRedirectPath); + if (isUpgrade) { + const { error: upgradeError } = await sendUpgradePackagePolicy([packagePolicyId]); + + if (upgradeError) { + notifications.toasts.addError(upgradeError, { + title: i18n.translate('xpack.fleet.upgradePackagePolicy.failedNotificationTitle', { + defaultMessage: 'Error upgrading {packagePolicyName}', + values: { + packagePolicyName: packagePolicy.name, + }, + }), + toastMessage: i18n.translate( + 'xpack.fleet.editPackagePolicy.failedConflictNotificationMessage', + { + defaultMessage: `Data is out of date. Refresh the page to get the latest policy.`, + } + ), + }); + + return; + } + } + + application.navigateToUrl(successRedirectPath); notifications.toasts.addSuccess({ title: i18n.translate('xpack.fleet.editPackagePolicy.updatedNotificationTitle', { defaultMessage: `Successfully updated '{packagePolicyName}'`, @@ -468,10 +544,20 @@ export const EditPackagePolicyForm = memo<{ onCancel={() => setFormState('VALID')} /> )} + + {isUpgrade && dryRunData && ( + <> + + + + )} + {configurePackage} + {/* Extra space to accomodate the EuiBottomBar height */} + @@ -526,6 +612,101 @@ const PoliciesBreadcrumb: React.FunctionComponent<{ policyName: string; policyId return null; }; +const UpgradeStatusCallout: React.FunctionComponent<{ + dryRunData: UpgradePackagePolicyDryRunResponse; +}> = ({ dryRunData }) => { + const [isPreviousVersionFlyoutOpen, setIsPreviousVersionFlyoutOpen] = useState(false); + + if (!dryRunData) { + return null; + } + + const isReadyForUpgrade = !dryRunData[0].hasErrors; + + const [currentPackagePolicy, proposedUpgradePackagePolicy] = dryRunData[0].diff || []; + + const FlyoutBody = styled(EuiFlyoutBody)` + .euiFlyoutBody__overflowContent { + padding: 0; + } + `; + + return ( + <> + {isPreviousVersionFlyoutOpen && currentPackagePolicy && ( + + setIsPreviousVersionFlyoutOpen(false)} size="l" maxWidth={640}> + + +

    + +

    +
    +
    + + + {JSON.stringify( + storedPackagePoliciesToAgentInputs([currentPackagePolicy]), + null, + 2 + )} + + +
    +
    + )} + + {isReadyForUpgrade ? ( + + + + ) : ( + + setIsPreviousVersionFlyoutOpen(true)}> + + + ), + }} + /> + + )} + + ); +}; + const IntegrationsBreadcrumb = memo<{ pkgTitle: string; policyName: string; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/index.tsx index 19f0216a39e03..9d9077a9abdbd 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/index.tsx @@ -17,6 +17,7 @@ import { AgentPolicyListPage } from './list_page'; import { AgentPolicyDetailsPage } from './details_page'; import { CreatePackagePolicyPage } from './create_package_policy_page'; import { EditPackagePolicyPage } from './edit_package_policy_page'; +import { UpgradePackagePolicyPage } from './upgrade_package_policy_page'; export const AgentPolicyApp: React.FunctionComponent = () => { useBreadcrumbs('policies'); @@ -28,6 +29,9 @@ export const AgentPolicyApp: React.FunctionComponent = () => { + + + diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/upgrade_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/upgrade_package_policy_page/index.tsx new file mode 100644 index 0000000000000..e9442f84d228e --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/upgrade_package_policy_page/index.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { useLocation, useRouteMatch } from 'react-router-dom'; + +import type { EditPackagePolicyFrom } from '../create_package_policy_page/types'; + +import { EditPackagePolicyForm } from '../edit_package_policy_page'; + +export const UpgradePackagePolicyPage = memo(() => { + const { + params: { packagePolicyId }, + } = useRouteMatch<{ policyId: string; packagePolicyId: string }>(); + const { search } = useLocation(); + + const qs = new URLSearchParams(search); + const fromQs = qs.get('from'); + + let from: EditPackagePolicyFrom | undefined; + + // Shorten query strings to make them more presentable in the URL + if (fromQs && fromQs === 'fleet-policy-list') { + from = 'upgrade-from-fleet-policy-list'; + } else if (fromQs && fromQs === 'integrations-policy-list') { + from = 'upgrade-from-integrations-policy-list'; + } + + return ; +}); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx index b7f044040e43f..bdcfb1ebb6534 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx @@ -28,12 +28,14 @@ import { FormattedRelative, FormattedMessage } from '@kbn/i18n/react'; import styled from 'styled-components'; import { InstallStatus } from '../../../../../types'; +import type { GetAgentPoliciesResponseItem, InMemoryPackagePolicy } from '../../../../../types'; import { useLink, useUrlPagination, useGetPackageInstallStatus, AgentPolicyRefreshContext, useUIExtension, + usePackageInstallations, } from '../../../../../hooks'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; import { @@ -92,9 +94,39 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps perPage: pagination.pageSize, kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: ${name}`, }); - + const { updatableIntegrations } = usePackageInstallations(); const agentEnrollmentFlyoutExtension = useUIExtension(name, 'agent-enrollment-flyout'); + const packageAndAgentPolicies = useMemo((): Array<{ + agentPolicy: GetAgentPoliciesResponseItem; + packagePolicy: InMemoryPackagePolicy; + }> => { + if (!data?.items) { + return []; + } + + const newPolicies = data.items.map(({ agentPolicy, packagePolicy }) => { + const updatableIntegrationRecord = updatableIntegrations.get( + packagePolicy.package?.name ?? '' + ); + + const hasUpgrade = + !!updatableIntegrationRecord && + updatableIntegrationRecord.policiesToUpgrade.some( + ({ id }) => id === packagePolicy.policy_id + ); + return { + agentPolicy, + packagePolicy: { + ...packagePolicy, + hasUpgrade, + }, + }; + }); + + return newPolicies; + }, [data?.items, updatableIntegrations]); + // Handle the "add agent" link displayed in post-installation toast notifications in the case // where a user is clicking the link while on the package policies listing page useEffect(() => { @@ -180,6 +212,49 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.version', { defaultMessage: 'Version', }), + render(_version, { agentPolicy, packagePolicy }) { + const updatableIntegrationRecord = updatableIntegrations.get( + packagePolicy.package?.name ?? '' + ); + + const hasUpgrade = + !!updatableIntegrationRecord && + updatableIntegrationRecord.policiesToUpgrade.some( + ({ id }) => id === packagePolicy.policy_id + ); + + return ( + + + + + + + + {hasUpgrade && ( + + + + + + )} + + ); + }, }, { field: 'packagePolicy.policy_id', @@ -274,12 +349,16 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps packagePolicy={packagePolicy} viewDataStep={viewDataStep} showAddAgent={true} + upgradePackagePolicyHref={`${getHref('upgrade_package_policy', { + policyId: agentPolicy.id, + packagePolicyId: packagePolicy.id, + })}?from=integrations-policy-list`} /> ); }, }, ], - [viewDataStep] + [getHref, updatableIntegrations, viewDataStep] ); const noItemsMessage = useMemo(() => { @@ -314,7 +393,7 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps agentPolicy.id === flyoutOpenForPolicyId) - ?.agentPolicy + packageAndAgentPolicies.find( + ({ agentPolicy }) => agentPolicy.id === flyoutOpenForPolicyId + )?.agentPolicy } viewDataStep={viewDataStep} /> diff --git a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx index 01e2df6cc4102..a87cd7e39bfb2 100644 --- a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx +++ b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx @@ -10,7 +10,7 @@ import { EuiContextMenuItem, EuiPortal } from '@elastic/eui'; import type { EuiStepProps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { AgentPolicy, PackagePolicy } from '../types'; +import type { AgentPolicy, InMemoryPackagePolicy } from '../types'; import { useAgentPolicyRefresh, useCapabilities, useLink } from '../hooks'; @@ -21,10 +21,11 @@ import { PackagePolicyDeleteProvider } from './package_policy_delete_provider'; export const PackagePolicyActionsMenu: React.FunctionComponent<{ agentPolicy: AgentPolicy; - packagePolicy: PackagePolicy; + packagePolicy: InMemoryPackagePolicy; viewDataStep?: EuiStepProps; showAddAgent?: boolean; -}> = ({ agentPolicy, packagePolicy, viewDataStep, showAddAgent }) => { + upgradePackagePolicyHref: string; +}> = ({ agentPolicy, packagePolicy, viewDataStep, showAddAgent, upgradePackagePolicyHref }) => { const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false); const { getHref } = useLink(); const hasWriteCapabilities = useCapabilities().write; @@ -79,6 +80,16 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{ defaultMessage="Edit integration" /> , + + + , // FIXME: implement Copy package policy action // {}} key="packagePolicyCopy"> // [ + FLEET_BASE_PATH, + `/policies/${policyId}/upgrade-package-policy/${packagePolicyId}`, + ], agent_list: ({ kuery }) => [FLEET_BASE_PATH, `/agents${kuery ? `?kuery=${kuery}` : ''}`], agent_details: ({ agentId, tabId, logQuery }) => [ FLEET_BASE_PATH, diff --git a/x-pack/plugins/fleet/public/hooks/index.ts b/x-pack/plugins/fleet/public/hooks/index.ts index a00c0c5dacf11..c41dd1ad42a72 100644 --- a/x-pack/plugins/fleet/public/hooks/index.ts +++ b/x-pack/plugins/fleet/public/hooks/index.ts @@ -26,3 +26,4 @@ export * from './use_ui_extension'; export * from './use_intra_app_state'; export * from './use_platform'; export * from './use_agent_policy_refresh'; +export * from './use_package_installations'; diff --git a/x-pack/plugins/fleet/public/hooks/use_package_installations.tsx b/x-pack/plugins/fleet/public/hooks/use_package_installations.tsx index f8202c71f6004..f4735e6f85546 100644 --- a/x-pack/plugins/fleet/public/hooks/use_package_installations.tsx +++ b/x-pack/plugins/fleet/public/hooks/use_package_installations.tsx @@ -14,6 +14,18 @@ import type { PackagePolicy } from '../types'; import { useGetPackages } from './use_request/epm'; import { useGetAgentPolicies } from './use_request/agent_policy'; +interface UpdatableIntegration { + currentVersion: string; + policiesToUpgrade: Array<{ + id: string; + name: string; + agentsCount: number; + pkgPolicyId: string; + pkgPolicyName: string; + pkgPolicyIntegrationVersion: string; + }>; +} + export const usePackageInstallations = () => { const { data: allPackages, isLoading: isLoadingPackages } = useGetPackages({ experimental: true, @@ -38,7 +50,7 @@ export const usePackageInstallations = () => { [allInstalledPackages] ); - const updatableIntegrations = useMemo( + const updatableIntegrations = useMemo>( () => (agentPolicyData?.items || []).reduce((result, policy) => { policy.package_policies.forEach((pkgPolicy: PackagePolicy | string) => { @@ -60,7 +72,7 @@ export const usePackageInstallations = () => { packageData.policiesToUpgrade.push({ id: policy.id, name: policy.name, - agentsCount: policy.agents, + agentsCount: policy.agents ?? 0, pkgPolicyId: pkgPolicy.id, pkgPolicyName: pkgPolicy.name, pkgPolicyIntegrationVersion: version, @@ -69,7 +81,7 @@ export const usePackageInstallations = () => { } }); return result; - }, new Map()), + }, new Map()), [allInstalledPackages, agentPolicyData] ); diff --git a/x-pack/plugins/fleet/public/hooks/use_request/package_policy.ts b/x-pack/plugins/fleet/public/hooks/use_request/package_policy.ts index 5057b6f51b2c5..d39b15a3b3bfa 100644 --- a/x-pack/plugins/fleet/public/hooks/use_request/package_policy.ts +++ b/x-pack/plugins/fleet/public/hooks/use_request/package_policy.ts @@ -18,6 +18,8 @@ import type { GetPackagePoliciesRequest, GetPackagePoliciesResponse, GetOnePackagePolicyResponse, + UpgradePackagePolicyDryRunResponse, + UpgradePackagePolicyResponse, } from '../../../common/types/rest_spec'; import { sendRequest, useRequest } from './use_request'; @@ -63,3 +65,25 @@ export const sendGetOnePackagePolicy = (packagePolicyId: string) => { method: 'get', }); }; + +export function sendUpgradePackagePolicyDryRun(packagePolicyIds: string[]) { + return sendRequest({ + path: packagePolicyRouteService.getUpgradePath(), + method: 'post', + body: JSON.stringify({ + packagePolicyIds, + dryRun: true, + }), + }); +} + +export function sendUpgradePackagePolicy(packagePolicyIds: string[]) { + return sendRequest({ + path: packagePolicyRouteService.getUpgradePath(), + method: 'post', + body: JSON.stringify({ + packagePolicyIds, + dryRun: false, + }), + }); +} diff --git a/x-pack/plugins/fleet/public/types/in_memory_package_policy.ts b/x-pack/plugins/fleet/public/types/in_memory_package_policy.ts new file mode 100644 index 0000000000000..afdcc92396081 --- /dev/null +++ b/x-pack/plugins/fleet/public/types/in_memory_package_policy.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PackagePolicy } from '../../common/types'; + +// Used in list view tables where virtual/flattened fields are added +export interface InMemoryPackagePolicy extends PackagePolicy { + packageName?: string; + packageTitle?: string; + packageVersion?: string; + hasUpgrade: boolean; +} diff --git a/x-pack/plugins/fleet/public/types/index.ts b/x-pack/plugins/fleet/public/types/index.ts index c91ec42d3e527..2328ca826da71 100644 --- a/x-pack/plugins/fleet/public/types/index.ts +++ b/x-pack/plugins/fleet/public/types/index.ts @@ -50,6 +50,9 @@ export { UpdatePackagePolicyRequest, UpdatePackagePolicyResponse, GetPackagePoliciesResponse, + DryRunPackagePolicy, + UpgradePackagePolicyResponse, + UpgradePackagePolicyDryRunResponse, // API schemas - Data streams GetDataStreamsResponse, // API schemas - Agents @@ -128,5 +131,5 @@ export { } from '../../common'; export * from './intra_app_route_state'; - export * from './ui_extensions'; +export * from './in_memory_package_policy'; diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 8bfb9971ae07e..20734658b8a79 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -27,7 +27,6 @@ import type { UpgradePackagePolicyResponse, PackagePolicyInput, NewPackagePolicyInput, - NewPackagePolicyInputStream, PackagePolicyConfigRecordEntry, PackagePolicyInputStream, PackageInfo, @@ -516,7 +515,13 @@ class PackagePolicyService { updatePackagePolicy.inputs as PackagePolicyInput[] ); - await this.update(soClient, esClient, id, updatePackagePolicy, options); + await this.update( + soClient, + esClient, + id, + omit(updatePackagePolicy, 'missingVars'), + options + ); result.push({ id, name: packagePolicy.name, @@ -829,9 +834,10 @@ export function overridePackageInputs( const inputs = [...basePackagePolicy.inputs]; const packageName = basePackagePolicy.package!.name; const errors = []; + let responseMissingVars: string[] = []; for (const override of inputsOverride) { - const originalInput = inputs.find((i) => i.type === override.type); + let originalInput = inputs.find((i) => i.type === override.type); if (!originalInput) { const e = { error: new Error( @@ -860,7 +866,9 @@ export function overridePackageInputs( if (override.vars) { try { - deepMergeVars(override, originalInput); + const { result, missingVars } = deepMergeVars(override, originalInput); + originalInput = result; + responseMissingVars = [...responseMissingVars, ...missingVars]; } catch (e) { const varName = e.message; const err = { @@ -888,7 +896,7 @@ export function overridePackageInputs( if (override.streams) { for (const stream of override.streams) { - const originalStream = originalInput.streams.find( + let originalStream = originalInput?.streams.find( (s) => s.data_stream.dataset === stream.data_stream.dataset ); if (!originalStream) { @@ -920,7 +928,9 @@ export function overridePackageInputs( if (stream.vars) { try { - deepMergeVars(stream as InputsOverride, originalStream); + const { result, missingVars } = deepMergeVars(stream as InputsOverride, originalStream); + originalStream = result; + responseMissingVars = [...responseMissingVars, ...missingVars]; } catch (e) { const varName = e.message; const streamSet = stream.data_stream.dataset; @@ -951,25 +961,33 @@ export function overridePackageInputs( } } - if (dryRun && errors.length) return { ...basePackagePolicy, inputs, errors }; - return { ...basePackagePolicy, inputs }; + if (dryRun && errors.length) { + return { ...basePackagePolicy, inputs, errors, missingVars: responseMissingVars }; + } + + return { ...basePackagePolicy, inputs, missingVars: responseMissingVars }; } -function deepMergeVars( - override: NewPackagePolicyInput | InputsOverride, - original: NewPackagePolicyInput | NewPackagePolicyInputStream -) { +function deepMergeVars(override: any, original: any): { result: any; missingVars: string[] } { + const result = { ...original }; + const missingVars: string[] = []; + const overrideVars = Array.isArray(override.vars) ? override.vars : Object.entries(override.vars!).map(([key, rest]) => ({ name: key, - ...rest, + ...(rest as any), })); + for (const { name, ...val } of overrideVars) { - if (!original.vars || !Reflect.has(original.vars, name)) { - throw new Error(name); + if (!original.vars || !(name in original.vars)) { + missingVars.push(name); + continue; } + const originalVar = original.vars[name]; - Reflect.set(original.vars, name, { ...originalVar, ...val }); + result[name] = { ...originalVar, ...val }; } + + return { result, missingVars }; } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 00992a049cbc0..9fbfe10f66b20 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9675,7 +9675,6 @@ "xpack.fleet.editPackagePolicy.failedNotificationTitle": "「{packagePolicyName}」の更新エラー", "xpack.fleet.editPackagePolicy.pageDescription": "統合設定を修正し、選択したエージェントポリシーに変更をデプロイします。", "xpack.fleet.editPackagePolicy.pageTitle": "統合の編集", - "xpack.fleet.editPackagePolicy.pageTitleWithPackageName": "{packageName}統合の編集", "xpack.fleet.editPackagePolicy.saveButton": "統合の保存", "xpack.fleet.editPackagePolicy.updatedNotificationMessage": "Fleetは'{agentPolicyName}'ポリシーで使用されているすべてのエージェントに更新をデプロイします", "xpack.fleet.editPackagePolicy.updatedNotificationTitle": "正常に「{packagePolicyName}」を更新しました", @@ -9906,7 +9905,6 @@ "xpack.fleet.policyDetails.ErrorGettingFullAgentPolicy": "エージェントポリシーの読み込みエラー", "xpack.fleet.policyDetails.packagePoliciesTable.actionsColumnTitle": "アクション", "xpack.fleet.policyDetails.packagePoliciesTable.deleteActionTitle": "統合の削除", - "xpack.fleet.policyDetails.packagePoliciesTable.descriptionColumnTitle": "説明", "xpack.fleet.policyDetails.packagePoliciesTable.editActionTitle": "統合の編集", "xpack.fleet.policyDetails.packagePoliciesTable.nameColumnTitle": "名前", "xpack.fleet.policyDetails.packagePoliciesTable.namespaceColumnTitle": "名前空間", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 78cd7e41cc4ec..abedf54509baf 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9937,7 +9937,6 @@ "xpack.fleet.editPackagePolicy.failedNotificationTitle": "更新“{packagePolicyName}”时出错", "xpack.fleet.editPackagePolicy.pageDescription": "修改集成设置并将更改部署到选定代理策略。", "xpack.fleet.editPackagePolicy.pageTitle": "编辑集成", - "xpack.fleet.editPackagePolicy.pageTitleWithPackageName": "编辑 {packageName} 集成", "xpack.fleet.editPackagePolicy.saveButton": "保存集成", "xpack.fleet.editPackagePolicy.updatedNotificationMessage": "Fleet 会将更新部署到所有使用策略“{agentPolicyName}”的代理", "xpack.fleet.editPackagePolicy.updatedNotificationTitle": "已成功更新“{packagePolicyName}”", @@ -10168,7 +10167,6 @@ "xpack.fleet.policyDetails.ErrorGettingFullAgentPolicy": "加载代理策略时出错", "xpack.fleet.policyDetails.packagePoliciesTable.actionsColumnTitle": "操作", "xpack.fleet.policyDetails.packagePoliciesTable.deleteActionTitle": "删除集成", - "xpack.fleet.policyDetails.packagePoliciesTable.descriptionColumnTitle": "描述", "xpack.fleet.policyDetails.packagePoliciesTable.editActionTitle": "编辑集成", "xpack.fleet.policyDetails.packagePoliciesTable.nameColumnTitle": "名称", "xpack.fleet.policyDetails.packagePoliciesTable.namespaceColumnTitle": "命名空间", diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts b/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts index 1cd94ba87ed5d..b310c88b5854d 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; import { skipIfNoDockerRegistry } from '../../helpers'; +import { setupFleetAndAgents } from '../agents/services'; import { UpgradePackagePolicyDryRunResponse, @@ -28,6 +29,8 @@ export default function (providerContext: FtrProviderContext) { await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); }); + setupFleetAndAgents(providerContext); + describe('when package is installed', function () { before(async function () { await supertest @@ -223,7 +226,7 @@ export default function (providerContext: FtrProviderContext) { }); describe('when "dryRun: true" is provided', function () { - it('should return a diff with errors', async function () { + it('should return a diff with missingVars', async function () { const { body }: { body: UpgradePackagePolicyDryRunResponse } = await supertest .post(`/api/fleet/package_policies/upgrade`) .set('kbn-xsrf', 'xxxx') @@ -235,12 +238,13 @@ export default function (providerContext: FtrProviderContext) { expect(body.length).to.be(1); expect(body[0].diff?.length).to.be(2); - expect(body[0].hasErrors).to.be(true); + expect(body[0].hasErrors).to.be(false); + expect(body[0].diff?.[1].missingVars).to.contain('test_var'); }); }); describe('when "dryRun: false" is provided', function () { - it('should respond with an error', async function () { + it('should succeed', async function () { const { body }: { body: UpgradePackagePolicyResponse } = await supertest .post(`/api/fleet/package_policies/upgrade`) .set('kbn-xsrf', 'xxxx') @@ -251,7 +255,7 @@ export default function (providerContext: FtrProviderContext) { .expect(200); expect(body.length).to.be(1); - expect(body[0].success).to.be(false); + expect(body[0].success).to.be(true); }); }); }); From 2071b58494053c09ebff883e7f34af332ad9a3e9 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Wed, 11 Aug 2021 10:41:46 -0400 Subject: [PATCH 078/104] [ML] Data Frame Analytics wizard: ensure cloning retains hyperparameters and results field is correct (#107811) * ensure all hyperparameters are retained on clone * ensure resultsField is set to default when switch enabled * Add FeatureProcessor type * ensure state persists for advanced fields when switching to form --- .../ml/common/types/data_frame_analytics.ts | 28 ++++++++++++ .../details_step/details_step_form.tsx | 7 ++- .../action_clone/clone_action_name.tsx | 6 +++ .../use_create_analytics_form/reducer.ts | 45 ++++++++++++------- .../hooks/use_create_analytics_form/state.ts | 16 ++++++- 5 files changed, 84 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/ml/common/types/data_frame_analytics.ts b/x-pack/plugins/ml/common/types/data_frame_analytics.ts index ec2a244c75468..0a43b15f412f9 100644 --- a/x-pack/plugins/ml/common/types/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/types/data_frame_analytics.ts @@ -143,3 +143,31 @@ export interface AnalyticsMapReturnType { details: Record; // transform, job, or index details error: null | any; } + +export interface FeatureProcessor { + frequency_encoding: { + feature_name: string; + field: string; + frequency_map: Record; + }; + multi_encoding: { + processors: any[]; + }; + n_gram_encoding: { + feature_prefix?: string; + field: string; + length?: number; + n_grams: number[]; + start?: number; + }; + one_hot_encoding: { + field: string; + hot_map: string; + }; + target_mean_encoding: { + default_value: number; + feature_name: string; + field: string; + target_map: Record; + }; +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx index 746b02d934002..824538a56acd7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx @@ -308,7 +308,12 @@ export const DetailsStepForm: FC = ({ values: { defaultValue: DEFAULT_RESULTS_FIELD }, })} checked={useResultsFieldDefault === true} - onChange={() => setUseResultsFieldDefault(!useResultsFieldDefault)} + onChange={() => { + if (!useResultsFieldDefault === true) { + setFormState({ resultsField: undefined }); + } + setUseResultsFieldDefault(!useResultsFieldDefault); + }} data-test-subj="mlAnalyticsCreateJobWizardUseResultsFieldDefault" /> diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx index c661c40958bc0..86734c87c5e16 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx @@ -133,6 +133,9 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo optional: true, formKey: 'etaGrowthRatePerTree', }, + feature_processors: { + optional: true, + }, max_optimization_rounds_per_hyperparameter: { optional: true, formKey: 'maxOptimizationRoundsPerHyperparameter', @@ -235,6 +238,9 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo loss_function_parameter: { optional: true, }, + feature_processors: { + optional: true, + }, early_stopping_enabled: { optional: true, ignore: true, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index 5065aefd921da..71da56c9e14e5 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -108,6 +108,28 @@ const getSourceIndexString = (state: State) => { return ''; }; +const isSourceIndexNameValid = ( + sourceIndexName: string, + sourceIndex: string | Array | undefined +) => { + // general check against Kibana index pattern names, but since this is about the advanced editor + // with support for arrays in the job config, we also need to check that each individual name + // doesn't include a comma if index names are supplied as an array. + // `indexPatterns.validate()` returns a map of messages, we're only interested here if it's valid or not. + // If there are no messages, it means the index pattern is valid. + let sourceIndexNameValid = Object.keys(indexPatterns.validate(sourceIndexName)).length === 0; + if (sourceIndexNameValid) { + if (typeof sourceIndex === 'string') { + sourceIndexNameValid = !sourceIndex.includes(','); + } + if (Array.isArray(sourceIndex)) { + sourceIndexNameValid = !sourceIndex.some((d) => d?.includes(',')); + } + } + + return sourceIndexNameValid; +}; + /** * Validates num_top_feature_importance_values. Must be an integer >= 0. */ @@ -129,21 +151,8 @@ export const validateAdvancedEditor = (state: State): State => { const sourceIndexName = getSourceIndexString(state); const sourceIndexNameEmpty = sourceIndexName === ''; - // general check against Kibana index pattern names, but since this is about the advanced editor - // with support for arrays in the job config, we also need to check that each individual name - // doesn't include a comma if index names are supplied as an array. - // `indexPatterns.validate()` returns a map of messages, we're only interested here if it's valid or not. - // If there are no messages, it means the index pattern is valid. - let sourceIndexNameValid = Object.keys(indexPatterns.validate(sourceIndexName)).length === 0; const sourceIndex = jobConfig?.source?.index; - if (sourceIndexNameValid) { - if (typeof sourceIndex === 'string') { - sourceIndexNameValid = !sourceIndex.includes(','); - } - if (Array.isArray(sourceIndex)) { - sourceIndexNameValid = !sourceIndex.some((d) => d?.includes(',')); - } - } + const sourceIndexNameValid = isSourceIndexNameValid(sourceIndexName, sourceIndex); const destinationIndexName = jobConfig?.dest?.index ?? ''; const destinationIndexNameEmpty = destinationIndexName === ''; @@ -562,9 +571,8 @@ export function reducer(state: State, action: Action): State { case ACTION.SWITCH_TO_FORM: const { jobConfig: config } = state; const { jobId } = state.form; - // Persist form state when switching back from advanced editor // @ts-ignore - const formState = { ...state.form, ...getFormStateFromJobConfig(config, false) }; + const formState = getFormStateFromJobConfig(config, false); if (typeof jobId === 'string' && jobId.trim() !== '') { formState.jobId = jobId; @@ -585,6 +593,11 @@ export function reducer(state: State, action: Action): State { ); } + const sourceIndexName = getSourceIndexString(state); + const sourceIndex = config?.source?.index; + const sourceIndexNameValid = isSourceIndexNameValid(sourceIndexName, sourceIndex); + formState.sourceIndexNameValid = sourceIndexNameValid; + return validateForm({ ...state, // @ts-ignore diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 22efe6f9eb3eb..3c9e394190733 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -17,6 +17,7 @@ import { DataFrameAnalyticsConfig, DataFrameAnalyticsId, DataFrameAnalysisConfigType, + FeatureProcessor, } from '../../../../../../../common/types/data_frame_analytics'; import { isClassificationAnalysis } from '../../../../../../../common/util/analytics_utils'; import { ANALYSIS_CONFIG_TYPE } from '../../../../../../../common/constants/data_frame_analytics'; @@ -61,12 +62,13 @@ export interface State { destinationIndexNameEmpty: boolean; destinationIndexNameValid: boolean; destinationIndexPatternTitleExists: boolean; - earlyStoppingEnabled: undefined | boolean; downsampleFactor: undefined | number; + earlyStoppingEnabled: undefined | boolean; eta: undefined | number; etaGrowthRatePerTree: undefined | number; featureBagFraction: undefined | number; featureInfluenceThreshold: undefined | number; + featureProcessors: undefined | FeatureProcessor[]; gamma: undefined | number; includes: string[]; jobId: DataFrameAnalyticsId; @@ -78,6 +80,8 @@ export interface State { jobConfigQuery: any; jobConfigQueryString: string | undefined; lambda: number | undefined; + lossFunction: string | undefined; + lossFunctionParameter: number | undefined; loadingFieldOptions: boolean; maxNumThreads: undefined | number; maxOptimizationRoundsPerHyperparameter: undefined | number; @@ -147,6 +151,7 @@ export const getInitialState = (): State => ({ etaGrowthRatePerTree: undefined, featureBagFraction: undefined, featureInfluenceThreshold: undefined, + featureProcessors: undefined, gamma: undefined, includes: [], jobId: '', @@ -158,6 +163,8 @@ export const getInitialState = (): State => ({ jobConfigQuery: defaultSearchQuery, jobConfigQueryString: undefined, lambda: undefined, + lossFunction: undefined, + lossFunctionParameter: undefined, loadingFieldOptions: false, maxNumThreads: DEFAULT_MAX_NUM_THREADS, maxOptimizationRoundsPerHyperparameter: undefined, @@ -268,8 +275,15 @@ export const getJobConfigFromFormState = ( formState.featureBagFraction && { feature_bag_fraction: formState.featureBagFraction, }, + formState.featureProcessors && { + feature_processors: formState.featureProcessors, + }, formState.gamma && { gamma: formState.gamma }, formState.lambda && { lambda: formState.lambda }, + formState.lossFunction && { loss_function: formState.lossFunction }, + formState.lossFunctionParameter && { + loss_function_parameter: formState.lossFunctionParameter, + }, formState.maxOptimizationRoundsPerHyperparameter && { max_optimization_rounds_per_hyperparameter: formState.maxOptimizationRoundsPerHyperparameter, From 7a1448aa08d84fea1dc317fb369093a0d8b12082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Wed, 11 Aug 2021 16:50:04 +0200 Subject: [PATCH 079/104] [Security Solution][Endpoint] Add new fleet PackagePolicy DELETE API extension point (#107507) * Refactor - moved extension types to its own type file * initial add of new extension point type * Removes unused imports * fix import order * try to fix ts errors * Add ts types for delete callback and register it. Also add packagePolicy to the delete response * Use logger instead of console log * Removes array check * Try fix some types and added TODO's * Fix ts errors and remove @ts-ignore 's * Remove useless ts-ignore * Remove useless todo * Remove enforced assertion by checking externalCallbackType Co-authored-by: Paul Tavares Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../fleet/register_fleet_policy_callbacks.ts | 18 +++- .../common/types/rest_spec/package_policy.ts | 2 + x-pack/plugins/fleet/server/index.ts | 9 +- x-pack/plugins/fleet/server/plugin.ts | 29 +------ .../routes/package_policy/handlers.test.ts | 83 +++++++++++-------- .../server/routes/package_policy/handlers.ts | 11 +++ .../fleet/server/services/app_context.ts | 30 ++++++- .../server/services/package_policy.test.ts | 16 ++-- .../fleet/server/services/package_policy.ts | 53 ++++++++---- .../plugins/fleet/server/types/extensions.ts | 46 ++++++++++ x-pack/plugins/fleet/server/types/index.tsx | 1 + .../fleet_integration/fleet_integration.ts | 9 +- 12 files changed, 212 insertions(+), 95 deletions(-) create mode 100644 x-pack/plugins/fleet/server/types/extensions.ts diff --git a/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts b/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts index b0065b70673a5..428378178afc8 100644 --- a/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts +++ b/x-pack/plugins/apm/server/lib/fleet/register_fleet_policy_callbacks.ts @@ -6,7 +6,15 @@ */ import { APMPlugin, APMRouteHandlerResources } from '../..'; -import { ExternalCallback } from '../../../../fleet/server'; +import { + ExternalCallback, + PostPackagePolicyDeleteCallback, + PutPackagePolicyUpdateCallback, +} from '../../../../fleet/server'; +import { + NewPackagePolicy, + UpdatePackagePolicy, +} from '../../../../fleet/common'; import { AgentConfiguration } from '../../../common/agent_configuration/configuration_types'; import { AGENT_NAME } from '../../../common/elasticsearch_fieldnames'; import { APMPluginStartDependencies } from '../../types'; @@ -53,7 +61,7 @@ export async function registerFleetPolicyCallbacks({ } type ExternalCallbackParams = Parameters; -export type PackagePolicy = ExternalCallbackParams[0]; +export type PackagePolicy = NewPackagePolicy | UpdatePackagePolicy; type Context = ExternalCallbackParams[1]; type Request = ExternalCallbackParams[2]; @@ -66,13 +74,15 @@ function registerPackagePolicyExternalCallback({ logger, }: { fleetPluginStart: NonNullable; - callbackName: ExternalCallback[0]; + callbackName: 'packagePolicyCreate' | 'packagePolicyUpdate'; plugins: APMRouteHandlerResources['plugins']; ruleDataClient: APMRouteHandlerResources['ruleDataClient']; config: NonNullable; logger: NonNullable; }) { - const callbackFn: ExternalCallback[1] = async ( + const callbackFn: + | PostPackagePolicyDeleteCallback + | PutPackagePolicyUpdateCallback = async ( packagePolicy: PackagePolicy, context: Context, request: Request diff --git a/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts b/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts index e9e4d40f25f6b..ed5f8e07098d4 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts @@ -10,6 +10,7 @@ import type { NewPackagePolicy, UpdatePackagePolicy, DryRunPackagePolicy, + PackagePolicyPackage, } from '../models'; export interface GetPackagePoliciesRequest { @@ -61,6 +62,7 @@ export type DeletePackagePoliciesResponse = Array<{ id: string; name?: string; success: boolean; + package?: PackagePolicyPackage; }>; export interface UpgradePackagePolicyBaseResponse { diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts index ab1cd9002d04a..8841c897fcb2a 100644 --- a/x-pack/plugins/fleet/server/index.ts +++ b/x-pack/plugins/fleet/server/index.ts @@ -23,7 +23,14 @@ export { ArtifactsClientInterface, Artifact, } from './services'; -export { FleetSetupContract, FleetSetupDeps, FleetStartContract, ExternalCallback } from './plugin'; + +export { FleetSetupContract, FleetSetupDeps, FleetStartContract } from './plugin'; +export type { + ExternalCallback, + PutPackagePolicyUpdateCallback, + PostPackagePolicyDeleteCallback, + PostPackagePolicyCreateCallback, +} from './types'; export { AgentNotFoundError } from './errors'; export const config: PluginConfigDescriptor = { diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 94e8032f3375b..9991f4ee20980 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -15,8 +15,6 @@ import type { PluginInitializerContext, SavedObjectsServiceStart, HttpServiceSetup, - RequestHandlerContext, - KibanaRequest, } from 'kibana/server'; import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; @@ -29,7 +27,7 @@ import type { } from '../../encrypted_saved_objects/server'; import type { SecurityPluginSetup, SecurityPluginStart } from '../../security/server'; import type { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; -import type { FleetConfigType, NewPackagePolicy, UpdatePackagePolicy } from '../common'; +import type { FleetConfigType } from '../common'; import { INTEGRATIONS_PLUGIN_ID } from '../common'; import type { CloudSetup } from '../../cloud/server'; @@ -57,6 +55,8 @@ import { registerAppRoutes, registerPreconfigurationRoutes, } from './routes'; + +import type { ExternalCallback } from './types'; import type { ESIndexPatternService, AgentService, @@ -127,29 +127,6 @@ const allSavedObjectTypes = [ PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, ]; -/** - * Callbacks supported by the Fleet plugin - */ -export type ExternalCallback = - | [ - 'packagePolicyCreate', - ( - newPackagePolicy: NewPackagePolicy, - context: RequestHandlerContext, - request: KibanaRequest - ) => Promise - ] - | [ - 'packagePolicyUpdate', - ( - newPackagePolicy: UpdatePackagePolicy, - context: RequestHandlerContext, - request: KibanaRequest - ) => Promise - ]; - -export type ExternalCallbacksStorage = Map>; - /** * Describes public Fleet plugin contract returned at the `startup` stage. */ diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts index c0b4eeecdfe82..fb28c7e2f5155 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts @@ -12,7 +12,11 @@ import type { IRouter, RequestHandler, RouteConfig } from 'kibana/server'; import { PACKAGE_POLICY_API_ROUTES } from '../../../common/constants'; import { appContextService, packagePolicyService } from '../../services'; import { createAppContextStartContractMock, xpackMocks } from '../../mocks'; -import type { PackagePolicyServiceInterface, ExternalCallback } from '../..'; +import type { + PackagePolicyServiceInterface, + PostPackagePolicyCreateCallback, + PutPackagePolicyUpdateCallback, +} from '../..'; import type { CreatePackagePolicyRequestSchema } from '../../types/rest_spec'; import { registerRoutes } from './index'; @@ -58,8 +62,11 @@ jest.mock('../../services/package_policy', (): { list: jest.fn(), listIds: jest.fn(), update: jest.fn(), - runExternalCallbacks: jest.fn((callbackType, newPackagePolicy, context, request) => - Promise.resolve(newPackagePolicy) + // @ts-ignore + runExternalCallbacks: jest.fn((callbackType, packagePolicy, context, request) => + callbackType === 'postPackagePolicyDelete' + ? Promise.resolve(undefined) + : Promise.resolve(packagePolicy) ), upgrade: jest.fn(), getUpgradeDryRunDiff: jest.fn(), @@ -132,45 +139,49 @@ describe('When calling package policy', () => { const callbackCallingOrder: string[] = []; // Callback one adds an input that includes a `config` property - const callbackOne: ExternalCallback[1] = jest.fn(async (ds) => { - callbackCallingOrder.push('one'); - const newDs = { - ...ds, - inputs: [ - { - type: 'endpoint', - enabled: true, - streams: [], - config: { - one: { - value: 'inserted by callbackOne', + const callbackOne: PostPackagePolicyCreateCallback | PutPackagePolicyUpdateCallback = jest.fn( + async (ds) => { + callbackCallingOrder.push('one'); + const newDs = { + ...ds, + inputs: [ + { + type: 'endpoint', + enabled: true, + streams: [], + config: { + one: { + value: 'inserted by callbackOne', + }, }, }, - }, - ], - }; - return newDs; - }); + ], + }; + return newDs; + } + ); // Callback two adds an additional `input[0].config` property - const callbackTwo: ExternalCallback[1] = jest.fn(async (ds) => { - callbackCallingOrder.push('two'); - const newDs = { - ...ds, - inputs: [ - { - ...ds.inputs[0], - config: { - ...ds.inputs[0].config, - two: { - value: 'inserted by callbackTwo', + const callbackTwo: PostPackagePolicyCreateCallback | PutPackagePolicyUpdateCallback = jest.fn( + async (ds) => { + callbackCallingOrder.push('two'); + const newDs = { + ...ds, + inputs: [ + { + ...ds.inputs[0], + config: { + ...ds.inputs[0].config, + two: { + value: 'inserted by callbackTwo', + }, }, }, - }, - ], - }; - return newDs; - }); + ], + }; + return newDs; + } + ); beforeEach(() => { appContextService.addExternalCallback('packagePolicyCreate', callbackOne); diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts index 78785a7b2f01f..88e200e90467a 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts @@ -171,6 +171,17 @@ export const deletePackagePolicyHandler: RequestHandler< request.body.packagePolicyIds, { user, force: request.body.force } ); + try { + await packagePolicyService.runExternalCallbacks( + 'postPackagePolicyDelete', + body, + context, + request + ); + } catch (error) { + const logger = appContextService.getLogger(); + logger.error(`An error occurred executing external callback: ${error}`); + } return response.ok({ body, }); diff --git a/x-pack/plugins/fleet/server/services/app_context.ts b/x-pack/plugins/fleet/server/services/app_context.ts index 82ec0aad52651..1fb34a9a399eb 100644 --- a/x-pack/plugins/fleet/server/services/app_context.ts +++ b/x-pack/plugins/fleet/server/services/app_context.ts @@ -21,9 +21,17 @@ import type { EncryptedSavedObjectsClient, EncryptedSavedObjectsPluginSetup, } from '../../../encrypted_saved_objects/server'; + import type { SecurityPluginStart } from '../../../security/server'; import type { FleetConfigType } from '../../common'; -import type { ExternalCallback, ExternalCallbacksStorage, FleetAppContext } from '../plugin'; +import type { + ExternalCallback, + ExternalCallbacksStorage, + PostPackagePolicyCreateCallback, + PostPackagePolicyDeleteCallback, + PutPackagePolicyUpdateCallback, +} from '../types'; +import type { FleetAppContext } from '../plugin'; import type { CloudSetup } from '../../../cloud/server'; class AppContextService { @@ -165,9 +173,25 @@ class AppContextService { this.externalCallbacks.get(type)!.add(callback); } - public getExternalCallbacks(type: ExternalCallback[0]) { + public getExternalCallbacks( + type: T + ): + | Set< + T extends 'packagePolicyCreate' + ? PostPackagePolicyCreateCallback + : T extends 'postPackagePolicyDelete' + ? PostPackagePolicyDeleteCallback + : PutPackagePolicyUpdateCallback + > + | undefined { if (this.externalCallbacks) { - return this.externalCallbacks.get(type); + return this.externalCallbacks.get(type) as Set< + T extends 'packagePolicyCreate' + ? PostPackagePolicyCreateCallback + : T extends 'postPackagePolicyDelete' + ? PostPackagePolicyDeleteCallback + : PutPackagePolicyUpdateCallback + >; } } } diff --git a/x-pack/plugins/fleet/server/services/package_policy.test.ts b/x-pack/plugins/fleet/server/services/package_policy.test.ts index b3626a83c41d1..66128c7e6c3e2 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -16,7 +16,7 @@ import type { KibanaRequest } from 'kibana/server'; import type { PackageInfo, PackagePolicySOAttributes, AgentPolicySOAttributes } from '../types'; import { createPackagePolicyMock } from '../../common/mocks'; -import type { ExternalCallback } from '..'; +import type { PutPackagePolicyUpdateCallback, PostPackagePolicyCreateCallback } from '..'; import { createAppContextStartContractMock, xpackMocks } from '../mocks'; @@ -105,6 +105,8 @@ jest.mock('./agent_policy', () => { }; }); +type CombinedExternalCallback = PutPackagePolicyUpdateCallback | PostPackagePolicyCreateCallback; + describe('Package policy service', () => { describe('compilePackagePolicyInputs', () => { it('should work with config variables from the stream', async () => { @@ -835,7 +837,7 @@ describe('Package policy service', () => { const callbackCallingOrder: string[] = []; // Callback one adds an input that includes a `config` property - const callbackOne: ExternalCallback[1] = jest.fn(async (ds) => { + const callbackOne: CombinedExternalCallback = jest.fn(async (ds) => { callbackCallingOrder.push('one'); return { ...ds, @@ -855,7 +857,7 @@ describe('Package policy service', () => { }); // Callback two adds an additional `input[0].config` property - const callbackTwo: ExternalCallback[1] = jest.fn(async (ds) => { + const callbackTwo: CombinedExternalCallback = jest.fn(async (ds) => { callbackCallingOrder.push('two'); return { ...ds, @@ -886,12 +888,12 @@ describe('Package policy service', () => { }); it('should call external callbacks in expected order', async () => { - const callbackA: ExternalCallback[1] = jest.fn(async (ds) => { + const callbackA: CombinedExternalCallback = jest.fn(async (ds) => { callbackCallingOrder.push('a'); return ds; }); - const callbackB: ExternalCallback[1] = jest.fn(async (ds) => { + const callbackB: CombinedExternalCallback = jest.fn(async (ds) => { callbackCallingOrder.push('b'); return ds; }); @@ -927,12 +929,12 @@ describe('Package policy service', () => { }); describe('with a callback that throws an exception', () => { - const callbackThree: ExternalCallback[1] = jest.fn(async () => { + const callbackThree: CombinedExternalCallback = jest.fn(async () => { callbackCallingOrder.push('three'); throw new Error('callbackThree threw error on purpose'); }); - const callbackFour: ExternalCallback[1] = jest.fn(async (ds) => { + const callbackFour: CombinedExternalCallback = jest.fn(async (ds) => { callbackCallingOrder.push('four'); return { ...ds, diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 20734658b8a79..573e1847f8eb3 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -424,6 +424,11 @@ class PackagePolicyService { id, name: packagePolicy.name, success: true, + package: { + name: packagePolicy.name, + title: '', + version: packagePolicy.version || '', + }, }); } catch (error) { result.push({ @@ -625,29 +630,47 @@ class PackagePolicyService { return Promise.all(inputsPromises); } + public async runExternalCallbacks
    ( + externalCallbackType: A, + packagePolicy: NewPackagePolicy | DeletePackagePoliciesResponse, + context: RequestHandlerContext, + request: KibanaRequest + ): Promise; public async runExternalCallbacks( externalCallbackType: ExternalCallback[0], - newPackagePolicy: NewPackagePolicy, + packagePolicy: NewPackagePolicy | DeletePackagePoliciesResponse, context: RequestHandlerContext, request: KibanaRequest - ): Promise { - let newData = newPackagePolicy; - - const externalCallbacks = appContextService.getExternalCallbacks(externalCallbackType); - if (externalCallbacks && externalCallbacks.size > 0) { - let updatedNewData: NewPackagePolicy = newData; - for (const callback of externalCallbacks) { - const result = await callback(updatedNewData, context, request); - if (externalCallbackType === 'packagePolicyCreate') { - updatedNewData = NewPackagePolicySchema.validate(result); - } else if (externalCallbackType === 'packagePolicyUpdate') { - updatedNewData = UpdatePackagePolicySchema.validate(result); + ): Promise { + if (externalCallbackType === 'postPackagePolicyDelete') { + const externalCallbacks = appContextService.getExternalCallbacks(externalCallbackType); + if (externalCallbacks && externalCallbacks.size > 0) { + for (const callback of externalCallbacks) { + if (Array.isArray(packagePolicy)) { + await callback(packagePolicy, context, request); + } } } + } else { + if (!Array.isArray(packagePolicy)) { + let newData = packagePolicy; + const externalCallbacks = appContextService.getExternalCallbacks(externalCallbackType); + if (externalCallbacks && externalCallbacks.size > 0) { + let updatedNewData = newData; + for (const callback of externalCallbacks) { + const result = await callback(updatedNewData, context, request); + if (externalCallbackType === 'packagePolicyCreate') { + updatedNewData = NewPackagePolicySchema.validate(result); + } else if (externalCallbackType === 'packagePolicyUpdate') { + updatedNewData = UpdatePackagePolicySchema.validate(result); + } + } - newData = updatedNewData; + newData = updatedNewData; + } + return newData; + } } - return newData; } } diff --git a/x-pack/plugins/fleet/server/types/extensions.ts b/x-pack/plugins/fleet/server/types/extensions.ts new file mode 100644 index 0000000000000..bca9cc016f828 --- /dev/null +++ b/x-pack/plugins/fleet/server/types/extensions.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { KibanaRequest, RequestHandlerContext } from 'kibana/server'; + +import type { + DeletePackagePoliciesResponse, + NewPackagePolicy, + UpdatePackagePolicy, +} from '../../common'; + +export type PostPackagePolicyDeleteCallback = ( + deletedPackagePolicies: DeletePackagePoliciesResponse, + context: RequestHandlerContext, + request: KibanaRequest +) => Promise; + +export type PostPackagePolicyCreateCallback = ( + newPackagePolicy: NewPackagePolicy, + context: RequestHandlerContext, + request: KibanaRequest +) => Promise; + +export type PutPackagePolicyUpdateCallback = ( + updatePackagePolicy: UpdatePackagePolicy, + context: RequestHandlerContext, + request: KibanaRequest +) => Promise; + +export type ExternalCallbackCreate = ['packagePolicyCreate', PostPackagePolicyCreateCallback]; +export type ExternalCallbackDelete = ['postPackagePolicyDelete', PostPackagePolicyDeleteCallback]; +export type ExternalCallbackUpdate = ['packagePolicyUpdate', PutPackagePolicyUpdateCallback]; + +/** + * Callbacks supported by the Fleet plugin + */ +export type ExternalCallback = + | ExternalCallbackCreate + | ExternalCallbackDelete + | ExternalCallbackUpdate; + +export type ExternalCallbacksStorage = Map>; diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index e32b462d11ca6..f686b969fd038 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -94,3 +94,4 @@ export interface BulkActionResult { export * from './models'; export * from './rest_spec'; +export * from './extensions'; diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts index 3e12fcac52a94..62c7b3719d6a6 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts @@ -9,7 +9,10 @@ import { KibanaRequest, Logger, RequestHandlerContext } from 'kibana/server'; import { ExceptionListClient } from '../../../lists/server'; import { PluginStartContract as AlertsStartContract } from '../../../alerting/server'; import { SecurityPluginStart } from '../../../security/server'; -import { ExternalCallback } from '../../../fleet/server'; +import { + PostPackagePolicyCreateCallback, + PutPackagePolicyUpdateCallback, +} from '../../../fleet/server'; import { NewPackagePolicy, UpdatePackagePolicy } from '../../../fleet/common'; import { NewPolicyData, PolicyConfig } from '../../common/endpoint/types'; import { ManifestManager } from '../endpoint/services'; @@ -40,7 +43,7 @@ export const getPackagePolicyCreateCallback = ( alerts: AlertsStartContract, licenseService: LicenseService, exceptionsClient: ExceptionListClient | undefined -): ExternalCallback[1] => { +): PostPackagePolicyCreateCallback => { return async ( newPackagePolicy: NewPackagePolicy, context: RequestHandlerContext, @@ -101,7 +104,7 @@ export const getPackagePolicyCreateCallback = ( export const getPackagePolicyUpdateCallback = ( logger: Logger, licenseService: LicenseService -): ExternalCallback[1] => { +): PutPackagePolicyUpdateCallback => { return async ( newPackagePolicy: NewPackagePolicy // context: RequestHandlerContext, From dfaf54082a2baf898497b8744eeb8f7ebfa5a704 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 11 Aug 2021 15:59:21 +0100 Subject: [PATCH 080/104] chore(NA): moving @kbn/rule-data-utils to babel transpiler (#107573) * chore(NA): moving @kbn/rule-data-utils to babel transpiler * chore(NA): update imports * chore(NA): targetted imports for apm * chore(NA): fix imports Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-rule-data-utils/.babelrc | 3 ++ packages/kbn-rule-data-utils/BUILD.bazel | 25 ++++++++++---- packages/kbn-rule-data-utils/package.json | 4 +-- packages/kbn-rule-data-utils/tsconfig.json | 10 +++--- .../alerting/register_apm_alerts.ts | 18 +++++++--- .../Distribution/index.tsx | 6 +++- .../charts/helper/get_alert_annotations.tsx | 30 ++++++++++++---- .../shared/charts/latency_chart/index.tsx | 6 +++- .../alerts/register_error_count_alert_type.ts | 14 ++++++-- ...egister_transaction_duration_alert_type.ts | 14 ++++++-- ...transaction_duration_anomaly_alert_type.ts | 22 +++++++++--- ...ister_transaction_error_rate_alert_type.ts | 14 ++++++-- .../server/lib/services/get_service_alerts.ts | 6 +++- .../pages/alerts/alerts_flyout/index.tsx | 34 ++++++++++++++----- .../public/pages/alerts/alerts_table.tsx | 6 +--- .../pages/alerts/alerts_table_t_grid.tsx | 32 ++++++++++++----- .../alerts/alerts_table_t_grid_actions.tsx | 14 ++++++-- .../public/pages/alerts/decorate_response.ts | 22 +++++++++--- .../public/pages/alerts/render_cell_value.tsx | 26 ++++++++++---- .../server/lib/rules/get_top_alerts.ts | 2 +- .../server/utils/queries.test.ts | 2 +- .../observability/server/utils/queries.ts | 2 +- .../common/technical_rule_data_field_names.ts | 2 +- .../server/alert_data_client/alerts_client.ts | 24 +++++++++---- .../server/routes/get_alert_index.ts | 2 +- .../server/rule_data_client/types.ts | 2 +- .../server/rule_data_plugin_service/index.ts | 2 +- .../create_persistence_rule_type_factory.ts | 2 +- .../common/search_strategy/timeline/index.ts | 2 +- .../components/t_grid/integrated/index.tsx | 6 +++- .../components/t_grid/standalone/index.tsx | 2 +- .../timelines/public/container/index.tsx | 2 +- .../server/search_strategy/timeline/index.ts | 17 +++++++--- .../lib/alerts/duration_anomaly.test.ts | 2 +- .../server/lib/alerts/duration_anomaly.ts | 2 +- .../applications/timelines_test/index.tsx | 6 +++- 36 files changed, 284 insertions(+), 101 deletions(-) create mode 100644 packages/kbn-rule-data-utils/.babelrc diff --git a/packages/kbn-rule-data-utils/.babelrc b/packages/kbn-rule-data-utils/.babelrc new file mode 100644 index 0000000000000..7da72d1779128 --- /dev/null +++ b/packages/kbn-rule-data-utils/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@kbn/babel-preset/node_preset"] +} diff --git a/packages/kbn-rule-data-utils/BUILD.bazel b/packages/kbn-rule-data-utils/BUILD.bazel index 1268daf54cd47..bbc2d524102db 100644 --- a/packages/kbn-rule-data-utils/BUILD.bazel +++ b/packages/kbn-rule-data-utils/BUILD.bazel @@ -1,5 +1,6 @@ load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") +load("//src/dev/bazel:index.bzl", "jsts_transpiler") PKG_BASE_NAME = "kbn-rule-data-utils" PKG_REQUIRE_NAME = "@kbn/rule-data-utils" @@ -21,19 +22,27 @@ NPM_MODULE_EXTRA_FILES = [ "package.json", ] -SRC_DEPS = [ +RUNTIME_DEPS = [ "//packages/kbn-es-query", + "@npm//@elastic/elasticsearch", "@npm//tslib", "@npm//utility-types", - "@npm//@elastic/elasticsearch", ] TYPES_DEPS = [ + "//packages/kbn-es-query", + "@npm//@elastic/elasticsearch", + "@npm//tslib", + "@npm//utility-types", "@npm//@types/jest", "@npm//@types/node", ] -DEPS = SRC_DEPS + TYPES_DEPS +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) ts_config( name = "tsconfig", @@ -45,13 +54,15 @@ ts_config( ) ts_project( - name = "tsc", + name = "tsc_types", args = ['--pretty'], srcs = SRCS, - deps = DEPS, + deps = TYPES_DEPS, declaration = True, declaration_map = True, - out_dir = "target", + emit_declaration_only = True, + incremental = False, + out_dir = "target_types", source_map = True, root_dir = "src", tsconfig = ":tsconfig", @@ -60,7 +71,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = DEPS + [":tsc"], + deps = RUNTIME_DEPS + [":target_node", ":tsc_types"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-rule-data-utils/package.json b/packages/kbn-rule-data-utils/package.json index 42223e51ec2d6..64bbb69d14403 100644 --- a/packages/kbn-rule-data-utils/package.json +++ b/packages/kbn-rule-data-utils/package.json @@ -1,7 +1,7 @@ { "name": "@kbn/rule-data-utils", - "main": "./target/index.js", - "types": "./target/index.d.ts", + "main": "./target_node/index.js", + "types": "./target_types/index.d.ts", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", "private": true diff --git a/packages/kbn-rule-data-utils/tsconfig.json b/packages/kbn-rule-data-utils/tsconfig.json index 1c568530306c7..be6095d187ef3 100644 --- a/packages/kbn-rule-data-utils/tsconfig.json +++ b/packages/kbn-rule-data-utils/tsconfig.json @@ -1,19 +1,21 @@ { "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "outDir": "./target/types", - "stripInternal": false, "declaration": true, "declarationMap": true, - "rootDir": "./src", + "emitDeclarationOnly": true, + "incremental": false, + "outDir": "./target_types", + "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-rule-data-utils/src", + "stripInternal": false, "types": [ "jest", "node" ] }, "include": [ - "./src/**/*.ts" + "src/**/*.ts" ] } diff --git a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts index bcac3ad4e126e..12102a294bf9f 100644 --- a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts +++ b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts @@ -8,11 +8,17 @@ import { i18n } from '@kbn/i18n'; import { lazy } from 'react'; import { stringify } from 'querystring'; +import type { + ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, + ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, + ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED, +} from '@kbn/rule-data-utils'; import { - ALERT_EVALUATION_THRESHOLD, - ALERT_EVALUATION_VALUE, - ALERT_SEVERITY_LEVEL, -} from '@kbn/rule-data-utils/target/technical_field_names'; + ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, + ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, + ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_NON_TYPED, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/technical_field_names'; import type { ObservabilityRuleTypeRegistry } from '../../../../observability/public'; import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; import { AlertType } from '../../../common/alert_types'; @@ -22,6 +28,10 @@ const SERVICE_ENVIRONMENT = 'service.environment'; const SERVICE_NAME = 'service.name'; const TRANSACTION_TYPE = 'transaction.type'; +const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; +const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = ALERT_EVALUATION_VALUE_NON_TYPED; +const ALERT_SEVERITY_LEVEL: typeof ALERT_SEVERITY_LEVEL_TYPED = ALERT_SEVERITY_LEVEL_NON_TYPED; + const format = ({ pathname, query, diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx index f969b5802e8b6..fa458b95d0d80 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/Distribution/index.tsx @@ -19,7 +19,9 @@ import { import { EuiTitle } from '@elastic/eui'; import d3 from 'd3'; import React, { Suspense, useState } from 'react'; -import { ALERT_RULE_TYPE_ID } from '@kbn/rule-data-utils/target/technical_field_names'; +import type { ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_TYPED } from '@kbn/rule-data-utils'; +// @ts-expect-error +import { ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_NON_TYPED } from '@kbn/rule-data-utils/target_node/technical_field_names'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { asRelativeDateTimeRange } from '../../../../../common/utils/formatters'; @@ -29,6 +31,8 @@ import { getAlertAnnotations } from '../../../shared/charts/helper/get_alert_ann import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { LazyAlertsFlyout } from '../../../../../../observability/public'; +const ALERT_RULE_TYPE_ID: typeof ALERT_RULE_TYPE_ID_TYPED = ALERT_RULE_TYPE_ID_NON_TYPED; + type ErrorDistributionAPIResponse = APIReturnType<'GET /api/apm/services/{serviceName}/errors/distribution'>; interface FormattedBucket { diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx index 31a8cbf44ea27..f51494b8fa1d8 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx @@ -12,14 +12,23 @@ import { } from '@elastic/charts'; import { EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import type { + ALERT_DURATION as ALERT_DURATION_TYPED, + ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED, + ALERT_START as ALERT_START_TYPED, + ALERT_UUID as ALERT_UUID_TYPED, + ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_TYPED, + ALERT_RULE_NAME as ALERT_RULE_NAME_TYPED, +} from '@kbn/rule-data-utils'; import { - ALERT_DURATION, - ALERT_SEVERITY_LEVEL, - ALERT_START, - ALERT_UUID, - ALERT_RULE_TYPE_ID, - ALERT_RULE_NAME, -} from '@kbn/rule-data-utils/target/technical_field_names'; + ALERT_DURATION as ALERT_DURATION_NON_TYPED, + ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_NON_TYPED, + ALERT_START as ALERT_START_NON_TYPED, + ALERT_UUID as ALERT_UUID_NON_TYPED, + ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_NON_TYPED, + ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/technical_field_names'; import React, { Dispatch, SetStateAction } from 'react'; import { EuiTheme } from 'src/plugins/kibana_react/common'; import { ValuesType } from 'utility-types'; @@ -28,6 +37,13 @@ import { parseTechnicalFields } from '../../../../../../rule_registry/common'; import { asDuration, asPercent } from '../../../../../common/utils/formatters'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; +const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; +const ALERT_SEVERITY_LEVEL: typeof ALERT_SEVERITY_LEVEL_TYPED = ALERT_SEVERITY_LEVEL_NON_TYPED; +const ALERT_START: typeof ALERT_START_TYPED = ALERT_START_NON_TYPED; +const ALERT_UUID: typeof ALERT_UUID_TYPED = ALERT_UUID_NON_TYPED; +const ALERT_RULE_TYPE_ID: typeof ALERT_RULE_TYPE_ID_TYPED = ALERT_RULE_TYPE_ID_NON_TYPED; +const ALERT_RULE_NAME: typeof ALERT_RULE_NAME_TYPED = ALERT_RULE_NAME_NON_TYPED; + type Alert = ValuesType< APIReturnType<'GET /api/apm/services/{serviceName}/alerts'>['alerts'] >; diff --git a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx index d2df99ba29197..ff9e46fc0552a 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx @@ -9,7 +9,9 @@ import { EuiFlexGroup, EuiFlexItem, EuiSelect, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { useHistory } from 'react-router-dom'; -import { ALERT_RULE_TYPE_ID } from '@kbn/rule-data-utils/target/technical_field_names'; +import type { ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_TYPED } from '@kbn/rule-data-utils'; +// @ts-expect-error +import { ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_NON_TYPED } from '@kbn/rule-data-utils/target_node/technical_field_names'; import { AlertType } from '../../../../../common/alert_types'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types'; @@ -27,6 +29,8 @@ import { MLHeader } from '../../../shared/charts/transaction_charts/ml_header'; import * as urlHelpers from '../../../shared/Links/url_helpers'; import { getComparisonChartTheme } from '../../time_comparison/get_time_range_comparison'; +const ALERT_RULE_TYPE_ID: typeof ALERT_RULE_TYPE_ID_TYPED = ALERT_RULE_TYPE_ID_NON_TYPED; + interface Props { height?: number; } diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts index bdd6b240c4bbc..6a6a67e9fd97f 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts @@ -7,10 +7,15 @@ import { schema } from '@kbn/config-schema'; import { take } from 'rxjs/operators'; +import type { + ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, + ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, +} from '@kbn/rule-data-utils'; import { - ALERT_EVALUATION_THRESHOLD, - ALERT_EVALUATION_VALUE, -} from '@kbn/rule-data-utils/target/technical_field_names'; + ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, + ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/technical_field_names'; import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; import { ENVIRONMENT_NOT_DEFINED, @@ -34,6 +39,9 @@ import { apmActionVariables } from './action_variables'; import { alertingEsClient } from './alerting_es_client'; import { RegisterRuleDependencies } from './register_apm_alerts'; +const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; +const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = ALERT_EVALUATION_VALUE_NON_TYPED; + const paramsSchema = schema.object({ windowSize: schema.number(), windowUnit: schema.string(), diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts index 88394cb2c5988..790e62eae66d4 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts @@ -8,10 +8,15 @@ import { schema } from '@kbn/config-schema'; import { take } from 'rxjs/operators'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; +import type { + ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, + ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, +} from '@kbn/rule-data-utils'; import { - ALERT_EVALUATION_THRESHOLD, - ALERT_EVALUATION_VALUE, -} from '@kbn/rule-data-utils/target/technical_field_names'; + ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, + ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/technical_field_names'; import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; import { getEnvironmentLabel, @@ -36,6 +41,9 @@ import { apmActionVariables } from './action_variables'; import { alertingEsClient } from './alerting_es_client'; import { RegisterRuleDependencies } from './register_apm_alerts'; +const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; +const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = ALERT_EVALUATION_VALUE_NON_TYPED; + const paramsSchema = schema.object({ serviceName: schema.string(), transactionType: schema.string(), diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts index de0657d075d7f..2041f06a5a6a8 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -9,12 +9,19 @@ import { schema } from '@kbn/config-schema'; import { compact } from 'lodash'; import { ESSearchResponse } from 'src/core/types/elasticsearch'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; +import type { + ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, + ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, + ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED, + ALERT_SEVERITY_VALUE as ALERT_SEVERITY_VALUE_TYPED, +} from '@kbn/rule-data-utils'; import { - ALERT_EVALUATION_THRESHOLD, - ALERT_EVALUATION_VALUE, - ALERT_SEVERITY_LEVEL, - ALERT_SEVERITY_VALUE, -} from '@kbn/rule-data-utils/target/technical_field_names'; + ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, + ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, + ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_NON_TYPED, + ALERT_SEVERITY_VALUE as ALERT_SEVERITY_VALUE_NON_TYPED, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/technical_field_names'; import { createLifecycleRuleTypeFactory } from '../../../../rule_registry/server'; import { ProcessorEvent } from '../../../common/processor_event'; import { getSeverity } from '../../../common/anomaly_detection'; @@ -39,6 +46,11 @@ import { getEnvironmentLabel, } from '../../../common/environment_filter_values'; +const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; +const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = ALERT_EVALUATION_VALUE_NON_TYPED; +const ALERT_SEVERITY_LEVEL: typeof ALERT_SEVERITY_LEVEL_TYPED = ALERT_SEVERITY_LEVEL_NON_TYPED; +const ALERT_SEVERITY_VALUE: typeof ALERT_SEVERITY_VALUE_TYPED = ALERT_SEVERITY_VALUE_NON_TYPED; + const paramsSchema = schema.object({ serviceName: schema.maybe(schema.string()), transactionType: schema.maybe(schema.string()), diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts index a397302659538..fc63e4827e5de 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts @@ -7,10 +7,15 @@ import { schema } from '@kbn/config-schema'; import { take } from 'rxjs/operators'; +import type { + ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, + ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, +} from '@kbn/rule-data-utils'; import { - ALERT_EVALUATION_THRESHOLD, - ALERT_EVALUATION_VALUE, -} from '@kbn/rule-data-utils/target/technical_field_names'; + ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, + ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/technical_field_names'; import { ENVIRONMENT_NOT_DEFINED, getEnvironmentEsField, @@ -38,6 +43,9 @@ import { apmActionVariables } from './action_variables'; import { alertingEsClient } from './alerting_es_client'; import { RegisterRuleDependencies } from './register_apm_alerts'; +const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; +const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = ALERT_EVALUATION_VALUE_NON_TYPED; + const paramsSchema = schema.object({ windowSize: schema.number(), windowUnit: schema.string(), diff --git a/x-pack/plugins/apm/server/lib/services/get_service_alerts.ts b/x-pack/plugins/apm/server/lib/services/get_service_alerts.ts index a9fe55456ad4e..4cbc62d87eff6 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_alerts.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_alerts.ts @@ -5,7 +5,9 @@ * 2.0. */ -import { EVENT_KIND } from '@kbn/rule-data-utils/target/technical_field_names'; +import type { EVENT_KIND as EVENT_KIND_TYPED } from '@kbn/rule-data-utils'; +// @ts-expect-error +import { EVENT_KIND as EVENT_KIND_NON_TYPED } from '@kbn/rule-data-utils/target_node/technical_field_names'; import { RuleDataClient } from '../../../../rule_registry/server'; import { SERVICE_NAME, @@ -14,6 +16,8 @@ import { import { rangeQuery } from '../../../../observability/server'; import { environmentQuery } from '../../../common/utils/environment_query'; +const EVENT_KIND: typeof EVENT_KIND_TYPED = EVENT_KIND_NON_TYPED; + export async function getServiceAlerts({ ruleDataClient, start, diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx index 7c23aa8582ece..3d63f7bdaeaf7 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx @@ -20,15 +20,25 @@ import { EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import type { + ALERT_DURATION as ALERT_DURATION_TYPED, + ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, + ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, + ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED, + ALERT_UUID as ALERT_UUID_TYPED, + ALERT_RULE_CATEGORY as ALERT_RULE_CATEGORY_TYPED, + ALERT_RULE_NAME as ALERT_RULE_NAME_TYPED, +} from '@kbn/rule-data-utils'; import { - ALERT_DURATION, - ALERT_EVALUATION_THRESHOLD, - ALERT_EVALUATION_VALUE, - ALERT_SEVERITY_LEVEL, - ALERT_UUID, - ALERT_RULE_CATEGORY, - ALERT_RULE_NAME, -} from '@kbn/rule-data-utils/target/technical_field_names'; + ALERT_DURATION as ALERT_DURATION_NON_TYPED, + ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, + ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, + ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_NON_TYPED, + ALERT_UUID as ALERT_UUID_NON_TYPED, + ALERT_RULE_CATEGORY as ALERT_RULE_CATEGORY_NON_TYPED, + ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/technical_field_names'; import moment from 'moment-timezone'; import React, { useMemo } from 'react'; import type { TopAlert, TopAlertResponse } from '../'; @@ -46,6 +56,14 @@ type AlertsFlyoutProps = { selectedAlertId?: string; } & EuiFlyoutProps; +const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; +const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; +const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = ALERT_EVALUATION_VALUE_NON_TYPED; +const ALERT_SEVERITY_LEVEL: typeof ALERT_SEVERITY_LEVEL_TYPED = ALERT_SEVERITY_LEVEL_NON_TYPED; +const ALERT_UUID: typeof ALERT_UUID_TYPED = ALERT_UUID_NON_TYPED; +const ALERT_RULE_CATEGORY: typeof ALERT_RULE_CATEGORY_TYPED = ALERT_RULE_CATEGORY_NON_TYPED; +const ALERT_RULE_NAME: typeof ALERT_RULE_NAME_TYPED = ALERT_RULE_NAME_NON_TYPED; + export function AlertsFlyout({ alert, alerts, diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table.tsx index 28d211766cfe5..395c2a5253ec6 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table.tsx @@ -14,11 +14,7 @@ import { EuiLink, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { - ALERT_DURATION, - ALERT_SEVERITY_LEVEL, - ALERT_UUID, -} from '@kbn/rule-data-utils/target/technical_field_names'; +import { ALERT_DURATION, ALERT_SEVERITY_LEVEL, ALERT_UUID } from '@kbn/rule-data-utils'; import React, { Suspense, useMemo, useState } from 'react'; import { LazyAlertsFlyout } from '../..'; import { asDuration } from '../../../common/utils/formatters'; diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx index 798be42fce5cd..f82446db2ebec 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx @@ -5,19 +5,28 @@ * 2.0. */ -import { AlertConsumers } from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; +import type { + AlertConsumers as AlertConsumersTyped, + ALERT_DURATION as ALERT_DURATION_TYPED, + ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED, + ALERT_STATUS as ALERT_STATUS_TYPED, + ALERT_START as ALERT_START_TYPED, + ALERT_RULE_NAME as ALERT_RULE_NAME_TYPED, +} from '@kbn/rule-data-utils'; +import { + AlertConsumers as AlertConsumersNonTyped, + ALERT_DURATION as ALERT_DURATION_NON_TYPED, + ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_NON_TYPED, + ALERT_STATUS as ALERT_STATUS_NON_TYPED, + ALERT_START as ALERT_START_NON_TYPED, + ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/alerts_as_data_rbac'; import { EuiButtonIcon, EuiDataGridColumn } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import React, { Suspense, useState } from 'react'; -import { - ALERT_DURATION, - ALERT_SEVERITY_LEVEL, - ALERT_STATUS, - ALERT_START, - ALERT_RULE_NAME, -} from '@kbn/rule-data-utils/target/technical_field_names'; import type { TimelinesUIStart } from '../../../../timelines/public'; import type { TopAlert } from './'; @@ -35,6 +44,13 @@ import { decorateResponse } from './decorate_response'; import { getDefaultCellActions } from './default_cell_actions'; import { LazyAlertsFlyout } from '../..'; +const AlertConsumers: typeof AlertConsumersTyped = AlertConsumersNonTyped; +const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; +const ALERT_SEVERITY_LEVEL: typeof ALERT_SEVERITY_LEVEL_TYPED = ALERT_SEVERITY_LEVEL_NON_TYPED; +const ALERT_STATUS: typeof ALERT_STATUS_TYPED = ALERT_STATUS_NON_TYPED; +const ALERT_START: typeof ALERT_START_TYPED = ALERT_START_NON_TYPED; +const ALERT_RULE_NAME: typeof ALERT_RULE_NAME_TYPED = ALERT_RULE_NAME_NON_TYPED; + interface AlertsTableTGridProps { indexName: string; rangeFrom: string; diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid_actions.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid_actions.tsx index 2ba105113fec9..3c31b8cfda87c 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid_actions.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid_actions.tsx @@ -14,10 +14,15 @@ import { EuiPopoverTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import type { + ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_TYPED, + ALERT_RULE_NAME as ALERT_RULE_NAME_TYPED, +} from '@kbn/rule-data-utils'; import { - ALERT_RULE_TYPE_ID, - ALERT_RULE_NAME, -} from '@kbn/rule-data-utils/target/technical_field_names'; + ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_NON_TYPED, + ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/technical_field_names'; import React, { useState } from 'react'; import { format, parse } from 'url'; @@ -26,6 +31,9 @@ import type { ActionProps } from '../../../../timelines/common'; import { asDuration, asPercent } from '../../../common/utils/formatters'; import { usePluginContext } from '../../hooks/use_plugin_context'; +const ALERT_RULE_TYPE_ID: typeof ALERT_RULE_TYPE_ID_TYPED = ALERT_RULE_TYPE_ID_NON_TYPED; +const ALERT_RULE_NAME: typeof ALERT_RULE_NAME_TYPED = ALERT_RULE_NAME_NON_TYPED; + export function RowCellActionsRender({ data }: ActionProps) { const { core, observabilityRuleTypeRegistry } = usePluginContext(); const [isPopoverOpen, setIsPopoverOpen] = useState(false); diff --git a/x-pack/plugins/observability/public/pages/alerts/decorate_response.ts b/x-pack/plugins/observability/public/pages/alerts/decorate_response.ts index f09a735de97be..7eb6d785779b7 100644 --- a/x-pack/plugins/observability/public/pages/alerts/decorate_response.ts +++ b/x-pack/plugins/observability/public/pages/alerts/decorate_response.ts @@ -5,17 +5,29 @@ * 2.0. */ +import type { + ALERT_START as ALERT_START_TYPED, + ALERT_STATUS as ALERT_STATUS_TYPED, + ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_TYPED, + ALERT_RULE_NAME as ALERT_RULE_NAME_TYPED, +} from '@kbn/rule-data-utils'; import { - ALERT_RULE_TYPE_ID, - ALERT_RULE_NAME, - ALERT_STATUS, - ALERT_START, -} from '@kbn/rule-data-utils/target/technical_field_names'; + ALERT_START as ALERT_START_NON_TYPED, + ALERT_STATUS as ALERT_STATUS_NON_TYPED, + ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_NON_TYPED, + ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/technical_field_names'; import type { TopAlertResponse, TopAlert } from '.'; import { parseTechnicalFields } from '../../../../rule_registry/common/parse_technical_fields'; import { asDuration, asPercent } from '../../../common/utils/formatters'; import { ObservabilityRuleTypeRegistry } from '../../rules/create_observability_rule_type_registry'; +const ALERT_START: typeof ALERT_START_TYPED = ALERT_START_NON_TYPED; +const ALERT_STATUS: typeof ALERT_STATUS_TYPED = ALERT_STATUS_NON_TYPED; +const ALERT_RULE_TYPE_ID: typeof ALERT_RULE_TYPE_ID_TYPED = ALERT_RULE_TYPE_ID_NON_TYPED; +const ALERT_RULE_NAME: typeof ALERT_RULE_NAME_TYPED = ALERT_RULE_NAME_NON_TYPED; + export function decorateResponse( alerts: TopAlertResponse[], observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry diff --git a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx index 560926bf20e87..dde10b7a3f3e1 100644 --- a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx @@ -7,13 +7,21 @@ import { EuiIconTip, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect } from 'react'; +import type { + ALERT_DURATION as ALERT_DURATION_TYPED, + ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED, + ALERT_START as ALERT_START_TYPED, + ALERT_STATUS as ALERT_STATUS_TYPED, + ALERT_RULE_NAME as ALERT_RULE_NAME_TYPED, +} from '@kbn/rule-data-utils'; import { - ALERT_DURATION, - ALERT_SEVERITY_LEVEL, - ALERT_STATUS, - ALERT_START, - ALERT_RULE_NAME, -} from '@kbn/rule-data-utils/target/technical_field_names'; + ALERT_DURATION as ALERT_DURATION_NON_TYPED, + ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_NON_TYPED, + ALERT_START as ALERT_START_NON_TYPED, + ALERT_STATUS as ALERT_STATUS_NON_TYPED, + ALERT_RULE_NAME as ALERT_RULE_NAME_NON_TYPED, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/technical_field_names'; import type { CellValueElementProps, TimelineNonEcsData } from '../../../../timelines/common'; import { TimestampTooltip } from '../../components/shared/timestamp_tooltip'; @@ -23,6 +31,12 @@ import { TopAlert } from '.'; import { decorateResponse } from './decorate_response'; import { usePluginContext } from '../../hooks/use_plugin_context'; +const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; +const ALERT_SEVERITY_LEVEL: typeof ALERT_SEVERITY_LEVEL_TYPED = ALERT_SEVERITY_LEVEL_NON_TYPED; +const ALERT_START: typeof ALERT_START_TYPED = ALERT_START_NON_TYPED; +const ALERT_STATUS: typeof ALERT_STATUS_TYPED = ALERT_STATUS_NON_TYPED; +const ALERT_RULE_NAME: typeof ALERT_RULE_NAME_TYPED = ALERT_RULE_NAME_NON_TYPED; + export const getMappedNonEcsValue = ({ data, fieldName, diff --git a/x-pack/plugins/observability/server/lib/rules/get_top_alerts.ts b/x-pack/plugins/observability/server/lib/rules/get_top_alerts.ts index db8191136686a..74e6a5bc177fe 100644 --- a/x-pack/plugins/observability/server/lib/rules/get_top_alerts.ts +++ b/x-pack/plugins/observability/server/lib/rules/get_top_alerts.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EVENT_KIND, TIMESTAMP } from '@kbn/rule-data-utils/target/technical_field_names'; +import { EVENT_KIND, TIMESTAMP } from '@kbn/rule-data-utils'; import { RuleDataClient } from '../../../../rule_registry/server'; import type { AlertStatus } from '../../../common/typings'; import { kqlQuery, rangeQuery, alertStatusQuery } from '../../utils/queries'; diff --git a/x-pack/plugins/observability/server/utils/queries.test.ts b/x-pack/plugins/observability/server/utils/queries.test.ts index a0a63b73d7170..dab42fa604dc4 100644 --- a/x-pack/plugins/observability/server/utils/queries.test.ts +++ b/x-pack/plugins/observability/server/utils/queries.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ALERT_STATUS } from '@kbn/rule-data-utils/target/technical_field_names'; +import { ALERT_STATUS } from '@kbn/rule-data-utils'; import * as queries from './queries'; describe('queries', () => { diff --git a/x-pack/plugins/observability/server/utils/queries.ts b/x-pack/plugins/observability/server/utils/queries.ts index 2ee3291e7fb62..283779c312906 100644 --- a/x-pack/plugins/observability/server/utils/queries.ts +++ b/x-pack/plugins/observability/server/utils/queries.ts @@ -6,7 +6,7 @@ */ import type { estypes } from '@elastic/elasticsearch'; -import { ALERT_STATUS } from '@kbn/rule-data-utils/target/technical_field_names'; +import { ALERT_STATUS } from '@kbn/rule-data-utils'; import { esKuery } from '../../../../../src/plugins/data/server'; import { AlertStatus } from '../../common/typings'; diff --git a/x-pack/plugins/rule_registry/common/technical_rule_data_field_names.ts b/x-pack/plugins/rule_registry/common/technical_rule_data_field_names.ts index 5c954a31e79ac..47b00fb1348eb 100644 --- a/x-pack/plugins/rule_registry/common/technical_rule_data_field_names.ts +++ b/x-pack/plugins/rule_registry/common/technical_rule_data_field_names.ts @@ -5,4 +5,4 @@ * 2.0. */ -export * from '@kbn/rule-data-utils/target/technical_field_names'; +export * from '@kbn/rule-data-utils'; diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts index aaceb167b2e51..a67c03abe8b32 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts @@ -8,13 +8,20 @@ import Boom from '@hapi/boom'; import { PublicMethodsOf } from '@kbn/utility-types'; import { Filter, buildEsQuery, EsQueryConfig } from '@kbn/es-query'; import { decodeVersion, encodeHitVersion } from '@kbn/securitysolution-es-utils'; -import { - mapConsumerToIndexName, - isValidFeatureId, - getSafeSortIds, +import type { + getEsQueryConfig as getEsQueryConfigTyped, + getSafeSortIds as getSafeSortIdsTyped, + isValidFeatureId as isValidFeatureIdTyped, + mapConsumerToIndexName as mapConsumerToIndexNameTyped, STATUS_VALUES, - getEsQueryConfig, -} from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; +} from '@kbn/rule-data-utils'; +import { + getEsQueryConfig as getEsQueryConfigNonTyped, + getSafeSortIds as getSafeSortIdsNonTyped, + isValidFeatureId as isValidFeatureIdNonTyped, + mapConsumerToIndexName as mapConsumerToIndexNameNonTyped, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/alerts_as_data_rbac'; import { InlineScript, QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; import { AlertTypeParams, AlertingAuthorizationFilterType } from '../../../alerting/server'; @@ -35,6 +42,11 @@ import { } from '../../common/technical_rule_data_field_names'; import { ParsedTechnicalFields } from '../../common/parse_technical_fields'; +const getEsQueryConfig: typeof getEsQueryConfigTyped = getEsQueryConfigNonTyped; +const getSafeSortIds: typeof getSafeSortIdsTyped = getSafeSortIdsNonTyped; +const isValidFeatureId: typeof isValidFeatureIdTyped = isValidFeatureIdNonTyped; +const mapConsumerToIndexName: typeof mapConsumerToIndexNameTyped = mapConsumerToIndexNameNonTyped; + // TODO: Fix typings https://github.com/elastic/kibana/issues/101776 type NonNullableProps = Omit & { [K in Props]-?: NonNullable }; diff --git a/x-pack/plugins/rule_registry/server/routes/get_alert_index.ts b/x-pack/plugins/rule_registry/server/routes/get_alert_index.ts index 3e3bde7429fe0..f3b0b9181c60f 100644 --- a/x-pack/plugins/rule_registry/server/routes/get_alert_index.ts +++ b/x-pack/plugins/rule_registry/server/routes/get_alert_index.ts @@ -8,7 +8,7 @@ import { IRouter } from 'kibana/server'; import { id as _id } from '@kbn/securitysolution-io-ts-list-types'; import { transformError } from '@kbn/securitysolution-es-utils'; -import { validFeatureIds } from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; +import { validFeatureIds } from '@kbn/rule-data-utils'; import { RacRequestHandlerContext } from '../types'; import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants'; diff --git a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts index 277121074f7f2..ef09dcc7550a0 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_client/types.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_client/types.ts @@ -7,7 +7,7 @@ import { ApiResponse } from '@elastic/elasticsearch'; import { BulkRequest, BulkResponse } from '@elastic/elasticsearch/api/types'; -import { ValidFeatureId } from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; +import { ValidFeatureId } from '@kbn/rule-data-utils'; import { ElasticsearchClient } from 'kibana/server'; import { FieldDescriptor } from 'src/plugins/data/server'; diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts index f81340889e4b5..e10b58a1c60c3 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/index.ts @@ -6,7 +6,7 @@ */ import { ClusterPutComponentTemplate } from '@elastic/elasticsearch/api/requestParams'; import { estypes } from '@elastic/elasticsearch'; -import { ValidFeatureId } from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; +import { ValidFeatureId } from '@kbn/rule-data-utils'; import { ElasticsearchClient, Logger } from 'kibana/server'; import { get, isEmpty } from 'lodash'; diff --git a/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts index 50e5b224f01d8..caf14e8ba3000 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ALERT_ID } from '@kbn/rule-data-utils/target/technical_field_names'; +import { ALERT_ID } from '@kbn/rule-data-utils'; import { CreatePersistenceRuleTypeFactory } from './persistence_types'; export const createPersistenceRuleTypeFactory: CreatePersistenceRuleTypeFactory = ({ diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts index 99ee021cb6800..269fc6598beaa 100644 --- a/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts +++ b/x-pack/plugins/timelines/common/search_strategy/timeline/index.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { AlertConsumers } from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; +import type { AlertConsumers } from '@kbn/rule-data-utils'; import { IEsSearchRequest } from '../../../../../../src/plugins/data/common'; import { ESQuery } from '../../typed_json'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx index 36b5537eb3010..8ce8427985213 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx @@ -5,7 +5,9 @@ * 2.0. */ -import { AlertConsumers } from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; +import type { AlertConsumers as AlertConsumersTyped } from '@kbn/rule-data-utils'; +// @ts-expect-error +import { AlertConsumers as AlertConsumersNonTyped } from '@kbn/rule-data-utils/target_node/alerts_as_data_rbac'; import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useEffect, useMemo, useState } from 'react'; @@ -47,6 +49,8 @@ import { ExitFullScreen } from '../../exit_full_screen'; import { Sort } from '../body/sort'; import { InspectButtonContainer } from '../../inspect'; +const AlertConsumers: typeof AlertConsumersTyped = AlertConsumersNonTyped; + export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px const COMPACT_HEADER_HEIGHT = 36; // px diff --git a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx index df8a5897bfcd1..7382d029be98e 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import type { AlertConsumers } from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; +import type { AlertConsumers } from '@kbn/rule-data-utils'; import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; import React, { useEffect, useMemo, useState } from 'react'; diff --git a/x-pack/plugins/timelines/public/container/index.tsx b/x-pack/plugins/timelines/public/container/index.tsx index 5fb0ed56afaae..b7345e90fdb02 100644 --- a/x-pack/plugins/timelines/public/container/index.tsx +++ b/x-pack/plugins/timelines/public/container/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import type { AlertConsumers } from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; +import type { AlertConsumers } from '@kbn/rule-data-utils'; import deepEqual from 'fast-deep-equal'; import { isEmpty, isString, noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts index 5b60443fc6f51..a2a5a3bc8a49a 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts @@ -8,11 +8,17 @@ import { ALERT_RULE_CONSUMER, ALERT_RULE_TYPE_ID, SPACE_IDS } from '@kbn/rule-data-utils'; import { map, mergeMap, catchError } from 'rxjs/operators'; import { from } from 'rxjs'; -import { - isValidFeatureId, - mapConsumerToIndexName, + +import type { AlertConsumers, -} from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; + mapConsumerToIndexName as mapConsumerToIndexNameTyped, + isValidFeatureId as isValidFeatureIdTyped, +} from '@kbn/rule-data-utils'; +import { + mapConsumerToIndexName as mapConsumerToIndexNameNonTyped, + isValidFeatureId as isValidFeatureIdNonTyped, + // @ts-expect-error +} from '@kbn/rule-data-utils/target_node/alerts_as_data_rbac'; import { AlertingAuthorizationEntity, @@ -38,6 +44,9 @@ import { ISearchOptions, } from '../../../../../../src/plugins/data/common'; +const mapConsumerToIndexName: typeof mapConsumerToIndexNameTyped = mapConsumerToIndexNameNonTyped; +const isValidFeatureId: typeof isValidFeatureIdTyped = isValidFeatureIdNonTyped; + export const timelineSearchStrategyProvider = ( data: PluginStart, alerting: AlertingPluginStartContract diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts index ce13ae4ce6cea..a51bbc49123dc 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts @@ -17,7 +17,7 @@ import { ALERT_SEVERITY_VALUE, ALERT_EVALUATION_VALUE, ALERT_EVALUATION_THRESHOLD, -} from '@kbn/rule-data-utils/target/technical_field_names'; +} from '@kbn/rule-data-utils'; interface MockAnomaly { severity: AnomaliesTableRecord['severity']; diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts index 2388a789f3b80..100d992f5f863 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts @@ -13,7 +13,7 @@ import { ALERT_SEVERITY_VALUE, ALERT_EVALUATION_VALUE, ALERT_EVALUATION_THRESHOLD, -} from '@kbn/rule-data-utils/target/technical_field_names'; +} from '@kbn/rule-data-utils'; import { ActionGroupIdsOf } from '../../../../alerting/common'; import { updateState, generateAlertMessage } from './common'; import { DURATION_ANOMALY } from '../../../common/constants/alerts'; diff --git a/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx b/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx index 585470aff23b2..bedaca0fc1836 100644 --- a/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx +++ b/x-pack/test/plugin_functional/plugins/timelines_test/public/applications/timelines_test/index.tsx @@ -5,7 +5,9 @@ * 2.0. */ -import { AlertConsumers } from '@kbn/rule-data-utils/target/alerts_as_data_rbac'; +import type { AlertConsumers as AlertConsumersTyped } from '@kbn/rule-data-utils'; +// @ts-expect-error +import { AlertConsumers as AlertConsumersNonTyped } from '@kbn/rule-data-utils/target_node/alerts_as_data_rbac'; import { Router } from 'react-router-dom'; import React, { useCallback, useRef } from 'react'; import ReactDOM from 'react-dom'; @@ -15,6 +17,8 @@ import { KibanaContextProvider } from '../../../../../../../../src/plugins/kiban import { TimelinesUIStart } from '../../../../../../../plugins/timelines/public'; import { DataPublicPluginStart } from '../../../../../../../../src/plugins/data/public'; +const AlertConsumers: typeof AlertConsumersTyped = AlertConsumersNonTyped; + type CoreStartTimelines = CoreStart & { data: DataPublicPluginStart }; /** From 6d2c1da2ba6bb8a95cfe53c745fbd0b171ef53d4 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Wed, 11 Aug 2021 09:45:36 -0600 Subject: [PATCH 081/104] [Security Solutions][Detection Engine] Adds exception lists to the saved object references when created or modified (part 1) (#107064) ## Summary This is part 1 to addressing the issue seen here: https://github.com/elastic/kibana/issues/101975 This part 1 wires up our rules to be able to `inject` and `extract` parameters from the saved object references. Follow up part 2 (not included here) will do the saved object migrations of existing rules to have the saved object references. The way the code is written it shouldn't interfere or blow up anything even though the existing rules have not been migrated since we do fallbacks and only log errors when we detect that the saved object references have not been migrated or have been deleted. Therefore this PR should be migration friendly in that you will only see an occasional error as it serializes and deserializes a non migrated rule without object references but still work both ways. Non-migrated rules or rules with deleted saved object references will self correct during the serialization phase when you edit a rule and save out the modification. This should be migration bug friendly as well in case something does not work out with migrations, we can still have users edit an existing rule to correct the bug. For manual testing, see the `README.md` in the folder. You should be able to create and modify existing rules and then see in their saved objects that they have `references` pointing to the top level exception list containers with this PR. * Adds the new folder in `detection_engine/signals/saved_object_references` with all the code needed * Adds a top level `README.md` about the functionality and tips for new programmers to add their own references * Adds a generic pattern for adding more saved object references within our rule set * Adds ~40 unit tests * Adds additional migration safe logic to de-couple this from required saved object migrations and hopefully helps mitigates any existing bugs within the stack or previous migration bugs a bit for us. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../signals/saved_object_references/README.md | 144 ++++++++++++++++++ .../extract_exceptions_list.test.ts | 74 +++++++++ .../extract_exceptions_list.ts | 41 +++++ .../extract_references.test.ts | 82 ++++++++++ .../extract_references.ts | 57 +++++++ .../signals/saved_object_references/index.ts | 9 ++ .../inject_exceptions_list.test.ts | 139 +++++++++++++++++ .../inject_exceptions_list.ts | 62 ++++++++ .../inject_references.test.ts | 102 +++++++++++++ .../inject_references.ts | 50 ++++++ .../utils/constants.ts | 21 +++ .../get_saved_object_name_pattern.test.ts | 30 ++++ .../utils/get_saved_object_name_pattern.ts | 26 ++++ ...ct_name_pattern_for_exception_list.test.ts | 37 +++++ ..._object_name_pattern_for_exception_list.ts | 23 +++ .../utils/get_saved_object_reference.test.ts | 133 ++++++++++++++++ .../utils/get_saved_object_reference.ts | 48 ++++++ ...ject_reference_for_exceptions_list.test.ts | 130 ++++++++++++++++ ...ed_object_reference_for_exceptions_list.ts | 40 +++++ .../saved_object_references/utils/index.ts | 14 ++ .../log_missing_saved_object_error.test.ts | 33 ++++ .../utils/log_missing_saved_object_error.ts | 31 ++++ ...g_if_different_references_detected.test.ts | 38 +++++ ...arning_if_different_references_detected.ts | 36 +++++ .../signals/signal_rule_alert_type.ts | 6 + .../lib/detection_engine/signals/types.ts | 4 +- 26 files changed, 1408 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/README.md create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_exceptions_list.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_exceptions_list.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_references.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_references.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/index.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_references.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_references.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/constants.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern_for_exception_list.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern_for_exception_list.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference_for_exceptions_list.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference_for_exceptions_list.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/index.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_missing_saved_object_error.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_missing_saved_object_error.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.ts diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/README.md b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/README.md new file mode 100644 index 0000000000000..059005707625f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/README.md @@ -0,0 +1,144 @@ +This is where you add code when you have rules which contain saved object references. Saved object references are for +when you have "joins" in the saved objects between one saved object and another one. This can be a 1 to M (1 to many) +relationship for example where you have a rule which contains the "id" of another saved object. + +Examples are the `exceptionsList` on a rule which contains a saved object reference from the rule to another set of +saved objects of the type `exception-list` + +## Useful queries +How to get all your alerts to see if you have `exceptionsList` on it or not in dev tools: + +```json +GET .kibana/_search +{ + "query": { + "term": { + "type": { + "value": "alert" + } + } + } +} +``` + +## Structure on disk +Run a query in dev tools and you should see this code that adds the following savedObject references +to any newly saved rule: + +```json + { + "_index" : ".kibana-hassanabad19_8.0.0_001", + "_id" : "alert:38482620-ef1b-11eb-ad71-7de7959be71c", + "_score" : 6.2607274, + "_source" : { + "alert" : { + "name" : "kql test rule 1", + "tags" : [ + "__internal_rule_id:4ec223b9-77fa-4895-8539-6b3e586a2858", + "__internal_immutable:false" + ], + "alertTypeId" : "siem.signals", + "other data... other data": "other data...other data", + "exceptionsList" : [ + { + "id" : "endpoint_list", + "list_id" : "endpoint_list", + "namespace_type" : "agnostic", + "type" : "endpoint" + }, + { + "id" : "50e3bd70-ef1b-11eb-ad71-7de7959be71c", + "list_id" : "cd152d0d-3590-4a45-a478-eed04da7936b", + "type" : "detection", + "namespace_type" : "single" + } + ], + "other data... other data": "other data...other data", + "references" : [ + { + "name" : "param:exceptionsList_0", + "id" : "endpoint_list", + "type" : "exception-list" + }, + { + "name" : "param:exceptionsList_1", + "id" : "50e3bd70-ef1b-11eb-ad71-7de7959be71c", + "type" : "exception-list" + } + ], + "other data... other data": "other data...other data" + } + } + } +``` + +The structure is that the alerting framework in conjunction with this code will make an array of saved object references which are going to be: +```json +{ + "references" : [ + { + "name" : "param:exceptionsList_1", + "id" : "50e3bd70-ef1b-11eb-ad71-7de7959be71c", + "type" : "exception-list" + } + ] +} +``` + +`name` is the pattern of `param:${name}_${index}`. See the functions and constants in `utils.ts` of: + +* EXCEPTIONS_LIST_NAME +* getSavedObjectNamePattern +* getSavedObjectNamePatternForExceptionsList +* getSavedObjectReference +* getSavedObjectReferenceForExceptionsList + +For how it is constructed and retrieved. If you need to add more types, you should copy and create your own versions or use the generic +utilities/helpers if possible. + +`id` is the saved object id and should always be the same value as the `"exceptionsList" : [ "id" : "50e3bd70-ef1b-11eb-ad71-7de7959be71c" ...`. +If for some reason the saved object id changes or is different, then on the next save/persist the `exceptionsList.id` will update to that within +its saved object. Note though, that the references id replaces _always_ the `exceptionsList.id` at all times through `inject_references.ts`. If +for some reason the `references` id is deleted, then on the next `inject_references` it will prefer to use the last good known reference and log +a warning. + +Within the rule parameters you can still keep the last known good saved object reference id as above it is shown +```json +{ + "exceptionsList" : [ + { + "id" : "endpoint_list", + "list_id" : "endpoint_list", + "namespace_type" : "agnostic", + "type" : "endpoint" + }, + { + "id" : "50e3bd70-ef1b-11eb-ad71-7de7959be71c", + "list_id" : "cd152d0d-3590-4a45-a478-eed04da7936b", + "type" : "detection", + "namespace_type" : "single" + } + ], +} +``` + +## How to add a new saved object id reference to a rule + +See the files of: +* extract_references.ts +* inject_references.ts + +And their top level comments for how to wire up new instances. It's best to create a new file per saved object reference and push only the needed data +per file. + +Good examples and utilities can be found in the folder of `utils` such as: +* EXCEPTIONS_LIST_NAME +* getSavedObjectNamePattern +* getSavedObjectNamePatternForExceptionsList +* getSavedObjectReference +* getSavedObjectReferenceForExceptionsList + +You can follow those patterns but if it doesn't fit your use case it's fine to just create a new file and wire up your new saved object references + +## End to end tests +At this moment there are none. \ No newline at end of file diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_exceptions_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_exceptions_list.test.ts new file mode 100644 index 0000000000000..56a1e875ac5e7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_exceptions_list.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { extractExceptionsList } from './extract_exceptions_list'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { RuleParams } from '../../schemas/rule_schemas'; +import { EXCEPTION_LIST_NAMESPACE } from '@kbn/securitysolution-list-constants'; +import { EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME } from './utils'; + +describe('extract_exceptions_list', () => { + type FuncReturn = ReturnType; + let logger = loggingSystemMock.create().get('security_solution'); + const mockExceptionsList = (): RuleParams['exceptionsList'] => [ + { + id: '123', + list_id: '456', + type: 'detection', + namespace_type: 'agnostic', + }, + ]; + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + test('it returns an empty array given an empty array for exceptionsList', () => { + expect(extractExceptionsList({ logger, exceptionsList: [] })).toEqual([]); + }); + + test('logs expect error message if the exceptionsList is undefined', () => { + extractExceptionsList({ + logger, + exceptionsList: (undefined as unknown) as RuleParams['exceptionsList'], + }); + expect(logger.error).toBeCalledWith( + 'Exception list is null when it never should be. This indicates potentially that saved object migrations did not run correctly. Returning empty saved object reference' + ); + }); + + test('It returns exception list transformed into a saved object references', () => { + expect( + extractExceptionsList({ logger, exceptionsList: mockExceptionsList() }) + ).toEqual([ + { + id: '123', + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_0`, + type: EXCEPTION_LIST_NAMESPACE, + }, + ]); + }); + + test('It returns two exception lists transformed into a saved object references', () => { + const twoInputs: RuleParams['exceptionsList'] = [ + mockExceptionsList()[0], + { ...mockExceptionsList()[0], id: '976' }, + ]; + expect(extractExceptionsList({ logger, exceptionsList: twoInputs })).toEqual([ + { + id: '123', + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_0`, + type: EXCEPTION_LIST_NAMESPACE, + }, + { + id: '976', + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_1`, + type: EXCEPTION_LIST_NAMESPACE, + }, + ]); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_exceptions_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_exceptions_list.ts new file mode 100644 index 0000000000000..9b7f8bbcefee1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_exceptions_list.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger, SavedObjectReference } from 'src/core/server'; +import { EXCEPTION_LIST_NAMESPACE } from '@kbn/securitysolution-list-constants'; +import { RuleParams } from '../../schemas/rule_schemas'; +import { getSavedObjectNamePatternForExceptionsList } from './utils'; + +/** + * This extracts the "exceptionsList" "id" and returns it as a saved object reference. + * NOTE: Due to rolling upgrades with migrations and a few bugs with migrations, I do an additional check for if "exceptionsList" exists or not. Once + * those bugs are fixed, we can remove the "if (exceptionsList == null) {" check, but for the time being it is there to keep things running even + * if exceptionsList has not been migrated. + * @param logger The kibana injected logger + * @param exceptionsList The exceptions list to get the id from and return it as a saved object reference. + * @returns The saved object references from the exceptions list + */ +export const extractExceptionsList = ({ + logger, + exceptionsList, +}: { + logger: Logger; + exceptionsList: RuleParams['exceptionsList']; +}): SavedObjectReference[] => { + if (exceptionsList == null) { + logger.error( + 'Exception list is null when it never should be. This indicates potentially that saved object migrations did not run correctly. Returning empty saved object reference' + ); + return []; + } else { + return exceptionsList.map((exceptionItem, index) => ({ + name: getSavedObjectNamePatternForExceptionsList(index), + id: exceptionItem.id, + type: EXCEPTION_LIST_NAMESPACE, + })); + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_references.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_references.test.ts new file mode 100644 index 0000000000000..31288559e9437 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_references.test.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggingSystemMock } from 'src/core/server/mocks'; +import { extractReferences } from './extract_references'; +import { RuleParams } from '../../schemas/rule_schemas'; +import { EXCEPTION_LIST_NAMESPACE } from '@kbn/securitysolution-list-constants'; +import { EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME } from './utils'; + +describe('extract_references', () => { + type FuncReturn = ReturnType; + let logger = loggingSystemMock.create().get('security_solution'); + const mockExceptionsList = (): RuleParams['exceptionsList'] => [ + { + id: '123', + list_id: '456', + type: 'detection', + namespace_type: 'agnostic', + }, + ]; + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + test('It returns params untouched and the references extracted as exception list saved object references', () => { + const params: Partial = { + note: 'some note', + exceptionsList: mockExceptionsList(), + }; + expect( + extractReferences({ + logger, + params: params as RuleParams, + }) + ).toEqual({ + params: params as RuleParams, + references: [ + { + id: '123', + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_0`, + type: EXCEPTION_LIST_NAMESPACE, + }, + ], + }); + }); + + test('It returns params untouched and the references an empty array if the exceptionsList is an empty array', () => { + const params: Partial = { + note: 'some note', + exceptionsList: [], + }; + expect( + extractReferences({ + logger, + params: params as RuleParams, + }) + ).toEqual({ + params: params as RuleParams, + references: [], + }); + }); + + test('It returns params untouched and the references an empty array if the exceptionsList is missing for any reason', () => { + const params: Partial = { + note: 'some note', + }; + expect( + extractReferences({ + logger, + params: params as RuleParams, + }) + ).toEqual({ + params: params as RuleParams, + references: [], + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_references.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_references.ts new file mode 100644 index 0000000000000..92e689e225764 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/extract_references.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger } from 'src/core/server'; +import { RuleParamsAndRefs } from '../../../../../../alerting/server'; +import { RuleParams } from '../../schemas/rule_schemas'; +import { extractExceptionsList } from './extract_exceptions_list'; + +/** + * Extracts references and returns the saved object references. + * How to add a new extracted references here: + * --- + * Add a new file for extraction named: extract_.ts, example: extract_foo.ts + * Add a function into that file named: extract, example: extractFoo(logger, params.foo) + * Add a new line below and concat together the new extract with existing ones like so: + * + * const exceptionReferences = extractExceptionsList(logger, params.exceptionsList); + * const fooReferences = extractFoo(logger, params.foo); + * const returnReferences = [...exceptionReferences, ...fooReferences]; + * + * Optionally you can remove any parameters you do not want to store within the Saved Object here: + * const paramsWithoutSavedObjectReferences = { removeParam, ...otherParams }; + * + * If you do remove params, then update the types in: security_solution/server/lib/detection_engine/signals/types.ts + * to use an omit for the functions of "isAlertExecutor" and "SignalRuleAlertTypeDefinition" + * @param logger Kibana injected logger + * @param params The params of the base rule(s). + * @returns The rule parameters and the saved object references to store. + */ +export const extractReferences = ({ + logger, + params, +}: { + logger: Logger; + params: RuleParams; +}): RuleParamsAndRefs => { + const exceptionReferences = extractExceptionsList({ + logger, + exceptionsList: params.exceptionsList, + }); + const returnReferences = [...exceptionReferences]; + + // Modify params if you want to remove any elements separately here. For exceptionLists, we do not remove the id and instead + // keep it to both fail safe guard against manually removed saved object references or if there are migration issues and the saved object + // references are removed. Also keeping it we can detect and log out a warning if the reference between it and the saved_object reference + // array have changed between each other indicating the saved_object array is being mutated outside of this functionality + const paramsWithoutSavedObjectReferences = { ...params }; + + return { + references: returnReferences, + params: paramsWithoutSavedObjectReferences, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/index.ts new file mode 100644 index 0000000000000..b855554545837 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './inject_references'; +export * from './extract_references'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.test.ts new file mode 100644 index 0000000000000..fc35088da66fc --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.test.ts @@ -0,0 +1,139 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggingSystemMock } from 'src/core/server/mocks'; +import { SavedObjectReference } from 'src/core/server'; +import { EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME } from './utils'; +import { EXCEPTION_LIST_NAMESPACE } from '@kbn/securitysolution-list-constants'; +import { injectExceptionsReferences } from './inject_exceptions_list'; +import { RuleParams } from '../../schemas/rule_schemas'; + +describe('inject_exceptions_list', () => { + type FuncReturn = ReturnType; + let logger = loggingSystemMock.create().get('security_solution'); + const mockExceptionsList = (): RuleParams['exceptionsList'] => [ + { + id: '123', + list_id: '456', + type: 'detection', + namespace_type: 'agnostic', + }, + ]; + const mockSavedObjectReferences = (): SavedObjectReference[] => [ + { + id: '123', + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_0`, + type: EXCEPTION_LIST_NAMESPACE, + }, + ]; + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + test('returns empty array given an empty array for both "exceptionsList" and "savedObjectReferences"', () => { + expect( + injectExceptionsReferences({ + logger, + exceptionsList: [], + savedObjectReferences: [], + }) + ).toEqual([]); + }); + + test('logs expect error message if the exceptionsList is undefined', () => { + injectExceptionsReferences({ + logger, + exceptionsList: (undefined as unknown) as RuleParams['exceptionsList'], + savedObjectReferences: mockSavedObjectReferences(), + }); + expect(logger.error).toBeCalledWith( + 'Exception list is null when it never should be. This indicates potentially that saved object migrations did not run correctly. Returning empty exception list' + ); + }); + + test('returns empty array given an empty array for "exceptionsList"', () => { + expect( + injectExceptionsReferences({ + logger, + exceptionsList: [], + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual([]); + }); + + test('returns exceptions list array given an empty array for "savedObjectReferences"', () => { + expect( + injectExceptionsReferences({ + logger, + exceptionsList: mockExceptionsList(), + savedObjectReferences: [], + }) + ).toEqual(mockExceptionsList()); + }); + + test('returns parameters from the saved object if found', () => { + expect( + injectExceptionsReferences({ + logger, + exceptionsList: mockExceptionsList(), + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual(mockExceptionsList()); + }); + + test('does not log an error if it returns parameters from the saved object when found', () => { + injectExceptionsReferences({ + logger, + exceptionsList: mockExceptionsList(), + savedObjectReferences: mockSavedObjectReferences(), + }); + expect(logger.error).not.toHaveBeenCalled(); + }); + + test('returns parameters from the saved object if found with a different saved object reference id', () => { + expect( + injectExceptionsReferences({ + logger, + exceptionsList: mockExceptionsList(), + savedObjectReferences: [{ ...mockSavedObjectReferences()[0], id: '456' }], + }) + ).toEqual([{ ...mockExceptionsList()[0], id: '456' }]); + }); + + test('logs an error if found with a different saved object reference id', () => { + injectExceptionsReferences({ + logger, + exceptionsList: mockExceptionsList(), + savedObjectReferences: [{ ...mockSavedObjectReferences()[0], id: '456' }], + }); + expect(logger.error).toBeCalledWith( + 'The id of the "saved object reference id": 456 is not the same as the "saved object id": 123. Preferring and using the "saved object reference id" instead of the "saved object id"' + ); + }); + + test('returns exceptionItem if the saved object reference cannot match as a fall back', () => { + expect( + injectExceptionsReferences({ + logger, + exceptionsList: mockExceptionsList(), + savedObjectReferences: [{ ...mockSavedObjectReferences()[0], name: 'other-name_0' }], + }) + ).toEqual(mockExceptionsList()); + }); + + test('logs an error if the saved object type could not be found', () => { + injectExceptionsReferences({ + logger, + exceptionsList: mockExceptionsList(), + savedObjectReferences: [{ ...mockSavedObjectReferences()[0], name: 'other-name_0' }], + }); + expect(logger.error).toBeCalledWith( + 'The saved object references were not found for our exception list when we were expecting to find it. Kibana migrations might not have run correctly or someone might have removed the saved object references manually. Returning the last known good exception list id which might not work. exceptionItem with its id being returned is: {"id":"123","list_id":"456","type":"detection","namespace_type":"agnostic"}' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.ts new file mode 100644 index 0000000000000..2e6559fbf18cf --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_exceptions_list.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger, SavedObjectReference } from 'src/core/server'; +import { RuleParams } from '../../schemas/rule_schemas'; +import { + getSavedObjectReferenceForExceptionsList, + logMissingSavedObjectError, + logWarningIfDifferentReferencesDetected, +} from './utils'; + +/** + * This injects any "exceptionsList" "id"'s from saved object reference and returns the "exceptionsList" using the saved object reference. If for + * some reason it is missing on saved object reference, we log an error about it and then take the last known good value from the "exceptionsList" + * + * @param logger The kibana injected logger + * @param exceptionsList The exceptions list to merge the saved object reference from. + * @param savedObjectReferences The saved object references which should contain an "exceptionsList" + * @returns The exceptionsList with the saved object reference replacing any value in the saved object's id. + */ +export const injectExceptionsReferences = ({ + logger, + exceptionsList, + savedObjectReferences, +}: { + logger: Logger; + exceptionsList: RuleParams['exceptionsList']; + savedObjectReferences: SavedObjectReference[]; +}): RuleParams['exceptionsList'] => { + if (exceptionsList == null) { + logger.error( + 'Exception list is null when it never should be. This indicates potentially that saved object migrations did not run correctly. Returning empty exception list' + ); + return []; + } + return exceptionsList.map((exceptionItem, index) => { + const savedObjectReference = getSavedObjectReferenceForExceptionsList({ + logger, + index, + savedObjectReferences, + }); + if (savedObjectReference != null) { + logWarningIfDifferentReferencesDetected({ + logger, + savedObjectReferenceId: savedObjectReference.id, + savedObjectId: exceptionItem.id, + }); + const reference: RuleParams['exceptionsList'][0] = { + ...exceptionItem, + id: savedObjectReference.id, + }; + return reference; + } else { + logMissingSavedObjectError({ logger, exceptionItem }); + return exceptionItem; + } + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_references.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_references.test.ts new file mode 100644 index 0000000000000..a80f19ae011d7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_references.test.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggingSystemMock } from 'src/core/server/mocks'; +import { SavedObjectReference } from 'src/core/server'; +import { EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME } from './utils'; +import { EXCEPTION_LIST_NAMESPACE } from '@kbn/securitysolution-list-constants'; +import { injectReferences } from './inject_references'; +import { RuleParams } from '../../schemas/rule_schemas'; + +describe('inject_references', () => { + type FuncReturn = ReturnType; + let logger = loggingSystemMock.create().get('security_solution'); + const mockExceptionsList = (): RuleParams['exceptionsList'] => [ + { + id: '123', + list_id: '456', + type: 'detection', + namespace_type: 'agnostic', + }, + ]; + const mockSavedObjectReferences = (): SavedObjectReference[] => [ + { + id: '123', + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_0`, + type: EXCEPTION_LIST_NAMESPACE, + }, + ]; + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + test('returns parameters from a saved object if found', () => { + const params: Partial = { + note: 'some note', + exceptionsList: mockExceptionsList(), + }; + expect( + injectReferences({ + logger, + params: params as RuleParams, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual(params as RuleParams); + }); + + test('returns parameters from the saved object if found with a different saved object reference id', () => { + const params: Partial = { + note: 'some note', + exceptionsList: mockExceptionsList(), + }; + + const returnParams: Partial = { + note: 'some note', + exceptionsList: [{ ...mockExceptionsList()[0], id: '456' }], + }; + + expect( + injectReferences({ + logger, + params: params as RuleParams, + savedObjectReferences: [{ ...mockSavedObjectReferences()[0], id: '456' }], + }) + ).toEqual(returnParams as RuleParams); + }); + + test('It returns params untouched and the references an empty array if the exceptionsList is an empty array', () => { + const params: Partial = { + note: 'some note', + exceptionsList: [], + }; + expect( + injectReferences({ + logger, + params: params as RuleParams, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual(params as RuleParams); + }); + + test('It returns params with an added exceptionsList if the exceptionsList is missing due to migration bugs', () => { + const params: Partial = { + note: 'some note', + }; + const returnParams: Partial = { + note: 'some note', + exceptionsList: [], + }; + expect( + injectReferences({ + logger, + params: params as RuleParams, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual(returnParams as RuleParams); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_references.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_references.ts new file mode 100644 index 0000000000000..dae5e3037b737 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/inject_references.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger, SavedObjectReference } from 'src/core/server'; +import { RuleParams } from '../../schemas/rule_schemas'; +import { injectExceptionsReferences } from './inject_exceptions_list'; + +/** + * Injects references and returns the saved object references. + * How to add a new injected references here: + * --- + * Add a new file for injection named: inject_.ts, example: inject_foo.ts + * Add a new function into that file named: inject, example: injectFooReferences(logger, params.foo) + * Add a new line below and spread the new parameter together like so: + * + * const foo = injectFooReferences(logger, params.foo, savedObjectReferences); + * const ruleParamsWithSavedObjectReferences: RuleParams = { + * ...params, + * foo, + * exceptionsList, + * }; + * @param logger Kibana injected logger + * @param params The params of the base rule(s). + * @param savedObjectReferences The saved object references to merge with the rule params + * @returns The rule parameters with the saved object references. + */ +export const injectReferences = ({ + logger, + params, + savedObjectReferences, +}: { + logger: Logger; + params: RuleParams; + savedObjectReferences: SavedObjectReference[]; +}): RuleParams => { + const exceptionsList = injectExceptionsReferences({ + logger, + exceptionsList: params.exceptionsList, + savedObjectReferences, + }); + const ruleParamsWithSavedObjectReferences: RuleParams = { + ...params, + exceptionsList, + }; + return ruleParamsWithSavedObjectReferences; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/constants.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/constants.ts new file mode 100644 index 0000000000000..1423dff3b92f1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/constants.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * The name of the exceptions list we give it when we save the saved object references. This name will + * end up in the saved object as in this example: + * { + * "references" : [ + * { + * "name" : "param:exceptionsList_1", + * "id" : "50e3bd70-ef1b-11eb-ad71-7de7959be71c", + * "type" : "exception-list" + * } + * ] + * } + */ +export const EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME = 'exceptionsList'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern.test.ts new file mode 100644 index 0000000000000..41dc2d9179d83 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getSavedObjectNamePattern } from '.'; + +describe('get_saved_object_name_pattern_for_exception_list', () => { + test('returns expected pattern given a zero', () => { + expect(getSavedObjectNamePattern({ name: 'test', index: 0 })).toEqual('test_0'); + }); + + test('returns expected pattern given a positive number', () => { + expect(getSavedObjectNamePattern({ name: 'test', index: 1 })).toEqual('test_1'); + }); + + test('throws given less than zero', () => { + expect(() => getSavedObjectNamePattern({ name: 'test', index: -1 })).toThrow( + '"index" should alway be >= 0 instead of: -1' + ); + }); + + test('throws given NaN', () => { + expect(() => getSavedObjectNamePattern({ name: 'test', index: NaN })).toThrow( + '"index" should alway be >= 0 instead of: NaN' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern.ts new file mode 100644 index 0000000000000..f4e33cf57fa2b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Given a name and index this will return the pattern of "${name_${index}" + * @param name The name to suffix the string + * @param index The index to suffix the string + * @returns The pattern "${name_${index}" + */ +export const getSavedObjectNamePattern = ({ + name, + index, +}: { + name: string; + index: number; +}): string => { + if (!(index >= 0)) { + throw new TypeError(`"index" should alway be >= 0 instead of: ${index}`); + } else { + return `${name}_${index}`; + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern_for_exception_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern_for_exception_list.test.ts new file mode 100644 index 0000000000000..98c575e8835be --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern_for_exception_list.test.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME, + getSavedObjectNamePatternForExceptionsList, +} from '.'; + +describe('get_saved_object_name_pattern_for_exception_list', () => { + test('returns expected pattern given a zero', () => { + expect(getSavedObjectNamePatternForExceptionsList(0)).toEqual( + `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_0` + ); + }); + + test('returns expected pattern given a positive number', () => { + expect(getSavedObjectNamePatternForExceptionsList(1)).toEqual( + `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_1` + ); + }); + + test('throws given less than zero', () => { + expect(() => getSavedObjectNamePatternForExceptionsList(-1)).toThrow( + '"index" should alway be >= 0 instead of: -1' + ); + }); + + test('throws given NaN', () => { + expect(() => getSavedObjectNamePatternForExceptionsList(NaN)).toThrow( + '"index" should alway be >= 0 instead of: NaN' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern_for_exception_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern_for_exception_list.ts new file mode 100644 index 0000000000000..c505309411689 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_name_pattern_for_exception_list.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME } from './constants'; +import { getSavedObjectNamePattern } from './get_saved_object_name_pattern'; + +/** + * Given an index this will return the pattern of "exceptionsList_${index}" + * @param index The index to suffix the string + * @returns The pattern of "exceptionsList_${index}" + * @throws TypeError if index is less than zero + */ +export const getSavedObjectNamePatternForExceptionsList = (index: number): string => { + if (!(index >= 0)) { + throw new TypeError(`"index" should alway be >= 0 instead of: ${index}`); + } else { + return getSavedObjectNamePattern({ name: EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME, index }); + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference.test.ts new file mode 100644 index 0000000000000..70f321eed030e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference.test.ts @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggingSystemMock } from 'src/core/server/mocks'; +import { SavedObjectReference } from 'src/core/server'; +import { getSavedObjectReference } from '.'; + +describe('get_saved_object_reference', () => { + type FuncReturn = ReturnType; + const mockSavedObjectReferences = (): SavedObjectReference[] => [ + { + id: '123', + name: 'test_0', + type: 'some-type', + }, + ]; + let logger = loggingSystemMock.create().get('security_solution'); + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + test('returns reference found, given index zero', () => { + expect( + getSavedObjectReference({ + name: 'test', + logger, + index: 0, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual(mockSavedObjectReferences()[0]); + }); + + test('returns reference found, given positive index', () => { + const savedObjectReferences: SavedObjectReference[] = [ + mockSavedObjectReferences()[0], + { + id: '345', + name: 'test_1', + type: 'some-type', + }, + ]; + expect( + getSavedObjectReference({ + name: 'test', + logger, + index: 1, + savedObjectReferences, + }) + ).toEqual(savedObjectReferences[1]); + }); + + test('returns undefined, given index larger than the size of object references', () => { + expect( + getSavedObjectReference({ + name: 'test', + logger, + index: 100, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual(undefined); + }); + + test('returns undefined, when it cannot find the reference', () => { + expect( + getSavedObjectReference({ + name: 'test', + logger, + index: 0, + savedObjectReferences: [{ ...mockSavedObjectReferences()[0], name: 'other-name_0' }], + }) + ).toEqual(undefined); + }); + + test('returns found reference, even if the reference is mixed with other references', () => { + expect( + getSavedObjectReference({ + name: 'test', + logger, + index: 0, + savedObjectReferences: [ + { ...mockSavedObjectReferences()[0], name: 'other-name_0' }, + mockSavedObjectReferences()[0], + ], + }) + ).toEqual(mockSavedObjectReferences()[0]); + }); + + test('returns found reference, even if the reference is mixed with other references and has an index of 1', () => { + const additionalException: SavedObjectReference = { + ...mockSavedObjectReferences()[0], + name: 'test_1', + }; + expect( + getSavedObjectReference({ + name: 'test', + logger, + index: 1, + savedObjectReferences: [ + { ...mockSavedObjectReferences()[0], name: 'other-name_0' }, + mockSavedObjectReferences()[0], + additionalException, + ], + }) + ).toEqual(additionalException); + }); + + test('throws given less than zero', () => { + expect(() => + getSavedObjectReference({ + name: 'test', + logger, + index: -1, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toThrow('"index" should alway be >= 0 instead of: -1'); + }); + + test('throws given NaN', () => { + expect(() => + getSavedObjectReference({ + name: 'test', + logger, + index: NaN, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toThrow('"index" should alway be >= 0 instead of: NaN'); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference.ts new file mode 100644 index 0000000000000..fe3a7393bf377 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger, SavedObjectReference } from 'src/core/server'; +import { getSavedObjectNamePattern } from './get_saved_object_name_pattern'; + +/** + * Given a saved object name, and an index, this will return the specific named saved object reference + * even if it is mixed in with other reference objects. This is needed since a references array can contain multiple + * types of saved objects in a single array, we have to use the name to get the value. + * @param logger The kibana injected logger + * @param name The name of the saved object reference we are getting from the array + * @param index The index position to get for the exceptions list. + * @param savedObjectReferences The saved object references which can contain "exceptionsList" mixed with other saved object types + * @returns The saved object reference if found, otherwise undefined + */ +export const getSavedObjectReference = ({ + logger, + name, + index, + savedObjectReferences, +}: { + logger: Logger; + name: string; + index: number; + savedObjectReferences: SavedObjectReference[]; +}): SavedObjectReference | undefined => { + if (!(index >= 0)) { + throw new TypeError(`"index" should alway be >= 0 instead of: ${index}`); + } else if (index > savedObjectReferences.length) { + logger.error( + [ + 'Cannot get a saved object reference using an index which is larger than the saved object references. Index is:', + index, + ' which is larger than the savedObjectReferences:', + JSON.stringify(savedObjectReferences), + ].join('') + ); + } else { + return savedObjectReferences.find( + (reference) => reference.name === getSavedObjectNamePattern({ name, index }) + ); + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference_for_exceptions_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference_for_exceptions_list.test.ts new file mode 100644 index 0000000000000..9a16037ed7fd5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference_for_exceptions_list.test.ts @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggingSystemMock } from 'src/core/server/mocks'; +import { SavedObjectReference } from 'src/core/server'; +import { + EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME, + getSavedObjectReferenceForExceptionsList, +} from '.'; +import { EXCEPTION_LIST_NAMESPACE } from '@kbn/securitysolution-list-constants'; + +describe('get_saved_object_reference_for_exceptions_list', () => { + type FuncReturn = ReturnType; + let logger = loggingSystemMock.create().get('security_solution'); + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + const mockSavedObjectReferences = (): SavedObjectReference[] => [ + { + id: '123', + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_0`, + type: EXCEPTION_LIST_NAMESPACE, + }, + ]; + + test('returns reference found, given index zero', () => { + expect( + getSavedObjectReferenceForExceptionsList({ + logger, + index: 0, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual(mockSavedObjectReferences()[0]); + }); + + test('returns reference found, given positive index', () => { + const savedObjectReferences: SavedObjectReference[] = [ + mockSavedObjectReferences()[0], + { + id: '345', + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_1`, + type: EXCEPTION_LIST_NAMESPACE, + }, + ]; + expect( + getSavedObjectReferenceForExceptionsList({ + logger, + index: 1, + savedObjectReferences, + }) + ).toEqual(savedObjectReferences[1]); + }); + + test('returns undefined, given index larger than the size of object references', () => { + expect( + getSavedObjectReferenceForExceptionsList({ + logger, + index: 100, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toEqual(undefined); + }); + + test('returns undefined, when it cannot find the reference', () => { + expect( + getSavedObjectReferenceForExceptionsList({ + logger, + index: 0, + savedObjectReferences: [{ ...mockSavedObjectReferences()[0], name: 'other-name_0' }], + }) + ).toEqual(undefined); + }); + + test('returns found reference, even if the reference is mixed with other references', () => { + expect( + getSavedObjectReferenceForExceptionsList({ + logger, + index: 0, + savedObjectReferences: [ + { ...mockSavedObjectReferences()[0], name: 'other-name_0' }, + mockSavedObjectReferences()[0], + ], + }) + ).toEqual(mockSavedObjectReferences()[0]); + }); + + test('returns found reference, even if the reference is mixed with other references and has an index of 1', () => { + const additionalException: SavedObjectReference = { + ...mockSavedObjectReferences()[0], + name: `${EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME}_1`, + }; + expect( + getSavedObjectReferenceForExceptionsList({ + logger, + index: 1, + savedObjectReferences: [ + { ...mockSavedObjectReferences()[0], name: 'other-name_0' }, + mockSavedObjectReferences()[0], + additionalException, + ], + }) + ).toEqual(additionalException); + }); + + test('throws given less than zero', () => { + expect(() => + getSavedObjectReferenceForExceptionsList({ + logger, + index: -1, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toThrow('"index" should alway be >= 0 instead of: -1'); + }); + + test('throws given NaN', () => { + expect(() => + getSavedObjectReferenceForExceptionsList({ + logger, + index: NaN, + savedObjectReferences: mockSavedObjectReferences(), + }) + ).toThrow('"index" should alway be >= 0 instead of: NaN'); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference_for_exceptions_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference_for_exceptions_list.ts new file mode 100644 index 0000000000000..d1534cc2a06bb --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/get_saved_object_reference_for_exceptions_list.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger, SavedObjectReference } from 'src/core/server'; +import { EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME } from './constants'; +import { getSavedObjectReference } from './get_saved_object_reference'; + +/** + * Given an index and a saved object reference, this will return the specific "exceptionsList" saved object reference + * even if it is mixed in with other reference objects. This is needed since a references array can contain multiple + * types of saved objects in a single array, we have to use the "exceptionsList" name to get the value. + * @param logger The kibana injected logger + * @param index The index position to get for the exceptions list. + * @param savedObjectReferences The saved object references which can contain "exceptionsList" mixed with other saved object types + * @returns The saved object reference if found, otherwise undefined + */ +export const getSavedObjectReferenceForExceptionsList = ({ + logger, + index, + savedObjectReferences, +}: { + logger: Logger; + index: number; + savedObjectReferences: SavedObjectReference[]; +}): SavedObjectReference | undefined => { + if (!(index >= 0)) { + throw new TypeError(`"index" should alway be >= 0 instead of: ${index}`); + } else { + return getSavedObjectReference({ + logger, + name: EXCEPTIONS_SAVED_OBJECT_REFERENCE_NAME, + index, + savedObjectReferences, + }); + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/index.ts new file mode 100644 index 0000000000000..ca88dae364a4b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './constants'; +export * from './get_saved_object_name_pattern_for_exception_list'; +export * from './get_saved_object_name_pattern'; +export * from './get_saved_object_reference_for_exceptions_list'; +export * from './get_saved_object_reference'; +export * from './log_missing_saved_object_error'; +export * from './log_warning_if_different_references_detected'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_missing_saved_object_error.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_missing_saved_object_error.test.ts new file mode 100644 index 0000000000000..d0158be285667 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_missing_saved_object_error.test.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggingSystemMock } from 'src/core/server/mocks'; + +import { logMissingSavedObjectError } from '.'; + +describe('log_missing_saved_object_error', () => { + let logger = loggingSystemMock.create().get('security_solution'); + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + test('logs expect error message', () => { + logMissingSavedObjectError({ + logger, + exceptionItem: { + id: '123', + list_id: '456', + type: 'detection', + namespace_type: 'agnostic', + }, + }); + expect(logger.error).toBeCalledWith( + 'The saved object references were not found for our exception list when we were expecting to find it. Kibana migrations might not have run correctly or someone might have removed the saved object references manually. Returning the last known good exception list id which might not work. exceptionItem with its id being returned is: {"id":"123","list_id":"456","type":"detection","namespace_type":"agnostic"}' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_missing_saved_object_error.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_missing_saved_object_error.ts new file mode 100644 index 0000000000000..8d448c3cd10c4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_missing_saved_object_error.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger } from 'src/core/server'; +import { RuleParams } from '../../../schemas/rule_schemas'; + +/** + * This will log a warning that we are missing an object reference. + * @param logger The kibana injected logger + * @param exceptionItem The exception item to log the warning out as + */ +export const logMissingSavedObjectError = ({ + logger, + exceptionItem, +}: { + logger: Logger; + exceptionItem: RuleParams['exceptionsList'][0]; +}): void => { + logger.error( + [ + 'The saved object references were not found for our exception list when we were expecting to find it. ', + 'Kibana migrations might not have run correctly or someone might have removed the saved object references manually. ', + 'Returning the last known good exception list id which might not work. exceptionItem with its id being returned is: ', + JSON.stringify(exceptionItem), + ].join('') + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.test.ts new file mode 100644 index 0000000000000..a27faa6356c2b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggingSystemMock } from 'src/core/server/mocks'; + +import { logWarningIfDifferentReferencesDetected } from '.'; + +describe('log_warning_if_different_references_detected', () => { + let logger = loggingSystemMock.create().get('security_solution'); + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + test('logs expect error message if the two ids are different', () => { + logWarningIfDifferentReferencesDetected({ + logger, + savedObjectReferenceId: '123', + savedObjectId: '456', + }); + expect(logger.error).toBeCalledWith( + 'The id of the "saved object reference id": 123 is not the same as the "saved object id": 456. Preferring and using the "saved object reference id" instead of the "saved object id"' + ); + }); + + test('logs nothing if the two ids are the same', () => { + logWarningIfDifferentReferencesDetected({ + logger, + savedObjectReferenceId: '123', + savedObjectId: '123', + }); + expect(logger.error).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.ts new file mode 100644 index 0000000000000..9f80ba6d8ce83 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/saved_object_references/utils/log_warning_if_different_references_detected.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger } from 'src/core/server'; + +/** + * This will log a warning that the saved object reference id and the saved object id are not the same if that is true. + * @param logger The kibana injected logger + * @param savedObjectReferenceId The saved object reference id from "references: [{ id: ...}]" + * @param savedObjectId The saved object id from a structure such as exceptions { exceptionsList: { "id": "..." } } + */ +export const logWarningIfDifferentReferencesDetected = ({ + logger, + savedObjectReferenceId, + savedObjectId, +}: { + logger: Logger; + savedObjectReferenceId: string; + savedObjectId: string; +}): void => { + if (savedObjectReferenceId !== savedObjectId) { + logger.error( + [ + 'The id of the "saved object reference id": ', + savedObjectReferenceId, + ' is not the same as the "saved object id": ', + savedObjectId, + '. Preferring and using the "saved object reference id" instead of the "saved object id"', + ].join('') + ); + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 7e467891e6d4d..b242691577b89 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -69,6 +69,7 @@ import { wrapHitsFactory } from './wrap_hits_factory'; import { wrapSequencesFactory } from './wrap_sequences_factory'; import { ConfigType } from '../../../config'; import { ExperimentalFeatures } from '../../../../common/experimental_features'; +import { injectReferences, extractReferences } from './saved_object_references'; import { RuleExecutionLogClient } from '../rule_execution_log/rule_execution_log_client'; import { IRuleDataPluginService } from '../rule_execution_log/types'; @@ -96,6 +97,11 @@ export const signalRulesAlertType = ({ name: 'SIEM signal', actionGroups: siemRuleActionGroups, defaultActionGroupId: 'default', + useSavedObjectReferences: { + extractReferences: (params) => extractReferences({ logger, params }), + injectReferences: (params, savedObjectReferences) => + injectReferences({ logger, params, savedObjectReferences }), + }, validate: { params: { validate: (object: unknown): RuleParams => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 6cbe0d1a52704..4da411d0c70a1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -195,7 +195,7 @@ export const isAlertExecutor = ( obj: SignalRuleAlertTypeDefinition ): obj is AlertType< RuleParams, - never, // Only use if defining useSavedObjectReferences hook + RuleParams, // This type is used for useSavedObjectReferences, use an Omit here if you want to remove any values. AlertTypeState, AlertInstanceState, AlertInstanceContext, @@ -206,7 +206,7 @@ export const isAlertExecutor = ( export type SignalRuleAlertTypeDefinition = AlertType< RuleParams, - never, // Only use if defining useSavedObjectReferences hook + RuleParams, // This type is used for useSavedObjectReferences, use an Omit here if you want to remove any values. AlertTypeState, AlertInstanceState, AlertInstanceContext, From def97bd73435efe509b55809a5c1551bf062c7aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Wed, 11 Aug 2021 16:56:23 +0100 Subject: [PATCH 082/104] [Status UI] Use the new output format of API `GET /api/status` (#107937) --- packages/kbn-optimizer/limits.yml | 2 +- .../__snapshots__/status_table.test.tsx.snap | 2 +- .../status/components/server_status.test.tsx | 4 +- .../status/components/status_table.test.tsx | 2 +- .../core_app/status/lib/load_status.test.ts | 77 ++++++++++++------- .../public/core_app/status/lib/load_status.ts | 63 ++++++++++++--- src/core/server/status/routes/status.ts | 51 +----------- src/core/types/status.ts | 56 ++++++++++---- 8 files changed, 150 insertions(+), 107 deletions(-) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 10ce4f7ab1d1f..4772e00d56450 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -8,7 +8,7 @@ pageLoadAssetSize: charts: 95000 cloud: 21076 console: 46091 - core: 434325 + core: 435325 crossClusterReplication: 65408 dashboard: 374194 dashboardEnhanced: 65646 diff --git a/src/core/public/core_app/status/components/__snapshots__/status_table.test.tsx.snap b/src/core/public/core_app/status/components/__snapshots__/status_table.test.tsx.snap index f5d3b837ce718..16b67bfa0584f 100644 --- a/src/core/public/core_app/status/components/__snapshots__/status_table.test.tsx.snap +++ b/src/core/public/core_app/status/components/__snapshots__/status_table.test.tsx.snap @@ -27,7 +27,7 @@ exports[`StatusTable renders when statuses is provided 1`] = ` Object { "id": "plugin:1", "state": Object { - "id": "green", + "id": "available", "message": "Ready", "title": "green", "uiColor": "secondary", diff --git a/src/core/public/core_app/status/components/server_status.test.tsx b/src/core/public/core_app/status/components/server_status.test.tsx index f0a9a9a88bc4f..3aa41827ff40f 100644 --- a/src/core/public/core_app/status/components/server_status.test.tsx +++ b/src/core/public/core_app/status/components/server_status.test.tsx @@ -12,7 +12,7 @@ import { ServerStatus } from './server_status'; import { FormattedStatus } from '../lib'; const getStatus = (parts: Partial = {}): FormattedStatus['state'] => ({ - id: 'green', + id: 'available', title: 'Green', uiColor: 'secondary', message: '', @@ -29,7 +29,7 @@ describe('ServerStatus', () => { it('renders correctly for red state', () => { const status = getStatus({ - id: 'red', + id: 'unavailable', title: 'Red', }); const component = mount(); diff --git a/src/core/public/core_app/status/components/status_table.test.tsx b/src/core/public/core_app/status/components/status_table.test.tsx index 6f6324bf809ee..af7d33bee5ed6 100644 --- a/src/core/public/core_app/status/components/status_table.test.tsx +++ b/src/core/public/core_app/status/components/status_table.test.tsx @@ -11,7 +11,7 @@ import { shallow } from 'enzyme'; import { StatusTable } from './status_table'; const state = { - id: 'green', + id: 'available' as const, uiColor: 'secondary', message: 'Ready', title: 'green', diff --git a/src/core/public/core_app/status/lib/load_status.test.ts b/src/core/public/core_app/status/lib/load_status.test.ts index 349f20a2385c6..e412192ea00ee 100644 --- a/src/core/public/core_app/status/lib/load_status.test.ts +++ b/src/core/public/core_app/status/lib/load_status.test.ts @@ -17,36 +17,37 @@ const mockedResponse: StatusResponse = { version: { number: '8.0.0', build_hash: '9007199254740991', - build_number: '12', - build_snapshot: 'XXXXXXXX', + build_number: 12, + build_snapshot: false, }, status: { overall: { - id: 'overall', - state: 'yellow', - title: 'Yellow', - message: 'yellow', - uiColor: 'secondary', + level: 'degraded', + summary: 'yellow', }, - statuses: [ - { - id: 'plugin:1', - state: 'green', - title: 'Green', - message: 'Ready', - uiColor: 'secondary', + core: { + elasticsearch: { + level: 'available', + summary: 'Elasticsearch is available', }, - { - id: 'plugin:2', - state: 'yellow', - title: 'Yellow', - message: 'Something is weird', - uiColor: 'warning', + savedObjects: { + level: 'available', + summary: 'SavedObjects service has completed migrations and is available', + }, + }, + plugins: { + '1': { + level: 'available', + summary: 'Ready', + }, + '2': { + level: 'degraded', + summary: 'Something is weird', }, - ], + }, }, metrics: { - collected_at: new Date('2020-01-01 01:00:00'), + last_updated: '2020-01-01 01:00:00', collection_interval_in_millis: 1000, os: { platform: 'darwin' as const, @@ -80,6 +81,7 @@ const mockedResponse: StatusResponse = { disconnects: 1, total: 400, statusCodes: {}, + status_codes: {}, }, concurrent_connections: 1, }, @@ -148,13 +150,36 @@ describe('response processing', () => { test('includes the plugin statuses', async () => { const data = await loadStatus({ http, notifications }); expect(data.statuses).toEqual([ + { + id: 'core:elasticsearch', + state: { + id: 'available', + title: 'Green', + message: 'Elasticsearch is available', + uiColor: 'secondary', + }, + }, + { + id: 'core:savedObjects', + state: { + id: 'available', + title: 'Green', + message: 'SavedObjects service has completed migrations and is available', + uiColor: 'secondary', + }, + }, { id: 'plugin:1', - state: { id: 'green', title: 'Green', message: 'Ready', uiColor: 'secondary' }, + state: { id: 'available', title: 'Green', message: 'Ready', uiColor: 'secondary' }, }, { id: 'plugin:2', - state: { id: 'yellow', title: 'Yellow', message: 'Something is weird', uiColor: 'warning' }, + state: { + id: 'degraded', + title: 'Yellow', + message: 'Something is weird', + uiColor: 'warning', + }, }, ]); }); @@ -162,10 +187,10 @@ describe('response processing', () => { test('includes the serverState', async () => { const data = await loadStatus({ http, notifications }); expect(data.serverState).toEqual({ - id: 'yellow', + id: 'degraded', title: 'Yellow', message: 'yellow', - uiColor: 'secondary', + uiColor: 'warning', }); }); diff --git a/src/core/public/core_app/status/lib/load_status.ts b/src/core/public/core_app/status/lib/load_status.ts index 0748c3dfe1dec..a5cc18ffd6c16 100644 --- a/src/core/public/core_app/status/lib/load_status.ts +++ b/src/core/public/core_app/status/lib/load_status.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import type { UnwrapPromise } from '@kbn/utility-types'; -import type { ServerStatus, StatusResponse } from '../../../../types/status'; +import type { StatusResponse, ServiceStatus, ServiceStatusLevel } from '../../../../types/status'; import type { HttpSetup } from '../../../http'; import type { NotificationsSetup } from '../../../notifications'; import type { DataType } from '../lib'; @@ -22,13 +22,18 @@ export interface Metric { export interface FormattedStatus { id: string; state: { - id: string; + id: ServiceStatusLevel; title: string; message: string; uiColor: string; }; } +interface StatusUIAttributes { + title: string; + uiColor: string; +} + /** * Returns an object of any keys that should be included for metrics. */ @@ -86,18 +91,47 @@ function formatMetrics({ metrics }: StatusResponse): Metric[] { /** * Reformat the backend data to make the frontend views simpler. */ -function formatStatus(status: ServerStatus): FormattedStatus { +function formatStatus(id: string, status: ServiceStatus): FormattedStatus { + const { title, uiColor } = STATUS_LEVEL_UI_ATTRS[status.level]; + return { - id: status.id, + id, state: { - id: status.state, - title: status.title, - message: status.message, - uiColor: status.uiColor, + id: status.level, + message: status.summary, + title, + uiColor, }, }; } +const STATUS_LEVEL_UI_ATTRS: Record = { + critical: { + title: i18n.translate('core.status.redTitle', { + defaultMessage: 'Red', + }), + uiColor: 'danger', + }, + unavailable: { + title: i18n.translate('core.status.redTitle', { + defaultMessage: 'Red', + }), + uiColor: 'danger', + }, + degraded: { + title: i18n.translate('core.status.yellowTitle', { + defaultMessage: 'Yellow', + }), + uiColor: 'warning', + }, + available: { + title: i18n.translate('core.status.greenTitle', { + defaultMessage: 'Green', + }), + uiColor: 'secondary', + }, +}; + /** * Get the status from the server API and format it for display. */ @@ -111,7 +145,7 @@ export async function loadStatus({ let response: StatusResponse; try { - response = await http.get('/api/status'); + response = await http.get('/api/status', { query: { v8format: true } }); } catch (e) { // API returns a 503 response if not all services are available. // In this case, we want to treat this as a successful API call, so that we can @@ -144,8 +178,15 @@ export async function loadStatus({ return { name: response.name, version: response.version, - statuses: response.status.statuses.map(formatStatus), - serverState: formatStatus(response.status.overall).state, + statuses: [ + ...Object.entries(response.status.core).map(([serviceName, status]) => + formatStatus(`core:${serviceName}`, status) + ), + ...Object.entries(response.status.plugins).map(([pluginName, status]) => + formatStatus(`plugin:${pluginName}`, status) + ), + ], + serverState: formatStatus('overall', response.status.overall).state, metrics: formatMetrics(response), }; } diff --git a/src/core/server/status/routes/status.ts b/src/core/server/status/routes/status.ts index 72f639231996f..43a596bd1e0ec 100644 --- a/src/core/server/status/routes/status.ts +++ b/src/core/server/status/routes/status.ts @@ -16,6 +16,7 @@ import { ServiceStatus, CoreStatus, ServiceStatusLevels } from '../types'; import { PluginName } from '../../plugins'; import { calculateLegacyStatus, LegacyStatusInfo } from '../legacy_status'; import { PackageInfo } from '../../config'; +import { StatusResponse } from '../../../types/status'; const SNAPSHOT_POSTFIX = /-SNAPSHOT$/; @@ -41,55 +42,9 @@ interface StatusInfo { plugins: Record; } -interface StatusHttpBody { - name: string; - uuid: string; - version: { - number: string; - build_hash: string; - build_number: number; - build_snapshot: boolean; - }; +// The moment we remove support for the LegacyStatusInfo, we can use the StatusResponse straight away. +interface StatusHttpBody extends Omit { status: StatusInfo | LegacyStatusInfo; - metrics: { - /** ISO-8601 date string w/o timezone */ - last_updated: string; - collection_interval_in_millis: number; - process: { - memory: { - heap: { - total_in_bytes: number; - used_in_bytes: number; - size_limit: number; - }; - resident_set_size_in_bytes: number; - }; - event_loop_delay: number; - pid: number; - uptime_in_millis: number; - }; - os: { - load: Record; - memory: { - total_in_bytes: number; - used_in_bytes: number; - free_in_bytes: number; - }; - uptime_in_millis: number; - platform: string; - platformRelease: string; - }; - response_times: { - max_in_millis: number; - }; - requests: { - total: number; - disconnects: number; - statusCodes: Record; - status_codes: Record; - }; - concurrent_connections: number; - }; } export const registerStatusRoute = ({ router, config, metrics, status }: Deps) => { diff --git a/src/core/types/status.ts b/src/core/types/status.ts index 5a59e46ef7d4d..58c954fb70050 100644 --- a/src/core/types/status.ts +++ b/src/core/types/status.ts @@ -6,36 +6,58 @@ * Side Public License, v 1. */ -import type { OpsMetrics } from '../server/metrics'; - -export interface ServerStatus { - id: string; - title: string; - state: string; - message: string; - uiColor: string; - icon?: string; - since?: string; +import type { + CoreStatus as CoreStatusFromServer, + ServiceStatus as ServiceStatusFromServer, + ServiceStatusLevel as ServiceStatusLevelFromServer, + OpsMetrics, +} from '../server'; + +/** + * We need this type to convert the object `ServiceStatusLevel` to a union of the possible strings. + * This is because of the "stringification" that occurs when serving HTTP requests. + */ +export type ServiceStatusLevel = ReturnType; + +export interface ServiceStatus extends Omit { + level: ServiceStatusLevel; } -export type ServerMetrics = OpsMetrics & { +/** + * Copy all the services listed in CoreStatus with their specific ServiceStatus declarations + * but overwriting the `level` to its stringified version. + */ +export type CoreStatus = { + [ServiceName in keyof CoreStatusFromServer]: Omit & { + level: ServiceStatusLevel; + }; +}; + +export type ServerMetrics = Omit & { + last_updated: string; collection_interval_in_millis: number; + requests: { + status_codes: Record; + }; }; export interface ServerVersion { number: string; build_hash: string; - build_number: string; - build_snapshot: string; + build_number: number; + build_snapshot: boolean; +} + +export interface StatusInfo { + overall: ServiceStatus; + core: CoreStatus; + plugins: Record; } export interface StatusResponse { name: string; uuid: string; version: ServerVersion; - status: { - overall: ServerStatus; - statuses: ServerStatus[]; - }; + status: StatusInfo; metrics: ServerMetrics; } From 947657118c474b55a374500eb6867dd58b9ebf18 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 11 Aug 2021 11:02:09 -0500 Subject: [PATCH 083/104] [Security Solution, Timelines] Replace more legacy elasticsearch types (#108087) * Replace more legacy elasticsearch types * Handle possibly undefined response fields These are both number | undefined, so we default to 0 if we need a value. Fixes the type errors resulting from the previous type changes. --- .../detection_engine/alerts/use_fetch_ecs_alerts_data.ts | 8 ++++---- .../timelines/public/container/use_update_alerts.ts | 5 +++-- .../public/hooks/use_status_bulk_action_items.tsx | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_fetch_ecs_alerts_data.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_fetch_ecs_alerts_data.ts index 8af4781284924..b082d90fc1488 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_fetch_ecs_alerts_data.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_fetch_ecs_alerts_data.ts @@ -5,7 +5,7 @@ * 2.0. */ import { useEffect, useState } from 'react'; -import { SearchResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; import { isEmpty } from 'lodash'; import { @@ -37,7 +37,7 @@ export const useFetchEcsAlertsData = ({ try { setIsLoading(true); const alertResponse = await KibanaServices.get().http.fetch< - SearchResponse<{ '@timestamp': string; [key: string]: unknown }> + estypes.SearchResponse<{ '@timestamp': string; [key: string]: unknown }> >(DETECTION_ENGINE_QUERY_SIGNALS_URL, { method: 'POST', body: JSON.stringify(buildAlertsQuery(alertIds ?? [])), @@ -45,10 +45,10 @@ export const useFetchEcsAlertsData = ({ setAlertEcsData( alertResponse?.hits.hits.reduce( - (acc, { _id, _index, _source }) => [ + (acc, { _id, _index, _source = {} }) => [ ...acc, { - ...formatAlertToEcsSignal(_source as {}), + ...formatAlertToEcsSignal(_source), _id, _index, timestamp: _source['@timestamp'], diff --git a/x-pack/plugins/timelines/public/container/use_update_alerts.ts b/x-pack/plugins/timelines/public/container/use_update_alerts.ts index 7576c831554cd..8e18281e78824 100644 --- a/x-pack/plugins/timelines/public/container/use_update_alerts.ts +++ b/x-pack/plugins/timelines/public/container/use_update_alerts.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { UpdateDocumentByQueryResponse } from 'elasticsearch'; +import type { estypes } from '@elastic/elasticsearch'; + import { useKibana } from '../../../../../src/plugins/kibana_react/public'; import { AlertStatus } from '../../../timelines/common'; @@ -24,7 +25,7 @@ export const useUpdateAlertsStatus = (): { updateAlertStatus: (params: { query: object; status: AlertStatus; - }) => Promise; + }) => Promise; } => { const { http } = useKibana().services; diff --git a/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx b/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx index 69d7ad36324de..426884ef8caae 100644 --- a/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx +++ b/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx @@ -57,11 +57,11 @@ export const useStatusBulkActionItems = ({ // TODO: Only delete those that were successfully updated from updatedRules setEventsDeleted({ eventIds, isDeleted: true }); - if (response.version_conflicts > 0 && eventIds.length === 1) { + if (response.version_conflicts && eventIds.length === 1) { throw new Error(i18n.BULK_ACTION_FAILED_SINGLE_ALERT); } - onUpdateSuccess(response.updated, response.version_conflicts, status); + onUpdateSuccess(response.updated ?? 0, response.version_conflicts ?? 0, status); } catch (error) { onUpdateFailure(status, error); } finally { From ae73cf8416db2e36173565ce83440b40ecac7b41 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Wed, 11 Aug 2021 09:11:29 -0700 Subject: [PATCH 084/104] [APM] Displays callout when transaction events are used instead of aggregrated metrics (#108080) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [APM] Displays callout when transaction events are used instead of aggregrated metrics (#107477) * Apply suggestions from code review Co-authored-by: Søren Louv-Jansen * PR feedback, and isolates the logic for getting the fallback strategy * PR feedback Co-authored-by: Søren Louv-Jansen --- .../app/service_inventory/index.tsx | 8 + .../service_inventory.test.tsx | 174 ++++++++++-------- .../components/app/service_overview/index.tsx | 8 + .../service_overview.test.tsx | 3 + .../components/app/trace_overview/index.tsx | 12 ++ .../app/transaction_overview/index.tsx | 15 +- .../aggregated_transactions_callout/index.tsx | 26 +++ .../use_fallback_to_transactions_fetcher.tsx | 28 +++ .../get_fallback_to_transactions.ts | 36 ++++ .../lib/helpers/get_bucket_size/index.ts | 2 +- .../apm/server/lib/helpers/setup_request.ts | 5 +- .../server/routes/fallback_to_transactions.ts | 36 ++++ .../get_global_apm_server_route_repository.ts | 4 +- .../tests/metrics_charts/metrics_charts.ts | 4 +- 14 files changed, 275 insertions(+), 86 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/shared/aggregated_transactions_callout/index.tsx create mode 100644 x-pack/plugins/apm/public/hooks/use_fallback_to_transactions_fetcher.tsx create mode 100644 x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_fallback_to_transactions.ts create mode 100644 x-pack/plugins/apm/server/routes/fallback_to_transactions.ts diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx index 290e71a23bbb8..2a59a98ee31f7 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx @@ -15,6 +15,8 @@ import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_ import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useLocalStorage } from '../../../hooks/useLocalStorage'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; +import { useFallbackToTransactionsFetcher } from '../../../hooks/use_fallback_to_transactions_fetcher'; +import { AggregatedTransactionsCallout } from '../../shared/aggregated_transactions_callout'; import { useUpgradeAssistantHref } from '../../shared/Links/kibana'; import { SearchBar } from '../../shared/search_bar'; import { getTimeRangeComparison } from '../../shared/time_comparison/get_time_range_comparison'; @@ -155,6 +157,7 @@ function useServicesFetcher() { export function ServiceInventory() { const { core } = useApmPluginContext(); + const { fallbackToTransactions } = useFallbackToTransactionsFetcher(); const { servicesData, servicesStatus, @@ -189,6 +192,11 @@ export function ServiceInventory() { setUserHasDismissedCallout(true)} /> )} + {fallbackToTransactions && ( + + + + )} { it('should render services, when list is not empty', async () => { // mock rest requests - httpGet.mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: true, - items: [ - { - serviceName: 'My Python Service', - agentName: 'python', - transactionsPerMinute: 100, - errorsPerMinute: 200, - avgResponseTime: 300, - environments: ['test', 'dev'], - healthStatus: ServiceHealthStatus.warning, - }, - { - serviceName: 'My Go Service', - agentName: 'go', - transactionsPerMinute: 400, - errorsPerMinute: 500, - avgResponseTime: 600, - environments: [], - severity: ServiceHealthStatus.healthy, - }, - ], - }); + httpGet + .mockResolvedValueOnce({ fallbackToTransactions: false }) + .mockResolvedValueOnce({ + hasLegacyData: false, + hasHistoricalData: true, + items: [ + { + serviceName: 'My Python Service', + agentName: 'python', + transactionsPerMinute: 100, + errorsPerMinute: 200, + avgResponseTime: 300, + environments: ['test', 'dev'], + healthStatus: ServiceHealthStatus.warning, + }, + { + serviceName: 'My Go Service', + agentName: 'go', + transactionsPerMinute: 400, + errorsPerMinute: 500, + avgResponseTime: 600, + environments: [], + severity: ServiceHealthStatus.healthy, + }, + ], + }); const { container, findByText } = render(, { wrapper }); // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); await findByText('My Python Service'); expect(container.querySelectorAll('.euiTableRow')).toHaveLength(2); }); it('should render getting started message, when list is empty and no historical data is found', async () => { - httpGet.mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: false, - items: [], - }); + httpGet + .mockResolvedValueOnce({ fallbackToTransactions: false }) + .mockResolvedValueOnce({ + hasLegacyData: false, + hasHistoricalData: false, + items: [], + }); const { findByText } = render(, { wrapper }); // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); // wait for elements to be rendered const gettingStartedMessage = await findByText( @@ -153,16 +157,18 @@ describe('ServiceInventory', () => { }); it('should render empty message, when list is empty and historical data is found', async () => { - httpGet.mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: true, - items: [], - }); + httpGet + .mockResolvedValueOnce({ fallbackToTransactions: false }) + .mockResolvedValueOnce({ + hasLegacyData: false, + hasHistoricalData: true, + items: [], + }); const { findByText } = render(, { wrapper }); // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); const noServicesText = await findByText('No services found'); expect(noServicesText).not.toBeEmptyDOMElement(); @@ -170,16 +176,18 @@ describe('ServiceInventory', () => { describe('when legacy data is found', () => { it('renders an upgrade migration notification', async () => { - httpGet.mockResolvedValueOnce({ - hasLegacyData: true, - hasHistoricalData: true, - items: [], - }); + httpGet + .mockResolvedValueOnce({ fallbackToTransactions: false }) + .mockResolvedValueOnce({ + hasLegacyData: true, + hasHistoricalData: true, + items: [], + }); render(, { wrapper }); // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); expect(addWarning).toHaveBeenLastCalledWith( expect.objectContaining({ @@ -191,16 +199,18 @@ describe('ServiceInventory', () => { describe('when legacy data is not found', () => { it('does not render an upgrade migration notification', async () => { - httpGet.mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: true, - items: [], - }); + httpGet + .mockResolvedValueOnce({ fallbackToTransactions: false }) + .mockResolvedValueOnce({ + hasLegacyData: false, + hasHistoricalData: true, + items: [], + }); render(, { wrapper }); // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); expect(addWarning).not.toHaveBeenCalled(); }); @@ -208,25 +218,27 @@ describe('ServiceInventory', () => { describe('when ML data is not found', () => { it('does not render the health column', async () => { - httpGet.mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: true, - items: [ - { - serviceName: 'My Python Service', - agentName: 'python', - transactionsPerMinute: 100, - errorsPerMinute: 200, - avgResponseTime: 300, - environments: ['test', 'dev'], - }, - ], - }); + httpGet + .mockResolvedValueOnce({ fallbackToTransactions: false }) + .mockResolvedValueOnce({ + hasLegacyData: false, + hasHistoricalData: true, + items: [ + { + serviceName: 'My Python Service', + agentName: 'python', + transactionsPerMinute: 100, + errorsPerMinute: 200, + avgResponseTime: 300, + environments: ['test', 'dev'], + }, + ], + }); const { queryByText } = render(, { wrapper }); // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); expect(queryByText('Health')).toBeNull(); }); @@ -234,26 +246,28 @@ describe('ServiceInventory', () => { describe('when ML data is found', () => { it('renders the health column', async () => { - httpGet.mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: true, - items: [ - { - serviceName: 'My Python Service', - agentName: 'python', - transactionsPerMinute: 100, - errorsPerMinute: 200, - avgResponseTime: 300, - environments: ['test', 'dev'], - healthStatus: ServiceHealthStatus.warning, - }, - ], - }); + httpGet + .mockResolvedValueOnce({ fallbackToTransactions: false }) + .mockResolvedValueOnce({ + hasLegacyData: false, + hasHistoricalData: true, + items: [ + { + serviceName: 'My Python Service', + agentName: 'python', + transactionsPerMinute: 100, + errorsPerMinute: 200, + avgResponseTime: 300, + environments: ['test', 'dev'], + healthStatus: ServiceHealthStatus.warning, + }, + ], + }); const { queryAllByText } = render(, { wrapper }); // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); expect(queryAllByText('Health').length).toBeGreaterThan(1); }); diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index 620eefda05b27..a261095fbe2f6 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -20,6 +20,8 @@ import { ServiceOverviewErrorsTable } from './service_overview_errors_table'; import { ServiceOverviewInstancesChartAndTable } from './service_overview_instances_chart_and_table'; import { ServiceOverviewThroughputChart } from './service_overview_throughput_chart'; import { TransactionsTable } from '../../shared/transactions_table'; +import { useFallbackToTransactionsFetcher } from '../../../hooks/use_fallback_to_transactions_fetcher'; +import { AggregatedTransactionsCallout } from '../../shared/aggregated_transactions_callout'; /** * The height a chart should be if it's next to a table with 5 rows and a title. @@ -28,6 +30,7 @@ import { TransactionsTable } from '../../shared/transactions_table'; export const chartHeight = 288; export function ServiceOverview() { + const { fallbackToTransactions } = useFallbackToTransactionsFetcher(); const { agentName, serviceName } = useApmServiceContext(); // The default EuiFlexGroup breaks at 768, but we want to break at 992, so we @@ -41,6 +44,11 @@ export function ServiceOverview() { + {fallbackToTransactions && ( + + + + )} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx index 5f77583143d75..b3c1afb32b0fd 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx @@ -140,6 +140,9 @@ describe('ServiceOverview', () => { 'GET /api/apm/services/{serviceName}/annotation/search': { annotations: [], }, + 'GET /api/apm/fallback_to_transactions': { + fallbackToTransactions: false, + }, }; /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx index eed0750a9e390..76e537dc66c0c 100644 --- a/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx @@ -5,12 +5,15 @@ * 2.0. */ +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; import { SearchBar } from '../../shared/search_bar'; import { TraceList } from './trace_list'; +import { useFallbackToTransactionsFetcher } from '../../../hooks/use_fallback_to_transactions_fetcher'; +import { AggregatedTransactionsCallout } from '../../shared/aggregated_transactions_callout'; type TracesAPIResponse = APIReturnType<'GET /api/apm/traces'>; const DEFAULT_RESPONSE: TracesAPIResponse = { @@ -18,6 +21,7 @@ const DEFAULT_RESPONSE: TracesAPIResponse = { }; export function TraceOverview() { + const { fallbackToTransactions } = useFallbackToTransactionsFetcher(); const { urlParams: { environment, kuery, start, end }, } = useUrlParams(); @@ -44,6 +48,14 @@ export function TraceOverview() { <> + {fallbackToTransactions && ( + + + + + + )} + + {fallbackToTransactions && ( + <> + + + + + + + + )} diff --git a/x-pack/plugins/apm/public/components/shared/aggregated_transactions_callout/index.tsx b/x-pack/plugins/apm/public/components/shared/aggregated_transactions_callout/index.tsx new file mode 100644 index 0000000000000..71aeb54d43702 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/aggregated_transactions_callout/index.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiCallOut, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export function AggregatedTransactionsCallout() { + return ( + + {i18n.translate('xpack.apm.aggregatedTransactions.callout.title', { + defaultMessage: `This page is using transaction event data as no metrics events were found in the current time range.`, + })} + + } + iconType="iInCircle" + /> + ); +} diff --git a/x-pack/plugins/apm/public/hooks/use_fallback_to_transactions_fetcher.tsx b/x-pack/plugins/apm/public/hooks/use_fallback_to_transactions_fetcher.tsx new file mode 100644 index 0000000000000..51e5429633eae --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_fallback_to_transactions_fetcher.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useUrlParams } from '../context/url_params_context/use_url_params'; +import { useFetcher } from './use_fetcher'; + +export function useFallbackToTransactionsFetcher() { + const { + urlParams: { kuery, start, end }, + } = useUrlParams(); + const { data = { fallbackToTransactions: false } } = useFetcher( + (callApmApi) => { + return callApmApi({ + endpoint: 'GET /api/apm/fallback_to_transactions', + params: { + query: { kuery, start, end }, + }, + }); + }, + [kuery, start, end] + ); + + return data; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_fallback_to_transactions.ts b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_fallback_to_transactions.ts new file mode 100644 index 0000000000000..d5c7eb596a986 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_fallback_to_transactions.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getSearchAggregatedTransactions } from '.'; +import { SearchAggregatedTransactionSetting } from '../../../../common/aggregated_transactions'; +import { Setup, SetupTimeRange } from '../setup_request'; + +export async function getFallbackToTransactions({ + setup: { config, start, end, apmEventClient }, + kuery, +}: { + setup: Setup & Partial; + kuery?: string; +}): Promise { + const searchAggregatedTransactions = + config['xpack.apm.searchAggregatedTransactions']; + const neverSearchAggregatedTransactions = + searchAggregatedTransactions === SearchAggregatedTransactionSetting.never; + + if (neverSearchAggregatedTransactions) { + return false; + } + + const searchesAggregatedTransactions = await getSearchAggregatedTransactions({ + config, + start, + end, + apmEventClient, + kuery, + }); + return !searchesAggregatedTransactions; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts b/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts index eb82a89811087..863929a2719a5 100644 --- a/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts @@ -12,7 +12,7 @@ import { calculateAuto } from './calculate_auto'; export function getBucketSize({ start, end, - numBuckets = 100, + numBuckets = 50, minBucketSize, }: { start: number; diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index a277b6d4e8c53..ba67a42fbbade 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -62,7 +62,10 @@ interface SetupRequestParams { type InferSetup = Setup & (TParams extends { query: { start: number } } ? { start: number } : {}) & - (TParams extends { query: { end: number } } ? { end: number } : {}); + (TParams extends { query: { end: number } } ? { end: number } : {}) & + (TParams extends { query: Partial } + ? Partial + : {}); export async function setupRequest({ context, diff --git a/x-pack/plugins/apm/server/routes/fallback_to_transactions.ts b/x-pack/plugins/apm/server/routes/fallback_to_transactions.ts new file mode 100644 index 0000000000000..3011d0c3864e8 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/fallback_to_transactions.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as t from 'io-ts'; +import { getFallbackToTransactions } from '../lib/helpers/aggregated_transactions/get_fallback_to_transactions'; +import { setupRequest } from '../lib/helpers/setup_request'; +import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { kueryRt, rangeRt } from './default_api_types'; + +const fallbackToTransactionsRoute = createApmServerRoute({ + endpoint: 'GET /api/apm/fallback_to_transactions', + params: t.partial({ + query: t.intersection([kueryRt, t.partial(rangeRt.props)]), + }), + options: { tags: ['access:apm'] }, + handler: async (resources) => { + const setup = await setupRequest(resources); + const { + params: { + query: { kuery }, + }, + } = resources; + return { + fallbackToTransactions: await getFallbackToTransactions({ setup, kuery }), + }; + }, +}); + +export const fallbackToTransactionsRouteRepository = createApmServerRouteRepository().add( + fallbackToTransactionsRoute +); diff --git a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts index ad4b48c090e58..b66daf80bd763 100644 --- a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts +++ b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts @@ -21,6 +21,7 @@ import { indexPatternRouteRepository } from './index_pattern'; import { metricsRouteRepository } from './metrics'; import { observabilityOverviewRouteRepository } from './observability_overview'; import { rumRouteRepository } from './rum_client'; +import { fallbackToTransactionsRouteRepository } from './fallback_to_transactions'; import { serviceRouteRepository } from './services'; import { serviceMapRouteRepository } from './service_map'; import { serviceNodeRouteRepository } from './service_nodes'; @@ -54,7 +55,8 @@ const getTypedGlobalApmServerRouteRepository = () => { .merge(customLinkRouteRepository) .merge(sourceMapsRouteRepository) .merge(apmFleetRouteRepository) - .merge(backendsRouteRepository); + .merge(backendsRouteRepository) + .merge(fallbackToTransactionsRouteRepository); return repository; }; diff --git a/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.ts b/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.ts index b767eaae1c203..805ec3adeace5 100644 --- a/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.ts +++ b/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.ts @@ -368,7 +368,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .toMatchInline(` Array [ 0, - 15, + 3, ] `); }); @@ -397,7 +397,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .toMatchInline(` Array [ 0, - 187.5, + 37.5, ] `); }); From 0d55d30c9739a2ae23d268c2fdad5d36a69e6c3d Mon Sep 17 00:00:00 2001 From: Byron Hulcher Date: Wed, 11 Aug 2021 12:45:25 -0400 Subject: [PATCH 085/104] [App Search] Migrate Crawl Schedule form (#108066) --- .../automatic_crawl_scheduler.test.tsx | 98 ++++++ .../automatic_crawl_scheduler.tsx | 204 ++++++++++++ .../automatic_crawl_scheduler_logic.test.ts | 307 ++++++++++++++++++ .../automatic_crawl_scheduler_logic.ts | 189 +++++++++++ .../manage_crawls_popover.test.tsx | 40 ++- .../manage_crawls_popover.tsx | 30 +- .../manage_crawls_popover_logic.test.ts | 2 +- .../manage_crawls_popover_logic.ts | 11 +- .../app_search/components/crawler/types.ts | 14 + .../applications/shared/constants/units.ts | 24 ++ .../server/routes/app_search/crawler.test.ts | 129 ++++++++ .../server/routes/app_search/crawler.ts | 46 +++ 12 files changed, 1066 insertions(+), 28 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler_logic.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/constants/units.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler.test.tsx new file mode 100644 index 0000000000000..21a76734fb330 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler.test.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic'; +import '../../../../../__mocks__/shallow_useeffect.mock'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { + EuiButton, + EuiButtonEmpty, + EuiFieldNumber, + EuiForm, + EuiSelect, + EuiSwitch, +} from '@elastic/eui'; + +import { CrawlUnits } from '../../types'; + +import { AutomaticCrawlScheduler } from './automatic_crawl_scheduler'; + +const MOCK_ACTIONS = { + // AutomaticCrawlSchedulerLogic + fetchCrawlSchedule: jest.fn(), + setCrawlFrequency: jest.fn(), + setCrawlUnit: jest.fn(), + saveChanges: jest.fn(), + toggleCrawlAutomatically: jest.fn(), + // ManageCrawlsPopoverLogic + closePopover: jest.fn(), +}; + +const MOCK_VALUES = { + crawlAutomatically: false, + crawlFrequency: 7, + crawlUnit: CrawlUnits.days, + isSubmitting: false, +}; + +describe('AutomaticCrawlScheduler', () => { + let wrapper: ShallowWrapper; + + beforeEach(() => { + setMockActions(MOCK_ACTIONS); + setMockValues(MOCK_VALUES); + + wrapper = shallow(); + }); + + it('calls fetchCrawlSchedule on component load', () => { + expect(MOCK_ACTIONS.fetchCrawlSchedule).toHaveBeenCalled(); + }); + + it('renders', () => { + expect(wrapper.find(EuiForm)).toHaveLength(1); + expect(wrapper.find(EuiFieldNumber)).toHaveLength(1); + expect(wrapper.find(EuiSelect)).toHaveLength(1); + }); + + it('saves changes on form submit', () => { + const preventDefault = jest.fn(); + wrapper.find(EuiForm).simulate('submit', { preventDefault }); + + expect(preventDefault).toHaveBeenCalled(); + expect(MOCK_ACTIONS.saveChanges).toHaveBeenCalled(); + }); + + it('contains a switch that toggles automatic crawling', () => { + wrapper.find(EuiSwitch).simulate('change'); + + expect(MOCK_ACTIONS.toggleCrawlAutomatically).toHaveBeenCalled(); + }); + + it('contains a number field that updates the crawl frequency', () => { + wrapper.find(EuiFieldNumber).simulate('change', { target: { value: '10' } }); + + expect(MOCK_ACTIONS.setCrawlFrequency).toHaveBeenCalledWith(10); + }); + + it('contains a select field that updates the crawl unit', () => { + wrapper.find(EuiSelect).simulate('change', { target: { value: CrawlUnits.weeks } }); + + expect(MOCK_ACTIONS.setCrawlUnit).toHaveBeenCalledWith(CrawlUnits.weeks); + }); + + it('contains a button to close the popover', () => { + expect(wrapper.find(EuiButtonEmpty).prop('onClick')).toEqual(MOCK_ACTIONS.closePopover); + }); + + it('contains a submit button', () => { + expect(wrapper.find(EuiButton).prop('type')).toEqual('submit'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler.tsx new file mode 100644 index 0000000000000..b0ec31d2ff648 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler.tsx @@ -0,0 +1,204 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiButtonEmpty, + EuiFieldNumber, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiLink, + EuiPopoverFooter, + EuiSelect, + EuiSpacer, + EuiSwitch, + EuiText, + htmlIdGenerator, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + DAYS_UNIT_LABEL, + HOURS_UNIT_LABEL, + MONTHS_UNIT_LABEL, + WEEKS_UNIT_LABEL, +} from '../../../../..//shared/constants/units'; +import { CANCEL_BUTTON_LABEL, SAVE_BUTTON_LABEL } from '../../../../../shared/constants'; + +import { DOCS_PREFIX } from '../../../../routes'; +import { CrawlUnits } from '../../types'; + +import { AutomaticCrawlSchedulerLogic } from './automatic_crawl_scheduler_logic'; + +import { ManageCrawlsPopoverLogic } from './manage_crawls_popover_logic'; + +export const AutomaticCrawlScheduler: React.FC = () => { + const { + fetchCrawlSchedule, + setCrawlFrequency, + setCrawlUnit, + saveChanges, + toggleCrawlAutomatically, + } = useActions(AutomaticCrawlSchedulerLogic); + + const { closePopover } = useActions(ManageCrawlsPopoverLogic); + + const { crawlAutomatically, crawlFrequency, crawlUnit, isSubmitting } = useValues( + AutomaticCrawlSchedulerLogic + ); + + useEffect(() => { + fetchCrawlSchedule(); + }, []); + + const formId = htmlIdGenerator('AutomaticCrawlScheduler')(); + + return ( + { + event.preventDefault(); + saveChanges(); + }} + component="form" + id={formId} + > + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.automaticCrawlSchedule.readMoreLink', + { + defaultMessage: 'Read more.', + } + )} + + ), + }} + /> + + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.automaticCrawlSchedule.crawlAutomaticallySwitchLabel', + { + defaultMessage: 'Crawl automatically', + } + )} + + } + onChange={toggleCrawlAutomatically} + compressed + /> + + + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.automaticCrawlSchedule.crawlUnitsPrefix', + { + defaultMessage: 'Every', + } + )} + + + + setCrawlFrequency(parseInt(e.target.value, 10))} + /> + + + setCrawlUnit(e.target.value as CrawlUnits)} + /> + + + + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.automaticCrawlSchedule.scheduleDescription', + { + defaultMessage: 'The crawl schedule applies to every domain on this engine.', + } + )} + + + + + + {CANCEL_BUTTON_LABEL} + + + + {SAVE_BUTTON_LABEL} + + + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler_logic.test.ts new file mode 100644 index 0000000000000..3c95243459a1c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler_logic.test.ts @@ -0,0 +1,307 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + LogicMounter, + mockHttpValues, + mockFlashMessageHelpers, +} from '../../../../../__mocks__/kea_logic'; +import '../../../../__mocks__/engine_logic.mock'; + +jest.mock('./manage_crawls_popover_logic', () => ({ + ManageCrawlsPopoverLogic: { + actions: { + closePopover: jest.fn(), + }, + }, +})); + +import { nextTick } from '@kbn/test/jest'; + +import { CrawlUnits } from '../../types'; + +import { AutomaticCrawlSchedulerLogic } from './automatic_crawl_scheduler_logic'; +import { ManageCrawlsPopoverLogic } from './manage_crawls_popover_logic'; + +describe('AutomaticCrawlSchedulerLogic', () => { + const { mount } = new LogicMounter(AutomaticCrawlSchedulerLogic); + const { http } = mockHttpValues; + const { flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + + expect(AutomaticCrawlSchedulerLogic.values).toEqual({ + crawlAutomatically: false, + crawlFrequency: 7, + crawlUnit: CrawlUnits.days, + isSubmitting: false, + }); + }); + + describe('actions', () => { + describe('clearCrawlSchedule', () => { + it('sets crawl schedule related values to their defaults', () => { + mount({ + crawlAutomatically: true, + crawlFrequency: 36, + crawlUnit: CrawlUnits.hours, + }); + + AutomaticCrawlSchedulerLogic.actions.clearCrawlSchedule(); + + expect(AutomaticCrawlSchedulerLogic.values).toMatchObject({ + crawlAutomatically: false, + crawlFrequency: 7, + crawlUnit: CrawlUnits.days, + }); + }); + }); + + describe('toggleCrawlAutomatically', () => { + it('toggles the ability to crawl automatically', () => { + mount({ + crawlAutomatically: false, + }); + + AutomaticCrawlSchedulerLogic.actions.toggleCrawlAutomatically(); + + expect(AutomaticCrawlSchedulerLogic.values.crawlAutomatically).toEqual(true); + + AutomaticCrawlSchedulerLogic.actions.toggleCrawlAutomatically(); + + expect(AutomaticCrawlSchedulerLogic.values.crawlAutomatically).toEqual(false); + }); + }); + + describe('onDoneSubmitting', () => { + mount({ + isSubmitting: true, + }); + + AutomaticCrawlSchedulerLogic.actions.onDoneSubmitting(); + + expect(AutomaticCrawlSchedulerLogic.values.isSubmitting).toEqual(false); + }); + + describe('setCrawlFrequency', () => { + it("sets the crawl schedule's frequency", () => { + mount({ + crawlFrequency: 36, + }); + + AutomaticCrawlSchedulerLogic.actions.setCrawlFrequency(12); + + expect(AutomaticCrawlSchedulerLogic.values.crawlFrequency).toEqual(12); + }); + }); + + describe('setCrawlSchedule', () => { + it("sets the crawl schedule's frequency and unit, and enables crawling automatically", () => { + mount(); + + AutomaticCrawlSchedulerLogic.actions.setCrawlSchedule({ + frequency: 3, + unit: CrawlUnits.hours, + }); + + expect(AutomaticCrawlSchedulerLogic.values).toMatchObject({ + crawlAutomatically: true, + crawlFrequency: 3, + crawlUnit: CrawlUnits.hours, + }); + }); + }); + + describe('setCrawlUnit', () => { + it("sets the crawl schedule's unit", () => { + mount({ + crawlUnit: CrawlUnits.months, + }); + + AutomaticCrawlSchedulerLogic.actions.setCrawlUnit(CrawlUnits.weeks); + + expect(AutomaticCrawlSchedulerLogic.values.crawlUnit).toEqual(CrawlUnits.weeks); + }); + }); + }); + + describe('listeners', () => { + describe('deleteCrawlSchedule', () => { + it('resets the states of the crawl scheduler and popover, and shows a toast, on success', async () => { + jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'clearCrawlSchedule'); + jest.spyOn(ManageCrawlsPopoverLogic.actions, 'closePopover'); + jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'onDoneSubmitting'); + http.delete.mockReturnValueOnce(Promise.resolve()); + + AutomaticCrawlSchedulerLogic.actions.deleteCrawlSchedule(); + await nextTick(); + + expect(AutomaticCrawlSchedulerLogic.actions.clearCrawlSchedule).toHaveBeenCalled(); + expect(flashSuccessToast).toHaveBeenCalledWith(expect.any(String)); + expect(ManageCrawlsPopoverLogic.actions.closePopover).toHaveBeenCalled(); + expect(AutomaticCrawlSchedulerLogic.actions.onDoneSubmitting).toHaveBeenCalled(); + }); + + describe('error paths', () => { + it('resets the states of the crawl scheduler and popover on a 404 respose', async () => { + jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'clearCrawlSchedule'); + jest.spyOn(ManageCrawlsPopoverLogic.actions, 'closePopover'); + jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'onDoneSubmitting'); + http.delete.mockReturnValueOnce( + Promise.reject({ + response: { status: 404 }, + }) + ); + + AutomaticCrawlSchedulerLogic.actions.deleteCrawlSchedule(); + await nextTick(); + + expect(AutomaticCrawlSchedulerLogic.actions.clearCrawlSchedule).toHaveBeenCalled(); + expect(ManageCrawlsPopoverLogic.actions.closePopover).toHaveBeenCalled(); + expect(AutomaticCrawlSchedulerLogic.actions.onDoneSubmitting).toHaveBeenCalled(); + }); + + it('flashes an error on a non-404 respose', async () => { + jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'onDoneSubmitting'); + http.delete.mockReturnValueOnce( + Promise.reject({ + response: { status: 500 }, + }) + ); + + AutomaticCrawlSchedulerLogic.actions.deleteCrawlSchedule(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith({ + response: { status: 500 }, + }); + expect(AutomaticCrawlSchedulerLogic.actions.onDoneSubmitting).toHaveBeenCalled(); + }); + }); + }); + + describe('fetchCrawlSchedule', () => { + it('set the state of the crawl scheduler on success', async () => { + jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'setCrawlSchedule'); + http.get.mockReturnValueOnce( + Promise.resolve({ + unit: CrawlUnits.days, + frequency: '30', + }) + ); + + AutomaticCrawlSchedulerLogic.actions.fetchCrawlSchedule(); + await nextTick(); + + expect(AutomaticCrawlSchedulerLogic.actions.setCrawlSchedule).toHaveBeenCalledWith({ + unit: CrawlUnits.days, + frequency: '30', + }); + }); + + describe('error paths', () => { + it('resets the states of the crawl scheduler on a 404 respose', async () => { + jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'clearCrawlSchedule'); + http.get.mockReturnValueOnce( + Promise.reject({ + response: { status: 404 }, + }) + ); + + AutomaticCrawlSchedulerLogic.actions.fetchCrawlSchedule(); + await nextTick(); + + expect(AutomaticCrawlSchedulerLogic.actions.clearCrawlSchedule).toHaveBeenCalled(); + }); + + it('flashes an error on a non-404 respose', async () => { + http.get.mockReturnValueOnce( + Promise.reject({ + response: { status: 500 }, + }) + ); + + AutomaticCrawlSchedulerLogic.actions.fetchCrawlSchedule(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith({ + response: { status: 500 }, + }); + }); + }); + }); + + describe('saveChanges', () => { + it('updates or creates a crawl schedule if the user has chosen to crawl automatically', () => { + jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'submitCrawlSchedule'); + mount({ + crawlAutomatically: true, + }); + + AutomaticCrawlSchedulerLogic.actions.saveChanges(); + + expect(AutomaticCrawlSchedulerLogic.actions.submitCrawlSchedule); + }); + + it('deletes the crawl schedule if the user has chosen to disable automatic crawling', () => { + jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'deleteCrawlSchedule'); + mount({ + crawlAutomatically: false, + }); + + AutomaticCrawlSchedulerLogic.actions.saveChanges(); + + expect(AutomaticCrawlSchedulerLogic.actions.deleteCrawlSchedule); + }); + }); + + describe('submitCrawlSchedule', () => { + it('sets the states of the crawl scheduler and closes the popover on success', async () => { + jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'setCrawlSchedule'); + jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'onDoneSubmitting'); + http.put.mockReturnValueOnce( + Promise.resolve({ + unit: CrawlUnits.days, + frequency: 30, + }) + ); + + AutomaticCrawlSchedulerLogic.actions.submitCrawlSchedule(); + await nextTick(); + + expect(AutomaticCrawlSchedulerLogic.actions.setCrawlSchedule).toHaveBeenCalledWith({ + unit: CrawlUnits.days, + frequency: 30, + }); + expect(ManageCrawlsPopoverLogic.actions.closePopover).toHaveBeenCalled(); + expect(AutomaticCrawlSchedulerLogic.actions.onDoneSubmitting).toHaveBeenCalled(); + }); + + it('flashes an error callout if there is an error', async () => { + jest.spyOn(AutomaticCrawlSchedulerLogic.actions, 'onDoneSubmitting'); + http.delete.mockReturnValueOnce( + Promise.reject({ + response: { status: 500 }, + }) + ); + + AutomaticCrawlSchedulerLogic.actions.deleteCrawlSchedule(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith({ + response: { status: 500 }, + }); + expect(AutomaticCrawlSchedulerLogic.actions.onDoneSubmitting).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler_logic.ts new file mode 100644 index 0000000000000..75c403fa35880 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/automatic_crawl_scheduler_logic.ts @@ -0,0 +1,189 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { i18n } from '@kbn/i18n'; + +import { flashAPIErrors, flashSuccessToast } from '../../../../../shared/flash_messages'; +import { HttpLogic } from '../../../../../shared/http'; +import { EngineLogic } from '../../../engine'; +import { CrawlSchedule, CrawlUnits } from '../../types'; + +import { ManageCrawlsPopoverLogic } from './manage_crawls_popover_logic'; + +export interface AutomaticCrawlSchedulerLogicValues { + crawlAutomatically: boolean; + crawlFrequency: CrawlSchedule['frequency']; + crawlUnit: CrawlSchedule['unit']; + isSubmitting: boolean; +} + +const DEFAULT_VALUES: Pick = { + crawlFrequency: 7, + crawlUnit: CrawlUnits.days, +}; + +export interface AutomaticCrawlSchedulerLogicActions { + clearCrawlSchedule(): void; + deleteCrawlSchedule(): void; + disableCrawlAutomatically(): void; + onDoneSubmitting(): void; + enableCrawlAutomatically(): void; + fetchCrawlSchedule(): void; + saveChanges(): void; + setCrawlFrequency( + crawlFrequency: CrawlSchedule['frequency'] + ): { crawlFrequency: CrawlSchedule['frequency'] }; + setCrawlSchedule(crawlSchedule: CrawlSchedule): { crawlSchedule: CrawlSchedule }; + setCrawlUnit(crawlUnit: CrawlSchedule['unit']): { crawlUnit: CrawlSchedule['unit'] }; + submitCrawlSchedule(): void; + toggleCrawlAutomatically(): void; +} + +export const AutomaticCrawlSchedulerLogic = kea< + MakeLogicType +>({ + path: ['enterprise_search', 'app_search', 'crawler', 'automatic_crawl_scheduler'], + actions: () => ({ + clearCrawlSchedule: true, + deleteCrawlSchedule: true, + disableCrawlAutomatically: true, + onDoneSubmitting: true, + enableCrawlAutomatically: true, + fetchCrawlSchedule: true, + saveChanges: true, + setCrawlSchedule: (crawlSchedule: CrawlSchedule) => ({ crawlSchedule }), + submitCrawlSchedule: true, + setCrawlFrequency: (crawlFrequency: string) => ({ crawlFrequency }), + setCrawlUnit: (crawlUnit: CrawlUnits) => ({ crawlUnit }), + toggleCrawlAutomatically: true, + }), + reducers: () => ({ + crawlAutomatically: [ + false, + { + clearCrawlSchedule: () => false, + setCrawlSchedule: () => true, + toggleCrawlAutomatically: (crawlAutomatically) => !crawlAutomatically, + }, + ], + crawlFrequency: [ + DEFAULT_VALUES.crawlFrequency, + { + clearCrawlSchedule: () => DEFAULT_VALUES.crawlFrequency, + setCrawlSchedule: (_, { crawlSchedule: { frequency } }) => frequency, + setCrawlFrequency: (_, { crawlFrequency }) => crawlFrequency, + }, + ], + crawlUnit: [ + DEFAULT_VALUES.crawlUnit, + { + clearCrawlSchedule: () => DEFAULT_VALUES.crawlUnit, + setCrawlSchedule: (_, { crawlSchedule: { unit } }) => unit, + setCrawlUnit: (_, { crawlUnit }) => crawlUnit, + }, + ], + isSubmitting: [ + false, + { + deleteCrawlSchedule: () => true, + onDoneSubmitting: () => false, + submitCrawlSchedule: () => true, + }, + ], + }), + listeners: ({ actions, values }) => ({ + deleteCrawlSchedule: async () => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + const { closePopover } = ManageCrawlsPopoverLogic.actions; + + try { + await http.delete(`/api/app_search/engines/${engineName}/crawler/crawl_schedule`); + actions.clearCrawlSchedule(); + flashSuccessToast( + i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.automaticCrawlScheduler.disableCrawlSchedule.successMessage', + { + defaultMessage: 'Automatic crawling has been disabled.', + } + ) + ); + closePopover(); + } catch (e) { + // A 404 is expected and means the user has no crawl schedule to delete + if (e.response?.status === 404) { + actions.clearCrawlSchedule(); + closePopover(); + } else { + flashAPIErrors(e); + // Keep the popover open + } + } finally { + actions.onDoneSubmitting(); + } + }, + fetchCrawlSchedule: async () => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + try { + const crawlSchedule: CrawlSchedule = await http.get( + `/api/app_search/engines/${engineName}/crawler/crawl_schedule` + ); + actions.setCrawlSchedule(crawlSchedule); + } catch (e) { + // A 404 is expected and means the user does not have crawl schedule + // for this engine. We continue to use the defaults. + if (e.response.status === 404) { + actions.clearCrawlSchedule(); + } else { + flashAPIErrors(e); + } + } + }, + saveChanges: () => { + if (values.crawlAutomatically) { + actions.submitCrawlSchedule(); + } else { + actions.deleteCrawlSchedule(); + } + }, + submitCrawlSchedule: async () => { + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + const { closePopover } = ManageCrawlsPopoverLogic.actions; + + try { + const crawlSchedule: CrawlSchedule = await http.put( + `/api/app_search/engines/${engineName}/crawler/crawl_schedule`, + { + body: JSON.stringify({ + unit: values.crawlUnit, + frequency: values.crawlFrequency, + }), + } + ); + actions.setCrawlSchedule(crawlSchedule); + flashSuccessToast( + i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.automaticCrawlScheduler.submitCrawlSchedule.successMessage', + { + defaultMessage: 'Your automatic crawling schedule has been updated.', + } + ) + ); + closePopover(); + } catch (e) { + flashAPIErrors(e); + } finally { + actions.onDoneSubmitting(); + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover.test.tsx index e97d9a703b668..db6063c2d6bd8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover.test.tsx @@ -9,7 +9,7 @@ import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logi import React from 'react'; -import { shallow } from 'enzyme'; +import { ReactWrapper, shallow } from 'enzyme'; import { EuiButton, @@ -22,6 +22,7 @@ import { import { mountWithIntl } from '../../../../../test_helpers'; import { CrawlerDomain } from '../../types'; +import { AutomaticCrawlScheduler } from './automatic_crawl_scheduler'; import { ManageCrawlsPopover } from './manage_crawls_popover'; const MOCK_ACTIONS = { @@ -57,22 +58,33 @@ describe('ManageCrawlsPopover', () => { expect(wrapper.find(EuiContextMenuPanel)).toHaveLength(0); }); - it('includes a context menu when open', () => { - setMockValues({ - ...MOCK_VALUES, - isOpen: true, - }); + describe('when open', () => { + let wrapper: ReactWrapper; + let menuItems: ReactWrapper; + + beforeEach(() => { + setMockValues({ + ...MOCK_VALUES, + isOpen: true, + }); + + wrapper = mountWithIntl(); - const wrapper = mountWithIntl(); + menuItems = wrapper + .find(EuiContextMenuPanel) + .find(EuiResizeObserver) + .find(EuiContextMenuItem); + }); - const menuItems = wrapper - .find(EuiContextMenuPanel) - .find(EuiResizeObserver) - .find(EuiContextMenuItem); + it('includes a button to reapply crawl rules', () => { + menuItems.at(0).simulate('click'); + expect(MOCK_ACTIONS.reApplyCrawlRules).toHaveBeenCalledWith(MOCK_DOMAIN); + }); - expect(menuItems).toHaveLength(1); + it('includes a form to set a crawl schedule ', () => { + menuItems.at(1).simulate('click'); - menuItems.first().simulate('click'); - expect(MOCK_ACTIONS.reApplyCrawlRules).toHaveBeenCalledWith(MOCK_DOMAIN); + expect(wrapper.find(EuiContextMenuPanel).find(AutomaticCrawlScheduler)); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover.tsx index a980ee73ebcd4..f76c793776858 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover.tsx @@ -15,7 +15,7 @@ import { i18n } from '@kbn/i18n'; import { CrawlerDomain } from '../../types'; -// import { AutomaticCrawlScheduler } from './automatic_crawl_scheduler'; +import { AutomaticCrawlScheduler } from './automatic_crawl_scheduler'; import { ManageCrawlsPopoverLogic } from './manage_crawls_popover_logic'; @@ -40,19 +40,25 @@ export const ManageCrawlsPopover: React.FC = ({ domain icon: 'refresh', onClick: () => reApplyCrawlRules(domain), }, - // { - // name: 'Automatic Crawling', - // icon: 'gear', - // panel: 1, - // }, + { + name: i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.manageCrawlsPopover.automaticCrawlingButtonLabel', + { defaultMessage: 'Automatic crawling' } + ), + icon: 'gear', + panel: 1, + }, ], }, - // { - // id: 1, - // title: 'Automatic Crawling', - // width: 400, - // content: , - // }, + { + id: 1, + title: i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.manageCrawlsPopover.automaticCrawlingTitle', + { defaultMessage: 'Automatic crawling' } + ), + width: 400, + content: , + }, ]; return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover_logic.test.ts index a804164a90dca..f80465fa2c407 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover_logic.test.ts @@ -76,7 +76,7 @@ describe('ManageCrawlsPopoverLogic', () => { } as CrawlerDomain); await nextTick(); - expect(flashSuccessToast).toHaveBeenCalled(); + expect(flashSuccessToast).toHaveBeenCalledWith(expect.any(String)); expect(ManageCrawlsPopoverLogic.actions.closePopover).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover_logic.ts index c269a2ab595c2..50d228ed2fc70 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/manage_crawls_popover/manage_crawls_popover_logic.ts @@ -7,6 +7,8 @@ import { kea, MakeLogicType } from 'kea'; +import { i18n } from '@kbn/i18n'; + import { flashAPIErrors, flashSuccessToast } from '../../../../../shared/flash_messages'; import { HttpLogic } from '../../../../../shared/http'; import { EngineLogic } from '../../../engine'; @@ -55,7 +57,14 @@ export const ManageCrawlsPopoverLogic = kea< body: JSON.stringify(requestBody), }); - flashSuccessToast('Crawl Rules are being re-applied in the background'); + flashSuccessToast( + i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.manageCrawlsPopover.reApplyCrawlRules.successMessage', + { + defaultMessage: 'Crawl rules are being re-applied in the background', + } + ) + ); } catch (e) { flashAPIErrors(e); } finally { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts index 8f1abd6cb055b..0902499feb4ed 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/types.ts @@ -175,3 +175,17 @@ export const readableCrawlerStatuses: { [key in CrawlerStatus]: string } = { { defaultMessage: 'Skipped' } ), }; + +export interface CrawlSchedule { + frequency: number; + unit: CrawlUnits; +} + +// The BE uses a singular form of each unit +// See shared_togo/app/models/shared_togo/crawler/crawl_schedule.rb +export enum CrawlUnits { + hours = 'hour', + days = 'day', + weeks = 'week', + months = 'month', +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/constants/units.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/units.ts new file mode 100644 index 0000000000000..9d8c30c7e57d2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/constants/units.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const HOURS_UNIT_LABEL = i18n.translate('xpack.enterpriseSearch.units.hoursLabel', { + defaultMessage: 'Hours', +}); + +export const DAYS_UNIT_LABEL = i18n.translate('xpack.enterpriseSearch.units.daysLabel', { + defaultMessage: 'Days', +}); + +export const WEEKS_UNIT_LABEL = i18n.translate('xpack.enterpriseSearch.units.weeksLabel', { + defaultMessage: 'Weeks', +}); + +export const MONTHS_UNIT_LABEL = i18n.translate('xpack.enterpriseSearch.units.monthsLabel', { + defaultMessage: 'Months', +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts index 3107afbf46cdd..b0e286077f838 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts @@ -365,4 +365,133 @@ describe('crawler routes', () => { mockRouter.shouldThrow(request); }); }); + + describe('GET /api/app_search/engines/{name}/crawler/crawl_schedule', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/app_search/engines/{name}/crawler/crawl_schedule', + }); + + registerCrawlerRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:name/crawler/crawl_schedule', + }); + }); + + it('validates correctly', () => { + const request = { + params: { name: 'some-engine' }, + }; + mockRouter.shouldValidate(request); + }); + + it('fails validation without a name param', () => { + const request = { + params: {}, + }; + mockRouter.shouldThrow(request); + }); + }); + + describe('PUT /api/app_search/engines/{name}/crawler/crawl_schedule', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'put', + path: '/api/app_search/engines/{name}/crawler/crawl_schedule', + }); + + registerCrawlerRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:name/crawler/crawl_schedule', + }); + }); + + it('validates correctly', () => { + const request = { + params: { name: 'some-engine' }, + body: { unit: 'day', frequency: 7 }, + }; + mockRouter.shouldValidate(request); + }); + + it('fails validation without a name param', () => { + const request = { + params: {}, + body: { unit: 'day', frequency: 7 }, + }; + mockRouter.shouldThrow(request); + }); + + it('fails validation without a unit property in body', () => { + const request = { + params: { name: 'some-engine' }, + body: { frequency: 7 }, + }; + mockRouter.shouldThrow(request); + }); + + it('fails validation without a frequency property in body', () => { + const request = { + params: { name: 'some-engine' }, + body: { unit: 'day' }, + }; + mockRouter.shouldThrow(request); + }); + }); + + describe('DELETE /api/app_search/engines/{name}/crawler/crawl_schedule', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'delete', + path: '/api/app_search/engines/{name}/crawler/crawl_schedule', + }); + + registerCrawlerRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:name/crawler/crawl_schedule', + }); + }); + + it('validates correctly', () => { + const request = { + params: { name: 'some-engine' }, + }; + mockRouter.shouldValidate(request); + }); + + it('fails validation without a name param', () => { + const request = { + params: {}, + }; + mockRouter.shouldThrow(request); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts index 6374b7c83de8f..1ba7885664ff3 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts @@ -158,4 +158,50 @@ export function registerCrawlerRoutes({ path: '/api/as/v0/engines/:name/crawler/process_crawls', }) ); + + router.get( + { + path: '/api/app_search/engines/{name}/crawler/crawl_schedule', + validate: { + params: schema.object({ + name: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:name/crawler/crawl_schedule', + }) + ); + + router.put( + { + path: '/api/app_search/engines/{name}/crawler/crawl_schedule', + validate: { + params: schema.object({ + name: schema.string(), + }), + body: schema.object({ + unit: schema.string(), + frequency: schema.number(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:name/crawler/crawl_schedule', + }) + ); + + router.delete( + { + path: '/api/app_search/engines/{name}/crawler/crawl_schedule', + validate: { + params: schema.object({ + name: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:name/crawler/crawl_schedule', + }) + ); } From 6710a0643d74d01d69ac8ca52d52e0da5e09befa Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Wed, 11 Aug 2021 18:13:48 +0100 Subject: [PATCH 086/104] [ML] Job import/export calendar and filter warnings (#107416) * [ML] Job import/export calendar and filter warnings * fixing translation id * adding export callout * fixing translation id * translation ids * bug in global calendar check * code clean up based on review * updating text * updatiung text * updating apidoc Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/ml/common/types/filters.ts | 24 +++ .../export_job_warning_callout.tsx | 187 ++++++++++++++++++ .../export_jobs_flyout/export_jobs_flyout.tsx | 29 +++ .../export_jobs_flyout/jobs_export_service.ts | 82 ++++++++ .../cannot_import_jobs_callout.tsx | 21 +- .../import_jobs_flyout/import_jobs_flyout.tsx | 4 +- .../import_jobs_flyout/jobs_import_service.ts | 48 ++++- .../services/ml_api_service/filters.ts | 11 +- .../ml/server/models/filter/filter_manager.ts | 17 +- .../plugins/ml/server/models/filter/index.ts | 2 +- x-pack/plugins/ml/server/routes/apidoc.json | 2 + 11 files changed, 391 insertions(+), 36 deletions(-) create mode 100644 x-pack/plugins/ml/common/types/filters.ts create mode 100644 x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_job_warning_callout.tsx diff --git a/x-pack/plugins/ml/common/types/filters.ts b/x-pack/plugins/ml/common/types/filters.ts new file mode 100644 index 0000000000000..200a659a98501 --- /dev/null +++ b/x-pack/plugins/ml/common/types/filters.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface Filter { + filter_id: string; + description?: string; + items: string[]; +} + +interface FilterUsage { + jobs: string[]; + detectors: string[]; +} + +export interface FilterStats { + filter_id: string; + description?: string; + item_count: number; + used_by?: FilterUsage; +} diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_job_warning_callout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_job_warning_callout.tsx new file mode 100644 index 0000000000000..f69df755d2aaa --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_job_warning_callout.tsx @@ -0,0 +1,187 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { EuiCallOut, EuiText, EuiAccordion, EuiSpacer } from '@elastic/eui'; +import type { JobDependencies } from './jobs_export_service'; + +interface Props { + jobs: JobDependencies; +} + +export const ExportJobDependenciesWarningCallout: FC = ({ jobs: allJobs }) => { + const [jobs, jobsWithCalendars, jobsWithFilters] = filterJobs(allJobs); + const usingCalendars = jobsWithCalendars.length > 0; + const usingFilters = jobsWithFilters.length > 0; + + if (usingCalendars === false && usingFilters === false) { + return null; + } + + return ( + <> + + + + + {usingCalendars && ( + + } + > + + + )} + + {usingFilters && ( + + } + > + + + )} + + + + + ); +}; + +const CalendarJobList: FC<{ jobs: JobDependencies }> = ({ jobs }) => ( + <> + {jobs.length > 0 && ( + <> + {jobs.map(({ jobId, calendarIds }) => ( + <> + +
    {jobId}
    + {calendarIds.length > 0 && ( + + )} +
    + + + ))} + + )} + +); + +const FilterJobList: FC<{ jobs: JobDependencies }> = ({ jobs }) => ( + <> + {jobs.length > 0 && ( + <> + {jobs.map(({ jobId, filterIds }) => ( + <> + +
    {jobId}
    + {filterIds.length > 0 && ( + + )} +
    + + + ))} + + )} + +); + +function getTitle(jobs: JobDependencies, calendarCount: number, filterCount: number) { + if (calendarCount > 0 && filterCount === 0) { + return i18n.translate( + 'xpack.ml.importExport.exportFlyout.exportJobDependenciesWarningCallout.calendarOnlyTitle', + { + defaultMessage: + '{jobCount, plural, one {# job uses} other {# jobs use}} {calendarCount, plural, one {a calendar} other {calendars}}', + values: { jobCount: jobs.length, calendarCount }, + } + ); + } + + if (calendarCount === 0 && filterCount > 0) { + return i18n.translate( + 'xpack.ml.importExport.exportFlyout.exportJobDependenciesWarningCallout.filterOnlyTitle', + { + defaultMessage: + '{jobCount, plural, one {# job uses} other {# jobs use}} {filterCount, plural, one {a filter list} other {filter lists}}', + values: { jobCount: jobs.length, filterCount }, + } + ); + } + + return i18n.translate( + 'xpack.ml.importExport.exportFlyout.exportJobDependenciesWarningCallout.filterAndCalendarTitle', + { + defaultMessage: + '{jobCount, plural, one {# job uses} other {# jobs use}} filter lists and calendars', + values: { jobCount: jobs.length }, + } + ); +} + +function filterJobs(jobs: JobDependencies) { + return jobs.reduce( + (acc, job) => { + const usingCalendars = job.calendarIds.length > 0; + const usingFilters = job.filterIds.length > 0; + if (usingCalendars || usingFilters) { + acc[0].push(job); + if (usingCalendars) { + acc[1].push(job); + } + if (usingFilters) { + acc[2].push(job); + } + } + return acc; + }, + [[], [], []] as JobDependencies[] + ); +} diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx index 5ae6159f806ba..ca13307384187 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/export_jobs_flyout/export_jobs_flyout.tsx @@ -27,7 +27,9 @@ import { } from '@elastic/eui'; import { useMlApiContext, useMlKibana } from '../../../contexts/kibana'; +import { ExportJobDependenciesWarningCallout } from './export_job_warning_callout'; import { JobsExportService } from './jobs_export_service'; +import type { JobDependencies } from './jobs_export_service'; import { toastNotificationServiceProvider } from '../../../services/toast_notification_service'; import type { JobType } from '../../../../../common/types/saved_objects'; @@ -66,6 +68,9 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { [toasts] ); + const [jobDependencies, setJobDependencies] = useState([]); + const [selectedJobDependencies, setSelectedJobDependencies] = useState([]); + useEffect( function onFlyoutChange() { setLoadingADJobs(true); @@ -81,6 +86,22 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { .then(({ jobs }) => { setLoadingADJobs(false); setAdJobIds(jobs.map((j) => j.job_id)); + + jobsExportService + .getJobDependencies(jobs) + .then((jobDeps) => { + setJobDependencies(jobDeps); + setLoadingADJobs(false); + }) + .catch((error) => { + const errorTitle = i18n.translate( + 'xpack.ml.importExport.exportFlyout.calendarsError', + { + defaultMessage: 'Could not load calendars', + } + ); + displayErrorToast(error, errorTitle); + }); }) .catch((error) => { const errorTitle = i18n.translate('xpack.ml.importExport.exportFlyout.adJobsError', { @@ -88,6 +109,7 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { }); displayErrorToast(error, errorTitle); }); + getDataFrameAnalytics() .then(({ data_frame_analytics: dataFrameAnalytics }) => { setLoadingDFAJobs(false); @@ -159,6 +181,12 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { switchTab(); }, [selectedJobIds]); + useEffect(() => { + setSelectedJobDependencies( + jobDependencies.filter(({ jobId }) => selectedJobIds.includes(jobId)) + ); + }, [selectedJobIds]); + function switchTab() { const jobType = selectedJobType === 'anomaly-detector' ? 'data-frame-analytics' : 'anomaly-detector'; @@ -195,6 +223,7 @@ export const ExportJobsFlyout: FC = ({ isDisabled, currentTab }) => { + ; +export type FiltersPerJob = Array<{ jobId: string; filterIds: string[] }>; type ExportableConfigs = | Array< @@ -51,4 +55,82 @@ export class JobsExportService { (jobType === 'anomaly-detector' ? 'anomaly_detection' : 'data_frame_analytics') + '_jobs.json' ); } + + public async getJobDependencies(jobs: Job[]): Promise { + const calendars = await this._mlApiServices.calendars(); + + // create a map of all jobs in groups + const groups = jobs.reduce((acc, cur) => { + if (Array.isArray(cur.groups)) { + cur.groups.forEach((g) => { + if (acc[g] === undefined) { + acc[g] = []; + } + acc[g].push(cur.job_id); + }); + } + return acc; + }, {} as Record); + + const isGroup = (id: string) => groups[id] !== undefined; + + // create a map of all calendars in jobs + const calendarsPerJob = calendars.reduce((acc, cur) => { + cur.job_ids.forEach((jId) => { + if (jId === GLOBAL_CALENDAR) { + // add the calendar to all jobs + jobs.forEach((j) => { + if (acc[j.job_id] === undefined) { + acc[j.job_id] = []; + } + acc[j.job_id].push(cur.calendar_id); + }); + } else if (isGroup(jId)) { + // add the calendar to every job in this group + groups[jId].forEach((jId2) => { + if (acc[jId2] === undefined) { + acc[jId2] = []; + } + acc[jId2].push(cur.calendar_id); + }); + } else { + // add the calendar to just this job + if (acc[jId] === undefined) { + acc[jId] = []; + } + acc[jId].push(cur.calendar_id); + } + }); + return acc; + }, {} as Record); + + // create a map of all filters in jobs, + // by extracting the filters from the job's detectors + const filtersPerJob = jobs.reduce((acc, cur) => { + if (acc[cur.job_id] === undefined) { + acc[cur.job_id] = []; + } + cur.analysis_config.detectors.forEach((d) => { + if (d.custom_rules !== undefined) { + d.custom_rules.forEach((r) => { + if (r.scope !== undefined) { + Object.values(r.scope).forEach((scope) => { + acc[cur.job_id].push(scope.filter_id); + }); + } + }); + } + }); + return acc; + }, {} as Record); + + return jobs.map((j) => { + const jobId = j.job_id; + return { + jobId, + calendarIds: [...new Set(calendarsPerJob[jobId])] ?? [], + filterIds: [...new Set(filtersPerJob[jobId])] ?? [], + }; + }); + } } diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx index 7d573462a6c8f..732be345a1ee4 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/cannot_import_jobs_callout.tsx @@ -64,14 +64,23 @@ const SkippedJobList: FC<{ jobs: SkippedJobs[] }> = ({ jobs }) => ( <> {jobs.length > 0 && ( <> - {jobs.map(({ jobId, missingIndices }) => ( + {jobs.map(({ jobId, missingIndices, missingFilters }) => (
    {jobId}
    - + {missingIndices.length > 0 && ( + + )} + {missingFilters.length > 0 && ( + + )}
    ))} diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx index 68f70d05ccc8f..34e77d1b64922 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/import_jobs_flyout.tsx @@ -47,6 +47,7 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => { const { jobs: { bulkCreateJobs }, dataFrameAnalytics: { createDataFrameAnalytics }, + filters: { filters: getFilters }, } = useMlApiContext(); const { services: { @@ -130,7 +131,8 @@ export const ImportJobsFlyout: FC = ({ isDisabled }) => { const validatedJobs = await jobImportService.validateJobs( loadedFile.jobs, loadedFile.jobType, - getIndexPatternTitles + getIndexPatternTitles, + getFilters ); if (loadedFile.jobType === 'anomaly-detector') { diff --git a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/jobs_import_service.ts b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/jobs_import_service.ts index 85028426fa23d..f767ac2f65cc8 100644 --- a/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/jobs_import_service.ts +++ b/x-pack/plugins/ml/public/application/components/import_export_jobs/import_jobs_flyout/jobs_import_service.ts @@ -7,6 +7,7 @@ import type { JobType } from '../../../../../common/types/saved_objects'; import type { Job, Datafeed } from '../../../../../common/types/anomaly_detection_jobs'; +import type { Filter } from '../../../../../common/types/filters'; import type { DataFrameAnalyticsConfig } from '../../../data_frame_analytics/common'; export interface ImportedAdJob { @@ -33,6 +34,7 @@ export interface JobIdObject { export interface SkippedJobs { jobId: string; missingIndices: string[]; + missingFilters: string[]; } function isImportedAdJobs(obj: any): obj is ImportedAdJob[] { @@ -127,17 +129,25 @@ export class JobImportService { public async validateJobs( jobs: ImportedAdJob[] | DataFrameAnalyticsConfig[], type: JobType, - getIndexPatternTitles: (refresh?: boolean) => Promise + getIndexPatternTitles: (refresh?: boolean) => Promise, + getFilters: () => Promise ) { const existingIndexPatterns = new Set(await getIndexPatternTitles()); + const existingFilters = new Set((await getFilters()).map((f) => f.filter_id)); const tempJobs: Array<{ jobId: string; destIndex?: string }> = []; - const tempSkippedJobIds: SkippedJobs[] = []; - - const commonJobs: Array<{ jobId: string; indices: string[]; destIndex?: string }> = + const skippedJobs: SkippedJobs[] = []; + + const commonJobs: Array<{ + jobId: string; + indices: string[]; + filters?: string[]; + destIndex?: string; + }> = type === 'anomaly-detector' ? (jobs as ImportedAdJob[]).map((j) => ({ jobId: j.job.job_id, indices: j.datafeed.indices, + filters: getFilterIdsFromJob(j.job), })) : (jobs as DataFrameAnalyticsConfig[]).map((j) => ({ jobId: j.id, @@ -145,24 +155,46 @@ export class JobImportService { indices: Array.isArray(j.source.index) ? j.source.index : [j.source.index], })); - commonJobs.forEach(({ jobId, indices, destIndex }) => { + commonJobs.forEach(({ jobId, indices, filters = [], destIndex }) => { const missingIndices = indices.filter((i) => existingIndexPatterns.has(i) === false); - if (missingIndices.length === 0) { + const missingFilters = filters.filter((i) => existingFilters.has(i) === false); + + if (missingIndices.length === 0 && missingFilters.length === 0) { tempJobs.push({ jobId, ...(type === 'data-frame-analytics' ? { destIndex } : {}), }); } else { - tempSkippedJobIds.push({ + skippedJobs.push({ jobId, missingIndices, + missingFilters, }); } }); return { jobs: tempJobs, - skippedJobs: tempSkippedJobIds, + skippedJobs, }; } } + +function getFilterIdsFromJob(job: Job) { + const filters = new Set(); + job.analysis_config.detectors.forEach((d) => { + if (d.custom_rules === undefined) { + return; + } + d.custom_rules.forEach((r) => { + if (r.scope === undefined) { + return; + } + Object.values(r.scope).forEach((s) => { + filters.add(s.filter_id); + }); + }); + }); + + return [...filters]; +} diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/filters.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/filters.ts index 0cb02cf682264..cf49031c665e7 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/filters.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/filters.ts @@ -11,18 +11,19 @@ import { http } from '../http_service'; import { basePath } from './index'; +import type { Filter, FilterStats } from '../../../../common/types/filters'; export const filters = { filters(obj?: { filterId?: string }) { const filterId = obj && obj.filterId ? `/${obj.filterId}` : ''; - return http({ + return http({ path: `${basePath()}/filters${filterId}`, method: 'GET', }); }, filtersStats() { - return http({ + return http({ path: `${basePath()}/filters/_stats`, method: 'GET', }); @@ -34,7 +35,7 @@ export const filters = { description, items, }); - return http({ + return http({ path: `${basePath()}/filters`, method: 'PUT', body, @@ -48,7 +49,7 @@ export const filters = { ...(removeItems !== undefined ? { removeItems } : {}), }); - return http({ + return http({ path: `${basePath()}/filters/${filterId}`, method: 'PUT', body, @@ -56,7 +57,7 @@ export const filters = { }, deleteFilter(filterId: string) { - return http({ + return http<{ acknowledged: boolean }>({ path: `${basePath()}/filters/${filterId}`, method: 'DELETE', }); diff --git a/x-pack/plugins/ml/server/models/filter/filter_manager.ts b/x-pack/plugins/ml/server/models/filter/filter_manager.ts index f48b71dedf3ee..a2b71ae572170 100644 --- a/x-pack/plugins/ml/server/models/filter/filter_manager.ts +++ b/x-pack/plugins/ml/server/models/filter/filter_manager.ts @@ -9,14 +9,8 @@ import { estypes } from '@elastic/elasticsearch'; import Boom from '@hapi/boom'; import type { MlClient } from '../../lib/ml_client'; -// import { DetectorRule, DetectorRuleScope } from '../../../common/types/detector_rules'; -import { Job } from '../../../common/types/anomaly_detection_jobs'; - -export interface Filter { - filter_id: string; - description?: string; - items: string[]; -} +import type { Job } from '../../../common/types/anomaly_detection_jobs'; +import type { Filter, FilterStats } from '../../../common/types/filters'; export interface FormFilter { filterId: string; @@ -43,13 +37,6 @@ interface FilterUsage { detectors: string[]; } -interface FilterStats { - filter_id: string; - description?: string; - item_count: number; - used_by: FilterUsage; -} - interface FiltersInUse { [id: string]: FilterUsage; } diff --git a/x-pack/plugins/ml/server/models/filter/index.ts b/x-pack/plugins/ml/server/models/filter/index.ts index ec6f922ada2f3..d5663478b4812 100644 --- a/x-pack/plugins/ml/server/models/filter/index.ts +++ b/x-pack/plugins/ml/server/models/filter/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { FilterManager, Filter, FormFilter, UpdateFilter } from './filter_manager'; +export { FilterManager, FormFilter, UpdateFilter } from './filter_manager'; diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index e238e690e7e97..646a250b96686 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -32,6 +32,7 @@ "GetAnomalyDetectorsStats", "GetAnomalyDetectorsStatsById", "CloseAnomalyDetectorsJob", + "ResetAnomalyDetectorsJob", "ValidateAnomalyDetector", "ForecastAnomalyDetector", "GetRecords", @@ -71,6 +72,7 @@ "ForceStartDatafeeds", "StopDatafeeds", "CloseJobs", + "ResetJobs", "JobsSummary", "JobsWithTimeRange", "GetJobForCloning", From e1843f925219fa5b8f9463e6ba2ef8223f2a55f8 Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Wed, 11 Aug 2021 13:16:54 -0400 Subject: [PATCH 087/104] [Security Solution][Endpoint][Admin] Endpoint List UI update (#106568) --- .../app/home/template_wrapper/index.tsx | 3 + .../components/administration_list_page.tsx | 69 +++++---- .../view/components/search_bar.tsx | 1 + .../pages/endpoint_hosts/view/index.test.tsx | 22 ++- .../pages/endpoint_hosts/view/index.tsx | 139 +++++++++++------- .../view/event_filters_list_page.tsx | 1 - .../trusted_apps/view/trusted_apps_page.tsx | 1 - .../apps/endpoint/endpoint_list.ts | 44 +++--- 8 files changed, 157 insertions(+), 123 deletions(-) diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx index abffe54ab761c..67d9943fabe83 100644 --- a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx @@ -75,6 +75,9 @@ export const SecuritySolutionTemplateWrapper: React.FC theme.eui.euiZNavigation + 1}; - } -`; interface AdministrationListPageProps { - beta: boolean; title: React.ReactNode; subtitle: React.ReactNode; actions?: React.ReactNode; @@ -30,25 +25,41 @@ interface AdministrationListPageProps { } export const AdministrationListPage: FC = memo( - ({ beta, title, subtitle, actions, children, headerBackComponent, ...otherProps }) => { - const badgeOptions = !beta ? undefined : { beta: true, text: BETA_BADGE_LABEL }; + ({ title, subtitle, actions, children, headerBackComponent, ...otherProps }) => { + const header = useMemo(() => { + return ( + + + {headerBackComponent && <>{headerBackComponent}} + + + + {title} + + + + ); + }, [headerBackComponent, title]); - return ( - - - {actions} - + const description = useMemo(() => { + return {subtitle}; + }, [subtitle]); - {children} + return ( + <> + + + {children} - + ); } ); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx index 1cbd45633b91e..e5ae0bc8aa4d5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx @@ -61,6 +61,7 @@ export const AdminSearchBar = memo(() => { timeHistory={timeHistory} onQuerySubmit={onQuerySubmit} isLoading={false} + iconType="search" showFilterBar={false} showDatePicker={false} showQueryBar={true} diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 9f72f64d3efcf..c054f7d06dde3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -25,7 +25,7 @@ import { HostStatus, } from '../../../../../common/endpoint/types'; import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; -import { POLICY_STATUS_TO_TEXT } from './host_constants'; +import { POLICY_STATUS_TO_HEALTH_COLOR, POLICY_STATUS_TO_TEXT } from './host_constants'; import { mockPolicyResultList } from '../../policy/store/test_mock_utils'; import { getEndpointDetailsPath } from '../../../common/routing'; import { KibanaServices, useKibana, useToasts } from '../../../../common/lib/kibana'; @@ -337,7 +337,7 @@ describe('when on the endpoint list page', () => { await middlewareSpy.waitForAction('serverReturnedEndpointList'); }); const total = await renderResult.findByTestId('endpointListTableTotal'); - expect(total.textContent).toEqual('5 Hosts'); + expect(total.textContent).toEqual('Showing 5 endpoints'); }); it('should display correct status', async () => { const renderResult = render(); @@ -380,18 +380,14 @@ describe('when on the endpoint list page', () => { const policyStatuses = await renderResult.findAllByTestId('rowPolicyStatus'); policyStatuses.forEach((status, index) => { - const policyStatusToRGBColor: Array<[string, string]> = [ - ['Success', 'background-color: rgb(109, 204, 177);'], - ['Warning', 'background-color: rgb(241, 216, 111);'], - ['Failure', 'background-color: rgb(255, 126, 98);'], - ['Unsupported', 'background-color: rgb(211, 218, 230);'], - ]; - const policyStatusStyleMap: ReadonlyMap = new Map( - policyStatusToRGBColor - ); - const expectedStatusColor: string = policyStatusStyleMap.get(status.textContent!) ?? ''; expect(status.textContent).toEqual(POLICY_STATUS_TO_TEXT[generatedPolicyStatuses[index]]); - expect(status.getAttribute('style')).toMatch(expectedStatusColor); + expect( + status.querySelector( + `[data-euiicon-type][color=${ + POLICY_STATUS_TO_HEALTH_COLOR[generatedPolicyStatuses[index]] + }]` + ) + ).not.toBeNull(); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 9b745039cac8b..8af1498c24318 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -5,14 +5,15 @@ * 2.0. */ -import React, { useMemo, useCallback, memo, useContext, useEffect, useState } from 'react'; +import React, { useMemo, useCallback, memo, useEffect, useState } from 'react'; +import styled from 'styled-components'; import { EuiHorizontalRule, EuiBasicTable, EuiBasicTableColumn, EuiText, EuiLink, - EuiBadge, + EuiHealth, EuiToolTip, EuiSelectableProps, EuiSuperDatePicker, @@ -26,12 +27,11 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { createStructuredSelector } from 'reselect'; import { useDispatch } from 'react-redux'; -import { ThemeContext } from 'styled-components'; import { EndpointDetailsFlyout } from './details'; import * as selectors from '../store/selectors'; import { useEndpointSelector } from './hooks'; import { isPolicyOutOfDate } from '../utils'; -import { POLICY_STATUS_TO_BADGE_COLOR, POLICY_STATUS_TO_TEXT } from './host_constants'; +import { POLICY_STATUS_TO_HEALTH_COLOR, POLICY_STATUS_TO_TEXT } from './host_constants'; import { useNavigateByRouterEventHandler } from '../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; import { CreateStructuredSelector } from '../../../../common/store'; import { Immutable, HostInfo } from '../../../../../common/endpoint/types'; @@ -64,32 +64,28 @@ import { metadataTransformPrefix } from '../../../../../common/endpoint/constant const MAX_PAGINATED_ITEM = 9999; const TRANSFORM_URL = '/data/transform'; +const StyledDatePicker = styled.div` + .euiFormControlLayout--group { + background-color: rgba(0, 119, 204, 0.2); + } + + .euiDatePickerRange--readOnly { + background-color: ${(props) => props.theme.eui.euiFormBackgroundColor}; + } +`; const EndpointListNavLink = memo<{ name: string; href: string; route: string; - isBadge?: boolean; dataTestSubj: string; -}>(({ name, href, route, isBadge = false, dataTestSubj }) => { +}>(({ name, href, route, dataTestSubj }) => { const clickHandler = useNavigateByRouterEventHandler(route); - const theme = useContext(ThemeContext); - return isBadge ? ( - // eslint-disable-next-line @elastic/eui/href-or-on-click - - {name} - - ) : ( + return ( // eslint-disable-next-line @elastic/eui/href-or-on-click @@ -269,7 +265,7 @@ export const EndpointList = () => { const columns: Array>> = useMemo(() => { const lastActiveColumnName = i18n.translate('xpack.securitySolution.endpoint.list.lastActive', { - defaultMessage: 'Last Active', + defaultMessage: 'Last active', }); return [ @@ -277,7 +273,7 @@ export const EndpointList = () => { field: 'metadata', width: '15%', name: i18n.translate('xpack.securitySolution.endpoint.list.hostname', { - defaultMessage: 'Hostname', + defaultMessage: 'Endpoint', }), render: ({ host: { hostname }, agent: { id } }: HostInfo['metadata']) => { const toRoutePath = getEndpointDetailsPath( @@ -305,7 +301,7 @@ export const EndpointList = () => { field: 'host_status', width: '14%', name: i18n.translate('xpack.securitySolution.endpoint.list.hostStatus', { - defaultMessage: 'Agent Status', + defaultMessage: 'Agent status', }), // eslint-disable-next-line react/display-name render: (hostStatus: HostInfo['host_status'], endpointInfo) => { @@ -318,7 +314,7 @@ export const EndpointList = () => { field: 'metadata.Endpoint.policy.applied', width: '15%', name: i18n.translate('xpack.securitySolution.endpoint.list.policy', { - defaultMessage: 'Integration Policy', + defaultMessage: 'Policy', }), truncateText: true, // eslint-disable-next-line react/display-name @@ -339,6 +335,7 @@ export const EndpointList = () => { color="subdued" size="xs" style={{ whiteSpace: 'nowrap', ...PAD_LEFT }} + className="eui-textTruncate" data-test-subj="policyListRevNo" > { field: 'metadata.Endpoint.policy.applied', width: '9%', name: i18n.translate('xpack.securitySolution.endpoint.list.policyStatus', { - defaultMessage: 'Policy Status', + defaultMessage: 'Policy status', }), render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied'], item: HostInfo) => { const toRoutePath = getEndpointDetailsPath({ @@ -369,19 +366,23 @@ export const EndpointList = () => { }); const toRouteUrl = getAppUrl({ path: toRoutePath }); return ( - - - + + + + ); }, }, @@ -389,24 +390,36 @@ export const EndpointList = () => { field: 'metadata.host.os.name', width: '9%', name: i18n.translate('xpack.securitySolution.endpoint.list.os', { - defaultMessage: 'Operating System', + defaultMessage: 'OS', }), - truncateText: true, + // eslint-disable-next-line react/display-name + render: (os: string) => { + return ( + + +

    {os}

    +
    +
    + ); + }, }, { field: 'metadata.host.ip', width: '12%', name: i18n.translate('xpack.securitySolution.endpoint.list.ip', { - defaultMessage: 'IP Address', + defaultMessage: 'IP address', }), // eslint-disable-next-line react/display-name render: (ip: string[]) => { return ( - - - + + +

    {ip.toString().replace(',', ', ')} - +

    ); @@ -418,6 +431,16 @@ export const EndpointList = () => { name: i18n.translate('xpack.securitySolution.endpoint.list.endpointVersion', { defaultMessage: 'Version', }), + // eslint-disable-next-line react/display-name + render: (version: string) => { + return ( + + +

    {version}

    +
    +
    + ); + }, }, { field: 'metadata.@timestamp', @@ -592,7 +615,6 @@ export const EndpointList = () => { return ( { subtitle={ } > @@ -647,15 +669,18 @@ export const EndpointList = () => {
    )} - + + +
    @@ -666,13 +691,13 @@ export const EndpointList = () => { {totalItemCount > MAX_PAGINATED_ITEM + 1 ? ( ) : ( )} diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx index 3c537320bc92a..8466e19100f73 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx @@ -188,7 +188,6 @@ export const EventFiltersListPage = memo(() => { return ( { return ( { const expectedData = [ [ - 'Hostname', - 'Agent Status', - 'Integration Policy', - 'Policy Status', - 'Operating System', - 'IP Address', + 'Endpoint', + 'Agent status', + 'Policy', + 'Policy status', + 'OS', + 'IP address', 'Version', - 'Last Active', + 'Last active', 'Actions', ], [ @@ -186,7 +186,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'Policy Status', 'IP Address', 'Hostname', - 'Sensor Version', + 'Version', ]; const keys = await pageObjects.endpoint.endpointFlyoutDescriptionKeys( 'endpointDetailsFlyout' @@ -233,14 +233,14 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await querySubmitButton.click(); const expectedDataFromQuery = [ [ - 'Hostname', - 'Agent Status', - 'Integration Policy', - 'Policy Status', - 'Operating System', - 'IP Address', + 'Endpoint', + 'Agent status', + 'Policy', + 'Policy status', + 'OS', + 'IP address', 'Version', - 'Last Active', + 'Last active', 'Actions', ], ['No items found'], @@ -263,14 +263,14 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await querySubmitButton.click(); const expectedDataFromQuery = [ [ - 'Hostname', - 'Agent Status', - 'Integration Policy', - 'Policy Status', - 'Operating System', - 'IP Address', + 'Endpoint', + 'Agent status', + 'Policy', + 'Policy status', + 'OS', + 'IP address', 'Version', - 'Last Active', + 'Last active', 'Actions', ], [ From c0baf834cc9c097a878e891afa4d7d5c15e45060 Mon Sep 17 00:00:00 2001 From: Vadim Yakhin Date: Wed, 11 Aug 2021 15:27:10 -0300 Subject: [PATCH 088/104] [Enterprise Search] Fix search not working on some table columns on Users and Roles page (#108228) * Fix role mappings table search not working on some columns Not searchable columns had non-string data: arrays, objects. The default implementation of search doesn't perform a search on non-string data. The solution used here is to have a custom search callback that converts the entire role mapping object to string and then checks if user query exists in this string. It was copied and adjusted from example in EUI docs: https://elastic.github.io/eui/#/tabular-content/in-memory-tables#in-memory-table-with-search-callback * Fix copy * Fix the same issue for Users table The search was not performed on Engines/Groups column. Also adjust the variable names in role_mappings_table to closely match variable names in users_table --- .../shared/role_mapping/constants.ts | 2 +- .../role_mapping/role_mappings_table.test.tsx | 27 ++++++++++++++- .../role_mapping/role_mappings_table.tsx | 22 ++++++++++-- .../shared/role_mapping/users_table.test.tsx | 34 +++++++++++++++++-- .../shared/role_mapping/users_table.tsx | 22 ++++++++++-- 5 files changed, 99 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts index 583652de1fa02..25a1e084a3a60 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts @@ -219,7 +219,7 @@ export const ROLE_MAPPINGS_HEADING_BUTTON = i18n.translate( export const ROLE_MAPPINGS_NO_RESULTS_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.roleMapping.noResults.message', - { defaultMessage: 'Create a new role mapping' } + { defaultMessage: 'No matching role mappings found' } ); export const ROLES_DISABLED_TITLE = i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx index 61043aa6ad9a8..003848d1da8c5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.test.tsx @@ -10,8 +10,10 @@ import { wsRoleMapping, asRoleMapping } from './__mocks__/roles'; import React from 'react'; import { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; -import { EuiInMemoryTable, EuiTableHeaderCell } from '@elastic/eui'; +import { EuiInMemoryTable, EuiTableHeaderCell, EuiTableRow } from '@elastic/eui'; +import type { EuiSearchBarProps } from '@elastic/eui'; import { engines } from '../../app_search/__mocks__/engines.mock'; @@ -106,4 +108,27 @@ describe('RoleMappingsTable', () => { `${engines[0].name}, ${engines[1].name} + 1` ); }); + + it('handles search', () => { + const wrapper = mount( + + ); + const roleMappingsTable = wrapper.find('[data-test-subj="RoleMappingsTable"]').first(); + const searchProp = roleMappingsTable.prop('search') as EuiSearchBarProps; + + act(() => { + if (searchProp.onChange) { + searchProp.onChange({ queryText: 'admin' } as any); + } + }); + wrapper.update(); + + expect(wrapper.find(EuiTableRow)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx index a98d36f04b4ad..d6299bc1b3896 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/role_mappings_table.tsx @@ -5,9 +5,10 @@ * 2.0. */ -import React from 'react'; +import React, { useState } from 'react'; import { EuiIconTip, EuiInMemoryTable, EuiBasicTableColumn, EuiLink } from '@elastic/eui'; +import type { EuiSearchBarOnChangeArgs } from '@elastic/eui'; import { ASRoleMapping } from '../../app_search/types'; import { WSRoleMapping } from '../../workplace_search/types'; @@ -70,6 +71,8 @@ export const RoleMappingsTable: React.FC = ({ return _rm; }) as SharedRoleMapping[]; + const [items, setItems] = useState(standardizedRoleMappings); + const attributeNameCol: EuiBasicTableColumn = { field: 'attribute', name: ( @@ -161,7 +164,22 @@ export const RoleMappingsTable: React.FC = ({ pageSize: 10, }; + const onQueryChange = ({ queryText }: EuiSearchBarOnChangeArgs) => { + const filteredItems = standardizedRoleMappings.filter((rm) => { + // JSON.stringify allows us to search all the object fields + // without converting all the nested arrays and objects to strings manually + // Some false-positives are possible, because the search is also performed on + // object keys, but the simplicity of JSON.stringify seems to worth the tradeoff. + const normalizedTableItemString = JSON.stringify(rm).toLowerCase(); + const normalizedQuery = queryText.toLowerCase(); + return normalizedTableItemString.indexOf(normalizedQuery) !== -1; + }); + + setItems(filteredItems); + }; + const search = { + onChange: onQueryChange, box: { incremental: true, fullWidth: false, @@ -173,7 +191,7 @@ export const RoleMappingsTable: React.FC = ({ { expect(cell.find(EuiBadge)).toHaveLength(1); }); + + it('handles search', () => { + const wrapper = mount( + + ); + const roleMappingsTable = wrapper.find('[data-test-subj="UsersTable"]').first(); + const searchProp = roleMappingsTable.prop('search') as EuiSearchBarProps; + + act(() => { + if (searchProp.onChange) { + searchProp.onChange({ queryText: 'admin' } as any); + } + }); + wrapper.update(); + + expect(wrapper.find(EuiTableRow)).toHaveLength(1); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx index 25a9eee38f93f..3b6e2dc440a76 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/users_table.tsx @@ -5,9 +5,10 @@ * 2.0. */ -import React from 'react'; +import React, { useState } from 'react'; import { EuiBadge, EuiBasicTableColumn, EuiInMemoryTable, EuiTextColor } from '@elastic/eui'; +import type { EuiSearchBarOnChangeArgs } from '@elastic/eui'; import { ASRoleMapping } from '../../app_search/types'; import { SingleUserRoleMapping } from '../../shared/types'; @@ -73,6 +74,8 @@ export const UsersTable: React.FC = ({ invitation: user.invitation, })) as unknown) as Array>; + const [items, setItems] = useState(users); + const columns: Array> = [ { field: 'username', @@ -134,7 +137,22 @@ export const UsersTable: React.FC = ({ pageSize: 10, }; + const onQueryChange = ({ queryText }: EuiSearchBarOnChangeArgs) => { + const filteredItems = users.filter((user) => { + // JSON.stringify allows us to search all the object fields + // without converting all the nested arrays and objects to strings manually + // Some false-positives are possible, because the search is also performed on + // object keys, but the simplicity of JSON.stringify seems to worth the tradeoff. + const normalizedTableItemString = JSON.stringify(user).toLowerCase(); + const normalizedQuery = queryText.toLowerCase(); + return normalizedTableItemString.indexOf(normalizedQuery) !== -1; + }); + + setItems(filteredItems); + }; + const search = { + onChange: onQueryChange, box: { incremental: true, fullWidth: false, @@ -147,7 +165,7 @@ export const UsersTable: React.FC = ({ Date: Wed, 11 Aug 2021 14:49:38 -0400 Subject: [PATCH 089/104] [Alerting] Alerting authorization should always exempt `alerts` consumer (#108220) * Reverting changes to genericize exempt consumer id * Adding unit test for find auth filter when user has no privileges --- ...rting_authorization_client_factory.test.ts | 30 -- .../alerting_authorization_client_factory.ts | 3 +- .../alerting_authorization.test.ts | 308 ++++-------------- .../authorization/alerting_authorization.ts | 36 +- .../server/rules_client_factory.test.ts | 9 +- .../alerting/server/rules_client_factory.ts | 3 +- 6 files changed, 87 insertions(+), 302 deletions(-) diff --git a/x-pack/plugins/alerting/server/alerting_authorization_client_factory.test.ts b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.test.ts index 77a15eda79cef..2ba3580745d59 100644 --- a/x-pack/plugins/alerting/server/alerting_authorization_client_factory.test.ts +++ b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.test.ts @@ -75,35 +75,6 @@ test('creates an alerting authorization client with proper constructor arguments auditLogger: expect.any(AlertingAuthorizationAuditLogger), getSpace: expect.any(Function), getSpaceId: expect.any(Function), - exemptConsumerIds: [], - }); - - expect(AlertingAuthorizationAuditLogger).toHaveBeenCalled(); - expect(securityPluginSetup.audit.getLogger).toHaveBeenCalledWith(ALERTS_FEATURE_ID); -}); - -test('creates an alerting authorization client with proper constructor arguments when exemptConsumerIds are specified', async () => { - const factory = new AlertingAuthorizationClientFactory(); - factory.initialize({ - securityPluginSetup, - securityPluginStart, - ...alertingAuthorizationClientFactoryParams, - }); - const request = KibanaRequest.from(fakeRequest); - const { AlertingAuthorizationAuditLogger } = jest.requireMock('./authorization/audit_logger'); - - factory.create(request, ['exemptConsumerA', 'exemptConsumerB']); - - const { AlertingAuthorization } = jest.requireMock('./authorization/alerting_authorization'); - expect(AlertingAuthorization).toHaveBeenCalledWith({ - request, - authorization: securityPluginStart.authz, - ruleTypeRegistry: alertingAuthorizationClientFactoryParams.ruleTypeRegistry, - features: alertingAuthorizationClientFactoryParams.features, - auditLogger: expect.any(AlertingAuthorizationAuditLogger), - getSpace: expect.any(Function), - getSpaceId: expect.any(Function), - exemptConsumerIds: ['exemptConsumerA', 'exemptConsumerB'], }); expect(AlertingAuthorizationAuditLogger).toHaveBeenCalled(); @@ -126,7 +97,6 @@ test('creates an alerting authorization client with proper constructor arguments auditLogger: expect.any(AlertingAuthorizationAuditLogger), getSpace: expect.any(Function), getSpaceId: expect.any(Function), - exemptConsumerIds: [], }); expect(AlertingAuthorizationAuditLogger).toHaveBeenCalled(); diff --git a/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts index 1df67ed8d4b79..27b2d92eba256 100644 --- a/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts +++ b/x-pack/plugins/alerting/server/alerting_authorization_client_factory.ts @@ -45,7 +45,7 @@ export class AlertingAuthorizationClientFactory { this.getSpaceId = options.getSpaceId; } - public create(request: KibanaRequest, exemptConsumerIds: string[] = []): AlertingAuthorization { + public create(request: KibanaRequest): AlertingAuthorization { const { securityPluginSetup, securityPluginStart, features } = this; return new AlertingAuthorization({ authorization: securityPluginStart?.authz, @@ -57,7 +57,6 @@ export class AlertingAuthorizationClientFactory { auditLogger: new AlertingAuthorizationAuditLogger( securityPluginSetup?.audit.getLogger(ALERTS_FEATURE_ID) ), - exemptConsumerIds, }); } } diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts index 6314488af88d7..7f5b06031c18c 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts @@ -37,8 +37,6 @@ const realAuditLogger = new AlertingAuthorizationAuditLogger(); const getSpace = jest.fn(); const getSpaceId = () => 'space1'; -const exemptConsumerIds: string[] = []; - const mockAuthorizationAction = ( type: string, app: string, @@ -235,7 +233,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); expect(getSpace).toHaveBeenCalledWith(request); @@ -251,7 +248,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); await alertAuthorization.ensureAuthorized({ @@ -275,7 +271,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); await alertAuthorization.ensureAuthorized({ @@ -302,7 +297,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); checkPrivileges.mockResolvedValueOnce({ @@ -359,7 +353,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); checkPrivileges.mockResolvedValueOnce({ @@ -402,7 +395,7 @@ describe('AlertingAuthorization', () => { `); }); - test('ensures the user has privileges to execute rules for the specified rule type and operation without consumer when consumer is exempt', async () => { + test('ensures the user has privileges to execute rules for the specified rule type and operation without consumer when consumer is alerts', async () => { const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction< ReturnType @@ -416,7 +409,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds: ['exemptConsumer'], }); checkPrivileges.mockResolvedValueOnce({ @@ -427,7 +419,7 @@ describe('AlertingAuthorization', () => { await alertAuthorization.ensureAuthorized({ ruleTypeId: 'myType', - consumer: 'exemptConsumer', + consumer: 'alerts', operation: WriteOperations.Create, entity: AlertingAuthorizationEntity.Rule, }); @@ -437,7 +429,7 @@ describe('AlertingAuthorization', () => { expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(2); expect(authorization.actions.alerting.get).toHaveBeenCalledWith( 'myType', - 'exemptConsumer', + 'alerts', 'rule', 'create' ); @@ -458,14 +450,14 @@ describe('AlertingAuthorization', () => { "some-user", "myType", 0, - "exemptConsumer", + "alerts", "create", "rule", ] `); }); - test('ensures the user has privileges to execute alerts for the specified rule type and operation without consumer when consumer is exempt', async () => { + test('ensures the user has privileges to execute alerts for the specified rule type and operation without consumer when consumer is alerts', async () => { const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction< ReturnType @@ -479,7 +471,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds: ['exemptConsumer'], }); checkPrivileges.mockResolvedValueOnce({ @@ -490,7 +481,7 @@ describe('AlertingAuthorization', () => { await alertAuthorization.ensureAuthorized({ ruleTypeId: 'myType', - consumer: 'exemptConsumer', + consumer: 'alerts', operation: WriteOperations.Update, entity: AlertingAuthorizationEntity.Alert, }); @@ -500,7 +491,7 @@ describe('AlertingAuthorization', () => { expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(2); expect(authorization.actions.alerting.get).toHaveBeenCalledWith( 'myType', - 'exemptConsumer', + 'alerts', 'alert', 'update' ); @@ -521,7 +512,7 @@ describe('AlertingAuthorization', () => { "some-user", "myType", 0, - "exemptConsumer", + "alerts", "update", "alert", ] @@ -548,7 +539,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); await alertAuthorization.ensureAuthorized({ @@ -614,7 +604,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); await alertAuthorization.ensureAuthorized({ @@ -674,7 +663,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); checkPrivileges.mockResolvedValueOnce({ @@ -733,7 +721,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); checkPrivileges.mockResolvedValueOnce({ @@ -796,7 +783,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); checkPrivileges.mockResolvedValueOnce({ @@ -855,7 +841,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); checkPrivileges.mockResolvedValueOnce({ @@ -947,7 +932,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); const { filter, @@ -970,7 +954,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( AlertingAuthorizationEntity.Rule, @@ -1005,7 +988,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); expect( @@ -1020,11 +1002,54 @@ describe('AlertingAuthorization', () => { ).filter ).toEqual( esKuery.fromKueryExpression( - `((path.to.rule_type_id:myAppAlertType and consumer-field:(myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule_type_id:myOtherAppAlertType and consumer-field:(myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule_type_id:mySecondAppAlertType and consumer-field:(myApp or myOtherApp or myAppWithSubFeature)))` + `((path.to.rule_type_id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule_type_id:myOtherAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule_type_id:mySecondAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)))` ) ); expect(auditLogger.logAuthorizationSuccess).not.toHaveBeenCalled(); }); + test('throws if user has no privileges to any rule type', async () => { + const { authorization } = mockSecurity(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myType', 'myOtherApp', 'rule', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myType', 'myApp', 'rule', 'create'), + authorized: false, + }, + ], + }, + }); + const alertAuthorization = new AlertingAuthorization({ + request, + authorization, + ruleTypeRegistry, + features, + auditLogger, + getSpace, + getSpaceId, + }); + ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); + await expect( + alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationEntity.Rule, { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'path.to.rule_type_id', + consumer: 'consumer-field', + }, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Unauthorized some-user/find"`); + expect(auditLogger.logAuthorizationSuccess).not.toHaveBeenCalled(); + }); test('creates an `ensureRuleTypeIsAuthorized` function which throws if type is unauthorized', async () => { const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction< @@ -1068,7 +1093,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( @@ -1142,7 +1166,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( @@ -1217,7 +1240,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); const { @@ -1270,7 +1292,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); const { filter } = await alertAuthorization.getFindAuthorizationFilter( AlertingAuthorizationEntity.Alert, @@ -1325,89 +1346,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, - }); - ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); - - await expect( - alertAuthorization.filterByRuleTypeAuthorization( - new Set([myAppAlertType, myOtherAppAlertType]), - [WriteOperations.Create], - AlertingAuthorizationEntity.Rule - ) - ).resolves.toMatchInlineSnapshot(` - Set { - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "myApp": Object { - "all": true, - "read": true, - }, - "myAppWithSubFeature": Object { - "all": true, - "read": true, - }, - "myOtherApp": Object { - "all": true, - "read": true, - }, - }, - "defaultActionGroupId": "default", - "enabledInLicense": true, - "id": "myAppAlertType", - "isExportable": true, - "minimumLicenseRequired": "basic", - "name": "myAppAlertType", - "producer": "myApp", - "recoveryActionGroup": Object { - "id": "recovered", - "name": "Recovered", - }, - }, - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "myApp": Object { - "all": true, - "read": true, - }, - "myAppWithSubFeature": Object { - "all": true, - "read": true, - }, - "myOtherApp": Object { - "all": true, - "read": true, - }, - }, - "defaultActionGroupId": "default", - "enabledInLicense": true, - "id": "myOtherAppAlertType", - "isExportable": true, - "minimumLicenseRequired": "basic", - "name": "myOtherAppAlertType", - "producer": "myOtherApp", - "recoveryActionGroup": Object { - "id": "recovered", - "name": "Recovered", - }, - }, - } - `); - }); - - test('augments a list of types with all features and exempt consumer ids when there is no authorization api', async () => { - const alertAuthorization = new AlertingAuthorization({ - request, - ruleTypeRegistry, - features, - auditLogger, - getSpace, - getSpaceId, - exemptConsumerIds: ['exemptConsumerA', 'exemptConsumerB'], }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1423,11 +1361,7 @@ describe('AlertingAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Object { - "exemptConsumerA": Object { - "all": true, - "read": true, - }, - "exemptConsumerB": Object { + "alerts": Object { "all": true, "read": true, }, @@ -1460,11 +1394,7 @@ describe('AlertingAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Object { - "exemptConsumerA": Object { - "all": true, - "read": true, - }, - "exemptConsumerB": Object { + "alerts": Object { "all": true, "read": true, }, @@ -1541,7 +1471,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1578,113 +1507,7 @@ describe('AlertingAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Object { - "myApp": Object { - "all": true, - "read": true, - }, - "myOtherApp": Object { - "all": true, - "read": true, - }, - }, - "defaultActionGroupId": "default", - "enabledInLicense": true, - "id": "myAppAlertType", - "isExportable": true, - "minimumLicenseRequired": "basic", - "name": "myAppAlertType", - "producer": "myApp", - "recoveryActionGroup": Object { - "id": "recovered", - "name": "Recovered", - }, - }, - } - `); - }); - - test('augments a list of types with consumers and exempt consumer ids under which the operation is authorized', async () => { - const { authorization } = mockSecurity(); - const checkPrivileges: jest.MockedFunction< - ReturnType - > = jest.fn(); - authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); - checkPrivileges.mockResolvedValueOnce({ - username: 'some-user', - hasAllRequested: false, - privileges: { - kibana: [ - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'rule', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction( - 'myOtherAppAlertType', - 'myOtherApp', - 'rule', - 'create' - ), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'rule', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'rule', 'create'), - authorized: true, - }, - ], - }, - }); - - const alertAuthorization = new AlertingAuthorization({ - request, - authorization, - ruleTypeRegistry, - features, - auditLogger, - getSpace, - getSpaceId, - exemptConsumerIds: ['exemptConsumerA'], - }); - ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); - - await expect( - alertAuthorization.filterByRuleTypeAuthorization( - new Set([myAppAlertType, myOtherAppAlertType]), - [WriteOperations.Create], - AlertingAuthorizationEntity.Rule - ) - ).resolves.toMatchInlineSnapshot(` - Set { - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "myApp": Object { - "all": true, - "read": true, - }, - }, - "defaultActionGroupId": "default", - "enabledInLicense": true, - "id": "myOtherAppAlertType", - "isExportable": true, - "minimumLicenseRequired": "basic", - "name": "myOtherAppAlertType", - "producer": "myOtherApp", - "recoveryActionGroup": Object { - "id": "recovered", - "name": "Recovered", - }, - }, - Object { - "actionGroups": Array [], - "actionVariables": undefined, - "authorizedConsumers": Object { - "exemptConsumerA": Object { + "alerts": Object { "all": true, "read": true, }, @@ -1713,7 +1536,7 @@ describe('AlertingAuthorization', () => { `); }); - test('authorizes user under exempt consumers when they are authorized by the producer', async () => { + test('authorizes user under the `alerts` consumer when they are authorized by the producer', async () => { const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction< ReturnType @@ -1744,7 +1567,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds: ['exemptConsumerA'], }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1760,7 +1582,7 @@ describe('AlertingAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Object { - "exemptConsumerA": Object { + "alerts": Object { "all": true, "read": true, }, @@ -1850,7 +1672,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1866,6 +1687,10 @@ describe('AlertingAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Object { + "alerts": Object { + "all": false, + "read": true, + }, "myApp": Object { "all": true, "read": true, @@ -1891,6 +1716,10 @@ describe('AlertingAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Object { + "alerts": Object { + "all": false, + "read": true, + }, "myApp": Object { "all": false, "read": true, @@ -1960,7 +1789,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -1976,6 +1804,10 @@ describe('AlertingAuthorization', () => { "actionGroups": Array [], "actionVariables": undefined, "authorizedConsumers": Object { + "alerts": Object { + "all": true, + "read": true, + }, "myApp": Object { "all": true, "read": true, @@ -2067,7 +1899,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); @@ -2142,7 +1973,6 @@ describe('AlertingAuthorization', () => { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts index ed14c2dd7f0ae..101ab675bb553 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts @@ -9,7 +9,7 @@ import Boom from '@hapi/boom'; import { map, mapValues, fromPairs, has } from 'lodash'; import { KibanaRequest } from 'src/core/server'; import { JsonObject } from '@kbn/utility-types'; -import { RuleTypeRegistry } from '../types'; +import { ALERTS_FEATURE_ID, RuleTypeRegistry } from '../types'; import { SecurityPluginSetup } from '../../../security/server'; import { RegistryRuleType } from '../rule_type_registry'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; @@ -71,7 +71,6 @@ export interface ConstructorOptions { getSpace: (request: KibanaRequest) => Promise; getSpaceId: (request: KibanaRequest) => string | undefined; auditLogger: AlertingAuthorizationAuditLogger; - exemptConsumerIds: string[]; authorization?: SecurityPluginSetup['authz']; } @@ -82,7 +81,6 @@ export class AlertingAuthorization { private readonly auditLogger: AlertingAuthorizationAuditLogger; private readonly featuresIds: Promise>; private readonly allPossibleConsumers: Promise; - private readonly exemptConsumerIds: string[]; private readonly spaceId: string | undefined; constructor({ @@ -93,18 +91,12 @@ export class AlertingAuthorization { auditLogger, getSpace, getSpaceId, - exemptConsumerIds, }: ConstructorOptions) { this.request = request; this.authorization = authorization; this.ruleTypeRegistry = ruleTypeRegistry; this.auditLogger = auditLogger; - // List of consumer ids that are exempt from privilege check. This should be used sparingly. - // An example of this is the Rules Management `consumer` as we don't want to have to - // manually authorize each rule type in the management UI. - this.exemptConsumerIds = exemptConsumerIds; - this.spaceId = getSpaceId(request); this.featuresIds = getSpace(request) @@ -132,7 +124,7 @@ export class AlertingAuthorization { this.allPossibleConsumers = this.featuresIds.then((featuresIds) => { return featuresIds.size - ? asAuthorizedConsumers([...this.exemptConsumerIds, ...featuresIds], { + ? asAuthorizedConsumers([ALERTS_FEATURE_ID, ...featuresIds], { read: true, all: true, }) @@ -185,8 +177,10 @@ export class AlertingAuthorization { ), }; - // Skip authorizing consumer if it is in the list of exempt consumer ids - const shouldAuthorizeConsumer = !this.exemptConsumerIds.includes(consumer); + // Skip authorizing consumer if consumer is the Rules Management consumer (`alerts`) + // This means that rules and their derivative alerts created in the Rules Management UI + // will only be subject to checking if user has access to the rule producer. + const shouldAuthorizeConsumer = consumer !== ALERTS_FEATURE_ID; const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); const { hasAllRequested, username, privileges } = await checkPrivileges({ @@ -199,8 +193,8 @@ export class AlertingAuthorization { requiredPrivilegesByScope.producer, ] : [ - // skip consumer privilege checks for exempt consumer ids as all rule types can - // be created for exempt consumers if user has producer level privileges + // skip consumer privilege checks under `alerts` as all rule types can + // be created under `alerts` if you have producer level privileges requiredPrivilegesByScope.producer, ], }); @@ -448,14 +442,12 @@ export class AlertingAuthorization { ruleType.authorizedConsumers[feature] ); - if (isAuthorizedAtProducerLevel && this.exemptConsumerIds.length > 0) { - // granting privileges under the producer automatically authorized exempt consumer IDs as well - this.exemptConsumerIds.forEach((exemptId: string) => { - ruleType.authorizedConsumers[exemptId] = mergeHasPrivileges( - hasPrivileges, - ruleType.authorizedConsumers[exemptId] - ); - }); + if (isAuthorizedAtProducerLevel) { + // granting privileges under the producer automatically authorized the Rules Management UI as well + ruleType.authorizedConsumers[ALERTS_FEATURE_ID] = mergeHasPrivileges( + hasPrivileges, + ruleType.authorizedConsumers[ALERTS_FEATURE_ID] + ); } authorizedRuleTypes.add(ruleType); } diff --git a/x-pack/plugins/alerting/server/rules_client_factory.test.ts b/x-pack/plugins/alerting/server/rules_client_factory.test.ts index df59205cf10b3..188ec652f40ce 100644 --- a/x-pack/plugins/alerting/server/rules_client_factory.test.ts +++ b/x-pack/plugins/alerting/server/rules_client_factory.test.ts @@ -21,7 +21,6 @@ import { securityMock } from '../../security/server/mocks'; import { PluginStartContract as ActionsStartContract } from '../../actions/server'; import { actionsMock, actionsAuthorizationMock } from '../../actions/server/mocks'; import { LegacyAuditLogger } from '../../security/server'; -import { ALERTS_FEATURE_ID } from '../common'; import { eventLogMock } from '../../event_log/server/mocks'; import { alertingAuthorizationMock } from './authorization/alerting_authorization.mock'; import { alertingAuthorizationClientFactoryMock } from './alerting_authorization_client_factory.mock'; @@ -105,9 +104,7 @@ test('creates an alerts client with proper constructor arguments when security i includedHiddenTypes: ['alert', 'api_key_pending_invalidation'], }); - expect(alertingAuthorizationClientFactory.create).toHaveBeenCalledWith(request, [ - ALERTS_FEATURE_ID, - ]); + expect(alertingAuthorizationClientFactory.create).toHaveBeenCalledWith(request); expect(rulesClientFactoryParams.actions.getActionsAuthorizationWithRequest).toHaveBeenCalledWith( request @@ -148,9 +145,7 @@ test('creates an alerts client with proper constructor arguments', async () => { includedHiddenTypes: ['alert', 'api_key_pending_invalidation'], }); - expect(alertingAuthorizationClientFactory.create).toHaveBeenCalledWith(request, [ - ALERTS_FEATURE_ID, - ]); + expect(alertingAuthorizationClientFactory.create).toHaveBeenCalledWith(request); expect(jest.requireMock('./rules_client').RulesClient).toHaveBeenCalledWith({ unsecuredSavedObjectsClient: savedObjectsClient, diff --git a/x-pack/plugins/alerting/server/rules_client_factory.ts b/x-pack/plugins/alerting/server/rules_client_factory.ts index 336c8e6de20e6..7961d3761d3ef 100644 --- a/x-pack/plugins/alerting/server/rules_client_factory.ts +++ b/x-pack/plugins/alerting/server/rules_client_factory.ts @@ -19,7 +19,6 @@ import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/serve import { TaskManagerStartContract } from '../../task_manager/server'; import { IEventLogClientService } from '../../../plugins/event_log/server'; import { AlertingAuthorizationClientFactory } from './alerting_authorization_client_factory'; -import { ALERTS_FEATURE_ID } from '../common'; export interface RulesClientFactoryOpts { logger: Logger; taskManager: TaskManagerStartContract; @@ -87,7 +86,7 @@ export class RulesClientFactory { excludedWrappers: ['security'], includedHiddenTypes: ['alert', 'api_key_pending_invalidation'], }), - authorization: this.authorization.create(request, [ALERTS_FEATURE_ID]), + authorization: this.authorization.create(request), actionsAuthorization: actions.getActionsAuthorizationWithRequest(request), namespace: this.spaceIdToNamespace(spaceId), encryptedSavedObjectsClient: this.encryptedSavedObjectsClient, From 900052f32c86a0cc93cbc499c5537b72437f4d57 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher <471693+Kerry350@users.noreply.github.com> Date: Wed, 11 Aug 2021 19:51:32 +0100 Subject: [PATCH 090/104] [APM] Add a logs tab for services (#107664) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add a logs tab for APM services Co-authored-by: Søren Louv-Jansen --- .../elasticsearch_fieldnames.test.ts.snap | 6 + .../apm/common/elasticsearch_fieldnames.ts | 1 + .../components/app/service_logs/index.tsx | 104 ++++++++++++++++++ .../routing/service_detail/index.tsx | 9 ++ .../templates/apm_service_template/index.tsx | 13 +++ .../services/get_service_infrastructure.ts | 86 +++++++++++++++ x-pack/plugins/apm/server/routes/services.ts | 33 +++++- .../log_stream/log_stream.stories.mdx | 35 +++++- .../components/log_stream/log_stream.tsx | 8 +- 9 files changed, 289 insertions(+), 6 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/service_logs/index.tsx create mode 100644 x-pack/plugins/apm/server/lib/services/get_service_infrastructure.ts diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index 52f1c3b44d795..141d94e2b168f 100644 --- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -75,6 +75,8 @@ exports[`Error HOST_NAME 1`] = `"my hostname"`; exports[`Error HOST_OS_PLATFORM 1`] = `undefined`; +exports[`Error HOSTNAME 1`] = `undefined`; + exports[`Error HTTP_REQUEST_METHOD 1`] = `undefined`; exports[`Error HTTP_RESPONSE_STATUS_CODE 1`] = `undefined`; @@ -314,6 +316,8 @@ exports[`Span HOST_NAME 1`] = `undefined`; exports[`Span HOST_OS_PLATFORM 1`] = `undefined`; +exports[`Span HOSTNAME 1`] = `undefined`; + exports[`Span HTTP_REQUEST_METHOD 1`] = `undefined`; exports[`Span HTTP_RESPONSE_STATUS_CODE 1`] = `undefined`; @@ -553,6 +557,8 @@ exports[`Transaction HOST_NAME 1`] = `"my hostname"`; exports[`Transaction HOST_OS_PLATFORM 1`] = `undefined`; +exports[`Transaction HOSTNAME 1`] = `undefined`; + exports[`Transaction HTTP_REQUEST_METHOD 1`] = `"GET"`; exports[`Transaction HTTP_RESPONSE_STATUS_CODE 1`] = `200`; diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts index 82a592cd7e4d1..d1f07c28bc808 100644 --- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts @@ -114,6 +114,7 @@ export const LABEL_NAME = 'labels.name'; export const HOST = 'host'; export const HOST_NAME = 'host.hostname'; +export const HOSTNAME = 'host.name'; export const HOST_OS_PLATFORM = 'host.os.platform'; export const CONTAINER_ID = 'container.id'; export const KUBERNETES = 'kubernetes'; diff --git a/x-pack/plugins/apm/public/components/app/service_logs/index.tsx b/x-pack/plugins/apm/public/components/app/service_logs/index.tsx new file mode 100644 index 0000000000000..8e642c1f27e1a --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_logs/index.tsx @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEmpty } from 'lodash'; +import { EuiLoadingSpinner, EuiEmptyPrompt } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; +import { LogStream } from '../../../../../infra/public'; +import { APIReturnType } from '../../../services/rest/createCallApmApi'; + +import { + CONTAINER_ID, + HOSTNAME, + POD_NAME, +} from '../../../../common/elasticsearch_fieldnames'; + +export function ServiceLogs() { + const { serviceName } = useApmServiceContext(); + const { + urlParams: { environment, kuery, start, end }, + } = useUrlParams(); + + const { data, status } = useFetcher( + (callApmApi) => { + if (start && end) { + return callApmApi({ + endpoint: 'GET /api/apm/services/{serviceName}/infrastructure', + params: { + path: { serviceName }, + query: { + environment, + kuery, + start, + end, + }, + }, + }); + } + }, + [environment, kuery, serviceName, start, end] + ); + + const noInfrastructureData = useMemo(() => { + return ( + isEmpty(data?.serviceInfrastructure?.containerIds) && + isEmpty(data?.serviceInfrastructure?.hostNames) && + isEmpty(data?.serviceInfrastructure?.podNames) + ); + }, [data]); + + if (status === FETCH_STATUS.LOADING) { + return ( +
    + +
    + ); + } + + if (status === FETCH_STATUS.SUCCESS && noInfrastructureData) { + return ( + + {i18n.translate('xpack.apm.serviceLogs.noInfrastructureMessage', { + defaultMessage: 'There are no log messages to display.', + })} +

    + } + /> + ); + } + + return ( + + ); +} + +const getInfrastructureKQLFilter = ( + data?: APIReturnType<'GET /api/apm/services/{serviceName}/infrastructure'> +) => { + const containerIds = data?.serviceInfrastructure?.containerIds ?? []; + const hostNames = data?.serviceInfrastructure?.hostNames ?? []; + const podNames = data?.serviceInfrastructure?.podNames ?? []; + + return [ + ...containerIds.map((id) => `${CONTAINER_ID}: "${id}"`), + ...hostNames.map((id) => `${HOSTNAME}: "${id}"`), + ...podNames.map((id) => `${POD_NAME}: "${id}"`), + ].join(' or '); +}; diff --git a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx index 9716dd01561e5..5aeae224d0d5d 100644 --- a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx @@ -22,6 +22,7 @@ import { ServiceMap } from '../../app/service_map'; import { TransactionDetails } from '../../app/transaction_details'; import { ServiceProfiling } from '../../app/service_profiling'; import { ServiceDependencies } from '../../app/service_dependencies'; +import { ServiceLogs } from '../../app/service_logs'; function page({ path, @@ -233,6 +234,14 @@ export const serviceDetail = { hidden: true, }, }), + page({ + path: '/logs', + tab: 'logs', + title: i18n.translate('xpack.apm.views.logs.title', { + defaultMessage: 'Logs', + }), + element: , + }), page({ path: '/profiling', tab: 'profiling', diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx index d92d7a8d94922..d332048338cc0 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx @@ -41,6 +41,7 @@ type Tab = NonNullable[0] & { | 'metrics' | 'nodes' | 'service-map' + | 'logs' | 'profiling'; hidden?: boolean; }; @@ -218,6 +219,18 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { defaultMessage: 'Service Map', }), }, + { + key: 'logs', + href: router.link('/services/:serviceName/logs', { + path: { serviceName }, + query, + }), + label: i18n.translate('xpack.apm.home.serviceLogsTabLabel', { + defaultMessage: 'Logs', + }), + hidden: + !agentName || isRumAgentName(agentName) || isIosAgentName(agentName), + }, { key: 'profiling', href: router.link('/services/:serviceName/profiling', { diff --git a/x-pack/plugins/apm/server/lib/services/get_service_infrastructure.ts b/x-pack/plugins/apm/server/lib/services/get_service_infrastructure.ts new file mode 100644 index 0000000000000..79ecab45c75b3 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/get_service_infrastructure.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { ESFilter } from '../../../../../../src/core/types/elasticsearch'; +import { rangeQuery, kqlQuery } from '../../../../observability/server'; +import { environmentQuery } from '../../../common/utils/environment_query'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { + SERVICE_NAME, + CONTAINER_ID, + HOSTNAME, + POD_NAME, +} from '../../../common/elasticsearch_fieldnames'; + +export const getServiceInfrastructure = async ({ + kuery, + serviceName, + environment, + setup, +}: { + kuery?: string; + serviceName: string; + environment?: string; + setup: Setup & SetupTimeRange; +}) => { + const { apmEventClient, start, end } = setup; + + const filter: ESFilter[] = [ + { term: { [SERVICE_NAME]: serviceName } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ]; + + const response = await apmEventClient.search('get_service_infrastructure', { + apm: { + events: [ProcessorEvent.metric], + }, + body: { + size: 0, + query: { + bool: { + filter, + }, + }, + aggs: { + containerIds: { + terms: { + field: CONTAINER_ID, + size: 500, + }, + }, + hostNames: { + terms: { + field: HOSTNAME, + size: 500, + }, + }, + podNames: { + terms: { + field: POD_NAME, + size: 500, + }, + }, + }, + }, + }); + + return { + containerIds: + response.aggregations?.containerIds?.buckets.map( + (bucket) => bucket.key + ) ?? [], + hostNames: + response.aggregations?.hostNames?.buckets.map((bucket) => bucket.key) ?? + [], + podNames: + response.aggregations?.podNames?.buckets.map((bucket) => bucket.key) ?? + [], + }; +}; diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 6509c5764edb8..10d5bc5e3abdb 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -31,6 +31,7 @@ import { getServiceTransactionTypes } from '../lib/services/get_service_transact import { getThroughput } from '../lib/services/get_throughput'; import { getServiceProfilingStatistics } from '../lib/services/profiling/get_service_profiling_statistics'; import { getServiceProfilingTimeline } from '../lib/services/profiling/get_service_profiling_timeline'; +import { getServiceInfrastructure } from '../lib/services/get_service_infrastructure'; import { withApmSpan } from '../utils/with_apm_span'; import { createApmServerRoute } from './create_apm_server_route'; import { createApmServerRouteRepository } from './create_apm_server_route_repository'; @@ -853,6 +854,35 @@ const serviceAlertsRoute = createApmServerRoute({ }, }); +const serviceInfrastructureRoute = createApmServerRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/infrastructure', + params: t.type({ + path: t.type({ + serviceName: t.string, + }), + query: t.intersection([kueryRt, rangeRt, environmentRt]), + }), + options: { tags: ['access:apm'] }, + handler: async (resources) => { + const setup = await setupRequest(resources); + + const { params } = resources; + + const { + path: { serviceName }, + query: { environment, kuery }, + } = params; + + const serviceInfrastructure = await getServiceInfrastructure({ + setup, + serviceName, + environment, + kuery, + }); + return { serviceInfrastructure }; + }, +}); + export const serviceRouteRepository = createApmServerRouteRepository() .add(servicesRoute) .add(servicesDetailedStatisticsRoute) @@ -873,4 +903,5 @@ export const serviceRouteRepository = createApmServerRouteRepository() .add(serviceDependenciesBreakdownRoute) .add(serviceProfilingTimelineRoute) .add(serviceProfilingStatisticsRoute) - .add(serviceAlertsRoute); + .add(serviceAlertsRoute) + .add(serviceInfrastructureRoute); diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.mdx b/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.mdx index 87419a9bfbe78..b241147c8d675 100644 --- a/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.mdx +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.mdx @@ -195,7 +195,11 @@ This will show a list of log entries between the specified timestamps. ## Query log entries -You might want to show specific log entries in your plugin. Maybe you want to show log lines from a specific host, or for an AMP trace. The component has a `query` prop that accepts valid KQL expressions. +You might want to show specific log entries in your plugin. Maybe you want to show log lines from a specific host, or for an AMP trace. The LogStream component supports both `query` and `filters`, and these are the standard `es-query` types. + +### Query + +The component has a `query` prop that accepts a valid es-query `query`. You can either supply this with a `language` and `query` property, or you can just supply a string which is a shortcut for KQL expressions. ```tsx ``` +### Filters + +The component also has a `filters` prop that accepts valid es-query `filters`. This example would specifiy that we want the `message` field to exist: + +```tsx + +``` + ## Center the view on a specific entry By default the component will load at the bottom of the list, showing the newest entries. You can change the rendering point with the `center` prop. The prop takes a [`LogEntriesCursor`](https://github.com/elastic/kibana/blob/0a6c748cc837c016901f69ff05d81395aa2d41c8/x-pack/plugins/infra/common/http_api/log_entries/common.ts#L9-L13). @@ -431,3 +460,7 @@ class MyPlugin { endTimestamp={...} /> ``` + +### Setting component height + +It's possible to pass a `height` prop, e.g. `60vh` or `300px`, to specify how much vertical space the component should consume. diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx b/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx index b927505a42c8a..2698e975cebca 100644 --- a/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream.tsx @@ -5,10 +5,11 @@ * 2.0. */ +import { buildEsQuery, Query, Filter } from '@kbn/es-query'; import React, { useMemo, useCallback, useEffect } from 'react'; import { noop } from 'lodash'; import { JsonValue } from '@kbn/utility-types'; -import { DataPublicPluginStart, esQuery, Filter } from '../../../../../../src/plugins/data/public'; +import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; import { LogEntryCursor } from '../../../common/log_entry'; @@ -18,7 +19,6 @@ import { BuiltEsQuery, useLogStream } from '../../containers/logs/log_stream'; import { ScrollableLogTextStreamView } from '../logging/log_text_stream'; import { LogColumnRenderConfiguration } from '../../utils/log_column_render_configuration'; -import { Query } from '../../../../../../src/plugins/data/common'; import { LogStreamErrorBoundary } from './log_stream_error_boundary'; interface LogStreamPluginDeps { @@ -123,9 +123,9 @@ Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_re const parsedQuery = useMemo(() => { if (typeof query === 'object' && 'bool' in query) { - return mergeBoolQueries(query, esQuery.buildEsQuery(derivedIndexPattern, [], filters ?? [])); + return mergeBoolQueries(query, buildEsQuery(derivedIndexPattern, [], filters ?? [])); } else { - return esQuery.buildEsQuery(derivedIndexPattern, coerceToQueries(query), filters ?? []); + return buildEsQuery(derivedIndexPattern, coerceToQueries(query), filters ?? []); } }, [derivedIndexPattern, filters, query]); From f236286b62529449ee7385af890e8004320eb854 Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 11 Aug 2021 11:51:48 -0700 Subject: [PATCH 091/104] [kbn/es-archiver] fix flaky test (#108143) Co-authored-by: spalger --- .../src/lib/docs/index_doc_records_stream.test.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts b/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts index 91cec7dc49094..bcf28a4976a1c 100644 --- a/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts +++ b/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts @@ -99,10 +99,8 @@ const testRecords = [ }, ]; -// FLAKY: https://github.com/elastic/kibana/issues/108043 -it.skip('indexes documents using the bulk client helper', async () => { +it('indexes documents using the bulk client helper', async () => { const client = new MockClient(); - client.helpers.bulk.mockImplementation(async () => {}); const progress = new Progress(); const stats = createStats('test', log); @@ -186,11 +184,11 @@ it.skip('indexes documents using the bulk client helper', async () => { "results": Array [ Object { "type": "return", - "value": Promise {}, + "value": undefined, }, Object { "type": "return", - "value": Promise {}, + "value": undefined, }, ], } From 12115d680c88d0b4306056f208e393e6d48dcc77 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Wed, 11 Aug 2021 12:42:10 -0700 Subject: [PATCH 092/104] [data.search] Add owner/description properties to kibana.json (#107954) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- examples/search_examples/kibana.json | 7 ++++++- x-pack/plugins/data_enhanced/kibana.json | 8 +++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/examples/search_examples/kibana.json b/examples/search_examples/kibana.json index 227fd7f1c6261..87839f2037f92 100644 --- a/examples/search_examples/kibana.json +++ b/examples/search_examples/kibana.json @@ -11,5 +11,10 @@ "ui": true, "requiredPlugins": ["navigation", "data", "developerExamples", "kibanaUtils", "share"], "optionalPlugins": [], - "requiredBundles": ["kibanaReact"] + "requiredBundles": ["kibanaReact"], + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, + "description": "Examples for using the data plugin search service. Includes examples for searching using the high level search source, or low-level search services, as well as integrating with search sessions." } diff --git a/x-pack/plugins/data_enhanced/kibana.json b/x-pack/plugins/data_enhanced/kibana.json index a0489ecd30aaa..da83ded471d0b 100644 --- a/x-pack/plugins/data_enhanced/kibana.json +++ b/x-pack/plugins/data_enhanced/kibana.json @@ -1,3 +1,4 @@ + { "id": "dataEnhanced", "version": "8.0.0", @@ -7,5 +8,10 @@ "optionalPlugins": ["kibanaUtils", "usageCollection", "security"], "server": true, "ui": true, - "requiredBundles": ["kibanaUtils", "kibanaReact"] + "requiredBundles": ["kibanaUtils", "kibanaReact"], + "owner": { + "name": "App Services", + "githubTeam": "kibana-app-services" + }, + "description": "Enhanced data plugin. (See src/plugins/data.) Enhances the main data plugin with a search session management UI. Includes a reusable search session indicator component to use in other applications. Exposes routes for managing search sessions. Includes a service that monitors, updates, and cleans up search session saved objects." } From 2ce80869bedcd849b32770a300406eba1ef7ffcf Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 11 Aug 2021 13:48:22 -0600 Subject: [PATCH 093/104] [maps] add indication in layer TOC when layer is filtered by map bounds (#107662) * [maps] add indication in layer TOC when layer is filtered by map bounds * fix i18n id collision * use ghost color so icons are more visible * revert icon color change Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../classes/sources/vector_source/vector_source.tsx | 1 + .../toc_entry/toc_entry_button/toc_entry_button.tsx | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx index 7dc816f6e1b6c..4dbf5f16b0673 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx @@ -71,6 +71,7 @@ export interface IVectorSource extends ISource { supportsFeatureEditing(): Promise; addFeature(geometry: Geometry | Position[]): Promise; deleteFeature(featureId: string): Promise; + isFilterByMapBounds(): boolean; } export class AbstractVectorSource extends AbstractSource implements IVectorSource { diff --git a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_button/toc_entry_button.tsx b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_button/toc_entry_button.tsx index ffad34454bb61..eabb2b782272b 100644 --- a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_button/toc_entry_button.tsx +++ b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/toc_entry_button/toc_entry_button.tsx @@ -10,6 +10,7 @@ import React, { Component, Fragment, ReactNode } from 'react'; import { EuiButtonEmpty, EuiIcon, EuiToolTip, EuiLoadingSpinner } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ILayer } from '../../../../../../classes/layers/layer'; +import { IVectorSource } from '../../../../../../classes/sources/vector_source'; interface Footnote { icon: ReactNode; @@ -128,6 +129,18 @@ export class TOCEntryButton extends Component { }), }); } + const source = this.props.layer.getSource(); + if ( + typeof source.isFilterByMapBounds === 'function' && + (source as IVectorSource).isFilterByMapBounds() + ) { + footnotes.push({ + icon: , + message: i18n.translate('xpack.maps.layer.isUsingBoundsFilter', { + defaultMessage: 'Results narrowed by visible map area', + }), + }); + } } return { From 124c85ed8cadca04abccdc372b25a18e1c5bded9 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Wed, 11 Aug 2021 16:07:41 -0400 Subject: [PATCH 094/104] [ML] Data Frame Analytics: add evaluation quality metrics to Classification exploration view (#107862) * add evaluation quality metrics to Classification exploration view * move type to common file * fix path * switch accuracy and recall columns and update MetricItem name * add evaluation metrics title * ensure evaluation metrics section is left aligned --- .../data_frame_analytics/common/analytics.ts | 6 ++ .../_classification_exploration.scss | 4 + .../evaluate_panel.tsx | 98 ++++++++++++------- .../evaluation_quality_metrics_table.tsx | 72 ++++++++++++++ .../use_confusion_matrix.ts | 47 ++++++++- 5 files changed, 189 insertions(+), 38 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluation_quality_metrics_table.tsx diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index bac6b0b9274f5..c2c2563c5ba7c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -65,6 +65,12 @@ export interface LoadExploreDataArg { searchQuery: SavedSearchQuery; } +export interface ClassificationMetricItem { + className: string; + accuracy?: number; + recall?: number; +} + export const SEARCH_SIZE = 1000; export const TRAINING_PERCENT_MIN = 1; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_classification_exploration.scss b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_classification_exploration.scss index 73ced778821cf..c429daaf3c8dc 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_classification_exploration.scss +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_classification_exploration.scss @@ -41,3 +41,7 @@ $labelColumnWidth: 80px; text-transform: none; } } + +.mlDataFrameAnalyticsClassification__evaluationMetrics { + width: 60%; +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx index 086adcecd077a..fb103886635a9 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx @@ -34,6 +34,7 @@ import { ResultsSearchQuery } from '../../../../common/analytics'; import { ExpandableSection, HEADER_ITEMS_LOADING } from '../expandable_section'; import { EvaluateStat } from './evaluate_stat'; +import { EvaluationQualityMetricsTable } from './evaluation_quality_metrics_table'; import { getRocCurveChartVegaLiteSpec } from './get_roc_curve_chart_vega_lite_spec'; @@ -85,6 +86,13 @@ const trainingDatasetHelpText = i18n.translate( } ); +const evaluationQualityMetricsHelpText = i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.evaluationQualityMetricsHelpText', + { + defaultMessage: 'Evaluation quality metrics', + } +); + function getHelpText(dataSubsetTitle: string): string { let helpText = entireDatasetHelpText; if (dataSubsetTitle === SUBSET_TITLE.TESTING) { @@ -120,6 +128,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, se error: errorConfusionMatrix, isLoading: isLoadingConfusionMatrix, overallAccuracy, + evaluationMetricsItems, } = useConfusionMatrix(jobConfig, searchQuery); useEffect(() => { @@ -365,46 +374,61 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, se ) : null} {/* Accuracy and Recall */} - + + {evaluationQualityMetricsHelpText} + + + - + + + + + + + + - + {/* AUC ROC Chart */} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluation_quality_metrics_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluation_quality_metrics_table.tsx new file mode 100644 index 0000000000000..32820b69b8a8b --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluation_quality_metrics_table.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { EuiAccordion, EuiInMemoryTable, EuiPanel } from '@elastic/eui'; + +import { ClassificationMetricItem } from '../../../../common/analytics'; + +const columns = [ + { + field: 'className', + name: i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.recallAndAccuracyClassColumn', + { + defaultMessage: 'Class', + } + ), + sortable: true, + truncateText: true, + }, + { + field: 'accuracy', + name: i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.recallAndAccuracyAccuracyColumn', + { + defaultMessage: 'Accuracy', + } + ), + render: (value: number) => Math.round(value * 1000) / 1000, + }, + { + field: 'recall', + name: i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.recallAndAccuracyRecallColumn', + { + defaultMessage: 'Recall', + } + ), + render: (value: number) => Math.round(value * 1000) / 1000, + }, +]; + +export const EvaluationQualityMetricsTable: FC<{ + evaluationMetricsItems: ClassificationMetricItem[]; +}> = ({ evaluationMetricsItems }) => ( + <> + + } + > + + + items={evaluationMetricsItems} + columns={columns} + pagination + sorting + /> + + + +); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts index df48d2c5ab44f..2a75acf823e88 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_confusion_matrix.ts @@ -9,9 +9,11 @@ import { useState, useEffect } from 'react'; import { isClassificationEvaluateResponse, + ClassificationEvaluateResponse, ConfusionMatrix, ResultsSearchQuery, ANALYSIS_CONFIG_TYPE, + ClassificationMetricItem, } from '../../../../common/analytics'; import { isKeywordAndTextType } from '../../../../common/fields'; @@ -25,6 +27,37 @@ import { import { isTrainingFilter } from './is_training_filter'; +function getEvalutionMetricsItems(evalMetrics?: ClassificationEvaluateResponse['classification']) { + if (evalMetrics === undefined) return []; + + const accuracyMetrics = evalMetrics.accuracy?.classes || []; + const recallMetrics = evalMetrics.recall?.classes || []; + + const metricsMap = accuracyMetrics.reduce((acc, accuracyMetric) => { + acc[accuracyMetric.class_name] = { + className: accuracyMetric.class_name, + accuracy: accuracyMetric.value, + }; + return acc; + }, {} as Record); + + recallMetrics.forEach((recallMetric) => { + if (metricsMap[recallMetric.class_name] !== undefined) { + metricsMap[recallMetric.class_name] = { + recall: recallMetric.value, + ...metricsMap[recallMetric.class_name], + }; + } else { + metricsMap[recallMetric.class_name] = { + className: recallMetric.class_name, + recall: recallMetric.value, + }; + } + }); + + return Object.values(metricsMap); +} + export const useConfusionMatrix = ( jobConfig: DataFrameAnalyticsConfig, searchQuery: ResultsSearchQuery @@ -32,6 +65,9 @@ export const useConfusionMatrix = ( const [confusionMatrixData, setConfusionMatrixData] = useState([]); const [overallAccuracy, setOverallAccuracy] = useState(null); const [avgRecall, setAvgRecall] = useState(null); + const [evaluationMetricsItems, setEvaluationMetricsItems] = useState( + [] + ); const [isLoading, setIsLoading] = useState(false); const [docsCount, setDocsCount] = useState(null); const [error, setError] = useState(null); @@ -81,6 +117,7 @@ export const useConfusionMatrix = ( setConfusionMatrixData(confusionMatrix || []); setAvgRecall(evalData.eval?.classification?.recall?.avg_recall || null); setOverallAccuracy(evalData.eval?.classification?.accuracy?.overall_accuracy || null); + setEvaluationMetricsItems(getEvalutionMetricsItems(evalData.eval?.classification)); setIsLoading(false); } else { setIsLoading(false); @@ -98,5 +135,13 @@ export const useConfusionMatrix = ( loadConfusionMatrixData(); }, [JSON.stringify([jobConfig, searchQuery])]); - return { avgRecall, confusionMatrixData, docsCount, error, isLoading, overallAccuracy }; + return { + avgRecall, + confusionMatrixData, + docsCount, + error, + isLoading, + overallAccuracy, + evaluationMetricsItems, + }; }; From c39c1292eb11f1913c4ba3aac8bee3966198aec4 Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 11 Aug 2021 13:08:10 -0700 Subject: [PATCH 095/104] [build-ts-refs] normalize paths before writing them to the FS (#108246) Co-authored-by: spalger --- src/dev/typescript/root_refs_config.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dev/typescript/root_refs_config.ts b/src/dev/typescript/root_refs_config.ts index c297a9288ddd5..f4aa88f1ea6b2 100644 --- a/src/dev/typescript/root_refs_config.ts +++ b/src/dev/typescript/root_refs_config.ts @@ -11,6 +11,7 @@ import Fs from 'fs/promises'; import dedent from 'dedent'; import { REPO_ROOT, ToolingLog } from '@kbn/dev-utils'; +import normalize from 'normalize-path'; import { PROJECTS } from './projects'; @@ -53,7 +54,7 @@ export async function updateRootRefsConfig(log: ToolingLog) { } const refs = PROJECTS.filter((p) => p.isCompositeProject()) - .map((p) => `./${Path.relative(REPO_ROOT, p.tsConfigPath)}`) + .map((p) => `./${normalize(Path.relative(REPO_ROOT, p.tsConfigPath))}`) .sort((a, b) => a.localeCompare(b)); log.debug('updating', ROOT_REFS_CONFIG_PATH); From 6563fad7be3b879dbb9e844decbf7edcfa591cc2 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Wed, 11 Aug 2021 13:13:40 -0700 Subject: [PATCH 096/104] [Reporting] implement content changes per feedback (#108068) --- .../components/download_options.tsx | 2 +- .../public/components/table_vis_controls.tsx | 2 +- .../lens/public/app_plugin/lens_top_nav.tsx | 2 +- x-pack/plugins/reporting/public/lib/job.tsx | 9 +++--- .../reporting_api_client.ts | 2 +- .../report_info_button.test.tsx.snap | 4 +-- .../report_listing.test.tsx.snap | 29 ++++++++++++------- .../public/management/report_info_button.tsx | 28 +++++++++--------- .../reporting_management/report_listing.ts | 14 ++++----- 9 files changed, 50 insertions(+), 42 deletions(-) diff --git a/src/plugins/data/public/utils/table_inspector_view/components/download_options.tsx b/src/plugins/data/public/utils/table_inspector_view/components/download_options.tsx index 57e586eaf12f8..03d9a8d2b35bc 100644 --- a/src/plugins/data/public/utils/table_inspector_view/components/download_options.tsx +++ b/src/plugins/data/public/utils/table_inspector_view/components/download_options.tsx @@ -122,7 +122,7 @@ class DataDownloadOptions extends Component {button} diff --git a/src/plugins/vis_type_table/public/components/table_vis_controls.tsx b/src/plugins/vis_type_table/public/components/table_vis_controls.tsx index 458bca4a54061..01dd693a31ff8 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_controls.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_controls.tsx @@ -99,7 +99,7 @@ export const TableVisControls = memo( position="top" content={i18n.translate('visTypeTable.vis.controls.exportButtonFormulasWarning', { defaultMessage: - 'Your CSV contains characters which spreadsheet applications can interpret as formulas', + 'Your CSV contains characters that spreadsheet applications might interpret as formulas.', })} > {button} diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index 06420051678ee..f777d053b314b 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -233,7 +233,7 @@ export const LensTopNavMenu = ({ if (formulaDetected) { return i18n.translate('xpack.lens.app.downloadButtonFormulasWarning', { defaultMessage: - 'Your CSV contains characters which spreadsheet applications can interpret as formulas', + 'Your CSV contains characters that spreadsheet applications might interpret as formulas.', }); } } diff --git a/x-pack/plugins/reporting/public/lib/job.tsx b/x-pack/plugins/reporting/public/lib/job.tsx index 96967dc9226c9..86d618c57379b 100644 --- a/x-pack/plugins/reporting/public/lib/job.tsx +++ b/x-pack/plugins/reporting/public/lib/job.tsx @@ -86,7 +86,7 @@ export class Job { let smallMessage; if (status === PENDING) { smallMessage = i18n.translate('xpack.reporting.jobStatusDetail.pendingStatusReachedText', { - defaultMessage: 'Waiting for job to be processed.', + defaultMessage: 'Waiting for job to process.', }); } else if (status === PROCESSING) { smallMessage = i18n.translate('xpack.reporting.jobStatusDetail.attemptXofY', { @@ -139,8 +139,7 @@ export class Job { getStatusLabel() { return ( <> - {this.getStatus()} - {this.getStatusMessage()} + {this.getStatus()} {this.getStatusMessage()} ); } @@ -184,14 +183,14 @@ export class Job { warnings.push( i18n.translate('xpack.reporting.jobWarning.csvContainsFormulas', { defaultMessage: - 'Your CSV contains characters which spreadsheet applications can interpret as formulas.', + 'Your CSV contains characters that spreadsheet applications might interpret as formulas.', }) ); } if (this.max_size_reached) { warnings.push( i18n.translate('xpack.reporting.jobWarning.maxSizeReachedTooltip', { - defaultMessage: 'Max size reached, contains partial data.', + defaultMessage: 'Your search reached the max size and contains partial data.', }) ); } diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts index 5c618ba8261fa..151519b0b6b8f 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client/reporting_api_client.ts @@ -123,7 +123,7 @@ export class ReportingAPIClient implements IReportingAPI { } return i18n.translate('xpack.reporting.apiClient.unknownError', { - defaultMessage: `Report job {job} failed: Unknown error.`, + defaultMessage: `Report job {job} failed. Error unknown.`, values: { job: jobId }, }); } diff --git a/x-pack/plugins/reporting/public/management/__snapshots__/report_info_button.test.tsx.snap b/x-pack/plugins/reporting/public/management/__snapshots__/report_info_button.test.tsx.snap index 4ab50750bbc52..e8b9362db7525 100644 --- a/x-pack/plugins/reporting/public/management/__snapshots__/report_info_button.test.tsx.snap +++ b/x-pack/plugins/reporting/public/management/__snapshots__/report_info_button.test.tsx.snap @@ -61,7 +61,7 @@ Array [ className="euiTitle euiTitle--medium" id="flyoutTitle" > - Unable to fetch report info + Unable to fetch report info.
    @@ -113,7 +113,7 @@ Array [ className="euiTitle euiTitle--medium" id="flyoutTitle" > - Unable to fetch report info + Unable to fetch report info.

    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

  • diff --git a/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap b/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap index 89511aaf96a67..926ca6e0b53dc 100644 --- a/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap +++ b/x-pack/plugins/reporting/public/management/__snapshots__/report_listing.test.tsx.snap @@ -549,6 +549,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` 2020-04-14 @ 05:01 PM
    + @@ -562,7 +563,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` className="euiTextColor euiTextColor--subdued" style={Object {}} > - Waiting for job to be processed. + Waiting for job to process.
    @@ -1293,7 +1294,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } > @@ -1562,6 +1563,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` 2020-04-14 @ 05:01 PM
    + @@ -2306,7 +2308,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } > @@ -2575,6 +2577,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` 2020-04-14 @ 04:19 PM + @@ -3348,7 +3351,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } > @@ -3617,6 +3620,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` 2020-04-14 @ 01:21 PM + @@ -4418,7 +4422,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } > @@ -4687,6 +4691,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` 2020-04-14 @ 01:19 PM + @@ -5460,7 +5465,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } > @@ -5729,6 +5734,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` 2020-04-14 @ 01:19 PM + @@ -6502,7 +6508,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } > @@ -6771,6 +6777,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` 2020-04-14 @ 01:18 PM + @@ -7544,7 +7551,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } > @@ -7813,6 +7820,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` 2020-04-14 @ 01:13 PM + @@ -8586,7 +8594,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } > @@ -8855,6 +8863,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` 2020-04-09 @ 03:10 PM + @@ -9628,7 +9637,7 @@ exports[`ReportListing Report job listing with some items 1`] = ` } > diff --git a/x-pack/plugins/reporting/public/management/report_info_button.tsx b/x-pack/plugins/reporting/public/management/report_info_button.tsx index 8513558fb89cc..7a70286785e4f 100644 --- a/x-pack/plugins/reporting/public/management/report_info_button.tsx +++ b/x-pack/plugins/reporting/public/management/report_info_button.tsx @@ -92,7 +92,7 @@ class ReportInfoButtonUi extends Component { { title: this.props.intl.formatMessage({ id: 'xpack.reporting.listing.infoPanel.createdAtInfo', - defaultMessage: 'Created At', + defaultMessage: 'Created at', }), description: info.getCreatedAtLabel(), }, @@ -106,7 +106,7 @@ class ReportInfoButtonUi extends Component { { title: this.props.intl.formatMessage({ id: 'xpack.reporting.listing.infoPanel.tzInfo', - defaultMessage: 'Timezone', + defaultMessage: 'Time zone', }), description: info.browserTimezone || NA, }, @@ -116,21 +116,21 @@ class ReportInfoButtonUi extends Component { { title: this.props.intl.formatMessage({ id: 'xpack.reporting.listing.infoPanel.startedAtInfo', - defaultMessage: 'Started At', + defaultMessage: 'Started at', }), description: info.started_at || NA, }, { title: this.props.intl.formatMessage({ id: 'xpack.reporting.listing.infoPanel.completedAtInfo', - defaultMessage: 'Completed At', + defaultMessage: 'Completed at', }), description: info.completed_at || NA, }, { title: this.props.intl.formatMessage({ id: 'xpack.reporting.listing.infoPanel.processedByInfo', - defaultMessage: 'Processed By', + defaultMessage: 'Processed by', }), description: info.kibana_name && info.kibana_id ? `${info.kibana_name} (${info.kibana_id})` : NA, @@ -138,14 +138,14 @@ class ReportInfoButtonUi extends Component { { title: this.props.intl.formatMessage({ id: 'xpack.reporting.listing.infoPanel.contentTypeInfo', - defaultMessage: 'Content Type', + defaultMessage: 'Content type', }), description: info.content_type || NA, }, { title: this.props.intl.formatMessage({ id: 'xpack.reporting.listing.infoPanel.sizeInfo', - defaultMessage: 'Size in Bytes', + defaultMessage: 'Size in bytes', }), description: info.size?.toString() || NA, }, @@ -159,7 +159,7 @@ class ReportInfoButtonUi extends Component { { title: this.props.intl.formatMessage({ id: 'xpack.reporting.listing.infoPanel.maxAttemptsInfo', - defaultMessage: 'Max Attempts', + defaultMessage: 'Max attempts', }), description: info.max_attempts?.toString() || NA, }, @@ -173,7 +173,7 @@ class ReportInfoButtonUi extends Component { { title: this.props.intl.formatMessage({ id: 'xpack.reporting.listing.infoPanel.exportTypeInfo', - defaultMessage: 'Export Type', + defaultMessage: 'Export type', }), description: info.isDeprecated ? this.props.intl.formatMessage( @@ -207,7 +207,7 @@ class ReportInfoButtonUi extends Component { { title: this.props.intl.formatMessage({ id: 'xpack.reporting.listing.infoPanel.browserTypeInfo', - defaultMessage: 'Browser Type', + defaultMessage: 'Browser type', }), description: info.browser_type || NA, }, @@ -293,17 +293,17 @@ class ReportInfoButtonUi extends Component { let message = this.props.intl.formatMessage({ id: 'xpack.reporting.listing.table.reportInfoButtonTooltip', - defaultMessage: 'See report info', + defaultMessage: 'See report info.', }); if (job.getError()) { message = this.props.intl.formatMessage({ id: 'xpack.reporting.listing.table.reportInfoAndErrorButtonTooltip', - defaultMessage: 'See report info and error message', + defaultMessage: 'See report info and error message.', }); } else if (job.getWarnings()) { message = this.props.intl.formatMessage({ id: 'xpack.reporting.listing.table.reportInfoAndWarningsButtonTooltip', - defaultMessage: 'See report info and warnings', + defaultMessage: 'See report info and warnings.', }); } @@ -349,7 +349,7 @@ class ReportInfoButtonUi extends Component { isLoading: false, calloutTitle: this.props.intl.formatMessage({ id: 'xpack.reporting.listing.table.reportInfoUnableToFetch', - defaultMessage: 'Unable to fetch report info', + defaultMessage: 'Unable to fetch report info.', }), info: null, error: err, diff --git a/x-pack/test/functional/apps/reporting_management/report_listing.ts b/x-pack/test/functional/apps/reporting_management/report_listing.ts index eb2e339e9be66..dd5c1e63f1bea 100644 --- a/x-pack/test/functional/apps/reporting_management/report_listing.ts +++ b/x-pack/test/functional/apps/reporting_management/report_listing.ts @@ -92,7 +92,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { "actions": "", "createdAt": "2021-07-19 @ 10:29 PMtest_user", "report": "Automated reportsearch", - "status": "Completed at 2021-07-19 @ 10:29 PMSee report info for warnings.", + "status": "Completed at 2021-07-19 @ 10:29 PM See report info for warnings.", }, Object { "actions": "", @@ -104,37 +104,37 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { "actions": "", "createdAt": "2021-07-19 @ 06:46 PMtest_user", "report": "Discover search [2021-07-19T11:46:00.132-07:00]search", - "status": "Completed at 2021-07-19 @ 06:46 PMSee report info for warnings.", + "status": "Completed at 2021-07-19 @ 06:46 PM See report info for warnings.", }, Object { "actions": "", "createdAt": "2021-07-19 @ 06:44 PMtest_user", "report": "Discover search [2021-07-19T11:44:48.670-07:00]search", - "status": "Completed at 2021-07-19 @ 06:44 PMSee report info for warnings.", + "status": "Completed at 2021-07-19 @ 06:44 PM See report info for warnings.", }, Object { "actions": "", "createdAt": "2021-07-19 @ 06:41 PMtest_user", "report": "[Flights] Global Flight Dashboarddashboard", - "status": "Pending at 2021-07-19 @ 06:41 PMWaiting for job to be processed.", + "status": "Pending at 2021-07-19 @ 06:41 PM Waiting for job to process.", }, Object { "actions": "", "createdAt": "2021-07-19 @ 06:41 PMtest_user", "report": "[Flights] Global Flight Dashboarddashboard", - "status": "Failed at 2021-07-19 @ 06:43 PMSee report info for error details.", + "status": "Failed at 2021-07-19 @ 06:43 PM See report info for error details.", }, Object { "actions": "", "createdAt": "2021-07-19 @ 06:41 PMtest_user", "report": "[Flights] Global Flight Dashboarddashboard", - "status": "Completed at 2021-07-19 @ 06:41 PMSee report info for warnings.", + "status": "Completed at 2021-07-19 @ 06:41 PM See report info for warnings.", }, Object { "actions": "", "createdAt": "2021-07-19 @ 06:38 PMtest_user", "report": "[Flights] Global Flight Dashboarddashboard", - "status": "Completed at 2021-07-19 @ 06:39 PMSee report info for warnings.", + "status": "Completed at 2021-07-19 @ 06:39 PM See report info for warnings.", }, Object { "actions": "", From 1eae08e2c1b1661bcc14561c0e141b47dc981ce5 Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Wed, 11 Aug 2021 22:16:53 +0200 Subject: [PATCH 097/104] [Expressions] Add support of partial results to the switch expression function (#108086) --- .../functions/common/switch.test.js | 68 +++++++++++++------ .../functions/common/switch.ts | 19 +++--- 2 files changed, 59 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js index ffa1557d2b54e..d0ea8fff0a45d 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.test.js @@ -56,25 +56,6 @@ describe('switch', () => { }); describe('function', () => { - describe('with no cases', () => { - it('should return the context if no default is provided', () => { - const context = 'foo'; - - testScheduler.run(({ expectObservable }) => - expectObservable(fn(context, {})).toBe('(0|)', [context]) - ); - }); - - it('should return the default if provided', () => { - const context = 'foo'; - const args = { default: () => of('bar') }; - - testScheduler.run(({ expectObservable }) => - expectObservable(fn(context, args)).toBe('(0|)', ['bar']) - ); - }); - }); - describe('with no matching cases', () => { it('should return the context if no default is provided', () => { const context = 'foo'; @@ -108,6 +89,55 @@ describe('switch', () => { expectObservable(fn(context, args)).toBe('(0|)', [result]) ); }); + + it('should support partial results', () => { + testScheduler.run(({ cold, expectObservable }) => { + const context = 'foo'; + const case1 = cold('--ab-c-', { + a: { + type: 'case', + matches: false, + result: 1, + }, + b: { + type: 'case', + matches: true, + result: 2, + }, + c: { + type: 'case', + matches: false, + result: 3, + }, + }); + const case2 = cold('-a--bc-', { + a: { + type: 'case', + matches: true, + result: 4, + }, + b: { + type: 'case', + matches: true, + result: 5, + }, + c: { + type: 'case', + matches: true, + result: 6, + }, + }); + const expected = ' --abc(de)-'; + const args = { case: [() => case1, () => case2] }; + expectObservable(fn(context, args)).toBe(expected, { + a: 4, + b: 2, + c: 2, + d: 5, + e: 6, + }); + }); + }); }); }); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.ts index f4e6c92c91cb6..3e676c829a301 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/switch.ts @@ -5,14 +5,14 @@ * 2.0. */ -import { Observable, defer, from, of } from 'rxjs'; -import { concatMap, filter, merge, pluck, take } from 'rxjs/operators'; +import { Observable, combineLatest, defer, of } from 'rxjs'; +import { concatMap } from 'rxjs/operators'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { Case } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; interface Arguments { - case?: Array<() => Observable>; + case: Array<() => Observable>; default?(): Observable; } @@ -43,12 +43,13 @@ export function switchFn(): ExpressionFunctionDefinition< }, }, fn(input, args) { - return from(args.case ?? []).pipe( - concatMap((item) => item()), - filter(({ matches }) => matches), - pluck('result'), - merge(defer(() => args.default?.() ?? of(input))), - take(1) + return combineLatest(args.case.map((item) => defer(() => item()))).pipe( + concatMap((items) => { + const item = items.find(({ matches }) => matches); + const item$ = item && of(item.result); + + return item$ ?? args.default?.() ?? of(input); + }) ); }, }; From 9dce033408cb0a00b754f996859a3a3171babf02 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Wed, 11 Aug 2021 16:23:58 -0400 Subject: [PATCH 098/104] [Task Manager] [8.0] Remove `xpack.task_manager.index` (#108111) * Remove support for the config field index * Fix type issues * Remove references from a few more places --- .../advanced/running-elasticsearch.asciidoc | 1 - docs/settings/task-manager-settings.asciidoc | 3 --- ...task-manager-production-considerations.asciidoc | 2 +- .../resources/base/bin/kibana-docker | 1 - x-pack/plugins/task_manager/server/config.test.ts | 14 -------------- x-pack/plugins/task_manager/server/config.ts | 9 --------- x-pack/plugins/task_manager/server/constants.ts | 8 ++++++++ .../server/ephemeral_task_lifecycle.test.ts | 1 - x-pack/plugins/task_manager/server/index.test.ts | 11 ----------- x-pack/plugins/task_manager/server/index.ts | 12 ------------ .../managed_configuration.test.ts | 1 - .../monitoring/configuration_statistics.test.ts | 1 - .../monitoring/monitoring_stats_stream.test.ts | 1 - x-pack/plugins/task_manager/server/plugin.test.ts | 2 -- x-pack/plugins/task_manager/server/plugin.ts | 5 +++-- .../task_manager/server/polling_lifecycle.test.ts | 1 - .../task_manager/server/saved_objects/index.ts | 5 +++-- 17 files changed, 15 insertions(+), 63 deletions(-) create mode 100644 x-pack/plugins/task_manager/server/constants.ts diff --git a/docs/developer/advanced/running-elasticsearch.asciidoc b/docs/developer/advanced/running-elasticsearch.asciidoc index 324d2af2ed3af..36f9ee420d41d 100644 --- a/docs/developer/advanced/running-elasticsearch.asciidoc +++ b/docs/developer/advanced/running-elasticsearch.asciidoc @@ -76,7 +76,6 @@ If many other users will be interacting with your remote cluster, you'll want to [source,bash] ---- kibana.index: '.{YourGitHubHandle}-kibana' -xpack.task_manager.index: '.{YourGitHubHandle}-task-manager-kibana' ---- ==== Running remote clusters diff --git a/docs/settings/task-manager-settings.asciidoc b/docs/settings/task-manager-settings.asciidoc index fa89b7780e475..387d2308aa5e8 100644 --- a/docs/settings/task-manager-settings.asciidoc +++ b/docs/settings/task-manager-settings.asciidoc @@ -22,9 +22,6 @@ Task Manager runs background tasks by polling for work on an interval. You can | `xpack.task_manager.request_capacity` | How many requests can Task Manager buffer before it rejects new requests. Defaults to 1000. -| `xpack.task_manager.index` - | The name of the index used to store task information. Defaults to `.kibana_task_manager`. - | `xpack.task_manager.max_workers` | The maximum number of tasks that this Kibana instance will run simultaneously. Defaults to 10. Starting in 8.0, it will not be possible to set the value greater than 100. diff --git a/docs/user/production-considerations/task-manager-production-considerations.asciidoc b/docs/user/production-considerations/task-manager-production-considerations.asciidoc index 17eae59ff2f9c..36745b913544b 100644 --- a/docs/user/production-considerations/task-manager-production-considerations.asciidoc +++ b/docs/user/production-considerations/task-manager-production-considerations.asciidoc @@ -12,7 +12,7 @@ This has three major benefits: [IMPORTANT] ============================================== -Task definitions for alerts and actions are stored in the index specified by <>. The default is `.kibana_task_manager`. +Task definitions for alerts and actions are stored in the index `.kibana_task_manager`. You must have at least one replica of this index for production deployments. diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index e2d81c5ae1752..e65c5542cce7e 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -395,7 +395,6 @@ kibana_vars=( xpack.spaces.enabled xpack.spaces.maxSpaces xpack.task_manager.enabled - xpack.task_manager.index xpack.task_manager.max_attempts xpack.task_manager.max_poll_inactivity_cycles xpack.task_manager.max_workers diff --git a/x-pack/plugins/task_manager/server/config.test.ts b/x-pack/plugins/task_manager/server/config.test.ts index 14d95e3fd2226..e237f5592419b 100644 --- a/x-pack/plugins/task_manager/server/config.test.ts +++ b/x-pack/plugins/task_manager/server/config.test.ts @@ -17,7 +17,6 @@ describe('config validation', () => { "enabled": false, "request_capacity": 10, }, - "index": ".kibana_task_manager", "max_attempts": 3, "max_poll_inactivity_cycles": 10, "max_workers": 10, @@ -42,17 +41,6 @@ describe('config validation', () => { `); }); - test('the ElastiSearch Tasks index cannot be used for task manager', () => { - const config: Record = { - index: '.tasks', - }; - expect(() => { - configSchema.validate(config); - }).toThrowErrorMatchingInlineSnapshot( - `"[index]: \\".tasks\\" is an invalid Kibana Task Manager index, as it is already in use by the ElasticSearch Tasks Manager"` - ); - }); - test('the required freshness of the monitored stats config must always be less-than-equal to the poll interval', () => { const config: Record = { monitored_stats_required_freshness: 100, @@ -73,7 +61,6 @@ describe('config validation', () => { "enabled": false, "request_capacity": 10, }, - "index": ".kibana_task_manager", "max_attempts": 3, "max_poll_inactivity_cycles": 10, "max_workers": 10, @@ -116,7 +103,6 @@ describe('config validation', () => { "enabled": false, "request_capacity": 10, }, - "index": ".kibana_task_manager", "max_attempts": 3, "max_poll_inactivity_cycles": 10, "max_workers": 10, diff --git a/x-pack/plugins/task_manager/server/config.ts b/x-pack/plugins/task_manager/server/config.ts index 9b4f4856bf8a9..7c541cd24cefd 100644 --- a/x-pack/plugins/task_manager/server/config.ts +++ b/x-pack/plugins/task_manager/server/config.ts @@ -65,15 +65,6 @@ export const configSchema = schema.object( defaultValue: 1000, min: 1, }), - /* The name of the index used to store task information. */ - index: schema.string({ - defaultValue: '.kibana_task_manager', - validate: (val) => { - if (val.toLowerCase() === '.tasks') { - return `"${val}" is an invalid Kibana Task Manager index, as it is already in use by the ElasticSearch Tasks Manager`; - } - }, - }), /* The maximum number of tasks that this Kibana instance will run simultaneously. */ max_workers: schema.number({ defaultValue: DEFAULT_MAX_WORKERS, diff --git a/x-pack/plugins/task_manager/server/constants.ts b/x-pack/plugins/task_manager/server/constants.ts new file mode 100644 index 0000000000000..9334fbede3176 --- /dev/null +++ b/x-pack/plugins/task_manager/server/constants.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const TASK_MANAGER_INDEX = '.kibana_task_manager'; diff --git a/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts b/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts index 182e7cd5bcabf..859f242f2f0a6 100644 --- a/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts +++ b/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts @@ -40,7 +40,6 @@ describe('EphemeralTaskLifecycle', () => { config: { enabled: true, max_workers: 10, - index: 'foo', max_attempts: 9, poll_interval: 6000000, version_conflict_threshold: 80, diff --git a/x-pack/plugins/task_manager/server/index.test.ts b/x-pack/plugins/task_manager/server/index.test.ts index 8eb98c39a2ccd..74d86c31e1bd1 100644 --- a/x-pack/plugins/task_manager/server/index.test.ts +++ b/x-pack/plugins/task_manager/server/index.test.ts @@ -31,17 +31,6 @@ const applyTaskManagerDeprecations = (settings: Record = {}) => }; describe('deprecations', () => { - ['.foo', '.kibana_task_manager'].forEach((index) => { - it('logs a warning if index is set', () => { - const { messages } = applyTaskManagerDeprecations({ index }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "\\"xpack.task_manager.index\\" is deprecated. Multitenancy by changing \\"kibana.index\\" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details", - ] - `); - }); - }); - it('logs a warning if max_workers is over limit', () => { const { messages } = applyTaskManagerDeprecations({ max_workers: 1000 }); expect(messages).toMatchInlineSnapshot(` diff --git a/x-pack/plugins/task_manager/server/index.ts b/x-pack/plugins/task_manager/server/index.ts index cc4217f41c5ef..067082955b3b1 100644 --- a/x-pack/plugins/task_manager/server/index.ts +++ b/x-pack/plugins/task_manager/server/index.ts @@ -41,18 +41,6 @@ export const config: PluginConfigDescriptor = { deprecations: () => [ (settings, fromPath, addDeprecation) => { const taskManager = get(settings, fromPath); - if (taskManager?.index) { - addDeprecation({ - documentationUrl: 'https://ela.st/kbn-remove-legacy-multitenancy', - message: `"${fromPath}.index" is deprecated. Multitenancy by changing "kibana.index" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details`, - correctiveActions: { - manualSteps: [ - `If you rely on this setting to achieve multitenancy you should use Spaces, cross-cluster replication, or cross-cluster search instead.`, - `To migrate to Spaces, we encourage using saved object management to export your saved objects from a tenant into the default tenant in a space.`, - ], - }, - }); - } if (taskManager?.max_workers > MAX_WORKERS_LIMIT) { addDeprecation({ message: `setting "${fromPath}.max_workers" (${taskManager?.max_workers}) greater than ${MAX_WORKERS_LIMIT} is deprecated. Values greater than ${MAX_WORKERS_LIMIT} will not be supported starting in 8.0.`, diff --git a/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts index 496c0138cb1e5..ce49466ff387c 100644 --- a/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts +++ b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts @@ -31,7 +31,6 @@ describe('managed configuration', () => { const context = coreMock.createPluginInitializerContext({ enabled: true, max_workers: 10, - index: 'foo', max_attempts: 9, poll_interval: 3000, version_conflict_threshold: 80, diff --git a/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts index 82a111305927f..e63beee7201fe 100644 --- a/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts @@ -15,7 +15,6 @@ describe('Configuration Statistics Aggregator', () => { const configuration: TaskManagerConfig = { enabled: true, max_workers: 10, - index: 'foo', max_attempts: 9, poll_interval: 6000000, version_conflict_threshold: 80, diff --git a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts index 50d4b6af9a4cf..d59d446144632 100644 --- a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts @@ -19,7 +19,6 @@ describe('createMonitoringStatsStream', () => { const configuration: TaskManagerConfig = { enabled: true, max_workers: 10, - index: 'foo', max_attempts: 9, poll_interval: 6000000, version_conflict_threshold: 80, diff --git a/x-pack/plugins/task_manager/server/plugin.test.ts b/x-pack/plugins/task_manager/server/plugin.test.ts index dff94259dbe62..de21b653823c9 100644 --- a/x-pack/plugins/task_manager/server/plugin.test.ts +++ b/x-pack/plugins/task_manager/server/plugin.test.ts @@ -18,7 +18,6 @@ describe('TaskManagerPlugin', () => { const pluginInitializerContext = coreMock.createPluginInitializerContext({ enabled: true, max_workers: 10, - index: 'foo', max_attempts: 9, poll_interval: 3000, version_conflict_threshold: 80, @@ -58,7 +57,6 @@ describe('TaskManagerPlugin', () => { const pluginInitializerContext = coreMock.createPluginInitializerContext({ enabled: true, max_workers: 10, - index: 'foo', max_attempts: 9, poll_interval: 3000, version_conflict_threshold: 80, diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index 3d3d180fc0665..c41bc8109ef4c 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -31,6 +31,7 @@ import { createMonitoringStats, MonitoringStats } from './monitoring'; import { EphemeralTaskLifecycle } from './ephemeral_task_lifecycle'; import { EphemeralTask } from './task'; import { registerTaskManagerUsageCollector } from './usage'; +import { TASK_MANAGER_INDEX } from './constants'; export type TaskManagerSetupContract = { /** @@ -114,7 +115,7 @@ export class TaskManagerPlugin } return { - index: this.config.index, + index: TASK_MANAGER_INDEX, addMiddleware: (middleware: Middleware) => { this.assertStillInSetup('add Middleware'); this.middleware = addMiddlewareToChain(this.middleware, middleware); @@ -134,7 +135,7 @@ export class TaskManagerPlugin serializer, savedObjectsRepository, esClient: elasticsearch.createClient('taskManager').asInternalUser, - index: this.config!.index, + index: TASK_MANAGER_INDEX, definitions: this.definitions, taskManagerId: `kibana:${this.taskManagerId!}`, }); diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts index aad03951bbb9b..1420a81b2dcaa 100644 --- a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts +++ b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts @@ -38,7 +38,6 @@ describe('TaskPollingLifecycle', () => { config: { enabled: true, max_workers: 10, - index: 'foo', max_attempts: 9, poll_interval: 6000000, version_conflict_threshold: 80, diff --git a/x-pack/plugins/task_manager/server/saved_objects/index.ts b/x-pack/plugins/task_manager/server/saved_objects/index.ts index d2d079c7747b1..e98a02b220d58 100644 --- a/x-pack/plugins/task_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/task_manager/server/saved_objects/index.ts @@ -11,6 +11,7 @@ import mappings from './mappings.json'; import { migrations } from './migrations'; import { TaskManagerConfig } from '../config.js'; import { getOldestIdleActionTask } from '../queries/oldest_idle_action_task'; +import { TASK_MANAGER_INDEX } from '../constants'; export function setupSavedObjects( savedObjects: SavedObjectsServiceSetup, @@ -23,11 +24,11 @@ export function setupSavedObjects( convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id; ctx._source.remove("kibana")`, mappings: mappings.task as SavedObjectsTypeMappingDefinition, migrations, - indexPattern: config.index, + indexPattern: TASK_MANAGER_INDEX, excludeOnUpgrade: async ({ readonlyEsClient }) => { const oldestNeededActionParams = await getOldestIdleActionTask( readonlyEsClient, - config.index + TASK_MANAGER_INDEX ); // Delete all action tasks that have failed and are no longer needed From a4f261a18714118cdfb0262225959556c87bb754 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 11 Aug 2021 14:53:30 -0600 Subject: [PATCH 099/104] [Maps] convert Join resources folder to TS (#108090) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../join_editor/join_editor.tsx | 1 - ....snap => metrics_expression.test.tsx.snap} | 26 +++--- .../resources/{join.js => join.tsx} | 88 +++++++++++++------ ...join_expression.js => join_expression.tsx} | 81 ++++++++++------- ...on.test.js => metrics_expression.test.tsx} | 10 ++- ...s_expression.js => metrics_expression.tsx} | 45 +++++----- ...ere_expression.js => where_expression.tsx} | 19 +++- 7 files changed, 165 insertions(+), 105 deletions(-) rename x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/__snapshots__/{metrics_expression.test.js.snap => metrics_expression.test.tsx.snap} (90%) rename x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/{join.js => join.tsx} (75%) rename x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/{join_expression.js => join_expression.tsx} (87%) rename x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/{metrics_expression.test.js => metrics_expression.test.tsx} (67%) rename x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/{metrics_expression.js => metrics_expression.tsx} (80%) rename x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/{where_expression.js => where_expression.tsx} (86%) diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.tsx index d07b39be6f6ab..e0d630994566d 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/join_editor.tsx @@ -19,7 +19,6 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -// @ts-expect-error import { Join } from './resources/join'; import { ILayer } from '../../../classes/layers/layer'; import { JoinDescriptor } from '../../../../common/descriptor_types'; diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.js.snap b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.tsx.snap similarity index 90% rename from x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.js.snap rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.tsx.snap index a9a1afabfc193..91eec4d8aac29 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.js.snap +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.tsx.snap @@ -43,13 +43,18 @@ exports[`Should render default props 1`] = ` values={Object {}} /> - - - + `; @@ -99,12 +104,7 @@ exports[`Should render metrics expression for metrics 1`] = ` void; + onRemove: () => void; + leftFields: JoinField[]; + leftSourceName: string; +} + +interface State { + rightFields: IFieldType[]; + indexPattern?: IndexPattern; + loadError?: string; +} + +export class Join extends Component { + private _isMounted = false; + + state: State = { + rightFields: [], indexPattern: undefined, loadError: undefined, }; @@ -36,7 +61,7 @@ export class Join extends Component { this._isMounted = false; } - async _loadRightFields(indexPatternId) { + async _loadRightFields(indexPatternId: string) { if (!indexPatternId) { return; } @@ -66,21 +91,26 @@ export class Join extends Component { }); } - _onLeftFieldChange = (leftField) => { + _onLeftFieldChange = (leftField: string) => { this.props.onChange({ - leftField: leftField, + leftField, right: this.props.join.right, }); }; - _onRightSourceChange = ({ indexPatternId, indexPatternTitle }) => { + _onRightSourceChange = ({ + indexPatternId, + indexPatternTitle, + }: { + indexPatternId: string; + indexPatternTitle: string; + }) => { this.setState({ - rightFields: undefined, + rightFields: [], loadError: undefined, }); this._loadRightFields(indexPatternId); - // eslint-disable-next-line no-unused-vars - const { term, ...restOfRight } = this.props.join.right; + const { term, ...restOfRight } = this.props.join.right as ESTermSourceDescriptor; this.props.onChange({ leftField: this.props.join.leftField, right: { @@ -88,74 +118,74 @@ export class Join extends Component { indexPatternId, indexPatternTitle, type: SOURCE_TYPES.ES_TERM_SOURCE, - }, + } as ESTermSourceDescriptor, }); }; - _onRightFieldChange = (term) => { + _onRightFieldChange = (term?: string) => { this.props.onChange({ leftField: this.props.join.leftField, right: { ...this.props.join.right, term, - }, + } as ESTermSourceDescriptor, }); }; - _onRightSizeChange = (size) => { + _onRightSizeChange = (size: number) => { this.props.onChange({ leftField: this.props.join.leftField, right: { ...this.props.join.right, size, - }, + } as ESTermSourceDescriptor, }); }; - _onMetricsChange = (metrics) => { + _onMetricsChange = (metrics: AggDescriptor[]) => { this.props.onChange({ leftField: this.props.join.leftField, right: { ...this.props.join.right, metrics, - }, + } as ESTermSourceDescriptor, }); }; - _onWhereQueryChange = (whereQuery) => { + _onWhereQueryChange = (whereQuery?: Query) => { this.props.onChange({ leftField: this.props.join.leftField, right: { ...this.props.join.right, whereQuery, - }, + } as ESTermSourceDescriptor, }); }; - _onApplyGlobalQueryChange = (applyGlobalQuery) => { + _onApplyGlobalQueryChange = (applyGlobalQuery: boolean) => { this.props.onChange({ leftField: this.props.join.leftField, right: { ...this.props.join.right, applyGlobalQuery, - }, + } as ESTermSourceDescriptor, }); }; - _onApplyGlobalTimeChange = (applyGlobalTime) => { + _onApplyGlobalTimeChange = (applyGlobalTime: boolean) => { this.props.onChange({ leftField: this.props.join.leftField, right: { ...this.props.join.right, applyGlobalTime, - }, + } as ESTermSourceDescriptor, }); }; render() { const { join, onRemove, leftFields, leftSourceName } = this.props; const { rightFields, indexPattern } = this.state; - const right = _.get(join, 'right', {}); + const right = _.get(join, 'right', {}) as ESTermSourceDescriptor; const rightSourceName = right.indexPatternTitle ? right.indexPatternTitle : right.indexPatternId; @@ -168,7 +198,7 @@ export class Join extends Component { metricsExpression = ( @@ -176,7 +206,9 @@ export class Join extends Component { ); globalFilterCheckbox = ( diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join_expression.js b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join_expression.tsx similarity index 87% rename from x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join_expression.js rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join_expression.tsx index 58e3e3aac0d6a..f2073a9f6e650 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join_expression.js +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join_expression.tsx @@ -7,30 +7,64 @@ import _ from 'lodash'; import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { EuiPopover, EuiPopoverTitle, EuiExpression, EuiFormRow, EuiComboBox, + EuiComboBoxOptionOption, EuiFormHelpText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { IFieldType } from 'src/plugins/data/public'; +import { FormattedMessage } from '@kbn/i18n/react'; import { DEFAULT_MAX_BUCKETS_LIMIT } from '../../../../../common/constants'; import { SingleFieldSelect } from '../../../../components/single_field_select'; import { ValidatedNumberInput } from '../../../../components/validated_number_input'; -import { FormattedMessage } from '@kbn/i18n/react'; import { getTermsFields } from '../../../../index_pattern_util'; import { getIndexPatternService, getIndexPatternSelectComponent, } from '../../../../kibana_services'; +import type { JoinField } from '../join_editor'; + +interface Props { + // Left source props (static - can not change) + leftSourceName?: string; + + // Left field props + leftValue?: string; + leftFields: JoinField[]; + onLeftFieldChange: (leftField: string) => void; -export class JoinExpression extends Component { - state = { + // Right source props + rightSourceIndexPatternId: string; + rightSourceName: string; + onRightSourceChange: ({ + indexPatternId, + indexPatternTitle, + }: { + indexPatternId: string; + indexPatternTitle: string; + }) => void; + + // Right field props + rightValue: string; + rightSize?: number; + rightFields: IFieldType[]; + onRightFieldChange: (term?: string) => void; + onRightSizeChange: (size: number) => void; +} + +interface State { + isPopoverOpen: boolean; +} + +export class JoinExpression extends Component { + state: State = { isPopoverOpen: false, }; @@ -46,7 +80,11 @@ export class JoinExpression extends Component { }); }; - _onRightSourceChange = async (indexPatternId) => { + _onRightSourceChange = async (indexPatternId?: string) => { + if (!indexPatternId || indexPatternId.length === 0) { + return; + } + try { const indexPattern = await getIndexPatternService().get(indexPatternId); this.props.onRightSourceChange({ @@ -58,7 +96,7 @@ export class JoinExpression extends Component { } }; - _onLeftFieldChange = (selectedFields) => { + _onLeftFieldChange = (selectedFields: Array>) => { this.props.onLeftFieldChange(_.get(selectedFields, '[0].value.name', null)); }; @@ -246,7 +284,9 @@ export class JoinExpression extends Component { })} > @@ -263,33 +303,6 @@ export class JoinExpression extends Component { } } -JoinExpression.propTypes = { - // Left source props (static - can not change) - leftSourceName: PropTypes.string, - - // Left field props - leftValue: PropTypes.string, - leftFields: PropTypes.arrayOf( - PropTypes.shape({ - label: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - }) - ), - onLeftFieldChange: PropTypes.func.isRequired, - - // Right source props - rightSourceIndexPatternId: PropTypes.string, - rightSourceName: PropTypes.string, - onRightSourceChange: PropTypes.func.isRequired, - - // Right field props - rightValue: PropTypes.string, - rightSize: PropTypes.number, - rightFields: PropTypes.array, - onRightFieldChange: PropTypes.func.isRequired, - onRightSizeChange: PropTypes.func.isRequired, -}; - function getSelectFieldPlaceholder() { return i18n.translate('xpack.maps.layerPanel.joinExpression.selectFieldPlaceholder', { defaultMessage: 'Select field', diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.test.js b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.test.tsx similarity index 67% rename from x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.test.js rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.test.tsx index 8140d7a36ea9b..aa696383fa37c 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.test.js +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.test.tsx @@ -8,9 +8,12 @@ import React from 'react'; import { shallow } from 'enzyme'; import { MetricsExpression } from './metrics_expression'; +import { AGG_TYPE } from '../../../../../common/constants'; const defaultProps = { onChange: () => {}, + metrics: [{ type: AGG_TYPE.COUNT }], + rightFields: [], }; test('Should render default props', () => { @@ -23,11 +26,10 @@ test('Should render metrics expression for metrics', () => { const component = shallow( ); diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.js b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.tsx similarity index 80% rename from x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.js rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.tsx index 581cb75b4500a..899430f3c2f2d 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.js +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.tsx @@ -6,7 +6,6 @@ */ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { i18n } from '@kbn/i18n'; import { EuiPopover, @@ -16,12 +15,24 @@ import { EuiFormHelpText, } from '@elastic/eui'; -import { MetricsEditor } from '../../../../components/metrics_editor'; +import { IFieldType } from 'src/plugins/data/public'; import { FormattedMessage } from '@kbn/i18n/react'; +import { MetricsEditor } from '../../../../components/metrics_editor'; import { AGG_TYPE } from '../../../../../common/constants'; +import { AggDescriptor, FieldedAggDescriptor } from '../../../../../common/descriptor_types'; + +interface Props { + metrics: AggDescriptor[]; + rightFields: IFieldType[]; + onChange: (metrics: AggDescriptor[]) => void; +} -export class MetricsExpression extends Component { - state = { +interface State { + isPopoverOpen: boolean; +} + +export class MetricsExpression extends Component { + state: State = { isPopoverOpen: false, }; @@ -61,23 +72,23 @@ export class MetricsExpression extends Component { render() { const metricExpressions = this.props.metrics - .filter(({ type, field }) => { - if (type === AGG_TYPE.COUNT) { + .filter((metric: AggDescriptor) => { + if (metric.type === AGG_TYPE.COUNT) { return true; } - if (field) { + if ((metric as FieldedAggDescriptor).field) { return true; } return false; }) - .map(({ type, field }) => { + .map((metric: AggDescriptor) => { // do not use metric label so field and aggregation are not obscured. - if (type === AGG_TYPE.COUNT) { - return 'count'; + if (metric.type === AGG_TYPE.COUNT) { + return AGG_TYPE.COUNT; } - return `${type} ${field}`; + return `${metric.type} ${(metric as FieldedAggDescriptor).field}`; }); const useMetricDescription = i18n.translate( 'xpack.maps.layerPanel.metricsExpression.useMetricsDescription', @@ -101,7 +112,7 @@ export class MetricsExpression extends Component { onClick={this._togglePopover} description={useMetricDescription} uppercase={false} - value={metricExpressions.length > 0 ? metricExpressions.join(', ') : 'count'} + value={metricExpressions.length > 0 ? metricExpressions.join(', ') : AGG_TYPE.COUNT} /> } > @@ -124,13 +135,3 @@ export class MetricsExpression extends Component { ); } } - -MetricsExpression.propTypes = { - metrics: PropTypes.array, - rightFields: PropTypes.array, - onChange: PropTypes.func.isRequired, -}; - -MetricsExpression.defaultProps = { - metrics: [{ type: AGG_TYPE.COUNT }], -}; diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/where_expression.js b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/where_expression.tsx similarity index 86% rename from x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/where_expression.js rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/where_expression.tsx index 93ff3c95d184e..16cef0d5bdad6 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/where_expression.js +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/where_expression.tsx @@ -9,10 +9,22 @@ import React, { Component } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButton, EuiPopover, EuiExpression, EuiFormHelpText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { IndexPattern, Query } from 'src/plugins/data/public'; +import { APP_ID } from '../../../../../common/constants'; import { getData } from '../../../../kibana_services'; -export class WhereExpression extends Component { - state = { +interface Props { + indexPattern: IndexPattern; + onChange: (whereQuery?: Query) => void; + whereQuery?: Query; +} + +interface State { + isPopoverOpen: boolean; +} + +export class WhereExpression extends Component { + state: State = { isPopoverOpen: false, }; @@ -28,7 +40,7 @@ export class WhereExpression extends Component { }); }; - _onQueryChange = ({ query }) => { + _onQueryChange = ({ query }: { query?: Query }) => { this.props.onChange(query); this._closePopover(); }; @@ -73,6 +85,7 @@ export class WhereExpression extends Component { /> Date: Wed, 11 Aug 2021 17:18:04 -0400 Subject: [PATCH 100/104] [Security Solution][RAC] - Add reason field (#107532) --- .../detection_alerts/alerts_details.spec.ts | 2 +- .../detection_rules/custom_query_rule.spec.ts | 4 - .../event_correlation_rule.spec.ts | 6 -- .../indicator_match_rule.spec.ts | 4 - .../detection_rules/override.spec.ts | 4 - .../detection_rules/threshold_rule.spec.ts | 4 - .../security_solution_detections/columns.ts | 30 ++----- .../get_signals_template.test.ts.snap | 14 ++++ .../routes/index/signal_aad_mapping.json | 2 + .../routes/index/signal_extra_fields.json | 3 + .../routes/index/signals_mapping.json | 6 ++ .../factories/utils/build_alert.test.ts | 9 ++- .../rule_types/factories/utils/build_alert.ts | 5 +- .../factories/utils/build_bulk_body.ts | 11 ++- .../rule_types/factories/wrap_hits_factory.ts | 11 ++- .../rule_types/field_maps/alerts.ts | 5 ++ .../signals/build_bulk_body.test.ts | 56 ++++++++++--- .../signals/build_bulk_body.ts | 36 ++++++--- .../signals/build_signal.test.ts | 9 ++- .../detection_engine/signals/build_signal.ts | 3 +- .../signals/bulk_create_ml_signals.ts | 3 +- .../detection_engine/signals/executors/eql.ts | 5 +- .../signals/executors/query.ts | 2 + .../signals/reason_formatter.test.ts | 78 +++++++++++++++++++ .../signals/reason_formatters.ts | 73 +++++++++++++++++ .../signals/search_after_bulk_create.test.ts | 16 ++++ .../signals/search_after_bulk_create.ts | 3 +- .../threat_mapping/create_threat_signal.ts | 2 + .../bulk_create_threshold_signals.ts | 5 +- .../lib/detection_engine/signals/types.ts | 13 +++- .../signals/wrap_hits_factory.ts | 4 +- .../signals/wrap_sequences_factory.ts | 10 ++- .../timelines/common/ecs/ecs_fields/index.ts | 1 + .../public/components/t_grid/body/helpers.tsx | 1 + .../timeline/factory/events/all/constants.ts | 1 + .../security_and_spaces/tests/create_ml.ts | 1 + .../tests/create_threat_matching.ts | 1 + .../tests/generating_signals.ts | 26 +++++-- 38 files changed, 373 insertions(+), 96 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatter.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatters.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts index 10ebae84365f5..f5cbc65effd85 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts @@ -54,7 +54,7 @@ describe('Alert details with unmapped fields', () => { it('Displays the unmapped field on the table', () => { const expectedUnmmappedField = { - row: 88, + row: 90, field: 'unmapped', text: 'This is the unmapped field', }; diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts index 7d833b134ddd7..a6043123ce0a8 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts @@ -14,11 +14,9 @@ import { getNewOverrideRule, } from '../../objects/rule'; import { - ALERT_RULE_METHOD, ALERT_RULE_NAME, ALERT_RULE_RISK_SCORE, ALERT_RULE_SEVERITY, - ALERT_RULE_VERSION, NUMBER_OF_ALERTS, } from '../../screens/alerts'; @@ -223,8 +221,6 @@ describe('Custom detection rules creation', () => { cy.get(NUMBER_OF_ALERTS).should(($count) => expect(+$count.text()).to.be.gte(1)); cy.get(ALERT_RULE_NAME).first().should('have.text', this.rule.name); - cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); - cy.get(ALERT_RULE_METHOD).first().should('have.text', 'query'); cy.get(ALERT_RULE_SEVERITY).first().should('have.text', this.rule.severity.toLowerCase()); cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', this.rule.riskScore); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts index 677a9b5546494..e06026ce12c7c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts @@ -9,11 +9,9 @@ import { formatMitreAttackDescription } from '../../helpers/rules'; import { getEqlRule, getEqlSequenceRule, getIndexPatterns } from '../../objects/rule'; import { - ALERT_RULE_METHOD, ALERT_RULE_NAME, ALERT_RULE_RISK_SCORE, ALERT_RULE_SEVERITY, - ALERT_RULE_VERSION, NUMBER_OF_ALERTS, } from '../../screens/alerts'; import { @@ -169,8 +167,6 @@ describe('Detection rules, EQL', () => { cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts); cy.get(ALERT_RULE_NAME).first().should('have.text', this.rule.name); - cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); - cy.get(ALERT_RULE_METHOD).first().should('have.text', 'eql'); cy.get(ALERT_RULE_SEVERITY).first().should('have.text', this.rule.severity.toLowerCase()); cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', this.rule.riskScore); }); @@ -221,8 +217,6 @@ describe('Detection rules, sequence EQL', () => { cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfSequenceAlerts); cy.get(ALERT_RULE_NAME).first().should('have.text', this.rule.name); - cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); - cy.get(ALERT_RULE_METHOD).first().should('have.text', 'eql'); cy.get(ALERT_RULE_SEVERITY).first().should('have.text', this.rule.severity.toLowerCase()); cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', this.rule.riskScore); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index 07b40df53e2d5..ff000c105a1b4 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -9,11 +9,9 @@ import { formatMitreAttackDescription } from '../../helpers/rules'; import { getIndexPatterns, getNewThreatIndicatorRule } from '../../objects/rule'; import { - ALERT_RULE_METHOD, ALERT_RULE_NAME, ALERT_RULE_RISK_SCORE, ALERT_RULE_SEVERITY, - ALERT_RULE_VERSION, NUMBER_OF_ALERTS, } from '../../screens/alerts'; import { @@ -482,8 +480,6 @@ describe('indicator match', () => { cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts); cy.get(ALERT_RULE_NAME).first().should('have.text', getNewThreatIndicatorRule().name); - cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); - cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threat_match'); cy.get(ALERT_RULE_SEVERITY) .first() .should('have.text', getNewThreatIndicatorRule().severity.toLowerCase()); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts index 24a56dd563e17..24c98aaee8f97 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts @@ -16,10 +16,8 @@ import { import { NUMBER_OF_ALERTS, ALERT_RULE_NAME, - ALERT_RULE_METHOD, ALERT_RULE_RISK_SCORE, ALERT_RULE_SEVERITY, - ALERT_RULE_VERSION, } from '../../screens/alerts'; import { @@ -196,8 +194,6 @@ describe('Detection rules, override', () => { cy.get(NUMBER_OF_ALERTS).should(($count) => expect(+$count.text()).to.be.gte(1)); cy.get(ALERT_RULE_NAME).first().should('have.text', 'auditbeat'); - cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); - cy.get(ALERT_RULE_METHOD).first().should('have.text', 'query'); cy.get(ALERT_RULE_SEVERITY).first().should('have.text', 'critical'); sortRiskScore(); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts index dba12fb4ab95c..665df89435952 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts @@ -14,11 +14,9 @@ import { } from '../../objects/rule'; import { - ALERT_RULE_METHOD, ALERT_RULE_NAME, ALERT_RULE_RISK_SCORE, ALERT_RULE_SEVERITY, - ALERT_RULE_VERSION, NUMBER_OF_ALERTS, } from '../../screens/alerts'; @@ -179,8 +177,6 @@ describe('Detection rules, threshold', () => { cy.get(NUMBER_OF_ALERTS).should(($count) => expect(+$count.text()).to.be.lt(100)); cy.get(ALERT_RULE_NAME).first().should('have.text', rule.name); - cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); - cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threshold'); cy.get(ALERT_RULE_SEVERITY).first().should('have.text', rule.severity.toLowerCase()); cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', rule.riskScore); }); diff --git a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts index d6d3d829d3be5..89de83ab6e5cf 100644 --- a/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts +++ b/x-pack/plugins/security_solution/public/detections/configurations/security_solution_detections/columns.ts @@ -35,18 +35,6 @@ export const columns: Array< initialWidth: DEFAULT_COLUMN_MIN_WIDTH, linkField: 'signal.rule.id', }, - { - columnHeaderType: defaultColumnHeaderType, - displayAsText: i18n.ALERTS_HEADERS_VERSION, - id: 'signal.rule.version', - initialWidth: 95, - }, - { - columnHeaderType: defaultColumnHeaderType, - displayAsText: i18n.ALERTS_HEADERS_METHOD, - id: 'signal.rule.type', - initialWidth: 100, - }, { columnHeaderType: defaultColumnHeaderType, displayAsText: i18n.ALERTS_HEADERS_SEVERITY, @@ -57,31 +45,29 @@ export const columns: Array< columnHeaderType: defaultColumnHeaderType, displayAsText: i18n.ALERTS_HEADERS_RISK_SCORE, id: 'signal.rule.risk_score', - initialWidth: 115, + initialWidth: 100, }, { columnHeaderType: defaultColumnHeaderType, - id: 'event.module', - linkField: 'rule.reference', + displayAsText: i18n.ALERTS_HEADERS_REASON, + id: 'signal.reason', + initialWidth: 450, }, { - aggregatable: true, - category: 'event', columnHeaderType: defaultColumnHeaderType, - id: 'event.action', - type: 'string', + id: 'host.name', }, { columnHeaderType: defaultColumnHeaderType, - id: 'event.category', + id: 'user.name', }, { columnHeaderType: defaultColumnHeaderType, - id: 'host.name', + id: 'process.name', }, { columnHeaderType: defaultColumnHeaderType, - id: 'user.name', + id: 'file.name', }, { columnHeaderType: defaultColumnHeaderType, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap index 9fd3e20f79b43..f07bed9fa556a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap @@ -1639,6 +1639,10 @@ Object { "path": "signal.original_event.provider", "type": "alias", }, + "kibana.alert.original_event.reason": Object { + "path": "signal.original_event.reason", + "type": "alias", + }, "kibana.alert.original_event.risk_score": Object { "path": "signal.original_event.risk_score", "type": "alias", @@ -1671,6 +1675,10 @@ Object { "path": "signal.original_time", "type": "alias", }, + "kibana.alert.reason": Object { + "path": "signal.reason", + "type": "alias", + }, "kibana.alert.risk_score": Object { "path": "signal.rule.risk_score", "type": "alias", @@ -3249,6 +3257,9 @@ Object { "provider": Object { "type": "keyword", }, + "reason": Object { + "type": "keyword", + }, "risk_score": Object { "type": "float", }, @@ -3318,6 +3329,9 @@ Object { }, }, }, + "reason": Object { + "type": "keyword", + }, "rule": Object { "properties": Object { "author": Object { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_aad_mapping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_aad_mapping.json index 066fdbc87f906..68c184b66c562 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_aad_mapping.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_aad_mapping.json @@ -17,6 +17,7 @@ "signal.original_event.module": "kibana.alert.original_event.module", "signal.original_event.outcome": "kibana.alert.original_event.outcome", "signal.original_event.provider": "kibana.alert.original_event.provider", + "signal.original_event.reason": "kibana.alert.original_event.reason", "signal.original_event.risk_score": "kibana.alert.original_event.risk_score", "signal.original_event.risk_score_norm": "kibana.alert.original_event.risk_score_norm", "signal.original_event.sequence": "kibana.alert.original_event.sequence", @@ -25,6 +26,7 @@ "signal.original_event.timezone": "kibana.alert.original_event.timezone", "signal.original_event.type": "kibana.alert.original_event.type", "signal.original_time": "kibana.alert.original_time", + "signal.reason": "kibana.alert.reason", "signal.rule.author": "kibana.alert.rule.author", "signal.rule.building_block_type": "kibana.alert.rule.building_block_type", "signal.rule.created_at": "kibana.alert.rule.created_at", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_extra_fields.json b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_extra_fields.json index e20aa0ef16df4..7bc20fd540b9b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_extra_fields.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signal_extra_fields.json @@ -43,6 +43,9 @@ } } }, + "reason": { + "type": "keyword" + }, "rule": { "type": "object", "properties": { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json index d6a06848592cc..4f754ecd2d33a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json @@ -360,6 +360,9 @@ "provider": { "type": "keyword" }, + "reason": { + "type": "keyword" + }, "risk_score": { "type": "float" }, @@ -421,6 +424,9 @@ }, "depth": { "type": "integer" + }, + "reason": { + "type": "keyword" } } } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts index 4c59063d39e60..09f35e279a244 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.test.ts @@ -6,6 +6,7 @@ */ import { + ALERT_REASON, ALERT_RULE_CONSUMER, ALERT_RULE_NAMESPACE, ALERT_STATUS, @@ -50,8 +51,9 @@ describe('buildAlert', () => { const doc = sampleDocNoSortIdWithTimestamp('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); delete doc._source.event; const rule = getRulesSchemaMock(); + const reason = 'alert reasonable reason'; const alert = { - ...buildAlert([doc], rule, SPACE_ID), + ...buildAlert([doc], rule, SPACE_ID, reason), ...additionalAlertFields(doc), }; const timestamp = alert['@timestamp']; @@ -68,6 +70,7 @@ describe('buildAlert', () => { }, ], [ALERT_ORIGINAL_TIME]: '2020-04-20T21:27:45.000Z', + [ALERT_REASON]: 'alert reasonable reason', [ALERT_STATUS]: 'open', [ALERT_WORKFLOW_STATUS]: 'open', ...flattenWithPrefix(ALERT_RULE_NAMESPACE, { @@ -119,8 +122,9 @@ describe('buildAlert', () => { module: 'system', }; const rule = getRulesSchemaMock(); + const reason = 'alert reasonable reason'; const alert = { - ...buildAlert([doc], rule, SPACE_ID), + ...buildAlert([doc], rule, SPACE_ID, reason), ...additionalAlertFields(doc), }; const timestamp = alert['@timestamp']; @@ -143,6 +147,7 @@ describe('buildAlert', () => { kind: 'event', module: 'system', }, + [ALERT_REASON]: 'alert reasonable reason', [ALERT_STATUS]: 'open', [ALERT_WORKFLOW_STATUS]: 'open', ...flattenWithPrefix(ALERT_RULE_NAMESPACE, { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts index ec667fa50934b..eea85ba26faf8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert.ts @@ -6,6 +6,7 @@ */ import { + ALERT_REASON, ALERT_RULE_CONSUMER, ALERT_RULE_NAMESPACE, ALERT_STATUS, @@ -92,7 +93,8 @@ export const removeClashes = (doc: SimpleHit) => { export const buildAlert = ( docs: SimpleHit[], rule: RulesSchema, - spaceId: string | null | undefined + spaceId: string | null | undefined, + reason: string ): RACAlert => { const removedClashes = docs.map(removeClashes); const parents = removedClashes.map(buildParent); @@ -110,6 +112,7 @@ export const buildAlert = ( [ALERT_STATUS]: 'open', [ALERT_WORKFLOW_STATUS]: 'open', [ALERT_DEPTH]: depth, + [ALERT_REASON]: reason, ...flattenWithPrefix(ALERT_RULE_NAMESPACE, rule), } as unknown) as RACAlert; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts index ca5857e0ee395..a67337d3b779d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_bulk_body.ts @@ -9,6 +9,7 @@ import { SavedObject } from 'src/core/types'; import { BaseHit } from '../../../../../../common/detection_engine/types'; import type { ConfigType } from '../../../../../config'; import { buildRuleWithOverrides, buildRuleWithoutOverrides } from '../../../signals/build_rule'; +import { BuildReasonMessage } from '../../../signals/reason_formatters'; import { getMergeStrategy } from '../../../signals/source_fields_merging/strategies'; import { AlertAttributes, SignalSource, SignalSourceHit } from '../../../signals/types'; import { RACAlert } from '../../types'; @@ -35,19 +36,23 @@ export const buildBulkBody = ( ruleSO: SavedObject, doc: SignalSourceHit, mergeStrategy: ConfigType['alertMergeStrategy'], - applyOverrides: boolean + applyOverrides: boolean, + buildReasonMessage: BuildReasonMessage ): RACAlert => { const mergedDoc = getMergeStrategy(mergeStrategy)({ doc }); const rule = applyOverrides ? buildRuleWithOverrides(ruleSO, mergedDoc._source ?? {}) : buildRuleWithoutOverrides(ruleSO); const filteredSource = filterSource(mergedDoc); + const timestamp = new Date().toISOString(); + + const reason = buildReasonMessage({ mergedDoc, rule, timestamp }); if (isSourceDoc(mergedDoc)) { return { ...filteredSource, - ...buildAlert([mergedDoc], rule, spaceId), + ...buildAlert([mergedDoc], rule, spaceId, reason), ...additionalAlertFields(mergedDoc), - '@timestamp': new Date().toISOString(), + '@timestamp': timestamp, }; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts index 0b00b2f656379..62946c52b7f40 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/wrap_hits_factory.ts @@ -24,7 +24,7 @@ export const wrapHitsFactory = ({ ruleSO: SearchAfterAndBulkCreateParams['ruleSO']; mergeStrategy: ConfigType['alertMergeStrategy']; spaceId: string | null | undefined; -}): WrapHits => (events) => { +}): WrapHits => (events, buildReasonMessage) => { try { const wrappedDocs: WrappedRACAlert[] = events.flatMap((doc) => [ { @@ -35,7 +35,14 @@ export const wrapHitsFactory = ({ String(doc._version), ruleSO.attributes.params.ruleId ?? '' ), - _source: buildBulkBody(spaceId, ruleSO, doc as SignalSourceHit, mergeStrategy, true), + _source: buildBulkBody( + spaceId, + ruleSO, + doc as SignalSourceHit, + mergeStrategy, + true, + buildReasonMessage + ), }, ]); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts index 7ab998fe16074..1c4b7f03fd73f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/field_maps/alerts.ts @@ -193,6 +193,11 @@ export const alertsFieldMap: FieldMap = { array: false, required: true, }, + 'kibana.alert.reason': { + type: 'keyword', + array: false, + required: false, + }, 'kibana.alert.threat': { type: 'object', array: false, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts index 117dcdf0c18da..206f3ae59d246 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts @@ -37,11 +37,13 @@ describe('buildBulkBody', () => { test('bulk body builds well-defined body', () => { const ruleSO = sampleRuleSO(getQueryRuleParams()); const doc = sampleDocNoSortId(); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); delete doc._source.source; const fakeSignalSourceHit: SignalHitOptionalTimestamp = buildBulkBody( ruleSO, doc, - 'missingFields' + 'missingFields', + buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; @@ -77,6 +79,7 @@ describe('buildBulkBody', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + reason: 'reasonable reason', status: 'open', rule: expectedRule(), depth: 1, @@ -91,6 +94,7 @@ describe('buildBulkBody', () => { test('bulk body builds well-defined body with threshold results', () => { const ruleSO = sampleRuleSO(getThresholdRuleParams()); const baseDoc = sampleDocNoSortId(); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); const doc: SignalSourceHit & { _source: Required['_source'] } = { ...baseDoc, _source: { @@ -109,7 +113,8 @@ describe('buildBulkBody', () => { const fakeSignalSourceHit: SignalHitOptionalTimestamp = buildBulkBody( ruleSO, doc, - 'missingFields' + 'missingFields', + buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; @@ -145,6 +150,7 @@ describe('buildBulkBody', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + reason: 'reasonable reason', status: 'open', rule: { ...expectedRule(), @@ -181,6 +187,7 @@ describe('buildBulkBody', () => { test('bulk body builds original_event if it exists on the event to begin with', () => { const ruleSO = sampleRuleSO(getQueryRuleParams()); const doc = sampleDocNoSortId(); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); delete doc._source.source; doc._source.event = { action: 'socket_opened', @@ -191,7 +198,8 @@ describe('buildBulkBody', () => { const fakeSignalSourceHit: SignalHitOptionalTimestamp = buildBulkBody( ruleSO, doc, - 'missingFields' + 'missingFields', + buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; @@ -227,6 +235,7 @@ describe('buildBulkBody', () => { depth: 0, }, ], + reason: 'reasonable reason', ancestors: [ { id: sampleIdGuid, @@ -250,6 +259,7 @@ describe('buildBulkBody', () => { test('bulk body builds original_event if it exists on the event to begin with but no kind information', () => { const ruleSO = sampleRuleSO(getQueryRuleParams()); const doc = sampleDocNoSortId(); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); delete doc._source.source; doc._source.event = { action: 'socket_opened', @@ -259,7 +269,8 @@ describe('buildBulkBody', () => { const fakeSignalSourceHit: SignalHitOptionalTimestamp = buildBulkBody( ruleSO, doc, - 'missingFields' + 'missingFields', + buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; @@ -303,6 +314,7 @@ describe('buildBulkBody', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + reason: 'reasonable reason', status: 'open', rule: expectedRule(), depth: 1, @@ -317,6 +329,7 @@ describe('buildBulkBody', () => { test('bulk body builds original_event if it exists on the event to begin with with only kind information', () => { const ruleSO = sampleRuleSO(getQueryRuleParams()); const doc = sampleDocNoSortId(); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); delete doc._source.source; doc._source.event = { kind: 'event', @@ -324,7 +337,8 @@ describe('buildBulkBody', () => { const fakeSignalSourceHit: SignalHitOptionalTimestamp = buildBulkBody( ruleSO, doc, - 'missingFields' + 'missingFields', + buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; @@ -363,6 +377,7 @@ describe('buildBulkBody', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + reason: 'reasonable reason', status: 'open', rule: expectedRule(), depth: 1, @@ -377,6 +392,7 @@ describe('buildBulkBody', () => { test('bulk body builds "original_signal" if it exists already as a numeric', () => { const ruleSO = sampleRuleSO(getQueryRuleParams()); const sampleDoc = sampleDocNoSortId(); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); delete sampleDoc._source.source; const doc = ({ ...sampleDoc, @@ -388,7 +404,8 @@ describe('buildBulkBody', () => { const { '@timestamp': timestamp, ...fakeSignalSourceHit } = buildBulkBody( ruleSO, doc, - 'missingFields' + 'missingFields', + buildReasonMessage ); const expected: Omit & { someKey: string } = { someKey: 'someValue', @@ -423,6 +440,7 @@ describe('buildBulkBody', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + reason: 'reasonable reason', status: 'open', rule: expectedRule(), depth: 1, @@ -437,6 +455,7 @@ describe('buildBulkBody', () => { test('bulk body builds "original_signal" if it exists already as an object', () => { const ruleSO = sampleRuleSO(getQueryRuleParams()); const sampleDoc = sampleDocNoSortId(); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); delete sampleDoc._source.source; const doc = ({ ...sampleDoc, @@ -448,7 +467,8 @@ describe('buildBulkBody', () => { const { '@timestamp': timestamp, ...fakeSignalSourceHit } = buildBulkBody( ruleSO, doc, - 'missingFields' + 'missingFields', + buildReasonMessage ); const expected: Omit & { someKey: string } = { someKey: 'someValue', @@ -483,6 +503,7 @@ describe('buildBulkBody', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + reason: 'reasonable reason', status: 'open', rule: expectedRule(), depth: 1, @@ -504,7 +525,12 @@ describe('buildSignalFromSequence', () => { block2._source.new_key = 'new_key_value'; const blocks = [block1, block2]; const ruleSO = sampleRuleSO(getQueryRuleParams()); - const signal: SignalHitOptionalTimestamp = buildSignalFromSequence(blocks, ruleSO); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); + const signal: SignalHitOptionalTimestamp = buildSignalFromSequence( + blocks, + ruleSO, + buildReasonMessage + ); // Timestamp will potentially always be different so remove it for the test delete signal['@timestamp']; const expected: Omit & { new_key: string } = { @@ -573,6 +599,7 @@ describe('buildSignalFromSequence', () => { }, ], status: 'open', + reason: 'reasonable reason', rule: expectedRule(), depth: 2, group: { @@ -589,7 +616,12 @@ describe('buildSignalFromSequence', () => { block2._source['@timestamp'] = '2021-05-20T22:28:46+0000'; block2._source.someKey = 'someOtherValue'; const ruleSO = sampleRuleSO(getQueryRuleParams()); - const signal: SignalHitOptionalTimestamp = buildSignalFromSequence([block1, block2], ruleSO); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); + const signal: SignalHitOptionalTimestamp = buildSignalFromSequence( + [block1, block2], + ruleSO, + buildReasonMessage + ); // Timestamp will potentially always be different so remove it for the test delete signal['@timestamp']; const expected: Omit = { @@ -657,6 +689,7 @@ describe('buildSignalFromSequence', () => { }, ], status: 'open', + reason: 'reasonable reason', rule: expectedRule(), depth: 2, group: { @@ -673,11 +706,13 @@ describe('buildSignalFromEvent', () => { const ancestor = sampleDocWithAncestors().hits.hits[0]; delete ancestor._source.source; const ruleSO = sampleRuleSO(getQueryRuleParams()); + const buildReasonMessage = jest.fn().mockReturnValue('reasonable reason'); const signal: SignalHitOptionalTimestamp = buildSignalFromEvent( ancestor, ruleSO, true, - 'missingFields' + 'missingFields', + buildReasonMessage ); // Timestamp will potentially always be different so remove it for the test @@ -724,6 +759,7 @@ describe('buildSignalFromEvent', () => { }, ], status: 'open', + reason: 'reasonable reason', rule: expectedRule(), depth: 2, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts index 54a41be5cbade..626dcb2fe83ff 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts @@ -22,6 +22,7 @@ import { buildEventTypeSignal } from './build_event_type_signal'; import { EqlSequence } from '../../../../common/detection_engine/types'; import { generateSignalId, wrapBuildingBlocks, wrapSignal } from './utils'; import type { ConfigType } from '../../../config'; +import { BuildReasonMessage } from './reason_formatters'; /** * Formats the search_after result for insertion into the signals index. We first create a @@ -35,12 +36,15 @@ import type { ConfigType } from '../../../config'; export const buildBulkBody = ( ruleSO: SavedObject, doc: SignalSourceHit, - mergeStrategy: ConfigType['alertMergeStrategy'] + mergeStrategy: ConfigType['alertMergeStrategy'], + buildReasonMessage: BuildReasonMessage ): SignalHit => { const mergedDoc = getMergeStrategy(mergeStrategy)({ doc }); const rule = buildRuleWithOverrides(ruleSO, mergedDoc._source ?? {}); + const timestamp = new Date().toISOString(); + const reason = buildReasonMessage({ mergedDoc, rule, timestamp }); const signal: Signal = { - ...buildSignal([mergedDoc], rule), + ...buildSignal([mergedDoc], rule, reason), ...additionalSignalFields(mergedDoc), }; const event = buildEventTypeSignal(mergedDoc); @@ -52,7 +56,7 @@ export const buildBulkBody = ( }; const signalHit: SignalHit = { ...filteredSource, - '@timestamp': new Date().toISOString(), + '@timestamp': timestamp, event, signal, }; @@ -71,11 +75,12 @@ export const buildSignalGroupFromSequence = ( sequence: EqlSequence, ruleSO: SavedObject, outputIndex: string, - mergeStrategy: ConfigType['alertMergeStrategy'] + mergeStrategy: ConfigType['alertMergeStrategy'], + buildReasonMessage: BuildReasonMessage ): WrappedSignalHit[] => { const wrappedBuildingBlocks = wrapBuildingBlocks( sequence.events.map((event) => { - const signal = buildSignalFromEvent(event, ruleSO, false, mergeStrategy); + const signal = buildSignalFromEvent(event, ruleSO, false, mergeStrategy, buildReasonMessage); signal.signal.rule.building_block_type = 'default'; return signal; }), @@ -94,7 +99,7 @@ export const buildSignalGroupFromSequence = ( // we can build the signal that links the building blocks together // and also insert the group id (which is also the "shell" signal _id) in each building block const sequenceSignal = wrapSignal( - buildSignalFromSequence(wrappedBuildingBlocks, ruleSO), + buildSignalFromSequence(wrappedBuildingBlocks, ruleSO, buildReasonMessage), outputIndex ); wrappedBuildingBlocks.forEach((block, idx) => { @@ -111,14 +116,18 @@ export const buildSignalGroupFromSequence = ( export const buildSignalFromSequence = ( events: WrappedSignalHit[], - ruleSO: SavedObject + ruleSO: SavedObject, + buildReasonMessage: BuildReasonMessage ): SignalHit => { const rule = buildRuleWithoutOverrides(ruleSO); - const signal: Signal = buildSignal(events, rule); + const timestamp = new Date().toISOString(); + + const reason = buildReasonMessage({ rule, timestamp }); + const signal: Signal = buildSignal(events, rule, reason); const mergedEvents = objectArrayIntersection(events.map((event) => event._source)); return { ...mergedEvents, - '@timestamp': new Date().toISOString(), + '@timestamp': timestamp, event: { kind: 'signal', }, @@ -137,14 +146,17 @@ export const buildSignalFromEvent = ( event: BaseSignalHit, ruleSO: SavedObject, applyOverrides: boolean, - mergeStrategy: ConfigType['alertMergeStrategy'] + mergeStrategy: ConfigType['alertMergeStrategy'], + buildReasonMessage: BuildReasonMessage ): SignalHit => { const mergedEvent = getMergeStrategy(mergeStrategy)({ doc: event }); const rule = applyOverrides ? buildRuleWithOverrides(ruleSO, mergedEvent._source ?? {}) : buildRuleWithoutOverrides(ruleSO); + const timestamp = new Date().toISOString(); + const reason = buildReasonMessage({ mergedDoc: mergedEvent, rule, timestamp }); const signal: Signal = { - ...buildSignal([mergedEvent], rule), + ...buildSignal([mergedEvent], rule, reason), ...additionalSignalFields(mergedEvent), }; const eventFields = buildEventTypeSignal(mergedEvent); @@ -155,7 +167,7 @@ export const buildSignalFromEvent = ( // TODO: better naming for SignalHit - it's really a new signal to be inserted const signalHit: SignalHit = { ...filteredSource, - '@timestamp': new Date().toISOString(), + '@timestamp': timestamp, event: eventFields, signal, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts index 8c0790761a5e0..90b9cce9e057d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts @@ -31,8 +31,10 @@ describe('buildSignal', () => { const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); delete doc._source.event; const rule = getRulesSchemaMock(); + const reason = 'signal reasonable reason'; + const signal = { - ...buildSignal([doc], rule), + ...buildSignal([doc], rule, reason), ...additionalSignalFields(doc), }; const expected: Signal = { @@ -62,6 +64,7 @@ describe('buildSignal', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + reason: 'signal reasonable reason', status: 'open', rule: { author: [], @@ -112,8 +115,9 @@ describe('buildSignal', () => { module: 'system', }; const rule = getRulesSchemaMock(); + const reason = 'signal reasonable reason'; const signal = { - ...buildSignal([doc], rule), + ...buildSignal([doc], rule, reason), ...additionalSignalFields(doc), }; const expected: Signal = { @@ -143,6 +147,7 @@ describe('buildSignal', () => { }, ], original_time: '2020-04-20T21:27:45.000Z', + reason: 'signal reasonable reason', original_event: { action: 'socket_opened', dataset: 'socket', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts index a415c83e857c2..962869cc4d61a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts @@ -77,7 +77,7 @@ export const removeClashes = (doc: BaseSignalHit): BaseSignalHit => { * @param docs The parent signals/events of the new signal to be built. * @param rule The rule that is generating the new signal. */ -export const buildSignal = (docs: BaseSignalHit[], rule: RulesSchema): Signal => { +export const buildSignal = (docs: BaseSignalHit[], rule: RulesSchema, reason: string): Signal => { const _meta = { version: SIGNALS_TEMPLATE_VERSION, }; @@ -94,6 +94,7 @@ export const buildSignal = (docs: BaseSignalHit[], rule: RulesSchema): Signal => ancestors, status: 'open', rule, + reason, depth, }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index ebb4462817eab..be6f4cb8feae5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -19,6 +19,7 @@ import { AnomalyResults, Anomaly } from '../../machine_learning'; import { BuildRuleMessage } from './rule_messages'; import { AlertAttributes, BulkCreate, WrapHits } from './types'; import { MachineLearningRuleParams } from '../schemas/rule_schemas'; +import { buildReasonMessageForMlAlert } from './reason_formatters'; interface BulkCreateMlSignalsParams { someResult: AnomalyResults; @@ -89,6 +90,6 @@ export const bulkCreateMlSignals = async ( const anomalyResults = params.someResult; const ecsResults = transformAnomalyResultsToEcs(anomalyResults); - const wrappedDocs = params.wrapHits(ecsResults.hits.hits); + const wrappedDocs = params.wrapHits(ecsResults.hits.hits, buildReasonMessageForMlAlert); return params.bulkCreate(wrappedDocs); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts index 8d19510c63477..9a2805610ca8b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts @@ -35,6 +35,7 @@ import { } from '../types'; import { createSearchAfterReturnType, makeFloatString } from '../utils'; import { ExperimentalFeatures } from '../../../../../common/experimental_features'; +import { buildReasonMessageForEqlAlert } from '../reason_formatters'; export const eqlExecutor = async ({ rule, @@ -119,9 +120,9 @@ export const eqlExecutor = async ({ result.searchAfterTimes = [eqlSearchDuration]; let newSignals: SimpleHit[] | undefined; if (response.hits.sequences !== undefined) { - newSignals = wrapSequences(response.hits.sequences); + newSignals = wrapSequences(response.hits.sequences, buildReasonMessageForEqlAlert); } else if (response.hits.events !== undefined) { - newSignals = wrapHits(response.hits.events); + newSignals = wrapHits(response.hits.events, buildReasonMessageForEqlAlert); } else { throw new Error( 'eql query response should have either `sequences` or `events` but had neither' diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts index f27680315d194..f281475fe59eb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts @@ -22,6 +22,7 @@ import { TelemetryEventsSender } from '../../../telemetry/sender'; import { BuildRuleMessage } from '../rule_messages'; import { QueryRuleParams, SavedQueryRuleParams } from '../../schemas/rule_schemas'; import { ExperimentalFeatures } from '../../../../../common/experimental_features'; +import { buildReasonMessageForQueryAlert } from '../reason_formatters'; export const queryExecutor = async ({ rule, @@ -84,6 +85,7 @@ export const queryExecutor = async ({ signalsIndex: ruleParams.outputIndex, filter: esFilter, pageSize: searchAfterSize, + buildReasonMessage: buildReasonMessageForQueryAlert, buildRuleMessage, bulkCreate, wrapHits, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatter.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatter.test.ts new file mode 100644 index 0000000000000..e7f4fb41c763b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatter.test.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { buildCommonReasonMessage } from './reason_formatters'; +import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; +import { SignalSourceHit } from './types'; + +describe('reason_formatter', () => { + let rule: RulesSchema; + let mergedDoc: SignalSourceHit; + let timestamp: string; + beforeAll(() => { + rule = { + name: 'What is in a name', + risk_score: 9000, + severity: 'medium', + } as RulesSchema; // Cast here as all fields aren't required + mergedDoc = { + _index: 'some-index', + _id: 'some-id', + fields: { + 'host.name': ['party host'], + 'user.name': ['ferris bueller'], + '@timestamp': '2021-08-11T02:28:59.101Z', + }, + }; + timestamp = '2021-08-11T02:28:59.401Z'; + }); + + describe('buildCommonReasonMessage', () => { + describe('when rule, mergedDoc, and timestamp are provided', () => { + it('should return the full reason message', () => { + expect(buildCommonReasonMessage({ rule, mergedDoc, timestamp })).toEqual( + 'Alert What is in a name created at 2021-08-11T02:28:59.401Z with a medium severity and risk score of 9000 by ferris bueller on party host.' + ); + }); + }); + describe('when rule, mergedDoc, and timestamp are provided and host.name is missing', () => { + it('should return the reason message without the host name', () => { + const updatedMergedDoc = { + ...mergedDoc, + fields: { + ...mergedDoc.fields, + 'host.name': ['-'], + }, + }; + expect(buildCommonReasonMessage({ rule, mergedDoc: updatedMergedDoc, timestamp })).toEqual( + 'Alert What is in a name created at 2021-08-11T02:28:59.401Z with a medium severity and risk score of 9000 by ferris bueller.' + ); + }); + }); + describe('when rule, mergedDoc, and timestamp are provided and user.name is missing', () => { + it('should return the reason message without the user name', () => { + const updatedMergedDoc = { + ...mergedDoc, + fields: { + ...mergedDoc.fields, + 'user.name': ['-'], + }, + }; + expect(buildCommonReasonMessage({ rule, mergedDoc: updatedMergedDoc, timestamp })).toEqual( + 'Alert What is in a name created at 2021-08-11T02:28:59.401Z with a medium severity and risk score of 9000 on party host.' + ); + }); + }); + describe('when only rule and timestamp are provided', () => { + it('should return the reason message without host name or user name', () => { + expect(buildCommonReasonMessage({ rule, timestamp })).toEqual( + 'Alert What is in a name created at 2021-08-11T02:28:59.401Z with a medium severity and risk score of 9000.' + ); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatters.ts new file mode 100644 index 0000000000000..0586462a2a581 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/reason_formatters.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; +import { SignalSourceHit } from './types'; + +export interface BuildReasonMessageArgs { + rule: RulesSchema; + mergedDoc?: SignalSourceHit; + timestamp: string; +} + +export type BuildReasonMessage = (args: BuildReasonMessageArgs) => string; + +/** + * Currently all security solution rule types share a common reason message string. This function composes that string + * In the future there may be different configurations based on the different rule types, so the plumbing has been put in place + * to more easily allow for this in the future. + * @export buildCommonReasonMessage - is only exported for testing purposes, and only used internally here. + */ +export const buildCommonReasonMessage = ({ + rule, + mergedDoc, + timestamp, +}: BuildReasonMessageArgs) => { + if (!rule) { + // This should never happen, but in case, better to not show a malformed string + return ''; + } + let hostName; + let userName; + if (mergedDoc?.fields) { + hostName = mergedDoc.fields['host.name'] != null ? mergedDoc.fields['host.name'] : hostName; + userName = mergedDoc.fields['user.name'] != null ? mergedDoc.fields['user.name'] : userName; + } + + const isFieldEmpty = (field: string | string[] | undefined | null) => + !field || !field.length || (field.length === 1 && field[0] === '-'); + + return i18n.translate('xpack.securitySolution.detectionEngine.signals.alertReasonDescription', { + defaultMessage: + 'Alert {alertName} created at {timestamp} with a {alertSeverity} severity and risk score of {alertRiskScore}{userName, select, null {} other {{whitespace}by {userName}} }{hostName, select, null {} other {{whitespace}on {hostName}} }.', + values: { + alertName: rule.name, + alertSeverity: rule.severity, + alertRiskScore: rule.risk_score, + hostName: isFieldEmpty(hostName) ? 'null' : hostName, + timestamp, + userName: isFieldEmpty(userName) ? 'null' : userName, + whitespace: ' ', // there isn't support for the unicode /u0020 for whitespace, and leading spaces are deleted, so to prevent double-whitespace explicitly passing the space in. + }, + }); +}; + +export const buildReasonMessageForEqlAlert = (args: BuildReasonMessageArgs) => + buildCommonReasonMessage({ ...args }); + +export const buildReasonMessageForMlAlert = (args: BuildReasonMessageArgs) => + buildCommonReasonMessage({ ...args }); + +export const buildReasonMessageForQueryAlert = (args: BuildReasonMessageArgs) => + buildCommonReasonMessage({ ...args }); + +export const buildReasonMessageForThreatMatchAlert = (args: BuildReasonMessageArgs) => + buildCommonReasonMessage({ ...args }); + +export const buildReasonMessageForThresholdAlert = (args: BuildReasonMessageArgs) => + buildCommonReasonMessage({ ...args }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 711db931e9072..8bf0c986b9c25 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -32,11 +32,13 @@ import { bulkCreateFactory } from './bulk_create_factory'; import { wrapHitsFactory } from './wrap_hits_factory'; import { mockBuildRuleMessage } from './__mocks__/build_rule_message.mock'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; +import { BuildReasonMessage } from './reason_formatters'; const buildRuleMessage = mockBuildRuleMessage; describe('searchAfterAndBulkCreate', () => { let mockService: AlertServicesMock; + let buildReasonMessage: BuildReasonMessage; let bulkCreate: BulkCreate; let wrapHits: WrapHits; let inputIndexPattern: string[] = []; @@ -48,6 +50,7 @@ describe('searchAfterAndBulkCreate', () => { let tuple: RuleRangeTuple; beforeEach(() => { jest.clearAllMocks(); + buildReasonMessage = jest.fn().mockResolvedValue('some alert reason message'); listClient = listMock.getListClient(); listClient.searchListItemByValues = jest.fn().mockResolvedValue([]); inputIndexPattern = ['auditbeat-*']; @@ -191,6 +194,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -295,6 +299,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -373,6 +378,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -432,6 +438,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -511,6 +518,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -566,6 +574,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -638,6 +647,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -712,6 +722,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -763,6 +774,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -810,6 +822,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -871,6 +884,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -997,6 +1011,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, @@ -1093,6 +1108,7 @@ describe('searchAfterAndBulkCreate', () => { signalsIndex: DEFAULT_SIGNALS_INDEX, pageSize: 1, filter: undefined, + buildReasonMessage, buildRuleMessage, bulkCreate, wrapHits, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index 7b5b61577cf32..8037a9a201510 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -34,6 +34,7 @@ export const searchAfterAndBulkCreate = async ({ filter, pageSize, buildRuleMessage, + buildReasonMessage, enrichment = identity, bulkCreate, wrapHits, @@ -146,7 +147,7 @@ export const searchAfterAndBulkCreate = async ({ ); } const enrichedEvents = await enrichment(filteredEvents); - const wrappedDocs = wrapHits(enrichedEvents.hits.hits); + const wrappedDocs = wrapHits(enrichedEvents.hits.hits, buildReasonMessage); const { bulkCreateDuration: bulkDuration, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts index fb9881b519a16..312d75f7a10cc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts @@ -9,6 +9,7 @@ import { buildThreatMappingFilter } from './build_threat_mapping_filter'; import { getFilter } from '../get_filter'; import { searchAfterAndBulkCreate } from '../search_after_bulk_create'; +import { buildReasonMessageForThreatMatchAlert } from '../reason_formatters'; import { CreateThreatSignalOptions } from './types'; import { SearchAfterAndBulkCreateReturnType } from '../types'; @@ -83,6 +84,7 @@ export const createThreatSignal = async ({ filter: esFilter, pageSize: searchAfterSize, buildRuleMessage, + buildReasonMessage: buildReasonMessageForThreatMatchAlert, enrichment: threatEnrichment, bulkCreate, wrapHits, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts index f56ed3a5e9eb4..afb0353c4ba03 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts @@ -24,6 +24,7 @@ import { getThresholdAggregationParts, getThresholdTermsHash, } from '../utils'; +import { buildReasonMessageForThresholdAlert } from '../reason_formatters'; import type { MultiAggBucket, SignalSource, @@ -248,5 +249,7 @@ export const bulkCreateThresholdSignals = async ( params.thresholdSignalHistory ); - return params.bulkCreate(params.wrapHits(ecsResults.hits.hits)); + return params.bulkCreate( + params.wrapHits(ecsResults.hits.hits, buildReasonMessageForThresholdAlert) + ); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 4da411d0c70a1..89233cf2c8242 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -35,6 +35,7 @@ import { RuleParams } from '../schemas/rule_schemas'; import { GenericBulkCreateResponse } from './bulk_create_factory'; import { EcsFieldMap } from '../../../../../rule_registry/common/assets/field_maps/ecs_field_map'; import { TypeOfFieldMap } from '../../../../../rule_registry/common/field_map'; +import { BuildReasonMessage } from './reason_formatters'; // used for gap detection code // eslint-disable-next-line @typescript-eslint/naming-convention @@ -238,6 +239,7 @@ export interface Signal { }; original_time?: string; original_event?: SearchTypes; + reason?: string; status: Status; threshold_result?: ThresholdResult; original_signal?: SearchTypes; @@ -286,9 +288,15 @@ export type BulkCreate = (docs: Array>) => Promise; -export type WrapHits = (hits: estypes.SearchHit[]) => SimpleHit[]; +export type WrapHits = ( + hits: Array>, + buildReasonMessage: BuildReasonMessage +) => SimpleHit[]; -export type WrapSequences = (sequences: Array>) => SimpleHit[]; +export type WrapSequences = ( + sequences: Array>, + buildReasonMessage: BuildReasonMessage +) => SimpleHit[]; export interface SearchAfterAndBulkCreateParams { tuple: { @@ -308,6 +316,7 @@ export interface SearchAfterAndBulkCreateParams { pageSize: number; filter: unknown; buildRuleMessage: BuildRuleMessage; + buildReasonMessage: BuildReasonMessage; enrichment?: SignalsEnrichment; bulkCreate: BulkCreate; wrapHits: WrapHits; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts index 5cef740e17895..19bdd58140a33 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_hits_factory.ts @@ -24,7 +24,7 @@ export const wrapHitsFactory = ({ ruleSO: SearchAfterAndBulkCreateParams['ruleSO']; signalsIndex: string; mergeStrategy: ConfigType['alertMergeStrategy']; -}): WrapHits => (events) => { +}): WrapHits => (events, buildReasonMessage) => { const wrappedDocs: WrappedSignalHit[] = events.flatMap((doc) => [ { _index: signalsIndex, @@ -34,7 +34,7 @@ export const wrapHitsFactory = ({ String(doc._version), ruleSO.attributes.params.ruleId ?? '' ), - _source: buildBulkBody(ruleSO, doc as SignalSourceHit, mergeStrategy), + _source: buildBulkBody(ruleSO, doc as SignalSourceHit, mergeStrategy, buildReasonMessage), }, ]); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts index f0b9e64047692..0ca4b9688f971 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/wrap_sequences_factory.ts @@ -17,11 +17,17 @@ export const wrapSequencesFactory = ({ ruleSO: SearchAfterAndBulkCreateParams['ruleSO']; signalsIndex: string; mergeStrategy: ConfigType['alertMergeStrategy']; -}): WrapSequences => (sequences) => +}): WrapSequences => (sequences, buildReasonMessage) => sequences.reduce( (acc: WrappedSignalHit[], sequence) => [ ...acc, - ...buildSignalGroupFromSequence(sequence, ruleSO, signalsIndex, mergeStrategy), + ...buildSignalGroupFromSequence( + sequence, + ruleSO, + signalsIndex, + mergeStrategy, + buildReasonMessage + ), ], [] ); diff --git a/x-pack/plugins/timelines/common/ecs/ecs_fields/index.ts b/x-pack/plugins/timelines/common/ecs/ecs_fields/index.ts index 292822019fc9c..239e295a1f8b1 100644 --- a/x-pack/plugins/timelines/common/ecs/ecs_fields/index.ts +++ b/x-pack/plugins/timelines/common/ecs/ecs_fields/index.ts @@ -292,6 +292,7 @@ export const systemFieldsMap: Readonly> = { export const signalFieldsMap: Readonly> = { 'signal.original_time': 'signal.original_time', + 'signal.reason': 'signal.reason', 'signal.rule.id': 'signal.rule.id', 'signal.rule.saved_id': 'signal.rule.saved_id', 'signal.rule.timeline_id': 'signal.rule.timeline_id', diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx index 790414314ecdd..3dea3e71445a1 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx @@ -147,6 +147,7 @@ export const allowSorting = ({ 'signal.parent.index', 'signal.parent.rule', 'signal.parent.type', + 'signal.reason', 'signal.rule.created_by', 'signal.rule.description', 'signal.rule.enabled', diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/constants.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/constants.ts index aae68dbcf86d1..9b45a5bebfc21 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/constants.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/constants.ts @@ -45,6 +45,7 @@ export const TIMELINE_EVENTS_FIELDS = [ 'signal.status', 'signal.group.id', 'signal.original_time', + 'signal.reason', 'signal.rule.filters', 'signal.rule.from', 'signal.rule.language', diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts index a03bd07c86020..cd209da25e883 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts @@ -193,6 +193,7 @@ export default ({ getService }: FtrProviderContext) => { index: '.ml-anomalies-custom-linux_anomalous_network_activity_ecs', depth: 0, }, + reason: `Alert Test ML rule created at ${signal._source['@timestamp']} with a critical severity and risk score of 50 by root on mothra.`, original_time: '2020-11-16T22:58:08.000Z', }, all_field_values: [ diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts index c341761160633..399eafc475a89 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -275,6 +275,7 @@ export default ({ getService }: FtrProviderContext) => { depth: 0, }, ], + reason: `Alert Query with a rule id created at ${fullSignal['@timestamp']} with a high severity and risk score of 55 by root on zeek-sensor-amsterdam.`, rule: fullSignal.signal.rule, status: 'open', }, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts index 66c94a7317b72..1c1e2b9966b7f 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts @@ -112,7 +112,8 @@ export default ({ getService }: FtrProviderContext) => { const signalsOpen = await getSignalsByIds(supertest, [id]); const signal = signalsOpen.hits.hits[0]._source?.signal; // remove rule to cut down on touch points for test changes when the rule format changes - const signalNoRule = omit(signal, 'rule'); + // remove reason to avoid failures due to @timestamp mismatches in the reason string + const signalNoRule = omit(signal, ['rule', 'reason']); expect(signalNoRule).eql({ parents: [ @@ -165,7 +166,8 @@ export default ({ getService }: FtrProviderContext) => { const signalsOpen = await getSignalsByIds(supertest, [id]); const signal = signalsOpen.hits.hits[0]._source?.signal; // remove rule to cut down on touch points for test changes when the rule format changes - const signalNoRule = omit(signal, 'rule'); + // remove reason to avoid failures due to @timestamp mismatches in the reason string + const signalNoRule = omit(signal, ['rule', 'reason']); expect(signalNoRule).eql({ parents: [ { @@ -228,7 +230,7 @@ export default ({ getService }: FtrProviderContext) => { const signal = signalsOpen.hits.hits[0]._source?.signal; // remove rule to cut down on touch points for test changes when the rule format changes - const signalNoRule = omit(signal, 'rule'); + const signalNoRule = omit(signal, ['rule', 'reason']); expect(signalNoRule).eql({ parents: [ { @@ -360,6 +362,7 @@ export default ({ getService }: FtrProviderContext) => { }, }, signal: { + reason: `Alert Signal Testing Query created at ${fullSignal['@timestamp']} with a high severity and risk score of 1 on suricata-zeek-sensor-toronto.`, rule: fullSignal.signal.rule, original_time: fullSignal.signal.original_time, status: 'open', @@ -494,6 +497,7 @@ export default ({ getService }: FtrProviderContext) => { }, }, signal: { + reason: `Alert Signal Testing Query created at ${fullSignal['@timestamp']} with a high severity and risk score of 1 on suricata-zeek-sensor-toronto.`, rule: fullSignal.signal.rule, original_time: fullSignal.signal.original_time, status: 'open', @@ -658,6 +662,7 @@ export default ({ getService }: FtrProviderContext) => { }, }, signal: { + reason: `Alert Signal Testing Query created at ${fullSignal['@timestamp']} with a high severity and risk score of 1 by root on zeek-sensor-amsterdam.`, rule: fullSignal.signal.rule, group: fullSignal.signal.group, original_time: fullSignal.signal.original_time, @@ -748,6 +753,7 @@ export default ({ getService }: FtrProviderContext) => { status: 'open', depth: 2, group: source.signal.group, + reason: `Alert Signal Testing Query created at ${source['@timestamp']} with a high severity and risk score of 1.`, rule: source.signal.rule, ancestors: [ { @@ -866,6 +872,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], status: 'open', + reason: `Alert Signal Testing Query created at ${fullSignal['@timestamp']} with a high severity and risk score of 1.`, rule: fullSignal.signal.rule, original_time: fullSignal.signal.original_time, depth: 1, @@ -1003,6 +1010,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], status: 'open', + reason: `Alert Signal Testing Query created at ${fullSignal['@timestamp']} with a high severity and risk score of 1.`, rule: fullSignal.signal.rule, original_time: fullSignal.signal.original_time, depth: 1, @@ -1086,6 +1094,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], status: 'open', + reason: `Alert Signal Testing Query created at ${fullSignal['@timestamp']} with a high severity and risk score of 1.`, rule: fullSignal.signal.rule, original_time: fullSignal.signal.original_time, depth: 1, @@ -1171,7 +1180,8 @@ export default ({ getService }: FtrProviderContext) => { const signalsOpen = await getSignalsByIds(supertest, [id]); const signal = signalsOpen.hits.hits[0]._source?.signal; // remove rule to cut down on touch points for test changes when the rule format changes - const signalNoRule = omit(signal, 'rule'); + // remove reason to avoid failures due to @timestamp mismatches in the reason string + const signalNoRule = omit(signal, ['rule', 'reason']); expect(signalNoRule).eql({ parents: [ { @@ -1228,7 +1238,7 @@ export default ({ getService }: FtrProviderContext) => { const signal = signalsOpen.hits.hits[0]._source?.signal; // remove rule to cut down on touch points for test changes when the rule format changes - const signalNoRule = omit(signal, 'rule'); + const signalNoRule = omit(signal, ['rule', 'reason']); expect(signalNoRule).eql({ parents: [ @@ -1325,7 +1335,8 @@ export default ({ getService }: FtrProviderContext) => { const signalsOpen = await getSignalsByIds(supertest, [id]); const signal = signalsOpen.hits.hits[0]._source?.signal; // remove rule to cut down on touch points for test changes when the rule format changes - const signalNoRule = omit(signal, 'rule'); + // remove reason to avoid failures due to @timestamp mismatches in the reason string + const signalNoRule = omit(signal, ['rule', 'reason']); expect(signalNoRule).eql({ parents: [ { @@ -1387,7 +1398,7 @@ export default ({ getService }: FtrProviderContext) => { const signalsOpen = await getSignalsByRuleIds(supertest, ['signal-on-signal']); const signal = signalsOpen.hits.hits[0]._source?.signal; // remove rule to cut down on touch points for test changes when the rule format changes - const signalNoRule = omit(signal, 'rule'); + const signalNoRule = omit(signal, ['rule', 'reason']); expect(signalNoRule).eql({ parents: [ @@ -1675,6 +1686,7 @@ export default ({ getService }: FtrProviderContext) => { }, ], status: 'open', + reason: `Alert boot created at ${fullSignal['@timestamp']} with a high severity and risk score of 1 on zeek-sensor-amsterdam.`, rule: { ...fullSignal.signal.rule, name: 'boot', From 73e7db5ead8b1aed47de005f579cecaa4c360c0f Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 11 Aug 2021 15:32:28 -0700 Subject: [PATCH 101/104] [scripts/type_check] don't fail if --project is a composite project (#108249) Co-authored-by: spalger --- src/dev/typescript/run_type_check_cli.ts | 31 ++++++++++++++++++------ 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/dev/typescript/run_type_check_cli.ts b/src/dev/typescript/run_type_check_cli.ts index 1bf31a6c5bac0..6a28631322857 100644 --- a/src/dev/typescript/run_type_check_cli.ts +++ b/src/dev/typescript/run_type_check_cli.ts @@ -37,19 +37,34 @@ export async function runTypeCheckCli() { : undefined; const projects = PROJECTS.filter((p) => { - return ( - !p.disableTypeCheck && - (!projectFilter || p.tsConfigPath === projectFilter) && - !p.isCompositeProject() - ); + return !p.disableTypeCheck && (!projectFilter || p.tsConfigPath === projectFilter); }); if (!projects.length) { - throw createFailError(`Unable to find project at ${flags.project}`); + if (projectFilter) { + throw createFailError(`Unable to find project at ${flags.project}`); + } else { + throw createFailError(`Unable to find projects to type-check`); + } + } + + const nonCompositeProjects = projects.filter((p) => !p.isCompositeProject()); + if (!nonCompositeProjects.length) { + if (projectFilter) { + log.success( + `${flags.project} is a composite project so its types are validated by scripts/build_ts_refs` + ); + } else { + log.success( + `All projects are composite so their types are validated by scripts/build_ts_refs` + ); + } + + return; } const concurrency = Math.min(4, Math.round((Os.cpus() || []).length / 2) || 1) || 1; - log.info('running type check in', projects.length, 'non-composite projects'); + log.info('running type check in', nonCompositeProjects.length, 'non-composite projects'); const tscArgs = [ ...['--emitDeclarationOnly', 'false'], @@ -61,7 +76,7 @@ export async function runTypeCheckCli() { ]; const failureCount = await lastValueFrom( - Rx.from(projects).pipe( + Rx.from(nonCompositeProjects).pipe( mergeMap(async (p) => { const relativePath = Path.relative(process.cwd(), p.tsConfigPath); From 33cd67a4b0566bd5838b158f548ffd246c13a5b2 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Wed, 11 Aug 2021 18:07:38 -0500 Subject: [PATCH 102/104] [Workplace Search] Add custom branding controls to Settings (#108235) * Add image upload route * Add base64 converter Loosely based on similar App Search util https://github.com/elastic/kibana/blob/master/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/utils.ts I opted to use this built-in class and strip out what the server does not need. Will have to manually add the prefix back in the template the way eweb does it: https://github.com/elastic/ent-search/blob/master/app/javascript/eweb/components/shared/search_results/ServiceTypeResultIcon.jsx#L7 * Swap out flash messages for success toasts Will be doing this app-wide in a future PR to match App Search * Make propperties optional After the redesign, it was decided that both icons could be uploaded individually. * Add constants * Add BrandingSection component * Add logic for image upload and display * Add BrandingSection to settings view * Fix failing test * Fix some typos in tests --- .../applications/shared/constants/actions.ts | 5 + .../workplace_search/utils/index.ts | 1 + .../read_uploaded_file_as_base64.test.ts | 21 +++ .../utils/read_uploaded_file_as_base64.ts | 26 +++ .../components/branding_section.test.tsx | 86 ++++++++++ .../settings/components/branding_section.tsx | 152 ++++++++++++++++++ .../settings/components/customize.test.tsx | 2 + .../views/settings/components/customize.tsx | 50 +++++- .../views/settings/constants.ts | 93 +++++++++++ .../views/settings/settings_logic.test.ts | 114 ++++++++++++- .../views/settings/settings_logic.ts | 95 ++++++++++- .../routes/workplace_search/settings.test.ts | 31 ++++ .../routes/workplace_search/settings.ts | 21 +++ 13 files changed, 687 insertions(+), 10 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/read_uploaded_file_as_base64.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/read_uploaded_file_as_base64.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/branding_section.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/branding_section.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/constants.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/constants/actions.ts b/x-pack/plugins/enterprise_search/public/applications/shared/constants/actions.ts index e6511947d2506..6579e911cc19b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/constants/actions.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/constants/actions.ts @@ -44,3 +44,8 @@ export const CLOSE_BUTTON_LABEL = i18n.translate( 'xpack.enterpriseSearch.actions.closeButtonLabel', { defaultMessage: 'Close' } ); + +export const RESET_DEFAULT_BUTTON_LABEL = i18n.translate( + 'xpack.enterpriseSearch.actions.resetDefaultButtonLabel', + { defaultMessage: 'Reset to default' } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts index e9ebc791622d9..fb9846dbccde8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts @@ -8,3 +8,4 @@ export { toSentenceSerial } from './to_sentence_serial'; export { getAsLocalDateTimeString } from './get_as_local_datetime_string'; export { mimeType } from './mime_types'; +export { readUploadedFileAsBase64 } from './read_uploaded_file_as_base64'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/read_uploaded_file_as_base64.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/read_uploaded_file_as_base64.test.ts new file mode 100644 index 0000000000000..9f612a7432ec5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/read_uploaded_file_as_base64.test.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { readUploadedFileAsBase64 } from './'; + +describe('readUploadedFileAsBase64', () => { + it('reads a file and returns base64 string', async () => { + const file = new File(['a mock file'], 'mockFile.png', { type: 'img/png' }); + const text = await readUploadedFileAsBase64(file); + expect(text).toEqual('YSBtb2NrIGZpbGU='); + }); + + it('throws an error if the file cannot be read', async () => { + const badFile = ('causes an error' as unknown) as File; + await expect(readUploadedFileAsBase64(badFile)).rejects.toThrow(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/read_uploaded_file_as_base64.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/read_uploaded_file_as_base64.ts new file mode 100644 index 0000000000000..d9f6d177cf9cd --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/read_uploaded_file_as_base64.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const readUploadedFileAsBase64 = (fileInput: File): Promise => { + const reader = new FileReader(); + + return new Promise((resolve, reject) => { + reader.onload = () => { + // We need to split off the prefix from the DataUrl and only pass the base64 string + // before: 'data:image/png;base64,encodedData==' + // after: 'encodedData==' + const base64 = (reader.result as string).split(',')[1]; + resolve(base64); + }; + try { + reader.readAsDataURL(fileInput); + } catch { + reader.abort(); + reject(new Error()); + } + }); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/branding_section.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/branding_section.test.tsx new file mode 100644 index 0000000000000..0f96b76130b4f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/branding_section.test.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow, mount } from 'enzyme'; + +import { EuiFilePicker, EuiConfirmModal } from '@elastic/eui'; +import { nextTick } from '@kbn/test/jest'; + +jest.mock('../../../utils', () => ({ + readUploadedFileAsBase64: jest.fn(({ img }) => img), +})); +import { readUploadedFileAsBase64 } from '../../../utils'; + +import { RESET_IMAGE_TITLE } from '../constants'; + +import { BrandingSection, defaultLogo } from './branding_section'; + +describe('BrandingSection', () => { + const stageImage = jest.fn(); + const saveImage = jest.fn(); + const resetImage = jest.fn(); + + const props = { + image: 'foo', + imageType: 'logo' as 'logo', + description: 'logo test', + helpText: 'this is a logo', + stageImage, + saveImage, + resetImage, + }; + + it('renders logo', () => { + const wrapper = mount(); + + expect(wrapper.find(EuiFilePicker)).toHaveLength(1); + }); + + it('renders icon copy', () => { + const wrapper = shallow(); + wrapper.find('[data-test-subj="ResetImageButton"]').simulate('click'); + + expect(wrapper.find(EuiConfirmModal).prop('title')).toEqual(RESET_IMAGE_TITLE); + }); + + it('renders default Workplace Search logo', () => { + const wrapper = shallow(); + + expect(wrapper.find('img').prop('src')).toContain(defaultLogo); + }); + + describe('resetConfirmModal', () => { + it('calls method and hides modal when modal confirmed', () => { + const wrapper = shallow(); + wrapper.find('[data-test-subj="ResetImageButton"]').simulate('click'); + wrapper.find(EuiConfirmModal).prop('onConfirm')!({} as any); + + expect(wrapper.find(EuiConfirmModal)).toHaveLength(0); + expect(resetImage).toHaveBeenCalled(); + }); + }); + + describe('handleUpload', () => { + it('handles empty files', () => { + const wrapper = shallow(); + wrapper.find(EuiFilePicker).prop('onChange')!([] as any); + + expect(stageImage).toHaveBeenCalledWith(null); + }); + + it('handles image', async () => { + const wrapper = shallow(); + wrapper.find(EuiFilePicker).prop('onChange')!(['foo'] as any); + + expect(readUploadedFileAsBase64).toHaveBeenCalledWith('foo'); + await nextTick(); + expect(stageImage).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/branding_section.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/branding_section.tsx new file mode 100644 index 0000000000000..776e72c4026cf --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/branding_section.tsx @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useEffect } from 'react'; + +import { + EuiButton, + EuiConfirmModal, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiFilePicker, + EuiText, + EuiSpacer, +} from '@elastic/eui'; + +import { + SAVE_BUTTON_LABEL, + CANCEL_BUTTON_LABEL, + RESET_DEFAULT_BUTTON_LABEL, +} from '../../../../shared/constants'; +import { readUploadedFileAsBase64 } from '../../../utils'; + +import { + LOGO_TEXT, + ICON_TEXT, + RESET_IMAGE_TITLE, + RESET_LOGO_DESCRIPTION, + RESET_ICON_DESCRIPTION, + RESET_IMAGE_CONFIRMATION_TEXT, + ORGANIZATION_LABEL, + BRAND_TEXT, +} from '../constants'; + +export const defaultLogo = + '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'; +const defaultIcon = + 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAA/1BMVEUAAADwTpj+xRT+xRTwTpjwTpj+xRTwTpj+wxT+xRT/yBMSHkn8yxL+wxX+xRT+xRT/1QwzN0E6OkH/xxPwTpjwTpjwTpj/xBQsMUPwTpj/UK3/yRMWHkvwTpj/zg7wTpj/0A3wTpjwTpgRIEf/0Qx/P2P/yBMuM0I1OEH+xRQuM0L+xRQuM0LntRr+xRT+xRT+xBQ1JlZjPVdaUDwtMEUbJkYbJEj+xRTwTpg0N0E2N0LuTZX/U6z/Uqf9UaFkPVYRMjD/UqnzTpgKMS0BMCn/UaL3T53gTJGwRn2jRHRdPFUtNj4qNjwmNToALyfKSojISoeJQWhtPlsFKTP/yxKq4k7GAAAAN3RSTlMA29vt7fPy6uPQdjYd/aSVBfHs49nPwq+nlIuEU084MichEAoK/vPXz6iempOSjn9kY1w0LBcVaxnnyQAAASFJREFUOMuVk3lbgkAQh6cIxQq0u6zM7vs+cHchRbE7O7//Z+nng60PDuDj+9/MvMCyM0O0YE4Ac35lkzTTp3M5A+QKCPK1HuY69bjY+3UjDERjNc1GVD9zNeNxIb+FeOfYZYJmEXHFzhBUGYnVdEHde1fILHFB1+uNG5zCYoKuh2L2jqhqJwnqwfsOpRQHyE0mCU3vqyOkEOIESYsLyv9svUoB5BRewYVm8NJCvcsymsGF9uP7m4iY2SYqMMF/aoh/8I1DLjz3hTWi4ogC/4Qz9JCj/6byP7IvCle925Fd4yj5qtGsoB7C2I83i7f7Fiew0wfm55qoZKWOXDu4zBo5UMbz50PGvop85uKUigMCXz0nJrDlja2OQcnrX3H0+v8BzVCfXpvPH1sAAAAASUVORK5CYII='; + +interface Props { + imageType: 'logo' | 'icon'; + description: string; + helpText: string; + image?: string | null; + stagedImage?: string | null; + stageImage(image: string | null): void; + saveImage(): void; + resetImage(): void; +} + +export const BrandingSection: React.FC = ({ + imageType, + description, + helpText, + image, + stagedImage, + stageImage, + saveImage, + resetImage, +}) => { + const [resetConfirmModalVisible, setVisible] = useState(false); + const [imageUploadKey, setKey] = useState(1); + const showDeleteModal = () => setVisible(true); + const closeDeleteModal = () => setVisible(false); + const isLogo = imageType === 'logo'; + const imageText = isLogo ? LOGO_TEXT : ICON_TEXT; + const defaultImage = isLogo ? defaultLogo : defaultIcon; + + const handleUpload = async (files: FileList | null) => { + if (!files || files.length < 1) { + return stageImage(null); + } + const file = files[0]; + const img = await readUploadedFileAsBase64(file); + stageImage(img); + }; + + const resetConfirmModal = ( + { + resetImage(); + closeDeleteModal(); + }} + cancelButtonText={CANCEL_BUTTON_LABEL} + confirmButtonText={RESET_DEFAULT_BUTTON_LABEL} + buttonColor="danger" + defaultFocusedButton="confirm" + > + <> +

    {isLogo ? RESET_LOGO_DESCRIPTION : RESET_ICON_DESCRIPTION}

    +

    {RESET_IMAGE_CONFIRMATION_TEXT}

    + +
    + ); + + // EUI currently does not support clearing an upload input programatically, so we can render a new + // one each time the image is changed. + useEffect(() => { + setKey(imageUploadKey + 1); + }, [image]); + + return ( + <> + + {description} +
    + } + > + <> + + {`${BRAND_TEXT} + + + + + + + + + {SAVE_BUTTON_LABEL} + + + + {image && ( + + {RESET_DEFAULT_BUTTON_LABEL} + + )} + + + + {resetConfirmModalVisible && resetConfirmModal} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.test.tsx index 15d0db4c415d0..9b17ec560ba51 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.test.tsx @@ -17,6 +17,7 @@ import { EuiFieldText } from '@elastic/eui'; import { ContentSection } from '../../../components/shared/content_section'; +import { BrandingSection } from './branding_section'; import { Customize } from './customize'; describe('Customize', () => { @@ -32,6 +33,7 @@ describe('Customize', () => { const wrapper = shallow(); expect(wrapper.find(ContentSection)).toHaveLength(1); + expect(wrapper.find(BrandingSection)).toHaveLength(2); }); it('handles input change', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.tsx index 98662585ce330..be4be08f54ebd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.tsx @@ -9,7 +9,14 @@ import React, { FormEvent } from 'react'; import { useActions, useValues } from 'kea'; -import { EuiButton, EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; +import { + EuiButton, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSpacer, +} from '@elastic/eui'; import { WorkplaceSearchPageTemplate } from '../../../components/layout'; import { ContentSection } from '../../../components/shared/content_section'; @@ -20,11 +27,25 @@ import { CUSTOMIZE_NAME_LABEL, CUSTOMIZE_NAME_BUTTON, } from '../../../constants'; +import { LOGO_DESCRIPTION, LOGO_HELP_TEXT, ICON_DESCRIPTION, ICON_HELP_TEXT } from '../constants'; import { SettingsLogic } from '../settings_logic'; +import { BrandingSection } from './branding_section'; + export const Customize: React.FC = () => { - const { onOrgNameInputChange, updateOrgName } = useActions(SettingsLogic); - const { orgNameInputValue } = useValues(SettingsLogic); + const { + onOrgNameInputChange, + updateOrgName, + setStagedIcon, + setStagedLogo, + updateOrgLogo, + updateOrgIcon, + resetOrgLogo, + resetOrgIcon, + } = useActions(SettingsLogic); + const { dataLoading, orgNameInputValue, icon, stagedIcon, logo, stagedLogo } = useValues( + SettingsLogic + ); const handleSubmit = (e: FormEvent) => { e.preventDefault(); @@ -38,6 +59,7 @@ export const Customize: React.FC = () => { pageTitle: CUSTOMIZE_HEADER_TITLE, description: CUSTOMIZE_HEADER_DESCRIPTION, }} + isLoading={dataLoading} >
    @@ -63,6 +85,28 @@ export const Customize: React.FC = () => {
    + + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/constants.ts new file mode 100644 index 0000000000000..1bcd038947117 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/constants.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const LOGO_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.logoText', + { + defaultMessage: 'logo', + } +); + +export const ICON_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.iconText', + { + defaultMessage: 'icon', + } +); + +export const RESET_IMAGE_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.resetImageTitle', + { + defaultMessage: 'Reset to default branding', + } +); + +export const RESET_LOGO_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.resetLogoDescription', + { + defaultMessage: "You're about to reset the logo to the default Workplace Search branding.", + } +); + +export const RESET_ICON_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.resetIconDescription', + { + defaultMessage: "You're about to reset the icon to the default Workplace Search branding.", + } +); + +export const RESET_IMAGE_CONFIRMATION_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.resetImageConfirmationText', + { + defaultMessage: 'Are you sure you want to do this?', + } +); + +export const ORGANIZATION_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.organizationLabel', + { + defaultMessage: 'Organization', + } +); + +export const BRAND_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.brandText', + { + defaultMessage: 'Brand', + } +); + +export const LOGO_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.logoDescription', + { + defaultMessage: 'Used as the main visual branding element across prebuilt search applications', + } +); + +export const LOGO_HELP_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.logoHelpText', + { + defaultMessage: 'Maximum file size is 2MB. Only PNG files are supported.', + } +); + +export const ICON_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.iconDescription', + { + defaultMessage: 'Used as the branding element for smaller screen sizes and browser icons', + } +); + +export const ICON_HELP_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.iconHelpText', + { + defaultMessage: + 'Maximum file size is 2MB and recommended aspect ratio is 1:1. Only PNG files are supported.', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts index 0aef84ccf20e2..005f2f016d561 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts @@ -25,7 +25,7 @@ describe('SettingsLogic', () => { const { clearFlashMessages, flashAPIErrors, - setSuccessMessage, + flashSuccessToast, setQueuedSuccessMessage, } = mockFlashMessageHelpers; const { mount } = new LogicMounter(SettingsLogic); @@ -35,8 +35,12 @@ describe('SettingsLogic', () => { connectors: [], orgNameInputValue: '', oauthApplication: null, + icon: null, + stagedIcon: null, + logo: null, + stagedLogo: null, }; - const serverProps = { organizationName: ORG_NAME, oauthApplication }; + const serverProps = { organizationName: ORG_NAME, oauthApplication, logo: null, icon: null }; beforeEach(() => { jest.clearAllMocks(); @@ -79,6 +83,34 @@ describe('SettingsLogic', () => { expect(SettingsLogic.values.oauthApplication).toEqual(oauthApplication); }); + it('setIcon', () => { + SettingsLogic.actions.setStagedIcon('stagedIcon'); + SettingsLogic.actions.setIcon('icon'); + + expect(SettingsLogic.values.icon).toEqual('icon'); + expect(SettingsLogic.values.stagedIcon).toEqual(null); + }); + + it('setStagedIcon', () => { + SettingsLogic.actions.setStagedIcon('stagedIcon'); + + expect(SettingsLogic.values.stagedIcon).toEqual('stagedIcon'); + }); + + it('setLogo', () => { + SettingsLogic.actions.setStagedLogo('stagedLogo'); + SettingsLogic.actions.setLogo('logo'); + + expect(SettingsLogic.values.logo).toEqual('logo'); + expect(SettingsLogic.values.stagedLogo).toEqual(null); + }); + + it('setStagedLogo', () => { + SettingsLogic.actions.setStagedLogo('stagedLogo'); + + expect(SettingsLogic.values.stagedLogo).toEqual('stagedLogo'); + }); + it('setUpdatedOauthApplication', () => { SettingsLogic.actions.setUpdatedOauthApplication({ oauthApplication }); @@ -143,7 +175,7 @@ describe('SettingsLogic', () => { body: JSON.stringify({ name: NAME }), }); await nextTick(); - expect(setSuccessMessage).toHaveBeenCalledWith(ORG_UPDATED_MESSAGE); + expect(flashSuccessToast).toHaveBeenCalledWith(ORG_UPDATED_MESSAGE); expect(setUpdatedNameSpy).toHaveBeenCalledWith({ organizationName: NAME }); }); @@ -156,6 +188,80 @@ describe('SettingsLogic', () => { }); }); + describe('updateOrgIcon', () => { + it('calls API and sets values', async () => { + const ICON = 'icon'; + SettingsLogic.actions.setStagedIcon(ICON); + const setIconSpy = jest.spyOn(SettingsLogic.actions, 'setIcon'); + http.put.mockReturnValue(Promise.resolve({ icon: ICON })); + + SettingsLogic.actions.updateOrgIcon(); + + expect(http.put).toHaveBeenCalledWith('/api/workplace_search/org/settings/upload_images', { + body: JSON.stringify({ icon: ICON }), + }); + await nextTick(); + expect(flashSuccessToast).toHaveBeenCalledWith(ORG_UPDATED_MESSAGE); + expect(setIconSpy).toHaveBeenCalledWith(ICON); + }); + + it('handles error', async () => { + http.put.mockReturnValue(Promise.reject('this is an error')); + SettingsLogic.actions.updateOrgIcon(); + + await nextTick(); + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + }); + + describe('updateOrgLogo', () => { + it('calls API and sets values', async () => { + const LOGO = 'logo'; + SettingsLogic.actions.setStagedLogo(LOGO); + const setLogoSpy = jest.spyOn(SettingsLogic.actions, 'setLogo'); + http.put.mockReturnValue(Promise.resolve({ logo: LOGO })); + + SettingsLogic.actions.updateOrgLogo(); + + expect(http.put).toHaveBeenCalledWith('/api/workplace_search/org/settings/upload_images', { + body: JSON.stringify({ logo: LOGO }), + }); + await nextTick(); + expect(flashSuccessToast).toHaveBeenCalledWith(ORG_UPDATED_MESSAGE); + expect(setLogoSpy).toHaveBeenCalledWith(LOGO); + }); + + it('handles error', async () => { + http.put.mockReturnValue(Promise.reject('this is an error')); + SettingsLogic.actions.updateOrgLogo(); + + await nextTick(); + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + }); + + it('resetOrgLogo', () => { + const updateOrgLogoSpy = jest.spyOn(SettingsLogic.actions, 'updateOrgLogo'); + SettingsLogic.actions.setStagedLogo('stagedLogo'); + SettingsLogic.actions.setLogo('logo'); + SettingsLogic.actions.resetOrgLogo(); + + expect(SettingsLogic.values.logo).toEqual(null); + expect(SettingsLogic.values.stagedLogo).toEqual(null); + expect(updateOrgLogoSpy).toHaveBeenCalled(); + }); + + it('resetOrgIcon', () => { + const updateOrgIconSpy = jest.spyOn(SettingsLogic.actions, 'updateOrgIcon'); + SettingsLogic.actions.setStagedIcon('stagedIcon'); + SettingsLogic.actions.setIcon('icon'); + SettingsLogic.actions.resetOrgIcon(); + + expect(SettingsLogic.values.icon).toEqual(null); + expect(SettingsLogic.values.stagedIcon).toEqual(null); + expect(updateOrgIconSpy).toHaveBeenCalled(); + }); + describe('updateOauthApplication', () => { it('calls API and sets values', async () => { const { name, redirectUri, confidential } = oauthApplication; @@ -179,7 +285,7 @@ describe('SettingsLogic', () => { ); await nextTick(); expect(setUpdatedOauthApplicationSpy).toHaveBeenCalledWith({ oauthApplication }); - expect(setSuccessMessage).toHaveBeenCalledWith(OAUTH_APP_UPDATED_MESSAGE); + expect(flashSuccessToast).toHaveBeenCalledWith(OAUTH_APP_UPDATED_MESSAGE); }); it('handles error', async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts index e07adbde15939..65a2cdf8c3f30 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts @@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n'; import { clearFlashMessages, setQueuedSuccessMessage, - setSuccessMessage, + flashSuccessToast, flashAPIErrors, } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; @@ -34,6 +34,8 @@ interface IOauthApplication { export interface SettingsServerProps { organizationName: string; oauthApplication: IOauthApplication; + logo: string | null; + icon: string | null; } interface SettingsActions { @@ -41,6 +43,10 @@ interface SettingsActions { onOrgNameInputChange(orgNameInputValue: string): string; setUpdatedName({ organizationName }: { organizationName: string }): string; setServerProps(props: SettingsServerProps): SettingsServerProps; + setIcon(icon: string | null): string | null; + setStagedIcon(stagedIcon: string | null): string | null; + setLogo(logo: string | null): string | null; + setStagedLogo(stagedLogo: string | null): string | null; setOauthApplication(oauthApplication: IOauthApplication): IOauthApplication; setUpdatedOauthApplication({ oauthApplication, @@ -52,6 +58,10 @@ interface SettingsActions { initializeConnectors(): void; updateOauthApplication(): void; updateOrgName(): void; + updateOrgLogo(): void; + updateOrgIcon(): void; + resetOrgLogo(): void; + resetOrgIcon(): void; deleteSourceConfig( serviceType: string, name: string @@ -66,14 +76,24 @@ interface SettingsValues { connectors: Connector[]; orgNameInputValue: string; oauthApplication: IOauthApplication | null; + logo: string | null; + icon: string | null; + stagedLogo: string | null; + stagedIcon: string | null; } +const imageRoute = '/api/workplace_search/org/settings/upload_images'; + export const SettingsLogic = kea>({ actions: { onInitializeConnectors: (connectors: Connector[]) => connectors, onOrgNameInputChange: (orgNameInputValue: string) => orgNameInputValue, setUpdatedName: ({ organizationName }) => organizationName, setServerProps: (props: SettingsServerProps) => props, + setIcon: (icon) => icon, + setStagedIcon: (stagedIcon) => stagedIcon, + setLogo: (logo) => logo, + setStagedLogo: (stagedLogo) => stagedLogo, setOauthApplication: (oauthApplication: IOauthApplication) => oauthApplication, setUpdatedOauthApplication: ({ oauthApplication }: { oauthApplication: IOauthApplication }) => oauthApplication, @@ -81,6 +101,10 @@ export const SettingsLogic = kea> initializeSettings: () => true, initializeConnectors: () => true, updateOrgName: () => true, + updateOrgLogo: () => true, + updateOrgIcon: () => true, + resetOrgLogo: () => true, + resetOrgIcon: () => true, updateOauthApplication: () => true, deleteSourceConfig: (serviceType: string, name: string) => ({ serviceType, @@ -113,10 +137,43 @@ export const SettingsLogic = kea> dataLoading: [ true, { + setServerProps: () => false, onInitializeConnectors: () => false, resetSettingsState: () => true, }, ], + logo: [ + null, + { + setServerProps: (_, { logo }) => logo, + setLogo: (_, logo) => logo, + resetOrgLogo: () => null, + }, + ], + stagedLogo: [ + null, + { + setStagedLogo: (_, stagedLogo) => stagedLogo, + resetOrgLogo: () => null, + setLogo: () => null, + }, + ], + icon: [ + null, + { + setServerProps: (_, { icon }) => icon, + setIcon: (_, icon) => icon, + resetOrgIcon: () => null, + }, + ], + stagedIcon: [ + null, + { + setStagedIcon: (_, stagedIcon) => stagedIcon, + resetOrgIcon: () => null, + setIcon: () => null, + }, + ], }, listeners: ({ actions, values }) => ({ initializeSettings: async () => { @@ -150,12 +207,38 @@ export const SettingsLogic = kea> try { const response = await http.put(route, { body }); actions.setUpdatedName(response); - setSuccessMessage(ORG_UPDATED_MESSAGE); + flashSuccessToast(ORG_UPDATED_MESSAGE); AppLogic.actions.setOrgName(name); } catch (e) { flashAPIErrors(e); } }, + updateOrgLogo: async () => { + const { http } = HttpLogic.values; + const { stagedLogo: logo } = values; + const body = JSON.stringify({ logo }); + + try { + const response = await http.put(imageRoute, { body }); + actions.setLogo(response.logo); + flashSuccessToast(ORG_UPDATED_MESSAGE); + } catch (e) { + flashAPIErrors(e); + } + }, + updateOrgIcon: async () => { + const { http } = HttpLogic.values; + const { stagedIcon: icon } = values; + const body = JSON.stringify({ icon }); + + try { + const response = await http.put(imageRoute, { body }); + actions.setIcon(response.icon); + flashSuccessToast(ORG_UPDATED_MESSAGE); + } catch (e) { + flashAPIErrors(e); + } + }, updateOauthApplication: async () => { const { http } = HttpLogic.values; const route = '/api/workplace_search/org/settings/oauth_application'; @@ -170,7 +253,7 @@ export const SettingsLogic = kea> try { const response = await http.put(route, { body }); actions.setUpdatedOauthApplication(response); - setSuccessMessage(OAUTH_APP_UPDATED_MESSAGE); + flashSuccessToast(OAUTH_APP_UPDATED_MESSAGE); } catch (e) { flashAPIErrors(e); } @@ -195,5 +278,11 @@ export const SettingsLogic = kea> resetSettingsState: () => { clearFlashMessages(); }, + resetOrgLogo: () => { + actions.updateOrgLogo(); + }, + resetOrgIcon: () => { + actions.updateOrgIcon(); + }, }), }); diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.test.ts index 00a5b6c75df0a..858bd71c50c44 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.test.ts @@ -10,6 +10,7 @@ import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks_ import { registerOrgSettingsRoute, registerOrgSettingsCustomizeRoute, + registerOrgSettingsUploadImagesRoute, registerOrgSettingsOauthApplicationRoute, } from './settings'; @@ -67,6 +68,36 @@ describe('settings routes', () => { }); }); + describe('PUT /api/workplace_search/org/settings/upload_images', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'put', + path: '/api/workplace_search/org/settings/upload_images', + }); + + registerOrgSettingsUploadImagesRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/settings/upload_images', + }); + }); + + describe('validates', () => { + it('correctly', () => { + const request = { body: { logo: 'foo', icon: null } }; + mockRouter.shouldValidate(request); + }); + }); + }); + describe('PUT /api/workplace_search/org/settings/oauth_application', () => { let mockRouter: MockRouter; diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.ts index bd8b5388625c6..aa8651f74bec5 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/settings.ts @@ -43,6 +43,26 @@ export function registerOrgSettingsCustomizeRoute({ ); } +export function registerOrgSettingsUploadImagesRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.put( + { + path: '/api/workplace_search/org/settings/upload_images', + validate: { + body: schema.object({ + logo: schema.maybe(schema.nullable(schema.string())), + icon: schema.maybe(schema.nullable(schema.string())), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/settings/upload_images', + }) + ); +} + export function registerOrgSettingsOauthApplicationRoute({ router, enterpriseSearchRequestHandler, @@ -69,5 +89,6 @@ export function registerOrgSettingsOauthApplicationRoute({ export const registerSettingsRoutes = (dependencies: RouteDependencies) => { registerOrgSettingsRoute(dependencies); registerOrgSettingsCustomizeRoute(dependencies); + registerOrgSettingsUploadImagesRoute(dependencies); registerOrgSettingsOauthApplicationRoute(dependencies); }; From 31b8a8229ca8110ba8a464a714588ec4532fbaca Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 11 Aug 2021 17:37:33 -0700 Subject: [PATCH 103/104] skip flaky suite (#107911) --- .../tests/exception_operators_data_types/text.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts index dbffeacb03b77..48832cef27cd9 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts @@ -33,7 +33,8 @@ export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const es = getService('es'); - describe('Rule exception operators for data type text', () => { + // FLAKY: https://github.com/elastic/kibana/issues/107911 + describe.skip('Rule exception operators for data type text', () => { beforeEach(async () => { await createSignalsIndex(supertest); await createListsIndex(supertest); From 7860c2aac3962b66730bc09a86a501e5196493b3 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 12 Aug 2021 03:09:50 +0100 Subject: [PATCH 104/104] chore(NA): moving @kbn/crypto to babel transpiler (#108189) * chore(NA): moving @kbn/crypto to babel transpiler * chore(NA): update configs --- packages/kbn-crypto/.babelrc | 3 +++ packages/kbn-crypto/BUILD.bazel | 22 +++++++++++++--------- packages/kbn-crypto/package.json | 4 ++-- packages/kbn-crypto/tsconfig.json | 3 ++- 4 files changed, 20 insertions(+), 12 deletions(-) create mode 100644 packages/kbn-crypto/.babelrc diff --git a/packages/kbn-crypto/.babelrc b/packages/kbn-crypto/.babelrc new file mode 100644 index 0000000000000..7da72d1779128 --- /dev/null +++ b/packages/kbn-crypto/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@kbn/babel-preset/node_preset"] +} diff --git a/packages/kbn-crypto/BUILD.bazel b/packages/kbn-crypto/BUILD.bazel index 36b61d0fb046b..0f35aab461078 100644 --- a/packages/kbn-crypto/BUILD.bazel +++ b/packages/kbn-crypto/BUILD.bazel @@ -1,6 +1,7 @@ load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") +load("//src/dev/bazel:index.bzl", "jsts_transpiler") PKG_BASE_NAME = "kbn-crypto" PKG_REQUIRE_NAME = "@kbn/crypto" @@ -26,22 +27,24 @@ NPM_MODULE_EXTRA_FILES = [ "README.md" ] -SRC_DEPS = [ +RUNTIME_DEPS = [ "//packages/kbn-dev-utils", "@npm//node-forge", ] TYPES_DEPS = [ + "//packages/kbn-dev-utils", "@npm//@types/flot", "@npm//@types/jest", "@npm//@types/node", "@npm//@types/node-forge", - "@npm//@types/testing-library__jest-dom", - "@npm//resize-observer-polyfill", - "@npm//@emotion/react", ] -DEPS = SRC_DEPS + TYPES_DEPS +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) ts_config( name = "tsconfig", @@ -53,13 +56,14 @@ ts_config( ) ts_project( - name = "tsc", + name = "tsc_types", args = ['--pretty'], srcs = SRCS, - deps = DEPS, + deps = TYPES_DEPS, declaration = True, declaration_map = True, - out_dir = "target", + emit_declaration_only = True, + out_dir = "target_types", source_map = True, root_dir = "src", tsconfig = ":tsconfig", @@ -68,7 +72,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = DEPS + [":tsc"], + deps = RUNTIME_DEPS + [":target_node", ":tsc_types"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-crypto/package.json b/packages/kbn-crypto/package.json index bbeb57e5b7cca..8fa6cd3c232fa 100644 --- a/packages/kbn-crypto/package.json +++ b/packages/kbn-crypto/package.json @@ -3,6 +3,6 @@ "version": "1.0.0", "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", - "main": "./target/index.js", - "types": "./target/index.d.ts" + "main": "./target_node/index.js", + "types": "./target_types/index.d.ts" } diff --git a/packages/kbn-crypto/tsconfig.json b/packages/kbn-crypto/tsconfig.json index af1a7c75c8e99..0863fc3f530de 100644 --- a/packages/kbn-crypto/tsconfig.json +++ b/packages/kbn-crypto/tsconfig.json @@ -1,9 +1,10 @@ { "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "outDir": "./target/types", "declaration": true, "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "./target_types", "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-crypto/src",