diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index 2e501984dfa76..21e052b5dfb62 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -80,6 +80,7 @@ interface Props { actionPredicate?: (actionId: string) => boolean; reportUiCounter?: UsageCollectionStart['reportUiCounter']; showShadow?: boolean; + hasBorder?: boolean; showBadges?: boolean; showNotifications?: boolean; containerContext?: EmbeddableContainerContext; @@ -273,6 +274,7 @@ export class EmbeddablePanel extends React.Component { role="figure" aria-labelledby={headerId} hasShadow={this.props.showShadow} + hasBorder={false} > {!this.props.hideHeader && ( diff --git a/x-pack/examples/exploratory_view_example/public/mount.tsx b/x-pack/examples/exploratory_view_example/public/mount.tsx index 58ec363223270..b589db9d531b7 100644 --- a/x-pack/examples/exploratory_view_example/public/mount.tsx +++ b/x-pack/examples/exploratory_view_example/public/mount.tsx @@ -7,9 +7,12 @@ import * as React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { CoreSetup, AppMountParameters } from 'kibana/public'; +import { CoreSetup, AppMountParameters, APP_WRAPPER_CLASS } from '../../../../src/core/public'; import { StartDependencies } from './plugin'; - +import { + KibanaContextProvider, + RedirectAppLinks, +} from '../../../../src/plugins/kibana_react/public'; export const mount = (coreSetup: CoreSetup) => async ({ element }: AppMountParameters) => { @@ -26,9 +29,13 @@ export const mount = const i18nCore = core.i18n; const reactElement = ( - - - + + + + + + + ); render(reactElement, element); return () => unmountComponentAtNode(element); diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx b/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx index e501138648b14..d6051ee5fb59c 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable_component.tsx @@ -7,7 +7,7 @@ import React, { FC, useEffect } from 'react'; import type { CoreStart, ThemeServiceStart } from 'kibana/public'; -import type { UiActionsStart } from 'src/plugins/ui_actions/public'; +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 { @@ -52,7 +52,7 @@ export type TypedLensByValueInput = Omit & { }; export type EmbeddableComponentProps = (TypedLensByValueInput | LensByReferenceInput) & { - withActions?: boolean; + withActions?: boolean | Action[]; }; interface PluginsStartDependencies { @@ -67,7 +67,7 @@ export function getEmbeddableComponent(core: CoreStart, plugins: PluginsStartDep const factory = embeddableStart.getEmbeddableFactory('lens')!; const input = { ...props }; const [embeddable, loading, error] = useEmbeddableFactory({ factory, input }); - const hasActions = props.withActions === true; + const hasActions = Boolean(props.withActions); const theme = core.theme; if (loading) { @@ -83,6 +83,7 @@ export function getEmbeddableComponent(core: CoreStart, plugins: PluginsStartDep actionPredicate={() => hasActions} input={input} theme={theme} + extraActions={Array.isArray(props.withActions) ? props.withActions : []} /> ); } @@ -98,6 +99,9 @@ interface EmbeddablePanelWrapperProps { actionPredicate: (id: string) => boolean; input: EmbeddableComponentProps; theme: ThemeServiceStart; + hideHeader?: boolean; + showShadow?: boolean; + extraActions: Action[]; } const EmbeddablePanelWrapper: FC = ({ @@ -107,6 +111,9 @@ const EmbeddablePanelWrapper: FC = ({ inspector, input, theme, + extraActions, + hideHeader = false, + showShadow = false, }) => { useEffect(() => { embeddable.updateInput(input); @@ -114,15 +121,19 @@ const EmbeddablePanelWrapper: FC = ({ return ( } - getActions={uiActions.getTriggerCompatibleActions} + getActions={async (triggerId, context) => { + const actions = await uiActions.getTriggerCompatibleActions(triggerId, context); + return [...extraActions, ...actions]; + }} inspector={inspector} actionPredicate={actionPredicate} - showShadow={false} + showShadow={showShadow} showBadges={false} showNotifications={false} theme={theme} + hasBorder={false} /> ); }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx index e02f11dfc4954..0567f5cdaa1d5 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx @@ -36,7 +36,7 @@ export function SeriesDatePicker({ series, seriesId }: Props) { const { setSeries, reportType, allSeries } = useSeriesStorage(); function onTimeChange({ start, end }: { start: string; end: string }) { - onRefreshTimeRange(); + onRefreshTimeRange?.(); if (reportType === ReportTypes.KPI) { allSeries.forEach((currSeries, seriesIndex) => { setSeries(seriesIndex, { ...currSeries, time: { from: start, to: end } }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index f873b1eb5cbab..818a3eb65d886 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -42,7 +42,7 @@ import { PERCENTILE_RANKS, ReportTypes, } from './constants'; -import { ColumnFilter, SeriesConfig, UrlFilter, URLReportDefinition } from '../types'; +import { ColumnFilter, ParamFilter, SeriesConfig, UrlFilter, URLReportDefinition } from '../types'; import { PersistableFilter } from '../../../../../../lens/common'; import { parseRelativeDate } from '../components/date_range_picker'; import { getDistributionInPercentageColumn } from './lens_columns/overall_column'; @@ -63,6 +63,8 @@ function buildNumberColumn(sourceField: string) { export const parseCustomFieldName = (seriesConfig: SeriesConfig, selectedMetricField?: string) => { let columnType; let columnFilters; + let columnFilter; + let paramFilters; let timeScale; let columnLabel; @@ -75,12 +77,22 @@ export const parseCustomFieldName = (seriesConfig: SeriesConfig, selectedMetricF ); columnType = currField?.columnType; columnFilters = currField?.columnFilters; + columnFilter = currField?.columnFilter; timeScale = currField?.timeScale; columnLabel = currField?.label; + paramFilters = currField?.paramFilters; } } - return { fieldName: selectedMetricField!, columnType, columnFilters, timeScale, columnLabel }; + return { + fieldName: selectedMetricField!, + columnType, + columnFilters, + paramFilters, + timeScale, + columnLabel, + columnFilter, + }; }; export interface LayerConfig { @@ -103,12 +115,14 @@ export class LensAttributes { layerConfigs: LayerConfig[]; isMultiSeries: boolean; globalFilter?: { query: string; language: string }; + reportType: string; - constructor(layerConfigs: LayerConfig[]) { + constructor(layerConfigs: LayerConfig[], reportType: string) { this.layers = {}; + this.reportType = reportType; layerConfigs.forEach(({ seriesConfig, operationType }) => { - if (operationType) { + if (operationType && reportType !== 'singleMetric') { seriesConfig.yAxisColumns.forEach((yAxisColumn) => { if (typeof yAxisColumn.operationType !== undefined) { yAxisColumn.operationType = @@ -121,7 +135,11 @@ export class LensAttributes { this.layerConfigs = layerConfigs; this.isMultiSeries = layerConfigs.length > 1; this.globalFilter = this.getGlobalFilter(this.isMultiSeries); - this.layers = this.getLayers(); + if (reportType === 'singleMetric') { + this.layers = this.getSingleMetricLayer(); + } else { + this.layers = this.getLayers(); + } this.visualization = this.getXyState(); } @@ -208,6 +226,19 @@ export class LensAttributes { }); } + getFiltersColumn({ label, paramFilters }: { paramFilters: ParamFilter[]; label?: string }) { + return { + label: label ?? 'Filters', + dataType: 'string', + operationType: 'filters', + scale: 'ordinal', + isBucketed: true, + params: { + filters: paramFilters, + }, + }; + } + getNumberColumn({ seriesConfig, label, @@ -389,6 +420,10 @@ export class LensAttributes { getXAxis(layerConfig: LayerConfig, layerId: string) { const { xAxisColumn } = layerConfig.seriesConfig; + if (!xAxisColumn.sourceField) { + return xAxisColumn; + } + if (xAxisColumn?.sourceField === USE_BREAK_DOWN_COLUMN) { return this.getBreakdownColumn({ layerId, @@ -398,10 +433,17 @@ export class LensAttributes { }); } + if (xAxisColumn.sourceField === REPORT_METRIC_FIELD) { + const { paramFilters } = this.getFieldMeta(xAxisColumn.sourceField, layerConfig); + if (paramFilters) { + return this.getFiltersColumn({ paramFilters }); + } + } + return this.getColumnBasedOnType({ layerConfig, label: xAxisColumn.label, - sourceField: xAxisColumn.sourceField!, + sourceField: xAxisColumn.sourceField, }); } @@ -485,12 +527,18 @@ export class LensAttributes { getFieldMeta(sourceField: string, layerConfig: LayerConfig) { if (sourceField === REPORT_METRIC_FIELD) { - const { fieldName, columnType, columnLabel, columnFilters, timeScale } = parseCustomFieldName( - layerConfig.seriesConfig, - layerConfig.selectedMetricField - ); + const { fieldName, columnType, columnLabel, columnFilters, timeScale, paramFilters } = + parseCustomFieldName(layerConfig.seriesConfig, layerConfig.selectedMetricField); const fieldMeta = layerConfig.indexPattern.getFieldByName(fieldName!); - return { fieldMeta, fieldName, columnType, columnLabel, columnFilters, timeScale }; + return { + fieldMeta, + fieldName, + columnType, + columnLabel, + columnFilters, + timeScale, + paramFilters, + }; } else { const fieldMeta = layerConfig.indexPattern.getFieldByName(sourceField); @@ -732,7 +780,52 @@ export class LensAttributes { return layers; } + getSingleMetricLayer() { + const layerConfig = this.layerConfigs[0]; + const { columnFilter } = parseCustomFieldName( + layerConfig.seriesConfig, + layerConfig.selectedMetricField + ); + + const getSourceField = () => { + if ( + layerConfig.selectedMetricField.startsWith('Records') || + layerConfig.selectedMetricField.startsWith('records') + ) { + return 'Records'; + } + return layerConfig.selectedMetricField; + }; + + return { + layer0: { + columns: { + 'layer-0-column-1': { + label: '', + dataType: 'number', + operationType: layerConfig.operationType, + scale: 'ratio', + sourceField: getSourceField(), + isBucketed: false, + filter: columnFilter, + }, + }, + columnOrder: ['layer-0-column-1'], + incompleteColumns: {}, + }, + }; + } + getXyState(): XYState { + if (this.reportType === 'singleMetric') { + return { + accessor: 'layer-0-column-1', + layerId: 'layer0', + layerType: 'data', + layers: [], + }; + } + return { legend: { isVisible: true, showSingleSeries: true, position: 'right' }, valueLabels: 'hide', @@ -793,7 +886,7 @@ export class LensAttributes { return { title: 'Prefilled from exploratory view app', description: String(refresh), - visualizationType: 'lnsXY', + visualizationType: this.reportType === 'singleMetric' ? 'lnsMetric' : 'lnsXY', references: [ ...uniqueIndexPatternsIds.map((patternId) => ({ id: patternId!, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts index 32d428916501c..e504cbb1568ae 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts @@ -51,13 +51,21 @@ export function convertToShortUrl(series: SeriesUrl) { export function createExploratoryViewUrl( { reportType, allSeries }: { reportType: ReportViewType; allSeries: AllSeries }, - baseHref = '' + baseHref = '', + appId = 'observability', + onlyPath?: boolean ) { const allShortSeries: AllShortSeries = allSeries.map((series) => convertToShortUrl(series)); + if (onlyPath) { + return `/exploratory-view/#?reportType=${reportType}&sr=${rison.encode( + allShortSeries as unknown as RisonValue + )}`; + } + return ( baseHref + - `/app/observability/exploratory-view/#?reportType=${reportType}&sr=${rison.encode( + `/app/${appId}/exploratory-view/#?reportType=${reportType}&sr=${rison.encode( allShortSeries as unknown as RisonValue )}` ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/contexts/exploratory_view_config.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/contexts/exploratory_view_config.tsx index b7734e675f394..c0b4f1e5bae3b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/contexts/exploratory_view_config.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/contexts/exploratory_view_config.tsx @@ -20,6 +20,7 @@ interface ExploratoryViewContextValue { }>; indexPatterns: Record; reportConfigMap: ReportConfigMap; + asPanel?: boolean; setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; theme$: AppMountParameters['theme$']; } @@ -35,9 +36,11 @@ export function ExploratoryViewContextProvider({ indexPatterns, reportConfigMap, setHeaderActionMenu, + asPanel = true, theme$, }: { children: JSX.Element } & ExploratoryViewContextValue) { const value = { + asPanel, reportTypes, dataTypes, indexPatterns, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/embeddable.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/embeddable.tsx index 8aa76d0e6228a..b44da6b438182 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/embeddable.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/embeddable.tsx @@ -6,49 +6,95 @@ */ import React, { useState } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; +import { EuiIcon, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; import styled from 'styled-components'; -import { AllSeries, useTheme } from '../../../..'; +import { AllSeries, createExploratoryViewUrl, SeriesUrl, useTheme } from '../../../..'; import { LayerConfig, LensAttributes } from '../configurations/lens_attributes'; import { AppDataType, ReportViewType } from '../types'; import { getLayerConfigs } from '../hooks/use_lens_attributes'; -import { LensPublicStart, XYState } from '../../../../../../lens/public'; +import { LensEmbeddableInput, LensPublicStart, XYState } from '../../../../../../lens/public'; import { OperationTypeComponent } from '../series_editor/columns/operation_type_select'; import { IndexPatternState } from '../hooks/use_app_index_pattern'; import { ReportConfigMap } from '../contexts/exploratory_view_config'; import { obsvReportConfigMap } from '../obsv_exploratory_view'; +import { useActions } from './use_actions'; +import { AddToCaseAction } from '../header/add_to_case_action'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { useDefaultTimeRange } from './use_default_time_range'; export interface ExploratoryEmbeddableProps { - reportType: ReportViewType; - attributes: AllSeries; + alignLnsMetric?: string; appendTitle?: JSX.Element; - title: string | JSX.Element; - showCalculationMethod?: boolean; + appendHeader?: JSX.Element; + appId?: 'security' | 'observability'; + attributes?: AllSeries; axisTitlesVisibility?: XYState['axisTitlesVisibilitySettings']; - legendIsVisible?: boolean; + compressed?: boolean; + customHeight?: string | number; + customLensAttrs?: any; + + disableBorder?: boolean; + disableShadow?: boolean; dataTypesIndexPatterns?: Partial>; + legendIsVisible?: boolean; + onBrushEnd?: (params: { + table: any; + column: number; + range: number[]; + timeFieldName?: string | undefined; + }) => void; reportConfigMap?: ReportConfigMap; + withActions?: boolean | Array<'explore' | 'save' | 'addToCase' | 'openInLens'>; + reportType: ReportViewType | string; + showCalculationMethod?: boolean; + showExploreButton?: boolean; + metricIcon?: string; + metricIconColor?: string; + metricPostfix?: string; + title?: string | JSX.Element; } export interface ExploratoryEmbeddableComponentProps extends ExploratoryEmbeddableProps { - lens: LensPublicStart; indexPatterns: IndexPatternState; + lens: LensPublicStart; } // eslint-disable-next-line import/no-default-export export default function Embeddable({ - reportType, - attributes, - title, + alignLnsMetric, appendTitle, + appendHeader, + appId, + attributes = [], + axisTitlesVisibility, + compressed = false, + customHeight, + customLensAttrs, + disableBorder = false, + disableShadow = false, indexPatterns, lens, - axisTitlesVisibility, legendIsVisible, + metricIcon, + metricIconColor, + metricPostfix, + onBrushEnd, + withActions = true, reportConfigMap = {}, + reportType, showCalculationMethod = false, + showExploreButton = false, + title, }: ExploratoryEmbeddableComponentProps) { const LensComponent = lens?.EmbeddableComponent; + const LensSaveModalComponent = lens?.SaveModalComponent; + + const [isSaveOpen, setIsSaveOpen] = useState(false); + const [isAddToCaseOpen, setAddToCaseOpen] = useState(false); + + const { http } = useKibana().services; + + const stateTime = useDefaultTimeRange(); const series = Object.entries(attributes)[0][1]; @@ -63,32 +109,60 @@ export default function Embeddable({ { ...reportConfigMap, ...obsvReportConfigMap } ); - if (layerConfigs.length < 1) { - return null; - } - const lensAttributes = new LensAttributes(layerConfigs); + let lensAttributes; - if (!LensComponent) { - return No lens component; - } + try { + lensAttributes = new LensAttributes(layerConfigs, reportType); + } catch (error) {} - const attributesJSON = lensAttributes.getJSON(); + const attributesJSON = customLensAttrs ?? lensAttributes?.getJSON(); - (attributesJSON.state.visualization as XYState).axisTitlesVisibilitySettings = - axisTitlesVisibility; + if (typeof axisTitlesVisibility !== 'undefined') { + (attributesJSON.state.visualization as XYState).axisTitlesVisibilitySettings = + axisTitlesVisibility; + } if (typeof legendIsVisible !== 'undefined') { (attributesJSON.state.visualization as XYState).legend.isVisible = legendIsVisible; } + const actions = useActions({ + withActions, + attributes, + reportType, + appId, + setIsSaveOpen, + setAddToCaseOpen, + timeRange: series?.time, + lensAttributes: attributesJSON, + }); + + const href = createExploratoryViewUrl( + { reportType, allSeries: attributes }, + http?.basePath.get(), + appId + ); + + if (!attributesJSON && layerConfigs.length < 1) { + console.log(attributesJSON, layerConfigs.length); + return null; + } + + if (!LensComponent) { + return No lens component; + } + return ( - - - - -

{title}

-
-
+ + + {title && ( + + +

{title}

+
+
+ )} + {showCalculationMethod && ( )} + {appendHeader} + {showExploreButton && ( + + + + )} {appendTitle} -
- {}} - /> + + + {metricIcon && ( + + + + )} + + {}} + withActions={actions} + /> + + {metricPostfix && ( + + +

{metricPostfix}

+
+
+ )} + {isSaveOpen && attributesJSON && ( + setIsSaveOpen(false)} + // if we want to do anything after the viz is saved + // right now there is no action, so an empty function + onSave={() => {}} + /> + )} + +
); } -const Wrapper = styled.div` +const LensWrapper = styled(EuiFlexGroup)<{ + $alignLnsMetric?: string; + $disableBorder?: boolean; + $disableShadow?: boolean; +}>` + .embPanel__optionsMenuPopover { + visibility: collapse; + } + .embPanel--editing { + background-color: transparent; + } + ${(props) => + props.$disableBorder + ? `.embPanel--editing { + border: 0; + }` + : ''} + &&&:hover { + .embPanel__optionsMenuPopover { + visibility: visible; + } + ${(props) => + props.$disableShadow + ? `.embPanel--editing { + box-shadow: none; + }` + : ''} + } + .embPanel__title { + display: none; + } + + ${(props) => + props.$alignLnsMetric + ? `.lnsMetricExpression__container { + align-items: ${props.$alignLnsMetric}; + }` + : ''} +`; + +const Wrapper = styled.div<{ + $customHeight?: string | number; + $compressed?: boolean; +}>` height: 100%; + ${(props) => (props.$compressed ? 'position: relative;' : '')} + &&& { > :nth-child(2) { - height: calc(100% - 32px); - } + height: ${(props) => (props.$customHeight ? `${props.$customHeight};` : `calc(100% - 32px);`)} } `; + +const StyledFlexGroup = styled(EuiFlexGroup)<{ + $compressed?: boolean; +}>` + width: calc(100% - 30px); + ${(props) => + props.$compressed + ? `position: absolute; + top: -5px; + z-index: 1;` + : ''} +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/index.tsx index e68ddfe55e6f5..b86decbcfa9a5 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/index.tsx @@ -33,7 +33,9 @@ export function getExploratoryViewEmbeddable( const [indexPatterns, setIndexPatterns] = useState({} as IndexPatternState); const [loading, setLoading] = useState(false); - const series = props.attributes[0]; + const series = + (props?.customLensAttrs && props.customLensAttrs[0]) ?? + (props?.attributes && props.attributes[0]); const isDarkMode = core.uiSettings.get('theme:darkMode'); @@ -59,8 +61,10 @@ export function getExploratoryViewEmbeddable( ); useEffect(() => { - loadIndexPattern({ dataType: series.dataType }); - }, [series.dataType, loadIndexPattern]); + if (series) { + loadIndexPattern({ dataType: series.dataType }); + } + }, [series, loadIndexPattern]); if (Object.keys(indexPatterns).length === 0 || loading) { return ; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/use_actions.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/use_actions.ts new file mode 100644 index 0000000000000..ea747e6267d09 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/use_actions.ts @@ -0,0 +1,202 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { createExploratoryViewUrl } from '../configurations/utils'; +import { ReportViewType } from '../types'; +import { AllSeries } from '../hooks/use_series_storage'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { + Action, + ActionExecutionContext, +} from '../../../../../../../../src/plugins/ui_actions/public'; +import { ObservabilityAppServices } from '../../../../application/types'; + +export function useActions({ + withActions, + attributes, + reportType, + setIsSaveOpen, + setAddToCaseOpen, + appId = 'observability', + timeRange, + lensAttributes, +}: { + withActions?: boolean | Array<'explore' | 'save' | 'addToCase' | 'openInLens'>; + reportType: ReportViewType; + attributes: AllSeries; + appId: 'security' | 'observability'; + setIsSaveOpen: (val: boolean) => void; + setAddToCaseOpen: (val: boolean) => void; +}) { + const kServices = useKibana().services; + + const { lens } = kServices; + const [defaultActions, setDefaultActions] = useState([ + 'explore', + 'save', + 'addToCase', + 'openInLens', + ]); + + useEffect(() => { + if (withActions === false) { + setDefaultActions([]); + } + if (Array.isArray(withActions)) { + setDefaultActions(withActions); + } + }, [withActions]); + + const { http, application } = useKibana().services; + + const href = createExploratoryViewUrl( + { reportType, allSeries: attributes }, + http?.basePath.get(), + appId + ); + + const hrefPath = createExploratoryViewUrl( + { reportType, allSeries: attributes }, + http?.basePath.get(), + appId, + true + ); + + const exploreCallback = useCallback(() => { + application?.navigateToApp(appId, { path: hrefPath }); + }, [appId, application, hrefPath]); + + const saveCallback = useCallback(() => { + setIsSaveOpen(true); + }, [setIsSaveOpen]); + + const addToCaseCallback = useCallback(() => { + setAddToCaseOpen(true); + }, [setAddToCaseOpen]); + + const openInLensCallback = useCallback(() => { + if (lensAttributes) { + lens.navigateToPrefilledEditor( + { + id: '', + timeRange, + attributes: lensAttributes, + }, + { + openInNewTab: true, + } + ); + } + }, [lens, lensAttributes, timeRange]); + + return defaultActions.map((action) => { + if (action === 'save') { + return getSaveAction({ callback: saveCallback }); + } + if (action === 'addToCase') { + return getAddToCaseAction({ callback: addToCaseCallback }); + } + + if (action === 'openInLens') { + return getOpenInLensAction({ callback: openInLensCallback }); + } + return getExploreAction({ href, callback: exploreCallback }); + }); +} + +const getOpenInLensAction = ({ callback }: { callback: () => void }): Action => { + return { + id: 'expViewOpenInLens', + getDisplayName(context: ActionExecutionContext): string { + return i18n.translate('xpack.observability.expView.openInLens', { + defaultMessage: 'Open in Lens', + }); + }, + getIconType(context: ActionExecutionContext): string | undefined { + return 'visArea'; + }, + type: 'link', + async isCompatible(context: ActionExecutionContext): Promise { + return true; + }, + async execute(context: ActionExecutionContext): Promise { + callback(); + return; + }, + }; +}; + +const getExploreAction = ({ href, callback }: { href: string; callback: () => void }): Action => { + return { + id: 'expViewExplore', + getDisplayName(context: ActionExecutionContext): string { + return i18n.translate('xpack.observability.expView.explore', { + defaultMessage: 'Explore', + }); + }, + getIconType(context: ActionExecutionContext): string | undefined { + return 'visArea'; + }, + type: 'link', + async isCompatible(context: ActionExecutionContext): Promise { + return true; + }, + async getHref(context: ActionExecutionContext): Promise { + return href; + }, + async execute(context: ActionExecutionContext): Promise { + callback(); + return; + }, + }; +}; + +const getSaveAction = ({ callback }: { callback: () => void }): Action => { + return { + id: 'expViewSave', + getDisplayName(context: ActionExecutionContext): string { + return i18n.translate('xpack.observability.expView.save', { + defaultMessage: 'Save visualization', + }); + }, + getIconType(context: ActionExecutionContext): string | undefined { + return 'save'; + }, + type: 'actionButton', + async isCompatible(context: ActionExecutionContext): Promise { + return true; + }, + async execute(context: ActionExecutionContext): Promise { + callback(); + return; + }, + }; +}; + +const getAddToCaseAction = ({ callback }: { callback: () => void }): Action => { + return { + id: 'expViewAddToCase', + getDisplayName(context: ActionExecutionContext): string { + return i18n.translate('xpack.observability.expView.addToCase', { + defaultMessage: 'Add to case', + }); + }, + getIconType(context: ActionExecutionContext): string | undefined { + return 'link'; + }, + type: 'actionButton', + async isCompatible(context: ActionExecutionContext): Promise { + return true; + }, + async execute(context: ActionExecutionContext): Promise { + callback(); + return; + }, + }; +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/use_default_time_range.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/use_default_time_range.ts new file mode 100644 index 0000000000000..1a0cf93373cdb --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/use_default_time_range.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useMemo, useState } from 'react'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { TimeRange } from '../../../../../../../../src/plugins/data/common'; +import { ObservabilityPublicPluginsStart } from '../../../../plugin'; + +export function useDefaultTimeRange() { + const { data } = useKibana().services; + + const [time, setTime] = useState(); + + const { to, from, mode } = data.query.timefilter.timefilter.getTime(); + + useEffect(() => { + const update$ = data.query.timefilter.timefilter.getTimeUpdate$(); + + update$.subscribe((value) => { + setTime(data.query.timefilter.timefilter.getTime()); + }); + }, [data.query.timefilter.timefilter]); + + return useMemo(() => { + return time || { to, from, mode }; + }, [time, to, from, mode]); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx index 6ffb49560722a..7e779c3148b70 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx @@ -21,6 +21,7 @@ import { SeriesViews } from './views/series_views'; import { LensEmbeddable } from './lens_embeddable'; import { EmptyView } from './components/empty_view'; import type { ChartTimeRange } from './header/last_updated'; +import { useExploratoryView } from './contexts/exploratory_view_config'; export type PanelId = 'seriesPanel' | 'chartPanel'; @@ -50,6 +51,8 @@ export function ExploratoryView({ const lensAttributesT = useLensAttributes(); + const { asPanel } = useExploratoryView(); + const setHeightOffset = () => { if (seriesBuilderRef?.current && wrapperRef.current) { const headerOffset = wrapperRef.current.getBoundingClientRect().top; @@ -93,8 +96,10 @@ export function ExploratoryView({ } }; + const WrapperC = asPanel ? PanelWrapper : Wrapper; + console.log('lensAttributes', lensAttributes); return ( - + {lens ? ( <> {LENS_NOT_AVAILABLE} )} - + ); } const LensWrapper = styled.div<{ height: string }>` @@ -177,7 +182,20 @@ const ResizableContainer = styled(EuiResizableContainer)` } `; -const Wrapper = styled(EuiPanel)` +const Wrapper = styled.div` + max-width: 1800px; + min-width: 800px; + margin: 0 auto; + width: 100%; + overflow-x: auto; + position: relative; + + .echLegendItem__action { + display: none; + } +`; + +const PanelWrapper = styled(EuiPanel)` max-width: 1800px; min-width: 800px; margin: 0 auto; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.tsx index f1607bc49a384..50f44f2d89b9b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.tsx @@ -7,7 +7,7 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { toMountPoint, useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityAppServices } from '../../../../application/types'; import { @@ -21,11 +21,20 @@ import { observabilityFeatureId, observabilityAppId } from '../../../../../commo import { parseRelativeDate } from '../components/date_range_picker'; export interface AddToCaseProps { + autoOpen?: boolean; + setAutoOpen?: (val: boolean) => void; timeRange: { from: string; to: string }; + appId?: 'security' | 'observability'; lensAttributes: TypedLensByValueInput['attributes'] | null; } -export function AddToCaseAction({ lensAttributes, timeRange }: AddToCaseProps) { +export function AddToCaseAction({ + lensAttributes, + timeRange, + autoOpen, + setAutoOpen, + appId, +}: AddToCaseProps) { const kServices = useKibana().services; const { @@ -58,6 +67,7 @@ export function AddToCaseAction({ lensAttributes, timeRange }: AddToCaseProps) { from: absoluteFromDate?.toISOString() ?? '', to: absoluteToDate?.toISOString() ?? '', }, + appId, }); const getAllCasesSelectorModalProps: GetAllCasesSelectorModalProps = { @@ -69,22 +79,36 @@ export function AddToCaseAction({ lensAttributes, timeRange }: AddToCaseProps) { }, }; + useEffect(() => { + if (autoOpen) { + setIsCasesOpen(true); + } + }, [autoOpen, setIsCasesOpen]); + + useEffect(() => { + if (!isCasesOpen) { + setAutoOpen?.(false); + } + }, [isCasesOpen, setAutoOpen]); + return ( <> - { - if (lensAttributes) { - setIsCasesOpen(true); - } - }} - > - {i18n.translate('xpack.observability.expView.heading.addToCase', { - defaultMessage: 'Add to case', - })} - + {typeof autoOpen === 'undefined' && ( + { + if (lensAttributes) { + setIsCasesOpen(true); + } + }} + > + {i18n.translate('xpack.observability.expView.heading.addToCase', { + defaultMessage: 'Add to case', + })} + + )} {isCasesOpen && lensAttributes && cases.getAllCasesSelectorModal(getAllCasesSelectorModalProps)} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_add_to_case.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_add_to_case.ts index 1f6620e632eff..2511fe44bfbc3 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_add_to_case.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_add_to_case.ts @@ -41,7 +41,11 @@ export const useAddToCase = ({ lensAttributes, getToastText, timeRange, -}: AddToCaseProps & { getToastText: (thaCase: Case | SubCase) => MountPoint }) => { + appId, +}: AddToCaseProps & { + appId?: 'security' | 'observability'; + getToastText: (thaCase: Case | SubCase) => MountPoint; +}) => { const [isSaving, setIsSaving] = useState(false); const [isCasesOpen, setIsCasesOpen] = useState(false); @@ -87,13 +91,13 @@ export const useAddToCase = ({ } ); } else { - navigateToApp(observabilityFeatureId, { + navigateToApp(appId || observabilityFeatureId, { deepLinkId: CasesDeepLinkId.casesCreate, openInNewTab: true, }); } }, - [getToastText, http, lensAttributes, navigateToApp, timeRange, toasts] + [appId, getToastText, http, lensAttributes, navigateToApp, timeRange, toasts] ); return { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts index cc258af65973e..30e1e12f114c3 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts @@ -49,12 +49,9 @@ export function getLayerConfigs( allSeries.forEach((series, seriesIndex) => { const indexPattern = indexPatterns?.[series?.dataType]; - if ( - indexPattern && - !isEmpty(series.reportDefinitions) && - !series.hidden && - series.selectedMetricField - ) { + const hasDefinitionFields = !isEmpty(series.reportDefinitions); + + if (indexPattern && hasDefinitionFields && !series.hidden && series.selectedMetricField) { const seriesConfig = getDefaultConfigs({ reportType, indexPattern, @@ -116,7 +113,7 @@ export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null return null; } - const lensAttributes = new LensAttributes(layerConfigs); + const lensAttributes = new LensAttributes(layerConfigs, reportTypeT); return lensAttributes.getJSON(lastRefresh); // we also want to check the state on allSeries changes diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx index f7f63097e2926..dc212f28059eb 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx @@ -6,9 +6,9 @@ */ import { i18n } from '@kbn/i18n'; -import React, { Dispatch, SetStateAction, useCallback } from 'react'; +import React, { Dispatch, SetStateAction, useCallback, useState } from 'react'; import styled from 'styled-components'; -import { TypedLensByValueInput } from '../../../../../lens/public'; +import { LensEmbeddableInput, TypedLensByValueInput } from '../../../../../lens/public'; import { useUiTracker } from '../../../hooks/use_track_metric'; import { useSeriesStorage } from './hooks/use_series_storage'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; @@ -17,6 +17,7 @@ import { useExpViewTimeRange } from './hooks/use_time_range'; import { parseRelativeDate } from './components/date_range_picker'; import { trackTelemetryOnLoad } from './utils/telemetry'; import type { ChartTimeRange } from './header/last_updated'; +import { Action, ActionExecutionContext } from '../../../../../../../src/plugins/ui_actions/public'; interface Props { lensAttributes: TypedLensByValueInput['attributes']; @@ -30,9 +31,12 @@ export function LensEmbeddable(props: Props) { } = useKibana(); const LensComponent = lens?.EmbeddableComponent; + const LensSaveModalComponent = lens?.SaveModalComponent; const { firstSeries, setSeries, reportType, lastRefresh } = useSeriesStorage(); + const [isSaveOpen, setIsSaveOpen] = useState(false); + const firstSeriesId = 0; const timeRange = useExpViewTimeRange(); @@ -89,7 +93,17 @@ export function LensEmbeddable(props: Props) { attributes={lensAttributes} onLoad={onLensLoad} onBrushEnd={onBrushEnd} + withActions={true} /> + {isSaveOpen && lensAttributes && ( + setIsSaveOpen(false)} + // if we want to do anything after the viz is saved + // right now there is no action, so an empty function + onSave={() => {}} + /> + )} ); } @@ -97,6 +111,26 @@ export function LensEmbeddable(props: Props) { const LensWrapper = styled.div` height: 100%; + .embPanel__optionsMenuPopover { + visibility: collapse; + } + + &&&:hover { + .embPanel__optionsMenuPopover { + visibility: visible; + } + } + + && .embPanel--editing { + border-style: initial !important; + :hover { + box-shadow: none; + } + } + .embPanel__title { + display: none; + } + &&& > div { height: 100%; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx index 5eec147379d25..2464aee4a8659 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx @@ -140,11 +140,9 @@ export function ReportMetricOptions({ seriesId, series, seriesConfig }: Props) { iconOnClick={() => onChange(undefined)} iconOnClickAriaLabel={REMOVE_REPORT_METRIC_LABEL} > - { - seriesConfig?.metricOptions?.find( - (option) => option.id === series.selectedMetricField - )?.label - } + {seriesConfig?.metricOptions?.find( + (option) => option.id === series.selectedMetricField + )?.label ?? series.selectedMetricField} ) : ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series.tsx index d320b84c6a684..238b5a23c931c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series.tsx @@ -56,7 +56,7 @@ export function Series({ item, isExpanded, toggleExpanded }: Props) { }, [isExpanded]); return ( - + | Partial; yAxisColumns: Array>; breakdownFields: string[]; @@ -106,7 +113,15 @@ export interface ConfigProps { series?: SeriesUrl; } -export type AppDataType = 'synthetics' | 'ux' | 'infra_logs' | 'infra_metrics' | 'apm' | 'mobile'; +export type AppDataType = + | 'synthetics' + | 'ux' + | 'infra_logs' + | 'infra_metrics' + | 'apm' + | 'mobile' + | 'security' + | 'securityAlerts'; type FormatType = 'duration' | 'number' | 'bytes' | 'percent'; type InputFormat = 'microseconds' | 'milliseconds' | 'seconds'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts index b35acaae6fe80..f4c68f39563cb 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts @@ -39,6 +39,7 @@ export const indexPatternList: Record = { infra_logs: 'infra_logs_static_index_pattern_id', infra_metrics: 'infra_metrics_static_index_pattern_id', mobile: 'mobile_static_index_pattern_id', + security: 'security_static_data_view', }; const appToPatternMap: Record = { @@ -48,6 +49,8 @@ const appToPatternMap: Record = { infra_logs: '', infra_metrics: '(infra-metrics-data-view)*', mobile: '(mobile-data-view)*', + security: '(security-data-view)*', + securityAlerts: '(security-alerts-data-view)*', }; const getAppIndicesWithPattern = (app: AppDataType, indices: string) => { @@ -151,8 +154,8 @@ export class ObservabilityIndexPatterns { if (appIndices) { try { - const indexPatternId = getAppIndexPatternId(app, appIndices); const indexPatternTitle = getAppIndicesWithPattern(app, appIndices); + const indexPatternId = getAppIndexPatternId(app, appIndices); // we will get index pattern by id const indexPattern = await this.data?.indexPatterns.get(indexPatternId); diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index e502cf7fb37e0..3256e9b4c07bc 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -98,6 +98,12 @@ export { enableComparisonByDefault } from '../common/ui_settings_keys'; export type { SeriesConfig, ConfigProps } from './components/shared/exploratory_view/types'; export { ReportTypes, + FILTER_RECORDS, REPORT_METRIC_FIELD, + USE_BREAK_DOWN_COLUMN, + RECORDS_FIELD, + OPERATION_COLUMN, + TERMS_COLUMN, + RECORDS_PERCENTAGE_FIELD, } from './components/shared/exploratory_view/configurations/constants'; export { ExploratoryViewContextProvider } from './components/shared/exploratory_view/contexts/exploratory_view_config'; diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index 9f22f229b33c1..9483af772cf40 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -20,6 +20,7 @@ "inspector", "licensing", "maps", + "observability", "ruleRegistry", "taskManager", "timelines", diff --git a/x-pack/plugins/security_solution/public/app/exploratory_view/alert_kpi_over_time_config.ts b/x-pack/plugins/security_solution/public/app/exploratory_view/alert_kpi_over_time_config.ts new file mode 100644 index 0000000000000..8f8bd82fed351 --- /dev/null +++ b/x-pack/plugins/security_solution/public/app/exploratory_view/alert_kpi_over_time_config.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ConfigProps, + SeriesConfig, + ReportTypes, + REPORT_METRIC_FIELD, + FILTER_RECORDS, +} from '../../../../observability/public'; + +export function getSecurityAlertsKPIConfig(_config: ConfigProps): SeriesConfig { + return { + reportType: ReportTypes.KPI, + defaultSeriesType: 'area', + seriesTypes: [], + xAxisColumn: { + sourceField: '@timestamp', + }, + yAxisColumns: [ + { + sourceField: REPORT_METRIC_FIELD, + operationType: 'unique_count', + }, + ], + hasOperationType: false, + filterFields: [], + breakdownFields: ['agent.type', 'event.module', 'event.category'], + baseFilters: [], + palette: { type: 'palette', name: 'status' }, + definitionFields: [{ field: 'kibana.alert.rule.name' }], + metricOptions: [ + { + label: 'Detection alerts', + id: 'detectionAerts', + columnType: FILTER_RECORDS, + columnFilters: [ + { + language: 'kuery', + query: 'NOT kibana.alert.building_block_type: *', + }, + ], + }, + ], + labels: { 'host.name': 'Hosts', 'url.full': 'URL', 'agent.type': 'Agent type' }, + }; +} diff --git a/x-pack/plugins/security_solution/public/app/exploratory_view/kpi_over_time_config.ts b/x-pack/plugins/security_solution/public/app/exploratory_view/kpi_over_time_config.ts new file mode 100644 index 0000000000000..1d44be27108ac --- /dev/null +++ b/x-pack/plugins/security_solution/public/app/exploratory_view/kpi_over_time_config.ts @@ -0,0 +1,372 @@ +/* + * 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 { + ConfigProps, + SeriesConfig, + ReportTypes, + FILTER_RECORDS, + REPORT_METRIC_FIELD, +} from '../../../../observability/public'; + +export function getSecurityKPIConfig(_config: ConfigProps): SeriesConfig { + return { + reportType: ReportTypes.KPI, + defaultSeriesType: 'area', + seriesTypes: [], + xAxisColumn: { + sourceField: '@timestamp', + }, + yAxisColumns: [ + { + sourceField: REPORT_METRIC_FIELD, + operationType: 'unique_count', + }, + ], + hasOperationType: false, + filterFields: [], + breakdownFields: [ + 'agent.type', + 'event.action', + 'event.module', + 'event.dataset', + 'event.category', + ], + baseFilters: [], + definitionFields: [{ field: 'host.name' }], + metricOptions: [ + { + label: 'Hosts', + field: 'host.name', + id: 'host.name', + }, + { + label: 'TOP_DNS_DOMAINS', + id: 'TOP_DNS_DOMAINS', + field: 'dns.question.registered_domain', + columnType: FILTER_RECORDS, + columnFilters: [], + }, + { + label: 'External alerts', + id: 'EXTERNAL_ALERTS', + columnType: FILTER_RECORDS, + columnFilters: [ + { + language: 'kuery', + query: `event.kind: alert and host.name: * `, + }, + ], + }, + { + label: 'Events', + id: 'EVENT_RECORDS', + field: 'EVENT_RECORDS', + columnType: FILTER_RECORDS, + columnFilters: [ + { + language: 'kuery', + query: `event: * `, + }, + ], + }, + { + label: 'source ip', + id: 'source.ip', + field: 'source.ip', + }, + { + label: 'destination ip', + id: 'destination.ip', + field: 'destination.ip', + }, + { + label: 'source private ip', + id: 'source.private.ip', + field: 'source.ip', + columnType: FILTER_RECORDS, + columnFilters: [ + { + language: 'kuery', + query: + 'source.ip: "10.0.0.0/8" or source.ip: "192.168.0.0/16" or source.ip: "172.16.0.0/12" or source.ip: "fd00::/8"', + }, + ], + }, + { + label: 'destination private ip', + id: 'destination.private.ip', + field: 'destination.ip', + columnType: FILTER_RECORDS, + columnFilters: [ + { + language: 'kuery', + query: + 'destination.ip: "10.0.0.0/8" or destination.ip: "192.168.0.0/16" or destination.ip: "172.16.0.0/12" or destination.ip: "fd00::/8"', + }, + ], + }, + ], + labels: { 'host.name': 'Hosts', 'url.full': 'URL', 'agent.type': 'Agent type' }, + }; +} + +export function getSecurityAuthenticationsConfig(_config: ConfigProps): SeriesConfig { + return { + reportType: 'event_outcome', + defaultSeriesType: 'bar', + seriesTypes: [], + xAxisColumn: { + sourceField: '@timestamp', + }, + yAxisColumns: [ + { + sourceField: REPORT_METRIC_FIELD, + operationType: 'unique_count', + }, + ], + hasOperationType: false, + filterFields: [], + breakdownFields: [], + baseFilters: [], + palette: { type: 'palette', name: 'status' }, + definitionFields: [{ field: 'host.name' }], + metricOptions: [ + { + label: 'success', + id: 'EVENT_SUCCESS', + columnType: FILTER_RECORDS, + columnFilters: [ + { + language: 'kuery', + query: `event.outcome: "success" and event.category: "authentication"`, + }, + ], + }, + { + label: 'failure', + id: 'EVENT_FAILURE', + columnType: FILTER_RECORDS, + columnFilters: [ + { + language: 'kuery', + query: `event.outcome: "failure" and event.category: "authentication"`, + }, + ], + }, + ], + labels: { 'host.name': 'Hosts', 'url.full': 'URL', 'agent.type': 'Agent type' }, + }; +} + +export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN'; + +export function getSecurityUniqueIpsKPIConfig(_config: ConfigProps): SeriesConfig { + return { + defaultSeriesType: 'bar_horizontal_stacked', + reportType: 'unique_ip', + seriesTypes: ['bar_horizontal_stacked'], + xAxisColumn: { + sourceField: REPORT_METRIC_FIELD, + }, + yAxisColumns: [ + { + sourceField: REPORT_METRIC_FIELD, + operationType: 'unique_count', + }, + ], + hasOperationType: false, + filterFields: [], + breakdownFields: [], + baseFilters: [], + labels: { 'host.name': 'Hosts', 'url.full': 'URL', 'agent.type': 'Agent type' }, + definitionFields: ['host.name'], + metricOptions: [ + { + id: 'source_ip', + field: 'source.ip', + label: 'Unique source IPs', + paramFilters: [{ label: 'Src', input: { query: 'source.ip : *', language: 'kuery' } }], + }, + { + id: 'destination_ip', + field: 'destination.ip', + label: 'Unique destination IPs', + paramFilters: [ + { label: 'Dest', input: { query: 'destination.ip : *', language: 'kuery' } }, + ], + }, + ], + }; +} + +export function getSecurityUniquePrivateIpsKPIConfig(_config: ConfigProps): SeriesConfig { + return { + defaultSeriesType: 'bar_horizontal_stacked', + reportType: 'unique_private_ip', + seriesTypes: ['bar_horizontal_stacked'], + xAxisColumn: { + sourceField: REPORT_METRIC_FIELD, + }, + yAxisColumns: [ + { + sourceField: REPORT_METRIC_FIELD, + operationType: 'unique_count', + }, + ], + hasOperationType: false, + filterFields: [], + breakdownFields: [], + baseFilters: [], + labels: { 'host.name': 'Hosts', 'url.full': 'URL', 'agent.type': 'Agent type' }, + definitionFields: ['host.name'], + metricOptions: [ + { + id: 'source_private_ip', + field: 'source.ip', + label: 'Unique source private IPs', + paramFilters: [ + { + label: 'Src', + input: { + query: + 'source.ip: "10.0.0.0/8" or source.ip: "192.168.0.0/16" or source.ip: "172.16.0.0/12" or source.ip: "fd00::/8"', + language: 'kuery', + }, + }, + ], + }, + { + id: 'destination_private_ip', + field: 'destination.ip', + label: 'Unique destination private IPs', + paramFilters: [ + { + label: 'Dest', + input: { + query: + 'destination.ip: "10.0.0.0/8" or destination.ip: "192.168.0.0/16" or destination.ip: "172.16.0.0/12" or destination.ip: "fd00::/8"', + language: 'kuery', + }, + }, + ], + }, + ], + }; +} + +export function getSingleMetricConfig(_config: ConfigProps): SeriesConfig { + return { + xAxisColumn: {}, + yAxisColumns: [], + breakdownFields: [], + defaultSeriesType: '', + filterFields: [], + seriesTypes: [], + definitionFields: [], + reportType: 'singleMetric', + metricOptions: [ + { + id: 'unique_host', + field: 'host.name', + label: 'Hosts', + }, + { + id: 'auth_success', + field: 'Records_auth_success', + label: 'Success', + columnFilter: { + language: 'kuery', + query: `event.outcome: "success" and event.category: "authentication"`, + }, + }, + { + id: 'auth_failure', + field: 'Records_auth_failure', + label: 'Failure', + columnFilter: { + language: 'kuery', + query: `event.outcome: "failure" and event.category: "authentication"`, + }, + }, + { + id: 'source.ip', + field: 'source.ip', + label: 'Source', + columnFilter: { + language: 'kuery', + query: `source.ip: *`, + }, + }, + { + id: 'destination.ip', + field: 'destination.ip', + label: 'Destination', + columnFilter: { + language: 'kuery', + query: `destination.ip: *`, + }, + }, + { + id: 'network_events', + field: 'Records_network_events', + label: 'Network events', + columnFilter: { + language: 'kuery', + query: `destination.ip: * or source.ip: *`, + }, + }, + { + id: 'records_dns_queries', + field: 'Records_dns_queries', + label: 'DNS queries', + columnFilter: { + language: 'kuery', + query: `dns.question.name: * or suricata.eve.dns.type: "query" or zeek.dns.query: *`, + }, + }, + { + id: 'unique_flow_ids', + field: 'network.community_id', + label: 'Unique flow IDs', + columnFilter: { + language: 'kuery', + query: `source.ip: * or destination.ip: *`, + }, + }, + { + id: 'TLS handshakes', + field: 'Records_tls_handshakes', + label: 'TLS handshakes', + columnFilter: { + language: 'kuery', + query: `(source.ip: * or destination.ip: *) and (tls.version: * or suricata.eve.tls.version: * or zeek.ssl.version: *)`, + }, + }, + { + id: 'source_private_ips', + field: 'records_source_private_ips', + label: 'Source', + columnFilter: { + language: 'kuery', + query: + 'source.ip: "10.0.0.0/8" or source.ip: "192.168.0.0/16" or source.ip: "172.16.0.0/12" or source.ip: "fd00::/8"', + }, + }, + { + id: 'destination_private_ips', + field: 'records_destination_private_ips', + label: 'Destination', + columnFilter: { + language: 'kuery', + query: + 'destination.ip: "10.0.0.0/8" or destination.ip: "192.168.0.0/16" or destination.ip: "172.16.0.0/12" or destination.ip: "fd00::/8"', + }, + }, + ], + labels: {}, + }; +} diff --git a/x-pack/plugins/security_solution/public/app/exploratory_view/security_exploratory_view.tsx b/x-pack/plugins/security_solution/public/app/exploratory_view/security_exploratory_view.tsx new file mode 100644 index 0000000000000..b7924bf730758 --- /dev/null +++ b/x-pack/plugins/security_solution/public/app/exploratory_view/security_exploratory_view.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as React from 'react'; + +import { ExploratoryViewContextProvider, ExploratoryView } from '../../../../observability/public'; +import { + getSecurityKPIConfig, + getSecurityUniqueIpsKPIConfig, + getSingleMetricConfig, + getSecurityAuthenticationsConfig, + getSecurityUniquePrivateIpsKPIConfig, +} from './kpi_over_time_config'; +import { RenderAppProps } from '../types'; +import { getSecurityAlertsKPIConfig } from './alert_kpi_over_time_config'; +// export from obsverabillity +import { AppDataType } from '../../../../observability/public/components/shared/exploratory_view/types'; +import { useSourcererDataView } from '../../common/containers/sourcerer'; + +export const reportConfigMap = { + security: [ + getSecurityKPIConfig, + getSecurityUniqueIpsKPIConfig, + getSingleMetricConfig, + getSecurityAuthenticationsConfig, + getSecurityUniquePrivateIpsKPIConfig, + ], + securityAlerts: [getSecurityAlertsKPIConfig], +}; + +export const indexPatternList = { + security: + 'apm-*-transaction*,traces-apm*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*,.alerts-security.alerts-default', + // 'remote_cluster:.alerts-security.alerts-default,remote_cluster:apm-*-transaction*,remote_cluster:auditbeat-*,remote_cluster:endgame-*,remote_cluster:filebeat-*,remote_cluster:logs-*,remote_cluster:packetbeat-*,remote_cluster:traces-apm*,remote_cluster:winlogbeat-*', + securityAlerts: '.alerts-security.alerts-default-*', +}; + +export const dataTypes = [ + { + id: 'security' as AppDataType, + label: 'Security', + }, + { + id: 'securityAlerts' as AppDataType, + label: 'Security alerts', + }, +]; + +export const reportTypes = [ + { reportType: 'kpi-over-time', label: 'KPI over time' }, + { reportType: 'event_outcome', label: 'bar' }, + { reportType: 'unique_ip', label: 'Unique IPs' }, + { reportType: 'events', label: 'events' }, + { reportType: 'unique_private_ip', label: 'Unique IPs' }, + { reportType: 'singleMetric', label: 'Single metric' }, +]; + +export const SecurityExploratoryView = ({ + setHeaderActionMenu, +}: { + setHeaderActionMenu: RenderAppProps['setHeaderActionMenu']; +}) => { + return ( + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx b/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx index 6afcc649da5f3..66312c68094d3 100644 --- a/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx @@ -86,9 +86,7 @@ export const GlobalHeader = React.memo( > {BUTTON_ADD_DATA} - {showSourcerer && !showTimeline && ( - - )} + diff --git a/x-pack/plugins/security_solution/public/app/index.tsx b/x-pack/plugins/security_solution/public/app/index.tsx index 527b0c96f318e..d35608dcc111c 100644 --- a/x-pack/plugins/security_solution/public/app/index.tsx +++ b/x-pack/plugins/security_solution/public/app/index.tsx @@ -12,6 +12,7 @@ import { Route, Switch } from 'react-router-dom'; import { NotFoundPage } from './404'; import { SecurityApp } from './app'; import { RenderAppProps } from './types'; +import { SecurityExploratoryView } from './exploratory_view/security_exploratory_view'; export const renderApp = ({ element, @@ -35,6 +36,9 @@ export const renderApp = ({ > + + + {subPluginRoutes.map((route, index) => { return ; })} diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx index b0471a72c6ee6..8d156e75eee17 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx @@ -5,9 +5,10 @@ * 2.0. */ -import React, { useEffect, useCallback, useMemo } from 'react'; +import React, { useEffect, useCallback, useMemo, useState } from 'react'; import numeral from '@elastic/numeral'; +import { EuiFlexItem, EuiPanel, EuiSelect } from '@elastic/eui'; import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; import { useGlobalFullScreen } from '../../containers/use_full_screen'; @@ -17,7 +18,15 @@ import * as i18n from './translations'; import { useUiSetting$ } from '../../lib/kibana'; import { MatrixHistogram } from '../matrix_histogram'; import { histogramConfigs } from './histogram_configs'; -import { MatrixHistogramConfigs } from '../matrix_histogram/types'; +import { MatrixHistogramConfigs, MatrixHistogramOption } from '../matrix_histogram/types'; +import { StartServices } from '../../../types'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { STACK_BY } from '../matrix_histogram/translations'; +import { + indexPatternList, + reportConfigMap, +} from '../../../app/exploratory_view/security_exploratory_view'; +import { ReportTypes } from '../../../../../observability/public'; const ID = 'alertsHistogramQuery'; @@ -35,6 +44,22 @@ const AlertsViewComponent: React.FC = ({ const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); const { globalFullScreen } = useGlobalFullScreen(); + const { observability } = useKibana().services; + const ExploratoryViewEmbeddable = observability.ExploratoryViewEmbeddable; + const [selectedStackByOption, setSelectedStackByOption] = useState( + histogramConfigs.defaultStackByOption + ); + + const setSelectedChartOptionCallback = useCallback( + (event: React.ChangeEvent) => { + setSelectedStackByOption( + histogramConfigs.stackByOptions.find((co) => co.value === event.target.value) ?? + histogramConfigs.defaultStackByOption + ); + }, + [] + ); + const getSubtitle = useCallback( (totalCount: number) => `${i18n.SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${i18n.UNIT( @@ -59,6 +84,23 @@ const AlertsViewComponent: React.FC = ({ }; }, [deleteQuery]); + const appendTitle = useMemo( + () => ( + + {histogramConfigs.stackByOptions.length > 1 && ( + + )} + + ), + [selectedStackByOption?.value, setSelectedChartOptionCallback] + ); + return ( <> {!globalFullScreen && ( @@ -72,6 +114,41 @@ const AlertsViewComponent: React.FC = ({ {...alertsHistogramConfigs} /> )} + {!globalFullScreen && ( + + + + )} (({ scope: scopeId } }, []); // always show sourcerer in timeline - return indicesExist || scopeId === SourcererScopeName.timeline ? ( + return ( (({ scope: scopeId } - ) : null; + ); }); Sourcerer.displayName = 'Sourcerer'; diff --git a/x-pack/plugins/security_solution/public/common/components/stat_items/exploratory_charts.tsx b/x-pack/plugins/security_solution/public/common/components/stat_items/exploratory_charts.tsx new file mode 100644 index 0000000000000..70df840ba41ed --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/stat_items/exploratory_charts.tsx @@ -0,0 +1,313 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiPanel, + EuiSplitPanel, +} from '@elastic/eui'; +import styled from 'styled-components'; +import { useDispatch } from 'react-redux'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { StartServices } from '../../../types'; +import { + indexPatternList, + reportConfigMap, +} from '../../../app/exploratory_view/security_exploratory_view'; +import { setAbsoluteRangeDatePicker } from '../../store/inputs/actions'; +import { InputsModelId } from '../../store/inputs/constants'; +import { TimeRange } from '../../../../../../../src/plugins/data/public'; + +const StyledEuiFlexGroup = styled(EuiFlexGroup)` + height: 100%; +`; + +const panelHeight = '280px'; +const metricHeight = '90px'; +interface Props { + from: string; + to: string; + inputsModelId?: InputsModelId; + indexNames: string[]; +} + +export const ExploratoryChartsComponents = ({ + from, + to, + indexNames, + inputsModelId = 'global', +}: Props) => { + const timerange = useMemo( + () => ({ + from: new Date(from).toISOString(), + to: new Date(to).toISOString(), + mode: 'absolute', + }), + [from, to] + ); + + const dispatch = useDispatch(); + const { observability } = useKibana().services; + + const ExploratoryViewEmbeddable = observability.ExploratoryViewEmbeddable; + + const onBrushEnd = useCallback( + ({ range }: { range: number[] }) => { + dispatch( + setAbsoluteRangeDatePicker({ + id: inputsModelId, + from: new Date(range[0]).toISOString(), + to: new Date(range[1]).toISOString(), + }) + ); + }, + [dispatch, inputsModelId] + ); + + indexPatternList.security = indexNames.join(','); + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export const ExploratoryCharts = React.memo(ExploratoryChartsComponents); + +ExploratoryCharts.displayName = 'ExploratoryCharts'; diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/common/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/common/index.tsx index 206b452d83898..83d12dc61a9cb 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/common/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/common/index.tsx @@ -54,11 +54,13 @@ export const HostsKpiBaseComponent = React.memo( } return ( - - {statItemsProps.map((mappedStatItemProps) => ( - - ))} - + <> + + {statItemsProps.map((mappedStatItemProps) => ( + + ))} + + ); }, (prevProps, nextProps) => diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx index 1f854b1328aad..c191eba3ebc3f 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx @@ -16,6 +16,7 @@ import { RiskyHosts } from './risky_hosts'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { useRiskyHosts } from '../../containers/kpi_hosts/risky_hosts'; import { CallOutSwitcher } from '../../../common/components/callouts'; +import { ExploratoryCharts } from '../../../common/components/stat_items/exploratory_charts'; import { RISKY_HOSTS_DOC_LINK } from '../../../overview/components/overview_risky_host_links/risky_hosts_disabled_module'; import * as i18n from './translations'; @@ -93,6 +94,7 @@ export const HostsKpiComponent = React.memo( /> + ); } diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx index b8dcc1dba28a0..a7ad67820f9f8 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx @@ -7,6 +7,7 @@ import { getOr } from 'lodash/fp'; import React, { useEffect } from 'react'; +import { EuiPanel, EuiSpacer } from '@elastic/eui'; import { AuthenticationTable } from '../../components/authentications_table'; import { manageQuery } from '../../../common/components/page/manage_query'; import { useAuthentications } from '../../containers/authentications'; @@ -16,10 +17,15 @@ import { MatrixHistogramMappingTypes, MatrixHistogramConfigs, } from '../../../common/components/matrix_histogram/types'; -import { MatrixHistogram } from '../../../common/components/matrix_histogram'; import { HostsKpiChartColors } from '../../components/kpi_hosts/types'; import * as i18n from '../translations'; import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { StartServices } from '../../../types'; +import { + indexPatternList, + reportConfigMap, +} from '../../../app/exploratory_view/security_exploratory_view'; const AuthenticationTableManage = manageQuery(AuthenticationTable); @@ -85,6 +91,9 @@ const AuthenticationsQueryTabBodyComponent: React.FC startDate, type, }); + const { observability } = useKibana().services; + + const ExploratoryViewEmbeddable = observability.ExploratoryViewEmbeddable; useEffect(() => { return () => { @@ -96,15 +105,46 @@ const AuthenticationsQueryTabBodyComponent: React.FC return ( <> - + + + + = ({ }, [deleteQuery]); const leadingControlColumns = useMemo(() => getDefaultControlColumn(ACTION_BUTTON_COUNT), []); + const { observability } = useKibana().services; + + const ExploratoryViewEmbeddable = observability.ExploratoryViewEmbeddable; + + const [selectedStackByOption, setSelectedStackByOption] = useState( + histogramConfigs.defaultStackByOption + ); + + const setSelectedChartOptionCallback = useCallback( + (event: React.ChangeEvent) => { + setSelectedStackByOption( + histogramConfigs.stackByOptions.find((co) => co.value === event.target.value) ?? + histogramConfigs.defaultStackByOption + ); + }, + [] + ); + const appendTitle = useMemo( + () => ( + + {histogramConfigs.stackByOptions.length > 1 && ( + + )} + + ), + [selectedStackByOption?.value, setSelectedChartOptionCallback] + ); return ( <> @@ -112,6 +154,41 @@ const EventsQueryTabBodyComponent: React.FC = ({ {...histogramConfigs} /> )} + + + + + ( + ({ filterQuery, from, to, inputsModelId = 'global' }) => { + const timerange = useMemo( + () => ({ + from: new Date(from).toISOString(), + to: new Date(to).toISOString(), + mode: 'absolute', + }), + [from, to] + ); + const dispatch = useDispatch(); + + const { observability } = useKibana().services; + + const ExploratoryViewEmbeddable = observability.ExploratoryViewEmbeddable; + + const onBrushEnd = useCallback( + ({ range }: { range: number[] }) => { + dispatch( + setAbsoluteRangeDatePicker({ + id: inputsModelId, + from: new Date(range[0]).toISOString(), + to: new Date(range[1]).toISOString(), + }) + ); + }, + [dispatch, inputsModelId] + ); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } +); + +NetworkKpiEmbeddablesComponent.displayName = 'NetworkKpiEmbeddablesComponent'; diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/types.ts b/x-pack/plugins/security_solution/public/network/components/kpi_network/types.ts index 3be0177557712..a6e14538cedfe 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/types.ts +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/types.ts @@ -7,6 +7,7 @@ import { UpdateDateRange } from '../../../common/components/charts/common'; import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; +import { InputsModelId } from '../../../common/store/inputs/constants'; export interface NetworkKpiProps { filterQuery?: string; @@ -17,3 +18,10 @@ export interface NetworkKpiProps { setQuery: GlobalTimeArgs['setQuery']; skip: boolean; } + +export interface NetworkKpiEmbessablesProps { + filterQuery?: string; + from: string; + to: string; + inputsModelId?: InputsModelId; +} diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx index 7c9b2af515900..ec2b68f5b523c 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx @@ -5,9 +5,10 @@ * 2.0. */ -import React, { useEffect, useCallback, useMemo } from 'react'; +import React, { useEffect, useCallback, useMemo, useState } from 'react'; import { getOr } from 'lodash/fp'; +import { EuiFlexItem, EuiPanel, EuiSelect, EuiSpacer } from '@elastic/eui'; import { NetworkDnsTable } from '../../components/network_dns_table'; import { useNetworkDns } from '../../containers/network_dns'; import { manageQuery } from '../../../common/components/page/manage_query'; @@ -23,6 +24,12 @@ import { MatrixHistogram } from '../../../common/components/matrix_histogram'; import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; import { networkSelectors } from '../../store'; import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { StartServices } from '../../../types'; +import { STACK_BY } from '../../../common/components/matrix_histogram/translations'; +import { reportConfigMap } from '../../../app/exploratory_view/security_exploratory_view'; +import { ReportTypes } from '../../../../../observability/public'; +import { useSourcererDataView } from '../../../common/containers/sourcerer'; const HISTOGRAM_ID = 'networkDnsHistogramQuery'; @@ -37,6 +44,158 @@ const dnsStackByOptions: MatrixHistogramOption[] = [ const DEFAULT_STACK_BY = 'dns.question.registered_domain'; +const topDomainsAttrs = { + title: 'Top domains by dns.question.registered_domain', + description: '', + visualizationType: 'lnsXY', + state: { + visualization: { + legend: { + isVisible: true, + position: 'right', + }, + valueLabels: 'hide', + fittingFunction: 'None', + yLeftExtent: { + mode: 'full', + }, + yRightExtent: { + mode: 'full', + }, + axisTitlesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + tickLabelsVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + labelsOrientation: { + x: 0, + yLeft: 0, + yRight: 0, + }, + gridlinesVisibilitySettings: { + x: true, + yLeft: true, + yRight: true, + }, + preferredSeriesType: 'bar', + layers: [ + { + layerId: 'b1c3efc6-c886-4fba-978f-3b6bb5e7948a', + accessors: ['2a4d5e20-f570-48e4-b9ab-ff3068919377'], + position: 'top', + seriesType: 'bar', + showGridlines: false, + layerType: 'data', + xAccessor: 'd1452b87-0e9e-4fc0-a725-3727a18e0b37', + splitAccessor: 'e8842815-2a45-4c74-86de-c19a391e2424', + }, + ], + }, + query: { + query: '', + language: 'kuery', + }, + filters: [ + { + meta: { + alias: null, + negate: true, + disabled: false, + type: 'phrase', + key: 'dns.question.type', + params: { + query: 'PTR', + }, + indexRefName: 'filter-index-pattern-0', + }, + query: { + match_phrase: { + 'dns.question.type': 'PTR', + }, + }, + $state: { + store: 'appState', + }, + }, + ], + datasourceStates: { + indexpattern: { + layers: { + 'b1c3efc6-c886-4fba-978f-3b6bb5e7948a': { + columns: { + 'd1452b87-0e9e-4fc0-a725-3727a18e0b37': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { + interval: 'auto', + }, + }, + '2a4d5e20-f570-48e4-b9ab-ff3068919377': { + label: 'Unique count of dns.question.registered_domain', + dataType: 'number', + operationType: 'unique_count', + scale: 'ratio', + sourceField: 'dns.question.registered_domain', + isBucketed: false, + }, + 'e8842815-2a45-4c74-86de-c19a391e2424': { + label: 'Top values of dns.question.name', + dataType: 'string', + operationType: 'terms', + scale: 'ordinal', + sourceField: 'dns.question.name', + isBucketed: true, + params: { + size: 6, + orderBy: { + type: 'column', + columnId: '2a4d5e20-f570-48e4-b9ab-ff3068919377', + }, + orderDirection: 'desc', + otherBucket: true, + missingBucket: false, + }, + }, + }, + columnOrder: [ + 'e8842815-2a45-4c74-86de-c19a391e2424', + 'd1452b87-0e9e-4fc0-a725-3727a18e0b37', + '2a4d5e20-f570-48e4-b9ab-ff3068919377', + ], + incompleteColumns: {}, + }, + }, + }, + }, + }, + references: [ + { + type: 'index-pattern', + id: 'filebeat-*', + name: 'indexpattern-datasource-current-indexpattern', + }, + { + type: 'index-pattern', + id: 'logs-*', + name: 'indexpattern-datasource-layer-b1c3efc6-c886-4fba-978f-3b6bb5e7948a', + }, + { + name: 'filter-index-pattern-0', + type: 'index-pattern', + id: 'logs-*', + }, + ], +}; + export const histogramConfigs: Omit = { defaultStackByOption: dnsStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? dnsStackByOptions[0], @@ -62,6 +221,31 @@ const DnsQueryTabBodyComponent: React.FC = ({ (state) => getNetworkDnsSelector(state).isPtrIncluded ); + const { observability } = useKibana().services; + const ExploratoryViewEmbeddable = observability.ExploratoryViewEmbeddable; + const [selectedStackByOption, setSelectedStackByOption] = useState( + histogramConfigs.defaultStackByOption + ); + + const setSelectedChartOptionCallback = useCallback( + (event: React.ChangeEvent) => { + setSelectedStackByOption( + histogramConfigs.stackByOptions.find((co) => co.value === event.target.value) ?? + histogramConfigs.defaultStackByOption + ); + }, + [] + ); + const { patternList, dataViewId } = useSourcererDataView(); + + const customLensAttrs = useMemo( + () => ({ + ...topDomainsAttrs, + references: topDomainsAttrs.references.map((ref) => ({ ...ref, id: dataViewId })), + }), + [dataViewId] + ); + useEffect(() => { return () => { if (deleteQuery) { @@ -83,17 +267,34 @@ const DnsQueryTabBodyComponent: React.FC = ({ type, }); - const getTitle = useCallback( - (option: MatrixHistogramOption) => i18n.DOMAINS_COUNT_BY(option.text), - [] + const title = useMemo( + () => i18n.DOMAINS_COUNT_BY(selectedStackByOption.text), + [selectedStackByOption.text] ); const dnsHistogramConfigs: MatrixHistogramConfigs = useMemo( () => ({ ...histogramConfigs, - title: getTitle, + title, }), - [getTitle] + [title] + ); + + const appendTitle = useMemo( + () => ( + + {histogramConfigs.stackByOptions.length > 1 && ( + + )} + + ), + [selectedStackByOption?.value, setSelectedChartOptionCallback] ); return ( @@ -110,6 +311,44 @@ const DnsQueryTabBodyComponent: React.FC = ({ startDate={startDate} {...dnsHistogramConfigs} /> + + + + + + ( skip={isInitializing || filterQuery === undefined} to={to} /> + + {capabilitiesFetched && !isInitializing ? ( diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index 6a5a0f3b42e04..c901a4544b977 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -42,6 +42,7 @@ import type { Ueba } from './ueba'; import type { LicensingPluginStart, LicensingPluginSetup } from '../../licensing/public'; import type { DashboardStart } from '../../../../src/plugins/dashboard/public'; import type { IndexPatternFieldEditorStart } from '../../../../src/plugins/data_view_field_editor/public'; +import { ObservabilityPublicStart } from '../../observability/public'; export interface SetupPlugins { home?: HomePublicPluginSetup; @@ -69,6 +70,7 @@ export interface StartPlugins { ml?: MlPluginStart; spaces?: SpacesPluginStart; dataViewFieldEditor: IndexPatternFieldEditorStart; + observability: ObservabilityPublicStart; } export type StartServices = CoreStart & diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json index 35be0b19d4521..461358c27fe3b 100644 --- a/x-pack/plugins/uptime/kibana.json +++ b/x-pack/plugins/uptime/kibana.json @@ -5,6 +5,7 @@ "optionalPlugins": ["cloud", "data", "fleet", "home", "ml"], "requiredPlugins": [ "alerting", + "cases", "embeddable", "encryptedSavedObjects", "features", diff --git a/x-pack/plugins/uptime/public/apps/uptime_app.tsx b/x-pack/plugins/uptime/public/apps/uptime_app.tsx index 703b9a3d6123b..5df0d1a00f905 100644 --- a/x-pack/plugins/uptime/public/apps/uptime_app.tsx +++ b/x-pack/plugins/uptime/public/apps/uptime_app.tsx @@ -120,6 +120,7 @@ const Application = (props: UptimeAppProps) => { inspector: startPlugins.inspector, triggersActionsUi: startPlugins.triggersActionsUi, observability: startPlugins.observability, + cases: startPlugins.cases, }} >