[RAC] T-Grid is moving to a new home (#100265)

* wip

* First pass at standalone and embedded redux stores and usage

* wip

* First pass at standalone and embedded redux stores and usage

* wip

* clean up

* wip

* refact(NA): remove extra pkg_npm target and add specific target folders on @kbn/i18n

* cleanup

* - fixes type errors in tests

* WIP remove use_manage_timeline

* wip add query + selector

* finishing integrating timeline manage context from redux

* integrating t-grid in security solution

* fix RowRender type

* WIP begin to move components from package to plugin

* integration of t-grid inside of security solution

* wip to make redux work

* little trick to make  it render

* - fixes a few type errors

* better integration betwen tgrid and security solutions

* bringing back tsconfig on timeline

* wip integration t-grid in observability

* fix types

* fix type in security solutions

* add type to import + trie dto get the bundle size as small as possible

* fix type in integration test

* fix type in integration test

* - fix tests

* clean up to use technical fields

* - fixes unit tests

* - mocks the `useDateFormat` function of the `useKibana` service to fix unit tests

* fix t-grid settings vs create timeline + fix inspect button

* fix last suites test

* Update unit tests, snapshots and lint

* Fix bad merge

* fix plugin export

* Fix some failing tests

* fix unit tets in timelines plugins

* fix latest test

* fix i18n

* free obs from t-grid

* Fix timeline functional plugin types

* fix store provider

* Update failing defaultHeader test

* Fix i18n usage in security solution

* Fix remaining i18n errors in timelines plugin

* Dedupe common shared types

* move drag and drop utils in package to avoid duplication

* More shared type cleanup

* add feature flag

* review I

* fix merge  with master

* fix i18n translation

* More type deduping

* Use @kbn/common-utils, fix remaining types

* fix types

* fix tests

* missing type

* fix cypress tests

Co-authored-by: Kevin Qualters <kevin.qualters@elastic.co>
Co-authored-by: Tiago Costa <tiagoffcc@hotmail.com>
Co-authored-by: Andrew Goldstein <andrew.goldstein@elastic.co>
This commit is contained in:
Xavier Mouligneau 2021-06-22 18:56:33 -04:00 committed by GitHub
parent 369127e8c2
commit 4fa3dc46cb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
528 changed files with 60237 additions and 3993 deletions

View file

@ -893,6 +893,8 @@ module.exports = {
files: [
'x-pack/plugins/security_solution/public/**/*.{js,mjs,ts,tsx}',
'x-pack/plugins/security_solution/common/**/*.{js,mjs,ts,tsx}',
'x-pack/plugins/timelines/public/**/*.{js,mjs,ts,tsx}',
'x-pack/plugins/timelines/common/**/*.{js,mjs,ts,tsx}',
],
rules: {
'import/no-nodejs-modules': 'error',
@ -907,7 +909,10 @@ module.exports = {
},
{
// typescript only for front and back end
files: ['x-pack/plugins/security_solution/**/*.{ts,tsx}'],
files: [
'x-pack/plugins/security_solution/**/*.{ts,tsx}',
'x-pack/plugins/timelines/**/*.{ts,tsx}',
],
rules: {
'@typescript-eslint/no-this-alias': 'error',
'@typescript-eslint/no-explicit-any': 'error',
@ -917,7 +922,10 @@ module.exports = {
},
{
// typescript and javascript for front and back end
files: ['x-pack/plugins/security_solution/**/*.{js,mjs,ts,tsx}'],
files: [
'x-pack/plugins/security_solution/**/*.{js,mjs,ts,tsx}',
'x-pack/plugins/timelines/**/*.{js,mjs,ts,tsx}',
],
plugins: ['eslint-plugin-node', 'react'],
env: {
jest: true,

View file

@ -149,6 +149,7 @@
"@kbn/securitysolution-list-api": "link:bazel-bin/packages/kbn-securitysolution-list-api",
"@kbn/securitysolution-list-hooks": "link:bazel-bin/packages/kbn-securitysolution-list-hooks",
"@kbn/securitysolution-list-utils": "link:bazel-bin/packages/kbn-securitysolution-list-utils",
"@kbn/securitysolution-t-grid": "link:bazel-bin/packages/kbn-securitysolution-t-grid",
"@kbn/securitysolution-utils": "link:bazel-bin/packages/kbn-securitysolution-utils",
"@kbn/server-http-tools": "link:bazel-bin/packages/kbn-server-http-tools",
"@kbn/server-route-repository": "link:bazel-bin/packages/kbn-server-route-repository",
@ -217,6 +218,8 @@
"cytoscape-dagre": "^2.2.2",
"d3": "3.5.17",
"d3-array": "1.2.4",
"d3-cloud": "1.2.5",
"d3-interpolate": "^3.0.1",
"d3-scale": "1.0.7",
"d3-shape": "^1.1.0",
"d3-time": "^1.1.0",
@ -511,6 +514,7 @@
"@types/cytoscape": "^3.14.0",
"@types/d3": "^3.5.43",
"@types/d3-array": "^1.2.7",
"@types/d3-interpolate": "^2.0.0",
"@types/d3-scale": "^2.1.1",
"@types/d3-shape": "^1.3.1",
"@types/d3-time": "^1.0.10",

View file

@ -3,7 +3,7 @@
filegroup(
name = "build",
srcs = [
"//packages/elastic-datemath:build",
"//packages/elastic-datemath:build",
"//packages/elastic-eslint-config-kibana:build",
"//packages/elastic-safer-lodash-set:build",
"//packages/kbn-ace:build",
@ -41,6 +41,7 @@ filegroup(
"//packages/kbn-securitysolution-list-utils:build",
"//packages/kbn-securitysolution-utils:build",
"//packages/kbn-securitysolution-es-utils:build",
"//packages/kbn-securitysolution-t-grid:build",
"//packages/kbn-securitysolution-hook-utils:build",
"//packages/kbn-server-http-tools:build",
"//packages/kbn-server-route-repository:build",

View file

@ -67,7 +67,7 @@ pageLoadAssetSize:
searchprofiler: 67080
security: 95864
securityOss: 30806
securitySolution: 76000
securitySolution: 217673
share: 99061
snapshotRestore: 79032
spaces: 57868
@ -107,7 +107,7 @@ pageLoadAssetSize:
dataVisualizer: 27530
banners: 17946
mapsEms: 26072
timelines: 28613
timelines: 230410
screenshotMode: 17856
visTypePie: 35583
cases: 144442

View file

@ -0,0 +1,125 @@
load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project")
load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm")
PKG_BASE_NAME = "kbn-securitysolution-t-grid"
PKG_REQUIRE_NAME = "@kbn/securitysolution-t-grid"
SOURCE_FILES = glob(
[
"src/**/*.ts",
"src/**/*.tsx",
],
exclude = [
"**/*.test.*",
"**/*.mock.*",
],
)
SRCS = SOURCE_FILES
filegroup(
name = "srcs",
srcs = SRCS,
)
NPM_MODULE_EXTRA_FILES = [
"react/package.json",
"package.json",
"README.md",
]
SRC_DEPS = [
"//packages/kbn-babel-preset",
"//packages/kbn-dev-utils",
"//packages/kbn-i18n",
"@npm//@babel/core",
"@npm//babel-loader",
"@npm//enzyme",
"@npm//jest",
"@npm//lodash",
"@npm//react",
"@npm//react-beautiful-dnd",
"@npm//tslib",
]
TYPES_DEPS = [
"@npm//typescript",
"@npm//@types/enzyme",
"@npm//@types/jest",
"@npm//@types/lodash",
"@npm//@types/node",
"@npm//@types/react",
"@npm//@types/react-beautiful-dnd",
]
DEPS = SRC_DEPS + TYPES_DEPS
ts_config(
name = "tsconfig",
src = "tsconfig.json",
deps = [
"//:tsconfig.base.json",
],
)
ts_config(
name = "tsconfig_browser",
src = "tsconfig.browser.json",
deps = [
"//:tsconfig.base.json",
"//:tsconfig.browser.json",
],
)
ts_project(
name = "tsc",
args = ["--pretty"],
srcs = SRCS,
deps = DEPS,
declaration = True,
declaration_dir = "target_types",
declaration_map = True,
incremental = True,
out_dir = "target_node",
root_dir = "src",
source_map = True,
tsconfig = ":tsconfig",
)
ts_project(
name = "tsc_browser",
args = ['--pretty'],
srcs = SRCS,
deps = DEPS,
allow_js = True,
declaration = False,
incremental = True,
out_dir = "target_web",
source_map = True,
root_dir = "src",
tsconfig = ":tsconfig_browser",
)
js_library(
name = PKG_BASE_NAME,
package_name = PKG_REQUIRE_NAME,
srcs = NPM_MODULE_EXTRA_FILES,
visibility = ["//visibility:public"],
deps = [":tsc", ":tsc_browser"] + DEPS,
)
pkg_npm(
name = "npm_module",
deps = [
":%s" % PKG_BASE_NAME,
],
)
filegroup(
name = "build",
srcs = [
":npm_module",
],
visibility = ["//visibility:public"],
)

View file

@ -0,0 +1,3 @@
# kbn-securitysolution-t-grid
We do not want to create circular dependencies between security_solution and timelines plugins. Therefore , we will use this packages to share components between these two plugins.

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
env: {
web: {
presets: ['@kbn/babel-preset/webpack_preset'],
},
node: {
presets: ['@kbn/babel-preset/node_preset'],
},
},
ignore: ['**/*.test.ts', '**/*.test.tsx'],
};

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-securitysolution-t-grid'],
};

View file

@ -0,0 +1,10 @@
{
"name": "@kbn/securitysolution-t-grid",
"version": "1.0.0",
"description": "security solution t-grid packages will allow sharing components between timelines and security_solution plugin until we transfer all functionality to timelines plugin",
"license": "SSPL-1.0 OR Elastic License 2.0",
"browser": "./target_web/browser.js",
"main": "./target_node/index.js",
"types": "./target_types/index.d.ts",
"private": true
}

View file

@ -0,0 +1,5 @@
{
"browser": "../target_web/react",
"main": "../target_node/react",
"types": "../target_types/react/index.d.ts"
}

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const HIGHLIGHTED_DROP_TARGET_CLASS_NAME = 'highlighted-drop-target';
export const EMPTY_PROVIDERS_GROUP_CLASS_NAME = 'empty-providers-group';
/** The draggable will move this many pixels via the keyboard when the arrow key is pressed */
export const KEYBOARD_DRAG_OFFSET = 20;
export const DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME = 'draggable-keyboard-wrapper';
export const ROW_RENDERER_CLASS_NAME = 'row-renderer';
export const NOTES_CONTAINER_CLASS_NAME = 'notes-container';
export const NOTE_CONTENT_CLASS_NAME = 'note-content';
/** This class is added to the document body while dragging */
export const IS_DRAGGING_CLASS_NAME = 'is-dragging';
export const HOVER_ACTIONS_ALWAYS_SHOW_CLASS_NAME = 'hover-actions-always-show';

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './constants';
export * from './utils';
export * from './mock';

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './mock_event_details';

View file

@ -1,8 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const eventHit = {

View file

@ -0,0 +1,42 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { has } from 'lodash/fp';
export interface AppError extends Error {
body: {
message: string;
};
}
export interface KibanaError extends AppError {
body: {
message: string;
statusCode: number;
};
}
export interface SecurityAppError extends AppError {
body: {
message: string;
status_code: number;
};
}
export const isKibanaError = (error: unknown): error is KibanaError =>
has('message', error) && has('body.message', error) && has('body.statusCode', error);
export const isSecurityAppError = (error: unknown): error is SecurityAppError =>
has('message', error) && has('body.message', error) && has('body.status_code', error);
export const isAppError = (error: unknown): error is AppError =>
isKibanaError(error) || isSecurityAppError(error);
export const isNotFoundError = (error: unknown) =>
(isKibanaError(error) && error.body.statusCode === 404) ||
(isSecurityAppError(error) && error.body.status_code === 404);

View file

@ -0,0 +1,133 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { DropResult } from 'react-beautiful-dnd';
export const draggableIdPrefix = 'draggableId';
export const droppableIdPrefix = 'droppableId';
export const draggableContentPrefix = `${draggableIdPrefix}.content.`;
export const draggableTimelineProvidersPrefix = `${draggableIdPrefix}.timelineProviders.`;
export const draggableFieldPrefix = `${draggableIdPrefix}.field.`;
export const droppableContentPrefix = `${droppableIdPrefix}.content.`;
export const droppableFieldPrefix = `${droppableIdPrefix}.field.`;
export const droppableTimelineProvidersPrefix = `${droppableIdPrefix}.timelineProviders.`;
export const droppableTimelineColumnsPrefix = `${droppableIdPrefix}.timelineColumns.`;
export const droppableTimelineFlyoutBottomBarPrefix = `${droppableIdPrefix}.flyoutButton.`;
export const getDraggableId = (dataProviderId: string): string =>
`${draggableContentPrefix}${dataProviderId}`;
export const getDraggableFieldId = ({
contextId,
fieldId,
}: {
contextId: string;
fieldId: string;
}): string => `${draggableFieldPrefix}${escapeContextId(contextId)}.${escapeFieldId(fieldId)}`;
export const getTimelineProviderDroppableId = ({
groupIndex,
timelineId,
}: {
groupIndex: number;
timelineId: string;
}): string => `${droppableTimelineProvidersPrefix}${timelineId}.group.${groupIndex}`;
export const getTimelineProviderDraggableId = ({
dataProviderId,
groupIndex,
timelineId,
}: {
dataProviderId: string;
groupIndex: number;
timelineId: string;
}): string =>
`${draggableTimelineProvidersPrefix}${timelineId}.group.${groupIndex}.${dataProviderId}`;
export const getDroppableId = (visualizationPlaceholderId: string): string =>
`${droppableContentPrefix}${visualizationPlaceholderId}`;
export const sourceIsContent = (result: DropResult): boolean =>
result.source.droppableId.startsWith(droppableContentPrefix);
export const sourceAndDestinationAreSameTimelineProviders = (result: DropResult): boolean => {
const regex = /^droppableId\.timelineProviders\.(\S+)\./;
const sourceMatches = result.source.droppableId.match(regex) || [];
const destinationMatches =
(result.destination && result.destination.droppableId.match(regex)) || [];
return (
sourceMatches.length >= 2 &&
destinationMatches.length >= 2 &&
sourceMatches[1] === destinationMatches[1]
);
};
export const draggableIsContent = (result: DropResult | { draggableId: string }): boolean =>
result.draggableId.startsWith(draggableContentPrefix);
export const draggableIsField = (result: DropResult | { draggableId: string }): boolean =>
result.draggableId.startsWith(draggableFieldPrefix);
export const reasonIsDrop = (result: DropResult): boolean => result.reason === 'DROP';
export const destinationIsTimelineProviders = (result: DropResult): boolean =>
result.destination != null &&
result.destination.droppableId.startsWith(droppableTimelineProvidersPrefix);
export const destinationIsTimelineColumns = (result: DropResult): boolean =>
result.destination != null &&
result.destination.droppableId.startsWith(droppableTimelineColumnsPrefix);
export const destinationIsTimelineButton = (result: DropResult): boolean =>
result.destination != null &&
result.destination.droppableId.startsWith(droppableTimelineFlyoutBottomBarPrefix);
export const getProviderIdFromDraggable = (result: DropResult): string =>
result.draggableId.substring(result.draggableId.lastIndexOf('.') + 1);
export const getFieldIdFromDraggable = (result: DropResult): string =>
unEscapeFieldId(result.draggableId.substring(result.draggableId.lastIndexOf('.') + 1));
export const escapeDataProviderId = (path: string) => path.replace(/\./g, '_');
export const escapeContextId = (path: string) => path.replace(/\./g, '_');
export const escapeFieldId = (path: string) => path.replace(/\./g, '!!!DOT!!!');
export const unEscapeFieldId = (path: string) => path.replace(/!!!DOT!!!/g, '.');
export const providerWasDroppedOnTimeline = (result: DropResult): boolean =>
reasonIsDrop(result) &&
draggableIsContent(result) &&
sourceIsContent(result) &&
destinationIsTimelineProviders(result);
export const userIsReArrangingProviders = (result: DropResult): boolean =>
reasonIsDrop(result) && sourceAndDestinationAreSameTimelineProviders(result);
export const fieldWasDroppedOnTimelineColumns = (result: DropResult): boolean =>
reasonIsDrop(result) && draggableIsField(result) && destinationIsTimelineColumns(result);
/**
* Prevents fields from being dragged or dropped to any area other than column
* header drop zone in the timeline
*/
export const DRAG_TYPE_FIELD = 'drag-type-field';
/** This class is added to the document body while timeline field dragging */
export const IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME = 'is-timeline-field-dragging';

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './api';
export * from './drag_and_drop';

View file

@ -0,0 +1,23 @@
{
"extends": "../../tsconfig.browser.json",
"compilerOptions": {
"allowJs": true,
"incremental": true,
"outDir": "./target_web",
"declaration": false,
"isolatedModules": true,
"sourceMap": true,
"sourceRoot": "../../../../../packages/kbn-securitysolution-t-grid/src",
"types": [
"jest",
"node"
],
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
],
"exclude": [
"**/__fixtures__/**/*"
]
}

View file

@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"incremental": true,
"outDir": "target",
"rootDir": "src",
"sourceMap": true,
"sourceRoot": "../../../../packages/kbn-securitysolution-t-grid/src",
"types": [
"jest",
"node"
]
},
"include": [
"src/**/*"
]
}

View file

@ -94,7 +94,7 @@ module.exports = {
transformIgnorePatterns: [
// ignore all node_modules except monaco-editor and react-monaco-editor which requires babel transforms to handle dynamic import()
// since ESM modules are not natively supported in Jest yet (https://github.com/facebook/jest/issues/4842)
'[/\\\\]node_modules(?![\\/\\\\](monaco-editor|react-monaco-editor))[/\\\\].+\\.js$',
'[/\\\\]node_modules(?![\\/\\\\](monaco-editor|react-monaco-editor|d3-interpolate|d3-color))[/\\\\].+\\.js$',
'packages/kbn-pm/dist/index.js',
],

View file

@ -70,7 +70,6 @@
{ "path": "./src/plugins/visualize/tsconfig.json" },
{ "path": "./src/plugins/index_pattern_management/tsconfig.json" },
{ "path": "./src/plugins/index_pattern_field_editor/tsconfig.json" },
{ "path": "./x-pack/plugins/actions/tsconfig.json" },
{ "path": "./x-pack/plugins/alerting/tsconfig.json" },
{ "path": "./x-pack/plugins/apm/tsconfig.json" },

View file

@ -105,6 +105,7 @@
{ "path": "./x-pack/plugins/stack_alerts/tsconfig.json" },
{ "path": "./x-pack/plugins/task_manager/tsconfig.json" },
{ "path": "./x-pack/plugins/telemetry_collection_xpack/tsconfig.json" },
{ "path": "./x-pack/plugins/timelines/tsconfig.json" },
{ "path": "./x-pack/plugins/transform/tsconfig.json" },
{ "path": "./x-pack/plugins/translations/tsconfig.json" },
{ "path": "./x-pack/plugins/triggers_actions_ui/tsconfig.json" },

View file

@ -15,6 +15,7 @@ const allowedExperimentalValues = Object.freeze({
trustedAppsByPolicyEnabled: false,
metricsEntitiesEnabled: false,
ruleRegistryEnabled: false,
tGridEnabled: false,
});
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;

View file

@ -4,3 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './types';
export * from './search_strategy';
export * from './utility_types';

View file

@ -4,52 +4,27 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { estypes } from '@elastic/elasticsearch';
import { IEsSearchResponse } from '../../../../../../src/plugins/data/common';
export type {
Inspect,
SortField,
TimerangeInput,
PaginationInputPaginated,
DocValueFields,
CursorType,
TotalValue,
} from '../../../../timelines/common';
export { Direction } from '../../../../timelines/common';
export type Maybe<T> = T | null;
export type SearchHit = IEsSearchResponse<object>['rawResponse']['hits']['hits'][0];
export interface TotalValue {
value: number;
relation: string;
}
export interface Inspect {
dsl: string[];
}
export interface PageInfoPaginated {
activePage: number;
fakeTotalCount: number;
showMorePagesIndicator: boolean;
}
export interface CursorType {
value?: Maybe<string>;
tiebreaker?: Maybe<string>;
}
export enum Direction {
asc = 'asc',
desc = 'desc',
}
export interface SortField<Field = string> {
field: Field;
direction: Direction;
}
export interface TimerangeInput {
/** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */
interval: string;
/** The end of the timerange */
to: string;
/** The beginning of the timerange */
from: string;
}
export interface PaginationInput {
/** The limit parameter allows you to configure the maximum amount of items to be returned */
limit: number;
@ -59,19 +34,6 @@ export interface PaginationInput {
tiebreaker?: Maybe<string>;
}
export interface PaginationInputPaginated {
/** The activePage parameter defines the page of results you want to fetch */
activePage: number;
/** The cursorStart parameter defines the start of the results to be displayed */
cursorStart: number;
/** The fakePossibleCount parameter determines the total count in order to show 5 additional pages */
fakePossibleCount: number;
/** The querySize parameter is the number of items to be returned */
querySize: number;
}
export type DocValueFields = estypes.SearchDocValueField;
export interface Explanation {
value: number;
description: string;
@ -111,13 +73,3 @@ export interface GenericBuckets {
}
export type StringOrNumber = string | number;
export interface TimerangeFilter {
range: {
[timestamp: string]: {
gte: string;
lte: string;
format: string;
};
};
}

View file

@ -8,3 +8,4 @@
export * from './common';
export * from './security_solution';
export * from './timeline';
export * from './index_fields';

View file

@ -5,37 +5,10 @@
* 2.0.
*/
import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
import { Ecs } from '../../../../ecs';
import { CursorType, Inspect, Maybe, PaginationInputPaginated } from '../../../common';
import { TimelineRequestOptionsPaginated } from '../..';
export interface TimelineEdges {
node: TimelineItem;
cursor: CursorType;
}
export interface TimelineItem {
_id: string;
_index?: Maybe<string>;
data: TimelineNonEcsData[];
ecs: Ecs;
}
export interface TimelineNonEcsData {
field: string;
value?: Maybe<string[]>;
}
export interface TimelineEventsAllStrategyResponse extends IEsSearchResponse {
edges: TimelineEdges[];
totalCount: number;
pageInfo: Pick<PaginationInputPaginated, 'activePage' | 'querySize'>;
inspect?: Maybe<Inspect>;
}
export interface TimelineEventsAllRequestOptions extends TimelineRequestOptionsPaginated {
fields: string[] | Array<{ field: string; include_unmapped: boolean }>;
fieldRequested: string[];
language: 'eql' | 'kuery' | 'lucene';
}
export type {
TimelineEdges,
TimelineItem,
TimelineNonEcsData,
TimelineEventsAllStrategyResponse,
TimelineEventsAllRequestOptions,
} from '../../../../../../timelines/common';

View file

@ -5,22 +5,8 @@
* 2.0.
*/
import { Ecs } from '../../../../ecs';
import { CursorType, Maybe } from '../../../common';
export interface TimelineEdges {
node: TimelineItem;
cursor: CursorType;
}
export interface TimelineItem {
_id: string;
_index?: Maybe<string>;
data: TimelineNonEcsData[];
ecs: Ecs;
}
export interface TimelineNonEcsData {
field: string;
value?: Maybe<string[] | string>;
}
export type {
TimelineEdges,
TimelineItem,
TimelineNonEcsData,
} from '../../../../../../timelines/common';

View file

@ -5,27 +5,8 @@
* 2.0.
*/
import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
import { Inspect, Maybe } from '../../../common';
import { TimelineRequestOptionsPaginated } from '../..';
export interface TimelineEventsDetailsItem {
ariaRowindex?: Maybe<number>;
category?: string;
field: string;
values?: Maybe<string[]>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
originalValue?: Maybe<any>;
isObjectArray: boolean;
}
export interface TimelineEventsDetailsStrategyResponse extends IEsSearchResponse {
data?: Maybe<TimelineEventsDetailsItem[]>;
inspect?: Maybe<Inspect>;
}
export interface TimelineEventsDetailsRequestOptions
extends Partial<TimelineRequestOptionsPaginated> {
indexName: string;
eventId: string;
}
export type {
TimelineEventsDetailsItem,
TimelineEventsDetailsStrategyResponse,
TimelineEventsDetailsRequestOptions,
} from '../../../../../../timelines/common';

View file

@ -5,43 +5,10 @@
* 2.0.
*/
import { EuiComboBoxOptionOption } from '@elastic/eui';
import {
EqlSearchStrategyRequest,
EqlSearchStrategyResponse,
} from '../../../../../../../../src/plugins/data/common';
import { Inspect, Maybe, PaginationInputPaginated } from '../../..';
import { TimelineEdges, TimelineEventsAllRequestOptions } from '../..';
import { EqlSearchResponse } from '../../../../detection_engine/types';
export interface TimelineEqlRequestOptions
extends EqlSearchStrategyRequest,
Omit<TimelineEventsAllRequestOptions, 'params'> {
eventCategoryField?: string;
tiebreakerField?: string;
timestampField?: string;
size?: number;
}
export interface TimelineEqlResponse extends EqlSearchStrategyResponse<EqlSearchResponse<unknown>> {
edges: TimelineEdges[];
totalCount: number;
pageInfo: Pick<PaginationInputPaginated, 'activePage' | 'querySize'>;
inspect: Maybe<Inspect>;
}
export interface EqlOptionsData {
keywordFields: EuiComboBoxOptionOption[];
dateFields: EuiComboBoxOptionOption[];
nonDateFields: EuiComboBoxOptionOption[];
}
export interface EqlOptionsSelected {
eventCategoryField?: string;
tiebreakerField?: string;
timestampField?: string;
query?: string;
size?: number;
}
export type FieldsEqlOptions = keyof EqlOptionsSelected;
export type {
TimelineEqlRequestOptions,
TimelineEqlResponse,
EqlOptionsData,
EqlOptionsSelected,
FieldsEqlOptions,
} from '../../../../../../timelines/common';

View file

@ -5,38 +5,11 @@
* 2.0.
*/
import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common';
import { Inspect, Maybe } from '../../../common';
import { TimelineRequestBasicOptions } from '../..';
export { LastEventIndexKey } from '../../../../../../timelines/common';
export enum LastEventIndexKey {
hostDetails = 'hostDetails',
hosts = 'hosts',
ipDetails = 'ipDetails',
network = 'network',
}
export interface LastTimeDetails {
hostName?: Maybe<string>;
ip?: Maybe<string>;
}
export interface TimelineEventsLastEventTimeStrategyResponse extends IEsSearchResponse {
lastSeen: Maybe<string>;
inspect?: Maybe<Inspect>;
}
export interface TimelineKpiStrategyResponse extends IEsSearchResponse {
destinationIpCount: number;
inspect?: Maybe<Inspect>;
hostCount: number;
processCount: number;
sourceIpCount: number;
userCount: number;
}
export interface TimelineEventsLastEventTimeRequestOptions
extends Omit<TimelineRequestBasicOptions, 'filterQuery' | 'timerange'> {
indexKey: LastEventIndexKey;
details: LastTimeDetails;
}
export type {
LastTimeDetails,
TimelineEventsLastEventTimeStrategyResponse,
TimelineKpiStrategyResponse,
TimelineEventsLastEventTimeRequestOptions,
} from '../../../../../../timelines/common';

View file

@ -24,7 +24,12 @@ import {
SortField,
Maybe,
} from '../common';
import { DataProviderType, TimelineType, TimelineStatus } from '../../types/timeline';
import {
DataProviderType,
TimelineType,
TimelineStatus,
RowRendererId,
} from '../../types/timeline';
export * from './events';
@ -165,25 +170,6 @@ export interface SortTimelineInput {
sortDirection?: Maybe<string>;
}
export enum RowRendererId {
alerts = 'alerts',
auditd = 'auditd',
auditd_file = 'auditd_file',
library = 'library',
netflow = 'netflow',
plain = 'plain',
registry = 'registry',
suricata = 'suricata',
system = 'system',
system_dns = 'system_dns',
system_endgame_process = 'system_endgame_process',
system_file = 'system_file',
system_fim = 'system_fim',
system_security_event = 'system_security_event',
system_socket = 'system_socket',
zeek = 'zeek',
}
export interface TimelineInput {
columns?: Maybe<ColumnHeaderInput[]>;
dataProviders?: Maybe<DataProviderInput[]>;

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './timeline';

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export type {
ActionProps,
HeaderActionProps,
GenericActionRowCellRenderProps,
HeaderCellRender,
RowCellRender,
ControlColumnProps,
} from '../../../../../timelines/common';

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export type { CellValueElementProps } from '../../../../../timelines/common';

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export type {
ColumnHeaderType,
ColumnId,
ColumnHeaderOptions,
ColumnRenderer,
} from '../../../../../timelines/common';

View file

@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { IS_OPERATOR, EXISTS_OPERATOR } from '../../../../../timelines/common';
export type {
QueryOperator,
DataProviderType,
QueryMatch,
DataProvider,
DataProvidersAnd,
} from '../../../../../timelines/common';

View file

@ -23,6 +23,13 @@ import { FlowTarget } from '../../search_strategy/security_solution/network';
import { errorSchema } from '../../detection_engine/schemas/response/error_schema';
import { Direction, Maybe } from '../../search_strategy';
export * from './actions';
export * from './cells';
export * from './columns';
export * from './data_provider';
export * from './rows';
export * from './store';
/*
* ColumnHeader Types
*/
@ -492,6 +499,11 @@ export type TimelineExpandedDetail = {
[tab in TimelineTabs]?: TimelineExpandedDetailType;
};
export type ToggleDetailPanel = TimelineExpandedDetailType & {
tabType?: TimelineTabs;
timelineId: string;
};
export const pageInfoTimeline = runtimeTypes.type({
pageIndex: runtimeTypes.number,
pageSize: runtimeTypes.number,

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export type { RowRenderer } from '../../../../../timelines/common';

View file

@ -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 {
ColumnHeaderOptions,
ColumnId,
RowRendererId,
TimelineExpandedDetail,
TimelineTypeLiteral,
} from '.';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { Filter } from '../../../../../../src/plugins/data/public';
import { Direction } from '../../search_strategy';
import { DataProvider } from './data_provider';
export type KueryFilterQueryKind = 'kuery' | 'lucene' | 'eql';
export interface KueryFilterQuery {
kind: KueryFilterQueryKind;
expression: string;
}
export interface SerializedFilterQuery {
kuery: KueryFilterQuery | null;
serializedQuery: string;
}
export type SortDirection = 'none' | 'asc' | 'desc' | Direction;
export interface SortColumnTimeline {
columnId: string;
columnType: string;
sortDirection: SortDirection;
}
export interface TimelinePersistInput {
id: string;
dataProviders?: DataProvider[];
dateRange?: {
start: string;
end: string;
};
excludedRowRendererIds?: RowRendererId[];
expandedDetail?: TimelineExpandedDetail;
filters?: Filter[];
columns: ColumnHeaderOptions[];
itemsPerPage?: number;
indexNames: string[];
kqlQuery?: {
filterQuery: SerializedFilterQuery | null;
};
show?: boolean;
sort?: SortColumnTimeline[];
showCheckboxes?: boolean;
timelineType?: TimelineTypeLiteral;
templateTimelineId?: string | null;
templateTimelineVersion?: number | null;
}
/** Invoked when a column is sorted */
export type OnColumnSorted = (sorted: { columnId: ColumnId; sortDirection: SortDirection }) => void;
export type OnColumnsSorted = (
sorted: Array<{ columnId: ColumnId; sortDirection: SortDirection }>
) => void;
export type OnColumnRemoved = (columnId: ColumnId) => void;
export type OnColumnResized = ({ columnId, delta }: { columnId: ColumnId; delta: number }) => void;
/** Invoked when a user clicks to load more item */
export type OnChangePage = (nextPage: number) => void;
/** Invoked when a user checks/un-checks a row */
export type OnRowSelected = ({
eventIds,
isSelected,
}: {
eventIds: string[];
isSelected: boolean;
}) => void;
/** Invoked when a user checks/un-checks the select all checkbox */
export type OnSelectAll = ({ isSelected }: { isSelected: boolean }) => void;
/** Invoked when columns are updated */
export type OnUpdateColumns = (columns: ColumnHeaderOptions[]) => void;
/** Invoked when a user pins an event */
export type OnPinEvent = (eventId: string) => void;
/** Invoked when a user unpins an event */
export type OnUnPinEvent = (eventId: string) => void;

View file

@ -7,7 +7,7 @@
import { EventHit, EventSource } from '../search_strategy';
import { getDataFromFieldsHits, getDataFromSourceHits, getDataSafety } from './field_formatters';
import { eventDetailsFormattedFields, eventHit } from './mock_event_details';
import { eventDetailsFormattedFields, eventHit } from '@kbn/securitysolution-t-grid';
describe('Events Details Helpers', () => {
const fields: EventHit['fields'] = eventHit.fields;

View file

@ -45,7 +45,7 @@ describe('Overview Page', () => {
describe('with no data', () => {
it('Splash screen should be here', () => {
cy.stubSearchStrategyApi(emptyInstance, undefined, 'securitySolutionIndexFields');
cy.stubSearchStrategyApi(emptyInstance, undefined, 'indexFields');
loginAndWaitForPage(OVERVIEW_URL);
cy.get(OVERVIEW_EMPTY_PAGE).should('be.visible');
});

View file

@ -35,7 +35,7 @@ Cypress.Commands.add(
'stubSearchStrategyApi',
function (stubObject, factoryQueryType, searchStrategyName = 'securitySolutionSearchStrategy') {
cy.intercept('POST', '/internal/bsearch', (req) => {
if (searchStrategyName === 'securitySolutionIndexFields') {
if (searchStrategyName === 'indexFields') {
req.reply(stubObject.rawResponse);
} else if (factoryQueryType === 'overviewHost') {
req.reply(stubObject.overviewHost);

View file

@ -17,6 +17,7 @@
"inspector",
"licensing",
"maps",
"timelines",
"triggersActionsUi",
"uiActions"
],

View file

@ -21,7 +21,6 @@ import { GlobalToaster, ManageGlobalToaster } from '../common/components/toaster
import { KibanaContextProvider, useKibana, useUiSetting$ } from '../common/lib/kibana';
import { State } from '../common/store';
import { ManageGlobalTimeline } from '../timelines/components/manage_timeline';
import { StartServices } from '../types';
import { PageRouter } from './routes';
import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common';
@ -42,23 +41,21 @@ const StartAppComponent: FC<StartAppComponent> = ({ children, history, onAppLeav
<EuiErrorBoundary>
<i18n.Context>
<ManageGlobalToaster>
<ManageGlobalTimeline>
<ReduxStoreProvider store={store}>
<EuiThemeProvider darkMode={darkMode}>
<MlCapabilitiesProvider>
<UserPrivilegesProvider>
<ManageUserInfo>
<PageRouter history={history} onAppLeave={onAppLeave}>
{children}
</PageRouter>
</ManageUserInfo>
</UserPrivilegesProvider>
</MlCapabilitiesProvider>
</EuiThemeProvider>
<ErrorToastDispatcher />
<GlobalToaster />
</ReduxStoreProvider>
</ManageGlobalTimeline>
<ReduxStoreProvider store={store}>
<EuiThemeProvider darkMode={darkMode}>
<MlCapabilitiesProvider>
<UserPrivilegesProvider>
<ManageUserInfo>
<PageRouter history={history} onAppLeave={onAppLeave}>
{children}
</PageRouter>
</ManageUserInfo>
</UserPrivilegesProvider>
</MlCapabilitiesProvider>
</EuiThemeProvider>
<ErrorToastDispatcher />
<GlobalToaster />
</ReduxStoreProvider>
</ManageGlobalToaster>
</i18n.Context>
</EuiErrorBoundary>

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './tooltip_with_keyboard_shortcut';

View file

@ -10,7 +10,7 @@ import React from 'react';
import * as i18n from './translations';
interface Props {
export interface TooltipWithKeyboardShortcutProps {
additionalScreenReaderOnlyContext?: string;
content: React.ReactNode;
shortcut: string;
@ -22,7 +22,7 @@ const TooltipWithKeyboardShortcutComponent = ({
content,
shortcut,
showShortcut,
}: Props) => (
}: TooltipWithKeyboardShortcutProps) => (
<>
<div data-test-subj="content">{content}</div>
{additionalScreenReaderOnlyContext !== '' && (

View file

@ -6,12 +6,12 @@
*/
import React, { useEffect, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { timelineActions } from '../../../timelines/store/timeline';
import { Filter } from '../../../../../../../src/plugins/data/public';
import { TimelineIdLiteral } from '../../../../common/types/timeline';
import { StatefulEventsViewer } from '../events_viewer';
import { alertsDefaultModel } from './default_headers';
import { useManageTimeline } from '../../../timelines/components/manage_timeline';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
import * as i18n from './translations';
@ -70,22 +70,24 @@ const AlertsTableComponent: React.FC<Props> = ({
startDate,
pageFilters = [],
}) => {
const dispatch = useDispatch();
const alertsFilter = useMemo(() => [...defaultAlertsFilters, ...pageFilters], [pageFilters]);
const { filterManager } = useKibana().services.data.query;
const { initializeTimeline } = useManageTimeline();
useEffect(() => {
initializeTimeline({
id: timelineId,
documentType: i18n.ALERTS_DOCUMENT_TYPE,
filterManager,
defaultModel: alertsDefaultModel,
footerText: i18n.TOTAL_COUNT_OF_ALERTS,
title: i18n.ALERTS_TABLE_TITLE,
unit: i18n.UNIT,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
dispatch(
timelineActions.initializeTGridSettings({
id: timelineId,
documentType: i18n.ALERTS_DOCUMENT_TYPE,
filterManager,
defaultColumns: alertsDefaultModel.columns,
excludedRowRendererIds: alertsDefaultModel.excludedRowRendererIds,
footerText: i18n.TOTAL_COUNT_OF_ALERTS,
title: i18n.ALERTS_TABLE_TITLE,
// TODO: avoid passing this through the store
})
);
}, [dispatch, filterManager, timelineId]);
return (
<StatefulEventsViewer

View file

@ -5,13 +5,13 @@
* 2.0.
*/
import { RowRendererId } from '../../../../common/types/timeline';
import { ColumnHeaderOptions, RowRendererId } from '../../../../common/types/timeline';
import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers';
import {
DEFAULT_COLUMN_MIN_WIDTH,
DEFAULT_DATE_COLUMN_MIN_WIDTH,
} from '../../../timelines/components/timeline/body/constants';
import { ColumnHeaderOptions, SubsetTimelineModel } from '../../../timelines/store/timeline/model';
import { SubsetTimelineModel } from '../../../timelines/store/timeline/model';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
export const alertsHeaders: ColumnHeaderOptions[] = [

View file

@ -15,6 +15,8 @@ import { TestProviders } from '../../mock';
import { MIN_LEGEND_HEIGHT, DraggableLegend } from './draggable_legend';
import { LegendItem } from './draggable_legend_item';
jest.mock('../../lib/kibana');
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {

View file

@ -14,6 +14,8 @@ import { TestProviders } from '../../mock';
import { DraggableLegendItem, LegendItem } from './draggable_legend_item';
jest.mock('../../../common/lib/kibana');
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {

View file

@ -13,6 +13,8 @@ import { TestProviders } from '../../mock';
import { DragDropContextWrapper } from './drag_drop_context_wrapper';
jest.mock('../../lib/kibana');
describe('DragDropContextWrapper', () => {
describe('rendering', () => {
test('it renders against the snapshot', () => {

View file

@ -11,6 +11,7 @@ import { DropResult, DragDropContext } from 'react-beautiful-dnd';
import { useDispatch } from 'react-redux';
import { Dispatch } from 'redux';
import deepEqual from 'fast-deep-equal';
import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid';
import { BeforeCapture } from './drag_drop_context';
import { BrowserFields } from '../../containers/source';
@ -23,22 +24,24 @@ import {
ADDED_TO_TIMELINE_MESSAGE,
ADDED_TO_TIMELINE_TEMPLATE_MESSAGE,
} from '../../hooks/translations';
import { useAddToTimelineSensor } from '../../hooks/use_add_to_timeline';
import { displaySuccessToast, useStateToaster } from '../toasters';
import { TimelineId, TimelineType } from '../../../../common/types/timeline';
import {
addFieldToTimelineColumns,
addProviderToTimeline,
fieldWasDroppedOnTimelineColumns,
getTimelineIdFromColumnDroppableId,
IS_DRAGGING_CLASS_NAME,
IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME,
providerWasDroppedOnTimeline,
draggableIsField,
userIsReArrangingProviders,
} from './helpers';
import { useDeepEqualSelector } from '../../hooks/use_selector';
import { useKibana } from '../../lib/kibana';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
import {
addFieldToTimelineColumns,
getTimelineIdFromColumnDroppableId,
} from '../../../../../timelines/public';
import { alertsHeaders } from '../alerts_viewer/default_headers';
// @ts-expect-error
window['__react-beautiful-dnd-disable-dev-warnings'] = true;
@ -85,6 +88,7 @@ const onDragEndHandler = ({
} else if (fieldWasDroppedOnTimelineColumns(result)) {
addFieldToTimelineColumns({
browserFields,
defaultsHeader: alertsHeaders,
dispatch,
result,
timelineId: getTimelineIdFromColumnDroppableId(result.destination?.droppableId ?? ''),
@ -92,8 +96,6 @@ const onDragEndHandler = ({
}
};
const sensors = [useAddToTimelineSensor];
/**
* DragDropContextWrapperComponent handles all drag end events
*/
@ -101,7 +103,8 @@ export const DragDropContextWrapperComponent: React.FC<Props> = ({ browserFields
const dispatch = useDispatch();
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const getDataProviders = useMemo(() => dragAndDropSelectors.getDataProvidersSelector(), []);
const { timelines } = useKibana().services;
const sensors = [timelines.getUseAddToTimelineSensor()];
const {
dataProviders: activeTimelineDataProviders,
timelineType,

View file

@ -17,6 +17,8 @@ import { DragDropContextWrapper } from './drag_drop_context_wrapper';
import { ConditionalPortal, DraggableWrapper, getStyle } from './draggable_wrapper';
import { useMountAppended } from '../../utils/use_mount_appended';
jest.mock('../../lib/kibana');
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {

View file

@ -6,6 +6,7 @@
*/
import { EuiScreenReaderOnly } from '@elastic/eui';
import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid';
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import {
Draggable,
@ -24,12 +25,12 @@ import { ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID } from '../../../timelines/com
import { TruncatableText } from '../truncatable_text';
import { WithHoverActions } from '../with_hover_actions';
import { useDraggableKeyboardWrapper } from './draggable_keyboard_wrapper_hook';
import { DraggableWrapperHoverContent, useGetTimelineId } from './draggable_wrapper_hover_content';
import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME, getDraggableId, getDroppableId } from './helpers';
import { getDraggableId, getDroppableId } from './helpers';
import { ProviderContainer } from './provider_container';
import * as i18n from './translations';
import { useKibana } from '../../lib/kibana';
// As right now, we do not know what we want there, we will keep it as a placeholder
export const DragEffects = styled.div``;
@ -142,6 +143,7 @@ const DraggableWrapperComponent: React.FC<Props> = ({
const isDisabled = dataProvider.id.includes(`-${ROW_RENDERER_BROWSER_EXAMPLE_TIMELINE_ID}-`);
const [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState<boolean>(false);
const dispatch = useDispatch();
const { timelines } = useKibana().services;
const handleClosePopOverTrigger = useCallback(() => {
setClosePopOverTrigger((prevClosePopOverTrigger) => !prevClosePopOverTrigger);
@ -297,7 +299,7 @@ const DraggableWrapperComponent: React.FC<Props> = ({
setHoverActionsOwnFocus(true);
}, []);
const { onBlur, onKeyDown } = useDraggableKeyboardWrapper({
const { onBlur, onKeyDown } = timelines.getUseDraggableKeyboardWrapper()({
closePopover: handleClosePopOverTrigger,
draggableId: getDraggableId(dataProvider.id),
fieldName: dataProvider.queryMatch.field,

View file

@ -17,14 +17,10 @@ import { TestProviders } from '../../mock';
import { FilterManager } from '../../../../../../../src/plugins/data/public';
import { useSourcererScope } from '../../containers/sourcerer';
import { DraggableWrapperHoverContent } from './draggable_wrapper_hover_content';
import {
ManageGlobalTimeline,
getTimelineDefaults,
} from '../../../timelines/components/manage_timeline';
import { TimelineId } from '../../../../common/types/timeline';
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
jest.mock('../link_to');
jest.mock('../../lib/kibana');
jest.mock('../../containers/sourcerer', () => {
const original = jest.requireActual('../../containers/sourcerer');
@ -42,29 +38,18 @@ jest.mock('uuid', () => {
};
});
const mockStartDragToTimeline = jest.fn();
jest.mock('../../hooks/use_add_to_timeline', () => {
const original = jest.requireActual('../../hooks/use_add_to_timeline');
jest.mock('../../../../../timelines/public/hooks/use_add_to_timeline', () => {
const original = jest.requireActual('../../../../../timelines/public/hooks/use_add_to_timeline');
return {
...original,
useAddToTimeline: () => ({ startDragToTimeline: mockStartDragToTimeline }),
};
});
const mockAddFilters = jest.fn();
const mockGetTimelineFilterManager = jest.fn().mockReturnValue({
addFilters: mockAddFilters,
});
jest.mock('../../../timelines/components/manage_timeline', () => {
const original = jest.requireActual('../../../timelines/components/manage_timeline');
return {
...original,
useManageTimeline: () => ({
getManageTimelineById: jest.fn().mockReturnValue({ indexToAdd: [] }),
getTimelineFilterManager: mockGetTimelineFilterManager,
isManagedTimeline: jest.fn().mockReturnValue(false),
}),
};
});
jest.mock('../../../common/hooks/use_selector', () => ({
useShallowEqualSelector: jest.fn(),
useDeepEqualSelector: jest.fn(),
}));
const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings;
const timelineId = TimelineId.active;
@ -85,6 +70,9 @@ const defaultProps = {
describe('DraggableWrapperHoverContent', () => {
beforeAll(() => {
mockStartDragToTimeline.mockReset();
(useDeepEqualSelector as jest.Mock).mockReturnValue({
filterManager: { addFilters: mockAddFilters },
});
(useSourcererScope as jest.Mock).mockReturnValue({
browserFields: mockBrowserFields,
selectedPatterns: [],
@ -144,15 +132,10 @@ describe('DraggableWrapperHoverContent', () => {
beforeEach(() => {
onFilterAdded = jest.fn();
const manageTimelineForTesting = {
[timelineId]: getTimelineDefaults(timelineId),
};
wrapper = mount(
<TestProviders>
<ManageGlobalTimeline manageTimelineForTesting={manageTimelineForTesting}>
<DraggableWrapperHoverContent {...{ ...defaultProps, onFilterAdded }} />
</ManageGlobalTimeline>
<DraggableWrapperHoverContent {...{ ...defaultProps, onFilterAdded }} />
</TestProviders>
);
});
@ -237,18 +220,9 @@ describe('DraggableWrapperHoverContent', () => {
filterManager.addFilters = jest.fn();
onFilterAdded = jest.fn();
const manageTimelineForTesting = {
[timelineId]: {
...getTimelineDefaults(timelineId),
filterManager,
},
};
wrapper = mount(
<TestProviders>
<ManageGlobalTimeline manageTimelineForTesting={manageTimelineForTesting}>
<DraggableWrapperHoverContent {...{ ...defaultProps, onFilterAdded, value: '' }} />
</ManageGlobalTimeline>
<DraggableWrapperHoverContent {...{ ...defaultProps, onFilterAdded, value: '' }} />
</TestProviders>
);
});
@ -586,39 +560,4 @@ describe('DraggableWrapperHoverContent', () => {
expect(wrapper.find(`[data-test-subj="copy-to-clipboard"]`).first().exists()).toBe(false);
});
});
describe('Filter Manager', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('filter manager, not active timeline', () => {
mount(
<TestProviders>
<DraggableWrapperHoverContent {...{ ...defaultProps, timelineId: TimelineId.test }} />
</TestProviders>
);
expect(mockGetTimelineFilterManager).not.toBeCalled();
});
test('filter manager, active timeline', () => {
mount(
<TestProviders>
<DraggableWrapperHoverContent {...defaultProps} />
</TestProviders>
);
expect(mockGetTimelineFilterManager).toBeCalled();
});
test('filter manager, active timeline in draggableId', () => {
mount(
<TestProviders>
<DraggableWrapperHoverContent
{...{ ...defaultProps, draggableId: `blahblah-${TimelineId.active}-lala` }}
/>
</TestProviders>
);
expect(mockGetTimelineFilterManager).toBeCalled();
});
});
});

View file

@ -12,14 +12,12 @@ import {
EuiScreenReaderOnly,
EuiToolTip,
} from '@elastic/eui';
import React, { useCallback, useEffect, useRef, useMemo, useState } from 'react';
import { DraggableId } from 'react-beautiful-dnd';
import styled from 'styled-components';
import { stopPropagationAndPreventDefault } from '../accessibility/helpers';
import { TooltipWithKeyboardShortcut } from '../accessibility/tooltip_with_keyboard_shortcut';
import { getAllFieldsByName } from '../../containers/source';
import { useAddToTimeline } from '../../hooks/use_add_to_timeline';
import { COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME } from '../../lib/clipboard/clipboard';
import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard';
import { useKibana } from '../../lib/kibana';
@ -28,11 +26,14 @@ import { StatefulTopN } from '../top_n';
import { allowTopN } from './helpers';
import * as i18n from './translations';
import { useManageTimeline } from '../../../timelines/components/manage_timeline';
import { useDeepEqualSelector } from '../../hooks/use_selector';
import { TimelineId } from '../../../../common/types/timeline';
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { useSourcererScope } from '../../containers/sourcerer';
import { timelineSelectors } from '../../../timelines/store/timeline';
import { stopPropagationAndPreventDefault } from '../../../../../timelines/public';
import { TooltipWithKeyboardShortcut } from '../accessibility';
export const AdditionalContent = styled.div`
padding: 2px;
@ -102,21 +103,25 @@ const DraggableWrapperHoverContentComponent: React.FC<Props> = ({
toggleTopN,
value,
}) => {
const { startDragToTimeline } = useAddToTimeline({ draggableId, fieldName: field });
const kibana = useKibana();
const { timelines } = kibana.services;
const { startDragToTimeline } = timelines.getUseAddToTimeline()({
draggableId,
fieldName: field,
});
const filterManagerBackup = useMemo(() => kibana.services.data.query.filterManager, [
kibana.services.data.query.filterManager,
]);
const { getTimelineFilterManager } = useManageTimeline();
const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
const { filterManager: activeFilterMananager } = useDeepEqualSelector((state) =>
getManageTimeline(state, timelineId ?? '')
);
const defaultFocusedButtonRef = useRef<HTMLButtonElement | null>(null);
const panelRef = useRef<HTMLDivElement | null>(null);
const filterManager = useMemo(
() =>
timelineId === TimelineId.active
? getTimelineFilterManager(TimelineId.active)
: filterManagerBackup,
[timelineId, getTimelineFilterManager, filterManagerBackup]
() => (timelineId === TimelineId.active ? activeFilterMananager : filterManagerBackup),
[timelineId, activeFilterMananager, filterManagerBackup]
);
// Regarding data from useManageTimeline:

View file

@ -15,6 +15,8 @@ import { DragDropContextWrapper } from './drag_drop_context_wrapper';
import { DroppableWrapper } from './droppable_wrapper';
import { useMountAppended } from '../../utils/use_mount_appended';
jest.mock('../../lib/kibana');
describe('DroppableWrapper', () => {
const mount = useMountAppended();

View file

@ -7,6 +7,7 @@
import { omit } from 'lodash/fp';
import { DropResult } from 'react-beautiful-dnd';
import { getTimelineIdFromColumnDroppableId } from '../../../../../timelines/public';
import { IdToDataProvider } from '../../store/drag_and_drop/model';
@ -33,7 +34,6 @@ import {
getDroppableId,
getFieldIdFromDraggable,
getProviderIdFromDraggable,
getTimelineIdFromColumnDroppableId,
getTimelineProviderDraggableId,
getTimelineProviderDroppableId,
providerWasDroppedOnTimeline,

View file

@ -4,138 +4,53 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { isString } from 'lodash/fp';
import { DropResult, FluidDragActions, Position } from 'react-beautiful-dnd';
import { DropResult } from 'react-beautiful-dnd';
import { Dispatch } from 'redux';
import { ActionCreator } from 'typescript-fsa';
import { getProviderIdFromDraggable } from '@kbn/securitysolution-t-grid';
import { stopPropagationAndPreventDefault } from '../accessibility/helpers';
import { alertsHeaders } from '../alerts_viewer/default_headers';
import { BrowserField, BrowserFields, getAllFieldsByName } from '../../containers/source';
import { BrowserField } from '../../containers/source';
import { dragAndDropActions } from '../../store/actions';
import { IdToDataProvider } from '../../store/drag_and_drop/model';
import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import { timelineActions } from '../../../timelines/store/timeline';
import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants';
import { addContentToTimeline } from '../../../timelines/components/timeline/data_providers/helpers';
import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider';
import { TimelineId } from '../../../../common/types/timeline';
export const draggableIdPrefix = 'draggableId';
export const droppableIdPrefix = 'droppableId';
export const draggableContentPrefix = `${draggableIdPrefix}.content.`;
export const draggableTimelineProvidersPrefix = `${draggableIdPrefix}.timelineProviders.`;
export const draggableFieldPrefix = `${draggableIdPrefix}.field.`;
export const droppableContentPrefix = `${droppableIdPrefix}.content.`;
export const droppableFieldPrefix = `${droppableIdPrefix}.field.`;
export const droppableTimelineProvidersPrefix = `${droppableIdPrefix}.timelineProviders.`;
export const droppableTimelineColumnsPrefix = `${droppableIdPrefix}.timelineColumns.`;
export const droppableTimelineFlyoutBottomBarPrefix = `${droppableIdPrefix}.flyoutButton.`;
export const getDraggableId = (dataProviderId: string): string =>
`${draggableContentPrefix}${dataProviderId}`;
export const getDraggableFieldId = ({
contextId,
fieldId,
}: {
contextId: string;
fieldId: string;
}): string => `${draggableFieldPrefix}${escapeContextId(contextId)}.${escapeFieldId(fieldId)}`;
export const getTimelineProviderDroppableId = ({
groupIndex,
timelineId,
}: {
groupIndex: number;
timelineId: string;
}): string => `${droppableTimelineProvidersPrefix}${timelineId}.group.${groupIndex}`;
export const getTimelineProviderDraggableId = ({
dataProviderId,
groupIndex,
timelineId,
}: {
dataProviderId: string;
groupIndex: number;
timelineId: string;
}): string =>
`${draggableTimelineProvidersPrefix}${timelineId}.group.${groupIndex}.${dataProviderId}`;
export const getDroppableId = (visualizationPlaceholderId: string): string =>
`${droppableContentPrefix}${visualizationPlaceholderId}`;
export const sourceIsContent = (result: DropResult): boolean =>
result.source.droppableId.startsWith(droppableContentPrefix);
export const sourceAndDestinationAreSameTimelineProviders = (result: DropResult): boolean => {
const regex = /^droppableId\.timelineProviders\.(\S+)\./;
const sourceMatches = result.source.droppableId.match(regex) ?? [];
const destinationMatches = result.destination?.droppableId.match(regex) ?? [];
return (
sourceMatches.length >= 2 &&
destinationMatches.length >= 2 &&
sourceMatches[1] === destinationMatches[1]
);
};
export const draggableIsContent = (result: DropResult | { draggableId: string }): boolean =>
result.draggableId.startsWith(draggableContentPrefix);
export const draggableIsField = (result: DropResult | { draggableId: string }): boolean =>
result.draggableId.startsWith(draggableFieldPrefix);
export const reasonIsDrop = (result: DropResult): boolean => result.reason === 'DROP';
export const destinationIsTimelineProviders = (result: DropResult): boolean =>
result.destination != null &&
result.destination.droppableId.startsWith(droppableTimelineProvidersPrefix);
export const destinationIsTimelineColumns = (result: DropResult): boolean =>
result.destination != null &&
result.destination.droppableId.startsWith(droppableTimelineColumnsPrefix);
export const destinationIsTimelineButton = (result: DropResult): boolean =>
result.destination != null &&
result.destination.droppableId.startsWith(droppableTimelineFlyoutBottomBarPrefix);
export const getProviderIdFromDraggable = (result: DropResult): string =>
result.draggableId.substring(result.draggableId.lastIndexOf('.') + 1);
export const getFieldIdFromDraggable = (result: DropResult): string =>
unEscapeFieldId(result.draggableId.substring(result.draggableId.lastIndexOf('.') + 1));
export const escapeDataProviderId = (path: string) => path.replace(/\./g, '_');
export const escapeContextId = (path: string) => path.replace(/\./g, '_');
export const escapeFieldId = (path: string) => path.replace(/\./g, '!!!DOT!!!');
export const unEscapeFieldId = (path: string) => path.replace(/!!!DOT!!!/g, '.');
export const providerWasDroppedOnTimeline = (result: DropResult): boolean =>
reasonIsDrop(result) &&
draggableIsContent(result) &&
sourceIsContent(result) &&
destinationIsTimelineProviders(result);
export const userIsReArrangingProviders = (result: DropResult): boolean =>
reasonIsDrop(result) && sourceAndDestinationAreSameTimelineProviders(result);
export const fieldWasDroppedOnTimelineColumns = (result: DropResult): boolean =>
reasonIsDrop(result) && draggableIsField(result) && destinationIsTimelineColumns(result);
export {
draggableIdPrefix,
droppableIdPrefix,
draggableContentPrefix,
draggableTimelineProvidersPrefix,
draggableFieldPrefix,
draggableIsField,
droppableContentPrefix,
droppableFieldPrefix,
droppableTimelineProvidersPrefix,
droppableTimelineColumnsPrefix,
droppableTimelineFlyoutBottomBarPrefix,
getDraggableId,
getDraggableFieldId,
getTimelineProviderDroppableId,
getTimelineProviderDraggableId,
getDroppableId,
sourceIsContent,
sourceAndDestinationAreSameTimelineProviders,
draggableIsContent,
reasonIsDrop,
destinationIsTimelineProviders,
destinationIsTimelineColumns,
destinationIsTimelineButton,
getProviderIdFromDraggable,
getFieldIdFromDraggable,
escapeDataProviderId,
escapeContextId,
escapeFieldId,
unEscapeFieldId,
providerWasDroppedOnTimeline,
userIsReArrangingProviders,
fieldWasDroppedOnTimelineColumns,
DRAG_TYPE_FIELD,
IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME,
} from '@kbn/securitysolution-t-grid';
interface AddProviderToTimelineParams {
activeTimelineDataProviders: DataProvider[];
dataProviders: IdToDataProvider;
@ -148,18 +63,6 @@ interface AddProviderToTimelineParams {
timelineId: string;
}
interface AddFieldToTimelineColumnsParams {
upsertColumn?: ActionCreator<{
column: ColumnHeaderOptions;
id: string;
index: number;
}>;
browserFields: BrowserFields;
dispatch: Dispatch;
result: DropResult;
timelineId: string;
}
export const addProviderToTimeline = ({
activeTimelineDataProviders,
dataProviders,
@ -186,73 +89,6 @@ export const addProviderToTimeline = ({
}
};
const linkFields: Record<string, string> = {
'signal.rule.name': 'signal.rule.id',
'event.module': 'rule.reference',
};
export const addFieldToTimelineColumns = ({
upsertColumn = timelineActions.upsertColumn,
browserFields,
dispatch,
result,
timelineId,
}: AddFieldToTimelineColumnsParams): void => {
const fieldId = getFieldIdFromDraggable(result);
const allColumns = getAllFieldsByName(browserFields);
const column = allColumns[fieldId];
const initColumnHeader =
timelineId === TimelineId.detectionsPage || timelineId === TimelineId.detectionsRulesDetailsPage
? alertsHeaders.find((c) => c.id === fieldId) ?? {}
: {};
if (column != null) {
dispatch(
upsertColumn({
column: {
category: column.category,
columnHeaderType: 'not-filtered',
description: isString(column.description) ? column.description : undefined,
example: isString(column.example) ? column.example : undefined,
id: fieldId,
linkField: linkFields[fieldId] ?? undefined,
type: column.type,
aggregatable: column.aggregatable,
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
...initColumnHeader,
},
id: timelineId,
index: result.destination != null ? result.destination.index : 0,
})
);
} else {
// create a column definition, because it doesn't exist in the browserFields:
dispatch(
upsertColumn({
column: {
columnHeaderType: 'not-filtered',
id: fieldId,
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
},
id: timelineId,
index: result.destination != null ? result.destination.index : 0,
})
);
}
};
/**
* Prevents fields from being dragged or dropped to any area other than column
* header drop zone in the timeline
*/
export const DRAG_TYPE_FIELD = 'drag-type-field';
/** This class is added to the document body while dragging */
export const IS_DRAGGING_CLASS_NAME = 'is-dragging';
/** This class is added to the document body while timeline field dragging */
export const IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME = 'is-timeline-field-dragging';
export const allowTopN = ({
browserField,
fieldName,
@ -347,125 +183,3 @@ export const allowTopN = ({
return isAllowlistedNonBrowserField || (isAggregatable && isAllowedType);
};
export const getTimelineIdFromColumnDroppableId = (droppableId: string) =>
droppableId.slice(droppableId.lastIndexOf('.') + 1);
/** The draggable will move this many pixes via the keyboard when the arrow key is pressed */
export const KEYBOARD_DRAG_OFFSET = 20;
export const DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME = 'draggable-keyboard-wrapper';
/**
* Temporarily disables tab focus on child links of the draggable to work
* around an issue where tab focus becomes stuck on the interactive children
*
* NOTE: This function is (intentionally) only effective when used in a key
* event handler, because it automatically restores focus capabilities on
* the next tick.
*/
export const temporarilyDisableInteractiveChildTabIndexes = (draggableElement: HTMLDivElement) => {
const interactiveChildren = draggableElement.querySelectorAll('a, button');
interactiveChildren.forEach((interactiveChild) => {
interactiveChild.setAttribute('tabindex', '-1'); // DOM mutation
});
// restore the default tabindexs on the next tick:
setTimeout(() => {
interactiveChildren.forEach((interactiveChild) => {
interactiveChild.setAttribute('tabindex', '0'); // DOM mutation
});
}, 0);
};
export const draggableKeyDownHandler = ({
beginDrag,
cancelDragActions,
closePopover,
draggableElement,
dragActions,
dragToLocation,
endDrag,
keyboardEvent,
openPopover,
setDragActions,
}: {
beginDrag: () => FluidDragActions | null;
cancelDragActions: () => void;
closePopover?: () => void;
draggableElement: HTMLDivElement;
dragActions: FluidDragActions | null;
dragToLocation: ({
// eslint-disable-next-line @typescript-eslint/no-shadow
dragActions,
position,
}: {
dragActions: FluidDragActions | null;
position: Position;
}) => void;
keyboardEvent: React.KeyboardEvent;
endDrag: (dragActions: FluidDragActions | null) => void;
openPopover?: () => void;
setDragActions: (value: React.SetStateAction<FluidDragActions | null>) => void;
}) => {
let currentPosition: DOMRect | null = null;
switch (keyboardEvent.key) {
case ' ':
if (!dragActions) {
// start dragging, because space was pressed
if (closePopover != null) {
closePopover();
}
setDragActions(beginDrag());
} else {
// end dragging, because space was pressed
endDrag(dragActions);
setDragActions(null);
}
break;
case 'Escape':
cancelDragActions();
break;
case 'Tab':
// IMPORTANT: we do NOT want to stop propagation and prevent default when Tab is pressed
temporarilyDisableInteractiveChildTabIndexes(draggableElement);
break;
case 'ArrowUp':
currentPosition = draggableElement.getBoundingClientRect();
dragToLocation({
dragActions,
position: { x: currentPosition.x, y: currentPosition.y - KEYBOARD_DRAG_OFFSET },
});
break;
case 'ArrowDown':
currentPosition = draggableElement.getBoundingClientRect();
dragToLocation({
dragActions,
position: { x: currentPosition.x, y: currentPosition.y + KEYBOARD_DRAG_OFFSET },
});
break;
case 'ArrowLeft':
currentPosition = draggableElement.getBoundingClientRect();
dragToLocation({
dragActions,
position: { x: currentPosition.x - KEYBOARD_DRAG_OFFSET, y: currentPosition.y },
});
break;
case 'ArrowRight':
currentPosition = draggableElement.getBoundingClientRect();
dragToLocation({
dragActions,
position: { x: currentPosition.x + KEYBOARD_DRAG_OFFSET, y: currentPosition.y },
});
break;
case 'Enter':
stopPropagationAndPreventDefault(keyboardEvent); // prevents the first item in the popover from getting an errant ENTER
if (!dragActions && openPopover != null) {
openPopover();
}
break;
default:
break;
}
};

View file

@ -21,6 +21,8 @@ import {
tooltipContentIsExplicitlyNull,
} from '.';
jest.mock('../../lib/kibana');
describe('draggables', () => {
const mount = useMountAppended();

View file

@ -17,6 +17,8 @@ import { TestProviders } from '../../mock';
import { mockBrowserFields } from '../../containers/source/mock';
import { useMountAppended } from '../../utils/use_mount_appended';
jest.mock('../../lib/kibana');
jest.mock('../../../detections/containers/detection_engine/rules/use_rule_async', () => {
return {
useRuleAsync: jest.fn(),

View file

@ -21,9 +21,8 @@ import { get, isEmpty } from 'lodash';
import memoizeOne from 'memoize-one';
import React from 'react';
import styled from 'styled-components';
import { onFocusReFocusDraggable } from '../accessibility/helpers';
import { onFocusReFocusDraggable } from '../../../../../timelines/public';
import { BrowserFields } from '../../containers/source';
import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import { DragEffects } from '../drag_and_drop/draggable_wrapper';
import { DroppableWrapper } from '../drag_and_drop/droppable_wrapper';
import { DRAG_TYPE_FIELD, getDroppableId } from '../drag_and_drop/helpers';
@ -38,6 +37,7 @@ import { OnUpdateColumns } from '../../../timelines/components/timeline/events';
import { getIconFromType, getExampleText } from './helpers';
import * as i18n from './translations';
import { EventFieldsData } from './types';
import { ColumnHeaderOptions } from '../../../../common';
const HoverActionsContainer = styled(EuiPanel)`
align-items: center;

View file

@ -20,6 +20,8 @@ import { mockAlertDetailsData } from './__mocks__';
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy';
import { TimelineTabs } from '../../../../common/types/timeline';
jest.mock('../../../common/lib/kibana');
jest.mock('../link_to');
describe('EventDetails', () => {
const mount = useMountAppended();

View file

@ -16,6 +16,8 @@ import { mockBrowserFields } from '../../containers/source/mock';
import { useMountAppended } from '../../utils/use_mount_appended';
import { TimelineTabs } from '../../../../common/types/timeline';
jest.mock('../../lib/kibana');
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {

View file

@ -11,26 +11,24 @@ import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { useDispatch } from 'react-redux';
import { rgba } from 'polished';
import styled from 'styled-components';
import {
arrayIndexToAriaIndex,
DATA_COLINDEX_ATTRIBUTE,
DATA_ROWINDEX_ATTRIBUTE,
isTab,
onKeyDownFocusHandler,
} from '../accessibility/helpers';
} from '../../../../../timelines/public';
import { ADD_TIMELINE_BUTTON_CLASS_NAME } from '../../../timelines/components/flyout/add_timeline_button';
import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline';
import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import { BrowserFields, getAllFieldsByName } from '../../containers/source';
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline';
import { getColumnHeaders } from '../../../timelines/components/timeline/body/column_headers/helpers';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
import { getColumns } from './columns';
import { EVENT_FIELDS_TABLE_CLASS_NAME, onEventDetailsTabKeyPressed, search } from './helpers';
import { useDeepEqualSelector } from '../../hooks/use_selector';
import { TimelineTabs } from '../../../../common/types/timeline';
import { ColumnHeaderOptions, TimelineTabs } from '../../../../common/types/timeline';
interface Props {
browserFields: BrowserFields;

View file

@ -15,15 +15,15 @@ import {
getTableSkipFocus,
handleSkipFocus,
stopPropagationAndPreventDefault,
} from '../accessibility/helpers';
} from '../../../../../timelines/public';
import { BrowserField, BrowserFields } from '../../containers/source';
import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import {
DEFAULT_DATE_COLUMN_MIN_WIDTH,
DEFAULT_COLUMN_MIN_WIDTH,
} from '../../../timelines/components/timeline/body/constants';
import * as i18n from './translations';
import { ColumnHeaderOptions } from '../../../../common';
/**
* Defines the behavior of the search input that appears above the table of data

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model';
import { ColumnHeaderOptions } from '../../../../common';
import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers';
import {
DEFAULT_COLUMN_MIN_WIDTH,

View file

@ -21,9 +21,8 @@ import { mockBrowserFields, mockDocValueFields } from '../../containers/source/m
import { eventsDefaultModel } from './default_model';
import { useMountAppended } from '../../utils/use_mount_appended';
import { inputsModel } from '../../store/inputs';
import { TimelineId } from '../../../../common/types/timeline';
import { TimelineId, SortDirection } from '../../../../common/types/timeline';
import { KqlMode } from '../../../timelines/store/timeline/model';
import { SortDirection } from '../../../timelines/components/timeline/body/sort';
import { AlertsTableFilterGroup } from '../../../detections/components/alerts_table/alerts_filter_group';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
@ -31,6 +30,8 @@ import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell
import { useTimelineEvents } from '../../../timelines/containers';
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
jest.mock('../../lib/kibana');
jest.mock('../../hooks/use_experimental_features');
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
@ -144,18 +145,18 @@ describe('EventsViewer', () => {
mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponseWithEvents]);
});
test('call the right reduce action to show event details', async () => {
test('call the right reduce action to show event details', () => {
const wrapper = mount(
<TestProviders>
<StatefulEventsViewer {...testProps} />
</TestProviders>
);
await act(async () => {
act(() => {
wrapper.find(`[data-test-subj="expand-event"]`).first().simulate('click');
});
await waitFor(() => {
waitFor(() => {
expect(mockDispatch).toBeCalledTimes(2);
expect(mockDispatch.mock.calls[1][0]).toEqual({
payload: {
@ -197,7 +198,7 @@ describe('EventsViewer', () => {
);
expect(wrapper.find(`[data-test-subj="show-field-browser"]`).first().exists()).toBe(true);
});
// TO DO sourcerer @X
test('it renders the footer containing the pagination', () => {
const wrapper = mount(
<TestProviders>

View file

@ -10,11 +10,12 @@ import React, { useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import deepEqual from 'fast-deep-equal';
import { useDispatch } from 'react-redux';
import { Direction } from '../../../../common/search_strategy';
import { BrowserFields, DocValueFields } from '../../containers/source';
import { useTimelineEvents } from '../../../timelines/containers';
import { useKibana } from '../../lib/kibana';
import { ColumnHeaderOptions, KqlMode } from '../../../timelines/store/timeline/model';
import { KqlMode } from '../../../timelines/store/timeline/model';
import { HeaderSection } from '../header_section';
import { defaultHeaders } from '../../../timelines/components/timeline/body/column_headers/default_headers';
import { Sort } from '../../../timelines/components/timeline/body/sort';
@ -36,18 +37,21 @@ import {
Query,
} from '../../../../../../../src/plugins/data/public';
import { inputsModel } from '../../store';
import { useManageTimeline } from '../../../timelines/components/manage_timeline';
import { ExitFullScreen } from '../exit_full_screen';
import { useGlobalFullScreen } from '../../containers/use_full_screen';
import { TimelineId, TimelineTabs } from '../../../../common/types/timeline';
import { RowRenderer } from '../../../timelines/components/timeline/body/renderers/row_renderer';
import {
ColumnHeaderOptions,
ControlColumnProps,
RowRenderer,
TimelineId,
TimelineTabs,
} from '../../../../common/types/timeline';
import { GraphOverlay } from '../../../timelines/components/graph_overlay';
import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering';
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles';
import {
defaultControlColumn,
ControlColumnProps,
} from '../../../timelines/components/timeline/body/control_columns';
import { defaultControlColumn } from '../../../timelines/components/timeline/body/control_columns';
import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline';
import { useDeepEqualSelector } from '../../hooks/use_selector';
export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px
const UTILITY_BAR_HEIGHT = 19; // px
@ -162,21 +166,19 @@ const EventsViewerComponent: React.FC<Props> = ({
utilityBar,
graphEventId,
}) => {
const dispatch = useDispatch();
const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen();
const columnsHeader = isEmpty(columns) ? defaultHeaders : columns;
const kibana = useKibana();
const [isQueryLoading, setIsQueryLoading] = useState(false);
const { getManageTimelineById, setIsTimelineLoading } = useManageTimeline();
useEffect(() => {
setIsTimelineLoading({ id, isLoading: isQueryLoading });
}, [id, isQueryLoading, setIsTimelineLoading]);
dispatch(timelineActions.updateIsLoading({ id, isLoading: isQueryLoading }));
}, [dispatch, id, isQueryLoading]);
const { queryFields, title, unit } = useMemo(() => getManageTimelineById(id), [
getManageTimelineById,
id,
]);
const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
const unit = useMemo(() => (n: number) => i18n.UNIT(n), []);
const { queryFields, title } = useDeepEqualSelector((state) => getManageTimeline(state, id));
const justTitle = useMemo(() => <TitleText data-test-subj="title">{title}</TitleText>, [title]);

View file

@ -22,6 +22,8 @@ import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell
import { useTimelineEvents } from '../../../timelines/containers';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
jest.mock('../../../common/lib/kibana');
jest.mock('../../../timelines/containers', () => ({
useTimelineEvents: jest.fn(),
}));
@ -60,7 +62,9 @@ describe('StatefulEventsViewer', () => {
await waitFor(() => {
wrapper.update();
expect(wrapper.find('[data-test-subj="events-viewer-panel"]').first().exists()).toBe(true);
expect(wrapper.text()).toMatchInlineSnapshot(
`"Showing: 12 events1 fields sorted@timestamp1event.severityevent.categoryevent.actionhost.namesource.ipdestination.ipdestination.bytesuser.name_idmessage0 of 12 events123"`
);
});
});

View file

@ -12,18 +12,20 @@ import styled from 'styled-components';
import { inputsModel, inputsSelectors, State } from '../../store';
import { inputsActions } from '../../store/actions';
import { TimelineId } from '../../../../common/types/timeline';
import { ControlColumnProps, RowRenderer, TimelineId } from '../../../../common/types/timeline';
import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline';
import { SubsetTimelineModel, TimelineModel } from '../../../timelines/store/timeline/model';
import { Filter } from '../../../../../../../src/plugins/data/public';
import { EventsViewer } from './events_viewer';
import { InspectButtonContainer } from '../inspect';
import { useGlobalFullScreen } from '../../containers/use_full_screen';
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { useSourcererScope } from '../../containers/sourcerer';
import { DetailsPanel } from '../../../timelines/components/side_panel';
import { RowRenderer } from '../../../timelines/components/timeline/body/renderers/row_renderer';
import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering';
import { useKibana } from '../../lib/kibana';
import { defaultControlColumn } from '../../../timelines/components/timeline/body/control_columns';
import { EventsViewer } from './events_viewer';
const DEFAULT_EVENTS_VIEWER_HEIGHT = 652;
@ -83,6 +85,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
// If truthy, the graph viewer (Resolver) is showing
graphEventId,
}) => {
const { timelines: timelinesUi } = useKibana().services;
const {
browserFields,
docValueFields,
@ -90,8 +93,9 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
selectedPatterns,
loading: isLoadingIndexPattern,
} = useSourcererScope(scopeId);
const { globalFullScreen } = useGlobalFullScreen();
const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen();
// TODO: Once we are past experimental phase this code should be removed
const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled');
useEffect(() => {
if (createTimeline != null) {
createTimeline({
@ -111,37 +115,73 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
}, []);
const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]);
const leadingControlColumns: ControlColumnProps[] = [defaultControlColumn];
const trailingControlColumns: ControlColumnProps[] = [];
return (
<>
<FullScreenContainer $isFullScreen={globalFullScreen}>
<InspectButtonContainer>
<EventsViewer
browserFields={browserFields}
columns={columns}
docValueFields={docValueFields}
id={id}
dataProviders={dataProviders!}
deletedEventIds={deletedEventIds}
end={end}
isLoadingIndexPattern={isLoadingIndexPattern}
filters={globalFilters}
headerFilterGroup={headerFilterGroup}
indexNames={selectedPatterns}
indexPattern={indexPattern}
isLive={isLive}
itemsPerPage={itemsPerPage!}
itemsPerPageOptions={itemsPerPageOptions!}
kqlMode={kqlMode}
query={query}
onRuleChange={onRuleChange}
renderCellValue={renderCellValue}
rowRenderers={rowRenderers}
start={start}
sort={sort}
utilityBar={utilityBar}
graphEventId={graphEventId}
/>
{tGridEnabled ? (
timelinesUi.getTGrid<'embedded'>({
type: 'embedded',
browserFields,
columns,
dataProviders: dataProviders!,
deletedEventIds,
docValueFields,
end,
filters: globalFilters,
globalFullScreen,
headerFilterGroup,
id,
indexNames: selectedPatterns,
indexPattern,
isLive,
isLoadingIndexPattern,
itemsPerPage,
itemsPerPageOptions: itemsPerPageOptions!,
kqlMode,
query,
onRuleChange,
renderCellValue,
rowRenderers,
setGlobalFullScreen,
start,
sort,
utilityBar,
graphEventId,
leadingControlColumns,
trailingControlColumns,
})
) : (
<EventsViewer
browserFields={browserFields}
columns={columns}
docValueFields={docValueFields}
id={id}
dataProviders={dataProviders!}
deletedEventIds={deletedEventIds}
end={end}
isLoadingIndexPattern={isLoadingIndexPattern}
filters={globalFilters}
headerFilterGroup={headerFilterGroup}
indexNames={selectedPatterns}
indexPattern={indexPattern}
isLive={isLive}
itemsPerPage={itemsPerPage!}
itemsPerPageOptions={itemsPerPageOptions!}
kqlMode={kqlMode}
query={query}
onRuleChange={onRuleChange}
renderCellValue={renderCellValue}
rowRenderers={rowRenderers}
start={start}
sort={sort}
utilityBar={utilityBar}
graphEventId={graphEventId}
/>
)}
</InspectButtonContainer>
</FullScreenContainer>
<DetailsPanel

View file

@ -22,13 +22,6 @@ export const EVENTS = i18n.translate('xpack.securitySolution.eventsViewer.events
defaultMessage: 'Events',
});
export const LOADING_EVENTS = i18n.translate(
'xpack.securitySolution.eventsViewer.footer.loadingEventsDataLabel',
{
defaultMessage: 'Loading Events',
}
);
export const UNIT = (totalCount: number) =>
i18n.translate('xpack.securitySolution.eventsViewer.unit', {
values: { totalCount },

View file

@ -13,6 +13,8 @@ import { TestProviders } from '../../mock';
import { Title } from './title';
import { useMountAppended } from '../../utils/use_mount_appended';
jest.mock('../../lib/kibana');
describe('Title', () => {
const mount = useMountAppended();

View file

@ -131,7 +131,7 @@ const InspectButtonComponent: React.FC<InspectButtonProps> = ({
color="text"
iconSide="left"
iconType="inspect"
isDisabled={loading || isDisabled}
isDisabled={loading || isDisabled || false}
isLoading={loading}
onClick={handleClick}
>
@ -145,7 +145,7 @@ const InspectButtonComponent: React.FC<InspectButtonProps> = ({
data-test-subj="inspect-icon-button"
iconSize="m"
iconType="inspect"
isDisabled={loading || isDisabled}
isDisabled={loading || isDisabled || false}
title={i18n.INSPECT}
onClick={handleClick}
/>

View file

@ -13,6 +13,8 @@ import { EntityDraggableComponent } from './entity_draggable';
import { TestProviders } from '../../mock/test_providers';
import { useMountAppended } from '../../utils/use_mount_appended';
jest.mock('../../../common/lib/kibana');
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {

View file

@ -17,6 +17,8 @@ import { useMountAppended } from '../../../utils/use_mount_appended';
import { Anomalies } from '../types';
import { waitFor } from '@testing-library/dom';
jest.mock('../../../lib/kibana');
const startDate: string = '2020-07-07T08:20:18.966Z';
const endDate: string = '3000-01-01T00:00:00.000Z';

View file

@ -18,6 +18,8 @@ import { Anomalies } from '../types';
import { useMountAppended } from '../../../utils/use_mount_appended';
import { waitFor } from '@testing-library/dom';
jest.mock('../../../lib/kibana');
const startDate: string = '2020-07-07T08:20:18.966Z';
const endDate: string = '3000-01-01T00:00:00.000Z';
const narrowDateRange = jest.fn();

View file

@ -16,6 +16,8 @@ import { Columns } from '../../paginated_table';
import { TestProviders } from '../../../mock';
import { useMountAppended } from '../../../utils/use_mount_appended';
jest.mock('../../../lib/kibana');
const startDate = new Date(2001).toISOString();
const endDate = new Date(3000).toISOString();
const interval = 'days';

View file

@ -15,6 +15,8 @@ import React from 'react';
import { TestProviders } from '../../../mock';
import { useMountAppended } from '../../../utils/use_mount_appended';
jest.mock('../../../../common/lib/kibana');
const startDate = new Date(2001).toISOString();
const endDate = new Date(3000).toISOString();
describe('get_anomalies_network_table_columns', () => {

View file

@ -18,6 +18,9 @@ import {
import { TestProviders } from '../../mock';
import { getEmptyValue } from '../empty_value';
import { useMountAppended } from '../../utils/use_mount_appended';
jest.mock('../../lib/kibana');
describe('Table Helpers', () => {
const items = ['item1', 'item2', 'item3'];
const mount = useMountAppended();

View file

@ -8,10 +8,10 @@
import type React from 'react';
import uuid from 'uuid';
import { isError } from 'lodash/fp';
import { isAppError } from '@kbn/securitysolution-t-grid';
import { AppToast, ActionToaster } from './';
import { isToasterError } from './errors';
import { isAppError } from '../../utils/api';
/**
* Displays an error toast for the provided title and message

View file

@ -18,17 +18,11 @@ import {
createSecuritySolutionStorageMock,
mockIndexPattern,
} from '../../mock';
import { FilterManager } from '../../../../../../../src/plugins/data/public';
import { createStore, State } from '../../store';
import { Props } from './top_n';
import { StatefulTopN } from '.';
import {
ManageGlobalTimeline,
getTimelineDefaults,
} from '../../../timelines/components/manage_timeline';
import { TimelineId } from '../../../../common/types/timeline';
import { coreMock } from '../../../../../../../src/core/public/mocks';
jest.mock('react-router-dom', () => {
const original = jest.requireActual('react-router-dom');
@ -45,8 +39,6 @@ jest.mock('../link_to');
jest.mock('../../lib/kibana');
jest.mock('../../../timelines/store/timeline/actions');
const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings;
const field = 'process.name';
const value = 'nice';
@ -175,9 +167,7 @@ describe('StatefulTopN', () => {
beforeEach(() => {
wrapper = mount(
<TestProviders store={store}>
<ManageGlobalTimeline>
<StatefulTopN {...testProps} />
</ManageGlobalTimeline>
<StatefulTopN {...testProps} />
</TestProviders>
);
});
@ -244,26 +234,16 @@ describe('StatefulTopN', () => {
});
describe('rendering in a timeline context', () => {
let filterManager: FilterManager;
let wrapper: ReactWrapper;
beforeEach(() => {
filterManager = new FilterManager(mockUiSettingsForFilterManager);
const manageTimelineForTesting = {
[TimelineId.active]: {
...getTimelineDefaults(TimelineId.active),
filterManager,
},
};
testProps = {
...testProps,
timelineId: TimelineId.active,
};
wrapper = mount(
<TestProviders store={store}>
<ManageGlobalTimeline manageTimelineForTesting={manageTimelineForTesting}>
<StatefulTopN {...testProps} />
</ManageGlobalTimeline>
<StatefulTopN {...testProps} />
</TestProviders>
);
});
@ -320,25 +300,13 @@ describe('StatefulTopN', () => {
});
describe('rendering in a NON-active timeline context', () => {
test(`defaults to the 'Alert events' option when rendering in a NON-active timeline context (e.g. the Alerts table on the Detections page) when 'documentType' from 'useTimelineTypeContext()' is 'alerts'`, async () => {
const filterManager = new FilterManager(mockUiSettingsForFilterManager);
const manageTimelineForTesting = {
[TimelineId.active]: {
...getTimelineDefaults(TimelineId.active),
filterManager,
documentType: 'alerts',
},
};
testProps = {
...testProps,
timelineId: TimelineId.detectionsPage,
};
const wrapper = mount(
<TestProviders store={store}>
<ManageGlobalTimeline manageTimelineForTesting={manageTimelineForTesting}>
<StatefulTopN {...testProps} />
</ManageGlobalTimeline>
<StatefulTopN {...testProps} />
</TestProviders>
);
await waitFor(() => {

View file

@ -6,13 +6,13 @@
*/
import { EuiPopover } from '@elastic/eui';
import {
HOVER_ACTIONS_ALWAYS_SHOW_CLASS_NAME,
IS_DRAGGING_CLASS_NAME,
} from '@kbn/securitysolution-t-grid';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import { IS_DRAGGING_CLASS_NAME } from '../drag_and_drop/helpers';
export const HOVER_ACTIONS_ALWAYS_SHOW_CLASS_NAME = 'hover-actions-always-show';
/**
* To avoid expensive changes to the DOM, delay showing the popover menu
*/

View file

@ -83,7 +83,7 @@ export const useTimelineLastEventTime = ({
TimelineEventsLastEventTimeRequestOptions,
TimelineEventsLastEventTimeStrategyResponse
>(request, {
strategy: 'securitySolutionTimelineSearchStrategy',
strategy: 'timelineSearchStrategy',
abortSignal: abortCtrl.current.signal,
})
.subscribe({

View file

@ -151,7 +151,7 @@ export const useFetchIndex = (
{ indices: iNames, onlyCheckIfIndicesExist },
{
abortSignal: abortCtrl.current.signal,
strategy: 'securitySolutionIndexFields',
strategy: 'indexFields',
}
)
.subscribe({
@ -235,7 +235,7 @@ export const useIndexFields = (sourcererScopeName: SourcererScopeName) => {
{ indices: indicesName, onlyCheckIfIndicesExist: false },
{
abortSignal: abortCtrl.current.signal,
strategy: 'securitySolutionIndexFields',
strategy: 'indexFields',
}
)
.subscribe({

View file

@ -7,9 +7,10 @@
import { renderHook } from '@testing-library/react-hooks';
import { IEsError } from 'src/plugins/data/public';
import { KibanaError, SecurityAppError } from '@kbn/securitysolution-t-grid';
import { useToasts } from '../lib/kibana';
import { KibanaError, SecurityAppError } from '../utils/api';
import {
appErrorToErrorStack,
convertErrorToEnumerable,

View file

@ -7,11 +7,17 @@
import { useCallback, useRef } from 'react';
import { isString } from 'lodash/fp';
import {
AppError,
isAppError,
isKibanaError,
isSecurityAppError,
} from '@kbn/securitysolution-t-grid';
import { IEsError, isEsError } from '../../../../../../src/plugins/data/public';
import { ErrorToastOptions, ToastsStart, Toast } from '../../../../../../src/core/public';
import { useToasts } from '../lib/kibana';
import { AppError, isAppError, isKibanaError, isSecurityAppError } from '../utils/api';
export type UseAppToasts = Pick<ToastsStart, 'addSuccess' | 'addWarning'> & {
api: ToastsStart;

View file

@ -6,9 +6,10 @@
*/
import { EuiToolTip } from '@elastic/eui';
import React from 'react';
import { TooltipWithKeyboardShortcut } from '../../components/accessibility/tooltip_with_keyboard_shortcut';
import { TooltipWithKeyboardShortcut } from '../../components/accessibility';
import * as i18n from '../../components/drag_and_drop/translations';
import { Clipboard } from './clipboard';

View file

@ -6,6 +6,10 @@
*/
import { notificationServiceMock } from '../../../../../../../../src/core/public/mocks';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { createTGridMocks } from '../../../../../../timelines/public/mock';
import {
createKibanaContextProviderMock,
createUseUiSettingMock,
@ -30,14 +34,24 @@ export const useKibana = jest.fn().mockReturnValue({
})),
})),
},
query: {
...mockStartServicesMock.data.query,
filterManager: {
addFilters: jest.fn(),
getFilters: jest.fn(),
getUpdates$: jest.fn().mockReturnValue({ subscribe: jest.fn() }),
setAppFilters: jest.fn(),
},
},
},
timelines: createTGridMocks(),
},
});
export const useUiSetting = jest.fn(createUseUiSettingMock());
export const useUiSetting$ = jest.fn(createUseUiSetting$Mock());
export const useHttp = jest.fn().mockReturnValue(createStartServicesMock().http);
export const useTimeZone = jest.fn();
export const useDateFormat = jest.fn();
export const useDateFormat = jest.fn().mockReturnValue('MMM D, YYYY @ HH:mm:ss.SSS');
export const useBasePath = jest.fn(() => '/test/base/path');
export const useToasts = jest
.fn()

View file

@ -43,6 +43,7 @@ export const mockGlobalState: State = {
trustedAppsByPolicyEnabled: false,
metricsEntitiesEnabled: false,
ruleRegistryEnabled: false,
tGridEnabled: false,
},
},
hosts: {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { ColumnHeaderOptions } from '../../timelines/store/timeline/model';
import { ColumnHeaderOptions } from '../../../common';
import { defaultColumnHeaderType } from '../../timelines/components/timeline/body/column_headers/default_headers';
import {
DEFAULT_COLUMN_MIN_WIDTH,

View file

@ -15,7 +15,7 @@ import {
EuiPopoverTitle,
EuiSpacer,
} from '@elastic/eui';
import { ControlColumnProps } from '../../timelines/components/timeline/body/control_columns';
import { ControlColumnProps } from '../../../common/types/timeline';
const SelectionHeaderCell = () => {
return (

View file

@ -5,12 +5,20 @@
* 2.0.
*/
import { AnyAction, Reducer } from 'redux';
import reduceReducers from 'reduce-reducers';
import { tGridReducer } from '../../../../timelines/public';
import { hostsReducer } from '../../hosts/store';
import { networkReducer } from '../../network/store';
import { timelineReducer } from '../../timelines/store/timeline/reducer';
import { managementReducer } from '../../management/store/reducer';
import { ManagementPluginReducer } from '../../management';
import { SubPluginsInitReducer } from '../store';
import { mockGlobalState } from './global_state';
import { TimelineState } from '../../timelines/store/timeline/types';
import { defaultHeaders } from '../../timelines/components/timeline/body/column_headers/default_headers';
interface Global extends NodeJS.Global {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -19,10 +27,32 @@ interface Global extends NodeJS.Global {
export const globalNode: Global = global;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const combineTimelineReducer = reduceReducers<any>(
{
...mockGlobalState.timeline,
timelineById: {
...mockGlobalState.timeline.timelineById,
test: {
...mockGlobalState.timeline.timelineById.test,
defaultColumns: defaultHeaders,
loadingText: 'events',
footerText: 'events',
documentType: '',
selectAll: false,
queryFields: [],
unit: (n: number) => n,
},
},
},
tGridReducer,
timelineReducer
) as Reducer<TimelineState, AnyAction>;
export const SUB_PLUGINS_REDUCER: SubPluginsInitReducer = {
hosts: hostsReducer,
network: networkReducer,
timeline: timelineReducer,
timeline: combineTimelineReducer,
/**
* These state's are wrapped in `Immutable`, but for compatibility with the overall app architecture,
* they are cast to mutable versions here.

View file

@ -60,6 +60,7 @@ export interface GlobalGenericQuery {
isInspected: boolean;
loading: boolean;
selectedInspectIndex: number;
invalidKqlQuery?: Error;
}
export interface GlobalGraphqlQuery extends GlobalGenericQuery {

View file

@ -37,18 +37,6 @@ export type StoreState = HostsPluginState &
*/
export type State = CombinedState<StoreState>;
export type KueryFilterQueryKind = 'kuery' | 'lucene' | 'eql';
export interface KueryFilterQuery {
kind: KueryFilterQueryKind;
expression: string;
}
export interface SerializedFilterQuery {
kuery: KueryFilterQuery | null;
serializedQuery: string;
}
/**
* like redux's `MiddlewareAPI` but `getState` returns an `Immutable` version of
* state and `dispatch` accepts `Immutable` versions of actions.

View file

@ -14,6 +14,7 @@ import { i18n } from '@kbn/i18n';
import type { Filter } from '../../../../../../../src/plugins/data/common/es_query/filters';
import {
KueryFilterQueryKind,
TimelineId,
TimelineResult,
TimelineStatus,
@ -44,7 +45,6 @@ import {
replaceTemplateFieldFromMatchFilters,
replaceTemplateFieldFromDataProviders,
} from './helpers';
import { KueryFilterQueryKind } from '../../../common/store';
import {
DataProvider,
QueryOperator,
@ -399,7 +399,7 @@ export const sendAlertToTimelineAction = async ({
factoryQueryType: TimelineEventsQueries.details,
},
{
strategy: 'securitySolutionTimelineSearchStrategy',
strategy: 'timelineSearchStrategy',
}
)
.toPromise(),

View file

@ -11,6 +11,7 @@ import { shallow, mount } from 'enzyme';
import { AlertsUtilityBar, AlertsUtilityBarProps } from './index';
import { TestProviders } from '../../../../common/mock/test_providers';
jest.useFakeTimers();
jest.mock('../../../../common/lib/kibana');
describe('AlertsUtilityBar', () => {

View file

@ -6,11 +6,11 @@
*/
import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers';
import { RowRendererId } from '../../../../common/types/timeline';
import { ColumnHeaderOptions, RowRendererId } from '../../../../common/types/timeline';
import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
import { Filter } from '../../../../../../../src/plugins/data/common/es_query';
import { ColumnHeaderOptions, SubsetTimelineModel } from '../../../timelines/store/timeline/model';
import { SubsetTimelineModel } from '../../../timelines/store/timeline/model';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
import { columns } from '../../configurations/security_solution_detections/columns';

View file

@ -8,11 +8,11 @@
import { EuiPanel, EuiLoadingContent } from '@elastic/eui';
import { isEmpty } from 'lodash/fp';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { connect, ConnectedProps, useDispatch } from 'react-redux';
import { Dispatch } from 'redux';
import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
import { Filter, esQuery } from '../../../../../../../src/plugins/data/public';
import { TimelineIdLiteral } from '../../../../common/types/timeline';
import { RowRendererId, TimelineIdLiteral } from '../../../../common/types/timeline';
import { useAppToasts } from '../../../common/hooks/use_app_toasts';
import { StatefulEventsViewer } from '../../../common/components/events_viewer';
import { HeaderSection } from '../../../common/components/header_section';
@ -23,8 +23,6 @@ import { inputsSelectors, State, inputsModel } from '../../../common/store';
import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline';
import { TimelineModel } from '../../../timelines/store/timeline/model';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
import { useManageTimeline } from '../../../timelines/components/manage_timeline';
import { updateAlertStatusAction } from './actions';
import {
requiredFieldsForActions,
@ -95,6 +93,7 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
timelineId,
to,
}) => {
const dispatch = useDispatch();
const [showClearSelectionAction, setShowClearSelectionAction] = useState(false);
const [filterGroup, setFilterGroup] = useState<Status>(FILTER_OPEN);
const {
@ -106,7 +105,6 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
const kibana = useKibana();
const [, dispatchToaster] = useStateToaster();
const { addWarning } = useAppToasts();
const { initializeTimeline, setSelectAll } = useManageTimeline();
// TODO: Once we are past experimental phase this code should be removed
const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled');
@ -195,14 +193,16 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
// Catches state change isSelectAllChecked->false upon user selection change to reset utility bar
useEffect(() => {
if (isSelectAllChecked) {
setSelectAll({
id: timelineId,
selectAll: false,
});
dispatch(
timelineActions.setTGridSelectAll({
id: timelineId,
selectAll: false,
})
);
} else {
setShowClearSelectionAction(false);
}
}, [isSelectAllChecked, setSelectAll, timelineId]);
}, [dispatch, isSelectAllChecked, timelineId]);
// Callback for when open/closed filter changes
const onFilterGroupChangedCallback = useCallback(
@ -218,23 +218,27 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
// Callback for clearing entire selection from utility bar
const clearSelectionCallback = useCallback(() => {
clearSelected!({ id: timelineId });
setSelectAll({
id: timelineId,
selectAll: false,
});
dispatch(
timelineActions.setTGridSelectAll({
id: timelineId,
selectAll: false,
})
);
setShowClearSelectionAction(false);
}, [clearSelected, setSelectAll, setShowClearSelectionAction, timelineId]);
}, [clearSelected, dispatch, timelineId]);
// Callback for selecting all events on all pages from utility bar
// Dispatches to stateful_body's selectAll via TimelineTypeContext props
// as scope of response data required to actually set selectedEvents
const selectAllOnAllPagesCallback = useCallback(() => {
setSelectAll({
id: timelineId,
selectAll: true,
});
dispatch(
timelineActions.setTGridSelectAll({
id: timelineId,
selectAll: true,
})
);
setShowClearSelectionAction(true);
}, [setSelectAll, setShowClearSelectionAction, timelineId]);
}, [dispatch, timelineId]);
const updateAlertsStatusCallback: UpdateAlertsStatusCallback = useCallback(
async (
@ -330,22 +334,22 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
: alertsDefaultModel;
useEffect(() => {
initializeTimeline({
defaultModel: {
...defaultTimelineModel,
columns,
},
documentType: i18n.ALERTS_DOCUMENT_TYPE,
filterManager,
footerText: i18n.TOTAL_COUNT_OF_ALERTS,
id: timelineId,
loadingText: i18n.LOADING_ALERTS,
selectAll: false,
queryFields: requiredFieldsForActions,
title: '',
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
dispatch(
timelineActions.initializeTGridSettings({
defaultColumns: columns,
documentType: i18n.ALERTS_DOCUMENT_TYPE,
excludedRowRendererIds: defaultTimelineModel.excludedRowRendererIds as RowRendererId[],
filterManager,
footerText: i18n.TOTAL_COUNT_OF_ALERTS,
id: timelineId,
loadingText: i18n.LOADING_ALERTS,
selectAll: false,
queryFields: requiredFieldsForActions,
title: '',
showCheckboxes: true,
})
);
}, [dispatch, defaultTimelineModel, filterManager, timelineId]);
const headerFilterGroup = useMemo(
() => <AlertsTableFilterGroup onFilterGroupChanged={onFilterGroupChangedCallback} />,

Some files were not shown because too many files have changed in this diff Show more