From 7ac836116313e458d793d8440f9cba7617ae4d14 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Mon, 7 Mar 2022 01:33:26 -0700 Subject: [PATCH 01/15] [Metrics UI] Update position of legend and it's controls (#115854) * [Metrics UI] Update position of legend and it's controls * updating button colors and moving history button back to the left * updating legend placement * removing unused dependencies * Adding data-test-subj for legendControls * removing unused deps * Fix linting errors * Move high value to top of legend * Reclaim top space left open by GroupNameContainer * Revert "Reclaim top space left open by GroupNameContainer" This reverts commit 411e89e01d99432714b042d0c2b0fcb248874ee2. This extra space is also serving as between-group margin. Also it doesn't solve the scrollbar overlap for multi-group cases. * Move legend after waffle map in dom This allows the waffle map to scroll without it overlapping the legend. * Move show/hide to right * Move timeline legend next to title * Move "hide history" button into timeline area * Revert "Move "hide history" button into timeline area" This reverts commit e6725c106faccdef505f1ffda4827c2fa8036111. * Revert "Move timeline legend next to title" This reverts commit 3d204d3e566d87da3e43c7e2ca9411490a560ced. * Revert "Move show/hide to right" This reverts commit fd1b9bd6571322d1560828d92f8644124b27729a. * Inline LegendControls and ViewSwitcher on mobile * Better legend alignment with action buttons Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kate Farrar Co-authored-by: Kate Farrar Co-authored-by: Mat Schaffer --- .../saved_views/toolbar_control.tsx | 1 + .../components/bottom_drawer.tsx | 17 +- .../inventory_view/components/layout.tsx | 323 +++++++++-------- .../components/nodes_overview.tsx | 7 + .../components/waffle/legend.tsx | 46 +-- .../components/waffle/legend_controls.tsx | 340 +++++++++--------- .../waffle/stepped_gradient_legend.tsx | 92 ++--- .../components/waffle/view_switcher.tsx | 4 +- 8 files changed, 398 insertions(+), 432 deletions(-) diff --git a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx index b44f3ffa20df71..7b7c256d5ad593 100644 --- a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx @@ -150,6 +150,7 @@ export function SavedViewsToolbarControls(props: Props) { data-test-subj="savedViews-openPopover" iconType="arrowDown" iconSide="right" + color="text" > {currentView ? currentView.name diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx index 3681d740d93d07..ad548a632573fd 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { useUiTracker } from '../../../../../../observability/public'; import { useWaffleOptionsContext } from '../hooks/use_waffle_options'; @@ -57,17 +57,6 @@ export const BottomDrawer: React.FC<{ {isOpen ? hideHistory : showHistory} - - {children} - - @@ -97,7 +86,3 @@ const BottomActionTopBar = euiStyled(EuiFlexGroup).attrs({ const ShowHideButton = euiStyled(EuiButtonEmpty).attrs({ size: 's' })` width: 140px; `; - -const RightSideSpacer = euiStyled(EuiSpacer).attrs({ size: 'xs' })` - width: 140px; -`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx index 5a3dafaabbd170..7f3de57b610a4d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx @@ -17,8 +17,12 @@ import { calculateBoundsFromNodes } from '../lib/calculate_bounds_from_nodes'; import { PageContent } from '../../../../components/page'; import { useWaffleTimeContext } from '../hooks/use_waffle_time'; import { useWaffleFiltersContext } from '../hooks/use_waffle_filters'; -import { DEFAULT_LEGEND, useWaffleOptionsContext } from '../hooks/use_waffle_options'; -import { InfraFormatterType } from '../../../../lib/lib'; +import { + DEFAULT_LEGEND, + useWaffleOptionsContext, + WaffleLegendOptions, +} from '../hooks/use_waffle_options'; +import { InfraFormatterType, InfraWaffleMapBounds } from '../../../../lib/lib'; import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { Toolbar } from './toolbars/toolbar'; import { ViewSwitcher } from './waffle/view_switcher'; @@ -26,7 +30,7 @@ import { createInventoryMetricFormatter } from '../lib/create_inventory_metric_f import { createLegend } from '../lib/create_legend'; import { useWaffleViewState } from '../hooks/use_waffle_view_state'; import { BottomDrawer } from './bottom_drawer'; -import { Legend } from './waffle/legend'; +import { LegendControls } from './waffle/legend_controls'; interface Props { shouldLoadDefault: boolean; @@ -37,149 +41,184 @@ interface Props { loading: boolean; } -export const Layout = ({ - shouldLoadDefault, - currentView, - reload, - interval, - nodes, - loading, -}: Props) => { - const [showLoading, setShowLoading] = useState(true); - const { metric, groupBy, sort, nodeType, changeView, view, autoBounds, boundsOverride, legend } = - useWaffleOptionsContext(); - const { currentTime, jumpToTime, isAutoReloading } = useWaffleTimeContext(); - const { applyFilterQuery } = useWaffleFiltersContext(); - const legendPalette = legend?.palette ?? DEFAULT_LEGEND.palette; - const legendSteps = legend?.steps ?? DEFAULT_LEGEND.steps; - const legendReverseColors = legend?.reverseColors ?? DEFAULT_LEGEND.reverseColors; - - const options = { - formatter: InfraFormatterType.percent, - formatTemplate: '{{value}}', - legend: createLegend(legendPalette, legendSteps, legendReverseColors), - metric, - sort, - groupBy, - }; - - useInterval( - () => { - if (!loading) { - jumpToTime(Date.now()); - } - }, - isAutoReloading ? 5000 : null - ); - - const dataBounds = calculateBoundsFromNodes(nodes); - const bounds = autoBounds ? dataBounds : boundsOverride; - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - const formatter = useCallback(createInventoryMetricFormatter(options.metric), [options.metric]); - const { onViewChange } = useWaffleViewState(); - - useEffect(() => { - if (currentView) { - onViewChange(currentView); - } - }, [currentView, onViewChange]); - - useEffect(() => { - // load snapshot data after default view loaded, unless we're not loading a view - if (currentView != null || !shouldLoadDefault) { - reload(); - } - - /** - * INFO: why disable exhaustive-deps - * We need to wait on the currentView not to be null because it is loaded async and could change the view state. - * We don't actually need to watch the value of currentView though, since the view state will be synched up by the - * changing params in the reload method so we should only "watch" the reload method. - * - * TODO: Should refactor this in the future to make it more clear where all the view state is coming - * from and it's precedence [query params, localStorage, defaultView, out of the box view] - */ +interface LegendControlOptions { + auto: boolean; + bounds: InfraWaffleMapBounds; + legend: WaffleLegendOptions; +} + +export const Layout = React.memo( + ({ shouldLoadDefault, currentView, reload, interval, nodes, loading }: Props) => { + const [showLoading, setShowLoading] = useState(true); + const { + metric, + groupBy, + sort, + nodeType, + changeView, + view, + autoBounds, + boundsOverride, + legend, + changeBoundsOverride, + changeAutoBounds, + changeLegend, + } = useWaffleOptionsContext(); + const { currentTime, jumpToTime, isAutoReloading } = useWaffleTimeContext(); + const { applyFilterQuery } = useWaffleFiltersContext(); + const legendPalette = legend?.palette ?? DEFAULT_LEGEND.palette; + const legendSteps = legend?.steps ?? DEFAULT_LEGEND.steps; + const legendReverseColors = legend?.reverseColors ?? DEFAULT_LEGEND.reverseColors; + + const options = { + formatter: InfraFormatterType.percent, + formatTemplate: '{{value}}', + legend: createLegend(legendPalette, legendSteps, legendReverseColors), + metric, + sort, + groupBy, + }; + + useInterval( + () => { + if (!loading) { + jumpToTime(Date.now()); + } + }, + isAutoReloading ? 5000 : null + ); + + const dataBounds = calculateBoundsFromNodes(nodes); + const bounds = autoBounds ? dataBounds : boundsOverride; /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [reload, shouldLoadDefault]); - - useEffect(() => { - setShowLoading(true); - }, [options.metric, nodeType]); - - useEffect(() => { - const hasNodes = nodes && nodes.length; - // Don't show loading screen when we're auto-reloading - setShowLoading(!hasNodes); - }, [nodes]); - - return ( - <> - - - {({ measureRef: pageMeasureRef, bounds: { width = 0 } }) => ( - - - {({ measureRef: topActionMeasureRef, bounds: { height: topActionHeight = 0 } }) => ( - <> - - - - - - - - - - {({ measureRef, bounds: { height = 0 } }) => ( - <> - - {view === 'map' && ( - { + if (currentView) { + onViewChange(currentView); + } + }, [currentView, onViewChange]); + + useEffect(() => { + // load snapshot data after default view loaded, unless we're not loading a view + if (currentView != null || !shouldLoadDefault) { + reload(); + } + + /** + * INFO: why disable exhaustive-deps + * We need to wait on the currentView not to be null because it is loaded async and could change the view state. + * We don't actually need to watch the value of currentView though, since the view state will be synched up by the + * changing params in the reload method so we should only "watch" the reload method. + * + * TODO: Should refactor this in the future to make it more clear where all the view state is coming + * from and it's precedence [query params, localStorage, defaultView, out of the box view] + */ + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + }, [reload, shouldLoadDefault]); + + useEffect(() => { + setShowLoading(true); + }, [options.metric, nodeType]); + + useEffect(() => { + const hasNodes = nodes && nodes.length; + // Don't show loading screen when we're auto-reloading + setShowLoading(!hasNodes); + }, [nodes]); + + const handleLegendControlChange = useCallback( + (opts: LegendControlOptions) => { + changeBoundsOverride(opts.bounds); + changeAutoBounds(opts.auto); + changeLegend(opts.legend); + }, + [changeBoundsOverride, changeAutoBounds, changeLegend] + ); + + return ( + <> + + + {({ measureRef: pageMeasureRef, bounds: { width = 0 } }) => ( + + + {({ + measureRef: topActionMeasureRef, + bounds: { height: topActionHeight = 0 }, + }) => ( + <> + + + + + {view === 'map' && ( + + + + )} + + + + + + + + {({ measureRef, bounds: { height = 0 } }) => ( + <> + - + {view === 'map' && ( + - - )} - - )} - - - )} - - - )} - - - - ); -}; + )} + + )} + + + )} + + + )} + + + + ); + } +); const MainContainer = euiStyled.div` position: relative; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx index 297f24e95bc4f1..cec595e4be3d66 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx @@ -18,6 +18,7 @@ import { Map } from './waffle/map'; import { TableView } from './table_view'; import { SnapshotNode } from '../../../../../common/http_api/snapshot_api'; import { calculateBoundsFromNodes } from '../lib/calculate_bounds_from_nodes'; +import { Legend } from './waffle/legend'; export interface KueryFilterQuery { kind: 'kuery'; @@ -131,6 +132,12 @@ export const NodesOverview = ({ bottomMargin={bottomMargin} staticHeight={isStatic} /> + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx index d305203b738c37..853aa98bf62447 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback } from 'react'; +import React from 'react'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { @@ -17,13 +17,7 @@ import { GradientLegendRT, } from '../../../../../lib/lib'; import { GradientLegend } from './gradient_legend'; -import { LegendControls } from './legend_controls'; import { StepLegend } from './steps_legend'; -import { - DEFAULT_LEGEND, - useWaffleOptionsContext, - WaffleLegendOptions, -} from '../../hooks/use_waffle_options'; import { SteppedGradientLegend } from './stepped_gradient_legend'; interface Props { legend: InfraWaffleMapLegend; @@ -32,39 +26,9 @@ interface Props { formatter: InfraFormatter; } -interface LegendControlOptions { - auto: boolean; - bounds: InfraWaffleMapBounds; - legend: WaffleLegendOptions; -} - -export const Legend: React.FC = ({ dataBounds, legend, bounds, formatter }) => { - const { - changeBoundsOverride, - changeAutoBounds, - autoBounds, - legend: legendOptions, - changeLegend, - boundsOverride, - } = useWaffleOptionsContext(); - const handleChange = useCallback( - (options: LegendControlOptions) => { - changeBoundsOverride(options.bounds); - changeAutoBounds(options.auto); - changeLegend(options.legend); - }, - [changeBoundsOverride, changeAutoBounds, changeLegend] - ); +export const Legend: React.FC = ({ legend, bounds, formatter }) => { return ( - {GradientLegendRT.is(legend) && ( )} @@ -77,8 +41,6 @@ export const Legend: React.FC = ({ dataBounds, legend, bounds, formatter }; const LegendContainer = euiStyled.div` - position: absolute; - bottom: 0px; - left: 10px; - right: 10px; + margin: 0 10px; + display: flex; `; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx index c7479434424a63..61b293888b85dc 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx @@ -26,7 +26,6 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import React, { SyntheticEvent, useState, useCallback, useEffect } from 'react'; import { first, last } from 'lodash'; -import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { InfraWaffleMapBounds, InventoryColorPalette, PALETTES } from '../../../../../lib/lib'; import { WaffleLegendOptions } from '../../hooks/use_waffle_options'; import { getColorPalette } from '../../lib/get_color_palette'; @@ -78,8 +77,10 @@ export const LegendControls = ({ const buttonComponent = ( - - Legend Options - - - <> - - - - - - - + Legend Options + + + <> + - - + + + + + - + + + + + + + + + } + isInvalid={!boundsValidRange} + display="columnCompressed" + error={errors} + > +
+ - - - + + + } + isInvalid={!boundsValidRange} + error={errors} + > +
+ - - + + + + + + - } - isInvalid={!boundsValidRange} - display="columnCompressed" - error={errors} - > -
- + + + + -
- - - } - isInvalid={!boundsValidRange} - error={errors} - > -
- -
-
- - - - - - - - - - - - - - - - + +
+
+ + ); }; - -const ControlContainer = euiStyled.div` - position: absolute; - top: -20px; - right: 6px; - bottom: 0; -`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/stepped_gradient_legend.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/stepped_gradient_legend.tsx index a9bcfa7995c200..339426b126b9e6 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/stepped_gradient_legend.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/stepped_gradient_legend.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { EuiText } from '@elastic/eui'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { InfraWaffleMapBounds, @@ -22,18 +23,19 @@ type TickValue = 0 | 1; export const SteppedGradientLegend: React.FC = ({ legend, bounds, formatter }) => { return ( - - - - + - {legend.rules.map((rule, index) => ( - - ))} + {legend.rules + .slice() + .reverse() + .map((rule, index) => ( + + ))} + ); }; @@ -46,62 +48,38 @@ interface TickProps { const TickLabel = ({ value, bounds, formatter }: TickProps) => { const normalizedValue = value === 0 ? bounds.min : bounds.max * value; - const style = { left: `${value * 100}%` }; const label = formatter(normalizedValue); - return {label}; + return ( +
+ {label} +
+ ); }; -const GradientStep = euiStyled.div` - height: ${(props) => props.theme.eui.paddingSizes.s}; - flex: 1 1 auto; - &:first-child { - border-radius: ${(props) => props.theme.eui.euiBorderRadius} 0 0 ${(props) => - props.theme.eui.euiBorderRadius}; - } - &:last-child { - border-radius: 0 ${(props) => props.theme.eui.euiBorderRadius} ${(props) => - props.theme.eui.euiBorderRadius} 0; - } +const LegendContainer = euiStyled.div` + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; `; -const Ticks = euiStyled.div` - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - top: -18px; +const GradientContainer = euiStyled.div` + height: 200px; + width: 10px; + display: flex; + flex-direction: column; + align-items: stretch; `; -const Tick = euiStyled.div` - position: absolute; - font-size: 11px; - text-align: center; - top: 0; - left: 0; - white-space: nowrap; - transform: translate(-50%, 0); +const GradientStep = euiStyled.div` + flex: 1 1 auto; &:first-child { - padding-left: 5px; - transform: translate(0, 0); + border-radius: ${(props) => props.theme.eui.euiBorderRadius} ${(props) => + props.theme.eui.euiBorderRadius} 0 0; } &:last-child { - padding-right: 5px; - transform: translate(-100%, 0); + border-radius: 0 0 ${(props) => props.theme.eui.euiBorderRadius} ${(props) => + props.theme.eui.euiBorderRadius}; } `; - -const GradientContainer = euiStyled.div` - display: flex; - flex-direction; row; - align-items: stretch; - flex-grow: 1; -`; - -const LegendContainer = euiStyled.div` - position: absolute; - height: 10px; - bottom: 0; - left: 0; - right: 40px; -`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx index 4dc288caa98332..8e911f7f829177 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx @@ -37,8 +37,8 @@ export const ViewSwitcher = ({ view, onChange }: Props) => { defaultMessage: 'Switch between table and map view', })} options={buttons} - color="primary" - buttonSize="m" + color="text" + buttonSize="s" idSelected={view} onChange={onChange} isIconOnly From 7c6d314cb0e91cde28db1c24b0acff21e447d839 Mon Sep 17 00:00:00 2001 From: Mark Hopkin Date: Mon, 7 Mar 2022 10:47:18 +0000 Subject: [PATCH 02/15] [Fleet] Retry Saved Object import on conflict error (#126900) * retry SO import on conflict errors * add jitter + increase retries * Apply suggestions from code review Co-authored-by: Josh Dover <1813008+joshdover@users.noreply.github.com> Co-authored-by: Josh Dover <1813008+joshdover@users.noreply.github.com> --- .../epm/kibana/assets/install.test.ts | 124 ++++++++++++++++++ .../services/epm/kibana/assets/install.ts | 51 +++++-- 2 files changed, 167 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/fleet/server/services/epm/kibana/assets/install.test.ts diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.test.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.test.ts new file mode 100644 index 00000000000000..51aee45c83cf3d --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.test.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 { + ISavedObjectsImporter, + SavedObjectsImportFailure, + SavedObjectsImportSuccess, + SavedObjectsImportResponse, +} from 'src/core/server'; + +import { loggingSystemMock } from '../../../../../../../../src/core/server/mocks'; + +import type { ArchiveAsset } from './install'; + +jest.mock('timers/promises', () => ({ + async setTimeout() {}, +})); + +import { installKibanaSavedObjects } from './install'; + +const mockLogger = loggingSystemMock.createLogger(); + +const mockImporter: jest.Mocked = { + import: jest.fn(), + resolveImportErrors: jest.fn(), +}; + +const createImportError = (so: ArchiveAsset, type: string) => + ({ id: so.id, error: { type } } as SavedObjectsImportFailure); +const createImportSuccess = (so: ArchiveAsset) => + ({ id: so.id, type: so.type, meta: {} } as SavedObjectsImportSuccess); +const createAsset = (asset: Partial) => + ({ id: 1234, type: 'dashboard', attributes: {}, ...asset } as ArchiveAsset); + +const createImportResponse = ( + errors: SavedObjectsImportFailure[] = [], + successResults: SavedObjectsImportSuccess[] = [] +) => + ({ + success: !!successResults.length, + errors, + successResults, + warnings: [], + successCount: successResults.length, + } as SavedObjectsImportResponse); + +describe('installKibanaSavedObjects', () => { + beforeEach(() => { + mockImporter.import.mockReset(); + mockImporter.resolveImportErrors.mockReset(); + }); + + it('should retry on conflict error', async () => { + const asset = createAsset({ attributes: { hello: 'world' } }); + const conflictResponse = createImportResponse([createImportError(asset, 'conflict')]); + const successResponse = createImportResponse([], [createImportSuccess(asset)]); + + mockImporter.import + .mockResolvedValueOnce(conflictResponse) + .mockResolvedValueOnce(successResponse); + + await installKibanaSavedObjects({ + savedObjectsImporter: mockImporter, + logger: mockLogger, + kibanaAssets: [asset], + }); + + expect(mockImporter.import).toHaveBeenCalledTimes(2); + }); + + it('should give up after 50 retries on conflict errors', async () => { + const asset = createAsset({ attributes: { hello: 'world' } }); + const conflictResponse = createImportResponse([createImportError(asset, 'conflict')]); + + mockImporter.import.mockImplementation(() => Promise.resolve(conflictResponse)); + + await expect( + installKibanaSavedObjects({ + savedObjectsImporter: mockImporter, + logger: mockLogger, + kibanaAssets: [asset], + }) + ).rejects.toEqual(expect.any(Error)); + expect(mockImporter.import).toHaveBeenCalledTimes(51); + }); + it('should not retry errors that arent conflict errors', async () => { + const asset = createAsset({ attributes: { hello: 'world' } }); + const errorResponse = createImportResponse([createImportError(asset, 'something_bad')]); + const successResponse = createImportResponse([], [createImportSuccess(asset)]); + + mockImporter.import.mockResolvedValueOnce(errorResponse).mockResolvedValueOnce(successResponse); + + expect( + installKibanaSavedObjects({ + savedObjectsImporter: mockImporter, + logger: mockLogger, + kibanaAssets: [asset], + }) + ).rejects.toEqual(expect.any(Error)); + }); + + it('should resolve reference errors', async () => { + const asset = createAsset({ attributes: { hello: 'world' } }); + const referenceErrorResponse = createImportResponse([ + createImportError(asset, 'missing_references'), + ]); + const successResponse = createImportResponse([], [createImportSuccess(asset)]); + + mockImporter.import.mockResolvedValueOnce(referenceErrorResponse); + mockImporter.resolveImportErrors.mockResolvedValueOnce(successResponse); + + await installKibanaSavedObjects({ + savedObjectsImporter: mockImporter, + logger: mockLogger, + kibanaAssets: [asset], + }); + + expect(mockImporter.import).toHaveBeenCalledTimes(1); + expect(mockImporter.resolveImportErrors).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts index 5ab15a1f52e755..d654fab427f198 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { setTimeout } from 'timers/promises'; + import type { SavedObject, SavedObjectsBulkCreateObject, @@ -13,7 +15,6 @@ import type { Logger, } from 'src/core/server'; import type { SavedObjectsImportSuccess, SavedObjectsImportFailure } from 'src/core/server/types'; - import { createListStream } from '@kbn/utils'; import { partition } from 'lodash'; @@ -166,7 +167,40 @@ export async function getKibanaAssets( return result; } -async function installKibanaSavedObjects({ +const isImportConflictError = (e: SavedObjectsImportFailure) => e?.error?.type === 'conflict'; +/** + * retry saved object import if only conflict errors are encountered + */ +async function retryImportOnConflictError( + importCall: () => ReturnType, + { + logger, + maxAttempts = 50, + _attempt = 0, + }: { logger?: Logger; _attempt?: number; maxAttempts?: number } = {} +): ReturnType { + const result = await importCall(); + + const errors = result.errors ?? []; + if (_attempt < maxAttempts && errors.length && errors.every(isImportConflictError)) { + const retryCount = _attempt + 1; + const retryDelayMs = 1000 + Math.floor(Math.random() * 3000); // 1s + 0-3s of jitter + + logger?.debug( + `Retrying import operation after [${ + retryDelayMs * 1000 + }s] due to conflict errors: ${JSON.stringify(errors)}` + ); + + await setTimeout(retryDelayMs); + return retryImportOnConflictError(importCall, { logger, _attempt: retryCount }); + } + + return result; +} + +// only exported for testing +export async function installKibanaSavedObjects({ savedObjectsImporter, kibanaAssets, logger, @@ -185,18 +219,19 @@ async function installKibanaSavedObjects({ return []; } else { const { successResults: importSuccessResults = [], errors: importErrors = [] } = - await savedObjectsImporter.import({ - overwrite: true, - readStream: createListStream(toBeSavedObjects), - createNewCopies: false, - }); + await retryImportOnConflictError(() => + savedObjectsImporter.import({ + overwrite: true, + readStream: createListStream(toBeSavedObjects), + createNewCopies: false, + }) + ); allSuccessResults = importSuccessResults; const [referenceErrors, otherErrors] = partition( importErrors, (e) => e?.error?.type === 'missing_references' ); - if (otherErrors?.length) { throw new Error( `Encountered ${ From 3c9014737a9791bd9365b1fd0f880a5b5d8bdacf Mon Sep 17 00:00:00 2001 From: Muhammad Ibragimov <53621505+mibragimov@users.noreply.github.com> Date: Mon, 7 Mar 2022 16:14:31 +0500 Subject: [PATCH 03/15] [Console] Support auto-complete for data streams (#126235) * Support auto-complete for data streams * Add a test case Co-authored-by: Muhammad Ibragimov Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../application/components/settings_modal.tsx | 11 +++++++++ .../data_stream_autocomplete_component.js | 20 ++++++++++++++++ .../lib/autocomplete/components/index.js | 1 + src/plugins/console/public/lib/kb/kb.js | 4 ++++ .../public/lib/mappings/mapping.test.js | 9 +++++++ .../console/public/lib/mappings/mappings.js | 24 ++++++++++++++++--- .../console/public/services/settings.ts | 3 ++- .../generated/indices.delete_data_stream.json | 2 +- .../generated/indices.get_data_stream.json | 3 ++- 9 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 src/plugins/console/public/lib/autocomplete/components/data_stream_autocomplete_component.js diff --git a/src/plugins/console/public/application/components/settings_modal.tsx b/src/plugins/console/public/application/components/settings_modal.tsx index c4be329dabcb88..eafc2dea3f8734 100644 --- a/src/plugins/console/public/application/components/settings_modal.tsx +++ b/src/plugins/console/public/application/components/settings_modal.tsx @@ -70,6 +70,7 @@ export function DevToolsSettingsModal(props: Props) { const [fields, setFields] = useState(props.settings.autocomplete.fields); const [indices, setIndices] = useState(props.settings.autocomplete.indices); const [templates, setTemplates] = useState(props.settings.autocomplete.templates); + const [dataStreams, setDataStreams] = useState(props.settings.autocomplete.dataStreams); const [polling, setPolling] = useState(props.settings.polling); const [pollInterval, setPollInterval] = useState(props.settings.pollInterval); const [tripleQuotes, setTripleQuotes] = useState(props.settings.tripleQuotes); @@ -97,12 +98,20 @@ export function DevToolsSettingsModal(props: Props) { }), stateSetter: setTemplates, }, + { + id: 'dataStreams', + label: i18n.translate('console.settingsPage.dataStreamsLabelText', { + defaultMessage: 'Data streams', + }), + stateSetter: setDataStreams, + }, ]; const checkboxIdToSelectedMap = { fields, indices, templates, + dataStreams, }; const onAutocompleteChange = (optionId: AutocompleteOptions) => { @@ -120,6 +129,7 @@ export function DevToolsSettingsModal(props: Props) { fields, indices, templates, + dataStreams, }, polling, pollInterval, @@ -170,6 +180,7 @@ export function DevToolsSettingsModal(props: Props) { fields, indices, templates, + dataStreams, }); }} > diff --git a/src/plugins/console/public/lib/autocomplete/components/data_stream_autocomplete_component.js b/src/plugins/console/public/lib/autocomplete/components/data_stream_autocomplete_component.js new file mode 100644 index 00000000000000..015136b7670f50 --- /dev/null +++ b/src/plugins/console/public/lib/autocomplete/components/data_stream_autocomplete_component.js @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { getDataStreams } from '../../mappings/mappings'; +import { ListComponent } from './list_component'; + +export class DataStreamAutocompleteComponent extends ListComponent { + constructor(name, parent, multiValued) { + super(name, getDataStreams, parent, multiValued); + } + + getContextKey() { + return 'data_stream'; + } +} diff --git a/src/plugins/console/public/lib/autocomplete/components/index.js b/src/plugins/console/public/lib/autocomplete/components/index.js index 32078ee2c1519a..4a8838a6fb821f 100644 --- a/src/plugins/console/public/lib/autocomplete/components/index.js +++ b/src/plugins/console/public/lib/autocomplete/components/index.js @@ -23,4 +23,5 @@ export { IdAutocompleteComponent } from './id_autocomplete_component'; export { UsernameAutocompleteComponent } from './username_autocomplete_component'; export { IndexTemplateAutocompleteComponent } from './index_template_autocomplete_component'; export { ComponentTemplateAutocompleteComponent } from './component_template_autocomplete_component'; +export { DataStreamAutocompleteComponent } from './data_stream_autocomplete_component'; export * from './legacy'; diff --git a/src/plugins/console/public/lib/kb/kb.js b/src/plugins/console/public/lib/kb/kb.js index 5f02365a48fdf9..e268f55be558e2 100644 --- a/src/plugins/console/public/lib/kb/kb.js +++ b/src/plugins/console/public/lib/kb/kb.js @@ -16,6 +16,7 @@ import { UsernameAutocompleteComponent, IndexTemplateAutocompleteComponent, ComponentTemplateAutocompleteComponent, + DataStreamAutocompleteComponent, } from '../autocomplete/components'; import $ from 'jquery'; @@ -94,6 +95,9 @@ const parametrizedComponentFactories = { component_template: function (name, parent) { return new ComponentTemplateAutocompleteComponent(name, parent); }, + data_stream: function (name, parent) { + return new DataStreamAutocompleteComponent(name, parent); + }, }; export function getUnmatchedEndpointComponents() { diff --git a/src/plugins/console/public/lib/mappings/mapping.test.js b/src/plugins/console/public/lib/mappings/mapping.test.js index 9191eb736be3c7..e2def74e892cc0 100644 --- a/src/plugins/console/public/lib/mappings/mapping.test.js +++ b/src/plugins/console/public/lib/mappings/mapping.test.js @@ -266,4 +266,13 @@ describe('Mappings', () => { expect(mappings.getIndexTemplates()).toEqual(expectedResult); expect(mappings.getComponentTemplates()).toEqual(expectedResult); }); + + test('Data streams', function () { + mappings.loadDataStreams({ + data_streams: [{ name: 'test_index1' }, { name: 'test_index2' }, { name: 'test_index3' }], + }); + + const expectedResult = ['test_index1', 'test_index2', 'test_index3']; + expect(mappings.getDataStreams()).toEqual(expectedResult); + }); }); diff --git a/src/plugins/console/public/lib/mappings/mappings.js b/src/plugins/console/public/lib/mappings/mappings.js index 75b8a263e8690c..96a5665e730a2b 100644 --- a/src/plugins/console/public/lib/mappings/mappings.js +++ b/src/plugins/console/public/lib/mappings/mappings.js @@ -17,6 +17,7 @@ let perAliasIndexes = []; let legacyTemplates = []; let indexTemplates = []; let componentTemplates = []; +let dataStreams = []; const mappingObj = {}; @@ -60,6 +61,10 @@ export function getComponentTemplates() { return [...componentTemplates]; } +export function getDataStreams() { + return [...dataStreams]; +} + export function getFields(indices, types) { // get fields for indices and types. Both can be a list, a string or null (meaning all). let ret = []; @@ -128,7 +133,9 @@ export function getTypes(indices) { export function getIndices(includeAliases) { const ret = []; $.each(perIndexTypes, function (index) { - ret.push(index); + if (!index.startsWith('.ds')) { + ret.push(index); + } }); if (typeof includeAliases === 'undefined' ? true : includeAliases) { $.each(perAliasIndexes, function (alias) { @@ -204,6 +211,10 @@ export function loadComponentTemplates(data) { componentTemplates = (data.component_templates ?? []).map(({ name }) => name); } +export function loadDataStreams(data) { + dataStreams = (data.data_streams ?? []).map(({ name }) => name); +} + export function loadMappings(mappings) { perIndexTypes = {}; @@ -265,6 +276,7 @@ function retrieveSettings(settingsKey, settingsToRetrieve) { legacyTemplates: '_template', indexTemplates: '_index_template', componentTemplates: '_component_template', + dataStreams: '_data_stream', }; // Fetch autocomplete info if setting is set to true, and if user has made changes. @@ -326,14 +338,16 @@ export function retrieveAutoCompleteInfo(settings, settingsToRetrieve) { 'componentTemplates', templatesSettingToRetrieve ); + const dataStreamsPromise = retrieveSettings('dataStreams', settingsToRetrieve); $.when( mappingPromise, aliasesPromise, legacyTemplatesPromise, indexTemplatesPromise, - componentTemplatesPromise - ).done((mappings, aliases, legacyTemplates, indexTemplates, componentTemplates) => { + componentTemplatesPromise, + dataStreamsPromise + ).done((mappings, aliases, legacyTemplates, indexTemplates, componentTemplates, dataStreams) => { let mappingsResponse; try { if (mappings && mappings.length) { @@ -365,6 +379,10 @@ export function retrieveAutoCompleteInfo(settings, settingsToRetrieve) { loadComponentTemplates(JSON.parse(componentTemplates[0])); } + if (dataStreams) { + loadDataStreams(JSON.parse(dataStreams[0])); + } + if (mappings && aliases) { // Trigger an update event with the mappings, aliases $(mappingObj).trigger('update', [mappingsResponse, aliases[0]]); diff --git a/src/plugins/console/public/services/settings.ts b/src/plugins/console/public/services/settings.ts index 058f6c20c18887..1a7eff3e7ca540 100644 --- a/src/plugins/console/public/services/settings.ts +++ b/src/plugins/console/public/services/settings.ts @@ -14,7 +14,7 @@ export const DEFAULT_SETTINGS = Object.freeze({ pollInterval: 60000, tripleQuotes: true, wrapMode: true, - autocomplete: Object.freeze({ fields: true, indices: true, templates: true }), + autocomplete: Object.freeze({ fields: true, indices: true, templates: true, dataStreams: true }), historyDisabled: false, }); @@ -25,6 +25,7 @@ export interface DevToolsSettings { fields: boolean; indices: boolean; templates: boolean; + dataStreams: boolean; }; polling: boolean; pollInterval: number; diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_data_stream.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_data_stream.json index 9b91e3deb3a089..fb5cb446fb77e2 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_data_stream.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.delete_data_stream.json @@ -13,7 +13,7 @@ "DELETE" ], "patterns": [ - "_data_stream/{name}" + "_data_stream/{data_stream}" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/data-streams.html" } diff --git a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_data_stream.json b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_data_stream.json index 45199a60f337d6..e383a1df4844a6 100644 --- a/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_data_stream.json +++ b/src/plugins/console/server/lib/spec_definitions/json/generated/indices.get_data_stream.json @@ -14,7 +14,8 @@ ], "patterns": [ "_data_stream", - "_data_stream/{name}" + "_data_stream/{name}", + "{data_stream}" ], "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/master/data-streams.html" } From c9e66b8327d3d3087bcd1788aed121977515fac0 Mon Sep 17 00:00:00 2001 From: CohenIdo <90558359+CohenIdo@users.noreply.github.com> Date: Mon, 7 Mar 2022 14:31:42 +0200 Subject: [PATCH 04/15] [Cloud Posture] add filterting for benchmark (#126980) --- .../routes/benchmarks/benchmarks.test.ts | 30 +++++++++++++++++++ .../server/routes/benchmarks/benchmarks.ts | 15 ++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.test.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.test.ts index b728948cf2a056..8c9d04dc207f3e 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.test.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.test.ts @@ -76,6 +76,18 @@ describe('benchmarks API', () => { }); }); + it('expect to find benchmark_name', async () => { + const validatedQuery = benchmarksInputSchema.validate({ + benchmark_name: 'my_cis_benchmark', + }); + + expect(validatedQuery).toMatchObject({ + page: 1, + per_page: DEFAULT_BENCHMARKS_PER_PAGE, + benchmark_name: 'my_cis_benchmark', + }); + }); + it('should throw when page field is not a positive integer', async () => { expect(() => { benchmarksInputSchema.validate({ page: -2 }); @@ -125,6 +137,24 @@ describe('benchmarks API', () => { }); }); + it('should format request by benchmark_name', async () => { + const mockAgentPolicyService = createPackagePolicyServiceMock(); + + await getPackagePolicies(mockSoClient, mockAgentPolicyService, 'myPackage', { + page: 1, + per_page: 100, + benchmark_name: 'my_cis_benchmark', + }); + + expect(mockAgentPolicyService.list.mock.calls[0][1]).toMatchObject( + expect.objectContaining({ + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:myPackage AND ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.name: *my_cis_benchmark*`, + page: 1, + perPage: 100, + }) + ); + }); + describe('test getAgentPolicies', () => { it('should return one agent policy id when there is duplication', async () => { const agentPolicyService = createMockAgentPolicyService(); diff --git a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts index 80c526c248c0ff..c52aeead6cd4da 100644 --- a/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts +++ b/x-pack/plugins/cloud_security_posture/server/routes/benchmarks/benchmarks.ts @@ -43,8 +43,13 @@ export interface Benchmark { export const DEFAULT_BENCHMARKS_PER_PAGE = 20; export const PACKAGE_POLICY_SAVED_OBJECT_TYPE = 'ingest-package-policies'; -const getPackageNameQuery = (packageName: string): string => { - return `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${packageName}`; +const getPackageNameQuery = (packageName: string, benchmarkFilter?: string): string => { + const integrationNameQuery = `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${packageName}`; + const kquery = benchmarkFilter + ? `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${packageName} AND ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.name: *${benchmarkFilter}*` + : integrationNameQuery; + + return kquery; }; export const getPackagePolicies = async ( @@ -57,7 +62,7 @@ export const getPackagePolicies = async ( throw new Error('packagePolicyService is undefined'); } - const packageNameQuery = getPackageNameQuery(packageName); + const packageNameQuery = getPackageNameQuery(packageName, queryParams.benchmark_name); const { items: packagePolicies } = (await packagePolicyService?.list(soClient, { kuery: packageNameQuery, @@ -193,4 +198,8 @@ export const benchmarksInputSchema = rt.object({ * The number of objects to include in each page */ per_page: rt.number({ defaultValue: DEFAULT_BENCHMARKS_PER_PAGE, min: 0 }), + /** + * Benchmark filter + */ + benchmark_name: rt.maybe(rt.string()), }); From bc0d9e70791be8dfdeec9770625950f884fb50e8 Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Mon, 7 Mar 2022 08:13:52 -0500 Subject: [PATCH 05/15] [Presentation] Fix some bugs with services dependency injection (#126936) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../create/dependency_manager.test.ts | 54 +++++++++++++------ .../services/create/dependency_manager.ts | 50 +++++++++++------ 2 files changed, 71 insertions(+), 33 deletions(-) diff --git a/src/plugins/presentation_util/public/services/create/dependency_manager.test.ts b/src/plugins/presentation_util/public/services/create/dependency_manager.test.ts index 29702c33568655..8e67dee3f8b6b4 100644 --- a/src/plugins/presentation_util/public/services/create/dependency_manager.test.ts +++ b/src/plugins/presentation_util/public/services/create/dependency_manager.test.ts @@ -24,6 +24,16 @@ describe('DependencyManager', () => { expect(DependencyManager.orderDependencies(graph)).toEqual(sortedTopology); }); + it('should include final vertex if it has dependencies', () => { + const graph = { + A: [], + B: [], + C: ['A', 'B'], + }; + const sortedTopology = ['A', 'B', 'C']; + expect(DependencyManager.orderDependencies(graph)).toEqual(sortedTopology); + }); + it('orderDependencies. Should return base topology if no depended vertices', () => { const graph = { N: [], @@ -34,22 +44,34 @@ describe('DependencyManager', () => { expect(DependencyManager.orderDependencies(graph)).toEqual(sortedTopology); }); - it('orderDependencies. Should detect circular dependencies and throw error with path', () => { - const graph = { - N: ['R'], - R: ['A'], - A: ['B'], - B: ['C'], - C: ['D'], - D: ['E'], - E: ['F'], - F: ['L'], - L: ['G'], - G: ['N'], - }; - const circularPath = ['N', 'R', 'A', 'B', 'C', 'D', 'E', 'F', 'L', 'G', 'N'].join(' -> '); - const errorMessage = `Circular dependency detected while setting up services: ${circularPath}`; + describe('circular dependencies', () => { + it('should detect circular dependencies and throw error with path', () => { + const graph = { + N: ['R'], + R: ['A'], + A: ['B'], + B: ['C'], + C: ['D'], + D: ['E'], + E: ['F'], + F: ['L'], + L: ['G'], + G: ['N'], + }; + const circularPath = ['G', 'L', 'F', 'E', 'D', 'C', 'B', 'A', 'R', 'N'].join(' -> '); + const errorMessage = `Circular dependency detected while setting up services: ${circularPath}`; + + expect(() => DependencyManager.orderDependencies(graph)).toThrowError(errorMessage); + }); + + it('should detect circular dependency if circular reference is the first dependency for a vertex', () => { + const graph = { + A: ['B'], + B: ['A', 'C'], + C: [], + }; - expect(() => DependencyManager.orderDependencies(graph)).toThrowError(errorMessage); + expect(() => DependencyManager.orderDependencies(graph)).toThrow(); + }); }); }); diff --git a/src/plugins/presentation_util/public/services/create/dependency_manager.ts b/src/plugins/presentation_util/public/services/create/dependency_manager.ts index de30b180607fe4..3925f3e9d9c4fe 100644 --- a/src/plugins/presentation_util/public/services/create/dependency_manager.ts +++ b/src/plugins/presentation_util/public/services/create/dependency_manager.ts @@ -41,7 +41,14 @@ export class DependencyManager { return cycleInfo; } - return DependencyManager.sortVerticesFrom(srcVertex, graph, sortedVertices, {}, {}); + return DependencyManager.sortVerticesFrom( + srcVertex, + graph, + sortedVertices, + {}, + {}, + cycleInfo + ); }, DependencyManager.createCycleInfo()); } @@ -58,24 +65,30 @@ export class DependencyManager { graph: Graph, sortedVertices: Set, visited: BreadCrumbs = {}, - inpath: BreadCrumbs = {} + inpath: BreadCrumbs = {}, + cycle: CycleDetectionResult ): CycleDetectionResult { visited[srcVertex] = true; inpath[srcVertex] = true; - const cycleInfo = graph[srcVertex]?.reduce | undefined>( - (info, vertex) => { - if (inpath[vertex]) { - const path = (Object.keys(inpath) as T[]).filter( - (visitedVertex) => inpath[visitedVertex] - ); - return DependencyManager.createCycleInfo([...path, vertex], true); - } else if (!visited[vertex]) { - return DependencyManager.sortVerticesFrom(vertex, graph, sortedVertices, visited, inpath); - } - return info; - }, - undefined - ); + + const vertexEdges = + graph[srcVertex] === undefined || graph[srcVertex] === null ? [] : graph[srcVertex]; + + cycle = vertexEdges!.reduce>((info, vertex) => { + if (inpath[vertex]) { + return { ...info, hasCycle: true }; + } else if (!visited[vertex]) { + return DependencyManager.sortVerticesFrom( + vertex, + graph, + sortedVertices, + visited, + inpath, + info + ); + } + return info; + }, cycle); inpath[srcVertex] = false; @@ -83,7 +96,10 @@ export class DependencyManager { sortedVertices.add(srcVertex); } - return cycleInfo ?? DependencyManager.createCycleInfo([...sortedVertices]); + return { + ...cycle, + path: [...sortedVertices], + }; } private static createCycleInfo( From f12891ee460536a9c7dbd1848290f0cb0d429502 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Mon, 7 Mar 2022 14:14:13 +0100 Subject: [PATCH 06/15] :bug: Handle case of undefined fitting for line/area (#126891) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/vis_types/xy/public/config/get_config.ts | 4 ++-- .../options/point_series/elastic_charts_options.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/vis_types/xy/public/config/get_config.ts b/src/plugins/vis_types/xy/public/config/get_config.ts index d7cf22625e10ed..7aad30c5b743e6 100644 --- a/src/plugins/vis_types/xy/public/config/get_config.ts +++ b/src/plugins/vis_types/xy/public/config/get_config.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { ScaleContinuousType } from '@elastic/charts'; +import { Fit, ScaleContinuousType } from '@elastic/charts'; import { Datatable } from '../../../../expressions/public'; import { BUCKET_TYPES } from '../../../../data/public'; @@ -92,7 +92,7 @@ export function getConfig( return { // NOTE: downscale ratio to match current vislib implementation markSizeRatio: radiusRatio * 0.6, - fittingFunction, + fittingFunction: fittingFunction ?? Fit.Linear, fillOpacity, detailedTooltip, orderBucketsBySum, diff --git a/src/plugins/vis_types/xy/public/editor/components/options/point_series/elastic_charts_options.tsx b/src/plugins/vis_types/xy/public/editor/components/options/point_series/elastic_charts_options.tsx index 105cd667990416..1c93fe92b79af7 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/point_series/elastic_charts_options.tsx +++ b/src/plugins/vis_types/xy/public/editor/components/options/point_series/elastic_charts_options.tsx @@ -78,7 +78,7 @@ export function ElasticChartsOptions(props: ValidationVisOptionsProps })} options={fittingFunctions} paramName="fittingFunction" - value={stateParams.fittingFunction} + value={stateParams.fittingFunction ?? fittingFunctions[2].value} setValue={(paramName, value) => { if (trackUiMetric) { trackUiMetric(METRIC_TYPE.CLICK, 'fitting_function_selected'); From 0807b53d75f663db4fb8508e4aa729aaba2ad792 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Mon, 7 Mar 2022 07:23:35 -0600 Subject: [PATCH 07/15] fix data view load err msg (#126974) --- src/plugins/data_views/common/data_views/data_views.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/data_views/common/data_views/data_views.ts b/src/plugins/data_views/common/data_views/data_views.ts index 2e31ed793c3dbb..04c1fd98a0f608 100644 --- a/src/plugins/data_views/common/data_views/data_views.ts +++ b/src/plugins/data_views/common/data_views/data_views.ts @@ -424,7 +424,7 @@ export class DataViewsService { ); if (!savedObject.version) { - throw new SavedObjectNotFound(DATA_VIEW_SAVED_OBJECT_TYPE, id, 'management/kibana/dataViews'); + throw new SavedObjectNotFound('data view', id, 'management/kibana/dataViews'); } return this.initFromSavedObject(savedObject); From 0adb328a9af39d35c02442198a74476ba86d9687 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Mon, 7 Mar 2022 07:24:14 -0600 Subject: [PATCH 08/15] [data views] Reenable data view validation functional test (#125892) * reenable test --- ...e_delete.js => _index_pattern_create_delete.ts} | 14 ++++++++------ test/functional/page_objects/settings_page.ts | 13 +++++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) rename test/functional/apps/management/{_index_pattern_create_delete.js => _index_pattern_create_delete.ts} (91%) diff --git a/test/functional/apps/management/_index_pattern_create_delete.js b/test/functional/apps/management/_index_pattern_create_delete.ts similarity index 91% rename from test/functional/apps/management/_index_pattern_create_delete.js rename to test/functional/apps/management/_index_pattern_create_delete.ts index 4c9f5a5210ac68..6b2036499a1edd 100644 --- a/test/functional/apps/management/_index_pattern_create_delete.js +++ b/test/functional/apps/management/_index_pattern_create_delete.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const browser = getService('browser'); @@ -35,8 +36,7 @@ export default function ({ getService, getPageObjects }) { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/124663 - describe.skip('validation', function () { + describe('validation', function () { it('can display errors', async function () { await PageObjects.settings.clickAddNewIndexPatternButton(); await PageObjects.settings.setIndexPatternField('log-fake*'); @@ -46,7 +46,7 @@ export default function ({ getService, getPageObjects }) { it('can resolve errors and submit', async function () { await PageObjects.settings.setIndexPatternField('log*'); - await (await PageObjects.settings.getSaveIndexPatternButton()).click(); + await (await PageObjects.settings.getSaveDataViewButtonActive()).click(); await PageObjects.settings.removeIndexPattern(); }); }); @@ -72,10 +72,12 @@ export default function ({ getService, getPageObjects }) { }); describe('index pattern creation', function indexPatternCreation() { - let indexPatternId; + let indexPatternId: string; before(function () { - return PageObjects.settings.createIndexPattern().then((id) => (indexPatternId = id)); + return PageObjects.settings + .createIndexPattern('logstash-*') + .then((id) => (indexPatternId = id)); }); it('should have index pattern in page header', async function () { diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 70cdbea7fa8970..9c0fc73a23675e 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -164,6 +164,19 @@ export class SettingsPageObject extends FtrService { return await this.testSubjects.find('saveIndexPatternButton'); } + async getSaveDataViewButtonActive() { + await this.retry.try(async () => { + expect( + ( + await this.find.allByCssSelector( + '[data-test-subj="saveIndexPatternButton"]:not(.euiButton-isDisabled)' + ) + ).length + ).to.be(1); + }); + return await this.testSubjects.find('saveIndexPatternButton'); + } + async getCreateButton() { return await this.find.displayedByCssSelector('[type="submit"]'); } From 8b82657d46e18920e3a1acc3133f10f78e25a6f4 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Mon, 7 Mar 2022 07:24:39 -0600 Subject: [PATCH 09/15] [data views] functional tests to typescript (#126977) js => ts --- ...ard.js => _create_index_pattern_wizard.ts} | 6 ++-- ...x_pattern.js => _exclude_index_pattern.ts} | 3 +- .../{_handle_alias.js => _handle_alias.ts} | 3 +- ...onflict.js => _handle_version_conflict.ts} | 4 +-- ...ern_filter.js => _index_pattern_filter.ts} | 5 +-- ...larity.js => _index_pattern_popularity.ts} | 5 +-- ...sort.js => _index_pattern_results_sort.ts} | 10 +++--- ...kibana_settings.js => _kibana_settings.ts} | 3 +- ...jects.js => _mgmt_import_saved_objects.ts} | 9 ++--- ...{_runtime_fields.js => _runtime_fields.ts} | 9 +++-- ...scripted_fields.js => _scripted_fields.ts} | 35 ++++++++++--------- ...s_filter.js => _scripted_fields_filter.ts} | 3 +- ...preview.js => _scripted_fields_preview.ts} | 9 ++--- ...st_huge_fields.js => _test_huge_fields.ts} | 5 +-- test/functional/page_objects/settings_page.ts | 6 ++-- 15 files changed, 65 insertions(+), 50 deletions(-) rename test/functional/apps/management/{_create_index_pattern_wizard.js => _create_index_pattern_wizard.ts} (93%) rename test/functional/apps/management/{_exclude_index_pattern.js => _exclude_index_pattern.ts} (89%) rename test/functional/apps/management/{_handle_alias.js => _handle_alias.ts} (95%) rename test/functional/apps/management/{_handle_version_conflict.js => _handle_version_conflict.ts} (96%) rename test/functional/apps/management/{_index_pattern_filter.js => _index_pattern_filter.ts} (90%) rename test/functional/apps/management/{_index_pattern_popularity.js => _index_pattern_popularity.ts} (92%) rename test/functional/apps/management/{_index_pattern_results_sort.js => _index_pattern_results_sort.ts} (90%) rename test/functional/apps/management/{_kibana_settings.js => _kibana_settings.ts} (96%) rename test/functional/apps/management/{_mgmt_import_saved_objects.js => _mgmt_import_saved_objects.ts} (80%) rename test/functional/apps/management/{_runtime_fields.js => _runtime_fields.ts} (91%) rename test/functional/apps/management/{_scripted_fields.js => _scripted_fields.ts} (96%) rename test/functional/apps/management/{_scripted_fields_filter.js => _scripted_fields_filter.ts} (95%) rename test/functional/apps/management/{_scripted_fields_preview.js => _scripted_fields_preview.ts} (90%) rename test/functional/apps/management/{_test_huge_fields.js => _test_huge_fields.ts} (90%) diff --git a/test/functional/apps/management/_create_index_pattern_wizard.js b/test/functional/apps/management/_create_index_pattern_wizard.ts similarity index 93% rename from test/functional/apps/management/_create_index_pattern_wizard.js rename to test/functional/apps/management/_create_index_pattern_wizard.ts index b2f24e530cb120..cf732e178aa74c 100644 --- a/test/functional/apps/management/_create_index_pattern_wizard.js +++ b/test/functional/apps/management/_create_index_pattern_wizard.ts @@ -6,7 +6,9 @@ * Side Public License, v 1. */ -export default function ({ getService, getPageObjects }) { +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); const es = getService('es'); @@ -38,7 +40,7 @@ export default function ({ getService, getPageObjects }) { body: { actions: [{ add: { index: 'blogs', alias: 'alias1' } }] }, }); - await PageObjects.settings.createIndexPattern('alias1', false); + await PageObjects.settings.createIndexPattern('alias1', null); }); it('can delete an index pattern', async () => { diff --git a/test/functional/apps/management/_exclude_index_pattern.js b/test/functional/apps/management/_exclude_index_pattern.ts similarity index 89% rename from test/functional/apps/management/_exclude_index_pattern.js rename to test/functional/apps/management/_exclude_index_pattern.ts index b71222c1ec44d6..8c20acdc21f926 100644 --- a/test/functional/apps/management/_exclude_index_pattern.js +++ b/test/functional/apps/management/_exclude_index_pattern.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['settings']); const es = getService('es'); diff --git a/test/functional/apps/management/_handle_alias.js b/test/functional/apps/management/_handle_alias.ts similarity index 95% rename from test/functional/apps/management/_handle_alias.js rename to test/functional/apps/management/_handle_alias.ts index 891e59d84a04bc..04496bf9ed7583 100644 --- a/test/functional/apps/management/_handle_alias.js +++ b/test/functional/apps/management/_handle_alias.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const es = getService('es'); const retry = getService('retry'); diff --git a/test/functional/apps/management/_handle_version_conflict.js b/test/functional/apps/management/_handle_version_conflict.ts similarity index 96% rename from test/functional/apps/management/_handle_version_conflict.js rename to test/functional/apps/management/_handle_version_conflict.ts index a04c5d34b2d351..2f65f966c55967 100644 --- a/test/functional/apps/management/_handle_version_conflict.js +++ b/test/functional/apps/management/_handle_version_conflict.ts @@ -16,8 +16,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const kibanaServer = getService('kibanaServer'); const browser = getService('browser'); @@ -93,7 +94,6 @@ export default function ({ getService, getPageObjects }) { expect(response.body.result).to.be('updated'); await PageObjects.settings.controlChangeSave(); await retry.try(async function () { - //await PageObjects.common.sleep(2000); const message = await PageObjects.common.closeToast(); expect(message).to.contain('Unable'); }); diff --git a/test/functional/apps/management/_index_pattern_filter.js b/test/functional/apps/management/_index_pattern_filter.ts similarity index 90% rename from test/functional/apps/management/_index_pattern_filter.js rename to test/functional/apps/management/_index_pattern_filter.ts index 3e9d316b59c618..afa64c474d39d4 100644 --- a/test/functional/apps/management/_index_pattern_filter.js +++ b/test/functional/apps/management/_index_pattern_filter.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const retry = getService('retry'); const PageObjects = getPageObjects(['settings']); @@ -23,7 +24,7 @@ export default function ({ getService, getPageObjects }) { }); beforeEach(async function () { - await PageObjects.settings.createIndexPattern(); + await PageObjects.settings.createIndexPattern('logstash-*'); }); afterEach(async function () { diff --git a/test/functional/apps/management/_index_pattern_popularity.js b/test/functional/apps/management/_index_pattern_popularity.ts similarity index 92% rename from test/functional/apps/management/_index_pattern_popularity.js rename to test/functional/apps/management/_index_pattern_popularity.ts index 1a71e4c5fbc68b..bff6cdce0f7a69 100644 --- a/test/functional/apps/management/_index_pattern_popularity.js +++ b/test/functional/apps/management/_index_pattern_popularity.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); const log = getService('log'); @@ -23,7 +24,7 @@ export default function ({ getService, getPageObjects }) { }); beforeEach(async () => { - await PageObjects.settings.createIndexPattern(); + await PageObjects.settings.createIndexPattern('logstash-*'); // increase Popularity of geo.coordinates log.debug('Starting openControlsByName (' + fieldName + ')'); await PageObjects.settings.openControlsByName(fieldName); diff --git a/test/functional/apps/management/_index_pattern_results_sort.js b/test/functional/apps/management/_index_pattern_results_sort.ts similarity index 90% rename from test/functional/apps/management/_index_pattern_results_sort.js rename to test/functional/apps/management/_index_pattern_results_sort.ts index cedf5ee355b36a..305a72889e95ab 100644 --- a/test/functional/apps/management/_index_pattern_results_sort.js +++ b/test/functional/apps/management/_index_pattern_results_sort.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const retry = getService('retry'); const PageObjects = getPageObjects(['settings', 'common']); @@ -18,7 +19,7 @@ export default function ({ getService, getPageObjects }) { // delete .kibana index and then wait for Kibana to re-create it await kibanaServer.uiSettings.replace({}); await PageObjects.settings.navigateTo(); - await PageObjects.settings.createIndexPattern(); + await PageObjects.settings.createIndexPattern('logstash-*'); }); after(async function () { @@ -30,7 +31,7 @@ export default function ({ getService, getPageObjects }) { heading: 'Name', first: '@message', last: 'xss.raw', - selector: async function () { + async selector() { const tableRow = await PageObjects.settings.getTableRow(0, 0); return await tableRow.getVisibleText(); }, @@ -39,7 +40,7 @@ export default function ({ getService, getPageObjects }) { heading: 'Type', first: '', last: 'text', - selector: async function () { + async selector() { const tableRow = await PageObjects.settings.getTableRow(0, 1); return await tableRow.getVisibleText(); }, @@ -49,7 +50,6 @@ export default function ({ getService, getPageObjects }) { columns.forEach(function (col) { describe('sort by heading - ' + col.heading, function indexPatternCreation() { it('should sort ascending', async function () { - console.log('col.heading', col.heading); if (col.heading !== 'Name') { await PageObjects.settings.sortBy(col.heading); } diff --git a/test/functional/apps/management/_kibana_settings.js b/test/functional/apps/management/_kibana_settings.ts similarity index 96% rename from test/functional/apps/management/_kibana_settings.js rename to test/functional/apps/management/_kibana_settings.ts index cfe4e88cda21de..d459643849fbc0 100644 --- a/test/functional/apps/management/_kibana_settings.js +++ b/test/functional/apps/management/_kibana_settings.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const browser = getService('browser'); const PageObjects = getPageObjects(['settings', 'common', 'dashboard', 'timePicker', 'header']); diff --git a/test/functional/apps/management/_mgmt_import_saved_objects.js b/test/functional/apps/management/_mgmt_import_saved_objects.ts similarity index 80% rename from test/functional/apps/management/_mgmt_import_saved_objects.js rename to test/functional/apps/management/_mgmt_import_saved_objects.ts index 95b0bbb7ed03b3..04a1bb59383223 100644 --- a/test/functional/apps/management/_mgmt_import_saved_objects.js +++ b/test/functional/apps/management/_mgmt_import_saved_objects.ts @@ -8,13 +8,14 @@ import expect from '@kbn/expect'; import path from 'path'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects']); - //in 6.4.0 bug the Saved Search conflict would be resolved and get imported but the visualization - //that referenced the saved search was not imported.( https://github.com/elastic/kibana/issues/22238) + // in 6.4.0 bug the Saved Search conflict would be resolved and get imported but the visualization + // that referenced the saved search was not imported.( https://github.com/elastic/kibana/issues/22238) describe('mgmt saved objects', function describeIndexTests() { before(async () => { @@ -41,7 +42,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.savedObjects.waitTableIsLoaded(); await PageObjects.savedObjects.searchForObject('mysaved'); - //instead of asserting on count- am asserting on the titles- which is more accurate than count. + // instead of asserting on count- am asserting on the titles- which is more accurate than count. const objects = await PageObjects.savedObjects.getRowTitles(); expect(objects.includes('mysavedsearch')).to.be(true); expect(objects.includes('mysavedviz')).to.be(true); diff --git a/test/functional/apps/management/_runtime_fields.js b/test/functional/apps/management/_runtime_fields.ts similarity index 91% rename from test/functional/apps/management/_runtime_fields.js rename to test/functional/apps/management/_runtime_fields.ts index 3a70df81b55d96..8ec9fb92c58eae 100644 --- a/test/functional/apps/management/_runtime_fields.js +++ b/test/functional/apps/management/_runtime_fields.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const log = getService('log'); const browser = getService('browser'); @@ -36,7 +37,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickIndexPatternLogstash(); - const startingCount = parseInt(await PageObjects.settings.getFieldsTabCount()); + const startingCount = parseInt(await PageObjects.settings.getFieldsTabCount(), 10); await log.debug('add runtime field'); await PageObjects.settings.addRuntimeField( fieldName, @@ -51,7 +52,9 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.clickSaveField(); await retry.try(async function () { - expect(parseInt(await PageObjects.settings.getFieldsTabCount())).to.be(startingCount + 1); + expect(parseInt(await PageObjects.settings.getFieldsTabCount(), 10)).to.be( + startingCount + 1 + ); }); }); diff --git a/test/functional/apps/management/_scripted_fields.js b/test/functional/apps/management/_scripted_fields.ts similarity index 96% rename from test/functional/apps/management/_scripted_fields.js rename to test/functional/apps/management/_scripted_fields.ts index 72f45e1fedb4db..c8c605ec7ed19e 100644 --- a/test/functional/apps/management/_scripted_fields.js +++ b/test/functional/apps/management/_scripted_fields.ts @@ -23,8 +23,9 @@ // it will automatically insert a a closing square brace ], etc. import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const log = getService('log'); const browser = getService('browser'); @@ -77,7 +78,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickIndexPatternLogstash(); - const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount()); + const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10); await PageObjects.settings.clickScriptedFieldsTab(); await log.debug('add scripted field'); const script = `1`; @@ -90,7 +91,7 @@ export default function ({ getService, getPageObjects }) { script ); await retry.try(async function () { - expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount())).to.be( + expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10)).to.be( startingCount + 1 ); }); @@ -111,7 +112,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickIndexPatternLogstash(); - const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount()); + const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10); await PageObjects.settings.clickScriptedFieldsTab(); await log.debug('add scripted field'); const script = `if (doc['machine.ram'].size() == 0) return -1; @@ -126,7 +127,7 @@ export default function ({ getService, getPageObjects }) { script ); await retry.try(async function () { - expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount())).to.be( + expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10)).to.be( startingCount + 1 ); }); @@ -150,7 +151,7 @@ export default function ({ getService, getPageObjects }) { }); }); - //add a test to sort numeric scripted field + // add a test to sort numeric scripted field it('should sort scripted field value in Discover', async function () { await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName}`); // after the first click on the scripted field, it becomes secondary sort after time. @@ -201,7 +202,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickIndexPatternLogstash(); - const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount()); + const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10); await PageObjects.settings.clickScriptedFieldsTab(); await log.debug('add scripted field'); await PageObjects.settings.addScriptedField( @@ -213,7 +214,7 @@ export default function ({ getService, getPageObjects }) { "if (doc['response.raw'].value == '200') { return 'good'} else { return 'bad'}" ); await retry.try(async function () { - expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount())).to.be( + expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10)).to.be( startingCount + 1 ); }); @@ -237,7 +238,7 @@ export default function ({ getService, getPageObjects }) { }); }); - //add a test to sort string scripted field + // add a test to sort string scripted field it('should sort scripted field value in Discover', async function () { await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); // after the first click on the scripted field, it becomes secondary sort after time. @@ -287,7 +288,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickIndexPatternLogstash(); - const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount()); + const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10); await PageObjects.settings.clickScriptedFieldsTab(); await log.debug('add scripted field'); await PageObjects.settings.addScriptedField( @@ -299,7 +300,7 @@ export default function ({ getService, getPageObjects }) { "doc['response.raw'].value == '200'" ); await retry.try(async function () { - expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount())).to.be( + expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10)).to.be( startingCount + 1 ); }); @@ -335,8 +336,8 @@ export default function ({ getService, getPageObjects }) { await filterBar.removeAllFilters(); }); - //add a test to sort boolean - //existing bug: https://github.com/elastic/kibana/issues/75519 hence the issue is skipped. + // add a test to sort boolean + // existing bug: https://github.com/elastic/kibana/issues/75519 hence the issue is skipped. it.skip('should sort scripted field value in Discover', async function () { await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); // after the first click on the scripted field, it becomes secondary sort after time. @@ -374,7 +375,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickIndexPatternLogstash(); - const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount()); + const startingCount = parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10); await PageObjects.settings.clickScriptedFieldsTab(); await log.debug('add scripted field'); await PageObjects.settings.addScriptedField( @@ -386,7 +387,7 @@ export default function ({ getService, getPageObjects }) { "doc['utc_time'].value.toEpochMilli() + (1000) * 60 * 60" ); await retry.try(async function () { - expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount())).to.be( + expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount(), 10)).to.be( startingCount + 1 ); }); @@ -410,8 +411,8 @@ export default function ({ getService, getPageObjects }) { }); }); - //add a test to sort date scripted field - //https://github.com/elastic/kibana/issues/75711 + // add a test to sort date scripted field + // https://github.com/elastic/kibana/issues/75711 it.skip('should sort scripted field value in Discover', async function () { await testSubjects.click(`docTableHeaderFieldSort_${scriptedPainlessFieldName2}`); // after the first click on the scripted field, it becomes secondary sort after time. diff --git a/test/functional/apps/management/_scripted_fields_filter.js b/test/functional/apps/management/_scripted_fields_filter.ts similarity index 95% rename from test/functional/apps/management/_scripted_fields_filter.js rename to test/functional/apps/management/_scripted_fields_filter.ts index abae9a300994dc..82d15908197506 100644 --- a/test/functional/apps/management/_scripted_fields_filter.js +++ b/test/functional/apps/management/_scripted_fields_filter.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const kibanaServer = getService('kibanaServer'); const retry = getService('retry'); const log = getService('log'); diff --git a/test/functional/apps/management/_scripted_fields_preview.js b/test/functional/apps/management/_scripted_fields_preview.ts similarity index 90% rename from test/functional/apps/management/_scripted_fields_preview.js rename to test/functional/apps/management/_scripted_fields_preview.ts index b6c941fe21d0ac..380b4659c0f38f 100644 --- a/test/functional/apps/management/_scripted_fields_preview.js +++ b/test/functional/apps/management/_scripted_fields_preview.ts @@ -7,13 +7,14 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const PageObjects = getPageObjects(['settings']); const SCRIPTED_FIELD_NAME = 'myScriptedField'; - const scriptResultToJson = (scriptResult) => { + const scriptResultToJson = (scriptResult: string) => { try { return JSON.parse(scriptResult); } catch (e) { @@ -26,7 +27,7 @@ export default function ({ getService, getPageObjects }) { await browser.setWindowSize(1200, 800); await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); - await PageObjects.settings.createIndexPattern(); + await PageObjects.settings.createIndexPattern('logstash-*'); await PageObjects.settings.navigateTo(); await PageObjects.settings.clickKibanaIndexPatterns(); @@ -67,7 +68,7 @@ export default function ({ getService, getPageObjects }) { it('should display additional fields', async function () { const scriptResults = await PageObjects.settings.executeScriptedField( `doc['bytes'].value * 2`, - ['bytes'] + 'bytes' ); const [{ _id, bytes }] = scriptResultToJson(scriptResults); expect(_id).to.be.a('string'); diff --git a/test/functional/apps/management/_test_huge_fields.js b/test/functional/apps/management/_test_huge_fields.ts similarity index 90% rename from test/functional/apps/management/_test_huge_fields.js rename to test/functional/apps/management/_test_huge_fields.ts index 7b756839409286..abc338cb8abc82 100644 --- a/test/functional/apps/management/_test_huge_fields.js +++ b/test/functional/apps/management/_test_huge_fields.ts @@ -7,8 +7,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const security = getService('security'); const PageObjects = getPageObjects(['common', 'home', 'settings']); @@ -19,7 +20,7 @@ export default function ({ getService, getPageObjects }) { const EXPECTED_FIELD_COUNT = '10006'; before(async function () { - await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader'], false); + await security.testUser.setRoles(['kibana_admin', 'test_testhuge_reader']); await esArchiver.emptyKibanaIndex(); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/large_fields'); await PageObjects.settings.navigateTo(); diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 9c0fc73a23675e..98fdff82e13c55 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -563,7 +563,7 @@ export class SettingsPageObject extends FtrService { name: string, language: string, type: string, - format: Record, + format: Record | null, popularity: string, script: string ) { @@ -803,7 +803,7 @@ export class SettingsPageObject extends FtrService { await this.flyout.ensureClosed('scriptedFieldsHelpFlyout'); } - async executeScriptedField(script: string, additionalField: string) { + async executeScriptedField(script: string, additionalField?: string) { this.log.debug('execute Scripted Fields help'); await this.closeScriptedFieldHelp(); // ensure script help is closed so script input is not blocked await this.setScriptedFieldScript(script); @@ -814,7 +814,7 @@ export class SettingsPageObject extends FtrService { await this.testSubjects.click('runScriptButton'); await this.testSubjects.waitForDeleted('.euiLoadingSpinner'); } - let scriptResults; + let scriptResults: string = ''; await this.retry.try(async () => { scriptResults = await this.testSubjects.getVisibleText('scriptedFieldPreview'); }); From d0b64d9cafb7ae33bb53f6f5437e3394a7210f33 Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Mon, 7 Mar 2022 13:55:24 +0000 Subject: [PATCH 10/15] [ML] Add API tests for analytics jobs_exist and new_job_caps endpoints (#126914) * [ML] Add API tests for analytics jobs_exist and new_job_caps endpoints * [ML] Edits following review * [ML] Edit to api doc Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../ml/server/routes/data_frame_analytics.ts | 10 +- .../apis/ml/data_frame_analytics/index.ts | 2 + .../data_frame_analytics/jobs_exist_spaces.ts | 97 ++++++++++++++++ .../ml/data_frame_analytics/new_job_caps.ts | 104 ++++++++++++++++++ 4 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 x-pack/test/api_integration/apis/ml/data_frame_analytics/jobs_exist_spaces.ts create mode 100644 x-pack/test/api_integration/apis/ml/data_frame_analytics/new_job_caps.ts diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index 2ab10bda361909..1fa7217e7d252d 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -609,12 +609,12 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense, routeGuard }: Rout /** * @apiGroup DataFrameAnalytics * - * @api {post} /api/ml/data_frame/analytics/job_exists Check whether jobs exists in current or any space - * @apiName JobExists - * @apiDescription Checks if each of the jobs in the specified list of IDs exist. + * @api {post} /api/ml/data_frame/analytics/jobs_exist Check whether jobs exist in current or any space + * @apiName JobsExist + * @apiDescription Checks if each of the jobs in the specified list of IDs exists. * If allSpaces is true, the check will look across all spaces. * - * @apiSchema (params) analyticsIdSchema + * @apiSchema (params) jobsExistSchema */ router.post( { @@ -707,7 +707,7 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense, routeGuard }: Rout /** * @apiGroup DataFrameAnalytics * - * @api {get} api/data_frame/analytics/fields/:indexPattern Get fields for a pattern of indices used for analytics + * @api {get} /api/ml/data_frame/analytics/new_job_caps/:indexPattern Get fields for a pattern of indices used for analytics * @apiName AnalyticsNewJobCaps * @apiDescription Retrieve the index fields for analytics */ diff --git a/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts b/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts index 21ff8f2cc64c16..9c9bcb318e7ec2 100644 --- a/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts +++ b/x-pack/test/api_integration/apis/ml/data_frame_analytics/index.ts @@ -22,5 +22,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./delete_spaces')); loadTestFile(require.resolve('./evaluate')); loadTestFile(require.resolve('./explain')); + loadTestFile(require.resolve('./jobs_exist_spaces')); + loadTestFile(require.resolve('./new_job_caps')); }); } diff --git a/x-pack/test/api_integration/apis/ml/data_frame_analytics/jobs_exist_spaces.ts b/x-pack/test/api_integration/apis/ml/data_frame_analytics/jobs_exist_spaces.ts new file mode 100644 index 00000000000000..4934af379ae660 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/data_frame_analytics/jobs_exist_spaces.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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; +import { USER } from '../../../../functional/services/ml/security_common'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + const spacesService = getService('spaces'); + const supertest = getService('supertestWithoutAuth'); + + const jobIdSpace1 = 'ihp_od_space1'; + const jobIdSpace2 = 'ihp_od_space2'; + const idSpace1 = 'space1'; + const idSpace2 = 'space2'; + + const initialModelMemoryLimit = '17mb'; + + async function runRequest( + space: string, + expectedStatusCode: number, + analyticsIds?: string[], + allSpaces?: boolean + ) { + const { body } = await supertest + .post(`/s/${space}/api/ml/data_frame/analytics/jobs_exist`) + .auth( + USER.ML_VIEWER_ALL_SPACES, + ml.securityCommon.getPasswordForUser(USER.ML_VIEWER_ALL_SPACES) + ) + .set(COMMON_REQUEST_HEADERS) + .send(allSpaces ? { analyticsIds, allSpaces } : { analyticsIds }) + .expect(expectedStatusCode); + + return body; + } + + describe('POST data_frame/analytics/jobs_exist with spaces', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/ihp_outlier'); + await spacesService.create({ id: idSpace1, name: 'space_one', disabledFeatures: [] }); + await spacesService.create({ id: idSpace2, name: 'space_two', disabledFeatures: [] }); + + const jobConfigSpace1 = ml.commonConfig.getDFAIhpOutlierDetectionJobConfig(jobIdSpace1); + await ml.api.createDataFrameAnalyticsJob( + { ...jobConfigSpace1, model_memory_limit: initialModelMemoryLimit }, + idSpace1 + ); + + const jobConfigSpace2 = ml.commonConfig.getDFAIhpOutlierDetectionJobConfig(jobIdSpace2); + await ml.api.createDataFrameAnalyticsJob( + { ...jobConfigSpace2, model_memory_limit: initialModelMemoryLimit }, + idSpace2 + ); + + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + after(async () => { + await spacesService.delete(idSpace1); + await spacesService.delete(idSpace2); + await ml.api.cleanMlIndices(); + await ml.testResources.cleanMLSavedObjects(); + }); + + it('should find single job from same space', async () => { + const body = await runRequest(idSpace1, 200, [jobIdSpace1]); + expect(body).to.eql({ [jobIdSpace1]: { exists: true } }); + }); + + it('should not find single job from different space', async () => { + const body = await runRequest(idSpace2, 200, [jobIdSpace1]); + expect(body).to.eql({ [jobIdSpace1]: { exists: false } }); + }); + + it('should only find job from same space when called with a list of jobs', async () => { + const body = await runRequest(idSpace1, 200, [jobIdSpace1, jobIdSpace2]); + expect(body).to.eql({ + [jobIdSpace1]: { exists: true }, + [jobIdSpace2]: { exists: false }, + }); + }); + + it('should find single job from different space when run across all spaces', async () => { + const body = await runRequest(idSpace1, 200, [jobIdSpace2], true); + expect(body).to.eql({ [jobIdSpace2]: { exists: true } }); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/data_frame_analytics/new_job_caps.ts b/x-pack/test/api_integration/apis/ml/data_frame_analytics/new_job_caps.ts new file mode 100644 index 00000000000000..72ac632a8b8dd2 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/data_frame_analytics/new_job_caps.ts @@ -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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; +import { USER } from '../../../../functional/services/ml/security_common'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + const supertest = getService('supertestWithoutAuth'); + const testIndexPattern = 'ft_bank_marketing'; + + async function runRequest(indexPattern: string, expectedStatusCode: number, rollup?: boolean) { + let url = `/api/ml/data_frame/analytics/new_job_caps/${indexPattern}`; + if (rollup !== undefined) { + url += `?rollup=${rollup}`; + } + const { body } = await supertest + .get(url) + .auth( + USER.ML_VIEWER_ALL_SPACES, + ml.securityCommon.getPasswordForUser(USER.ML_VIEWER_ALL_SPACES) + ) + .set(COMMON_REQUEST_HEADERS) + .expect(expectedStatusCode); + + return body; + } + + describe('GET data_frame/analytics/new_job_caps', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/bm_classification'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('should return job capabilities of fields for an index that exists', async () => { + const body = await runRequest(testIndexPattern, 200); + await ml.testExecution.logTestStep( + `response should contain object for ${testIndexPattern} index pattern` + ); + expect(body).to.have.keys(testIndexPattern); + const testIndexPatternCaps = body[testIndexPattern]; + + // The data frame analytics UI does not use the aggs prop, so just perform basic checks this prop + await ml.testExecution.logTestStep( + `should contain aggs and fields props for ${testIndexPattern} index pattern` + ); + expect(testIndexPatternCaps).to.have.keys('aggs', 'fields'); + const aggs = testIndexPatternCaps.aggs; + expect(aggs).to.have.length(35); + + // The data frames analytics UI uses this endpoint to extract the names and types of fields, + // so check this info is present for some example fields + const fields = testIndexPatternCaps.fields; + expect(fields).to.have.length(24); + + await ml.testExecution.logTestStep( + `fields should contain expected name and type attributes for ${testIndexPattern} index pattern` + ); + const balanceTextField = fields.find((obj: any) => obj.id === 'balance'); + expect(balanceTextField).to.have.keys('name', 'type'); + expect(balanceTextField.name).to.eql('balance'); + expect(balanceTextField.type).to.eql('text'); + + const balanceKeywordField = fields.find((obj: any) => obj.id === 'balance.keyword'); + expect(balanceKeywordField).to.have.keys('name', 'type'); + expect(balanceKeywordField.name).to.eql('balance.keyword'); + expect(balanceKeywordField.type).to.eql('keyword'); + }); + + it('should fail to return job capabilities of fields for an index that does not exist', async () => { + await runRequest(`${testIndexPattern}_invalid`, 404); + }); + + it('should return empty job capabilities of fields for a non-rollup index with rollup parameter set to true', async () => { + const body = await runRequest(testIndexPattern, 200, true); + await ml.testExecution.logTestStep( + `response should contain object for ${testIndexPattern} index pattern` + ); + expect(body).to.have.keys(testIndexPattern); + const testIndexPatternCaps = body[testIndexPattern]; + + await ml.testExecution.logTestStep( + `should contain empty aggs and fields props for ${testIndexPattern} index pattern` + ); + expect(testIndexPatternCaps).to.have.keys('aggs', 'fields'); + const aggs = testIndexPatternCaps.aggs; + expect(aggs).to.have.length(0); + const fields = testIndexPatternCaps.fields; + expect(fields).to.have.length(0); + }); + }); +}; From 3fe1270dd710ce81842356b254d78e9277aae007 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Mon, 7 Mar 2022 14:56:46 +0100 Subject: [PATCH 11/15] [Lens][Embeddable] Make Embeddable resilient when toggling actions (#126558) * :bug: Push to the bottom embeddable creation to better handle lifecycles * Update x-pack/plugins/lens/public/embeddable/embeddable_component.tsx * Update x-pack/plugins/lens/public/embeddable/embeddable_component.tsx * Update x-pack/plugins/lens/public/embeddable/embeddable_component.tsx Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../embeddable/embeddable_component.tsx | 53 ++++++++++++------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx b/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx index 482a5b931ed78a..f44aef76ab83d4 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx @@ -11,6 +11,7 @@ import type { Action, UiActionsStart } from 'src/plugins/ui_actions/public'; import type { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { EuiLoadingChart } from '@elastic/eui'; import { + EmbeddableFactory, EmbeddableInput, EmbeddableOutput, EmbeddablePanel, @@ -69,41 +70,48 @@ interface PluginsStartDependencies { } export function getEmbeddableComponent(core: CoreStart, plugins: PluginsStartDependencies) { + const { embeddable: embeddableStart, uiActions, inspector } = plugins; + const factory = embeddableStart.getEmbeddableFactory('lens')!; + const theme = core.theme; return (props: EmbeddableComponentProps) => { - const { embeddable: embeddableStart, uiActions, inspector } = plugins; - const factory = embeddableStart.getEmbeddableFactory('lens')!; const input = { ...props }; - const [embeddable, loading, error] = useEmbeddableFactory({ factory, input }); const hasActions = - Boolean(props.withDefaultActions) || (props.extraActions && props.extraActions?.length > 0); + Boolean(input.withDefaultActions) || (input.extraActions && input.extraActions?.length > 0); - const theme = core.theme; - - if (loading) { - return ; - } - - if (embeddable && hasActions) { + if (hasActions) { return ( } + factory={factory} uiActions={uiActions} inspector={inspector} actionPredicate={() => hasActions} input={input} theme={theme} - extraActions={props.extraActions} - withDefaultActions={props.withDefaultActions} + extraActions={input.extraActions} + withDefaultActions={input.withDefaultActions} /> ); } - - return ; + return ; }; } +function EmbeddableRootWrapper({ + factory, + input, +}: { + factory: EmbeddableFactory; + input: EmbeddableComponentProps; +}) { + const [embeddable, loading, error] = useEmbeddableFactory({ factory, input }); + if (loading) { + return ; + } + return ; +} + interface EmbeddablePanelWrapperProps { - embeddable: IEmbeddable; + factory: EmbeddableFactory; uiActions: PluginsStartDependencies['uiActions']; inspector: PluginsStartDependencies['inspector']; actionPredicate: (id: string) => boolean; @@ -114,7 +122,7 @@ interface EmbeddablePanelWrapperProps { } const EmbeddablePanelWrapper: FC = ({ - embeddable, + factory, uiActions, actionPredicate, inspector, @@ -123,10 +131,17 @@ const EmbeddablePanelWrapper: FC = ({ extraActions, withDefaultActions, }) => { + const [embeddable, loading] = useEmbeddableFactory({ factory, input }); useEffect(() => { - embeddable.updateInput(input); + if (embeddable) { + embeddable.updateInput(input); + } }, [embeddable, input]); + if (loading || !embeddable) { + return ; + } + return ( Date: Mon, 7 Mar 2022 15:58:24 +0200 Subject: [PATCH 12/15] [Cloud Posture] Update cloud security posture code owners (#127004) --- .github/CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 691daa042bba95..0a0aa994fb70bf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -425,8 +425,8 @@ x-pack/plugins/session_view @elastic/awp-platform # Security Asset Management /x-pack/plugins/osquery @elastic/security-asset-management -# Cloud Posture Security -/x-pack/plugins/cloud_security_posture/ @elastic/cloud-posture-security +# Cloud Security Posture +/x-pack/plugins/cloud_security_posture/ @elastic/cloud-security-posture-control-plane # Design (at the bottom for specificity of SASS files) **/*.scss @elastic/kibana-design From d5a1cdc1781bac6633e21d04b8598302dda3d158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=BCrkan=20YALAMAN?= Date: Mon, 7 Mar 2022 15:03:10 +0100 Subject: [PATCH 13/15] [App Search] Add an audit modal to show last changes on a modal. (#126486) * Add a last change column to the engine overview table * Add an audit modal to show last changes on the modal. --- .../enterprise_search/common/constants.ts | 1 + .../audit_logs_modal/audit_logs_modal.scss | 9 ++ .../audit_logs_modal.test.tsx | 52 ++++++++ .../audit_logs_modal/audit_logs_modal.tsx | 121 ++++++++++++++++++ .../audit_logs_modal_logic.test.ts | 53 ++++++++ .../audit_logs_modal_logic.ts | 32 +++++ .../components/tables/engine_link_helpers.tsx | 9 ++ .../components/tables/engines_table.tsx | 17 ++- .../components/tables/meta_engines_table.tsx | 14 ++ .../components/tables/shared_columns.tsx | 11 ++ .../components/engines/engines_overview.tsx | 2 + .../enterprise_search/server/plugin.ts | 9 ++ 12 files changed, 328 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal_logic.ts diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts index 7a6203c994f4d9..456a76d914f7d2 100644 --- a/x-pack/plugins/enterprise_search/common/constants.ts +++ b/x-pack/plugins/enterprise_search/common/constants.ts @@ -89,3 +89,4 @@ export const READ_ONLY_MODE_HEADER = 'x-ent-search-read-only-mode'; export const ENTERPRISE_SEARCH_KIBANA_COOKIE = '_enterprise_search'; export const LOGS_SOURCE_ID = 'ent-search-logs'; +export const ENTERPRISE_SEARCH_AUDIT_LOGS_SOURCE_ID = 'ent-search-audit-logs'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.scss new file mode 100644 index 00000000000000..11a008a3cc51fc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.scss @@ -0,0 +1,9 @@ +.auditLogsModal { + width: 75vw; +} + +@media (max-width: 1200px) { + .auditLogsModal { + width: 100vw; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.test.tsx new file mode 100644 index 00000000000000..f6687e431e9836 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.test.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LogicMounter, setMockValues, setMockActions } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiText, EuiModal } from '@elastic/eui'; + +import { EntSearchLogStream } from '../../../../../shared/log_stream'; + +import { AuditLogsModal } from './audit_logs_modal'; + +import { AuditLogsModalLogic } from './audit_logs_modal_logic'; + +describe('AuditLogsModal', () => { + const { mount } = new LogicMounter(AuditLogsModalLogic); + beforeEach(() => { + jest.clearAllMocks(); + mount({ isModalVisible: true }); + }); + + it('renders nothing by default', () => { + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('renders the modal when modal visible', () => { + const testEngineName = 'test-engine-123'; + const mockClose = jest.fn(); + setMockValues({ + isModalVisible: true, + engineName: testEngineName, + }); + setMockActions({ + hideModal: mockClose, + }); + + const wrapper = shallow(); + expect(wrapper.find(EntSearchLogStream).prop('query')).toBe( + `event.kind: event and event.action: audit and enterprisesearch.data_repository.name: ${testEngineName}` + ); + expect(wrapper.find(EuiText).children().text()).toBe('Showing events from last 24 hours'); + expect(wrapper.find(EuiModal).prop('onClose')).toBe(mockClose); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.tsx new file mode 100644 index 00000000000000..3807234fd5c118 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal.tsx @@ -0,0 +1,121 @@ +/* + * 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 { useValues, useActions } from 'kea'; + +import { + EuiButton, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { ENTERPRISE_SEARCH_AUDIT_LOGS_SOURCE_ID } from '../../../../../../../common/constants'; +import { EntSearchLogStream } from '../../../../../shared/log_stream'; + +import { AuditLogsModalLogic } from './audit_logs_modal_logic'; + +import './audit_logs_modal.scss'; + +export const AuditLogsModal: React.FC = () => { + const auditLogsModalLogic = AuditLogsModalLogic(); + const { isModalVisible, engineName } = useValues(auditLogsModalLogic); + const { hideModal } = useActions(auditLogsModalLogic); + + const filters = [ + 'event.kind: event', + 'event.action: audit', + `enterprisesearch.data_repository.name: ${engineName}`, + ].join(' and '); + + return !isModalVisible ? null : ( + + + +

{engineName}

+
+
+ + + {i18n.translate('xpack.enterpriseSearch.appSearch.engines.auditLogsModal.eventTip', { + defaultMessage: 'Showing events from last 24 hours', + })} + + + + + + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engines.auditLogsModal.closeButton', { + defaultMessage: 'Close', + })} + + +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal_logic.test.ts new file mode 100644 index 00000000000000..f869dd145087d3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal_logic.test.ts @@ -0,0 +1,53 @@ +/* + * 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 } from '../../../../../__mocks__/kea_logic'; + +import { AuditLogsModalLogic } from './audit_logs_modal_logic'; + +describe('AuditLogsModalLogic', () => { + const { mount } = new LogicMounter(AuditLogsModalLogic); + + beforeEach(() => { + jest.clearAllMocks(); + mount(); + }); + + it('has excepted default values', () => { + expect(AuditLogsModalLogic.values).toEqual({ + isModalVisible: false, + engineName: '', + }); + }); + + describe('actions', () => { + describe('hideModal', () => { + it('hides the modal', () => { + mount({ + isModalVisible: true, + engineName: 'test_engine', + }); + + AuditLogsModalLogic.actions.hideModal(); + expect(AuditLogsModalLogic.values).toEqual({ + isModalVisible: false, + engineName: '', + }); + }); + }); + + describe('showModal', () => { + it('show the modal with correct engine name', () => { + AuditLogsModalLogic.actions.showModal('test-engine-123'); + expect(AuditLogsModalLogic.values).toEqual({ + isModalVisible: true, + engineName: 'test-engine-123', + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal_logic.ts new file mode 100644 index 00000000000000..afa70b4f3dee0e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/audit_logs_modal/audit_logs_modal_logic.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea } from 'kea'; + +export const AuditLogsModalLogic = kea({ + path: ['enterprise_search', 'app_search', 'engines_overview', 'audit_logs_modal'], + actions: () => ({ + hideModal: true, + showModal: (engineName: string) => ({ engineName }), + }), + reducers: () => ({ + isModalVisible: [ + false, + { + showModal: () => true, + hideModal: () => false, + }, + ], + engineName: [ + '', + { + showModal: (_, { engineName }) => engineName, + hideModal: () => '', + }, + ], + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/engine_link_helpers.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/engine_link_helpers.tsx index a3350d1ef9939c..229e0def4700e9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/engine_link_helpers.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/engine_link_helpers.tsx @@ -7,11 +7,14 @@ import React from 'react'; +import { EuiLink } from '@elastic/eui'; + import { KibanaLogic } from '../../../../../shared/kibana'; import { EuiLinkTo } from '../../../../../shared/react_router_helpers'; import { TelemetryLogic } from '../../../../../shared/telemetry'; import { ENGINE_PATH } from '../../../../routes'; import { generateEncodedPath } from '../../../../utils/encode_path_params'; +import { FormattedDateTime } from '../../../../utils/formatted_date_time'; const sendEngineTableLinkClickTelemetry = () => { TelemetryLogic.actions.sendAppSearchTelemetry({ @@ -34,3 +37,9 @@ export const renderEngineLink = (engineName: string) => ( {engineName} ); + +export const renderLastChangeLink = (dateString: string, onClick = () => {}) => ( + + {!dateString ? '-' : } + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/engines_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/engines_table.tsx index 563e272a4a7303..5e6ece1003e7fc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/engines_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/engines_table.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import { useValues } from 'kea'; +import { useActions, useValues } from 'kea'; import { EuiBasicTable, EuiBasicTableColumn, EuiTableFieldDataColumnType } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -16,10 +16,13 @@ import { AppLogic } from '../../../../app_logic'; import { UNIVERSAL_LANGUAGE } from '../../../../constants'; import { EngineDetails } from '../../../engine/types'; -import { renderEngineLink } from './engine_link_helpers'; +import { AuditLogsModalLogic } from '../audit_logs_modal/audit_logs_modal_logic'; + +import { renderEngineLink, renderLastChangeLink } from './engine_link_helpers'; import { ACTIONS_COLUMN, CREATED_AT_COLUMN, + LAST_UPDATED_COLUMN, DOCUMENT_COUNT_COLUMN, FIELD_COUNT_COLUMN, NAME_COLUMN, @@ -46,12 +49,22 @@ export const EnginesTable: React.FC = ({ myRole: { canManageEngines }, } = useValues(AppLogic); + const { showModal: showAuditLogModal } = useActions(AuditLogsModalLogic); + const columns: Array> = [ { ...NAME_COLUMN, render: (name: string) => renderEngineLink(name), }, CREATED_AT_COLUMN, + { + ...LAST_UPDATED_COLUMN, + render: (dateString: string, engineDetails) => { + return renderLastChangeLink(dateString, () => { + showAuditLogModal(engineDetails.name); + }); + }, + }, LANGUAGE_COLUMN, DOCUMENT_COUNT_COLUMN, FIELD_COUNT_COLUMN, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table.tsx index f99dc7e15eaec5..24eb8cc8a6b812 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/meta_engines_table.tsx @@ -14,6 +14,9 @@ import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; import { AppLogic } from '../../../../app_logic'; import { EngineDetails } from '../../../engine/types'; +import { AuditLogsModalLogic } from '../audit_logs_modal/audit_logs_modal_logic'; + +import { renderLastChangeLink } from './engine_link_helpers'; import { MetaEnginesTableExpandedRow } from './meta_engines_table_expanded_row'; import { MetaEnginesTableLogic } from './meta_engines_table_logic'; import { MetaEnginesTableNameColumnContent } from './meta_engines_table_name_column_content'; @@ -21,6 +24,7 @@ import { ACTIONS_COLUMN, BLANK_COLUMN, CREATED_AT_COLUMN, + LAST_UPDATED_COLUMN, DOCUMENT_COUNT_COLUMN, FIELD_COUNT_COLUMN, NAME_COLUMN, @@ -49,6 +53,8 @@ export const MetaEnginesTable: React.FC = ({ myRole: { canManageMetaEngines }, } = useValues(AppLogic); + const { showModal: showAuditLogModal } = useActions(AuditLogsModalLogic); + const conflictingEnginesSets: ConflictingEnginesSets = useMemo( () => items.reduce((accumulator, metaEngine) => { @@ -89,6 +95,14 @@ export const MetaEnginesTable: React.FC = ({ ), }, CREATED_AT_COLUMN, + { + ...LAST_UPDATED_COLUMN, + render: (dateString: string, engineDetails) => { + return renderLastChangeLink(dateString, () => { + showAuditLogModal(engineDetails.name); + }); + }, + }, BLANK_COLUMN, DOCUMENT_COUNT_COLUMN, FIELD_COUNT_COLUMN, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/shared_columns.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/shared_columns.tsx index 325760b641efdc..b0ca36a7778389 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/shared_columns.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/tables/shared_columns.tsx @@ -50,6 +50,17 @@ export const CREATED_AT_COLUMN: EuiTableFieldDataColumnType = { render: (dateString: string) => , }; +export const LAST_UPDATED_COLUMN: EuiTableFieldDataColumnType = { + field: 'updated_at', + name: i18n.translate( + 'xpack.enterpriseSearch.appSearch.enginesOverview.table.column.lastUpdated', + { + defaultMessage: 'Last updated', + } + ), + dataType: 'string', +}; + export const DOCUMENT_COUNT_COLUMN: EuiTableFieldDataColumnType = { field: 'document_count', name: i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx index f8df9f5abfaa54..27cdff5d69812e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx @@ -21,6 +21,7 @@ import { DataPanel } from '../data_panel'; import { AppSearchPageTemplate } from '../layout'; import { EmptyState, EmptyMetaEnginesState } from './components'; +import { AuditLogsModal } from './components/audit_logs_modal/audit_logs_modal'; import { EnginesTable } from './components/tables/engines_table'; import { MetaEnginesTable } from './components/tables/meta_engines_table'; import { @@ -144,6 +145,7 @@ export const EnginesOverview: React.FC = () => { data-test-subj="metaEnginesLicenseCTA" /> )} + ); }; diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index ef9a0cea9da60f..f393ca59a44118 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -27,6 +27,7 @@ import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN, LOGS_SOURCE_ID, + ENTERPRISE_SEARCH_AUDIT_LOGS_SOURCE_ID, } from '../common/constants'; import { registerTelemetryUsageCollector as registerASTelemetryUsageCollector } from './collectors/app_search/telemetry'; @@ -185,6 +186,14 @@ export class EnterpriseSearchPlugin implements Plugin { indexName: '.ent-search-*', }, }); + + infra.defineInternalSourceConfiguration(ENTERPRISE_SEARCH_AUDIT_LOGS_SOURCE_ID, { + name: 'Enterprise Search Audit Logs', + logIndices: { + type: 'index_name', + indexName: 'logs-enterprise_search*', + }, + }); } public start() {} From 90f0d8de0130ec0b00dd666df1f99bb3a62ef27a Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Mon, 7 Mar 2022 07:07:04 -0700 Subject: [PATCH 14/15] [Reporting] Use the logger from Core instead of a wrapper (#126740) * [Reporting] Use the logger from Core instead of a wrapper * fix redudant log context in execute fns Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/reporting/server/config/config.ts | 7 +- .../server/config/create_config.test.ts | 13 ++-- .../reporting/server/config/create_config.ts | 7 +- x-pack/plugins/reporting/server/core.ts | 9 +-- .../common/decrypt_job_headers.test.ts | 4 +- .../common/decrypt_job_headers.ts | 5 +- .../export_types/common/generate_png.ts | 8 +-- .../common/get_custom_logo.test.ts | 11 ++-- .../export_types/common/get_custom_logo.ts | 5 +- .../csv_searchsource/execute_job.test.ts | 11 ++-- .../csv_searchsource/execute_job.ts | 3 +- .../generate_csv/generate_csv.test.ts | 17 ++--- .../generate_csv/generate_csv.ts | 15 ++--- .../generate_csv/get_export_settings.test.ts | 12 ++-- .../generate_csv/get_export_settings.ts | 5 +- .../csv_searchsource_immediate/execute_job.ts | 5 +- .../png/execute_job/index.test.ts | 14 ++-- .../export_types/png/execute_job/index.ts | 4 +- .../export_types/png_v2/execute_job.test.ts | 12 +--- .../server/export_types/png_v2/execute_job.ts | 4 +- .../printable_pdf/execute_job/index.test.ts | 12 +--- .../printable_pdf/execute_job/index.ts | 4 +- .../printable_pdf/lib/generate_pdf.ts | 6 +- .../printable_pdf_v2/execute_job.test.ts | 12 +--- .../printable_pdf_v2/execute_job.ts | 4 +- .../printable_pdf_v2/lib/generate_pdf.ts | 12 ++-- .../server/lib/check_params_version.ts | 6 +- .../server/lib/content_stream.test.ts | 8 +-- .../reporting/server/lib/content_stream.ts | 15 ++--- .../server/lib/event_logger/adapter.test.ts | 8 +-- .../server/lib/event_logger/adapter.ts | 15 +++-- .../server/lib/event_logger/logger.test.ts | 4 +- .../server/lib/event_logger/logger.ts | 5 +- x-pack/plugins/reporting/server/lib/index.ts | 1 - .../reporting/server/lib/level_logger.ts | 65 ------------------- .../reporting/server/lib/store/store.test.ts | 10 +-- .../reporting/server/lib/store/store.ts | 17 ++--- .../server/lib/tasks/error_logger.test.ts | 4 +- .../server/lib/tasks/error_logger.ts | 4 +- .../server/lib/tasks/execute_report.test.ts | 9 +-- .../server/lib/tasks/execute_report.ts | 19 +++--- .../server/lib/tasks/monitor_report.test.ts | 9 +-- .../server/lib/tasks/monitor_reports.ts | 11 ++-- .../plugins/reporting/server/plugin.test.ts | 12 ++-- x-pack/plugins/reporting/server/plugin.ts | 8 +-- .../routes/deprecations/deprecations.ts | 10 +-- .../integration_tests/deprecations.test.ts | 6 +- .../server/routes/diagnostic/browser.ts | 6 +- .../server/routes/diagnostic/index.ts | 4 +- .../integration_tests/browser.test.ts | 4 +- .../integration_tests/screenshot.test.ts | 4 +- .../server/routes/diagnostic/screenshot.ts | 2 +- .../generate/csv_searchsource_immediate.ts | 12 ++-- .../generate/generate_from_jobparams.ts | 8 +-- .../generation_from_jobparams.test.ts | 4 +- .../plugins/reporting/server/routes/index.ts | 4 +- .../reporting/server/routes/lib/jobs_query.ts | 4 +- .../server/routes/lib/request_handler.test.ts | 10 +-- .../server/routes/lib/request_handler.ts | 12 ++-- .../test_helpers/create_mock_levellogger.ts | 29 --------- .../create_mock_reportingplugin.ts | 12 ++-- .../reporting/server/test_helpers/index.ts | 1 - x-pack/plugins/reporting/server/types.ts | 7 +- 63 files changed, 223 insertions(+), 367 deletions(-) delete mode 100644 x-pack/plugins/reporting/server/lib/level_logger.ts delete mode 100644 x-pack/plugins/reporting/server/test_helpers/create_mock_levellogger.ts diff --git a/x-pack/plugins/reporting/server/config/config.ts b/x-pack/plugins/reporting/server/config/config.ts index 00c57053653f75..269a66503a741f 100644 --- a/x-pack/plugins/reporting/server/config/config.ts +++ b/x-pack/plugins/reporting/server/config/config.ts @@ -7,8 +7,7 @@ import { get } from 'lodash'; import { first } from 'rxjs/operators'; -import { CoreSetup, PluginInitializerContext } from 'src/core/server'; -import { LevelLogger } from '../lib'; +import type { CoreSetup, Logger, PluginInitializerContext } from 'kibana/server'; import { createConfig$ } from './create_config'; import { ReportingConfigType } from './schema'; @@ -63,13 +62,13 @@ export interface ReportingConfig extends Config { * @internal * @param {PluginInitializerContext} initContext * @param {CoreSetup} core - * @param {LevelLogger} logger + * @param {Logger} logger * @returns {Promise} */ export const buildConfig = async ( initContext: PluginInitializerContext, core: CoreSetup, - logger: LevelLogger + logger: Logger ): Promise => { const config$ = initContext.config.create(); const { http } = core; diff --git a/x-pack/plugins/reporting/server/config/create_config.test.ts b/x-pack/plugins/reporting/server/config/create_config.test.ts index fd8180bd46a05d..f839d72e1a45df 100644 --- a/x-pack/plugins/reporting/server/config/create_config.test.ts +++ b/x-pack/plugins/reporting/server/config/create_config.test.ts @@ -6,11 +6,10 @@ */ import * as Rx from 'rxjs'; -import { CoreSetup, HttpServerInfo, PluginInitializerContext } from 'src/core/server'; -import { coreMock } from 'src/core/server/mocks'; -import { LevelLogger } from '../lib/level_logger'; -import { createMockConfigSchema, createMockLevelLogger } from '../test_helpers'; -import { ReportingConfigType } from './'; +import type { CoreSetup, HttpServerInfo, Logger, PluginInitializerContext } from 'kibana/server'; +import { coreMock, loggingSystemMock } from 'src/core/server/mocks'; +import { createMockConfigSchema } from '../test_helpers'; +import type { ReportingConfigType } from './'; import { createConfig$ } from './create_config'; const createMockConfig = ( @@ -20,14 +19,14 @@ const createMockConfig = ( describe('Reporting server createConfig$', () => { let mockCoreSetup: CoreSetup; let mockInitContext: PluginInitializerContext; - let mockLogger: jest.Mocked; + let mockLogger: jest.Mocked; beforeEach(() => { mockCoreSetup = coreMock.createSetup(); mockInitContext = coreMock.createPluginInitializerContext( createMockConfigSchema({ kibanaServer: {} }) ); - mockLogger = createMockLevelLogger(); + mockLogger = loggingSystemMock.createLogger(); }); afterEach(() => { diff --git a/x-pack/plugins/reporting/server/config/create_config.ts b/x-pack/plugins/reporting/server/config/create_config.ts index 2ac225ec4576a8..ff8d00c30d4f83 100644 --- a/x-pack/plugins/reporting/server/config/create_config.ts +++ b/x-pack/plugins/reporting/server/config/create_config.ts @@ -7,11 +7,10 @@ import crypto from 'crypto'; import ipaddr from 'ipaddr.js'; +import type { CoreSetup, Logger } from 'kibana/server'; import { sum } from 'lodash'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { CoreSetup } from 'src/core/server'; -import { LevelLogger } from '../lib'; import { ReportingConfigType } from './schema'; /* @@ -22,9 +21,9 @@ import { ReportingConfigType } from './schema'; export function createConfig$( core: CoreSetup, config$: Observable, - parentLogger: LevelLogger + parentLogger: Logger ) { - const logger = parentLogger.clone(['config']); + const logger = parentLogger.get('config'); return config$.pipe( map((config) => { // encryption key diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index 745542c358a69b..a4e4f43f90e1ee 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -11,6 +11,7 @@ import { filter, first, map, switchMap, take } from 'rxjs/operators'; import type { BasePath, IClusterClient, + Logger, PackageInfo, PluginInitializerContext, SavedObjectsClientContract, @@ -32,7 +33,7 @@ import { REPORTING_REDIRECT_LOCATOR_STORE_KEY } from '../common/constants'; import { durationToNumber } from '../common/schema_utils'; import type { ReportingConfig, ReportingSetup } from './'; import { ReportingConfigType } from './config'; -import { checkLicense, getExportTypesRegistry, LevelLogger } from './lib'; +import { checkLicense, getExportTypesRegistry } from './lib'; import { reportingEventLoggerFactory } from './lib/event_logger/logger'; import type { IReport, ReportingStore } from './lib/store'; import { ExecuteReportTask, MonitorReportsTask, ReportTaskParams } from './lib/tasks'; @@ -45,7 +46,7 @@ export interface ReportingInternalSetup { security?: SecurityPluginSetup; spaces?: SpacesPluginSetup; taskManager: TaskManagerSetupContract; - logger: LevelLogger; + logger: Logger; status: StatusServiceSetup; } @@ -57,7 +58,7 @@ export interface ReportingInternalStart { data: DataPluginStart; fieldFormats: FieldFormatsStart; licensing: LicensingPluginStart; - logger: LevelLogger; + logger: Logger; screenshotting: ScreenshottingStart; security?: SecurityPluginStart; taskManager: TaskManagerStartContract; @@ -81,7 +82,7 @@ export class ReportingCore { public getContract: () => ReportingSetup; - constructor(private logger: LevelLogger, context: PluginInitializerContext) { + constructor(private logger: Logger, context: PluginInitializerContext) { this.packageInfo = context.env.packageInfo; const syncConfig = context.config.get(); this.deprecatedAllowedRoles = syncConfig.roles.enabled ? syncConfig.roles.allow : false; diff --git a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts index b5258d91485f70..56a1c39e75aa46 100644 --- a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts @@ -5,11 +5,11 @@ * 2.0. */ +import { loggingSystemMock } from 'src/core/server/mocks'; import { cryptoFactory } from '../../lib'; -import { createMockLevelLogger } from '../../test_helpers'; import { decryptJobHeaders } from './'; -const logger = createMockLevelLogger(); +const logger = loggingSystemMock.createLogger(); const encryptHeaders = async (encryptionKey: string, headers: Record) => { const crypto = cryptoFactory(encryptionKey); diff --git a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts index f126d1edbfce3a..3dfcfe362abd49 100644 --- a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts @@ -6,12 +6,13 @@ */ import { i18n } from '@kbn/i18n'; -import { cryptoFactory, LevelLogger } from '../../lib'; +import type { Logger } from 'kibana/server'; +import { cryptoFactory } from '../../lib'; export const decryptJobHeaders = async ( encryptionKey: string | undefined, headers: string, - logger: LevelLogger + logger: Logger ): Promise> => { try { if (typeof headers !== 'string') { diff --git a/x-pack/plugins/reporting/server/export_types/common/generate_png.ts b/x-pack/plugins/reporting/server/export_types/common/generate_png.ts index caa0b7fb91b3f2..272d1c287178ad 100644 --- a/x-pack/plugins/reporting/server/export_types/common/generate_png.ts +++ b/x-pack/plugins/reporting/server/export_types/common/generate_png.ts @@ -6,14 +6,14 @@ */ import apm from 'elastic-apm-node'; +import type { Logger } from 'kibana/server'; import * as Rx from 'rxjs'; import { finalize, map, tap } from 'rxjs/operators'; +import type { ReportingCore } from '../../'; import { LayoutTypes } from '../../../../screenshotting/common'; import { REPORTING_TRANSACTION_TYPE } from '../../../common/constants'; import type { PngMetrics } from '../../../common/types'; -import { ReportingCore } from '../../'; -import { ScreenshotOptions } from '../../types'; -import { LevelLogger } from '../../lib'; +import type { ScreenshotOptions } from '../../types'; interface PngResult { buffer: Buffer; @@ -23,7 +23,7 @@ interface PngResult { export function generatePngObservable( reporting: ReportingCore, - logger: LevelLogger, + logger: Logger, options: ScreenshotOptions ): Rx.Observable { const apmTrans = apm.startTransaction('generate-png', REPORTING_TRANSACTION_TYPE); diff --git a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts index f5675b50cfddd4..850d0ae507e126 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts @@ -5,17 +5,14 @@ * 2.0. */ -import { ReportingCore } from '../..'; -import { - createMockConfigSchema, - createMockLevelLogger, - createMockReportingCore, -} from '../../test_helpers'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { ReportingCore } from '../../'; +import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import { getCustomLogo } from './get_custom_logo'; let mockReportingPlugin: ReportingCore; -const logger = createMockLevelLogger(); +const logger = loggingSystemMock.createLogger(); beforeEach(async () => { mockReportingPlugin = await createMockReportingCore(createMockConfigSchema()); diff --git a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts index fcabd34a642c8c..10873155039885 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.ts @@ -5,16 +5,15 @@ * 2.0. */ -import type { Headers } from 'src/core/server'; +import type { Headers, Logger } from 'kibana/server'; import { ReportingCore } from '../../'; import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../common/constants'; -import { LevelLogger } from '../../lib'; export const getCustomLogo = async ( reporting: ReportingCore, headers: Headers, spaceId: string | undefined, - logger: LevelLogger + logger: Logger ) => { const fakeRequest = reporting.getFakeRequest({ headers }, spaceId, logger); const uiSettingsClient = await reporting.getUiSettingsClient(fakeRequest, logger); diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts index ee6d6daab88e06..5a8c4f1fd760c0 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts @@ -16,18 +16,15 @@ jest.mock('./generate_csv/generate_csv', () => ({ }, })); -import { Writable } from 'stream'; import nodeCrypto from '@elastic/node-crypto'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { Writable } from 'stream'; import { ReportingCore } from '../../'; import { CancellationToken } from '../../../common/cancellation_token'; -import { - createMockConfigSchema, - createMockLevelLogger, - createMockReportingCore, -} from '../../test_helpers'; +import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import { runTaskFnFactory } from './execute_job'; -const logger = createMockLevelLogger(); +const logger = loggingSystemMock.createLogger(); const encryptionKey = 'tetkey'; const headers = { sid: 'cooltestheaders' }; let encryptedHeaders: string; diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.ts index 97f0aa65e3d68e..8b5f0e5395827b 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { CSV_JOB_TYPE } from '../../../common/constants'; import { getFieldFormats } from '../../services'; import { RunTaskFn, RunTaskFnFactory } from '../../types'; import { decryptJobHeaders } from '../common'; @@ -19,7 +18,7 @@ export const runTaskFnFactory: RunTaskFnFactory> = ( const config = reporting.getConfig(); return async function runTask(jobId, job, cancellationToken, stream) { - const logger = parentLogger.clone([CSV_JOB_TYPE, 'execute-job', jobId]); + const logger = parentLogger.get(`execute-job:${jobId}`); const encryptionKey = config.get('encryptionKey'); const headers = await decryptJobHeaders(encryptionKey, job.headers, logger); diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts index c525cb7c0def2d..4755d153666e40 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.test.ts @@ -5,21 +5,22 @@ * 2.0. */ -import { Writable } from 'stream'; -import * as Rx from 'rxjs'; import { errors as esErrors } from '@elastic/elasticsearch'; +import type { IScopedClusterClient, IUiSettingsClient, SearchResponse } from 'kibana/server'; import { identity, range } from 'lodash'; -import { IScopedClusterClient, IUiSettingsClient, SearchResponse } from 'src/core/server'; +import * as Rx from 'rxjs'; import { elasticsearchServiceMock, + loggingSystemMock, savedObjectsClientMock, uiSettingsServiceMock, } from 'src/core/server/mocks'; import { ISearchStartSearchSource } from 'src/plugins/data/common'; -import { FieldFormatsRegistry } from 'src/plugins/field_formats/common'; import { searchSourceInstanceMock } from 'src/plugins/data/common/search/search_source/mocks'; import { IScopedSearchClient } from 'src/plugins/data/server'; import { dataPluginMock } from 'src/plugins/data/server/mocks'; +import { FieldFormatsRegistry } from 'src/plugins/field_formats/common'; +import { Writable } from 'stream'; import { ReportingConfig } from '../../../'; import { CancellationToken } from '../../../../common/cancellation_token'; import { @@ -28,11 +29,7 @@ import { UI_SETTINGS_DATEFORMAT_TZ, } from '../../../../common/constants'; import { UnknownError } from '../../../../common/errors'; -import { - createMockConfig, - createMockConfigSchema, - createMockLevelLogger, -} from '../../../test_helpers'; +import { createMockConfig, createMockConfigSchema } from '../../../test_helpers'; import { JobParamsCSV } from '../types'; import { CsvGenerator } from './generate_csv'; @@ -125,7 +122,7 @@ beforeEach(async () => { }); }); -const logger = createMockLevelLogger(); +const logger = loggingSystemMock.createLogger(); it('formats an empty search result to CSV content', async () => { const generateCsv = new CsvGenerator( diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts index 201484af9d7d0a..c913706f585624 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts @@ -5,9 +5,9 @@ * 2.0. */ -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { errors as esErrors } from '@elastic/elasticsearch'; -import type { IScopedClusterClient, IUiSettingsClient } from 'src/core/server'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { IScopedClusterClient, IUiSettingsClient, Logger } from 'kibana/server'; import type { IScopedSearchClient } from 'src/plugins/data/server'; import type { Datatable } from 'src/plugins/expressions/server'; import type { Writable } from 'stream'; @@ -32,16 +32,15 @@ import type { CancellationToken } from '../../../../common/cancellation_token'; import { CONTENT_TYPE_CSV } from '../../../../common/constants'; import { AuthenticationExpiredError, - UnknownError, ReportingError, + UnknownError, } from '../../../../common/errors'; import { byteSizeValueToNumber } from '../../../../common/schema_utils'; -import type { LevelLogger } from '../../../lib'; import type { TaskRunResult } from '../../../lib/tasks'; import type { JobParamsCSV } from '../types'; import { CsvExportSettings, getExportSettings } from './get_export_settings'; -import { MaxSizeStringBuilder } from './max_size_string_builder'; import { i18nTexts } from './i18n_texts'; +import { MaxSizeStringBuilder } from './max_size_string_builder'; interface Clients { es: IScopedClusterClient; @@ -65,7 +64,7 @@ export class CsvGenerator { private clients: Clients, private dependencies: Dependencies, private cancellationToken: CancellationToken, - private logger: LevelLogger, + private logger: Logger, private stream: Writable ) {} @@ -316,7 +315,7 @@ export class CsvGenerator { } if (!results) { - this.logger.warning(`Search results are undefined!`); + this.logger.warn(`Search results are undefined!`); break; } @@ -396,7 +395,7 @@ export class CsvGenerator { this.logger.debug(`Finished generating. Row count: ${this.csvRowCount}.`); if (!this.maxSizeReached && this.csvRowCount !== totalRecords) { - this.logger.warning( + this.logger.warn( `ES scroll returned fewer total hits than expected! ` + `Search result total hits: ${totalRecords}. Row count: ${this.csvRowCount}.` ); diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.test.ts index 2ae3e5e712d313..ef0f0062bf19b6 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.test.ts @@ -12,18 +12,18 @@ import { UI_SETTINGS_SEARCH_INCLUDE_FROZEN, } from '../../../../common/constants'; import { IUiSettingsClient } from 'kibana/server'; -import { savedObjectsClientMock, uiSettingsServiceMock } from 'src/core/server/mocks'; import { - createMockConfig, - createMockConfigSchema, - createMockLevelLogger, -} from '../../../test_helpers'; + loggingSystemMock, + savedObjectsClientMock, + uiSettingsServiceMock, +} from 'src/core/server/mocks'; +import { createMockConfig, createMockConfigSchema } from '../../../test_helpers'; import { getExportSettings } from './get_export_settings'; describe('getExportSettings', () => { let uiSettingsClient: IUiSettingsClient; const config = createMockConfig(createMockConfigSchema({})); - const logger = createMockLevelLogger(); + const logger = loggingSystemMock.createLogger(); beforeEach(() => { uiSettingsClient = uiSettingsServiceMock diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.ts index 5b69e33624c5c2..6a07e3184eb48f 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/get_export_settings.ts @@ -6,7 +6,7 @@ */ import { ByteSizeValue } from '@kbn/config-schema'; -import { IUiSettingsClient } from 'kibana/server'; +import type { IUiSettingsClient, Logger } from 'kibana/server'; import { createEscapeValue } from '../../../../../../../src/plugins/data/common'; import { ReportingConfig } from '../../../'; import { @@ -16,7 +16,6 @@ import { UI_SETTINGS_DATEFORMAT_TZ, UI_SETTINGS_SEARCH_INCLUDE_FROZEN, } from '../../../../common/constants'; -import { LevelLogger } from '../../../lib'; export interface CsvExportSettings { timezone: string; @@ -37,7 +36,7 @@ export const getExportSettings = async ( client: IUiSettingsClient, config: ReportingConfig, timezone: string | undefined, - logger: LevelLogger + logger: Logger ): Promise => { let setTimezone: string; if (timezone) { diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts index 53e1f6ba3c95bf..50ae2ab10f6e75 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource_immediate/execute_job.ts @@ -8,7 +8,6 @@ import { KibanaRequest } from 'src/core/server'; import { Writable } from 'stream'; import { CancellationToken } from '../../../common/cancellation_token'; -import { CSV_SEARCHSOURCE_IMMEDIATE_TYPE } from '../../../common/constants'; import { TaskRunResult } from '../../lib/tasks'; import { getFieldFormats } from '../../services'; import { ReportingRequestHandlerContext, RunTaskFnFactory } from '../../types'; @@ -32,7 +31,7 @@ export const runTaskFnFactory: RunTaskFnFactory = function e parentLogger ) { const config = reporting.getConfig(); - const logger = parentLogger.clone([CSV_SEARCHSOURCE_IMMEDIATE_TYPE, 'execute-job']); + const logger = parentLogger.get('execute-job'); return async function runTask(_jobId, immediateJobParams, context, stream, req) { const job = { @@ -82,7 +81,7 @@ export const runTaskFnFactory: RunTaskFnFactory = function e const { warnings } = result; if (warnings) { warnings.forEach((warning) => { - logger.warning(warning); + logger.warn(warning); }); } diff --git a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts index 9069ec63a88250..bc37978372ba63 100644 --- a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts +++ b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { Writable } from 'stream'; import * as Rx from 'rxjs'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { Writable } from 'stream'; import { ReportingCore } from '../../../'; import { CancellationToken } from '../../../../common/cancellation_token'; -import { cryptoFactory, LevelLogger } from '../../../lib'; +import { cryptoFactory } from '../../../lib'; import { createMockConfig, createMockConfigSchema, @@ -29,14 +30,7 @@ const cancellationToken = { on: jest.fn(), } as unknown as CancellationToken; -const mockLoggerFactory = { - get: jest.fn().mockImplementation(() => ({ - error: jest.fn(), - debug: jest.fn(), - warn: jest.fn(), - })), -}; -const getMockLogger = () => new LevelLogger(mockLoggerFactory); +const getMockLogger = () => loggingSystemMock.createLogger(); const mockEncryptionKey = 'abcabcsecuresecret'; const encryptHeaders = async (headers: Record) => { diff --git a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts index 67d013740bedd3..52023e53b80b56 100644 --- a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts @@ -8,7 +8,7 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { finalize, map, mergeMap, takeUntil, tap } from 'rxjs/operators'; -import { PNG_JOB_TYPE, REPORTING_TRANSACTION_TYPE } from '../../../../common/constants'; +import { REPORTING_TRANSACTION_TYPE } from '../../../../common/constants'; import { TaskRunResult } from '../../../lib/tasks'; import { RunTaskFn, RunTaskFnFactory } from '../../../types'; import { decryptJobHeaders, getFullUrls, generatePngObservable } from '../../common'; @@ -24,7 +24,7 @@ export const runTaskFnFactory: RunTaskFnFactory> = const apmGetAssets = apmTrans?.startSpan('get-assets', 'setup'); let apmGeneratePng: { end: () => void } | null | undefined; - const jobLogger = parentLogger.clone([PNG_JOB_TYPE, 'execute', jobId]); + const jobLogger = parentLogger.get(`execute:${jobId}`); const process$: Rx.Observable = Rx.of(1).pipe( mergeMap(() => decryptJobHeaders(encryptionKey, job.headers, jobLogger)), mergeMap((headers) => { diff --git a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts index 1b1ad6878d78fb..1403873e8da4b8 100644 --- a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts @@ -6,11 +6,12 @@ */ import * as Rx from 'rxjs'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { Writable } from 'stream'; import { ReportingCore } from '../../'; import { CancellationToken } from '../../../common/cancellation_token'; import { LocatorParams } from '../../../common/types'; -import { cryptoFactory, LevelLogger } from '../../lib'; +import { cryptoFactory } from '../../lib'; import { createMockConfig, createMockConfigSchema, @@ -30,14 +31,7 @@ const cancellationToken = { on: jest.fn(), } as unknown as CancellationToken; -const mockLoggerFactory = { - get: jest.fn().mockImplementation(() => ({ - error: jest.fn(), - debug: jest.fn(), - warn: jest.fn(), - })), -}; -const getMockLogger = () => new LevelLogger(mockLoggerFactory); +const getMockLogger = () => loggingSystemMock.createLogger(); const mockEncryptionKey = 'abcabcsecuresecret'; const encryptHeaders = async (headers: Record) => { diff --git a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts index 51044aa324a1aa..5df7a497adf6c7 100644 --- a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts @@ -8,7 +8,7 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { finalize, map, mergeMap, takeUntil, tap } from 'rxjs/operators'; -import { PNG_JOB_TYPE_V2, REPORTING_TRANSACTION_TYPE } from '../../../common/constants'; +import { REPORTING_TRANSACTION_TYPE } from '../../../common/constants'; import { TaskRunResult } from '../../lib/tasks'; import { RunTaskFn, RunTaskFnFactory } from '../../types'; import { decryptJobHeaders, generatePngObservable } from '../common'; @@ -25,7 +25,7 @@ export const runTaskFnFactory: RunTaskFnFactory> = const apmGetAssets = apmTrans?.startSpan('get-assets', 'setup'); let apmGeneratePng: { end: () => void } | null | undefined; - const jobLogger = parentLogger.clone([PNG_JOB_TYPE_V2, 'execute', jobId]); + const jobLogger = parentLogger.get(`execute:${jobId}`); const process$: Rx.Observable = Rx.of(1).pipe( mergeMap(() => decryptJobHeaders(encryptionKey, job.headers, jobLogger)), mergeMap((headers) => { diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts index a8d2027f2ba120..7faa13486b5a18 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts @@ -6,10 +6,11 @@ */ import * as Rx from 'rxjs'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { Writable } from 'stream'; import { ReportingCore } from '../../../'; import { CancellationToken } from '../../../../common/cancellation_token'; -import { cryptoFactory, LevelLogger } from '../../../lib'; +import { cryptoFactory } from '../../../lib'; import { createMockConfigSchema, createMockReportingCore } from '../../../test_helpers'; import { generatePdfObservable } from '../lib/generate_pdf'; import { TaskPayloadPDF } from '../types'; @@ -25,14 +26,7 @@ const cancellationToken = { on: jest.fn(), } as unknown as CancellationToken; -const mockLoggerFactory = { - get: jest.fn().mockImplementation(() => ({ - error: jest.fn(), - debug: jest.fn(), - warn: jest.fn(), - })), -}; -const getMockLogger = () => new LevelLogger(mockLoggerFactory); +const getMockLogger = () => loggingSystemMock.createLogger(); const mockEncryptionKey = 'testencryptionkey'; const encryptHeaders = async (headers: Record) => { diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts index ab3793935e1d86..9b4db48ed66970 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts @@ -8,7 +8,7 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil, tap } from 'rxjs/operators'; -import { PDF_JOB_TYPE, REPORTING_TRANSACTION_TYPE } from '../../../../common/constants'; +import { REPORTING_TRANSACTION_TYPE } from '../../../../common/constants'; import { TaskRunResult } from '../../../lib/tasks'; import { RunTaskFn, RunTaskFnFactory } from '../../../types'; import { decryptJobHeaders, getFullUrls, getCustomLogo } from '../../common'; @@ -21,7 +21,7 @@ export const runTaskFnFactory: RunTaskFnFactory> = const encryptionKey = config.get('encryptionKey'); return async function runTask(jobId, job, cancellationToken, stream) { - const jobLogger = parentLogger.clone([PDF_JOB_TYPE, 'execute-job', jobId]); + const jobLogger = parentLogger.get(`execute-job:${jobId}`); const apmTrans = apm.startTransaction('execute-job-pdf', REPORTING_TRANSACTION_TYPE); const apmGetAssets = apmTrans?.startSpan('get-assets', 'setup'); let apmGeneratePdf: { end: () => void } | null | undefined; 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 a401f59b8f4bf0..ff0ef2cf39af4b 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 @@ -5,13 +5,13 @@ * 2.0. */ +import type { Logger } from 'kibana/server'; import { groupBy } from 'lodash'; import * as Rx from 'rxjs'; import { mergeMap, tap } from 'rxjs/operators'; +import { ReportingCore } from '../../../'; import { ScreenshotResult } from '../../../../../screenshotting/server'; import type { PdfMetrics } from '../../../../common/types'; -import { ReportingCore } from '../../../'; -import { LevelLogger } from '../../../lib'; import { ScreenshotOptions } from '../../../types'; import { PdfMaker } from '../../common/pdf'; import { getTracker } from './tracker'; @@ -34,7 +34,7 @@ interface PdfResult { export function generatePdfObservable( reporting: ReportingCore, - logger: LevelLogger, + logger: Logger, title: string, options: ScreenshotOptions, logo?: string diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.test.ts index 3cf7f82058563a..efad71a64a81d6 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.test.ts @@ -8,11 +8,12 @@ jest.mock('./lib/generate_pdf'); import * as Rx from 'rxjs'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { Writable } from 'stream'; import { ReportingCore } from '../../'; import { CancellationToken } from '../../../common/cancellation_token'; import { LocatorParams } from '../../../common/types'; -import { cryptoFactory, LevelLogger } from '../../lib'; +import { cryptoFactory } from '../../lib'; import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import { runTaskFnFactory } from './execute_job'; import { generatePdfObservable } from './lib/generate_pdf'; @@ -26,14 +27,7 @@ const cancellationToken = { on: jest.fn(), } as unknown as CancellationToken; -const mockLoggerFactory = { - get: jest.fn().mockImplementation(() => ({ - error: jest.fn(), - debug: jest.fn(), - warn: jest.fn(), - })), -}; -const getMockLogger = () => new LevelLogger(mockLoggerFactory); +const getMockLogger = () => loggingSystemMock.createLogger(); const mockEncryptionKey = 'testencryptionkey'; const encryptHeaders = async (headers: Record) => { diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts index 85684bca66b869..7f887707829cb1 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts @@ -8,7 +8,7 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil, tap } from 'rxjs/operators'; -import { PDF_JOB_TYPE_V2, REPORTING_TRANSACTION_TYPE } from '../../../common/constants'; +import { REPORTING_TRANSACTION_TYPE } from '../../../common/constants'; import { TaskRunResult } from '../../lib/tasks'; import { RunTaskFn, RunTaskFnFactory } from '../../types'; import { decryptJobHeaders, getCustomLogo } from '../common'; @@ -21,7 +21,7 @@ export const runTaskFnFactory: RunTaskFnFactory> = const encryptionKey = config.get('encryptionKey'); return async function runTask(jobId, job, cancellationToken, stream) { - const jobLogger = parentLogger.clone([PDF_JOB_TYPE_V2, 'execute-job', jobId]); + const jobLogger = parentLogger.get(`execute-job:${jobId}`); const apmTrans = apm.startTransaction('execute-job-pdf-v2', REPORTING_TRANSACTION_TYPE); const apmGetAssets = apmTrans?.startSpan('get-assets', 'setup'); let apmGeneratePdf: { end: () => void } | null | undefined; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts index ac922c07574b3c..8bec3cac28f430 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts @@ -5,14 +5,14 @@ * 2.0. */ +import type { Logger } from 'kibana/server'; import { groupBy } from 'lodash'; import * as Rx from 'rxjs'; import { mergeMap, tap } from 'rxjs/operators'; -import { ReportingCore } from '../../../'; -import { ScreenshotResult } from '../../../../../screenshotting/server'; -import { LocatorParams, PdfMetrics, UrlOrUrlLocatorTuple } from '../../../../common/types'; -import { LevelLogger } from '../../../lib'; -import { ScreenshotOptions } from '../../../types'; +import type { ReportingCore } from '../../../'; +import type { ScreenshotResult } from '../../../../../screenshotting/server'; +import type { LocatorParams, PdfMetrics, UrlOrUrlLocatorTuple } from '../../../../common/types'; +import type { ScreenshotOptions } from '../../../types'; import { PdfMaker } from '../../common/pdf'; import { getFullRedirectAppUrl } from '../../common/v2/get_full_redirect_app_url'; import type { TaskPayloadPDFV2 } from '../types'; @@ -36,7 +36,7 @@ interface PdfResult { export function generatePdfObservable( reporting: ReportingCore, - logger: LevelLogger, + logger: Logger, job: TaskPayloadPDFV2, title: string, locatorParams: LocatorParams[], diff --git a/x-pack/plugins/reporting/server/lib/check_params_version.ts b/x-pack/plugins/reporting/server/lib/check_params_version.ts index 7298384b875715..79237ba56677a5 100644 --- a/x-pack/plugins/reporting/server/lib/check_params_version.ts +++ b/x-pack/plugins/reporting/server/lib/check_params_version.ts @@ -5,16 +5,16 @@ * 2.0. */ +import type { Logger } from 'kibana/server'; import { UNVERSIONED_VERSION } from '../../common/constants'; import type { BaseParams } from '../../common/types'; -import type { LevelLogger } from './'; -export function checkParamsVersion(jobParams: BaseParams, logger: LevelLogger) { +export function checkParamsVersion(jobParams: BaseParams, logger: Logger) { if (jobParams.version) { logger.debug(`Using reporting job params v${jobParams.version}`); return jobParams.version; } - logger.warning(`No version provided in report job params. Assuming ${UNVERSIONED_VERSION}`); + logger.warn(`No version provided in report job params. Assuming ${UNVERSIONED_VERSION}`); return UNVERSIONED_VERSION; } diff --git a/x-pack/plugins/reporting/server/lib/content_stream.test.ts b/x-pack/plugins/reporting/server/lib/content_stream.test.ts index 0c45ef2d5f5ce8..069ac22258ad10 100644 --- a/x-pack/plugins/reporting/server/lib/content_stream.test.ts +++ b/x-pack/plugins/reporting/server/lib/content_stream.test.ts @@ -5,20 +5,20 @@ * 2.0. */ +import type { Logger } from 'kibana/server'; import { set } from 'lodash'; -import { elasticsearchServiceMock } from 'src/core/server/mocks'; -import { createMockLevelLogger } from '../test_helpers'; +import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; import { ContentStream } from './content_stream'; describe('ContentStream', () => { let client: ReturnType; - let logger: ReturnType; + let logger: Logger; let stream: ContentStream; let base64Stream: ContentStream; beforeEach(() => { client = elasticsearchServiceMock.createClusterClient().asInternalUser; - logger = createMockLevelLogger(); + logger = loggingSystemMock.createLogger(); stream = new ContentStream( client, logger, diff --git a/x-pack/plugins/reporting/server/lib/content_stream.ts b/x-pack/plugins/reporting/server/lib/content_stream.ts index c0b2d458b4d594..b09e446ff576c3 100644 --- a/x-pack/plugins/reporting/server/lib/content_stream.ts +++ b/x-pack/plugins/reporting/server/lib/content_stream.ts @@ -5,14 +5,13 @@ * 2.0. */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { Duplex } from 'stream'; +import { ByteSizeValue } from '@kbn/config-schema'; +import type { ElasticsearchClient, Logger } from 'kibana/server'; import { defaults, get } from 'lodash'; import Puid from 'puid'; -import { ByteSizeValue } from '@kbn/config-schema'; -import type { ElasticsearchClient } from 'src/core/server'; -import { ReportingCore } from '..'; -import { ReportSource } from '../../common/types'; -import { LevelLogger } from './level_logger'; +import { Duplex } from 'stream'; +import type { ReportingCore } from '../'; +import type { ReportSource } from '../../common/types'; /** * @note The Elasticsearch `http.max_content_length` is including the whole POST body. @@ -87,7 +86,7 @@ export class ContentStream extends Duplex { constructor( private client: ElasticsearchClient, - private logger: LevelLogger, + private logger: Logger, private document: ContentStreamDocument, { encoding = 'base64' }: ContentStreamParameters = {} ) { @@ -348,7 +347,7 @@ export async function getContentStream( return new ContentStream( client, - logger.clone(['content_stream', document.id]), + logger.get('content_stream').get(document.id), document, parameters ); diff --git a/x-pack/plugins/reporting/server/lib/event_logger/adapter.test.ts b/x-pack/plugins/reporting/server/lib/event_logger/adapter.test.ts index aef569a49e357d..90c546b198a08b 100644 --- a/x-pack/plugins/reporting/server/lib/event_logger/adapter.test.ts +++ b/x-pack/plugins/reporting/server/lib/event_logger/adapter.test.ts @@ -6,11 +6,11 @@ */ import { LogMeta } from 'kibana/server'; -import { createMockLevelLogger } from '../../test_helpers'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { EcsLogAdapter } from './adapter'; describe('EcsLogAdapter', () => { - const logger = createMockLevelLogger(); + const logger = loggingSystemMock.createLogger(); beforeAll(() => { jest .spyOn(global.Date, 'now') @@ -28,7 +28,7 @@ describe('EcsLogAdapter', () => { const event = { kibana: { reporting: { wins: 5000 } } } as object & LogMeta; // an object that extends LogMeta eventLogger.logEvent('hello world', event); - expect(logger.debug).toBeCalledWith('hello world', ['events'], { + expect(logger.debug).toBeCalledWith('hello world', { event: { duration: undefined, end: undefined, @@ -50,7 +50,7 @@ describe('EcsLogAdapter', () => { const event = { kibana: { reporting: { wins: 9000 } } } as object & LogMeta; // an object that extends LogMeta eventLogger.logEvent('hello duration', event); - expect(logger.debug).toBeCalledWith('hello duration', ['events'], { + expect(logger.debug).toBeCalledWith('hello duration', { event: { duration: 120000000000, end: '2021-04-12T16:02:00.000Z', diff --git a/x-pack/plugins/reporting/server/lib/event_logger/adapter.ts b/x-pack/plugins/reporting/server/lib/event_logger/adapter.ts index c9487a79d9e70e..71116d8f334b50 100644 --- a/x-pack/plugins/reporting/server/lib/event_logger/adapter.ts +++ b/x-pack/plugins/reporting/server/lib/event_logger/adapter.ts @@ -6,23 +6,26 @@ */ import deepMerge from 'deepmerge'; -import { LogMeta } from 'src/core/server'; -import { LevelLogger } from '../level_logger'; -import { IReportingEventLogger } from './logger'; +import type { Logger, LogMeta } from 'kibana/server'; +import type { IReportingEventLogger } from './logger'; /** @internal */ export class EcsLogAdapter implements IReportingEventLogger { start?: Date; end?: Date; + private logger: Logger; + /** * This class provides a logging system to Reporting code, using a shape similar to the EventLog service. * The logging action causes ECS data with Reporting metrics sent to DEBUG logs. * - * @param {LevelLogger} logger - Reporting's wrapper of the core logger + * @param {Logger} logger - Reporting's wrapper of the core logger * @param {Partial} properties - initial ECS data with template for Reporting metrics */ - constructor(private logger: LevelLogger, private properties: Partial) {} + constructor(logger: Logger, private properties: Partial) { + this.logger = logger.get('events'); + } logEvent(message: string, properties: LogMeta) { if (this.start && !this.end) { @@ -44,7 +47,7 @@ export class EcsLogAdapter implements IReportingEventLogger { }); // sends an ECS object with Reporting metrics to the DEBUG logs - this.logger.debug(message, ['events'], deepMerge(newProperties, properties)); + this.logger.debug(message, deepMerge(newProperties, properties)); } startTiming() { diff --git a/x-pack/plugins/reporting/server/lib/event_logger/logger.test.ts b/x-pack/plugins/reporting/server/lib/event_logger/logger.test.ts index fa45a8d04176c8..c58777747c3fd3 100644 --- a/x-pack/plugins/reporting/server/lib/event_logger/logger.test.ts +++ b/x-pack/plugins/reporting/server/lib/event_logger/logger.test.ts @@ -5,8 +5,8 @@ * 2.0. */ +import { loggingSystemMock } from 'src/core/server/mocks'; import { ConcreteTaskInstance } from '../../../../task_manager/server'; -import { createMockLevelLogger } from '../../test_helpers'; import { BasePayload } from '../../types'; import { Report } from '../store'; import { ReportingEventLogger, reportingEventLoggerFactory } from './logger'; @@ -21,7 +21,7 @@ describe('Event Logger', () => { let factory: ReportingEventLogger; beforeEach(() => { - factory = reportingEventLoggerFactory(createMockLevelLogger()); + factory = reportingEventLoggerFactory(loggingSystemMock.createLogger()); }); it(`should construct with an internal seed object`, () => { diff --git a/x-pack/plugins/reporting/server/lib/event_logger/logger.ts b/x-pack/plugins/reporting/server/lib/event_logger/logger.ts index 6a7feea0c335d0..965a55e24229a2 100644 --- a/x-pack/plugins/reporting/server/lib/event_logger/logger.ts +++ b/x-pack/plugins/reporting/server/lib/event_logger/logger.ts @@ -6,8 +6,7 @@ */ import deepMerge from 'deepmerge'; -import { LogMeta } from 'src/core/server'; -import { LevelLogger } from '../'; +import type { Logger, LogMeta } from 'kibana/server'; import { PLUGIN_ID } from '../../../common/constants'; import type { TaskRunMetrics } from '../../../common/types'; import { IReport } from '../store'; @@ -46,7 +45,7 @@ export interface BaseEvent { } /** @internal */ -export function reportingEventLoggerFactory(logger: LevelLogger) { +export function reportingEventLoggerFactory(logger: Logger) { const genericLogger = new EcsLogAdapter(logger, { event: { provider: PLUGIN_ID } }); return class ReportingEventLogger { diff --git a/x-pack/plugins/reporting/server/lib/index.ts b/x-pack/plugins/reporting/server/lib/index.ts index 682f547380ba0d..36d310fcd131b1 100644 --- a/x-pack/plugins/reporting/server/lib/index.ts +++ b/x-pack/plugins/reporting/server/lib/index.ts @@ -10,7 +10,6 @@ export { checkParamsVersion } from './check_params_version'; export { ContentStream, getContentStream } from './content_stream'; export { cryptoFactory } from './crypto'; export { ExportTypesRegistry, getExportTypesRegistry } from './export_types_registry'; -export { LevelLogger } from './level_logger'; export { PassThroughStream } from './passthrough_stream'; export { statuses } from './statuses'; export { ReportingStore, IlmPolicyManager } from './store'; diff --git a/x-pack/plugins/reporting/server/lib/level_logger.ts b/x-pack/plugins/reporting/server/lib/level_logger.ts deleted file mode 100644 index 91cf6757dbee22..00000000000000 --- a/x-pack/plugins/reporting/server/lib/level_logger.ts +++ /dev/null @@ -1,65 +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 { LoggerFactory, LogMeta } from 'src/core/server'; - -const trimStr = (toTrim: string) => { - return typeof toTrim === 'string' ? toTrim.trim() : toTrim; -}; - -export interface GenericLevelLogger { - debug: (msg: string, tags: string[], meta: T) => void; - info: (msg: string) => void; - warning: (msg: string) => void; - error: (msg: Error) => void; -} - -export class LevelLogger implements GenericLevelLogger { - private _logger: LoggerFactory; - private _tags: string[]; - public warning: (msg: string, tags?: string[]) => void; - - constructor(logger: LoggerFactory, tags?: string[]) { - this._logger = logger; - this._tags = tags || []; - - /* - * This shortcut provides maintenance convenience: Reporting code has been - * using both .warn and .warning - */ - this.warning = this.warn.bind(this); - } - - private getLogger(tags: string[]) { - return this._logger.get(...this._tags, ...tags); - } - - public error(err: string | Error, tags: string[] = []) { - this.getLogger(tags).error(err); - } - - public warn(msg: string, tags: string[] = []) { - this.getLogger(tags).warn(msg); - } - - // only "debug" logging supports the LogMeta for now... - public debug(msg: string, tags: string[] = [], meta?: T) { - this.getLogger(tags).debug(msg, meta); - } - - public trace(msg: string, tags: string[] = []) { - this.getLogger(tags).trace(msg); - } - - public info(msg: string, tags: string[] = []) { - this.getLogger(tags).info(trimStr(msg)); - } - - public clone(tags: string[]) { - return new LevelLogger(this._logger, [...this._tags, ...tags]); - } -} diff --git a/x-pack/plugins/reporting/server/lib/store/store.test.ts b/x-pack/plugins/reporting/server/lib/store/store.test.ts index 3e8942be1ffa0b..7ceafef261dd49 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.test.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.test.ts @@ -5,17 +5,13 @@ * 2.0. */ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { elasticsearchServiceMock } from 'src/core/server/mocks'; +import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; import { ReportingCore } from '../../'; -import { - createMockConfigSchema, - createMockLevelLogger, - createMockReportingCore, -} from '../../test_helpers'; +import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import { Report, ReportDocument, ReportingStore, SavedReport } from './'; describe('ReportingStore', () => { - const mockLogger = createMockLevelLogger(); + const mockLogger = loggingSystemMock.createLogger(); let mockCore: ReportingCore; let mockEsClient: ReturnType; diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts index 41fdd9580c996c..7e920e718d51ee 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.ts @@ -6,13 +6,14 @@ */ import { IndexResponse, UpdateResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { ElasticsearchClient } from 'src/core/server'; -import { LevelLogger, statuses } from '../'; -import { ReportingCore } from '../../'; +import type { ElasticsearchClient, Logger } from 'kibana/server'; +import { statuses } from '../'; +import type { ReportingCore } from '../../'; import { ILM_POLICY_NAME, REPORTING_SYSTEM_INDEX } from '../../../common/constants'; -import { JobStatus, ReportOutput, ReportSource } from '../../../common/types'; -import { ReportTaskParams } from '../tasks'; -import { IReport, Report, ReportDocument, SavedReport } from './'; +import type { JobStatus, ReportOutput, ReportSource } from '../../../common/types'; +import type { ReportTaskParams } from '../tasks'; +import type { IReport, Report, ReportDocument } from './'; +import { SavedReport } from './'; import { IlmPolicyManager } from './ilm_policy_manager'; import { indexTimestamp } from './index_timestamp'; import { mapping } from './mapping'; @@ -83,12 +84,12 @@ export class ReportingStore { private client?: ElasticsearchClient; private ilmPolicyManager?: IlmPolicyManager; - constructor(private reportingCore: ReportingCore, private logger: LevelLogger) { + constructor(private reportingCore: ReportingCore, private logger: Logger) { const config = reportingCore.getConfig(); this.indexPrefix = REPORTING_SYSTEM_INDEX; this.indexInterval = config.get('queue', 'indexInterval'); - this.logger = logger.clone(['store']); + this.logger = logger.get('store'); } private async getClient() { diff --git a/x-pack/plugins/reporting/server/lib/tasks/error_logger.test.ts b/x-pack/plugins/reporting/server/lib/tasks/error_logger.test.ts index 607c9c32538be4..302088e6a6eb13 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/error_logger.test.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/error_logger.test.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { createMockLevelLogger } from '../../test_helpers'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { errorLogger } from './error_logger'; -const logger = createMockLevelLogger(); +const logger = loggingSystemMock.createLogger(); describe('Execute Report Error Logger', () => { const errorLogSpy = jest.spyOn(logger, 'error'); diff --git a/x-pack/plugins/reporting/server/lib/tasks/error_logger.ts b/x-pack/plugins/reporting/server/lib/tasks/error_logger.ts index b4d4028230666a..a67e3caeb2c78c 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/error_logger.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/error_logger.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { LevelLogger } from '..'; +import type { Logger } from 'kibana/server'; const MAX_PARTIAL_ERROR_LENGTH = 1000; // 1000 of beginning, 1000 of end const ERROR_PARTIAL_SEPARATOR = '...'; @@ -15,7 +15,7 @@ const MAX_ERROR_LENGTH = MAX_PARTIAL_ERROR_LENGTH * 2 + ERROR_PARTIAL_SEPARATOR. * An error message string could be very long, as it sometimes includes huge * amount of base64 */ -export const errorLogger = (logger: LevelLogger, message: string, err?: Error) => { +export const errorLogger = (logger: Logger, message: string, err?: Error) => { if (err) { const errString = `${message}: ${err}`; const errLength = errString.length; diff --git a/x-pack/plugins/reporting/server/lib/tasks/execute_report.test.ts b/x-pack/plugins/reporting/server/lib/tasks/execute_report.test.ts index df662d963d0edf..b47df99b7a0fde 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/execute_report.test.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/execute_report.test.ts @@ -5,18 +5,15 @@ * 2.0. */ +import { loggingSystemMock } from 'src/core/server/mocks'; import { ReportingCore } from '../..'; import { RunContext } from '../../../../task_manager/server'; import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { ReportingConfigType } from '../../config'; -import { - createMockConfigSchema, - createMockLevelLogger, - createMockReportingCore, -} from '../../test_helpers'; +import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import { ExecuteReportTask } from './'; -const logger = createMockLevelLogger(); +const logger = loggingSystemMock.createLogger(); describe('Execute Report Task', () => { let mockReporting: ReportingCore; diff --git a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts index 449f3b8da7671a..4d4959eef00c44 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts @@ -6,20 +6,21 @@ */ import { UpdateResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { Logger } from 'kibana/server'; import moment from 'moment'; import * as Rx from 'rxjs'; import { timeout } from 'rxjs/operators'; import { finished, Writable } from 'stream'; import { promisify } from 'util'; -import { getContentStream, LevelLogger } from '../'; -import { ReportingCore } from '../../'; -import { +import { getContentStream } from '../'; +import type { ReportingCore } from '../../'; +import type { RunContext, TaskManagerStartContract, TaskRunCreatorFunction, } from '../../../../task_manager/server'; import { CancellationToken } from '../../../common/cancellation_token'; -import { ReportingError, UnknownError, QueueTimeoutError } from '../../../common/errors'; +import { QueueTimeoutError, ReportingError, UnknownError } from '../../../common/errors'; import { durationToNumber, numberToDuration } from '../../../common/schema_utils'; import type { ReportOutput } from '../../../common/types'; import type { ReportingConfigType } from '../../config'; @@ -60,7 +61,7 @@ function reportFromTask(task: ReportTaskParams) { export class ExecuteReportTask implements ReportingTask { public TYPE = REPORTING_EXECUTE_TYPE; - private logger: LevelLogger; + private logger: Logger; private taskManagerStart?: TaskManagerStartContract; private taskExecutors?: Map; private kibanaId?: string; @@ -70,9 +71,9 @@ export class ExecuteReportTask implements ReportingTask { constructor( private reporting: ReportingCore, private config: ReportingConfigType, - logger: LevelLogger + logger: Logger ) { - this.logger = logger.clone(['runTask']); + this.logger = logger.get('runTask'); } /* @@ -86,7 +87,7 @@ export class ExecuteReportTask implements ReportingTask { const exportTypesRegistry = reporting.getExportTypesRegistry(); const executors = new Map(); for (const exportType of exportTypesRegistry.getAll()) { - const exportTypeLogger = this.logger.clone([exportType.id]); + const exportTypeLogger = this.logger.get(exportType.jobType); const jobExecutor = exportType.runTaskFnFactory(reporting, exportTypeLogger); // The task will run the function with the job type as a param. // This allows us to retrieve the specific export type runFn when called to run an export @@ -476,7 +477,7 @@ export class ExecuteReportTask implements ReportingTask { return await this.getTaskManagerStart().schedule(taskInstance); } - private async rescheduleTask(task: ReportTaskParams, logger: LevelLogger) { + private async rescheduleTask(task: ReportTaskParams, logger: Logger) { logger.info(`Rescheduling task:${task.id} to retry after error.`); const oldTaskInstance: ReportingExecuteTaskInstance = { diff --git a/x-pack/plugins/reporting/server/lib/tasks/monitor_report.test.ts b/x-pack/plugins/reporting/server/lib/tasks/monitor_report.test.ts index d737c7032855b9..b7e75de2475358 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/monitor_report.test.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/monitor_report.test.ts @@ -5,18 +5,15 @@ * 2.0. */ +import { loggingSystemMock } from 'src/core/server/mocks'; import { ReportingCore } from '../..'; import { RunContext } from '../../../../task_manager/server'; import { taskManagerMock } from '../../../../task_manager/server/mocks'; import { ReportingConfigType } from '../../config'; -import { - createMockConfigSchema, - createMockLevelLogger, - createMockReportingCore, -} from '../../test_helpers'; +import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import { MonitorReportsTask } from './'; -const logger = createMockLevelLogger(); +const logger = loggingSystemMock.createLogger(); describe('Execute Report Task', () => { let mockReporting: ReportingCore; diff --git a/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts b/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts index 4af28e3d1a6981..1d406d7a5cc623 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/monitor_reports.ts @@ -5,8 +5,9 @@ * 2.0. */ +import type { Logger } from 'kibana/server'; import moment from 'moment'; -import { LevelLogger, ReportingStore } from '../'; +import { ReportingStore } from '../'; import { ReportingCore } from '../../'; import { TaskManagerStartContract, TaskRunCreatorFunction } from '../../../../task_manager/server'; import { numberToDuration } from '../../../common/schema_utils'; @@ -38,7 +39,7 @@ import { ReportingTask, ReportingTaskStatus, REPORTING_MONITOR_TYPE, ReportTaskP export class MonitorReportsTask implements ReportingTask { public TYPE = REPORTING_MONITOR_TYPE; - private logger: LevelLogger; + private logger: Logger; private taskManagerStart?: TaskManagerStartContract; private store?: ReportingStore; private timeout: moment.Duration; @@ -46,9 +47,9 @@ export class MonitorReportsTask implements ReportingTask { constructor( private reporting: ReportingCore, private config: ReportingConfigType, - parentLogger: LevelLogger + parentLogger: Logger ) { - this.logger = parentLogger.clone([REPORTING_MONITOR_TYPE]); + this.logger = parentLogger.get(REPORTING_MONITOR_TYPE); this.timeout = numberToDuration(config.queue.timeout); } @@ -145,7 +146,7 @@ export class MonitorReportsTask implements ReportingTask { } // reschedule the task with TM - private async rescheduleTask(task: ReportTaskParams, logger: LevelLogger) { + private async rescheduleTask(task: ReportTaskParams, logger: Logger) { if (!this.taskManagerStart) { throw new Error('Reporting task runner has not been initialized!'); } diff --git a/x-pack/plugins/reporting/server/plugin.test.ts b/x-pack/plugins/reporting/server/plugin.test.ts index e179d847d95260..98f02668323b1a 100644 --- a/x-pack/plugins/reporting/server/plugin.test.ts +++ b/x-pack/plugins/reporting/server/plugin.test.ts @@ -5,14 +5,12 @@ * 2.0. */ -import type { CoreSetup, CoreStart } from 'kibana/server'; -import { coreMock } from 'src/core/server/mocks'; +import type { CoreSetup, CoreStart, Logger } from 'kibana/server'; +import { coreMock, loggingSystemMock } from 'src/core/server/mocks'; import type { ReportingCore, ReportingInternalStart } from './core'; -import { LevelLogger } from './lib'; import { ReportingPlugin } from './plugin'; import { createMockConfigSchema, - createMockLevelLogger, createMockPluginSetup, createMockPluginStart, } from './test_helpers'; @@ -27,7 +25,7 @@ describe('Reporting Plugin', () => { let coreStart: CoreStart; let pluginSetup: ReportingSetupDeps; let pluginStart: ReportingInternalStart; - let logger: jest.Mocked; + let logger: jest.Mocked; let plugin: ReportingPlugin; beforeEach(async () => { @@ -38,9 +36,9 @@ describe('Reporting Plugin', () => { pluginSetup = createMockPluginSetup({}) as unknown as ReportingSetupDeps; pluginStart = await createMockPluginStart(coreStart, configSchema); - logger = createMockLevelLogger(); + logger = loggingSystemMock.createLogger(); plugin = new ReportingPlugin(initContext); - (plugin as unknown as { logger: LevelLogger }).logger = logger; + (plugin as unknown as { logger: Logger }).logger = logger; }); it('has a sync setup process', () => { diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index a0d4bfed7c7e0a..37d6494f5e079b 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -5,12 +5,12 @@ * 2.0. */ -import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server'; +import type { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; import { PLUGIN_ID } from '../common/constants'; import { ReportingCore } from './'; import { buildConfig, registerUiSettings, ReportingConfigType } from './config'; import { registerDeprecations } from './deprecations'; -import { LevelLogger, ReportingStore } from './lib'; +import { ReportingStore } from './lib'; import { registerRoutes } from './routes'; import { setFieldFormats } from './services'; import type { @@ -28,11 +28,11 @@ import { registerReportingUsageCollector } from './usage'; export class ReportingPlugin implements Plugin { - private logger: LevelLogger; + private logger: Logger; private reportingCore?: ReportingCore; constructor(private initContext: PluginInitializerContext) { - this.logger = new LevelLogger(initContext.logger.get()); + this.logger = initContext.logger.get(); } public setup(core: CoreSetup, plugins: ReportingSetupDeps) { diff --git a/x-pack/plugins/reporting/server/routes/deprecations/deprecations.ts b/x-pack/plugins/reporting/server/routes/deprecations/deprecations.ts index 4c368337cd4822..89d55ff04ab8fd 100644 --- a/x-pack/plugins/reporting/server/routes/deprecations/deprecations.ts +++ b/x-pack/plugins/reporting/server/routes/deprecations/deprecations.ts @@ -5,16 +5,16 @@ * 2.0. */ import { errors } from '@elastic/elasticsearch'; -import { SecurityHasPrivilegesIndexPrivilegesCheck } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { RequestHandler } from 'src/core/server'; +import type { SecurityHasPrivilegesIndexPrivilegesCheck } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { Logger, RequestHandler } from 'kibana/server'; import { API_GET_ILM_POLICY_STATUS, API_MIGRATE_ILM_POLICY_URL, ILM_POLICY_NAME, } from '../../../common/constants'; -import { IlmPolicyStatusResponse } from '../../../common/types'; -import { ReportingCore } from '../../core'; -import { IlmPolicyManager, LevelLogger as Logger } from '../../lib'; +import type { IlmPolicyStatusResponse } from '../../../common/types'; +import type { ReportingCore } from '../../core'; +import { IlmPolicyManager } from '../../lib'; import { deprecations } from '../../lib/deprecations'; export const registerDeprecationsRoutes = (reporting: ReportingCore, logger: Logger) => { diff --git a/x-pack/plugins/reporting/server/routes/deprecations/integration_tests/deprecations.test.ts b/x-pack/plugins/reporting/server/routes/deprecations/integration_tests/deprecations.test.ts index 67d7d0c4a0c080..9c76aade058f0e 100644 --- a/x-pack/plugins/reporting/server/routes/deprecations/integration_tests/deprecations.test.ts +++ b/x-pack/plugins/reporting/server/routes/deprecations/integration_tests/deprecations.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { loggingSystemMock } from 'src/core/server/mocks'; import { setupServer } from 'src/core/server/test_utils'; import supertest from 'supertest'; import { licensingMock } from '../../../../../licensing/server/mocks'; @@ -12,7 +13,6 @@ import { securityMock } from '../../../../../security/server/mocks'; import { API_GET_ILM_POLICY_STATUS } from '../../../../common/constants'; import { createMockConfigSchema, - createMockLevelLogger, createMockPluginSetup, createMockPluginStart, createMockReportingCore, @@ -54,7 +54,7 @@ describe(`GET ${API_GET_ILM_POLICY_STATUS}`, () => { it('correctly handles authz when security is unavailable', async () => { const core = await createReportingCore({}); - registerDeprecationsRoutes(core, createMockLevelLogger()); + registerDeprecationsRoutes(core, loggingSystemMock.createLogger()); await server.start(); await supertest(httpSetup.server.listener) @@ -68,7 +68,7 @@ describe(`GET ${API_GET_ILM_POLICY_STATUS}`, () => { security.license.isEnabled.mockReturnValue(false); const core = await createReportingCore({ security }); - registerDeprecationsRoutes(core, createMockLevelLogger()); + registerDeprecationsRoutes(core, loggingSystemMock.createLogger()); await server.start(); await supertest(httpSetup.server.listener) diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts b/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts index f68df294b41183..fb95ad9e318804 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts @@ -6,11 +6,11 @@ */ import { i18n } from '@kbn/i18n'; -import { ReportingCore } from '../..'; +import type { Logger } from 'kibana/server'; +import type { ReportingCore } from '../..'; import { API_DIAGNOSE_URL } from '../../../common/constants'; -import { LevelLogger as Logger } from '../../lib'; import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing'; -import { DiagnosticResponse } from './'; +import type { DiagnosticResponse } from './'; const logsToHelpMap = { 'error while loading shared libraries': i18n.translate( diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/index.ts b/x-pack/plugins/reporting/server/routes/diagnostic/index.ts index 92404b76e07418..b5e2a8585afb32 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/index.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/index.ts @@ -5,10 +5,10 @@ * 2.0. */ +import type { Logger } from 'kibana/server'; +import type { ReportingCore } from '../../core'; import { registerDiagnoseBrowser } from './browser'; import { registerDiagnoseScreenshot } from './screenshot'; -import { LevelLogger as Logger } from '../../lib'; -import { ReportingCore } from '../../core'; export const registerDiagnosticRoutes = (reporting: ReportingCore, logger: Logger) => { registerDiagnoseBrowser(reporting, logger); diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/browser.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/browser.test.ts index 911807e63a9d56..dc8fdb7e6d0c87 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/browser.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/browser.test.ts @@ -6,13 +6,13 @@ */ import * as Rx from 'rxjs'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { setupServer } from 'src/core/server/test_utils'; import supertest from 'supertest'; import { ReportingCore } from '../../../'; import type { ScreenshottingStart } from '../../../../../screenshotting/server'; import { createMockConfigSchema, - createMockLevelLogger, createMockPluginSetup, createMockReportingCore, } from '../../../test_helpers'; @@ -27,7 +27,7 @@ const fontNotFoundMessage = 'Could not find the default font'; describe('POST /diagnose/browser', () => { jest.setTimeout(6000); const reportingSymbol = Symbol('reporting'); - const mockLogger = createMockLevelLogger(); + const mockLogger = loggingSystemMock.createLogger(); let server: SetupServerReturn['server']; let httpSetup: SetupServerReturn['httpSetup']; diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/screenshot.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/screenshot.test.ts index ad90679e67adb8..3bc3f5bbb5e287 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/screenshot.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/integration_tests/screenshot.test.ts @@ -5,13 +5,13 @@ * 2.0. */ +import { loggingSystemMock } from 'src/core/server/mocks'; import { setupServer } from 'src/core/server/test_utils'; import supertest from 'supertest'; import { ReportingCore } from '../../../'; import { generatePngObservable } from '../../../export_types/common'; import { createMockConfigSchema, - createMockLevelLogger, createMockPluginSetup, createMockReportingCore, } from '../../../test_helpers'; @@ -38,7 +38,7 @@ describe('POST /diagnose/screenshot', () => { }; const config = createMockConfigSchema({ queue: { timeout: 120000 } }); - const mockLogger = createMockLevelLogger(); + const mockLogger = loggingSystemMock.createLogger(); beforeEach(async () => { ({ server, httpSetup } = await setupServer(reportingSymbol)); diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts index 90b4c9d9a30c6f..6819970fe753a9 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts @@ -6,12 +6,12 @@ */ import { i18n } from '@kbn/i18n'; +import type { Logger } from 'kibana/server'; import { ReportingCore } from '../..'; import { APP_WRAPPER_CLASS } from '../../../../../../src/core/server'; import { API_DIAGNOSE_URL } from '../../../common/constants'; import { generatePngObservable } from '../../export_types/common'; import { getAbsoluteUrlFactory } from '../../export_types/common/get_absolute_url'; -import { LevelLogger as Logger } from '../../lib'; import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing'; import { DiagnosticResponse } from './'; diff --git a/x-pack/plugins/reporting/server/routes/generate/csv_searchsource_immediate.ts b/x-pack/plugins/reporting/server/routes/generate/csv_searchsource_immediate.ts index b6ada00ba55ab7..19687b9d3ec9b8 100644 --- a/x-pack/plugins/reporting/server/routes/generate/csv_searchsource_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/generate/csv_searchsource_immediate.ts @@ -6,13 +6,13 @@ */ import { schema } from '@kbn/config-schema'; -import { KibanaRequest } from 'src/core/server'; -import { ReportingCore } from '../../'; +import type { KibanaRequest, Logger } from 'kibana/server'; +import type { ReportingCore } from '../../'; import { CSV_SEARCHSOURCE_IMMEDIATE_TYPE } from '../../../common/constants'; import { runTaskFnFactory } from '../../export_types/csv_searchsource_immediate/execute_job'; -import { JobParamsDownloadCSV } from '../../export_types/csv_searchsource_immediate/types'; -import { LevelLogger as Logger, PassThroughStream } from '../../lib'; -import { BaseParams } from '../../types'; +import type { JobParamsDownloadCSV } from '../../export_types/csv_searchsource_immediate/types'; +import { PassThroughStream } from '../../lib'; +import type { BaseParams } from '../../types'; import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing'; import { RequestHandler } from '../lib/request_handler'; @@ -64,7 +64,7 @@ export function registerGenerateCsvFromSavedObjectImmediate( authorizedUserPreRouting( reporting, async (user, context, req: CsvFromSavedObjectRequest, res) => { - const logger = parentLogger.clone([CSV_SEARCHSOURCE_IMMEDIATE_TYPE]); + const logger = parentLogger.get(CSV_SEARCHSOURCE_IMMEDIATE_TYPE); const runTaskFn = runTaskFnFactory(reporting, logger); const requestHandler = new RequestHandler(reporting, user, context, req, res, logger); const stream = new PassThroughStream(); diff --git a/x-pack/plugins/reporting/server/routes/generate/generate_from_jobparams.ts b/x-pack/plugins/reporting/server/routes/generate/generate_from_jobparams.ts index cfcb7d6d2b05c6..c5e7bb2197d722 100644 --- a/x-pack/plugins/reporting/server/routes/generate/generate_from_jobparams.ts +++ b/x-pack/plugins/reporting/server/routes/generate/generate_from_jobparams.ts @@ -7,16 +7,16 @@ import { schema } from '@kbn/config-schema'; import rison from 'rison-node'; -import { ReportingCore } from '../..'; +import type { Logger } from 'kibana/server'; +import type { ReportingCore } from '../..'; import { API_BASE_URL } from '../../../common/constants'; -import { LevelLogger } from '../../lib'; -import { BaseParams } from '../../types'; +import type { BaseParams } from '../../types'; import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing'; import { RequestHandler } from '../lib/request_handler'; const BASE_GENERATE = `${API_BASE_URL}/generate`; -export function registerJobGenerationRoutes(reporting: ReportingCore, logger: LevelLogger) { +export function registerJobGenerationRoutes(reporting: ReportingCore, logger: Logger) { const setupDeps = reporting.getPluginSetupDeps(); const { router } = setupDeps; diff --git a/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts b/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts index f6db9e92086eb2..f0db06485cf448 100644 --- a/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts +++ b/x-pack/plugins/reporting/server/routes/generate/integration_tests/generation_from_jobparams.test.ts @@ -7,6 +7,7 @@ import rison from 'rison-node'; import { BehaviorSubject } from 'rxjs'; +import { loggingSystemMock } from 'src/core/server/mocks'; import { setupServer } from 'src/core/server/test_utils'; import supertest from 'supertest'; import { ReportingCore } from '../../../'; @@ -16,7 +17,6 @@ import { ExportTypesRegistry } from '../../../lib/export_types_registry'; import { Report } from '../../../lib/store'; import { createMockConfigSchema, - createMockLevelLogger, createMockPluginSetup, createMockPluginStart, createMockReportingCore, @@ -38,7 +38,7 @@ describe('POST /api/reporting/generate', () => { queue: { indexInterval: 'year', timeout: 10000, pollEnabled: true }, }); - const mockLogger = createMockLevelLogger(); + const mockLogger = loggingSystemMock.createLogger(); beforeEach(async () => { ({ server, httpSetup } = await setupServer(reportingSymbol)); diff --git a/x-pack/plugins/reporting/server/routes/index.ts b/x-pack/plugins/reporting/server/routes/index.ts index 49f602062b0c16..0cc0d1bdc67967 100644 --- a/x-pack/plugins/reporting/server/routes/index.ts +++ b/x-pack/plugins/reporting/server/routes/index.ts @@ -5,8 +5,8 @@ * 2.0. */ +import type { Logger } from 'kibana/server'; import { ReportingCore } from '..'; -import { LevelLogger } from '../lib'; import { registerDeprecationsRoutes } from './deprecations/deprecations'; import { registerDiagnosticRoutes } from './diagnostic'; import { @@ -15,7 +15,7 @@ import { } from './generate'; import { registerJobInfoRoutes } from './management'; -export function registerRoutes(reporting: ReportingCore, logger: LevelLogger) { +export function registerRoutes(reporting: ReportingCore, logger: Logger) { registerDeprecationsRoutes(reporting, logger); registerDiagnosticRoutes(reporting, logger); registerGenerateCsvFromSavedObjectImmediate(reporting, logger); diff --git a/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts b/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts index 7f4d85ff141560..27126baad021d6 100644 --- a/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts +++ b/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts @@ -138,7 +138,7 @@ export function jobsQueryFactory(reportingCore: ReportingCore): JobsQueryFactory async get(user, id) { const { logger } = reportingCore.getPluginSetupDeps(); if (!id) { - logger.warning(`No ID provided for GET`); + logger.warn(`No ID provided for GET`); return; } @@ -163,7 +163,7 @@ export function jobsQueryFactory(reportingCore: ReportingCore): JobsQueryFactory const result = response?.hits?.hits?.[0]; if (!result?._source) { - logger.warning(`No hits resulted in search`); + logger.warn(`No hits resulted in search`); return; } diff --git a/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts b/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts index d1c1dddb3c3021..c97ec3285839df 100644 --- a/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts +++ b/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts @@ -6,16 +6,12 @@ */ import { KibanaRequest, KibanaResponseFactory } from 'kibana/server'; -import { coreMock, httpServerMock } from 'src/core/server/mocks'; +import { coreMock, httpServerMock, loggingSystemMock } from 'src/core/server/mocks'; import { ReportingCore } from '../..'; import { JobParamsPDFDeprecated, TaskPayloadPDF } from '../../export_types/printable_pdf/types'; import { Report, ReportingStore } from '../../lib/store'; import { ReportApiJSON } from '../../lib/store/report'; -import { - createMockConfigSchema, - createMockLevelLogger, - createMockReportingCore, -} from '../../test_helpers'; +import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import { ReportingRequestHandlerContext, ReportingSetup } from '../../types'; import { RequestHandler } from './request_handler'; @@ -43,7 +39,7 @@ const getMockResponseFactory = () => unauthorized: (obj: unknown) => obj, } as unknown as KibanaResponseFactory); -const mockLogger = createMockLevelLogger(); +const mockLogger = loggingSystemMock.createLogger(); describe('Handle request to generate', () => { let reportingCore: ReportingCore; diff --git a/x-pack/plugins/reporting/server/routes/lib/request_handler.ts b/x-pack/plugins/reporting/server/routes/lib/request_handler.ts index b0a2032c18f19e..b8a3a4c69802ca 100644 --- a/x-pack/plugins/reporting/server/routes/lib/request_handler.ts +++ b/x-pack/plugins/reporting/server/routes/lib/request_handler.ts @@ -7,12 +7,12 @@ import Boom from '@hapi/boom'; import { i18n } from '@kbn/i18n'; -import { KibanaRequest, KibanaResponseFactory } from 'kibana/server'; -import { ReportingCore } from '../..'; +import type { KibanaRequest, KibanaResponseFactory, Logger } from 'kibana/server'; +import type { ReportingCore } from '../..'; import { API_BASE_URL } from '../../../common/constants'; -import { checkParamsVersion, cryptoFactory, LevelLogger } from '../../lib'; +import { checkParamsVersion, cryptoFactory } from '../../lib'; import { Report } from '../../lib/store'; -import { BaseParams, ReportingRequestHandlerContext, ReportingUser } from '../../types'; +import type { BaseParams, ReportingRequestHandlerContext, ReportingUser } from '../../types'; export const handleUnavailable = (res: KibanaResponseFactory) => { return res.custom({ statusCode: 503, body: 'Not Available' }); @@ -30,7 +30,7 @@ export class RequestHandler { private context: ReportingRequestHandlerContext, private req: KibanaRequest, private res: KibanaResponseFactory, - private logger: LevelLogger + private logger: Logger ) {} private async encryptHeaders() { @@ -53,7 +53,7 @@ export class RequestHandler { } const [createJob, store] = await Promise.all([ - exportType.createJobFnFactory(reporting, logger.clone([exportType.id])), + exportType.createJobFnFactory(reporting, logger.get(exportType.id)), reporting.getStore(), ]); diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_levellogger.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_levellogger.ts deleted file mode 100644 index a6e6be47bdfcdd..00000000000000 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_levellogger.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -jest.mock('../lib/level_logger'); - -import { loggingSystemMock } from 'src/core/server/mocks'; -import { LevelLogger } from '../lib/level_logger'; - -export function createMockLevelLogger() { - // eslint-disable-next-line no-console - const consoleLogger = (tag: string) => (message: unknown) => console.log(tag, message); - - const logger = new LevelLogger(loggingSystemMock.create()) as jest.Mocked; - - // logger.debug.mockImplementation(consoleLogger('debug')); // uncomment this to see debug logs in jest tests - logger.info.mockImplementation(consoleLogger('info')); - logger.warn.mockImplementation(consoleLogger('warn')); - logger.warning = jest.fn().mockImplementation(consoleLogger('warn')); - logger.error.mockImplementation(consoleLogger('error')); - logger.trace.mockImplementation(consoleLogger('trace')); - - logger.clone.mockImplementation(() => logger); - - return logger; -} diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts index 49d92a0fe4448e..e00ebd99f0420a 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts @@ -10,7 +10,12 @@ jest.mock('../usage'); import _ from 'lodash'; import { BehaviorSubject } from 'rxjs'; -import { coreMock, elasticsearchServiceMock, statusServiceMock } from 'src/core/server/mocks'; +import { + coreMock, + elasticsearchServiceMock, + loggingSystemMock, + statusServiceMock, +} from 'src/core/server/mocks'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { dataPluginMock } from 'src/plugins/data/server/mocks'; import { FieldFormatsRegistry } from 'src/plugins/field_formats/common'; @@ -27,7 +32,6 @@ import { buildConfig, ReportingConfigType } from '../config'; import { ReportingInternalSetup, ReportingInternalStart } from '../core'; import { ReportingStore } from '../lib'; import { setFieldFormats } from '../services'; -import { createMockLevelLogger } from './create_mock_levellogger'; export const createMockPluginSetup = ( setupMock: Partial> @@ -38,13 +42,13 @@ export const createMockPluginSetup = ( router: { get: jest.fn(), post: jest.fn(), put: jest.fn(), delete: jest.fn() }, security: securityMock.createSetup(), taskManager: taskManagerMock.createSetup(), - logger: createMockLevelLogger(), + logger: loggingSystemMock.createLogger(), status: statusServiceMock.createSetupContract(), ...setupMock, }; }; -const logger = createMockLevelLogger(); +const logger = loggingSystemMock.createLogger(); const createMockReportingStore = async (config: ReportingConfigType) => { const mockConfigSchema = createMockConfigSchema(config); diff --git a/x-pack/plugins/reporting/server/test_helpers/index.ts b/x-pack/plugins/reporting/server/test_helpers/index.ts index df0a182075341a..0e1dffe142c748 100644 --- a/x-pack/plugins/reporting/server/test_helpers/index.ts +++ b/x-pack/plugins/reporting/server/test_helpers/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -export { createMockLevelLogger } from './create_mock_levellogger'; export { createMockConfig, createMockConfigSchema, diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index fa69509d16be8a..b3c9261bfd9244 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { IRouter, RequestHandlerContext } from 'src/core/server'; +import type { IRouter, Logger, RequestHandlerContext } from 'kibana/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import type { DataPluginStart } from 'src/plugins/data/server/plugin'; import { FieldFormatsStart } from 'src/plugins/field_formats/server'; @@ -29,7 +29,6 @@ import type { CancellationToken } from '../common/cancellation_token'; import type { BaseParams, BasePayload, TaskRunResult, UrlOrUrlLocatorTuple } from '../common/types'; import type { ReportingConfigType } from './config'; import type { ReportingCore } from './core'; -import type { LevelLogger } from './lib'; import type { ReportTaskParams } from './lib/tasks'; /** @@ -71,12 +70,12 @@ export type RunTaskFn = ( export type CreateJobFnFactory = ( reporting: ReportingCore, - logger: LevelLogger + logger: Logger ) => CreateJobFnType; export type RunTaskFnFactory = ( reporting: ReportingCore, - logger: LevelLogger + logger: Logger ) => RunTaskFnType; export interface ExportTypeDefinition< From a79562a67e1dcffe18b5da7c08dde57a1374acef Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Mon, 7 Mar 2022 15:14:41 +0100 Subject: [PATCH 15/15] [SecuritySolution] Alerts table Fields Browser revamp (#126105) * field browser first revamp implementation * customize columns for security solution alert tables * cleaning * some tests * clean unused code * field browser tests created and existing fixed * security solution test fixes * translations cleaned * fix test * adapt cypress tests * remove translation * fix typo * remove duplicated test * type error fixed * enable body vertical scroll for small screens * fix new field not added to the table bug * addapt Kevin performance improvement * fixed linter error Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../integration/hosts/events_viewer.spec.ts | 14 +- .../timelines/fields_browser.spec.ts | 116 ++--- .../cypress/screens/fields_browser.ts | 26 +- .../cypress/tasks/fields_browser.ts | 31 +- .../components/events_viewer/index.test.tsx | 2 +- .../common/components/events_viewer/index.tsx | 12 +- .../truncatable_text/truncatable_text.tsx | 2 +- .../components/alerts_table/index.tsx | 3 +- .../detection_engine/detection_engine.tsx | 42 +- .../create_field_button/index.test.tsx | 12 +- .../create_field_button/index.tsx | 29 +- .../create_field_button/translations.ts | 0 .../components/fields_browser/field_items.tsx | 143 ------ .../fields_browser/field_name.test.tsx | 81 ---- .../components/fields_browser/field_name.tsx | 165 ------- .../field_table_columns/index.tsx | 117 +++++ .../field_table_columns/translations.ts | 36 ++ .../components/fields_browser/index.tsx | 31 ++ .../timeline/body/actions/header_actions.tsx | 4 +- .../body/column_headers/index.test.tsx | 2 +- .../timeline/body/column_headers/index.tsx | 20 +- .../components/timeline/body/index.test.tsx | 2 +- x-pack/plugins/timelines/common/index.ts | 2 +- .../search_strategy/index_fields/index.ts | 2 + .../common/types/fields_browser/index.ts | 50 +++ .../plugins/timelines/common/types/index.ts | 1 + .../common/types/timeline/actions/index.ts | 5 +- .../timelines/common/types/timeline/index.ts | 4 - .../components/fields_browser/index.tsx | 9 +- .../public/components/t_grid/body/index.tsx | 22 +- .../components/t_grid/integrated/index.tsx | 8 +- .../fields_browser/categories_badges.test.tsx | 60 +++ .../fields_browser/categories_badges.tsx | 56 +++ .../fields_browser/categories_pane.test.tsx | 51 --- .../fields_browser/categories_pane.tsx | 118 ----- .../categories_selector.test.tsx | 92 ++++ .../fields_browser/categories_selector.tsx | 173 ++++++++ .../toolbar/fields_browser/category.test.tsx | 100 ----- .../toolbar/fields_browser/category.tsx | 114 ----- .../fields_browser/category_columns.test.tsx | 153 ------- .../fields_browser/category_columns.tsx | 157 ------- .../fields_browser/category_title.test.tsx | 72 --- .../toolbar/fields_browser/category_title.tsx | 67 --- .../fields_browser/field_browser.test.tsx | 49 +-- .../toolbar/fields_browser/field_browser.tsx | 145 ++---- .../fields_browser/field_items.test.tsx | 416 +++++++----------- .../toolbar/fields_browser/field_items.tsx | 230 ++++++---- .../fields_browser/field_name.test.tsx | 2 +- .../toolbar/fields_browser/field_name.tsx | 2 +- .../fields_browser/field_table.test.tsx | 225 ++++++++++ .../toolbar/fields_browser/field_table.tsx | 126 ++++++ .../fields_browser/fields_pane.test.tsx | 112 ----- .../toolbar/fields_browser/fields_pane.tsx | 145 ------ .../toolbar/fields_browser/helpers.test.tsx | 33 -- .../t_grid/toolbar/fields_browser/helpers.tsx | 311 +------------ .../toolbar/fields_browser/index.test.tsx | 138 +++--- .../t_grid/toolbar/fields_browser/index.tsx | 108 ++--- .../toolbar/fields_browser/search.test.tsx | 74 +--- .../t_grid/toolbar/fields_browser/search.tsx | 69 +-- .../toolbar/fields_browser/translations.ts | 33 +- .../t_grid/toolbar/fields_browser/types.ts | 27 -- .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - 63 files changed, 1670 insertions(+), 2787 deletions(-) rename x-pack/plugins/security_solution/public/timelines/components/{ => fields_browser}/create_field_button/index.test.tsx (92%) rename x-pack/plugins/security_solution/public/timelines/components/{ => fields_browser}/create_field_button/index.tsx (80%) rename x-pack/plugins/security_solution/public/timelines/components/{ => fields_browser}/create_field_button/translations.ts (100%) delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/index.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/translations.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx create mode 100644 x-pack/plugins/timelines/common/types/fields_browser/index.ts create mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_badges.test.tsx create mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_badges.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.test.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.tsx create mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_selector.test.tsx create mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_selector.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.test.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.test.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.test.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.tsx create mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.test.tsx create mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.test.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx delete mode 100644 x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/types.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts b/x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts index c28c55e0eb3f7f..47e71345ff0c49 100644 --- a/x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/hosts/events_viewer.spec.ts @@ -8,7 +8,7 @@ import { FIELDS_BROWSER_CHECKBOX, FIELDS_BROWSER_CONTAINER, - FIELDS_BROWSER_SELECTED_CATEGORY_TITLE, + FIELDS_BROWSER_SELECTED_CATEGORIES_BADGES, } from '../../screens/fields_browser'; import { HOST_GEO_CITY_NAME_HEADER, @@ -17,7 +17,11 @@ import { SERVER_SIDE_EVENT_COUNT, } from '../../screens/hosts/events'; -import { closeFieldsBrowser, filterFieldsBrowser } from '../../tasks/fields_browser'; +import { + closeFieldsBrowser, + filterFieldsBrowser, + toggleCategory, +} from '../../tasks/fields_browser'; import { loginAndWaitForPage } from '../../tasks/login'; import { openEvents } from '../../tasks/hosts/main'; import { @@ -60,11 +64,13 @@ describe('Events Viewer', () => { cy.get(FIELDS_BROWSER_CONTAINER).should('not.exist'); }); - it('displays the `default ECS` category (by default)', () => { - cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_TITLE).should('have.text', 'default ECS'); + it('displays all categories (by default)', () => { + cy.get(FIELDS_BROWSER_SELECTED_CATEGORIES_BADGES).should('be.empty'); }); it('displays a checked checkbox for all of the default events viewer columns that are also in the default ECS category', () => { + const category = 'default ECS'; + toggleCategory(category); defaultHeadersInDefaultEcsCategory.forEach((header) => cy.get(FIELDS_BROWSER_CHECKBOX(header.id)).should('be.checked') ); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts index 07ea4078ce7c4b..89a9fc4c0c6ba1 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts @@ -8,14 +8,13 @@ import { FIELDS_BROWSER_CATEGORIES_COUNT, FIELDS_BROWSER_FIELDS_COUNT, - FIELDS_BROWSER_HOST_CATEGORIES_COUNT, FIELDS_BROWSER_HOST_GEO_CITY_NAME_HEADER, FIELDS_BROWSER_HEADER_HOST_GEO_CONTINENT_NAME_HEADER, FIELDS_BROWSER_MESSAGE_HEADER, - FIELDS_BROWSER_SELECTED_CATEGORY_TITLE, - FIELDS_BROWSER_SELECTED_CATEGORY_COUNT, - FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT, FIELDS_BROWSER_FILTER_INPUT, + FIELDS_BROWSER_CATEGORIES_FILTER_CONTAINER, + FIELDS_BROWSER_SELECTED_CATEGORIES_BADGES, + FIELDS_BROWSER_CATEGORY_BADGE, } from '../../screens/fields_browser'; import { TIMELINE_FIELDS_BUTTON } from '../../screens/timeline'; import { cleanKibana } from '../../tasks/common'; @@ -26,13 +25,14 @@ import { clearFieldsBrowser, closeFieldsBrowser, filterFieldsBrowser, + toggleCategoryFilter, removesMessageField, resetFields, + toggleCategory, } from '../../tasks/fields_browser'; import { loginAndWaitForPage } from '../../tasks/login'; import { openTimelineUsingToggle } from '../../tasks/security_main'; import { openTimelineFieldsBrowser, populateTimeline } from '../../tasks/timeline'; -import { ecsFieldMap } from '../../../../rule_registry/common/assets/field_maps/ecs_field_map'; import { HOSTS_URL } from '../../urls/navigation'; @@ -61,21 +61,8 @@ describe('Fields Browser', () => { clearFieldsBrowser(); }); - it('displays the `default ECS` category (by default)', () => { - cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_TITLE).should('have.text', 'default ECS'); - }); - - it('the `defaultECS` (selected) category count matches the default timeline header count', () => { - cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_COUNT).should( - 'have.text', - `${defaultHeaders.length}` - ); - }); - - it('displays a checked checkbox for all of the default timeline columns', () => { - defaultHeaders.forEach((header) => - cy.get(`[data-test-subj="field-${header.id}-checkbox"]`).should('be.checked') - ); + it('displays all categories (by default)', () => { + cy.get(FIELDS_BROWSER_SELECTED_CATEGORIES_BADGES).should('be.empty'); }); it('displays the expected count of categories that match the filter input', () => { @@ -83,54 +70,50 @@ describe('Fields Browser', () => { filterFieldsBrowser(filterInput); - cy.get(FIELDS_BROWSER_CATEGORIES_COUNT).should('have.text', '2 categories'); + cy.get(FIELDS_BROWSER_CATEGORIES_COUNT).should('have.text', '2'); }); it('displays a search results label with the expected count of fields matching the filter input', () => { const filterInput = 'host.mac'; - filterFieldsBrowser(filterInput); - cy.get(FIELDS_BROWSER_HOST_CATEGORIES_COUNT) - .invoke('text') - .then((hostCategoriesCount) => { - cy.get(FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT) - .invoke('text') - .then((systemCategoriesCount) => { - cy.get(FIELDS_BROWSER_FIELDS_COUNT).should( - 'have.text', - `${+hostCategoriesCount + +systemCategoriesCount} fields` - ); - }); - }); - }); - - it('displays a count of only the fields in the selected category that match the filter input', () => { - const filterInput = 'host.geo.c'; + cy.get(FIELDS_BROWSER_FIELDS_COUNT).should('contain.text', '2'); + }); - filterFieldsBrowser(filterInput); + it('the `default ECS` category matches the default timeline header fields', () => { + const category = 'default ECS'; + toggleCategory(category); + cy.get(FIELDS_BROWSER_FIELDS_COUNT).should('contain.text', `${defaultHeaders.length}`); + + defaultHeaders.forEach((header) => { + cy.get(`[data-test-subj="field-${header.id}-checkbox"]`).should('be.checked'); + }); + toggleCategory(category); + }); + + it('creates the category badge when it is selected', () => { + const category = 'host'; + + cy.get(FIELDS_BROWSER_CATEGORY_BADGE(category)).should('not.exist'); + toggleCategory(category); + cy.get(FIELDS_BROWSER_CATEGORY_BADGE(category)).should('exist'); + toggleCategory(category); + }); + + it('search a category should match the category in the category filter', () => { + const category = 'host'; - const fieldsThatMatchFilterInput = Object.keys(ecsFieldMap).filter((fieldName) => { - const dotDelimitedFieldParts = fieldName.split('.'); - const fieldPartMatch = dotDelimitedFieldParts.filter((fieldPart) => { - const camelCasedStringsMatching = fieldPart - .split('_') - .some((part) => part.startsWith(filterInput)); - if (fieldPart.startsWith(filterInput)) { - return true; - } else if (camelCasedStringsMatching) { - return true; - } else { - return false; - } - }); - return fieldName.startsWith(filterInput) || fieldPartMatch.length > 0; - }).length; - - cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_COUNT).should( - 'have.text', - fieldsThatMatchFilterInput - ); + filterFieldsBrowser(category); + toggleCategoryFilter(); + cy.get(FIELDS_BROWSER_CATEGORIES_FILTER_CONTAINER).should('contain.text', category); + }); + + it('search a category should filter out non matching categories in the category filter', () => { + const category = 'host'; + const categoryCheck = 'event'; + filterFieldsBrowser(category); + toggleCategoryFilter(); + cy.get(FIELDS_BROWSER_CATEGORIES_FILTER_CONTAINER).should('not.contain.text', categoryCheck); }); }); @@ -157,18 +140,15 @@ describe('Fields Browser', () => { cy.get(FIELDS_BROWSER_MESSAGE_HEADER).should('not.exist'); }); - it('selects a search results label with the expected count of categories matching the filter input', () => { - const category = 'host'; - filterFieldsBrowser(category); - - cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_TITLE).should('have.text', category); - }); - it('adds a field to the timeline when the user clicks the checkbox', () => { const filterInput = 'host.geo.c'; - filterFieldsBrowser(filterInput); + closeFieldsBrowser(); cy.get(FIELDS_BROWSER_HOST_GEO_CITY_NAME_HEADER).should('not.exist'); + + openTimelineFieldsBrowser(); + + filterFieldsBrowser(filterInput); addsHostGeoCityNameToTimeline(); closeFieldsBrowser(); diff --git a/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts b/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts index 4a5f813c301db0..66a7ba50c8070e 100644 --- a/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts +++ b/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts @@ -7,20 +7,16 @@ export const CLOSE_BTN = '[data-test-subj="close"]'; -export const FIELDS_BROWSER_CATEGORIES_COUNT = '[data-test-subj="categories-count"]'; +export const FIELDS_BROWSER_CONTAINER = '[data-test-subj="fields-browser-container"]'; export const FIELDS_BROWSER_CHECKBOX = (id: string) => { - return `[data-test-subj="category-table-container"] [data-test-subj="field-${id}-checkbox"]`; + return `${FIELDS_BROWSER_CONTAINER} [data-test-subj="field-${id}-checkbox"]`; }; -export const FIELDS_BROWSER_CONTAINER = '[data-test-subj="fields-browser-container"]'; - export const FIELDS_BROWSER_FIELDS_COUNT = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="fields-count"]`; export const FIELDS_BROWSER_FILTER_INPUT = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="field-search"]`; -export const FIELDS_BROWSER_HOST_CATEGORIES_COUNT = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="host-category-count"]`; - export const FIELDS_BROWSER_HOST_GEO_CITY_NAME_CHECKBOX = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="field-host.geo.city_name-checkbox"]`; export const FIELDS_BROWSER_HOST_GEO_CITY_NAME_HEADER = @@ -38,8 +34,22 @@ export const FIELDS_BROWSER_MESSAGE_HEADER = export const FIELDS_BROWSER_RESET_FIELDS = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="reset-fields"]`; -export const FIELDS_BROWSER_SELECTED_CATEGORY_COUNT = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="selected-category-count-badge"]`; +export const FIELDS_BROWSER_CATEGORIES_FILTER_BUTTON = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="categories-filter-button"]`; +export const FIELDS_BROWSER_SELECTED_CATEGORY_COUNT = `${FIELDS_BROWSER_CATEGORIES_FILTER_BUTTON} span.euiNotificationBadge`; +export const FIELDS_BROWSER_CATEGORIES_COUNT = `${FIELDS_BROWSER_CATEGORIES_FILTER_BUTTON} span.euiNotificationBadge`; -export const FIELDS_BROWSER_SELECTED_CATEGORY_TITLE = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="selected-category-title"]`; +export const FIELDS_BROWSER_SELECTED_CATEGORIES_BADGES = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="category-badges"]`; +export const FIELDS_BROWSER_CATEGORY_BADGE = (id: string) => { + return `${FIELDS_BROWSER_SELECTED_CATEGORIES_BADGES} [data-test-subj="category-badge-${id}"]`; +}; + +export const FIELDS_BROWSER_CATEGORIES_FILTER_CONTAINER = + '[data-test-subj="categories-selector-container"]'; +export const FIELDS_BROWSER_CATEGORIES_FILTER_SEARCH = + '[data-test-subj="categories-selector-search"]'; +export const FIELDS_BROWSER_CATEGORY_FILTER_OPTION = (id: string) => { + const idAttr = id.replace(/\s/g, ''); + return `${FIELDS_BROWSER_CATEGORIES_FILTER_CONTAINER} [data-test-subj="categories-selector-option-${idAttr}"]`; +}; export const FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="system-category-count"]`; diff --git a/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts b/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts index 941a19669f2efb..04b59305b591a2 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts @@ -13,6 +13,9 @@ import { FIELDS_BROWSER_RESET_FIELDS, FIELDS_BROWSER_CHECKBOX, CLOSE_BTN, + FIELDS_BROWSER_CATEGORIES_FILTER_BUTTON, + FIELDS_BROWSER_CATEGORY_FILTER_OPTION, + FIELDS_BROWSER_CATEGORIES_FILTER_SEARCH, } from '../screens/fields_browser'; export const addsFields = (fields: string[]) => { @@ -34,10 +37,9 @@ export const addsHostGeoContinentNameToTimeline = () => { }; export const clearFieldsBrowser = () => { - cy.clock(); - cy.get(FIELDS_BROWSER_FILTER_INPUT).type('{selectall}{backspace}'); - cy.wait(0); - cy.tick(1000); + cy.get(FIELDS_BROWSER_FILTER_INPUT) + .type('{selectall}{backspace}') + .waitUntil((subject) => !subject.hasClass('euiFieldSearch-isLoading')); }; export const closeFieldsBrowser = () => { @@ -46,12 +48,21 @@ export const closeFieldsBrowser = () => { }; export const filterFieldsBrowser = (fieldName: string) => { - cy.clock(); - cy.get(FIELDS_BROWSER_FILTER_INPUT).type(fieldName, { delay: 50 }); - cy.wait(0); - cy.tick(1000); - // the text filter is debounced by 250 ms, wait 1s for changes to be applied - cy.get(FIELDS_BROWSER_FILTER_INPUT).should('not.have.class', 'euiFieldSearch-isLoading'); + cy.get(FIELDS_BROWSER_FILTER_INPUT) + .clear() + .type(fieldName) + .waitUntil((subject) => !subject.hasClass('euiFieldSearch-isLoading')); +}; + +export const toggleCategoryFilter = () => { + cy.get(FIELDS_BROWSER_CATEGORIES_FILTER_BUTTON).click({ force: true }); +}; + +export const toggleCategory = (category: string) => { + toggleCategoryFilter(); + cy.get(FIELDS_BROWSER_CATEGORIES_FILTER_SEARCH).clear().type(category); + cy.get(FIELDS_BROWSER_CATEGORY_FILTER_OPTION(category)).click({ force: true }); + toggleCategoryFilter(); }; export const removesMessageField = () => { diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx index 2ecae444879082..cdc9cc9b6f32dc 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx @@ -34,7 +34,7 @@ jest.mock('../../../timelines/containers', () => ({ jest.mock('../../components/url_state/normalize_time_range.ts'); const mockUseCreateFieldButton = jest.fn().mockReturnValue(<>); -jest.mock('../../../timelines/components/create_field_button', () => ({ +jest.mock('../../../timelines/components/fields_browser/create_field_button', () => ({ useCreateFieldButton: (...params: unknown[]) => mockUseCreateFieldButton(...params), })); 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 5e3fc4e81f9dc3..68c4af5ee2fe8f 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 @@ -30,9 +30,9 @@ import { FIELDS_WITHOUT_CELL_ACTIONS } from '../../lib/cell_actions/constants'; import { useGetUserCasesPermissions, useKibana } from '../../lib/kibana'; import { GraphOverlay } from '../../../timelines/components/graph_overlay'; import { + useFieldBrowserOptions, CreateFieldEditorActions, - useCreateFieldButton, -} from '../../../timelines/components/create_field_button'; +} from '../../../timelines/components/fields_browser'; const EMPTY_CONTROL_COLUMNS: ControlColumnProps[] = []; @@ -177,7 +177,11 @@ const StatefulEventsViewerComponent: React.FC = ({ }, [id, timelineQuery, globalQuery]); const bulkActions = useMemo(() => ({ onAlertStatusActionSuccess }), [onAlertStatusActionSuccess]); - const createFieldComponent = useCreateFieldButton(scopeId, id, editorActionsRef); + const fieldBrowserOptions = useFieldBrowserOptions({ + sourcererScope: scopeId, + timelineId: id, + editorActionsRef, + }); const casesPermissions = useGetUserCasesPermissions(); const CasesContext = casesUi.getCasesContext(); @@ -201,6 +205,7 @@ const StatefulEventsViewerComponent: React.FC = ({ docValueFields, end, entityType, + fieldBrowserOptions, filters: globalFilters, filterStatus: currentFilter, globalFullScreen, @@ -228,7 +233,6 @@ const StatefulEventsViewerComponent: React.FC = ({ trailingControlColumns, type: 'embedded', unit, - createFieldComponent, })} diff --git a/x-pack/plugins/security_solution/public/common/components/truncatable_text/truncatable_text.tsx b/x-pack/plugins/security_solution/public/common/components/truncatable_text/truncatable_text.tsx index cc1c53d1071002..27369dadb8a3bf 100644 --- a/x-pack/plugins/security_solution/public/common/components/truncatable_text/truncatable_text.tsx +++ b/x-pack/plugins/security_solution/public/common/components/truncatable_text/truncatable_text.tsx @@ -16,7 +16,7 @@ import { EuiToolTip } from '@elastic/eui'; * Note: Requires a parent container with a defined width or max-width. */ -const EllipsisText = styled.span` +export const EllipsisText = styled.span` &, & * { display: inline-block; 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 1499e803fdf37e..0f6d2d260ae0d7 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 @@ -99,7 +99,6 @@ export const AlertsTableComponent: React.FC = ({ const { browserFields, indexPattern: indexPatterns, - loading: indexPatternsLoading, selectedPatterns, } = useSourcererDataView(SourcererScopeName.detections); const kibana = useKibana(); @@ -360,7 +359,7 @@ export const AlertsTableComponent: React.FC = ({ const casesPermissions = useGetUserCasesPermissions(); const CasesContext = kibana.services.cases.getCasesContext(); - if (loading || indexPatternsLoading || isEmpty(selectedPatterns)) { + if (loading || isEmpty(selectedPatterns)) { return null; } 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 e4f51b05ad6d9b..eccb2e081cd9de 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 @@ -140,7 +140,7 @@ const DetectionEnginePageComponent: React.FC = ({ const { formatUrl } = useFormatUrl(SecurityPageName.rules); const [showBuildingBlockAlerts, setShowBuildingBlockAlerts] = useState(false); const [showOnlyThreatIndicatorAlerts, setShowOnlyThreatIndicatorAlerts] = useState(false); - const loading = userInfoLoading || listsConfigLoading || isLoadingIndexPattern; + const loading = userInfoLoading || listsConfigLoading; const { application: { navigateToUrl }, timelines: timelinesUi, @@ -341,24 +341,32 @@ const DetectionEnginePageComponent: React.FC = ({ - + {isLoadingIndexPattern ? ( + + ) : ( + + )} - + {isLoadingIndexPattern ? ( + + ) : ( + + )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/create_field_button/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.test.tsx similarity index 92% rename from x-pack/plugins/security_solution/public/timelines/components/create_field_button/index.test.tsx rename to x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.test.tsx index 0afb2bf6413517..1bddd96c057277 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/create_field_button/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.test.tsx @@ -11,15 +11,15 @@ import { CreateFieldButton, CreateFieldEditorActions } from './index'; import { indexPatternFieldEditorPluginMock, Start, -} from '../../../../../../../src/plugins/data_view_field_editor/public/mocks'; +} from '../../../../../../../../src/plugins/data_view_field_editor/public/mocks'; -import { TestProviders } from '../../../common/mock'; -import { useKibana } from '../../../common/lib/kibana'; -import type { DataView } from '../../../../../../../src/plugins/data/common'; -import { TimelineId } from '../../../../common/types'; +import { TestProviders } from '../../../../common/mock'; +import { useKibana } from '../../../../common/lib/kibana'; +import type { DataView } from '../../../../../../../../src/plugins/data/common'; +import { TimelineId } from '../../../../../common/types'; let mockIndexPatternFieldEditor: Start; -jest.mock('../../../common/lib/kibana'); +jest.mock('../../../../common/lib/kibana'); const useKibanaMock = useKibana as jest.Mocked; const runAllPromises = () => new Promise(setImmediate); diff --git a/x-pack/plugins/security_solution/public/timelines/components/create_field_button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.tsx similarity index 80% rename from x-pack/plugins/security_solution/public/timelines/components/create_field_button/index.tsx rename to x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.tsx index 8979a78d7aa465..645e1f0b29aed5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/create_field_button/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/create_field_button/index.tsx @@ -10,23 +10,26 @@ import { EuiButton } from '@elastic/eui'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; -import type { DataViewField, DataView } from '../../../../../../../src/plugins/data_views/common'; -import { useKibana } from '../../../common/lib/kibana'; +import type { + DataViewField, + DataView, +} from '../../../../../../../../src/plugins/data_views/common'; +import { useKibana } from '../../../../common/lib/kibana'; import * as i18n from './translations'; -import { CreateFieldComponentType, TimelineId } from '../../../../../timelines/common'; -import { upsertColumn } from '../../../../../timelines/public'; -import { useDataView } from '../../../common/containers/source/use_data_view'; -import { SourcererScopeName } from '../../../common/store/sourcerer/model'; -import { sourcererSelectors } from '../../../common/store'; -import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; -import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/constants'; -import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; +import { FieldBrowserOptions, TimelineId } from '../../../../../../timelines/common'; +import { upsertColumn } from '../../../../../../timelines/public'; +import { useDataView } from '../../../../common/containers/source/use_data_view'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { sourcererSelectors } from '../../../../common/store'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { DEFAULT_COLUMN_MIN_WIDTH } from '../../timeline/body/constants'; +import { defaultColumnHeaderType } from '../../timeline/body/column_headers/default_headers'; export type CreateFieldEditorActions = { closeEditor: () => void } | null; -type CreateFieldEditorActionsRef = MutableRefObject; +export type CreateFieldEditorActionsRef = MutableRefObject; -interface CreateFieldButtonProps { +export interface CreateFieldButtonProps { selectedDataViewId: string; onClick: () => void; timelineId: TimelineId; @@ -142,7 +145,7 @@ export const useCreateFieldButton = ( return; } // It receives onClick props from field browser in order to close the modal. - const CreateFieldButtonComponent: CreateFieldComponentType = ({ onClick }) => ( + const CreateFieldButtonComponent: FieldBrowserOptions['createFieldButton'] = ({ onClick }) => ( void; -}) => { - const keyboardHandlerRef = useRef(null); - const [closePopOverTrigger, setClosePopOverTrigger] = useState(false); - const [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState(false); - const { timelines } = useKibana().services; - - const handleClosePopOverTrigger = useCallback(() => { - setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger); - - setHoverActionsOwnFocus((prevHoverActionsOwnFocus) => { - if (prevHoverActionsOwnFocus) { - // on the next tick, re-focus the keyboard handler if the hover actions owned focus - setTimeout(() => { - keyboardHandlerRef.current?.focus(); - }, 0); - } - return false; // always give up ownership - }); - - setTimeout(() => { - setHoverActionsOwnFocus(false); - }, 0); // invoked on the next tick, because we want to restore focus first - }, []); - - const openPopover = useCallback(() => { - setHoverActionsOwnFocus(true); - }, [setHoverActionsOwnFocus]); - - const { onBlur, onKeyDown } = timelines.getUseDraggableKeyboardWrapper()({ - closePopover: handleClosePopOverTrigger, - draggableId: getDraggableFieldId({ - contextId: `field-browser-field-items-field-draggable-${timelineId}-${categoryId}-${fieldName}`, - fieldId: fieldName, - }), - fieldName, - keyboardHandlerRef, - openPopover, - }); - - const onFocus = useCallback(() => { - keyboardHandlerRef.current?.focus(); - }, []); - - const onCloseRequested = useCallback(() => { - setHoverActionsOwnFocus((prevHoverActionOwnFocus) => - prevHoverActionOwnFocus ? false : prevHoverActionOwnFocus - ); - - setTimeout(() => { - onFocus(); // return focus to this draggable on the next tick, because we owned focus - }, 0); - }, [onFocus]); - - return ( -
- - {(provided) => ( -
- -
- )} -
-
- ); -}; - -export const DraggableFieldsBrowserField = React.memo(DraggableFieldsBrowserFieldComponent); -DraggableFieldsBrowserField.displayName = 'DraggableFieldsBrowserFieldComponent'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx deleted file mode 100644 index 5acc0ef9aa46b3..00000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mount } from 'enzyme'; -import React from 'react'; -import { waitFor } from '@testing-library/react'; -import { mockBrowserFields } from '../../../common/containers/source/mock'; -import { TestProviders } from '../../../common/mock'; -import '../../../common/mock/match_media'; -import { getColumnsWithTimestamp } from '../../../common/components/event_details/helpers'; - -import { FieldName } from './field_name'; - -jest.mock('../../../common/lib/kibana'); - -const categoryId = 'base'; -const timestampFieldId = '@timestamp'; - -const defaultProps = { - categoryId, - categoryColumns: getColumnsWithTimestamp({ - browserFields: mockBrowserFields, - category: categoryId, - }), - closePopOverTrigger: false, - fieldId: timestampFieldId, - handleClosePopOverTrigger: jest.fn(), - hoverActionsOwnFocus: false, - onCloseRequested: jest.fn(), - onUpdateColumns: jest.fn(), - setClosePopOverTrigger: jest.fn(), -}; - -describe('FieldName', () => { - beforeEach(() => { - jest.useFakeTimers(); - }); - - test('it renders the field name', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`[data-test-subj="field-name-${timestampFieldId}"]`).first().text() - ).toEqual(timestampFieldId); - }); - - test('it renders a copy to clipboard action menu item a user hovers over the name', async () => { - const wrapper = mount( - - - - ); - await waitFor(() => { - wrapper.find('[data-test-subj="withHoverActionsButton"]').simulate('mouseenter'); - wrapper.update(); - jest.runAllTimers(); - wrapper.update(); - expect(wrapper.find('[data-test-subj="hover-actions-copy-button"]').exists()).toBe(true); - }); - }); - - test('it highlights the text specified by the `highlight` prop', () => { - const highlight = 'stamp'; - - const wrapper = mount( - - - - ); - - expect(wrapper.find('mark').first().text()).toEqual(highlight); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx deleted file mode 100644 index 6e9672d08b3666..00000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.tsx +++ /dev/null @@ -1,165 +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 { EuiHighlight, EuiText } from '@elastic/eui'; -import React, { useCallback, useState, useMemo, useRef, useContext } from 'react'; -import styled from 'styled-components'; - -import { OnUpdateColumns } from '../timeline/events'; -import { WithHoverActions } from '../../../common/components/with_hover_actions'; -import { ColumnHeaderOptions } from '../../../../common/types'; -import { HoverActions } from '../../../common/components/hover_actions'; -import { TimelineContext } from '../../../../../timelines/public'; - -/** - * The name of a (draggable) field - */ -export const FieldNameContainer = styled.span` - border-radius: 4px; - display: flex; - padding: 0 4px 0 8px; - position: relative; - - &::before { - background-image: linear-gradient( - 135deg, - ${({ theme }) => theme.eui.euiColorMediumShade} 25%, - transparent 25% - ), - linear-gradient(-135deg, ${({ theme }) => theme.eui.euiColorMediumShade} 25%, transparent 25%), - linear-gradient(135deg, transparent 75%, ${({ theme }) => theme.eui.euiColorMediumShade} 75%), - linear-gradient(-135deg, transparent 75%, ${({ theme }) => theme.eui.euiColorMediumShade} 75%); - background-position: 0 0, 1px 0, 1px -1px, 0px 1px; - background-size: 2px 2px; - bottom: 2px; - content: ''; - display: block; - left: 2px; - position: absolute; - top: 2px; - width: 4px; - } - - &:hover, - &:focus { - transition: background-color 0.7s ease; - background-color: #000; - color: #fff; - - &::before { - background-image: linear-gradient(135deg, #fff 25%, transparent 25%), - linear-gradient( - -135deg, - ${({ theme }) => theme.eui.euiColorLightestShade} 25%, - transparent 25% - ), - linear-gradient( - 135deg, - transparent 75%, - ${({ theme }) => theme.eui.euiColorLightestShade} 75% - ), - linear-gradient( - -135deg, - transparent 75%, - ${({ theme }) => theme.eui.euiColorLightestShade} 75% - ); - } - } -`; - -FieldNameContainer.displayName = 'FieldNameContainer'; - -/** Renders a field name in it's non-dragging state */ -export const FieldName = React.memo<{ - categoryId: string; - categoryColumns: ColumnHeaderOptions[]; - closePopOverTrigger: boolean; - fieldId: string; - highlight?: string; - handleClosePopOverTrigger: () => void; - hoverActionsOwnFocus: boolean; - onCloseRequested: () => void; - onUpdateColumns: OnUpdateColumns; -}>( - ({ - closePopOverTrigger, - fieldId, - highlight = '', - handleClosePopOverTrigger, - hoverActionsOwnFocus, - onCloseRequested, - }) => { - const containerRef = useRef(null); - const [showTopN, setShowTopN] = useState(false); - const { timelineId: timelineIdFind } = useContext(TimelineContext); - - const toggleTopN = useCallback(() => { - setShowTopN((prevShowTopN) => { - const newShowTopN = !prevShowTopN; - if (newShowTopN === false) { - handleClosePopOverTrigger(); - } - return newShowTopN; - }); - }, [handleClosePopOverTrigger]); - - const closeTopN = useCallback(() => { - setShowTopN(false); - }, []); - - const hoverContent = useMemo( - () => ( - - ), - [ - closeTopN, - fieldId, - handleClosePopOverTrigger, - hoverActionsOwnFocus, - showTopN, - timelineIdFind, - toggleTopN, - ] - ); - - const render = useCallback( - () => ( - - - - {fieldId} - - - - ), - [fieldId, highlight] - ); - - return ( -
- -
- ); - } -); - -FieldName.displayName = 'FieldName'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/index.tsx new file mode 100644 index 00000000000000..b060575fdc5cb5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/index.tsx @@ -0,0 +1,117 @@ +/* + * 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 { + EuiToolTip, + EuiFlexGroup, + EuiFlexItem, + EuiScreenReaderOnly, + EuiHealth, + EuiBadge, + EuiIcon, + EuiText, + EuiHighlight, +} from '@elastic/eui'; +import type { FieldTableColumns } from '../../../../../../timelines/common/types'; +import * as i18n from './translations'; +import { + getExampleText, + getIconFromType, +} from '../../../../common/components/event_details/helpers'; +import { getEmptyValue } from '../../../../common/components/empty_value'; +import { EllipsisText } from '../../../../common/components/truncatable_text'; + +const TypeIcon = styled(EuiIcon)` + margin: 0 4px; + position: relative; + top: -1px; +`; +TypeIcon.displayName = 'TypeIcon'; + +export const Description = styled.span` + user-select: text; + width: 400px; +`; +Description.displayName = 'Description'; + +export const FieldName = React.memo<{ + fieldId: string; + highlight?: string; +}>(({ fieldId, highlight = '' }) => ( + + + {fieldId} + + +)); +FieldName.displayName = 'FieldName'; + +export const getFieldTableColumns = (highlight: string): FieldTableColumns => [ + { + field: 'name', + name: i18n.NAME, + render: (name: string, { type }) => { + return ( + + + + + + + + + + + + ); + }, + sortable: true, + width: '200px', + }, + { + field: 'description', + name: i18n.DESCRIPTION, + render: (description, { name, example }) => ( + + <> + +

{i18n.DESCRIPTION_FOR_FIELD(name)}

+
+ + + {`${description ?? getEmptyValue()} ${getExampleText(example)}`} + + + +
+ ), + sortable: true, + width: '400px', + }, + { + field: 'isRuntime', + name: i18n.RUNTIME, + render: (isRuntime: boolean) => + isRuntime ? : null, + sortable: true, + width: '80px', + }, + { + field: 'category', + name: i18n.CATEGORY, + render: (category: string, { name }) => ( + {category} + ), + sortable: true, + width: '100px', + }, +]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/translations.ts new file mode 100644 index 00000000000000..c16307250c2c81 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_table_columns/translations.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 { i18n } from '@kbn/i18n'; + +export const NAME = i18n.translate('xpack.securitySolution.fieldBrowser.fieldName', { + defaultMessage: 'Name', +}); + +export const DESCRIPTION = i18n.translate('xpack.securitySolution.fieldBrowser.descriptionLabel', { + defaultMessage: 'Description', +}); + +export const DESCRIPTION_FOR_FIELD = (field: string) => + i18n.translate('xpack.securitySolution.fieldBrowser.descriptionForScreenReaderOnly', { + values: { + field, + }, + defaultMessage: 'Description for field {field}:', + }); + +export const CATEGORY = i18n.translate('xpack.securitySolution.fieldBrowser.categoryLabel', { + defaultMessage: 'Category', +}); + +export const RUNTIME = i18n.translate('xpack.securitySolution.fieldBrowser.runtimeLabel', { + defaultMessage: 'Runtime', +}); + +export const RUNTIME_FIELD = i18n.translate('xpack.securitySolution.fieldBrowser.runtimeTitle', { + defaultMessage: 'Runtime Field', +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx new file mode 100644 index 00000000000000..46f2caa147a408 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx @@ -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 { TimelineId } from '../../../../common/types'; +import { SourcererScopeName } from '../../../common/store/sourcerer/model'; +import { useCreateFieldButton, CreateFieldEditorActionsRef } from './create_field_button'; +import { getFieldTableColumns } from './field_table_columns'; + +export type { CreateFieldEditorActions } from './create_field_button'; + +export interface UseFieldBrowserOptions { + sourcererScope: SourcererScopeName; + timelineId: TimelineId; + editorActionsRef?: CreateFieldEditorActionsRef; +} + +export const useFieldBrowserOptions = ({ + sourcererScope, + timelineId, + editorActionsRef, +}: UseFieldBrowserOptions) => { + const createFieldButton = useCreateFieldButton(sourcererScope, timelineId, editorActionsRef); + return { + createFieldButton, + getFieldTableColumns, + }; +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx index 9636aadbc08e3c..0e26edc6ae1c1a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/header_actions.tsx @@ -86,7 +86,7 @@ const HeaderActionsComponent: React.FC = ({ sort, tabType, timelineId, - createFieldComponent, + fieldBrowserOptions, }) => { const { timelines: timelinesUi } = useKibana().services; const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); @@ -184,7 +184,7 @@ const HeaderActionsComponent: React.FC = ({ browserFields, columnHeaders, timelineId, - createFieldComponent, + options: fieldBrowserOptions, })} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx index aec28732f38afa..7e3de3514f5a78 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx @@ -28,7 +28,7 @@ import { HeaderActions } from '../actions/header_actions'; jest.mock('../../../../../common/lib/kibana'); const mockUseCreateFieldButton = jest.fn().mockReturnValue(<>); -jest.mock('../../../create_field_button', () => ({ +jest.mock('../../../fields_browser/create_field_button', () => ({ useCreateFieldButton: (...params: unknown[]) => mockUseCreateFieldButton(...params), })); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx index ca1cdef903de84..e58dd520181c1a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx @@ -34,7 +34,7 @@ import { Sort } from '../sort'; import { ColumnHeader } from './column_header'; import { SourcererScopeName } from '../../../../../common/store/sourcerer/model'; -import { CreateFieldEditorActions, useCreateFieldButton } from '../../../create_field_button'; +import { useFieldBrowserOptions, CreateFieldEditorActions } from '../../../fields_browser'; export interface ColumnHeadersComponentProps { actionsColumnWidth: number; @@ -190,11 +190,11 @@ export const ColumnHeadersComponent = ({ [trailingControlColumns] ); - const createFieldComponent = useCreateFieldButton( - SourcererScopeName.timeline, - timelineId as TimelineId, - fieldEditorActionsRef - ); + const fieldBrowserOptions = useFieldBrowserOptions({ + sourcererScope: SourcererScopeName.timeline, + timelineId: timelineId as TimelineId, + editorActionsRef: fieldEditorActionsRef, + }); const LeadingHeaderActions = useMemo(() => { return leadingHeaderCells.map( @@ -221,7 +221,7 @@ export const ColumnHeadersComponent = ({ sort={sort} tabType={tabType} timelineId={timelineId} - createFieldComponent={createFieldComponent} + fieldBrowserOptions={fieldBrowserOptions} /> )} @@ -234,7 +234,7 @@ export const ColumnHeadersComponent = ({ actionsColumnWidth, browserFields, columnHeaders, - createFieldComponent, + fieldBrowserOptions, isEventViewer, isSelectAllChecked, onSelectAll, @@ -270,7 +270,7 @@ export const ColumnHeadersComponent = ({ sort={sort} tabType={tabType} timelineId={timelineId} - createFieldComponent={createFieldComponent} + fieldBrowserOptions={fieldBrowserOptions} /> )} @@ -283,7 +283,7 @@ export const ColumnHeadersComponent = ({ actionsColumnWidth, browserFields, columnHeaders, - createFieldComponent, + fieldBrowserOptions, isEventViewer, isSelectAllChecked, onSelectAll, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index f616b4afc2af5f..5a9f981988d591 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -114,7 +114,7 @@ jest.mock('../../../../common/lib/helpers/scheduler', () => ({ maxDelay: () => 3000, })); -jest.mock('../../create_field_button', () => ({ +jest.mock('../../fields_browser/create_field_button', () => ({ useCreateFieldButton: () => <>, })); diff --git a/x-pack/plugins/timelines/common/index.ts b/x-pack/plugins/timelines/common/index.ts index 0002dd6eb14327..96728a07432fdb 100644 --- a/x-pack/plugins/timelines/common/index.ts +++ b/x-pack/plugins/timelines/common/index.ts @@ -20,7 +20,6 @@ export type { ActionProps, AlertWorkflowStatus, CellValueElementProps, - CreateFieldComponentType, ColumnId, ColumnRenderer, ColumnHeaderType, @@ -28,6 +27,7 @@ export type { ControlColumnProps, DataProvidersAnd, DataProvider, + FieldBrowserOptions, GenericActionRowCellRenderProps, HeaderActionProps, HeaderCellRender, diff --git a/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts b/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts index 0b83cf28f9bb7a..544ca033b060cc 100644 --- a/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts +++ b/x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts @@ -11,6 +11,7 @@ import type { IEsSearchRequest, IEsSearchResponse, FieldSpec, + RuntimeField, } from '../../../../../../src/plugins/data/common'; import type { DocValueFields, Maybe } from '../common'; @@ -71,6 +72,7 @@ export interface BrowserField { type: string; subType?: IFieldSubType; readFromDocValues: boolean; + runtimeField?: RuntimeField; } export type BrowserFields = Readonly>>; diff --git a/x-pack/plugins/timelines/common/types/fields_browser/index.ts b/x-pack/plugins/timelines/common/types/fields_browser/index.ts new file mode 100644 index 00000000000000..7aac02be877d21 --- /dev/null +++ b/x-pack/plugins/timelines/common/types/fields_browser/index.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 { EuiBasicTableColumn } from '@elastic/eui'; +import { BrowserFields } from '../../search_strategy'; +import { ColumnHeaderOptions } from '../timeline/columns'; + +/** + * An item rendered in the table + */ +export interface BrowserFieldItem { + name: string; + type?: string; + description?: string; + example?: string; + category: string; + selected: boolean; + isRuntime: boolean; +} + +export type OnFieldSelected = (fieldId: string) => void; + +export type CreateFieldComponent = React.FC<{ + onClick: () => void; +}>; +export type FieldTableColumns = Array>; +export type GetFieldTableColumns = (highlight: string) => FieldTableColumns; +export interface FieldBrowserOptions { + createFieldButton?: CreateFieldComponent; + getFieldTableColumns?: GetFieldTableColumns; +} + +export interface FieldBrowserProps { + /** The timeline associated with this field browser */ + timelineId: string; + /** The timeline's current column headers */ + columnHeaders: ColumnHeaderOptions[]; + /** A map of categoryId -> metadata about the fields in that category */ + browserFields: BrowserFields; + /** When true, this Fields Browser is being used as an "events viewer" */ + isEventViewer?: boolean; + /** The options to customize the field browser, supporting columns rendering and button to create fields */ + options?: FieldBrowserOptions; + /** The width of the field browser */ + width?: number; +} diff --git a/x-pack/plugins/timelines/common/types/index.ts b/x-pack/plugins/timelines/common/types/index.ts index 9464a33082a495..f8e2b030638606 100644 --- a/x-pack/plugins/timelines/common/types/index.ts +++ b/x-pack/plugins/timelines/common/types/index.ts @@ -5,4 +5,5 @@ * 2.0. */ +export * from './fields_browser'; export * from './timeline'; diff --git a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts index 0662c63f35eddb..6a9c6bf8e74a00 100644 --- a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts @@ -7,11 +7,12 @@ import { ComponentType, JSXElementConstructor } from 'react'; import { EuiDataGridControlColumn, EuiDataGridCellValueElementProps } from '@elastic/eui'; -import { CreateFieldComponentType, OnRowSelected, SortColumnTimeline, TimelineTabs } from '..'; +import { OnRowSelected, SortColumnTimeline, TimelineTabs } from '..'; import { BrowserFields } from '../../../search_strategy/index_fields'; import { ColumnHeaderOptions } from '../columns'; import { TimelineNonEcsData } from '../../../search_strategy'; import { Ecs } from '../../../ecs'; +import { FieldBrowserOptions } from '../../fields_browser'; export interface ActionProps { action?: RowCellRender; @@ -67,7 +68,7 @@ export interface HeaderActionProps { width: number; browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; - createFieldComponent?: CreateFieldComponentType; + fieldBrowserOptions?: FieldBrowserOptions; isEventViewer?: boolean; isSelectAllChecked: boolean; onSelectAll: ({ isSelected }: { isSelected: boolean }) => void; diff --git a/x-pack/plugins/timelines/common/types/timeline/index.ts b/x-pack/plugins/timelines/common/types/timeline/index.ts index 4ebc84a41f4b3f..a6c8ed1b74bfff 100644 --- a/x-pack/plugins/timelines/common/types/timeline/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/index.ts @@ -465,10 +465,6 @@ export enum TimelineTabs { eql = 'eql', } -export type CreateFieldComponentType = React.FC<{ - onClick: () => void; -}>; - // eslint-disable-next-line @typescript-eslint/no-explicit-any type EmptyObject = Partial>; diff --git a/x-pack/plugins/timelines/public/components/fields_browser/index.tsx b/x-pack/plugins/timelines/public/components/fields_browser/index.tsx index 31b8e9f62803ec..12133cbee303ea 100644 --- a/x-pack/plugins/timelines/public/components/fields_browser/index.tsx +++ b/x-pack/plugins/timelines/public/components/fields_browser/index.tsx @@ -9,9 +9,14 @@ import React from 'react'; import type { Store } from 'redux'; import { Provider } from 'react-redux'; import { I18nProvider } from '@kbn/i18n-react'; -import type { FieldBrowserProps } from '../t_grid/toolbar/fields_browser/types'; import { StatefulFieldsBrowser } from '../t_grid/toolbar/fields_browser'; -export type { FieldBrowserProps } from '../t_grid/toolbar/fields_browser/types'; +import { FieldBrowserProps } from '../../../common/types/fields_browser'; +export type { + CreateFieldComponent, + FieldBrowserOptions, + FieldBrowserProps, + GetFieldTableColumns, +} from '../../../common/types/fields_browser'; const EMPTY_BROWSER_FIELDS = {}; 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 2ae0146f80f7e5..4ba36a3ec6419b 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 @@ -47,7 +47,6 @@ import { TimelineTabs, SetEventsLoading, SetEventsDeleted, - CreateFieldComponentType, } from '../../../../common/types/timeline'; import type { TimelineItem, TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; @@ -63,10 +62,11 @@ import { import type { BrowserFields } from '../../../../common/search_strategy/index_fields'; import type { OnRowSelected, OnSelectAll } from '../types'; +import type { FieldBrowserOptions } from '../../../../common/types'; import type { Refetch } from '../../../store/t_grid/inputs'; import { getPageRowIndex } from '../../../../common/utils/pagination'; import { StatefulEventContext } from '../../../components/stateful_event_context'; -import { StatefulFieldsBrowser } from '../../../components/t_grid/toolbar/fields_browser'; +import { StatefulFieldsBrowser } from '../toolbar/fields_browser'; import { tGridActions, TGridModel, tGridSelectors, TimelineState } from '../../../store/t_grid'; import { useDeepEqualSelector } from '../../../hooks/use_selector'; import { RowAction } from './row_action'; @@ -88,10 +88,10 @@ interface OwnProps { appId?: string; browserFields: BrowserFields; bulkActions?: BulkActionsProp; - createFieldComponent?: CreateFieldComponentType; data: TimelineItem[]; defaultCellActions?: TGridCellAction[]; disabledCellActions: string[]; + fieldBrowserOptions?: FieldBrowserOptions; filters?: Filter[]; filterQuery?: string; filterStatus?: AlertStatus; @@ -149,8 +149,8 @@ const EuiDataGridContainer = styled.div<{ hideLastPage: boolean }>` const transformControlColumns = ({ columnHeaders, controlColumns, - createFieldComponent, data, + fieldBrowserOptions, isEventViewer = false, loadingEventIds, onRowSelected, @@ -171,9 +171,9 @@ const transformControlColumns = ({ }: { columnHeaders: ColumnHeaderOptions[]; controlColumns: ControlColumnProps[]; - createFieldComponent?: CreateFieldComponentType; data: TimelineItem[]; disabledCellActions: string[]; + fieldBrowserOptions?: FieldBrowserOptions; isEventViewer?: boolean; loadingEventIds: string[]; onRowSelected: OnRowSelected; @@ -209,6 +209,7 @@ const transformControlColumns = ({ )} @@ -303,10 +303,10 @@ export const BodyComponent = React.memo( bulkActions = true, clearSelected, columnHeaders, - createFieldComponent, data, defaultCellActions, disabledCellActions, + fieldBrowserOptions, filterQuery, filters, filterStatus, @@ -502,7 +502,7 @@ export const BodyComponent = React.memo( @@ -529,6 +529,7 @@ export const BodyComponent = React.memo( id, totalSelectAllAlerts, totalItems, + fieldBrowserOptions, filterStatus, filterQuery, indexNames, @@ -539,7 +540,6 @@ export const BodyComponent = React.memo( additionalControls, browserFields, columnHeaders, - createFieldComponent, ] ); @@ -629,9 +629,9 @@ export const BodyComponent = React.memo( transformControlColumns({ columnHeaders, controlColumns, - createFieldComponent, data, disabledCellActions, + fieldBrowserOptions, isEventViewer, loadingEventIds, onRowSelected, @@ -656,9 +656,9 @@ export const BodyComponent = React.memo( leadingControlColumns, trailingControlColumns, columnHeaders, - createFieldComponent, data, disabledCellActions, + fieldBrowserOptions, isEventViewer, id, loadingEventIds, 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 b97e4047d10e7e..69c04b31fa44be 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 @@ -21,7 +21,6 @@ import type { CoreStart } from '../../../../../../../src/core/public'; import type { BrowserFields } from '../../../../common/search_strategy/index_fields'; import { BulkActionsProp, - CreateFieldComponentType, TGridCellAction, TimelineId, TimelineTabs, @@ -43,6 +42,7 @@ import { defaultHeaders } from '../body/column_headers/default_headers'; import { buildCombinedQuery, getCombinedFilterQuery, resolverIsShowing } from '../helpers'; import { tGridActions, tGridSelectors } from '../../../store/t_grid'; import { useTimelineEvents, InspectResponse, Refetch } from '../../../container'; +import { FieldBrowserOptions } from '../../fields_browser'; import { StatefulBody } from '../body'; import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexGroup, UpdatedFlexItem } from '../styles'; import { Sort } from '../body/sort'; @@ -98,7 +98,6 @@ export interface TGridIntegratedProps { browserFields: BrowserFields; bulkActions?: BulkActionsProp; columns: ColumnHeaderOptions[]; - createFieldComponent?: CreateFieldComponentType; data?: DataPublicPluginStart; dataProviders: DataProvider[]; dataViewId?: string | null; @@ -108,6 +107,7 @@ export interface TGridIntegratedProps { docValueFields: DocValueFields[]; end: string; entityType: EntityType; + fieldBrowserOptions?: FieldBrowserOptions; filters: Filter[]; filterStatus?: AlertStatus; globalFullScreen: boolean; @@ -153,12 +153,12 @@ const TGridIntegratedComponent: React.FC = ({ docValueFields, end, entityType, + fieldBrowserOptions, filters, filterStatus, globalFullScreen, graphEventId, graphOverlay = null, - createFieldComponent, hasAlertsCrud, id, indexNames, @@ -363,10 +363,10 @@ const TGridIntegratedComponent: React.FC = ({ appId={appId} browserFields={browserFields} bulkActions={bulkActions} - createFieldComponent={createFieldComponent} data={nonDeletedEvents} defaultCellActions={defaultCellActions} disabledCellActions={disabledCellActions} + fieldBrowserOptions={fieldBrowserOptions} filterQuery={filterQuery} filters={filters} filterStatus={filterStatus} diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_badges.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_badges.test.tsx new file mode 100644 index 00000000000000..e945f91c47afda --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_badges.test.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render } from '@testing-library/react'; +import React from 'react'; +import { TestProviders } from '../../../../mock'; + +import { CategoriesBadges } from './categories_badges'; + +const mockSetSelectedCategoryIds = jest.fn(); +const defaultProps = { + setSelectedCategoryIds: mockSetSelectedCategoryIds, + selectedCategoryIds: [], +}; + +describe('CategoriesBadges', () => { + beforeEach(() => { + mockSetSelectedCategoryIds.mockClear(); + }); + + it('should render empty badges', () => { + const result = render( + + + + ); + + const badges = result.getByTestId('category-badges'); + expect(badges).toBeInTheDocument(); + expect(badges.childNodes.length).toBe(0); + }); + + it('should render the selector button with selected categories', () => { + const result = render( + + + + ); + + const badges = result.getByTestId('category-badges'); + expect(badges.childNodes.length).toBe(2); + expect(result.getByTestId('category-badge-base')).toBeInTheDocument(); + expect(result.getByTestId('category-badge-event')).toBeInTheDocument(); + }); + + it('should call the set selected callback when badge unselect button clicked', () => { + const result = render( + + + + ); + + result.getByTestId('category-badge-unselect-base').click(); + expect(mockSetSelectedCategoryIds).toHaveBeenCalledWith(['event']); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_badges.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_badges.tsx new file mode 100644 index 00000000000000..14b928d18de452 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_badges.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import styled from 'styled-components'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +interface CategoriesBadgesProps { + setSelectedCategoryIds: (categoryIds: string[]) => void; + selectedCategoryIds: string[]; +} + +const CategoriesBadgesGroup = styled(EuiFlexGroup)` + margin-top: ${({ theme }) => theme.eui.euiSizeXS}; + min-height: 24px; +`; +CategoriesBadgesGroup.displayName = 'CategoriesBadgesGroup'; + +const CategoriesBadgesComponent: React.FC = ({ + setSelectedCategoryIds, + selectedCategoryIds, +}) => { + const onUnselectCategory = useCallback( + (categoryId: string) => { + setSelectedCategoryIds( + selectedCategoryIds.filter((selectedCategoryId) => selectedCategoryId !== categoryId) + ); + }, + [setSelectedCategoryIds, selectedCategoryIds] + ); + + return ( + + {selectedCategoryIds.map((categoryId) => ( + + onUnselectCategory(categoryId)} + iconOnClickAriaLabel="unselect category" + data-test-subj={`category-badge-${categoryId}`} + closeButtonProps={{ 'data-test-subj': `category-badge-unselect-${categoryId}` }} + > + {categoryId} + + + ))} + + ); +}; + +export const CategoriesBadges = React.memo(CategoriesBadgesComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.test.tsx deleted file mode 100644 index e2f1d78cf5bc2b..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { mount } from 'enzyme'; -import React from 'react'; - -import { mockBrowserFields } from '../../../../mock'; - -import { CATEGORY_PANE_WIDTH } from './helpers'; -import { CategoriesPane } from './categories_pane'; -import * as i18n from './translations'; - -const timelineId = 'test'; - -describe('CategoriesPane', () => { - test('it renders the expected title', () => { - const wrapper = mount( - - ); - - expect(wrapper.find('[data-test-subj="categories-pane-title"]').first().text()).toEqual( - i18n.CATEGORIES - ); - }); - - test('it renders a "No fields match" message when filteredBrowserFields is empty', () => { - const wrapper = mount( - - ); - - expect(wrapper.find('[data-test-subj="categories-container"] tbody').first().text()).toEqual( - i18n.NO_FIELDS_MATCH - ); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.tsx deleted file mode 100644 index ffb93aee11b556..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.tsx +++ /dev/null @@ -1,118 +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 { EuiInMemoryTable, EuiTitle } from '@elastic/eui'; -import { noop } from 'lodash/fp'; -import React, { useCallback, useRef } from 'react'; -import styled from 'styled-components'; -import { - DATA_COLINDEX_ATTRIBUTE, - DATA_ROWINDEX_ATTRIBUTE, - onKeyDownFocusHandler, -} from '../../../../../common/utils/accessibility'; -import type { BrowserFields } from '../../../../../common/search_strategy'; -import { getCategoryColumns } from './category_columns'; -import { CATEGORIES_PANE_CLASS_NAME, TABLE_HEIGHT } from './helpers'; - -import * as i18n from './translations'; - -const CategoryNames = styled.div<{ height: number; width: number }>` - ${({ width }) => `width: ${width}px`}; - ${({ height }) => `height: ${height}px`}; - overflow-y: hidden; - padding: 5px; - thead { - display: none; - } -`; - -CategoryNames.displayName = 'CategoryNames'; - -const Title = styled(EuiTitle)` - padding-left: 5px; -`; - -const H3 = styled.h3` - text-align: left; -`; - -Title.displayName = 'Title'; - -interface Props { - /** - * A map of categoryId -> metadata about the fields in that category, - * filtered such that the name of every field in the category includes - * the filter input (as a substring). - */ - filteredBrowserFields: BrowserFields; - /** - * Invoked when the user clicks on the name of a category in the left-hand - * side of the field browser - */ - onCategorySelected: (categoryId: string) => void; - /** The category selected on the left-hand side of the field browser */ - selectedCategoryId: string; - timelineId: string; - /** The width of the categories pane */ - width: number; -} - -export const CategoriesPane = React.memo( - ({ filteredBrowserFields, onCategorySelected, selectedCategoryId, timelineId, width }) => { - const containerElement = useRef(null); - const onKeyDown = useCallback( - (e: React.KeyboardEvent) => { - onKeyDownFocusHandler({ - colindexAttribute: DATA_COLINDEX_ATTRIBUTE, - containerElement: containerElement?.current, - event: e, - maxAriaColindex: 1, - maxAriaRowindex: Object.keys(filteredBrowserFields).length, - onColumnFocused: noop, - rowindexAttribute: DATA_ROWINDEX_ATTRIBUTE, - }); - }, - [containerElement, filteredBrowserFields] - ); - - return ( - <> - - <H3 data-test-subj="categories-pane-title">{i18n.CATEGORIES}</H3> - - - - ({ categoryId, ariaRowindex: i + 1 }))} - message={i18n.NO_FIELDS_MATCH} - pagination={false} - sorting={false} - tableCaption={i18n.CATEGORIES} - /> - - - ); - } -); - -CategoriesPane.displayName = 'CategoriesPane'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_selector.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_selector.test.tsx new file mode 100644 index 00000000000000..eff37376a296e4 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_selector.test.tsx @@ -0,0 +1,92 @@ +/* + * 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 '@testing-library/react'; +import React from 'react'; +import { mockBrowserFields, TestProviders } from '../../../../mock'; + +import { CategoriesSelector } from './categories_selector'; + +const mockSetSelectedCategoryIds = jest.fn(); +const defaultProps = { + filteredBrowserFields: mockBrowserFields, + setSelectedCategoryIds: mockSetSelectedCategoryIds, + selectedCategoryIds: [], +}; + +describe('CategoriesSelector', () => { + beforeEach(() => { + mockSetSelectedCategoryIds.mockClear(); + }); + + it('should render the default selector button', () => { + const categoriesCount = Object.keys(mockBrowserFields).length; + const result = render( + + + + ); + + expect(result.getByTestId('categories-filter-button')).toBeInTheDocument(); + expect(result.getByText('Categories')).toBeInTheDocument(); + expect(result.getByText(categoriesCount)).toBeInTheDocument(); + }); + + it('should render the selector button with selected categories', () => { + const result = render( + + + + ); + + expect(result.getByTestId('categories-filter-button')).toBeInTheDocument(); + expect(result.getByText('Categories')).toBeInTheDocument(); + expect(result.getByText('2')).toBeInTheDocument(); + }); + + it('should open the category selector', () => { + const result = render( + + + + ); + + result.getByTestId('categories-filter-button').click(); + + expect(result.getByTestId('categories-selector-search')).toBeInTheDocument(); + expect(result.getByTestId(`categories-selector-option-base`)).toBeInTheDocument(); + }); + + it('should open the category selector with selected categories', () => { + const result = render( + + + + ); + + result.getByTestId('categories-filter-button').click(); + + expect(result.getByTestId('categories-selector-search')).toBeInTheDocument(); + expect(result.getByTestId(`categories-selector-option-base`)).toBeInTheDocument(); + expect(result.getByTestId(`categories-selector-option-name-base`)).toHaveStyleRule( + 'font-weight', + 'bold' + ); + }); + + it('should call setSelectedCategoryIds when category selected', () => { + const result = render( + + + + ); + + result.getByTestId('categories-filter-button').click(); + result.getByTestId(`categories-selector-option-base`).click(); + expect(mockSetSelectedCategoryIds).toHaveBeenCalledWith(['base']); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_selector.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_selector.tsx new file mode 100644 index 00000000000000..6aebd32543ea3e --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_selector.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo, useState } from 'react'; +import { omit } from 'lodash'; +import { + EuiFilterButton, + EuiFilterGroup, + EuiFlexGroup, + EuiFlexItem, + EuiHighlight, + EuiPopover, + EuiSelectable, + FilterChecked, +} from '@elastic/eui'; +import { BrowserFields } from '../../../../../common'; +import * as i18n from './translations'; +import { CountBadge, getFieldCount, CategoryName, CategorySelectableContainer } from './helpers'; +import { isEscape } from '../../../../../common/utils/accessibility'; + +interface CategoriesSelectorProps { + /** + * A map of categoryId -> metadata about the fields in that category, + * filtered such that the name of every field in the category includes + * the filter input (as a substring). + */ + filteredBrowserFields: BrowserFields; + /** + * Invoked when the user clicks on the name of a category in the left-hand + * side of the field browser + */ + setSelectedCategoryIds: (categoryIds: string[]) => void; + /** The category selected on the left-hand side of the field browser */ + selectedCategoryIds: string[]; +} + +interface CategoryOption { + label: string; + count: number; + checked?: FilterChecked; +} + +const renderOption = (option: CategoryOption, searchValue: string) => { + const { label, count, checked } = option; + // Some category names have spaces, but test selectors don't like spaces, + // Tests are not able to find subjects with spaces, so we need to clean them. + const idAttr = label.replace(/\s/g, ''); + return ( + + + + {label} + + + + {count} + + + ); +}; + +const CategoriesSelectorComponent: React.FC = ({ + filteredBrowserFields, + setSelectedCategoryIds, + selectedCategoryIds, +}) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const togglePopover = useCallback(() => { + setIsPopoverOpen((open) => !open); + }, []); + const closePopover = useCallback(() => { + setIsPopoverOpen(false); + }, []); + + const totalCategories = useMemo( + () => Object.keys(filteredBrowserFields).length, + [filteredBrowserFields] + ); + + const categoryOptions: CategoryOption[] = useMemo(() => { + const unselectedCategoryIds = Object.keys( + omit(filteredBrowserFields, selectedCategoryIds) + ).sort(); + return [ + ...selectedCategoryIds.map((categoryId) => ({ + label: categoryId, + count: getFieldCount(filteredBrowserFields[categoryId]), + checked: 'on', + })), + ...unselectedCategoryIds.map((categoryId) => ({ + label: categoryId, + count: getFieldCount(filteredBrowserFields[categoryId]), + })), + ]; + }, [selectedCategoryIds, filteredBrowserFields]); + + const onCategoriesChange = useCallback( + (options: CategoryOption[]) => { + setSelectedCategoryIds( + options.filter(({ checked }) => checked === 'on').map(({ label }) => label) + ); + }, + [setSelectedCategoryIds] + ); + + const onKeyDown = useCallback((keyboardEvent: React.KeyboardEvent) => { + if (isEscape(keyboardEvent)) { + // Prevent escape to close the field browser modal after closing the category selector + keyboardEvent.stopPropagation(); + } + }, []); + + return ( + + 0} + iconType="arrowDown" + isSelected={isPopoverOpen} + numActiveFilters={selectedCategoryIds.length} + numFilters={totalCategories} + onClick={togglePopover} + > + {i18n.CATEGORIES} + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + > + + + {(list, search) => ( + <> + {search} + {list} + + )} + + + + + ); +}; + +export const CategoriesSelector = React.memo(CategoriesSelectorComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.test.tsx deleted file mode 100644 index 98f02a9484eaba..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.test.tsx +++ /dev/null @@ -1,100 +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 { useMountAppended } from '../../../utils/use_mount_appended'; - -import { Category } from './category'; -import { getFieldItems } from './field_items'; -import { FIELDS_PANE_WIDTH } from './helpers'; -import { mockBrowserFields, TestProviders } from '../../../../mock'; - -import * as i18n from './translations'; - -describe('Category', () => { - const timelineId = 'test'; - const selectedCategoryId = 'client'; - const mount = useMountAppended(); - - test('it renders the category id as the value of the title', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="selected-category-title"]').first().text()).toEqual( - selectedCategoryId - ); - }); - - test('it renders the Field column header', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('.euiTableCellContent__text').at(1).text()).toEqual(i18n.FIELD); - }); - - test('it renders the Description column header', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('.euiTableCellContent__text').at(2).text()).toEqual(i18n.DESCRIPTION); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.tsx deleted file mode 100644 index 3130c46aa06843..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.tsx +++ /dev/null @@ -1,114 +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 { EuiInMemoryTable } from '@elastic/eui'; -import { noop } from 'lodash/fp'; -import React, { useCallback, useMemo, useRef } from 'react'; -import styled from 'styled-components'; -import { - arrayIndexToAriaIndex, - DATA_COLINDEX_ATTRIBUTE, - DATA_ROWINDEX_ATTRIBUTE, - onKeyDownFocusHandler, -} from '../../../../../common/utils/accessibility'; -import type { BrowserFields } from '../../../../../common/search_strategy'; -import type { OnUpdateColumns } from '../../../../../common/types'; - -import { CategoryTitle } from './category_title'; -import { getFieldColumns } from './field_items'; -import type { FieldItem } from './field_items'; -import { CATEGORY_TABLE_CLASS_NAME, TABLE_HEIGHT } from './helpers'; - -import * as i18n from './translations'; - -const TableContainer = styled.div<{ height: number; width: number }>` - ${({ height }) => `height: ${height}px`}; - ${({ width }) => `width: ${width}px`}; - overflow: hidden; -`; - -TableContainer.displayName = 'TableContainer'; - -/** - * This callback, invoked via `EuiInMemoryTable`'s `rowProps, assigns - * attributes to every ``. - */ -const getAriaRowindex = (fieldItem: FieldItem) => - fieldItem.ariaRowindex != null ? { 'data-rowindex': fieldItem.ariaRowindex } : {}; - -interface Props { - categoryId: string; - fieldItems: FieldItem[]; - filteredBrowserFields: BrowserFields; - onCategorySelected: (categoryId: string) => void; - onUpdateColumns: OnUpdateColumns; - timelineId: string; - width: number; -} - -export const Category = React.memo( - ({ categoryId, filteredBrowserFields, fieldItems, onUpdateColumns, timelineId, width }) => { - const containerElement = useRef(null); - const onKeyDown = useCallback( - (keyboardEvent: React.KeyboardEvent) => { - onKeyDownFocusHandler({ - colindexAttribute: DATA_COLINDEX_ATTRIBUTE, - containerElement: containerElement?.current, - event: keyboardEvent, - maxAriaColindex: 3, - maxAriaRowindex: fieldItems.length, - onColumnFocused: noop, - rowindexAttribute: DATA_ROWINDEX_ATTRIBUTE, - }); - }, - [fieldItems.length] - ); - - const fieldItemsWithRowindex = useMemo( - () => - fieldItems.map((fieldItem, i) => ({ - ...fieldItem, - ariaRowindex: arrayIndexToAriaIndex(i), - })), - [fieldItems] - ); - - const columns = useMemo(() => getFieldColumns(), []); - - return ( - <> - - - - - - - ); - } -); - -Category.displayName = 'Category'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.test.tsx deleted file mode 100644 index a94ffee597c791..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.test.tsx +++ /dev/null @@ -1,153 +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 { mount } from 'enzyme'; -import React from 'react'; - -import { mockBrowserFields, TestProviders } from '../../../../mock'; - -import { CATEGORY_PANE_WIDTH, getFieldCount, VIEW_ALL_BUTTON_CLASS_NAME } from './helpers'; -import { CategoriesPane } from './categories_pane'; -import { ViewAllButton } from './category_columns'; - -const timelineId = 'test'; - -describe('getCategoryColumns', () => { - Object.keys(mockBrowserFields).forEach((categoryId) => { - test(`it renders the ${categoryId} category name (from filteredBrowserFields)`, () => { - const wrapper = mount( - - ); - - const fieldCount = Object.keys(mockBrowserFields[categoryId].fields ?? {}).length; - - expect( - wrapper.find(`.field-browser-category-pane-${categoryId}-${timelineId}`).first().text() - ).toEqual(`${categoryId}${fieldCount}`); - }); - }); - - Object.keys(mockBrowserFields).forEach((categoryId) => { - test(`it renders the correct field count for the ${categoryId} category (from filteredBrowserFields)`, () => { - const wrapper = mount( - - ); - - expect( - wrapper.find(`[data-test-subj="${categoryId}-category-count"]`).first().text() - ).toEqual(`${getFieldCount(mockBrowserFields[categoryId])}`); - }); - }); - - test('it renders the selected category with bold text', () => { - const selectedCategoryId = 'auditd'; - - const wrapper = mount( - - ); - - expect( - wrapper - .find(`.field-browser-category-pane-${selectedCategoryId}-${timelineId}`) - .find('[data-test-subj="categoryName"]') - .at(1) - ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); - }); - - test('it does NOT render an un-selected category with bold text', () => { - const selectedCategoryId = 'auditd'; - const notTheSelectedCategoryId = 'base'; - - const wrapper = mount( - - ); - - expect( - wrapper - .find(`.field-browser-category-pane-${notTheSelectedCategoryId}-${timelineId}`) - .find('[data-test-subj="categoryName"]') - .at(1) - ).toHaveStyleRule('font-weight', 'normal', { modifier: '.euiText' }); - }); - - test('it invokes onCategorySelected when a user clicks a category', () => { - const selectedCategoryId = 'auditd'; - const notTheSelectedCategoryId = 'base'; - - const onCategorySelected = jest.fn(); - - const wrapper = mount( - - ); - - wrapper - .find(`.field-browser-category-pane-${notTheSelectedCategoryId}-${timelineId}`) - .first() - .simulate('click'); - - expect(onCategorySelected).toHaveBeenCalledWith(notTheSelectedCategoryId); - }); -}); - -describe('ViewAllButton', () => { - it(`should update fields with the timestamp and category fields`, () => { - const onUpdateColumns = jest.fn(); - - const wrapper = mount( - - - - ); - - wrapper.find(`.${VIEW_ALL_BUTTON_CLASS_NAME}`).first().simulate('click'); - - expect(onUpdateColumns).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining({ id: '@timestamp' }), - expect.objectContaining({ id: 'agent.ephemeral_id' }), - expect.objectContaining({ id: 'agent.hostname' }), - expect.objectContaining({ id: 'agent.id' }), - expect.objectContaining({ id: 'agent.name' }), - ]) - ); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.tsx deleted file mode 100644 index 0fdf71ff5ffe1a..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.tsx +++ /dev/null @@ -1,157 +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 { - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiLink, - EuiText, - EuiToolTip, -} from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; -import styled from 'styled-components'; - -import { useDeepEqualSelector } from '../../../../hooks/use_selector'; -import { - LoadingSpinner, - getCategoryPaneCategoryClassName, - getFieldCount, - VIEW_ALL_BUTTON_CLASS_NAME, - CountBadge, -} from './helpers'; -import * as i18n from './translations'; -import { tGridSelectors } from '../../../../store/t_grid'; -import { getColumnsWithTimestamp } from '../../../utils/helpers'; -import type { BrowserFields } from '../../../../../common/search_strategy'; -import type { OnUpdateColumns } from '../../../../../common/types'; - -const CategoryName = styled.span<{ bold: boolean }>` - .euiText { - font-weight: ${({ bold }) => (bold ? 'bold' : 'normal')}; - } -`; - -CategoryName.displayName = 'CategoryName'; - -const LinkContainer = styled.div` - width: 100%; - .euiLink { - width: 100%; - } -`; - -LinkContainer.displayName = 'LinkContainer'; - -const ViewAll = styled(EuiButtonIcon)` - margin-left: 2px; -`; - -ViewAll.displayName = 'ViewAll'; - -export interface CategoryItem { - categoryId: string; -} - -interface ViewAllButtonProps { - categoryId: string; - browserFields: BrowserFields; - onUpdateColumns: OnUpdateColumns; - timelineId: string; -} - -export const ViewAllButton = React.memo( - ({ categoryId, browserFields, onUpdateColumns, timelineId }) => { - const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []); - const { isLoading } = useDeepEqualSelector((state) => - getManageTimeline(state, timelineId ?? '') - ); - - const handleClick = useCallback(() => { - onUpdateColumns( - getColumnsWithTimestamp({ - browserFields, - category: categoryId, - }) - ); - }, [browserFields, categoryId, onUpdateColumns]); - - return ( - - {!isLoading ? ( - - ) : ( - - )} - - ); - } -); - -ViewAllButton.displayName = 'ViewAllButton'; - -/** - * Returns the column definition for the (single) column that displays all the - * category names in the field browser */ -export const getCategoryColumns = ({ - filteredBrowserFields, - onCategorySelected, - selectedCategoryId, - timelineId, -}: { - filteredBrowserFields: BrowserFields; - onCategorySelected: (categoryId: string) => void; - selectedCategoryId: string; - timelineId: string; -}) => [ - { - field: 'categoryId', - name: '', - sortable: true, - truncateText: false, - render: ( - categoryId: string, - { ariaRowindex }: { categoryId: string; ariaRowindex: number } - ) => ( - - onCategorySelected(categoryId)} - > - - - - {categoryId} - - - - - - {getFieldCount(filteredBrowserFields[categoryId])} - - - - - - ), - }, -]; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.test.tsx deleted file mode 100644 index 746668491abb84..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.test.tsx +++ /dev/null @@ -1,72 +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 { mount } from 'enzyme'; -import React from 'react'; - -import { mockBrowserFields, TestProviders } from '../../../../mock'; - -import { CategoryTitle } from './category_title'; -import { getFieldCount } from './helpers'; - -describe('CategoryTitle', () => { - const timelineId = 'test'; - - test('it renders the category id as the value of the title', () => { - const categoryId = 'client'; - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="selected-category-title"]').first().text()).toEqual( - categoryId - ); - }); - - test('when `categoryId` specifies a valid category in `filteredBrowserFields`, a count of the field is displayed in the badge', () => { - const validCategoryId = 'client'; - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="selected-category-count-badge"]`).first().text()).toEqual( - `${getFieldCount(mockBrowserFields[validCategoryId])}` - ); - }); - - test('when `categoryId` specifies an INVALID category in `filteredBrowserFields`, a count of zero is displayed in the badge', () => { - const invalidCategoryId = 'this.is.not.happening'; - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="selected-category-count-badge"]`).first().text()).toEqual( - '0' - ); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.tsx deleted file mode 100644 index 0858f30a352463..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.tsx +++ /dev/null @@ -1,67 +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 { EuiFlexGroup, EuiFlexItem, EuiScreenReaderOnly, EuiTitle } from '@elastic/eui'; -import React from 'react'; - -import { CountBadge, getFieldBrowserCategoryTitleClassName, getFieldCount } from './helpers'; -import type { BrowserFields } from '../../../../../common/search_strategy'; -import type { OnUpdateColumns } from '../../../../../common/types'; - -import { ViewAllButton } from './category_columns'; -import * as i18n from './translations'; - -interface Props { - /** The title of the category */ - categoryId: string; - /** - * A map of categoryId -> metadata about the fields in that category, - * filtered such that the name of every field in the category includes - * the filter input (as a substring). - */ - filteredBrowserFields: BrowserFields; - onUpdateColumns: OnUpdateColumns; - - /** The timeline associated with this field browser */ - timelineId: string; -} - -export const CategoryTitle = React.memo( - ({ filteredBrowserFields, categoryId, onUpdateColumns, timelineId }) => ( - - - -

{i18n.CATEGORY}

-
- -

{categoryId}

-
-
- - - - {getFieldCount(filteredBrowserFields[categoryId])} - - - - - - -
- ) -); - -CategoryTitle.displayName = 'CategoryTitle'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx index d435d7a280840b..ed665155ddcf52 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.test.tsx @@ -34,18 +34,20 @@ const testProps = { searchInput: '', appliedFilterInput: '', isSearching: false, - onCategorySelected: jest.fn(), + setSelectedCategoryIds: jest.fn(), onHide, onSearchInputChange: jest.fn(), restoreFocusTo: React.createRef(), - selectedCategoryId: '', + selectedCategoryIds: [], timelineId, }; const { storage } = createSecuritySolutionStorageMock(); + describe('FieldsBrowser', () => { beforeEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); + test('it renders the Close button', () => { const wrapper = mount( @@ -80,20 +82,7 @@ describe('FieldsBrowser', () => { test('it invokes updateColumns action when the user clicks the Reset Fields button', () => { const wrapper = mount( - ()} - selectedCategoryId={''} - timelineId={timelineId} - /> + ); @@ -129,24 +118,24 @@ describe('FieldsBrowser', () => { expect(wrapper.find('[data-test-subj="field-search"]').exists()).toBe(true); }); - test('it renders the categories pane', () => { + test('it renders the categories selector', () => { const wrapper = mount( ); - expect(wrapper.find('[data-test-subj="left-categories-pane"]').exists()).toBe(true); + expect(wrapper.find('[data-test-subj="categories-selector"]').exists()).toBe(true); }); - test('it renders the fields pane', () => { + test('it renders the fields table', () => { const wrapper = mount( ); - expect(wrapper.find('[data-test-subj="fields-pane"]').exists()).toBe(true); + expect(wrapper.find('[data-test-subj="field-table"]').exists()).toBe(true); }); test('focuses the search input when the component mounts', () => { @@ -183,19 +172,24 @@ describe('FieldsBrowser', () => { expect(onSearchInputChange).toBeCalledWith(inputText); }); - test('does not render the CreateField button when createFieldComponent is provided without a dataViewId', () => { + test('does not render the CreateFieldButton when it is provided but does not have a dataViewId', () => { const MyTestComponent = () =>
{'test'}
; const wrapper = mount( - + ); expect(wrapper.find(MyTestComponent).exists()).toBeFalsy(); }); - test('it renders the CreateField button when createFieldComponent is provided with a dataViewId', () => { + test('it renders the CreateFieldButton when it is provided and have a dataViewId', () => { const state: State = { ...mockGlobalState, timelineById: { @@ -212,7 +206,12 @@ describe('FieldsBrowser', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx index e55f54e946ad13..5a01c820aa9619 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx @@ -17,51 +17,27 @@ import { EuiButtonEmpty, EuiSpacer, } from '@elastic/eui'; -import React, { useEffect, useCallback, useRef, useMemo } from 'react'; -import styled from 'styled-components'; +import React, { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import type { BrowserFields } from '../../../../../common/search_strategy'; -import type { ColumnHeaderOptions, CreateFieldComponentType } from '../../../../../common/types'; -import { - isEscape, - isTab, - stopPropagationAndPreventDefault, -} from '../../../../../common/utils/accessibility'; -import { CategoriesPane } from './categories_pane'; -import { FieldsPane } from './fields_pane'; +import type { FieldBrowserProps, ColumnHeaderOptions } from '../../../../../common/types'; import { Search } from './search'; -import { - CATEGORY_PANE_WIDTH, - CLOSE_BUTTON_CLASS_NAME, - FIELDS_PANE_WIDTH, - FIELD_BROWSER_WIDTH, - focusSearchInput, - onFieldsBrowserTabPressed, - PANES_FLEX_GROUP_WIDTH, - RESET_FIELDS_CLASS_NAME, - scrollCategoriesPane, -} from './helpers'; -import type { FieldBrowserProps } from './types'; +import { CLOSE_BUTTON_CLASS_NAME, FIELD_BROWSER_WIDTH, RESET_FIELDS_CLASS_NAME } from './helpers'; import { tGridActions, tGridSelectors } from '../../../../store/t_grid'; import * as i18n from './translations'; import { useDeepEqualSelector } from '../../../../hooks/use_selector'; +import { CategoriesSelector } from './categories_selector'; +import { FieldTable } from './field_table'; +import { CategoriesBadges } from './categories_badges'; -const PanesFlexGroup = styled(EuiFlexGroup)` - width: ${PANES_FLEX_GROUP_WIDTH}px; -`; -PanesFlexGroup.displayName = 'PanesFlexGroup'; - -type Props = Pick & { +type Props = Pick & { /** * The current timeline column headers */ columnHeaders: ColumnHeaderOptions[]; - - createFieldComponent?: CreateFieldComponentType; - /** * A map of categoryId -> metadata about the fields in that category, * filtered such that the name of every field in the category includes @@ -80,12 +56,12 @@ type Props = Pick & /** * The category selected on the left-hand side of the field browser */ - selectedCategoryId: string; + selectedCategoryIds: string[]; /** * Invoked when the user clicks on the name of a category in the left-hand * side of the field browser */ - onCategorySelected: (categoryId: string) => void; + setSelectedCategoryIds: (categoryIds: string[]) => void; /** * Hides the field browser when invoked */ @@ -110,23 +86,23 @@ type Props = Pick & const FieldsBrowserComponent: React.FC = ({ columnHeaders, filteredBrowserFields, - createFieldComponent: CreateField, isSearching, - onCategorySelected, + setSelectedCategoryIds, onSearchInputChange, onHide, + options, restoreFocusTo, searchInput, appliedFilterInput, - selectedCategoryId, + selectedCategoryIds, timelineId, width = FIELD_BROWSER_WIDTH, }) => { const dispatch = useDispatch(); - const containerElement = useRef(null); const onUpdateColumns = useCallback( - (columns) => dispatch(tGridActions.updateColumns({ id: timelineId, columns })), + (columns: ColumnHeaderOptions[]) => + dispatch(tGridActions.updateColumns({ id: timelineId, columns })), [dispatch, timelineId] ); @@ -156,45 +132,14 @@ const FieldsBrowserComponent: React.FC = ({ [onSearchInputChange] ); - const scrollViewsAndFocusInput = useCallback(() => { - scrollCategoriesPane({ - containerElement: containerElement.current, - selectedCategoryId, - timelineId, - }); - - // always re-focus the input to enable additional filtering - focusSearchInput({ - containerElement: containerElement.current, - timelineId, - }); - }, [selectedCategoryId, timelineId]); - - useEffect(() => { - scrollViewsAndFocusInput(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedCategoryId, timelineId]); - - const onKeyDown = useCallback( - (keyboardEvent: React.KeyboardEvent) => { - if (isEscape(keyboardEvent)) { - stopPropagationAndPreventDefault(keyboardEvent); - closeAndRestoreFocus(); - } else if (isTab(keyboardEvent)) { - onFieldsBrowserTabPressed({ - containerElement: containerElement.current, - keyboardEvent, - selectedCategoryId, - timelineId, - }); - } - }, - [closeAndRestoreFocus, containerElement, selectedCategoryId, timelineId] - ); + const [CreateFieldButton, getFieldTableColumns] = [ + options?.createFieldButton, + options?.getFieldTableColumns, + ]; return ( -
+

{i18n.FIELDS_BROWSER}

@@ -202,11 +147,10 @@ const FieldsBrowserComponent: React.FC = ({
- + = ({ /> - {CreateField && dataViewId != null && dataViewId.length > 0 && ( - + + + + {CreateFieldButton && dataViewId != null && dataViewId.length > 0 && ( + )} + + - - - - - - - - + diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.test.tsx index a4c830c3d8808a..45b122354528b4 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.test.tsx @@ -5,21 +5,17 @@ * 2.0. */ -import { omit } from 'lodash/fp'; import React from 'react'; -import { waitFor } from '@testing-library/react'; -import { mockBrowserFields, TestProviders } from '../../../../mock'; +import { omit } from 'lodash/fp'; +import { render } from '@testing-library/react'; +import { EuiInMemoryTable } from '@elastic/eui'; +import { mockBrowserFields } from '../../../../mock'; import { defaultColumnHeaderType } from '../../body/column_headers/default_headers'; import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../../body/constants'; -import { Category } from './category'; import { getFieldColumns, getFieldItems } from './field_items'; -import { FIELDS_PANE_WIDTH } from './helpers'; -import { useMountAppended } from '../../../utils/use_mount_appended'; import { ColumnHeaderOptions } from '../../../../../common/types'; -const selectedCategoryId = 'base'; -const selectedCategoryFields = mockBrowserFields[selectedCategoryId].fields; const timestampFieldId = '@timestamp'; const columnHeaders: ColumnHeaderOptions[] = [ { @@ -28,7 +24,7 @@ const columnHeaders: ColumnHeaderOptions[] = [ description: 'Date/time when the event originated.\nFor log events this is the date/time when the event was generated, and not when it was read.\nRequired field for all events.', example: '2016-05-23T08:05:34.853Z', - id: '@timestamp', + id: timestampFieldId, type: 'date', aggregatable: true, initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH, @@ -36,295 +32,199 @@ const columnHeaders: ColumnHeaderOptions[] = [ ]; describe('field_items', () => { - const timelineId = 'test'; - const mount = useMountAppended(); - describe('getFieldItems', () => { - Object.keys(selectedCategoryFields!).forEach((fieldId) => { - test(`it renders the name of the ${fieldId} field`, () => { - const wrapper = mount( - - - - ); + const timestampField = mockBrowserFields.base.fields![timestampFieldId]; - expect(wrapper.find(`[data-test-subj="field-name-${fieldId}"]`).first().text()).toEqual( - fieldId - ); + it('should return browser field item format', () => { + const fieldItems = getFieldItems({ + selectedCategoryIds: ['base'], + browserFields: { base: { fields: { [timestampFieldId]: timestampField } } }, + columnHeaders: [], }); - }); - Object.keys(selectedCategoryFields!).forEach((fieldId) => { - test(`it renders a checkbox for the ${fieldId} field`, () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="field-${fieldId}-checkbox"]`).first().exists()).toBe( - true - ); + expect(fieldItems[0]).toEqual({ + name: timestampFieldId, + description: timestampField.description, + category: 'base', + selected: false, + type: timestampField.type, + example: timestampField.example, + isRuntime: false, }); }); - test('it renders a checkbox in the checked state when the field is selected to be displayed as a column in the timeline', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find(`[data-test-subj="field-${timestampFieldId}-checkbox"]`).first().props() - .checked - ).toBe(true); - }); - - test('it does NOT render a checkbox in the checked state when the field is NOT selected to be displayed as a column in the timeline', () => { - const wrapper = mount( - - header.id !== timestampFieldId), - highlight: '', - timelineId, - toggleColumn: jest.fn(), - })} - width={FIELDS_PANE_WIDTH} - onCategorySelected={jest.fn()} - onUpdateColumns={jest.fn()} - timelineId={timelineId} - /> - - ); - - expect( - wrapper.find(`[data-test-subj="field-${timestampFieldId}-checkbox"]`).first().props() - .checked - ).toBe(false); - }); - - test('it invokes `toggleColumn` when the user interacts with the checkbox', () => { - const toggleColumn = jest.fn(); - - const wrapper = mount( - - - - ); - - wrapper - .find('input[type="checkbox"]') - .first() - .simulate('change', { - target: { checked: true }, - }); - wrapper.update(); + it('should return selected item', () => { + const fieldItems = getFieldItems({ + selectedCategoryIds: ['base'], + browserFields: { base: { fields: { [timestampFieldId]: timestampField } } }, + columnHeaders, + }); - expect(toggleColumn).toBeCalledWith({ - columnHeaderType: 'not-filtered', - id: '@timestamp', - initialWidth: 180, + expect(fieldItems[0]).toMatchObject({ + selected: true, }); }); - test('it returns the expected signal column settings', async () => { - const mockSelectedCategoryId = 'signal'; - const mockBrowserFieldsWithSignal = { - ...mockBrowserFields, - signal: { - fields: { - 'signal.rule.name': { - aggregatable: true, - category: 'signal', - description: 'rule name', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'signal.rule.name', - searchable: true, - type: 'string', + it('should return isRuntime field', () => { + const fieldItems = getFieldItems({ + selectedCategoryIds: ['base'], + browserFields: { + base: { + fields: { + [timestampFieldId]: { + ...timestampField, + runtimeField: { type: 'keyword', script: { source: 'scripts are fun' } }, + }, }, }, }, - }; - const toggleColumn = jest.fn(); - const wrapper = mount( - - - - ); - wrapper - .find(`[data-test-subj="field-signal.rule.name-checkbox"]`) - .last() - .simulate('change', { - target: { checked: true }, - }); + columnHeaders, + }); - await waitFor(() => { - expect(toggleColumn).toBeCalledWith({ - columnHeaderType: 'not-filtered', - id: 'signal.rule.name', - initialWidth: 180, - }); + expect(fieldItems[0]).toMatchObject({ + isRuntime: true, }); }); - test('it renders the expected icon for a field', () => { - const wrapper = mount( - - - + it('should return all field items of all categories if no category selected', () => { + const fieldCount = Object.values(mockBrowserFields).reduce( + (total, { fields }) => total + Object.keys(fields ?? {}).length, + 0 ); - expect( - wrapper.find(`[data-test-subj="field-${timestampFieldId}-icon"]`).first().props().type - ).toEqual('clock'); + const fieldItems = getFieldItems({ + selectedCategoryIds: [], + browserFields: mockBrowserFields, + columnHeaders: [], + }); + + expect(fieldItems.length).toBe(fieldCount); }); - test('it renders the expected field description', () => { - const wrapper = mount( - - - + it('should return filtered field items of selected categories', () => { + const selectedCategoryIds = ['base', 'event']; + const fieldCount = selectedCategoryIds.reduce( + (total, selectedCategoryId) => + total + Object.keys(mockBrowserFields[selectedCategoryId].fields ?? {}).length, + 0 ); - expect( - wrapper.find(`[data-test-subj="field-${timestampFieldId}-description"]`).first().text() - ).toEqual( - 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events. Example: 2016-05-23T08:05:34.853Z' - ); + const fieldItems = getFieldItems({ + selectedCategoryIds, + browserFields: mockBrowserFields, + columnHeaders: [], + }); + + expect(fieldItems.length).toBe(fieldCount); }); }); describe('getFieldColumns', () => { - test('it returns the expected column definitions', () => { - expect(getFieldColumns().map((column) => omit('render', column))).toEqual([ + const onToggleColumn = jest.fn(); + + beforeEach(() => { + onToggleColumn.mockClear(); + }); + + it('should return default field columns', () => { + expect(getFieldColumns({ onToggleColumn }).map((column) => omit('render', column))).toEqual([ { - field: 'checkbox', + field: 'selected', name: '', sortable: false, width: '25px', }, - { field: 'field', name: 'Field', sortable: false, width: '225px' }, + { + field: 'name', + name: 'Name', + sortable: true, + width: '225px', + }, { field: 'description', name: 'Description', + sortable: true, + width: '400px', + }, + { + field: 'category', + name: 'Category', + sortable: true, + width: '100px', + }, + ]); + }); + + it('should return custom field columns', () => { + const customColumns = [ + { + field: 'name', + name: 'customColumn1', sortable: false, - truncateText: true, + width: '225px', + }, + { + field: 'description', + name: 'customColumn2', + sortable: true, width: '400px', }, + ]; + + expect( + getFieldColumns({ + onToggleColumn, + getFieldTableColumns: () => customColumns, + }).map((column) => omit('render', column)) + ).toEqual([ + { + field: 'selected', + name: '', + sortable: false, + width: '25px', + }, + ...customColumns, ]); }); + + it('should render default columns', () => { + const timestampField = mockBrowserFields.base.fields![timestampFieldId]; + const fieldItems = getFieldItems({ + selectedCategoryIds: ['base'], + browserFields: { base: { fields: { [timestampFieldId]: timestampField } } }, + columnHeaders: [], + }); + + const columns = getFieldColumns({ onToggleColumn }); + const { getByTestId, getAllByText } = render( + + ); + + expect(getAllByText('Name').at(0)).toBeInTheDocument(); + expect(getAllByText('Description').at(0)).toBeInTheDocument(); + expect(getAllByText('Category').at(0)).toBeInTheDocument(); + + expect(getByTestId(`field-${timestampFieldId}-checkbox`)).toBeInTheDocument(); + expect(getByTestId(`field-${timestampFieldId}-name`)).toBeInTheDocument(); + expect(getByTestId(`field-${timestampFieldId}-description`)).toBeInTheDocument(); + expect(getByTestId(`field-${timestampFieldId}-category`)).toBeInTheDocument(); + }); + + it('should call call toggle callback on checkbox click', () => { + const timestampField = mockBrowserFields.base.fields![timestampFieldId]; + const fieldItems = getFieldItems({ + selectedCategoryIds: ['base'], + browserFields: { base: { fields: { [timestampFieldId]: timestampField } } }, + columnHeaders: [], + }); + + const columns = getFieldColumns({ onToggleColumn }); + const { getByTestId } = render( + + ); + + getByTestId(`field-${timestampFieldId}-checkbox`).click(); + expect(onToggleColumn).toHaveBeenCalledWith(timestampFieldId); + }); }); }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.tsx index a979e209bf64aa..1e066eb2174a53 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.tsx @@ -13,14 +13,22 @@ import { EuiFlexGroup, EuiFlexItem, EuiScreenReaderOnly, + EuiBadge, + EuiBasicTableColumn, + EuiTableActionsColumnType, } from '@elastic/eui'; import { uniqBy } from 'lodash/fp'; import styled from 'styled-components'; import { getEmptyValue } from '../../../empty_value'; import { getExampleText, getIconFromType } from '../../../utils/helpers'; -import type { BrowserField } from '../../../../../common/search_strategy'; -import type { ColumnHeaderOptions } from '../../../../../common/types'; +import type { BrowserFields } from '../../../../../common/search_strategy'; +import type { + ColumnHeaderOptions, + BrowserFieldItem, + FieldTableColumns, + GetFieldTableColumns, +} from '../../../../../common/types'; import { defaultColumnHeaderType } from '../../body/column_headers/default_headers'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../../body/constants'; import { TruncatableText } from '../../../truncatable_text'; @@ -33,125 +41,155 @@ const TypeIcon = styled(EuiIcon)` position: relative; top: -1px; `; - TypeIcon.displayName = 'TypeIcon'; export const Description = styled.span` user-select: text; width: 400px; `; - Description.displayName = 'Description'; /** - * An item rendered in the table - */ -export interface FieldItem { - ariaRowindex?: number; - checkbox: React.ReactNode; - description: React.ReactNode; - field: React.ReactNode; - fieldId: string; -} - -/** - * Returns the fields items, values, and descriptions shown when a user expands an event + * Returns the field items of all categories selected */ export const getFieldItems = ({ - category, + browserFields, + selectedCategoryIds, columnHeaders, - highlight = '', - timelineId, - toggleColumn, }: { - category: Partial; + browserFields: BrowserFields; + selectedCategoryIds: string[]; columnHeaders: ColumnHeaderOptions[]; - highlight?: string; - timelineId: string; - toggleColumn: (column: ColumnHeaderOptions) => void; -}): FieldItem[] => - uniqBy('name', [ - ...Object.values(category != null && category.fields != null ? category.fields : {}), - ]).map((field) => ({ - checkbox: ( - - c.id === field.name) !== -1} - data-test-subj={`field-${field.name}-checkbox`} - data-colindex={1} - id={field.name ?? ''} - onChange={() => - toggleColumn({ - columnHeaderType: defaultColumnHeaderType, - id: field.name ?? '', - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - ...getAlertColumnHeader(timelineId, field.name ?? ''), - }) - } - /> - - ), - field: ( - - - - - - +}): BrowserFieldItem[] => { + const categoryIds = + selectedCategoryIds.length > 0 ? selectedCategoryIds : Object.keys(browserFields); + const selectedFieldIds = new Set(columnHeaders.map(({ id }) => id)); - - - - + return uniqBy( + 'name', + categoryIds.reduce((fieldItems, categoryId) => { + const categoryBrowserFields = Object.values(browserFields[categoryId]?.fields ?? {}); + if (categoryBrowserFields.length > 0) { + fieldItems.push( + ...categoryBrowserFields.map(({ name = '', ...field }) => ({ + name, + type: field.type, + description: field.description ?? '', + example: field.example?.toString(), + category: categoryId, + selected: selectedFieldIds.has(name), + isRuntime: !!field.runtimeField, + })) + ); + } + return fieldItems; + }, []) + ); +}; + +/** + * Returns the column header for a field + */ +export const getColumnHeader = (timelineId: string, fieldName: string): ColumnHeaderOptions => ({ + columnHeaderType: defaultColumnHeaderType, + id: fieldName, + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + ...getAlertColumnHeader(timelineId, fieldName), +}); + +const getDefaultFieldTableColumns = (highlight: string): FieldTableColumns => [ + { + field: 'name', + name: i18n.NAME, + render: (name: string, { type }) => { + return ( + + + + + + + + + + + + ); + }, + sortable: true, + width: '225px', + }, + { + field: 'description', + name: i18n.DESCRIPTION, + render: (description: string, { name, example }) => ( + + <> + +

{i18n.DESCRIPTION_FOR_FIELD(name)}

+
+ + + {`${description ?? getEmptyValue()} ${getExampleText(example)}`} + + + +
), - description: ( -
- - <> - -

{i18n.DESCRIPTION_FOR_FIELD(field.name ?? '')}

-
- - - {`${field.description ?? getEmptyValue()} ${getExampleText(field.example)}`} - - - -
-
+ sortable: true, + width: '400px', + }, + { + field: 'category', + name: i18n.CATEGORY, + render: (category: string, { name }) => ( + {category} ), - fieldId: field.name ?? '', - })); + sortable: true, + width: '100px', + }, +]; /** * Returns a table column template provided to the `EuiInMemoryTable`'s * `columns` prop */ -export const getFieldColumns = () => [ +export const getFieldColumns = ({ + onToggleColumn, + highlight = '', + getFieldTableColumns, +}: { + onToggleColumn: (id: string) => void; + highlight?: string; + getFieldTableColumns?: GetFieldTableColumns; +}): FieldTableColumns => [ { - field: 'checkbox', + field: 'selected', name: '', - render: (checkbox: React.ReactNode, _: FieldItem) => checkbox, + render: (selected: boolean, { name }) => ( + + onToggleColumn(name)} + /> + + ), sortable: false, width: '25px', }, - { - field: 'field', - name: i18n.FIELD, - render: (field: React.ReactNode, _: FieldItem) => field, - sortable: false, - width: '225px', - }, - { - field: 'description', - name: i18n.DESCRIPTION, - render: (description: React.ReactNode, _: FieldItem) => description, - sortable: false, - truncateText: true, - width: '400px', - }, + ...(getFieldTableColumns + ? getFieldTableColumns(highlight) + : getDefaultFieldTableColumns(highlight)), ]; + +/** Returns whether the table column has actions attached to it */ +export const isActionsColumn = (column: EuiBasicTableColumn): boolean => { + return !!(column as EuiTableActionsColumnType).actions?.length; +}; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.test.tsx index 05f093eaf1805d..6bda5873edc257 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.test.tsx @@ -43,7 +43,7 @@ describe('FieldName', () => { ); expect( - wrapper.find(`[data-test-subj="field-name-${timestampFieldId}"]`).first().text() + wrapper.find(`[data-test-subj="field-${timestampFieldId}-name"]`).first().text() ).toEqual(timestampFieldId); }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.tsx index 5781211058d3c3..0ef0ce64c637b8 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_name.tsx @@ -15,7 +15,7 @@ export const FieldName = React.memo<{ }>(({ fieldId, highlight = '' }) => { return ( - + {fieldId} diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.test.tsx new file mode 100644 index 00000000000000..14f2151d240747 --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.test.tsx @@ -0,0 +1,225 @@ +/* + * 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 { render } from '@testing-library/react'; +import { mockBrowserFields, TestProviders } from '../../../../mock'; +import { tGridActions } from '../../../../store/t_grid'; +import { defaultColumnHeaderType } from '../../body/column_headers/default_headers'; +import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../../body/constants'; + +import { ColumnHeaderOptions } from '../../../../../common'; +import { FieldTable } from './field_table'; + +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + +const timestampFieldId = '@timestamp'; + +const columnHeaders: ColumnHeaderOptions[] = [ + { + category: 'base', + columnHeaderType: defaultColumnHeaderType, + description: + 'Date/time when the event originated.\nFor log events this is the date/time when the event was generated, and not when it was read.\nRequired field for all events.', + example: '2016-05-23T08:05:34.853Z', + id: timestampFieldId, + type: 'date', + aggregatable: true, + initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH, + }, +]; + +describe('FieldTable', () => { + const timelineId = 'test'; + const timestampField = mockBrowserFields.base.fields![timestampFieldId]; + const defaultPageSize = 10; + const totalFields = Object.values(mockBrowserFields).reduce( + (total, { fields }) => total + Object.keys(fields ?? {}).length, + 0 + ); + + beforeEach(() => { + mockDispatch.mockClear(); + }); + + it('should render empty field table', () => { + const result = render( + + + + ); + + expect(result.getByText('No items found')).toBeInTheDocument(); + expect(result.getByTestId('fields-count').textContent).toContain('0'); + }); + + it('should render field table with fields of all categories', () => { + const result = render( + + + + ); + + expect(result.container.getElementsByClassName('euiTableRow').length).toBe(defaultPageSize); + expect(result.getByTestId('fields-count').textContent).toContain(totalFields); + }); + + it('should render field table with fields of categories selected', () => { + const selectedCategoryIds = ['client', 'event']; + + const fieldCount = selectedCategoryIds.reduce( + (total, selectedCategoryId) => + total + Object.keys(mockBrowserFields[selectedCategoryId].fields ?? {}).length, + 0 + ); + + const result = render( + + + + ); + + expect(result.container.getElementsByClassName('euiTableRow').length).toBe(fieldCount); + expect(result.getByTestId('fields-count').textContent).toContain(fieldCount); + }); + + it('should render field table with custom columns', () => { + const fieldTableColumns = [ + { + field: 'name', + name: 'Custom column', + render: () =>
, + }, + ]; + + const result = render( + + fieldTableColumns} + selectedCategoryIds={[]} + columnHeaders={[]} + filteredBrowserFields={mockBrowserFields} + searchInput="" + timelineId={timelineId} + /> + + ); + + expect(result.getByTestId('fields-count').textContent).toContain(totalFields); + expect(result.getAllByText('Custom column').length).toBeGreaterThan(0); + expect(result.getAllByTestId('customColumn').length).toEqual(defaultPageSize); + }); + + it('should render field table with unchecked field', () => { + const result = render( + + + + ); + + const checkbox = result.getByTestId(`field-${timestampFieldId}-checkbox`); + expect(checkbox).not.toHaveAttribute('checked'); + }); + + it('should render field table with checked field', () => { + const result = render( + + + + ); + + const checkbox = result.getByTestId(`field-${timestampFieldId}-checkbox`); + expect(checkbox).toHaveAttribute('checked'); + }); + + it('should dispatch remove column action on field unchecked', () => { + const result = render( + + + + ); + + result.getByTestId(`field-${timestampFieldId}-checkbox`).click(); + + expect(mockDispatch).toHaveBeenCalledTimes(1); + expect(mockDispatch).toHaveBeenCalledWith( + tGridActions.removeColumn({ id: timelineId, columnId: timestampFieldId }) + ); + }); + + it('should dispatch upsert column action on field checked', () => { + const result = render( + + + + ); + + result.getByTestId(`field-${timestampFieldId}-checkbox`).click(); + + expect(mockDispatch).toHaveBeenCalledTimes(1); + expect(mockDispatch).toHaveBeenCalledWith( + tGridActions.upsertColumn({ + id: timelineId, + column: { + columnHeaderType: defaultColumnHeaderType, + id: timestampFieldId, + initialWidth: DEFAULT_COLUMN_MIN_WIDTH, + }, + index: 1, + }) + ); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.tsx new file mode 100644 index 00000000000000..332422ed664f6c --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_table.tsx @@ -0,0 +1,126 @@ +/* + * 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, { useCallback, useMemo } from 'react'; +import styled from 'styled-components'; +import { EuiInMemoryTable, EuiText } from '@elastic/eui'; +import { useDispatch } from 'react-redux'; +import { BrowserFields, ColumnHeaderOptions } from '../../../../../common'; +import * as i18n from './translations'; +import { getColumnHeader, getFieldColumns, getFieldItems, isActionsColumn } from './field_items'; +import { CATEGORY_TABLE_CLASS_NAME, TABLE_HEIGHT } from './helpers'; +import { tGridActions } from '../../../../store/t_grid'; +import type { GetFieldTableColumns } from '../../../../../common/types/fields_browser'; + +interface FieldTableProps { + timelineId: string; + columnHeaders: ColumnHeaderOptions[]; + /** + * A map of categoryId -> metadata about the fields in that category, + * filtered such that the name of every field in the category includes + * the filter input (as a substring). + */ + filteredBrowserFields: BrowserFields; + /** + * Optional function to customize field table columns + */ + getFieldTableColumns?: GetFieldTableColumns; + /** + * The category selected on the left-hand side of the field browser + */ + selectedCategoryIds: string[]; + /** The text displayed in the search input */ + /** Invoked when a user chooses to view a new set of columns in the timeline */ + searchInput: string; +} + +const TableContainer = styled.div<{ height: number }>` + margin-top: ${({ theme }) => theme.eui.euiSizeXS}; + border-top: ${({ theme }) => theme.eui.euiBorderThin}; + ${({ height }) => `height: ${height}px`}; + overflow: hidden; +`; +TableContainer.displayName = 'TableContainer'; + +const Count = styled.span` + font-weight: bold; +`; +Count.displayName = 'Count'; + +const FieldTableComponent: React.FC = ({ + columnHeaders, + filteredBrowserFields, + getFieldTableColumns, + searchInput, + selectedCategoryIds, + timelineId, +}) => { + const dispatch = useDispatch(); + + const fieldItems = useMemo( + () => + getFieldItems({ + browserFields: filteredBrowserFields, + selectedCategoryIds, + columnHeaders, + }), + [columnHeaders, filteredBrowserFields, selectedCategoryIds] + ); + + const onToggleColumn = useCallback( + (fieldId: string) => { + if (columnHeaders.some(({ id }) => id === fieldId)) { + dispatch( + tGridActions.removeColumn({ + columnId: fieldId, + id: timelineId, + }) + ); + } else { + dispatch( + tGridActions.upsertColumn({ + column: getColumnHeader(timelineId, fieldId), + id: timelineId, + index: 1, + }) + ); + } + }, + [columnHeaders, dispatch, timelineId] + ); + + const columns = useMemo( + () => getFieldColumns({ highlight: searchInput, onToggleColumn, getFieldTableColumns }), + [onToggleColumn, searchInput, getFieldTableColumns] + ); + const hasActions = useMemo(() => columns.some((column) => isActionsColumn(column)), [columns]); + + return ( + <> + + {i18n.FIELDS_SHOWING} + {fieldItems.length} + {i18n.FIELDS_COUNT(fieldItems.length)} + + + + + + + ); +}; + +export const FieldTable = React.memo(FieldTableComponent); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.test.tsx deleted file mode 100644 index aec21b48471362..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.test.tsx +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { useMountAppended } from '../../../utils/use_mount_appended'; -import { mockBrowserFields, TestProviders } from '../../../../mock'; - -import { FIELDS_PANE_WIDTH } from './helpers'; -import { FieldsPane } from './fields_pane'; - -const timelineId = 'test'; - -describe('FieldsPane', () => { - const mount = useMountAppended(); - - test('it renders the selected category', () => { - const selectedCategory = 'auditd'; - - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="selected-category-title"]`).first().text()).toEqual( - selectedCategory - ); - }); - - test('it renders a unknown category that does not exist in filteredBrowserFields', () => { - const selectedCategory = 'unknown'; - - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="selected-category-title"]`).first().text()).toEqual( - selectedCategory - ); - }); - - test('it renders the expected message when `filteredBrowserFields` is empty and `searchInput` is empty', () => { - const searchInput = ''; - - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="no-fields-match"]`).first().text()).toEqual( - 'No fields match ' - ); - }); - - test('it renders the expected message when `filteredBrowserFields` is empty and `searchInput` is an unknown field name', () => { - const searchInput = 'thisFieldDoesNotExist'; - - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="no-fields-match"]`).first().text()).toEqual( - `No fields match ${searchInput}` - ); - }); -}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx deleted file mode 100644 index d1d0254d0c917d..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx +++ /dev/null @@ -1,145 +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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; -import styled from 'styled-components'; -import { useDispatch } from 'react-redux'; - -import { Category } from './category'; -import { getFieldItems } from './field_items'; -import { FIELDS_PANE_WIDTH, TABLE_HEIGHT } from './helpers'; - -import * as i18n from './translations'; -import type { BrowserFields } from '../../../../../common/search_strategy'; -import type { ColumnHeaderOptions, OnUpdateColumns } from '../../../../../common/types'; -import { tGridActions } from '../../../../store/t_grid'; - -const NoFieldsPanel = styled.div` - background-color: ${(props) => props.theme.eui.euiColorLightestShade}; - width: ${FIELDS_PANE_WIDTH}px; - height: ${TABLE_HEIGHT}px; -`; - -NoFieldsPanel.displayName = 'NoFieldsPanel'; - -const NoFieldsFlexGroup = styled(EuiFlexGroup)` - height: 100%; -`; - -NoFieldsFlexGroup.displayName = 'NoFieldsFlexGroup'; - -interface Props { - timelineId: string; - columnHeaders: ColumnHeaderOptions[]; - /** - * A map of categoryId -> metadata about the fields in that category, - * filtered such that the name of every field in the category includes - * the filter input (as a substring). - */ - filteredBrowserFields: BrowserFields; - /** - * Invoked when the user clicks on the name of a category in the left-hand - * side of the field browser - */ - onCategorySelected: (categoryId: string) => void; - /** The text displayed in the search input */ - /** Invoked when a user chooses to view a new set of columns in the timeline */ - onUpdateColumns: OnUpdateColumns; - searchInput: string; - /** - * The category selected on the left-hand side of the field browser - */ - selectedCategoryId: string; - /** The width field browser */ - width: number; -} -export const FieldsPane = React.memo( - ({ - columnHeaders, - filteredBrowserFields, - onCategorySelected, - onUpdateColumns, - searchInput, - selectedCategoryId, - timelineId, - width, - }) => { - const dispatch = useDispatch(); - - const toggleColumn = useCallback( - (column: ColumnHeaderOptions) => { - if (columnHeaders.some((c) => c.id === column.id)) { - dispatch( - tGridActions.removeColumn({ - columnId: column.id, - id: timelineId, - }) - ); - } else { - dispatch( - tGridActions.upsertColumn({ - column, - id: timelineId, - index: 1, - }) - ); - } - }, - [columnHeaders, dispatch, timelineId] - ); - - const filteredBrowserFieldsExists = useMemo( - () => Object.keys(filteredBrowserFields).length > 0, - [filteredBrowserFields] - ); - - const fieldItems = useMemo(() => { - return getFieldItems({ - category: filteredBrowserFields[selectedCategoryId], - columnHeaders, - highlight: searchInput, - timelineId, - toggleColumn, - }); - }, [ - columnHeaders, - filteredBrowserFields, - searchInput, - selectedCategoryId, - timelineId, - toggleColumn, - ]); - - if (filteredBrowserFieldsExists) { - return ( - - ); - } - - return ( - - - -

{i18n.NO_FIELDS_MATCH_INPUT(searchInput)}

-
-
-
- ); - } -); - -FieldsPane.displayName = 'FieldsPane'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx index 239d7c726e286b..ad90956013e41d 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx @@ -10,45 +10,12 @@ import { mockBrowserFields } from '../../../../mock'; import { categoryHasFields, createVirtualCategory, - getCategoryPaneCategoryClassName, - getFieldBrowserCategoryTitleClassName, - getFieldBrowserSearchInputClassName, getFieldCount, filterBrowserFieldsByFieldName, } from './helpers'; import { BrowserFields } from '../../../../../common/search_strategy'; -const timelineId = 'test'; - describe('helpers', () => { - describe('getCategoryPaneCategoryClassName', () => { - test('it returns the expected class name', () => { - const categoryId = 'auditd'; - - expect(getCategoryPaneCategoryClassName({ categoryId, timelineId })).toEqual( - 'field-browser-category-pane-auditd-test' - ); - }); - }); - - describe('getFieldBrowserCategoryTitleClassName', () => { - test('it returns the expected class name', () => { - const categoryId = 'auditd'; - - expect(getFieldBrowserCategoryTitleClassName({ categoryId, timelineId })).toEqual( - 'field-browser-category-title-auditd-test' - ); - }); - }); - - describe('getFieldBrowserSearchInputClassName', () => { - test('it returns the expected class name', () => { - expect(getFieldBrowserSearchInputClassName(timelineId)).toEqual( - 'field-browser-search-input-test' - ); - }); - }); - describe('categoryHasFields', () => { test('it returns false if the category fields property is undefined', () => { expect(categoryHasFields({})).toBe(false); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx index 5406940aab3e9c..21829bda265e1f 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx @@ -9,11 +9,6 @@ import { EuiBadge, EuiLoadingSpinner } from '@elastic/eui'; import { filter, get, pickBy } from 'lodash/fp'; import styled from 'styled-components'; -import { - elementOrChildrenHasFocus, - skipFocusInContainerTo, - stopPropagationAndPreventDefault, -} from '../../../../../common/utils/accessibility'; import { TimelineId } from '../../../../../public/types'; import type { BrowserField, BrowserFields } from '../../../../../common/search_strategy'; import { defaultHeaders } from '../../../../store/t_grid/defaults'; @@ -27,44 +22,8 @@ export const LoadingSpinner = styled(EuiLoadingSpinner)` LoadingSpinner.displayName = 'LoadingSpinner'; -export const CATEGORY_PANE_WIDTH = 200; -export const DESCRIPTION_COLUMN_WIDTH = 300; -export const FIELD_COLUMN_WIDTH = 200; export const FIELD_BROWSER_WIDTH = 925; -export const FIELDS_PANE_WIDTH = 670; -export const HEADER_HEIGHT = 40; -export const PANES_FLEX_GROUP_WIDTH = CATEGORY_PANE_WIDTH + FIELDS_PANE_WIDTH + 10; -export const PANES_FLEX_GROUP_HEIGHT = 260; export const TABLE_HEIGHT = 260; -export const TYPE_COLUMN_WIDTH = 50; - -/** - * Returns the CSS class name for the title of a category shown in the left - * side field browser - */ -export const getCategoryPaneCategoryClassName = ({ - categoryId, - timelineId, -}: { - categoryId: string; - timelineId: string; -}): string => `field-browser-category-pane-${categoryId}-${timelineId}`; - -/** - * Returns the CSS class name for the title of a category shown in the right - * side of field browser - */ -export const getFieldBrowserCategoryTitleClassName = ({ - categoryId, - timelineId, -}: { - categoryId: string; - timelineId: string; -}): string => `field-browser-category-title-${categoryId}-${timelineId}`; - -/** Returns the class name for a field browser search input */ -export const getFieldBrowserSearchInputClassName = (timelineId: string): string => - `field-browser-search-input-${timelineId}`; /** Returns true if the specified category has at least one field */ export const categoryHasFields = (category: Partial): boolean => @@ -160,272 +119,22 @@ export const getAlertColumnHeader = (timelineId: string, fieldId: string) => ? defaultHeaders.find((c) => c.id === fieldId) ?? {} : {}; -export const CATEGORIES_PANE_CLASS_NAME = 'categories-pane'; export const CATEGORY_TABLE_CLASS_NAME = 'category-table'; export const CLOSE_BUTTON_CLASS_NAME = 'close-button'; export const RESET_FIELDS_CLASS_NAME = 'reset-fields'; -export const VIEW_ALL_BUTTON_CLASS_NAME = 'view-all'; - -export const categoriesPaneHasFocus = (containerElement: HTMLElement | null): boolean => - elementOrChildrenHasFocus( - containerElement?.querySelector(`.${CATEGORIES_PANE_CLASS_NAME}`) - ); - -export const categoryTableHasFocus = (containerElement: HTMLElement | null): boolean => - elementOrChildrenHasFocus( - containerElement?.querySelector(`.${CATEGORY_TABLE_CLASS_NAME}`) - ); - -export const closeButtonHasFocus = (containerElement: HTMLElement | null): boolean => - elementOrChildrenHasFocus( - containerElement?.querySelector(`.${CLOSE_BUTTON_CLASS_NAME}`) - ); - -export const searchInputHasFocus = ({ - containerElement, - timelineId, -}: { - containerElement: HTMLElement | null; - timelineId: string; -}): boolean => - elementOrChildrenHasFocus( - containerElement?.querySelector( - `.${getFieldBrowserSearchInputClassName(timelineId)}` - ) - ); - -export const viewAllHasFocus = (containerElement: HTMLElement | null): boolean => - elementOrChildrenHasFocus( - containerElement?.querySelector(`.${VIEW_ALL_BUTTON_CLASS_NAME}`) - ); - -export const resetButtonHasFocus = (containerElement: HTMLElement | null): boolean => - elementOrChildrenHasFocus( - containerElement?.querySelector(`.${RESET_FIELDS_CLASS_NAME}`) - ); - -export const scrollCategoriesPane = ({ - containerElement, - selectedCategoryId, - timelineId, -}: { - containerElement: HTMLElement | null; - selectedCategoryId: string; - timelineId: string; -}) => { - if (selectedCategoryId !== '') { - const selectedCategories = - containerElement?.getElementsByClassName( - getCategoryPaneCategoryClassName({ - categoryId: selectedCategoryId, - timelineId, - }) - ) ?? []; - - if (selectedCategories.length > 0) { - selectedCategories[0].scrollIntoView(); - } - } -}; - -export const focusCategoriesPane = ({ - containerElement, - selectedCategoryId, - timelineId, -}: { - containerElement: HTMLElement | null; - selectedCategoryId: string; - timelineId: string; -}) => { - if (selectedCategoryId !== '') { - const selectedCategories = - containerElement?.getElementsByClassName( - getCategoryPaneCategoryClassName({ - categoryId: selectedCategoryId, - timelineId, - }) - ) ?? []; - - if (selectedCategories.length > 0) { - (selectedCategories[0] as HTMLButtonElement).focus(); - } - } -}; - -export const focusCategoryTable = (containerElement: HTMLElement | null) => { - const firstEntry = containerElement?.querySelector( - `.${CATEGORY_TABLE_CLASS_NAME} [data-colindex="1"]` - ); - - if (firstEntry != null) { - firstEntry.focus(); - } else { - skipFocusInContainerTo({ - containerElement, - className: CATEGORY_TABLE_CLASS_NAME, - }); - } -}; - -export const focusCloseButton = (containerElement: HTMLElement | null) => - skipFocusInContainerTo({ - containerElement, - className: CLOSE_BUTTON_CLASS_NAME, - }); - -export const focusResetFieldsButton = (containerElement: HTMLElement | null) => - skipFocusInContainerTo({ containerElement, className: RESET_FIELDS_CLASS_NAME }); - -export const focusSearchInput = ({ - containerElement, - timelineId, -}: { - containerElement: HTMLElement | null; - timelineId: string; -}) => - skipFocusInContainerTo({ - containerElement, - className: getFieldBrowserSearchInputClassName(timelineId), - }); - -export const focusViewAllButton = (containerElement: HTMLElement | null) => - skipFocusInContainerTo({ containerElement, className: VIEW_ALL_BUTTON_CLASS_NAME }); - -export const onCategoriesPaneFocusChanging = ({ - containerElement, - shiftKey, - timelineId, -}: { - containerElement: HTMLElement | null; - shiftKey: boolean; - timelineId: string; -}) => - shiftKey - ? focusSearchInput({ - containerElement, - timelineId, - }) - : focusViewAllButton(containerElement); - -export const onCategoryTableFocusChanging = ({ - containerElement, - shiftKey, -}: { - containerElement: HTMLElement | null; - shiftKey: boolean; -}) => (shiftKey ? focusViewAllButton(containerElement) : focusResetFieldsButton(containerElement)); - -export const onCloseButtonFocusChanging = ({ - containerElement, - shiftKey, - timelineId, -}: { - containerElement: HTMLElement | null; - shiftKey: boolean; - timelineId: string; -}) => - shiftKey - ? focusResetFieldsButton(containerElement) - : focusSearchInput({ containerElement, timelineId }); - -export const onSearchInputFocusChanging = ({ - containerElement, - selectedCategoryId, - shiftKey, - timelineId, -}: { - containerElement: HTMLElement | null; - selectedCategoryId: string; - shiftKey: boolean; - timelineId: string; -}) => - shiftKey - ? focusCloseButton(containerElement) - : focusCategoriesPane({ containerElement, selectedCategoryId, timelineId }); - -export const onViewAllFocusChanging = ({ - containerElement, - selectedCategoryId, - shiftKey, - timelineId, -}: { - containerElement: HTMLElement | null; - selectedCategoryId: string; - shiftKey: boolean; - timelineId: string; -}) => - shiftKey - ? focusCategoriesPane({ containerElement, selectedCategoryId, timelineId }) - : focusCategoryTable(containerElement); - -export const onResetButtonFocusChanging = ({ - containerElement, - shiftKey, -}: { - containerElement: HTMLElement | null; - shiftKey: boolean; -}) => (shiftKey ? focusCategoryTable(containerElement) : focusCloseButton(containerElement)); - -export const onFieldsBrowserTabPressed = ({ - containerElement, - keyboardEvent, - selectedCategoryId, - timelineId, -}: { - containerElement: HTMLElement | null; - keyboardEvent: React.KeyboardEvent; - selectedCategoryId: string; - timelineId: string; -}) => { - const { shiftKey } = keyboardEvent; - - if (searchInputHasFocus({ containerElement, timelineId })) { - stopPropagationAndPreventDefault(keyboardEvent); - onSearchInputFocusChanging({ - containerElement, - selectedCategoryId, - shiftKey, - timelineId, - }); - } else if (categoriesPaneHasFocus(containerElement)) { - stopPropagationAndPreventDefault(keyboardEvent); - onCategoriesPaneFocusChanging({ - containerElement, - shiftKey, - timelineId, - }); - } else if (viewAllHasFocus(containerElement)) { - stopPropagationAndPreventDefault(keyboardEvent); - onViewAllFocusChanging({ - containerElement, - selectedCategoryId, - shiftKey, - timelineId, - }); - } else if (categoryTableHasFocus(containerElement)) { - stopPropagationAndPreventDefault(keyboardEvent); - onCategoryTableFocusChanging({ - containerElement, - shiftKey, - }); - } else if (resetButtonHasFocus(containerElement)) { - stopPropagationAndPreventDefault(keyboardEvent); - onResetButtonFocusChanging({ - containerElement, - shiftKey, - }); - } else if (closeButtonHasFocus(containerElement)) { - stopPropagationAndPreventDefault(keyboardEvent); - onCloseButtonFocusChanging({ - containerElement, - shiftKey, - timelineId, - }); - } -}; export const CountBadge = styled(EuiBadge)` margin-left: 5px; ` as unknown as typeof EuiBadge; CountBadge.displayName = 'CountBadge'; + +export const CategoryName = styled.span<{ bold: boolean }>` + font-weight: ${({ bold }) => (bold ? 'bold' : 'normal')}; +`; +CategoryName.displayName = 'CategoryName'; + +export const CategorySelectableContainer = styled.div` + width: 300px; +`; +CategorySelectableContainer.displayName = 'CategorySelectableContainer'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.test.tsx index b8bc2a12ffd6e8..7db742fd11302c 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.test.tsx @@ -5,9 +5,8 @@ * 2.0. */ -import { mount } from 'enzyme'; import React from 'react'; -import { waitFor } from '@testing-library/react'; +import { act, fireEvent, render, waitFor } from '@testing-library/react'; import { mockBrowserFields, TestProviders } from '../../../../mock'; @@ -18,12 +17,8 @@ import { StatefulFieldsBrowserComponent } from '.'; describe('StatefulFieldsBrowser', () => { const timelineId = 'test'; - beforeEach(() => { - window.HTMLElement.prototype.scrollIntoView = jest.fn(); - }); - - test('it renders the Fields button, which displays the fields browser on click', () => { - const wrapper = mount( + it('should render the Fields button, which displays the fields browser on click', () => { + const result = render( { ); - expect(wrapper.find('[data-test-subj="show-field-browser"]').exists()).toBe(true); + expect(result.getByTestId('show-field-browser')).toBeInTheDocument(); }); describe('toggleShow', () => { - test('it does NOT render the fields browser until the Fields button is clicked', () => { - const wrapper = mount( + it('should NOT render the fields browser until the Fields button is clicked', () => { + const result = render( { ); - expect(wrapper.find('[data-test-subj="fields-browser-container"]').exists()).toBe(false); + expect(result.queryByTestId('fields-browser-container')).toBeNull(); }); - test('it renders the fields browser when the Fields button is clicked', () => { - const wrapper = mount( + it('should render the fields browser when the Fields button is clicked', async () => { + const result = render( { /> ); - - wrapper.find('[data-test-subj="show-field-browser"]').first().simulate('click'); - - expect(wrapper.find('[data-test-subj="fields-browser-container"]').exists()).toBe(true); + result.getByTestId('show-field-browser').click(); + await waitFor(() => { + expect(result.getByTestId('fields-browser-container')).toBeInTheDocument(); + }); }); }); - describe('updateSelectedCategoryId', () => { - beforeEach(() => { - jest.useFakeTimers(); - }); - test('it updates the selectedCategoryId state, which makes the category bold, when the user clicks a category name in the left hand side of the field browser', async () => { - const wrapper = mount( + describe('updateSelectedCategoryIds', () => { + it('should add a selected category, which creates the category badge', async () => { + const result = render( { ); - wrapper.find('[data-test-subj="show-field-browser"]').first().simulate('click'); + result.getByTestId('show-field-browser').click(); + await waitFor(() => { + expect(result.getByTestId('fields-browser-container')).toBeInTheDocument(); + }); + + await act(async () => { + result.getByTestId('categories-filter-button').click(); + }); + await act(async () => { + result.getByTestId('categories-selector-option-base').click(); + }); + + expect(result.getByTestId('category-badge-base')).toBeInTheDocument(); + }); + + it('should remove a selected category, which deletes the category badge', async () => { + const result = render( + + + + ); - wrapper.find(`.field-browser-category-pane-auditd-${timelineId}`).first().simulate('click'); + result.getByTestId('show-field-browser').click(); await waitFor(() => { - wrapper.update(); - expect( - wrapper - .find(`.field-browser-category-pane-auditd-${timelineId}`) - .find('[data-test-subj="categoryName"]') - .at(1) - ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); + expect(result.getByTestId('fields-browser-container')).toBeInTheDocument(); }); + + await act(async () => { + result.getByTestId('categories-filter-button').click(); + }); + await act(async () => { + result.getByTestId('categories-selector-option-base').click(); + }); + expect(result.getByTestId('category-badge-base')).toBeInTheDocument(); + + await act(async () => { + result.getByTestId('category-badge-unselect-base').click(); + }); + expect(result.queryByTestId('category-badge-base')).toBeNull(); }); - test('it updates the selectedCategoryId state according to most fields returned', async () => { - const wrapper = mount( + it('should update the available categories according to the search input', async () => { + const result = render( { ); + result.getByTestId('show-field-browser').click(); await waitFor(() => { - wrapper.find('[data-test-subj="show-field-browser"]').first().simulate('click'); - jest.runOnlyPendingTimers(); - wrapper.update(); - - expect( - wrapper - .find(`.field-browser-category-pane-cloud-${timelineId}`) - .find('[data-test-subj="categoryName"]') - .at(1) - ).toHaveStyleRule('font-weight', 'normal', { modifier: '.euiText' }); + expect(result.getByTestId('fields-browser-container')).toBeInTheDocument(); }); + result.getByTestId('categories-filter-button').click(); + expect(result.getByTestId('categories-selector-option-base')).toBeInTheDocument(); + + fireEvent.change(result.getByTestId('field-search'), { target: { value: 'client' } }); await waitFor(() => { - wrapper - .find('[data-test-subj="field-search"]') - .last() - .simulate('change', { target: { value: 'cloud' } }); - - jest.runOnlyPendingTimers(); - wrapper.update(); - expect( - wrapper - .find(`.field-browser-category-pane-cloud-${timelineId}`) - .find('[data-test-subj="categoryName"]') - .at(1) - ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); + expect(result.queryByTestId('categories-selector-option-base')).toBeNull(); }); + expect(result.queryByTestId('categories-selector-option-client')).toBeInTheDocument(); }); }); - test('it renders the Fields Browser button as a settings gear when the isEventViewer prop is true', () => { + it('should render the Fields Browser button as a settings gear when the isEventViewer prop is true', () => { const isEventViewer = true; - const wrapper = mount( + const result = render( { ); - expect(wrapper.find('[data-test-subj="show-field-browser"]').first().exists()).toBe(true); + expect(result.getByTestId('show-field-browser')).toBeInTheDocument(); }); - test('it renders the Fields Browser button as a settings gear when the isEventViewer prop is false', () => { - const isEventViewer = true; + it('should render the Fields Browser button as a settings gear when the isEventViewer prop is false', () => { + const isEventViewer = false; - const wrapper = mount( + const result = render( { ); - expect(wrapper.find('[data-test-subj="show-field-browser"]').first().exists()).toBe(true); + expect(result.getByTestId('show-field-browser')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.tsx index 13549e2d5be109..c5647c973b9d8c 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/index.tsx @@ -6,15 +6,15 @@ */ import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; +import { debounce } from 'lodash'; import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react'; import styled from 'styled-components'; import type { BrowserFields } from '../../../../../common/search_strategy/index_fields'; -import { DEFAULT_CATEGORY_NAME } from '../../body/column_headers/default_headers'; +import type { FieldBrowserProps } from '../../../../../common/types/fields_browser'; import { FieldsBrowser } from './field_browser'; import { filterBrowserFieldsByFieldName, mergeBrowserFieldsWithDefaultCategory } from './helpers'; import * as i18n from './translations'; -import type { FieldBrowserProps } from './types'; const FIELDS_BUTTON_CLASS_NAME = 'fields-button'; @@ -34,26 +34,48 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ timelineId, columnHeaders, browserFields, - createFieldComponent, + options, width, }) => { const customizeColumnsButtonRef = useRef(null); - /** tracks the latest timeout id from `setTimeout`*/ - const inputTimeoutId = useRef(0); - /** all field names shown in the field browser must contain this string (when specified) */ const [filterInput, setFilterInput] = useState(''); - + /** debounced filterInput, the one that is applied to the filteredBrowserFields */ const [appliedFilterInput, setAppliedFilterInput] = useState(''); /** all fields in this collection have field names that match the filterInput */ const [filteredBrowserFields, setFilteredBrowserFields] = useState(null); /** when true, show a spinner in the input to indicate the field browser is searching for matching field names */ const [isSearching, setIsSearching] = useState(false); /** this category will be displayed in the right-hand pane of the field browser */ - const [selectedCategoryId, setSelectedCategoryId] = useState(DEFAULT_CATEGORY_NAME); + const [selectedCategoryIds, setSelectedCategoryIds] = useState([]); /** show the field browser */ const [show, setShow] = useState(false); + // debounced function to apply the input filter + // will delay the call to setAppliedFilterInput by INPUT_TIMEOUT ms + // the parameter used will be the last one passed + const debouncedApplyFilterInput = useMemo( + () => + debounce((input: string) => { + setAppliedFilterInput(input); + }, INPUT_TIMEOUT), + [] + ); + useEffect(() => { + return () => { + debouncedApplyFilterInput.cancel(); + }; + }, [debouncedApplyFilterInput]); + + useEffect(() => { + const newFilteredBrowserFields = filterBrowserFieldsByFieldName({ + browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields), + substring: appliedFilterInput, + }); + setFilteredBrowserFields(newFilteredBrowserFields); + setIsSearching(false); + }, [appliedFilterInput, browserFields]); + /** Shows / hides the field browser */ const onShow = useCallback(() => { setShow(true); @@ -65,65 +87,19 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ setAppliedFilterInput(''); setFilteredBrowserFields(null); setIsSearching(false); - setSelectedCategoryId(DEFAULT_CATEGORY_NAME); + setSelectedCategoryIds([]); setShow(false); }, []); - const newFilteredBrowserFields = useMemo(() => { - return filterBrowserFieldsByFieldName({ - browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields), - substring: appliedFilterInput, - }); - }, [appliedFilterInput, browserFields]); - - const newSelectedCategoryId = useMemo(() => { - if (appliedFilterInput === '' || Object.keys(newFilteredBrowserFields).length === 0) { - return DEFAULT_CATEGORY_NAME; - } else { - return Object.keys(newFilteredBrowserFields) - .sort() - .reduce((selected, category) => { - const filteredBrowserFieldsByCategory = - (newFilteredBrowserFields[category] && newFilteredBrowserFields[category].fields) || []; - const filteredBrowserFieldsBySelected = - (newFilteredBrowserFields[selected] && newFilteredBrowserFields[selected].fields) || []; - return newFilteredBrowserFields[category].fields != null && - newFilteredBrowserFields[selected].fields != null && - Object.keys(filteredBrowserFieldsByCategory).length > - Object.keys(filteredBrowserFieldsBySelected).length - ? category - : selected; - }, Object.keys(newFilteredBrowserFields)[0]); - } - }, [appliedFilterInput, newFilteredBrowserFields]); - /** Invoked when the user types in the filter input */ - const updateFilter = useCallback((newFilterInput: string) => { - setFilterInput(newFilterInput); - setIsSearching(true); - }, []); - - useEffect(() => { - if (inputTimeoutId.current !== 0) { - clearTimeout(inputTimeoutId.current); // ⚠️ mutation: cancel any previous timers - } - // ⚠️ mutation: schedule a new timer that will apply the filter when it fires: - inputTimeoutId.current = window.setTimeout(() => { - setIsSearching(false); - setAppliedFilterInput(filterInput); - }, INPUT_TIMEOUT); - return () => { - clearTimeout(inputTimeoutId.current); - }; - }, [filterInput]); - - useEffect(() => { - setFilteredBrowserFields(newFilteredBrowserFields); - }, [newFilteredBrowserFields]); - - useEffect(() => { - setSelectedCategoryId(newSelectedCategoryId); - }, [newSelectedCategoryId]); + const updateFilter = useCallback( + (newFilterInput: string) => { + setIsSearching(true); + setFilterInput(newFilterInput); + debouncedApplyFilterInput(newFilterInput); + }, + [debouncedApplyFilterInput] + ); // only merge in the default category if the field browser is visible const browserFieldsWithDefaultCategory = useMemo(() => { @@ -150,19 +126,19 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ {show && ( diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.test.tsx index f5668b1bdc08d4..fb6363e2444592 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.test.tsx @@ -7,7 +7,7 @@ import { mount } from 'enzyme'; import React from 'react'; -import { mockBrowserFields, TestProviders } from '../../../../mock'; +import { TestProviders } from '../../../../mock'; import { Search } from './search'; const timelineId = 'test'; @@ -17,7 +17,6 @@ describe('Search', () => { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { expect(onSearchInputChange).toBeCalled(); }); - - test('it returns the expected categories count when filteredBrowserFields is empty', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="categories-count"]').first().text()).toEqual( - '0 categories' - ); - }); - - test('it returns the expected categories count when filteredBrowserFields is NOT empty', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="categories-count"]').first().text()).toEqual( - '12 categories' - ); - }); - - test('it returns the expected fields count when filteredBrowserFields is empty', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="fields-count"]').first().text()).toEqual('0 fields'); - }); - - test('it returns the expected fields count when filteredBrowserFields is NOT empty', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="fields-count"]').first().text()).toEqual('34 fields'); - }); }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.tsx index 935952fbf37e00..037dcdc9033d2f 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.tsx @@ -6,75 +6,28 @@ */ import React from 'react'; -import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import styled from 'styled-components'; -import type { BrowserFields } from '../../../../../common/search_strategy'; - -import { getFieldBrowserSearchInputClassName, getFieldCount } from './helpers'; - +import { EuiFieldSearch } from '@elastic/eui'; import * as i18n from './translations'; - -const CountsFlexGroup = styled(EuiFlexGroup)` - margin-top: ${({ theme }) => theme.eui.euiSizeXS}; - margin-left: ${({ theme }) => theme.eui.euiSizeXS}; -`; - -CountsFlexGroup.displayName = 'CountsFlexGroup'; - interface Props { - filteredBrowserFields: BrowserFields; isSearching: boolean; onSearchInputChange: (event: React.ChangeEvent) => void; searchInput: string; timelineId: string; } -const CountRow = React.memo>(({ filteredBrowserFields }) => ( - - - - {i18n.CATEGORIES_COUNT(Object.keys(filteredBrowserFields).length)} - - - - - - {i18n.FIELDS_COUNT( - Object.keys(filteredBrowserFields).reduce( - (fieldsCount, category) => getFieldCount(filteredBrowserFields[category]) + fieldsCount, - 0 - ) - )} - - - -)); - -CountRow.displayName = 'CountRow'; - const inputRef = (node: HTMLInputElement | null) => node?.focus(); export const Search = React.memo( - ({ isSearching, filteredBrowserFields, onSearchInputChange, searchInput, timelineId }) => ( - <> - - - + ({ isSearching, onSearchInputChange, searchInput, timelineId }) => ( + ) ); - Search.displayName = 'Search'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/translations.ts index ac0160fad6cdee..eab412971c580c 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/translations.ts +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/translations.ts @@ -21,21 +21,6 @@ export const CATEGORIES_COUNT = (totalCount: number) => defaultMessage: '{totalCount} {totalCount, plural, =1 {category} other {categories}}', }); -export const CATEGORY_LINK = ({ category, totalCount }: { category: string; totalCount: number }) => - i18n.translate('xpack.timelines.fieldBrowser.categoryLinkAriaLabel', { - values: { category, totalCount }, - defaultMessage: - '{category} {totalCount} {totalCount, plural, =1 {field} other {fields}}. Click this button to select the {category} category.', - }); - -export const CATEGORY_FIELDS_TABLE_CAPTION = (categoryId: string) => - i18n.translate('xpack.timelines.fieldBrowser.categoryFieldsTableCaption', { - defaultMessage: 'category {categoryId} fields', - values: { - categoryId, - }, - }); - export const CLOSE = i18n.translate('xpack.timelines.fieldBrowser.closeButton', { defaultMessage: 'Close', }); @@ -56,6 +41,10 @@ export const DESCRIPTION_FOR_FIELD = (field: string) => defaultMessage: 'Description for field {field}:', }); +export const NAME = i18n.translate('xpack.timelines.fieldBrowser.fieldName', { + defaultMessage: 'Name', +}); + export const FIELD = i18n.translate('xpack.timelines.fieldBrowser.fieldLabel', { defaultMessage: 'Field', }); @@ -64,10 +53,14 @@ export const FIELDS = i18n.translate('xpack.timelines.fieldBrowser.fieldsTitle', defaultMessage: 'Fields', }); +export const FIELDS_SHOWING = i18n.translate('xpack.timelines.fieldBrowser.fieldsCountShowing', { + defaultMessage: 'Showing', +}); + export const FIELDS_COUNT = (totalCount: number) => i18n.translate('xpack.timelines.fieldBrowser.fieldsCountTitle', { values: { totalCount }, - defaultMessage: '{totalCount} {totalCount, plural, =1 {field} other {fields}}', + defaultMessage: '{totalCount, plural, =1 {field} other {fields}}', }); export const FILTER_PLACEHOLDER = i18n.translate('xpack.timelines.fieldBrowser.filterPlaceholder', { @@ -90,14 +83,6 @@ export const RESET_FIELDS = i18n.translate('xpack.timelines.fieldBrowser.resetFi defaultMessage: 'Reset Fields', }); -export const VIEW_ALL_CATEGORY_FIELDS = (categoryId: string) => - i18n.translate('xpack.timelines.fieldBrowser.viewCategoryTooltip', { - defaultMessage: 'View all {categoryId} fields', - values: { - categoryId, - }, - }); - export const VIEW_COLUMN = (field: string) => i18n.translate('xpack.timelines.fieldBrowser.viewColumnCheckboxAriaLabel', { values: { field }, diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/types.ts b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/types.ts deleted file mode 100644 index bcf7287950624f..00000000000000 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/types.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 { CreateFieldComponentType } from '../../../../../common/types'; -import type { BrowserFields } from '../../../../../common/search_strategy/index_fields'; -import type { ColumnHeaderOptions } from '../../../../../common/types/timeline/columns'; - -export type OnFieldSelected = (fieldId: string) => void; - -export interface FieldBrowserProps { - /** The timeline associated with this field browser */ - timelineId: string; - /** The timeline's current column headers */ - columnHeaders: ColumnHeaderOptions[]; - /** A map of categoryId -> metadata about the fields in that category */ - browserFields: BrowserFields; - - createFieldComponent?: CreateFieldComponentType; - /** When true, this Fields Browser is being used as an "events viewer" */ - isEventViewer?: boolean; - /** The width of the field browser */ - width?: number; -} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 70d65550a40563..c35b5dbe666782 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -27035,9 +27035,7 @@ "xpack.timelines.exitFullScreenButton": "全画面を終了", "xpack.timelines.fieldBrowser.categoriesCountTitle": "{totalCount} {totalCount, plural, other {カテゴリ}}", "xpack.timelines.fieldBrowser.categoriesTitle": "カテゴリー", - "xpack.timelines.fieldBrowser.categoryFieldsTableCaption": "カテゴリ {categoryId} フィールド", "xpack.timelines.fieldBrowser.categoryLabel": "カテゴリー", - "xpack.timelines.fieldBrowser.categoryLinkAriaLabel": "{category} {totalCount} {totalCount, plural, other {フィールド}}このボタンをクリックすると、{category} カテゴリを選択します。", "xpack.timelines.fieldBrowser.closeButton": "閉じる", "xpack.timelines.fieldBrowser.descriptionForScreenReaderOnly": "フィールド {field} の説明:", "xpack.timelines.fieldBrowser.descriptionLabel": "説明", @@ -27049,7 +27047,6 @@ "xpack.timelines.fieldBrowser.noFieldsMatchInputLabel": "{searchInput} に一致するフィールドがありません", "xpack.timelines.fieldBrowser.noFieldsMatchLabel": "一致するフィールドがありません", "xpack.timelines.fieldBrowser.resetFieldsLink": "フィールドをリセット", - "xpack.timelines.fieldBrowser.viewCategoryTooltip": "すべての {categoryId} フィールドを表示します", "xpack.timelines.fieldBrowser.viewColumnCheckboxAriaLabel": "{field} 列を表示", "xpack.timelines.footer.autoRefreshActiveDescription": "自動更新アクション", "xpack.timelines.footer.autoRefreshActiveTooltip": "自動更新が有効な間、タイムラインはクエリに一致する最新の {numberOfItems} 件のイベントを表示します。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 90eb0d3c35f653..f706762740ad8e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -27067,9 +27067,7 @@ "xpack.timelines.exitFullScreenButton": "退出全屏", "xpack.timelines.fieldBrowser.categoriesCountTitle": "{totalCount} {totalCount, plural, other {个类别}}", "xpack.timelines.fieldBrowser.categoriesTitle": "类别", - "xpack.timelines.fieldBrowser.categoryFieldsTableCaption": "类别 {categoryId} 字段", "xpack.timelines.fieldBrowser.categoryLabel": "类别", - "xpack.timelines.fieldBrowser.categoryLinkAriaLabel": "{category} {totalCount} 个{totalCount, plural, other {字段}}。单击此按钮可选择 {category} 类别。", "xpack.timelines.fieldBrowser.closeButton": "关闭", "xpack.timelines.fieldBrowser.descriptionForScreenReaderOnly": "{field} 字段的描述:", "xpack.timelines.fieldBrowser.descriptionLabel": "描述", @@ -27081,7 +27079,6 @@ "xpack.timelines.fieldBrowser.noFieldsMatchInputLabel": "没有字段匹配“{searchInput}”", "xpack.timelines.fieldBrowser.noFieldsMatchLabel": "没有字段匹配", "xpack.timelines.fieldBrowser.resetFieldsLink": "重置字段", - "xpack.timelines.fieldBrowser.viewCategoryTooltip": "查看所有 {categoryId} 字段", "xpack.timelines.fieldBrowser.viewColumnCheckboxAriaLabel": "查看 {field} 列", "xpack.timelines.footer.autoRefreshActiveDescription": "自动刷新已启用", "xpack.timelines.footer.autoRefreshActiveTooltip": "自动刷新已启用时,时间线将显示匹配查询的最近 {numberOfItems} 个事件。",