mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] Deletes Old Timeline Code (#196243)
## Summary Fixes https://github.com/elastic/security-team/issues/9676 This is follow up of the PR : https://github.com/elastic/kibana/pull/195959 where we had removed feature flag `unifiedComponentsInTimelineDisabled`. This PR completes unified components migration in Timeline. ## Changes Most changes in this PR are : 1. Deletion for the obsolete code. 2. Refactoring of needed utils ( only 3. have marked in comments below ) 3. Updating import path of above utils.
This commit is contained in:
parent
3fff48a2ca
commit
c9d167f1ab
92 changed files with 212 additions and 7104 deletions
|
@ -19,12 +19,12 @@ import type {
|
|||
SetEventsLoading,
|
||||
ControlColumnProps,
|
||||
} from '../../../../../common/types';
|
||||
import { getMappedNonEcsValue } from '../../../../timelines/components/timeline/body/data_driven_columns';
|
||||
import type { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy';
|
||||
import type { ColumnHeaderOptions, OnRowSelected } from '../../../../../common/types/timeline';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';
|
||||
import { useTourContext } from '../../guided_onboarding_tour';
|
||||
import { AlertsCasesTourSteps, SecurityStepId } from '../../guided_onboarding_tour/tour_config';
|
||||
import { getMappedNonEcsValue } from '../../../utils/get_mapped_non_ecs_value';
|
||||
|
||||
export type RowActionProps = EuiDataGridCellValueElementProps & {
|
||||
columnHeaders: ColumnHeaderOptions[];
|
||||
|
|
|
@ -36,7 +36,6 @@ import type { EuiTheme } from '@kbn/kibana-react-plugin/common';
|
|||
import type { EuiDataGridRowHeightsOptions } from '@elastic/eui';
|
||||
import type { RunTimeMappings } from '@kbn/timelines-plugin/common/search_strategy';
|
||||
import { ALERTS_TABLE_VIEW_SELECTION_KEY } from '../../../../common/constants';
|
||||
import type { Sort } from '../../../timelines/components/timeline/body/sort';
|
||||
import type {
|
||||
ControlColumnProps,
|
||||
OnRowSelected,
|
||||
|
@ -44,7 +43,7 @@ import type {
|
|||
SetEventsDeleted,
|
||||
SetEventsLoading,
|
||||
} from '../../../../common/types';
|
||||
import type { RowRenderer } from '../../../../common/types/timeline';
|
||||
import type { RowRenderer, SortColumnTimeline as Sort } from '../../../../common/types/timeline';
|
||||
import { InputsModelId } from '../../store/inputs/constants';
|
||||
import type { State } from '../../store';
|
||||
import { inputsActions } from '../../store/actions';
|
||||
|
|
|
@ -10,9 +10,9 @@ import { EuiButtonIcon, EuiToolTip, EuiCheckbox } from '@elastic/eui';
|
|||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import styled from 'styled-components';
|
||||
import { isFullScreen } from '../../../timelines/components/timeline/helpers';
|
||||
import type { HeaderActionProps } from '../../../../common/types';
|
||||
import { TimelineId } from '../../../../common/types';
|
||||
import { isFullScreen } from '../../../timelines/components/timeline/body/column_headers';
|
||||
import { isActiveTimeline } from '../../../helpers';
|
||||
import { getColumnHeader } from '../../../timelines/components/timeline/body/column_headers/helpers';
|
||||
import { timelineActions } from '../../../timelines/store';
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { TimelineNonEcsData } from '@kbn/timelines-plugin/common';
|
||||
import { getMappedNonEcsValue, useGetMappedNonEcsValue } from './get_mapped_non_ecs_value';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
describe('getMappedNonEcsValue', () => {
|
||||
it('should return the correct value', () => {
|
||||
const data: TimelineNonEcsData[] = [{ field: 'field1', value: ['value1'] }];
|
||||
const fieldName = 'field1';
|
||||
const result = getMappedNonEcsValue({ data, fieldName });
|
||||
expect(result).toEqual(['value1']);
|
||||
});
|
||||
|
||||
it('should return undefined if item is null', () => {
|
||||
const data: TimelineNonEcsData[] = [{ field: 'field1', value: ['value1'] }];
|
||||
const fieldName = 'field2';
|
||||
const result = getMappedNonEcsValue({ data, fieldName });
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should return undefined if item.value is null', () => {
|
||||
const data: TimelineNonEcsData[] = [{ field: 'field1', value: null }];
|
||||
const fieldName = 'non_existent_field';
|
||||
const result = getMappedNonEcsValue({ data, fieldName });
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should return undefined if data is undefined', () => {
|
||||
const data = undefined;
|
||||
const fieldName = 'field1';
|
||||
const result = getMappedNonEcsValue({ data, fieldName });
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should return undefined if data is empty', () => {
|
||||
const data: TimelineNonEcsData[] = [];
|
||||
const fieldName = 'field1';
|
||||
const result = getMappedNonEcsValue({ data, fieldName });
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useGetMappedNonEcsValue', () => {
|
||||
it('should return the correct value', () => {
|
||||
const data: TimelineNonEcsData[] = [{ field: 'field1', value: ['value1'] }];
|
||||
const fieldName = 'field1';
|
||||
const { result } = renderHook(() => useGetMappedNonEcsValue({ data, fieldName }));
|
||||
expect(result.current).toEqual(['value1']);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { TimelineNonEcsData } from '@kbn/timelines-plugin/common';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const getMappedNonEcsValue = ({
|
||||
data,
|
||||
fieldName,
|
||||
}: {
|
||||
data?: TimelineNonEcsData[];
|
||||
fieldName: string;
|
||||
}): string[] | undefined => {
|
||||
/*
|
||||
While data _should_ always be defined
|
||||
There is the potential for race conditions where a component using this function
|
||||
is still visible in the UI, while the data has since been removed.
|
||||
To cover all scenarios where this happens we'll check for the presence of data here
|
||||
*/
|
||||
if (!data || data.length === 0) return undefined;
|
||||
const item = data.find((d) => d.field === fieldName);
|
||||
if (item != null && item.value != null) {
|
||||
return item.value;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const useGetMappedNonEcsValue = ({
|
||||
data,
|
||||
fieldName,
|
||||
}: {
|
||||
data?: TimelineNonEcsData[];
|
||||
fieldName: string;
|
||||
}): string[] | undefined => {
|
||||
return useMemo(() => getMappedNonEcsValue({ data, fieldName }), [data, fieldName]);
|
||||
};
|
|
@ -57,7 +57,7 @@ import {
|
|||
} from '@kbn/lists-plugin/common/constants.mock';
|
||||
import { of } from 'rxjs';
|
||||
import { timelineDefaults } from '../../../timelines/store/defaults';
|
||||
import { defaultUdtHeaders } from '../../../timelines/components/timeline/unified_components/default_headers';
|
||||
import { defaultUdtHeaders } from '../../../timelines/components/timeline/body/column_headers/default_headers';
|
||||
|
||||
jest.mock('../../../timelines/containers/api', () => ({
|
||||
getTimelineTemplate: jest.fn(),
|
||||
|
|
|
@ -33,7 +33,7 @@ import { getField } from '../../../../helpers';
|
|||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction';
|
||||
import { ALERTS_ACTIONS } from '../../../../common/lib/apm/user_actions';
|
||||
import { defaultUdtHeaders } from '../../../../timelines/components/timeline/unified_components/default_headers';
|
||||
import { defaultUdtHeaders } from '../../../../timelines/components/timeline/body/column_headers/default_headers';
|
||||
|
||||
interface UseInvestigateInTimelineActionProps {
|
||||
ecsRowData?: Ecs | Ecs[] | null;
|
||||
|
|
|
@ -12,9 +12,9 @@ import type { EuiDataGridCellValueElementProps } from '@elastic/eui';
|
|||
import { EuiLink } from '@elastic/eui';
|
||||
import { ALERT_DURATION, ALERT_REASON, ALERT_SEVERITY, ALERT_STATUS } from '@kbn/rule-data-utils';
|
||||
|
||||
import { useGetMappedNonEcsValue } from '../../../../common/utils/get_mapped_non_ecs_value';
|
||||
import { TruncatableText } from '../../../../common/components/truncatable_text';
|
||||
import { Severity } from '../../../components/severity';
|
||||
import { useGetMappedNonEcsValue } from '../../../../timelines/components/timeline/body/data_driven_columns';
|
||||
import type { CellValueElementProps } from '../../../../timelines/components/timeline/cell_rendering';
|
||||
import { DefaultCellRenderer } from '../../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
|
||||
import { Status } from '../../../components/status';
|
||||
|
|
|
@ -9,10 +9,10 @@ import type { EuiDataGridCellValueElementProps } from '@elastic/eui';
|
|||
import { ALERT_SEVERITY, ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
import React from 'react';
|
||||
|
||||
import { useGetMappedNonEcsValue } from '../../../../common/utils/get_mapped_non_ecs_value';
|
||||
import { DefaultDraggable } from '../../../../common/components/draggables';
|
||||
import { TruncatableText } from '../../../../common/components/truncatable_text';
|
||||
import { Severity } from '../../../components/severity';
|
||||
import { useGetMappedNonEcsValue } from '../../../../timelines/components/timeline/body/data_driven_columns';
|
||||
import type { CellValueElementProps } from '../../../../timelines/components/timeline/cell_rendering';
|
||||
import { DefaultCellRenderer } from '../../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
|
||||
|
||||
|
|
|
@ -29,12 +29,12 @@ import {
|
|||
useGlobalFullScreen,
|
||||
useTimelineFullScreen,
|
||||
} from '../../../common/containers/use_full_screen';
|
||||
import { isFullScreen } from '../timeline/body/column_headers';
|
||||
import { inputsActions } from '../../../common/store/actions';
|
||||
import { Resolver } from '../../../resolver/view';
|
||||
import { useTimelineDataFilters } from '../../containers/use_timeline_data_filters';
|
||||
import { timelineSelectors } from '../../store';
|
||||
import { timelineDefaults } from '../../store/defaults';
|
||||
import { isFullScreen } from '../timeline/helpers';
|
||||
|
||||
const SESSION_VIEW_FULL_SCREEN = 'sessionViewFullScreen';
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import { TimelineId } from '../../../../../common/types';
|
|||
import { timelineActions } from '../../../store';
|
||||
import { TestProviders } from '../../../../common/mock';
|
||||
import { RowRendererValues } from '../../../../../common/api/timeline';
|
||||
import { defaultUdtHeaders } from '../../timeline/unified_components/default_headers';
|
||||
import { defaultUdtHeaders } from '../../timeline/body/column_headers/default_headers';
|
||||
|
||||
jest.mock('../../../../common/components/discover_in_timeline/use_discover_in_timeline_context');
|
||||
jest.mock('../../../../common/hooks/use_selector');
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
TimelineTypeEnum,
|
||||
} from '../../../../common/api/timeline';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
import { defaultUdtHeaders } from '../timeline/unified_components/default_headers';
|
||||
import { defaultUdtHeaders } from '../timeline/body/column_headers/default_headers';
|
||||
|
||||
jest.mock('../../../common/components/discover_in_timeline/use_discover_in_timeline_context');
|
||||
jest.mock('../../../common/hooks/use_selector');
|
||||
|
|
|
@ -35,7 +35,7 @@ import {
|
|||
mockTemplate as mockSelectedTemplate,
|
||||
} from './__mocks__';
|
||||
import { resolveTimeline } from '../../containers/api';
|
||||
import { defaultUdtHeaders } from '../timeline/unified_components/default_headers';
|
||||
import { defaultUdtHeaders } from '../timeline/body/column_headers/default_headers';
|
||||
|
||||
jest.mock('../../../common/hooks/use_experimental_features');
|
||||
|
||||
|
|
|
@ -35,7 +35,10 @@ import { useUpdateTimeline } from './use_update_timeline';
|
|||
import type { TimelineModel } from '../../store/model';
|
||||
import { timelineDefaults } from '../../store/defaults';
|
||||
|
||||
import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers';
|
||||
import {
|
||||
defaultColumnHeaderType,
|
||||
defaultUdtHeaders,
|
||||
} from '../timeline/body/column_headers/default_headers';
|
||||
|
||||
import type { OpenTimelineResult, TimelineErrorCallback } from './types';
|
||||
import { IS_OPERATOR } from '../timeline/data_providers/data_provider';
|
||||
|
@ -46,7 +49,6 @@ import {
|
|||
DEFAULT_TO_MOMENT,
|
||||
} from '../../../common/utils/default_date_settings';
|
||||
import { resolveTimeline } from '../../containers/api';
|
||||
import { defaultUdtHeaders } from '../timeline/unified_components/default_headers';
|
||||
import { timelineActions } from '../../store';
|
||||
|
||||
export const OPEN_TIMELINE_CLASS_NAME = 'open-timeline';
|
||||
|
|
|
@ -53,7 +53,7 @@ import { SourcererScopeName } from '../../../sourcerer/store/model';
|
|||
import { useSourcererDataView } from '../../../sourcerer/containers';
|
||||
import { useStartTransaction } from '../../../common/lib/apm/use_start_transaction';
|
||||
import { TIMELINE_ACTIONS } from '../../../common/lib/apm/user_actions';
|
||||
import { defaultUdtHeaders } from '../timeline/unified_components/default_headers';
|
||||
import { defaultUdtHeaders } from '../timeline/body/column_headers/default_headers';
|
||||
import { timelineDefaults } from '../../store/defaults';
|
||||
|
||||
interface OwnProps<TCache = object> {
|
||||
|
|
|
@ -1,513 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = `
|
||||
<ColumnHeadersComponent
|
||||
actionsColumnWidth={124}
|
||||
browserFields={
|
||||
Object {
|
||||
"agent": Object {
|
||||
"fields": Object {
|
||||
"agent.ephemeral_id": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"name": "agent.ephemeral_id",
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
"agent.hostname": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"name": "agent.hostname",
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
"agent.id": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"name": "agent.id",
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
"agent.name": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"name": "agent.name",
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"auditd": Object {
|
||||
"fields": Object {
|
||||
"auditd.data.a0": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"name": "auditd.data.a0",
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
"auditd.data.a1": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"name": "auditd.data.a1",
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
"auditd.data.a2": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"name": "auditd.data.a2",
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"base": Object {
|
||||
"fields": Object {
|
||||
"@timestamp": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"date",
|
||||
],
|
||||
"name": "@timestamp",
|
||||
"readFromDocValues": true,
|
||||
"searchable": true,
|
||||
"type": "date",
|
||||
},
|
||||
"_id": Object {
|
||||
"aggregatable": false,
|
||||
"esTypes": Array [],
|
||||
"name": "_id",
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
"message": Object {
|
||||
"aggregatable": false,
|
||||
"esTypes": Array [
|
||||
"text",
|
||||
],
|
||||
"format": Object {
|
||||
"id": "string",
|
||||
},
|
||||
"name": "message",
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"client": Object {
|
||||
"fields": Object {
|
||||
"client.address": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"name": "client.address",
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
"client.bytes": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"long",
|
||||
],
|
||||
"name": "client.bytes",
|
||||
"searchable": true,
|
||||
"type": "number",
|
||||
},
|
||||
"client.domain": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"name": "client.domain",
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
"client.geo.country_iso_code": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"name": "client.geo.country_iso_code",
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"cloud": Object {
|
||||
"fields": Object {
|
||||
"cloud.account.id": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"name": "cloud.account.id",
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
"cloud.availability_zone": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"name": "cloud.availability_zone",
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"container": Object {
|
||||
"fields": Object {
|
||||
"container.id": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"name": "container.id",
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
"container.image.name": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"name": "container.image.name",
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
"container.image.tag": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"name": "container.image.tag",
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"destination": Object {
|
||||
"fields": Object {
|
||||
"destination.address": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"name": "destination.address",
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
"destination.bytes": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"long",
|
||||
],
|
||||
"name": "destination.bytes",
|
||||
"searchable": true,
|
||||
"type": "number",
|
||||
},
|
||||
"destination.domain": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"name": "destination.domain",
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
"destination.ip": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"ip",
|
||||
],
|
||||
"name": "destination.ip",
|
||||
"searchable": true,
|
||||
"type": "ip",
|
||||
},
|
||||
"destination.port": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"long",
|
||||
],
|
||||
"name": "destination.port",
|
||||
"searchable": true,
|
||||
"type": "number",
|
||||
},
|
||||
},
|
||||
},
|
||||
"event": Object {
|
||||
"fields": Object {
|
||||
"event.action": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"format": Object {
|
||||
"id": "string",
|
||||
},
|
||||
"name": "event.action",
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
"event.category": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"format": Object {
|
||||
"id": "string",
|
||||
},
|
||||
"name": "event.category",
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
"event.end": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"date",
|
||||
],
|
||||
"name": "event.end",
|
||||
"searchable": true,
|
||||
"type": "date",
|
||||
},
|
||||
"event.kind": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"format": Object {
|
||||
"id": "string",
|
||||
},
|
||||
"name": "event.kind",
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
"event.severity": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"long",
|
||||
],
|
||||
"format": Object {
|
||||
"id": "number",
|
||||
},
|
||||
"name": "event.severity",
|
||||
"searchable": true,
|
||||
"type": "number",
|
||||
},
|
||||
},
|
||||
},
|
||||
"host": Object {
|
||||
"fields": Object {
|
||||
"host.name": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"format": Object {
|
||||
"id": "string",
|
||||
},
|
||||
"name": "host.name",
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"nestedField": Object {
|
||||
"fields": Object {
|
||||
"nestedField.firstAttributes": Object {
|
||||
"aggregatable": false,
|
||||
"name": "nestedField.firstAttributes",
|
||||
"searchable": true,
|
||||
"subType": Object {
|
||||
"nested": Object {
|
||||
"path": "nestedField",
|
||||
},
|
||||
},
|
||||
"type": "string",
|
||||
},
|
||||
"nestedField.secondAttributes": Object {
|
||||
"aggregatable": false,
|
||||
"name": "nestedField.secondAttributes",
|
||||
"searchable": true,
|
||||
"subType": Object {
|
||||
"nested": Object {
|
||||
"path": "nestedField",
|
||||
},
|
||||
},
|
||||
"type": "string",
|
||||
},
|
||||
"nestedField.thirdAttributes": Object {
|
||||
"aggregatable": false,
|
||||
"name": "nestedField.thirdAttributes",
|
||||
"searchable": true,
|
||||
"subType": Object {
|
||||
"nested": Object {
|
||||
"path": "nestedField",
|
||||
},
|
||||
},
|
||||
"type": "date",
|
||||
},
|
||||
},
|
||||
},
|
||||
"process": Object {
|
||||
"fields": Object {
|
||||
"process.args": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"name": "process.args",
|
||||
"readFromDocValues": true,
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"source": Object {
|
||||
"fields": Object {
|
||||
"source.ip": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"ip",
|
||||
],
|
||||
"name": "source.ip",
|
||||
"searchable": true,
|
||||
"type": "ip",
|
||||
},
|
||||
"source.port": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"long",
|
||||
],
|
||||
"name": "source.port",
|
||||
"searchable": true,
|
||||
"type": "number",
|
||||
},
|
||||
},
|
||||
},
|
||||
"user": Object {
|
||||
"fields": Object {
|
||||
"user.name": Object {
|
||||
"aggregatable": true,
|
||||
"esTypes": Array [
|
||||
"keyword",
|
||||
],
|
||||
"format": Object {
|
||||
"id": "string",
|
||||
},
|
||||
"name": "user.name",
|
||||
"searchable": true,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
columnHeaders={
|
||||
Array [
|
||||
Object {
|
||||
"columnHeaderType": "not-filtered",
|
||||
"esTypes": Array [
|
||||
"date",
|
||||
],
|
||||
"id": "@timestamp",
|
||||
"initialWidth": 190,
|
||||
"type": "date",
|
||||
},
|
||||
Object {
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "message",
|
||||
"initialWidth": 180,
|
||||
},
|
||||
Object {
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "event.category",
|
||||
"initialWidth": 180,
|
||||
},
|
||||
Object {
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "event.action",
|
||||
"initialWidth": 180,
|
||||
},
|
||||
Object {
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "host.name",
|
||||
"initialWidth": 180,
|
||||
},
|
||||
Object {
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "source.ip",
|
||||
"initialWidth": 180,
|
||||
},
|
||||
Object {
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "destination.ip",
|
||||
"initialWidth": 180,
|
||||
},
|
||||
Object {
|
||||
"columnHeaderType": "not-filtered",
|
||||
"id": "user.name",
|
||||
"initialWidth": 180,
|
||||
},
|
||||
]
|
||||
}
|
||||
isSelectAllChecked={false}
|
||||
leadingControlColumns={
|
||||
Array [
|
||||
Object {
|
||||
"headerCellRender": Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"compare": null,
|
||||
"type": Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
},
|
||||
},
|
||||
"id": "default-timeline-control-column",
|
||||
"rowCellRender": Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
},
|
||||
"width": 124,
|
||||
},
|
||||
]
|
||||
}
|
||||
onSelectAll={[Function]}
|
||||
show={true}
|
||||
showEventsSelect={false}
|
||||
showSelectAllCheckbox={false}
|
||||
sort={
|
||||
Array [
|
||||
Object {
|
||||
"columnId": "@timestamp",
|
||||
"columnType": "date",
|
||||
"esTypes": Array [
|
||||
"date",
|
||||
],
|
||||
"sortDirection": "desc",
|
||||
},
|
||||
]
|
||||
}
|
||||
tabType="query"
|
||||
timelineId="timeline-test"
|
||||
trailingControlColumns={Array []}
|
||||
/>
|
||||
`;
|
|
@ -1,70 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButtonIcon } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import type { ColumnHeaderOptions } from '../../../../../../../common/types';
|
||||
import type { OnColumnRemoved } from '../../../events';
|
||||
import { EventsHeadingExtra, EventsLoading } from '../../../styles';
|
||||
import type { Sort } from '../../sort';
|
||||
|
||||
import * as i18n from '../translations';
|
||||
|
||||
interface Props {
|
||||
header: ColumnHeaderOptions;
|
||||
isLoading: boolean;
|
||||
onColumnRemoved: OnColumnRemoved;
|
||||
sort: Sort[];
|
||||
}
|
||||
|
||||
/** Given a `header`, returns the `SortDirection` applicable to it */
|
||||
|
||||
export const CloseButton = React.memo<{
|
||||
columnId: string;
|
||||
onColumnRemoved: OnColumnRemoved;
|
||||
}>(({ columnId, onColumnRemoved }) => {
|
||||
const handleClick = useCallback(
|
||||
(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
// To avoid a re-sorting when you delete a column
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onColumnRemoved(columnId);
|
||||
},
|
||||
[columnId, onColumnRemoved]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.REMOVE_COLUMN}
|
||||
color="text"
|
||||
data-test-subj="remove-column"
|
||||
iconType="cross"
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
CloseButton.displayName = 'CloseButton';
|
||||
|
||||
export const Actions = React.memo<Props>(({ header, onColumnRemoved, sort, isLoading }) => {
|
||||
return (
|
||||
<>
|
||||
{sort.some((i) => i.columnId === header.id) && isLoading ? (
|
||||
<EventsHeadingExtra className="siemEventsHeading__extra--loading">
|
||||
<EventsLoading data-test-subj="timeline-loading-spinner" />
|
||||
</EventsHeadingExtra>
|
||||
) : (
|
||||
<EventsHeadingExtra className="siemEventsHeading__extra--close">
|
||||
<CloseButton columnId={header.id} onColumnRemoved={onColumnRemoved} />
|
||||
</EventsHeadingExtra>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
Actions.displayName = 'Actions';
|
|
@ -1,307 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { EuiContextMenuPanelDescriptor } from '@elastic/eui';
|
||||
import { EuiContextMenu, EuiIcon, EuiPopover } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import type { DraggableChildrenFn } from '@hello-pangea/dnd';
|
||||
import { Draggable } from '@hello-pangea/dnd';
|
||||
import type { ResizeCallback } from 're-resizable';
|
||||
import { Resizable } from 're-resizable';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid';
|
||||
|
||||
import { useDraggableKeyboardWrapper } from '../../../../../common/components/drag_and_drop/draggable_keyboard_wrapper_hook';
|
||||
import { DEFAULT_COLUMN_MIN_WIDTH } from '../constants';
|
||||
import { getDraggableFieldId } from '../../../../../common/components/drag_and_drop/helpers';
|
||||
import type { ColumnHeaderOptions } from '../../../../../../common/types/timeline';
|
||||
import { TimelineTabs } from '../../../../../../common/types/timeline';
|
||||
import { Direction } from '../../../../../../common/search_strategy';
|
||||
import type { OnFilterChange } from '../../events';
|
||||
import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers';
|
||||
import { EventsTh, EventsThContent, EventsHeadingHandle } from '../../styles';
|
||||
import type { Sort } from '../sort';
|
||||
|
||||
import { Header } from './header';
|
||||
import { timelineActions } from '../../../../store';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
const ContextMenu = styled(EuiContextMenu)`
|
||||
width: 115px;
|
||||
|
||||
& .euiContextMenuItem {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
width: 115px;
|
||||
}
|
||||
`;
|
||||
|
||||
const PopoverContainer = styled.div<{ $width: number }>`
|
||||
& .euiPopover {
|
||||
padding-right: 8px;
|
||||
width: ${({ $width }) => $width}px;
|
||||
}
|
||||
`;
|
||||
|
||||
const RESIZABLE_ENABLE = { right: true };
|
||||
|
||||
interface ColumneHeaderProps {
|
||||
draggableIndex: number;
|
||||
header: ColumnHeaderOptions;
|
||||
isDragging: boolean;
|
||||
onFilterChange?: OnFilterChange;
|
||||
sort: Sort[];
|
||||
tabType: TimelineTabs;
|
||||
timelineId: string;
|
||||
}
|
||||
|
||||
const ColumnHeaderComponent: React.FC<ColumneHeaderProps> = ({
|
||||
draggableIndex,
|
||||
header,
|
||||
timelineId,
|
||||
isDragging,
|
||||
onFilterChange,
|
||||
sort,
|
||||
tabType,
|
||||
}) => {
|
||||
const keyboardHandlerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [hoverActionsOwnFocus, setHoverActionsOwnFocus] = useState<boolean>(false);
|
||||
const restoreFocus = useCallback(() => keyboardHandlerRef.current?.focus(), []);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const resizableSize = useMemo(
|
||||
() => ({
|
||||
width: header.initialWidth ?? DEFAULT_COLUMN_MIN_WIDTH,
|
||||
height: 'auto',
|
||||
}),
|
||||
[header.initialWidth]
|
||||
);
|
||||
const resizableStyle: {
|
||||
position: 'absolute' | 'relative';
|
||||
} = useMemo(
|
||||
() => ({
|
||||
position: isDragging ? 'absolute' : 'relative',
|
||||
}),
|
||||
[isDragging]
|
||||
);
|
||||
const resizableHandleComponent = useMemo(
|
||||
() => ({
|
||||
right: <EventsHeadingHandle />,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const handleResizeStop: ResizeCallback = useCallback(
|
||||
(e, direction, ref, delta) => {
|
||||
dispatch(
|
||||
timelineActions.applyDeltaToColumnWidth({
|
||||
columnId: header.id,
|
||||
delta: delta.width,
|
||||
id: timelineId,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, header.id, timelineId]
|
||||
);
|
||||
const draggableId = useMemo(
|
||||
() =>
|
||||
getDraggableFieldId({
|
||||
contextId: `timeline-column-headers-${tabType}-${timelineId}`,
|
||||
fieldId: header.id,
|
||||
}),
|
||||
[tabType, timelineId, header.id]
|
||||
);
|
||||
|
||||
const onColumnSort = useCallback(
|
||||
(sortDirection: Direction) => {
|
||||
const columnId = header.id;
|
||||
const columnType = header.type ?? '';
|
||||
const esTypes = header.esTypes ?? [];
|
||||
const headerIndex = sort.findIndex((col) => col.columnId === columnId);
|
||||
const newSort =
|
||||
headerIndex === -1
|
||||
? [
|
||||
...sort,
|
||||
{
|
||||
columnId,
|
||||
columnType,
|
||||
esTypes,
|
||||
sortDirection,
|
||||
},
|
||||
]
|
||||
: [
|
||||
...sort.slice(0, headerIndex),
|
||||
{
|
||||
columnId,
|
||||
columnType,
|
||||
esTypes,
|
||||
sortDirection,
|
||||
},
|
||||
...sort.slice(headerIndex + 1),
|
||||
];
|
||||
|
||||
dispatch(
|
||||
timelineActions.updateSort({
|
||||
id: timelineId,
|
||||
sort: newSort,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, header, sort, timelineId]
|
||||
);
|
||||
|
||||
const handleClosePopOverTrigger = useCallback(() => {
|
||||
setHoverActionsOwnFocus(false);
|
||||
restoreFocus();
|
||||
}, [restoreFocus]);
|
||||
|
||||
const panels: EuiContextMenuPanelDescriptor[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 0,
|
||||
items: [
|
||||
{
|
||||
icon: <EuiIcon type="eyeClosed" size="s" />,
|
||||
name: i18n.HIDE_COLUMN,
|
||||
onClick: () => {
|
||||
dispatch(timelineActions.removeColumn({ id: timelineId, columnId: header.id }));
|
||||
handleClosePopOverTrigger();
|
||||
},
|
||||
},
|
||||
...(tabType !== TimelineTabs.eql
|
||||
? [
|
||||
{
|
||||
disabled: !header.aggregatable,
|
||||
icon: <EuiIcon type="sortUp" size="s" />,
|
||||
name: i18n.SORT_AZ,
|
||||
onClick: () => {
|
||||
onColumnSort(Direction.asc);
|
||||
handleClosePopOverTrigger();
|
||||
},
|
||||
},
|
||||
{
|
||||
disabled: !header.aggregatable,
|
||||
icon: <EuiIcon type="sortDown" size="s" />,
|
||||
name: i18n.SORT_ZA,
|
||||
onClick: () => {
|
||||
onColumnSort(Direction.desc);
|
||||
handleClosePopOverTrigger();
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
dispatch,
|
||||
handleClosePopOverTrigger,
|
||||
header.aggregatable,
|
||||
header.id,
|
||||
onColumnSort,
|
||||
tabType,
|
||||
timelineId,
|
||||
]
|
||||
);
|
||||
|
||||
const headerButton = useMemo(
|
||||
() => (
|
||||
<Header timelineId={timelineId} header={header} onFilterChange={onFilterChange} sort={sort} />
|
||||
),
|
||||
[header, onFilterChange, sort, timelineId]
|
||||
);
|
||||
|
||||
const DraggableContent = useCallback<DraggableChildrenFn>(
|
||||
(dragProvided) => (
|
||||
<EventsTh
|
||||
data-test-subj="draggable-header"
|
||||
{...dragProvided.draggableProps}
|
||||
{...dragProvided.dragHandleProps}
|
||||
ref={dragProvided.innerRef}
|
||||
>
|
||||
<EventsThContent>
|
||||
<PopoverContainer $width={header.initialWidth ?? DEFAULT_COLUMN_MIN_WIDTH}>
|
||||
<EuiPopover
|
||||
anchorPosition="downLeft"
|
||||
button={headerButton}
|
||||
closePopover={handleClosePopOverTrigger}
|
||||
isOpen={hoverActionsOwnFocus}
|
||||
ownFocus
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<ContextMenu initialPanelId={0} panels={panels} />
|
||||
</EuiPopover>
|
||||
</PopoverContainer>
|
||||
</EventsThContent>
|
||||
</EventsTh>
|
||||
),
|
||||
[handleClosePopOverTrigger, headerButton, header.initialWidth, hoverActionsOwnFocus, panels]
|
||||
);
|
||||
|
||||
const onFocus = useCallback(() => {
|
||||
keyboardHandlerRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
const openPopover = useCallback(() => {
|
||||
setHoverActionsOwnFocus(true);
|
||||
}, []);
|
||||
|
||||
const { onBlur, onKeyDown } = useDraggableKeyboardWrapper({
|
||||
closePopover: handleClosePopOverTrigger,
|
||||
draggableId,
|
||||
fieldName: header.id,
|
||||
keyboardHandlerRef,
|
||||
openPopover,
|
||||
});
|
||||
|
||||
const keyDownHandler = useCallback(
|
||||
(keyboardEvent: React.KeyboardEvent) => {
|
||||
if (!hoverActionsOwnFocus) {
|
||||
onKeyDown(keyboardEvent);
|
||||
}
|
||||
},
|
||||
[hoverActionsOwnFocus, onKeyDown]
|
||||
);
|
||||
|
||||
return (
|
||||
<Resizable
|
||||
enable={RESIZABLE_ENABLE}
|
||||
size={resizableSize}
|
||||
style={resizableStyle}
|
||||
handleComponent={resizableHandleComponent}
|
||||
onResizeStop={handleResizeStop}
|
||||
>
|
||||
<div
|
||||
aria-colindex={
|
||||
draggableIndex != null ? draggableIndex + ARIA_COLUMN_INDEX_OFFSET : undefined
|
||||
}
|
||||
className={DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME}
|
||||
data-test-subj="draggableWrapperKeyboardHandler"
|
||||
onClick={onFocus}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={keyDownHandler}
|
||||
ref={keyboardHandlerRef}
|
||||
role="columnheader"
|
||||
tabIndex={0}
|
||||
>
|
||||
<Draggable
|
||||
data-test-subj="draggable"
|
||||
// Required for drag events while hovering the sort button to work: https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/draggable.md#interactive-child-elements-within-a-draggable-
|
||||
disableInteractiveElementBlocking
|
||||
draggableId={draggableId}
|
||||
index={draggableIndex}
|
||||
key={header.id}
|
||||
>
|
||||
{DraggableContent}
|
||||
</Draggable>
|
||||
</div>
|
||||
</Resizable>
|
||||
);
|
||||
};
|
||||
|
||||
export const ColumnHeader = React.memo(ColumnHeaderComponent);
|
|
@ -6,51 +6,48 @@
|
|||
*/
|
||||
|
||||
import type { ColumnHeaderOptions, ColumnHeaderType } from '../../../../../../common/types';
|
||||
import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants';
|
||||
import {
|
||||
DEFAULT_COLUMN_MIN_WIDTH,
|
||||
DEFAULT_UNIFIED_TABLE_DATE_COLUMN_MIN_WIDTH,
|
||||
} from '../constants';
|
||||
|
||||
export const defaultColumnHeaderType: ColumnHeaderType = 'not-filtered';
|
||||
|
||||
export const defaultHeaders: ColumnHeaderOptions[] = [
|
||||
export const defaultUdtHeaders: ColumnHeaderOptions[] = [
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: '@timestamp',
|
||||
initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH,
|
||||
initialWidth: DEFAULT_UNIFIED_TABLE_DATE_COLUMN_MIN_WIDTH,
|
||||
esTypes: ['date'],
|
||||
type: 'date',
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'message',
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH * 2,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'event.category',
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'event.action',
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'host.name',
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'source.ip',
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'destination.ip',
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'user.name',
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Filter renders correctly against snapshot 1`] = `
|
||||
<TextFilter
|
||||
columnId="@timestamp"
|
||||
minWidth={190}
|
||||
onFilterChange={[Function]}
|
||||
/>
|
||||
`;
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { defaultHeaders } from '../default_headers';
|
||||
|
||||
import { Filter } from '.';
|
||||
import type { ColumnHeaderType } from '../../../../../../../common/types';
|
||||
|
||||
const textFilter: ColumnHeaderType = 'text-filter';
|
||||
const notFiltered: ColumnHeaderType = 'not-filtered';
|
||||
|
||||
describe('Filter', () => {
|
||||
test('renders correctly against snapshot', () => {
|
||||
const textFilterColumnHeader = {
|
||||
...defaultHeaders[0],
|
||||
columnHeaderType: textFilter,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<Filter header={textFilterColumnHeader} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
test('it renders a text filter when the columnHeaderType is "text-filter"', () => {
|
||||
const textFilterColumnHeader = {
|
||||
...defaultHeaders[0],
|
||||
columnHeaderType: textFilter,
|
||||
};
|
||||
|
||||
const wrapper = mount(<Filter header={textFilterColumnHeader} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="textFilter"]').first().props()).toHaveProperty(
|
||||
'placeholder'
|
||||
);
|
||||
});
|
||||
|
||||
test('it does NOT render a filter when the columnHeaderType is "not-filtered"', () => {
|
||||
const notFilteredHeader = {
|
||||
...defaultHeaders[0],
|
||||
columnHeaderType: notFiltered,
|
||||
};
|
||||
|
||||
const wrapper = mount(<Filter header={notFilteredHeader} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="textFilter"]').exists()).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { noop } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
|
||||
import type { ColumnHeaderOptions } from '../../../../../../../common/types';
|
||||
import { DEFAULT_COLUMN_MIN_WIDTH } from '../../constants';
|
||||
import type { OnFilterChange } from '../../../events';
|
||||
import { TextFilter } from '../text_filter';
|
||||
|
||||
interface Props {
|
||||
header: ColumnHeaderOptions;
|
||||
onFilterChange?: OnFilterChange;
|
||||
}
|
||||
|
||||
/** Renders a header's filter, based on the `columnHeaderType` */
|
||||
export const Filter = React.memo<Props>(({ header, onFilterChange = noop }) => {
|
||||
switch (header.columnHeaderType) {
|
||||
case 'text-filter':
|
||||
return (
|
||||
<TextFilter
|
||||
columnId={header.id}
|
||||
minWidth={header.initialWidth ?? DEFAULT_COLUMN_MIN_WIDTH}
|
||||
onFilterChange={onFilterChange}
|
||||
placeholder={header.placeholder}
|
||||
/>
|
||||
);
|
||||
case 'not-filtered': // fall through
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
Filter.displayName = 'Filter';
|
|
@ -1,77 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Header renders correctly against snapshot 1`] = `
|
||||
<Fragment>
|
||||
<Memo(HeaderContentComponent)
|
||||
header={
|
||||
Object {
|
||||
"columnHeaderType": "not-filtered",
|
||||
"esTypes": Array [
|
||||
"date",
|
||||
],
|
||||
"id": "@timestamp",
|
||||
"initialWidth": 190,
|
||||
"type": "date",
|
||||
}
|
||||
}
|
||||
isLoading={false}
|
||||
isResizing={false}
|
||||
onClick={[Function]}
|
||||
showSortingCapability={true}
|
||||
sort={
|
||||
Array [
|
||||
Object {
|
||||
"columnId": "@timestamp",
|
||||
"columnType": "date",
|
||||
"esTypes": Array [
|
||||
"date",
|
||||
],
|
||||
"sortDirection": "desc",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<Actions
|
||||
header={
|
||||
Object {
|
||||
"columnHeaderType": "not-filtered",
|
||||
"esTypes": Array [
|
||||
"date",
|
||||
],
|
||||
"id": "@timestamp",
|
||||
"initialWidth": 190,
|
||||
"type": "date",
|
||||
}
|
||||
}
|
||||
isLoading={false}
|
||||
onColumnRemoved={[Function]}
|
||||
sort={
|
||||
Array [
|
||||
Object {
|
||||
"columnId": "@timestamp",
|
||||
"columnType": "date",
|
||||
"esTypes": Array [
|
||||
"date",
|
||||
],
|
||||
"sortDirection": "desc",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</Memo(HeaderContentComponent)>
|
||||
<Filter
|
||||
header={
|
||||
Object {
|
||||
"columnHeaderType": "not-filtered",
|
||||
"esTypes": Array [
|
||||
"date",
|
||||
],
|
||||
"id": "@timestamp",
|
||||
"initialWidth": 190,
|
||||
"type": "date",
|
||||
}
|
||||
}
|
||||
onFilterChange={[Function]}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
|
@ -1,84 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiToolTip } from '@elastic/eui';
|
||||
import { noop } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import type { ColumnHeaderOptions } from '../../../../../../../common/types/timeline';
|
||||
|
||||
import { TruncatableText } from '../../../../../../common/components/truncatable_text';
|
||||
import { EventsHeading, EventsHeadingTitleButton, EventsHeadingTitleSpan } from '../../../styles';
|
||||
import type { Sort } from '../../sort';
|
||||
import { SortIndicator } from '../../sort/sort_indicator';
|
||||
import { HeaderToolTipContent } from '../header_tooltip_content';
|
||||
import { getSortDirection, getSortIndex } from './helpers';
|
||||
interface HeaderContentProps {
|
||||
children: React.ReactNode;
|
||||
header: ColumnHeaderOptions;
|
||||
isLoading: boolean;
|
||||
isResizing: boolean;
|
||||
onClick: () => void;
|
||||
showSortingCapability: boolean;
|
||||
sort: Sort[];
|
||||
}
|
||||
|
||||
const HeaderContentComponent: React.FC<HeaderContentProps> = ({
|
||||
children,
|
||||
header,
|
||||
isLoading,
|
||||
isResizing,
|
||||
onClick,
|
||||
showSortingCapability,
|
||||
sort,
|
||||
}) => (
|
||||
<EventsHeading data-test-subj={`header-${header.id}`} isLoading={isLoading}>
|
||||
{header.aggregatable && showSortingCapability ? (
|
||||
<EventsHeadingTitleButton
|
||||
data-test-subj="header-sort-button"
|
||||
onClick={!isResizing && !isLoading ? onClick : noop}
|
||||
>
|
||||
<TruncatableText data-test-subj={`header-text-${header.id}`}>
|
||||
<EuiToolTip
|
||||
data-test-subj="header-tooltip"
|
||||
content={<HeaderToolTipContent header={header} />}
|
||||
>
|
||||
<>
|
||||
{React.isValidElement(header.display)
|
||||
? header.display
|
||||
: header.displayAsText ?? header.id}
|
||||
</>
|
||||
</EuiToolTip>
|
||||
</TruncatableText>
|
||||
|
||||
<SortIndicator
|
||||
data-test-subj="header-sort-indicator"
|
||||
sortDirection={getSortDirection({ header, sort })}
|
||||
sortNumber={getSortIndex({ header, sort })}
|
||||
/>
|
||||
</EventsHeadingTitleButton>
|
||||
) : (
|
||||
<EventsHeadingTitleSpan>
|
||||
<TruncatableText data-test-subj={`header-text-${header.id}`}>
|
||||
<EuiToolTip
|
||||
data-test-subj="header-tooltip"
|
||||
content={<HeaderToolTipContent header={header} />}
|
||||
>
|
||||
<>
|
||||
{React.isValidElement(header.display)
|
||||
? header.display
|
||||
: header.displayAsText ?? header.id}
|
||||
</>
|
||||
</EuiToolTip>
|
||||
</TruncatableText>
|
||||
</EventsHeadingTitleSpan>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</EventsHeading>
|
||||
);
|
||||
|
||||
export const HeaderContent = React.memo(HeaderContentComponent);
|
|
@ -1,56 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Direction } from '../../../../../../../common/search_strategy';
|
||||
import type {
|
||||
ColumnHeaderOptions,
|
||||
SortDirection,
|
||||
} from '../../../../../../../common/types/timeline';
|
||||
import type { Sort } from '../../sort';
|
||||
|
||||
interface GetNewSortDirectionOnClickParams {
|
||||
clickedHeader: ColumnHeaderOptions;
|
||||
currentSort: Sort[];
|
||||
}
|
||||
|
||||
/** Given a `header`, returns the `SortDirection` applicable to it */
|
||||
export const getNewSortDirectionOnClick = ({
|
||||
clickedHeader,
|
||||
currentSort,
|
||||
}: GetNewSortDirectionOnClickParams): Direction =>
|
||||
currentSort.reduce<Direction>(
|
||||
(acc, item) => (clickedHeader.id === item.columnId ? getNextSortDirection(item) : acc),
|
||||
Direction.desc
|
||||
);
|
||||
|
||||
/** Given a current sort direction, it returns the next sort direction */
|
||||
export const getNextSortDirection = (currentSort: Sort): Direction => {
|
||||
switch (currentSort.sortDirection) {
|
||||
case Direction.desc:
|
||||
return Direction.asc;
|
||||
case Direction.asc:
|
||||
return Direction.desc;
|
||||
case 'none':
|
||||
return Direction.desc;
|
||||
default:
|
||||
return Direction.desc;
|
||||
}
|
||||
};
|
||||
|
||||
interface GetSortDirectionParams {
|
||||
header: ColumnHeaderOptions;
|
||||
sort: Sort[];
|
||||
}
|
||||
|
||||
export const getSortDirection = ({ header, sort }: GetSortDirectionParams): SortDirection =>
|
||||
sort.reduce<SortDirection>(
|
||||
(acc, item) => (header.id === item.columnId ? item.sortDirection : acc),
|
||||
'none'
|
||||
);
|
||||
|
||||
export const getSortIndex = ({ header, sort }: GetSortDirectionParams): number =>
|
||||
sort.findIndex((s) => s.columnId === header.id);
|
|
@ -1,364 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import React, { type ReactNode } from 'react';
|
||||
|
||||
import { timelineActions } from '../../../../../store';
|
||||
import { TestProviders } from '../../../../../../common/mock';
|
||||
import type { Sort } from '../../sort';
|
||||
import { CloseButton } from '../actions';
|
||||
import { defaultHeaders } from '../default_headers';
|
||||
|
||||
import { HeaderComponent } from '.';
|
||||
import { getNewSortDirectionOnClick, getNextSortDirection, getSortDirection } from './helpers';
|
||||
import { Direction } from '../../../../../../../common/search_strategy';
|
||||
import { useDeepEqualSelector } from '../../../../../../common/hooks/use_selector';
|
||||
import type { ColumnHeaderType } from '../../../../../../../common/types';
|
||||
import { TimelineId } from '../../../../../../../common/types/timeline';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => {
|
||||
const original = jest.requireActual('react-redux');
|
||||
|
||||
return {
|
||||
...original,
|
||||
useSelector: jest.fn(),
|
||||
useDispatch: () => mockDispatch,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../../../../common/hooks/use_selector', () => ({
|
||||
useShallowEqualSelector: jest.fn(),
|
||||
useDeepEqualSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
const filteredColumnHeader: ColumnHeaderType = 'text-filter';
|
||||
|
||||
describe('Header', () => {
|
||||
const columnHeader = defaultHeaders[0];
|
||||
const sort: Sort[] = [
|
||||
{
|
||||
columnId: columnHeader.id,
|
||||
columnType: columnHeader.type ?? '',
|
||||
esTypes: columnHeader.esTypes ?? [],
|
||||
sortDirection: Direction.desc,
|
||||
},
|
||||
];
|
||||
const timelineId = TimelineId.test;
|
||||
|
||||
beforeEach(() => {
|
||||
(useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: false });
|
||||
});
|
||||
|
||||
test('renders correctly against snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={columnHeader} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.find('HeaderComponent').dive()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
test('it renders the header text', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={columnHeader} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="header-text-${columnHeader.id}"]`).first().text()
|
||||
).toEqual(columnHeader.id);
|
||||
});
|
||||
|
||||
test('it renders the header text alias when displayAsText is provided', () => {
|
||||
const displayAsText = 'Timestamp';
|
||||
const headerWithLabel = { ...columnHeader, displayAsText };
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={headerWithLabel} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="header-text-${columnHeader.id}"]`).first().text()
|
||||
).toEqual(displayAsText);
|
||||
});
|
||||
|
||||
test('it renders the header as a `ReactNode` when `display` is provided', () => {
|
||||
const display: React.ReactNode = (
|
||||
<div data-test-subj="rendered-via-display">
|
||||
{'The display property renders the column heading as a ReactNode'}
|
||||
</div>
|
||||
);
|
||||
const headerWithLabel = { ...columnHeader, display };
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={headerWithLabel} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find(`[data-test-subj="rendered-via-display"]`).exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('it prefers to render `display` instead of `displayAsText` when both are provided', () => {
|
||||
const displayAsText = 'this text should NOT be rendered';
|
||||
const display: React.ReactNode = (
|
||||
<div data-test-subj="rendered-via-display">{'this text is rendered via display'}</div>
|
||||
);
|
||||
const headerWithLabel = { ...columnHeader, display, displayAsText };
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={headerWithLabel} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.text()).toBe('this text is rendered via display');
|
||||
});
|
||||
|
||||
test('it falls back to rendering header.id when `display` is not a valid React node', () => {
|
||||
const display = {} as unknown as ReactNode; // a plain object is NOT a `ReactNode`
|
||||
const headerWithLabel = { ...columnHeader, display };
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={headerWithLabel} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="header-text-${columnHeader.id}"]`).first().text()
|
||||
).toEqual(columnHeader.id);
|
||||
});
|
||||
|
||||
test('it renders a sort indicator', () => {
|
||||
const headerSortable = { ...columnHeader, aggregatable: true };
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={headerSortable} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="header-sort-indicator"]').first().exists()).toEqual(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders a filter', () => {
|
||||
const columnWithFilter = {
|
||||
...columnHeader,
|
||||
columnHeaderType: filteredColumnHeader,
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={columnWithFilter} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="textFilter"]').first().props()).toHaveProperty(
|
||||
'placeholder'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onColumnSorted', () => {
|
||||
test('it invokes the onColumnSorted callback when the header sort button is clicked', () => {
|
||||
const headerSortable = { ...columnHeader, aggregatable: true };
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={headerSortable} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="header-sort-button"]').first().simulate('click');
|
||||
|
||||
expect(mockDispatch).toBeCalledWith(
|
||||
timelineActions.updateSort({
|
||||
id: timelineId,
|
||||
sort: [
|
||||
{
|
||||
columnId: columnHeader.id,
|
||||
columnType: columnHeader.type ?? 'number',
|
||||
esTypes: columnHeader.esTypes ?? [],
|
||||
sortDirection: Direction.asc, // (because the previous state was Direction.desc)
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('it does NOT render the header sort button when aggregatable is false', () => {
|
||||
const headerSortable = { ...columnHeader, aggregatable: false };
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={headerSortable} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="header-sort-button"]').length).toEqual(0);
|
||||
});
|
||||
|
||||
test('it does NOT render the header sort button when aggregatable is missing', () => {
|
||||
const headerSortable = { ...columnHeader };
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={headerSortable} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="header-sort-button"]').length).toEqual(0);
|
||||
});
|
||||
|
||||
test('it does NOT invoke the onColumnSorted callback when the header is clicked and aggregatable is undefined', () => {
|
||||
const mockOnColumnSorted = jest.fn();
|
||||
const headerSortable = { ...columnHeader, aggregatable: undefined };
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={headerSortable} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper.find(`[data-test-subj="header-${columnHeader.id}"]`).first().simulate('click');
|
||||
|
||||
expect(mockOnColumnSorted).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CloseButton', () => {
|
||||
test('it invokes the onColumnRemoved callback with the column ID when the close button is clicked', () => {
|
||||
const mockOnColumnRemoved = jest.fn();
|
||||
|
||||
const wrapper = mount(
|
||||
<CloseButton columnId={columnHeader.id} onColumnRemoved={mockOnColumnRemoved} />
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="remove-column"]').first().simulate('click');
|
||||
|
||||
expect(mockOnColumnRemoved).toBeCalledWith(columnHeader.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSortDirection', () => {
|
||||
test('it returns the sort direction when the header id matches the sort column id', () => {
|
||||
expect(getSortDirection({ header: columnHeader, sort })).toEqual(sort[0].sortDirection);
|
||||
});
|
||||
|
||||
test('it returns "none" when sort direction when the header id does NOT match the sort column id', () => {
|
||||
const nonMatching: Sort[] = [
|
||||
{
|
||||
columnId: 'differentSocks',
|
||||
columnType: columnHeader.type ?? 'number',
|
||||
esTypes: columnHeader.esTypes ?? [],
|
||||
sortDirection: Direction.desc,
|
||||
},
|
||||
];
|
||||
|
||||
expect(getSortDirection({ header: columnHeader, sort: nonMatching })).toEqual('none');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNextSortDirection', () => {
|
||||
test('it returns "asc" when the current direction is "desc"', () => {
|
||||
const sortDescending: Sort = {
|
||||
columnId: columnHeader.id,
|
||||
columnType: columnHeader.type ?? 'number',
|
||||
esTypes: columnHeader.esTypes ?? [],
|
||||
sortDirection: Direction.desc,
|
||||
};
|
||||
|
||||
expect(getNextSortDirection(sortDescending)).toEqual('asc');
|
||||
});
|
||||
|
||||
test('it returns "desc" when the current direction is "asc"', () => {
|
||||
const sortAscending: Sort = {
|
||||
columnId: columnHeader.id,
|
||||
columnType: columnHeader.type ?? 'number',
|
||||
esTypes: columnHeader.esTypes ?? [],
|
||||
sortDirection: Direction.asc,
|
||||
};
|
||||
|
||||
expect(getNextSortDirection(sortAscending)).toEqual(Direction.desc);
|
||||
});
|
||||
|
||||
test('it returns "desc" by default', () => {
|
||||
const sortNone: Sort = {
|
||||
columnId: columnHeader.id,
|
||||
columnType: columnHeader.type ?? 'number',
|
||||
esTypes: columnHeader.esTypes ?? [],
|
||||
sortDirection: 'none',
|
||||
};
|
||||
|
||||
expect(getNextSortDirection(sortNone)).toEqual(Direction.desc);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNewSortDirectionOnClick', () => {
|
||||
test('it returns the expected new sort direction when the header id matches the sort column id', () => {
|
||||
const sortMatches: Sort[] = [
|
||||
{
|
||||
columnId: columnHeader.id,
|
||||
columnType: columnHeader.type ?? 'number',
|
||||
esTypes: columnHeader.esTypes ?? [],
|
||||
sortDirection: Direction.desc,
|
||||
},
|
||||
];
|
||||
|
||||
expect(
|
||||
getNewSortDirectionOnClick({
|
||||
clickedHeader: columnHeader,
|
||||
currentSort: sortMatches,
|
||||
})
|
||||
).toEqual(Direction.asc);
|
||||
});
|
||||
|
||||
test('it returns the expected new sort direction when the header id does NOT match the sort column id', () => {
|
||||
const sortDoesNotMatch: Sort[] = [
|
||||
{
|
||||
columnId: 'someOtherColumn',
|
||||
columnType: columnHeader.type ?? 'number',
|
||||
esTypes: columnHeader.esTypes ?? [],
|
||||
sortDirection: 'none',
|
||||
},
|
||||
];
|
||||
|
||||
expect(
|
||||
getNewSortDirectionOnClick({
|
||||
clickedHeader: columnHeader,
|
||||
currentSort: sortDoesNotMatch,
|
||||
})
|
||||
).toEqual(Direction.desc);
|
||||
});
|
||||
});
|
||||
|
||||
describe('text truncation styling', () => {
|
||||
test('truncates the header text with an ellipsis', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={columnHeader} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="header-text-${columnHeader.id}"]`).at(1)
|
||||
).toHaveStyleRule('text-overflow', 'ellipsis');
|
||||
});
|
||||
});
|
||||
|
||||
describe('header tooltip', () => {
|
||||
test('it has a tooltip to display the properties of the field', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderComponent header={columnHeader} sort={sort} timelineId={timelineId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="header-tooltip"]').exists()).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,118 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { noop } from 'lodash/fp';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { isDataViewFieldSubtypeNested } from '@kbn/es-query';
|
||||
|
||||
import type { ColumnHeaderOptions } from '../../../../../../../common/types';
|
||||
import {
|
||||
useDeepEqualSelector,
|
||||
useShallowEqualSelector,
|
||||
} from '../../../../../../common/hooks/use_selector';
|
||||
import { timelineActions, timelineSelectors } from '../../../../../store';
|
||||
import type { OnColumnRemoved, OnFilterChange } from '../../../events';
|
||||
import type { Sort } from '../../sort';
|
||||
import { Actions } from '../actions';
|
||||
import { Filter } from '../filter';
|
||||
import { getNewSortDirectionOnClick } from './helpers';
|
||||
import { HeaderContent } from './header_content';
|
||||
import { isEqlOnSelector } from './selectors';
|
||||
|
||||
interface Props {
|
||||
header: ColumnHeaderOptions;
|
||||
onFilterChange?: OnFilterChange;
|
||||
sort: Sort[];
|
||||
timelineId: string;
|
||||
}
|
||||
|
||||
export const HeaderComponent: React.FC<Props> = ({
|
||||
header,
|
||||
onFilterChange = noop,
|
||||
sort,
|
||||
timelineId,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const getIsEqlOn = useMemo(() => isEqlOnSelector(), []);
|
||||
const isEqlOn = useShallowEqualSelector((state) => getIsEqlOn(state, timelineId));
|
||||
|
||||
const onColumnSort = useCallback(() => {
|
||||
const columnId = header.id;
|
||||
const columnType = header.type ?? '';
|
||||
const esTypes = header.esTypes ?? [];
|
||||
const sortDirection = getNewSortDirectionOnClick({
|
||||
clickedHeader: header,
|
||||
currentSort: sort,
|
||||
});
|
||||
const headerIndex = sort.findIndex((col) => col.columnId === columnId);
|
||||
let newSort = [];
|
||||
if (headerIndex === -1) {
|
||||
newSort = [
|
||||
...sort,
|
||||
{
|
||||
columnId,
|
||||
columnType,
|
||||
esTypes,
|
||||
sortDirection,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
newSort = [
|
||||
...sort.slice(0, headerIndex),
|
||||
{
|
||||
columnId,
|
||||
columnType,
|
||||
esTypes,
|
||||
sortDirection,
|
||||
},
|
||||
...sort.slice(headerIndex + 1),
|
||||
];
|
||||
}
|
||||
dispatch(
|
||||
timelineActions.updateSort({
|
||||
id: timelineId,
|
||||
sort: newSort,
|
||||
})
|
||||
);
|
||||
}, [dispatch, header, sort, timelineId]);
|
||||
|
||||
const onColumnRemoved = useCallback<OnColumnRemoved>(
|
||||
(columnId) => dispatch(timelineActions.removeColumn({ id: timelineId, columnId })),
|
||||
[dispatch, timelineId]
|
||||
);
|
||||
|
||||
const getManageTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const { isLoading } = useDeepEqualSelector(
|
||||
(state) => getManageTimeline(state, timelineId) || { isLoading: false }
|
||||
);
|
||||
const showSortingCapability = !isEqlOn && !isDataViewFieldSubtypeNested(header);
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderContent
|
||||
header={header}
|
||||
isLoading={isLoading}
|
||||
isResizing={false}
|
||||
onClick={onColumnSort}
|
||||
showSortingCapability={showSortingCapability}
|
||||
sort={sort}
|
||||
>
|
||||
<Actions
|
||||
header={header}
|
||||
isLoading={isLoading}
|
||||
onColumnRemoved={onColumnRemoved}
|
||||
sort={sort}
|
||||
/>
|
||||
</HeaderContent>
|
||||
|
||||
<Filter header={header} onFilterChange={onFilterChange} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Header = React.memo(HeaderComponent);
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createSelector } from 'reselect';
|
||||
import { TimelineTabs } from '../../../../../../../common/types/timeline';
|
||||
import { selectTimeline } from '../../../../../store/selectors';
|
||||
|
||||
export const isEqlOnSelector = () =>
|
||||
createSelector(selectTimeline, (timeline) => timeline?.activeTab === TimelineTabs.eql);
|
|
@ -1,67 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`HeaderToolTipContent it renders the expected table content 1`] = `
|
||||
<Fragment>
|
||||
<P>
|
||||
<ToolTipTableMetadata
|
||||
data-test-subj="category"
|
||||
>
|
||||
Category
|
||||
:
|
||||
</ToolTipTableMetadata>
|
||||
<ToolTipTableValue
|
||||
data-test-subj="category-value"
|
||||
>
|
||||
base
|
||||
</ToolTipTableValue>
|
||||
</P>
|
||||
<P>
|
||||
<ToolTipTableMetadata
|
||||
data-test-subj="field"
|
||||
>
|
||||
Field
|
||||
:
|
||||
</ToolTipTableMetadata>
|
||||
<ToolTipTableValue
|
||||
data-test-subj="field-value"
|
||||
>
|
||||
@timestamp
|
||||
</ToolTipTableValue>
|
||||
</P>
|
||||
<P>
|
||||
<ToolTipTableMetadata
|
||||
data-test-subj="type"
|
||||
>
|
||||
Type
|
||||
:
|
||||
</ToolTipTableMetadata>
|
||||
<ToolTipTableValue>
|
||||
<IconType
|
||||
data-test-subj="type-icon"
|
||||
type="clock"
|
||||
/>
|
||||
<EuiBadge
|
||||
data-test-subj="type-value-date"
|
||||
key="date"
|
||||
>
|
||||
date
|
||||
</EuiBadge>
|
||||
</ToolTipTableValue>
|
||||
</P>
|
||||
<P>
|
||||
<ToolTipTableMetadata
|
||||
data-test-subj="description"
|
||||
>
|
||||
Description
|
||||
:
|
||||
</ToolTipTableMetadata>
|
||||
<ToolTipTableValue
|
||||
data-test-subj="description-value"
|
||||
>
|
||||
Date/time when the event originated.
|
||||
For log events this is the date/time when the event was generated, and not when it was read.
|
||||
Required field for all events.
|
||||
</ToolTipTableValue>
|
||||
</P>
|
||||
</Fragment>
|
||||
`;
|
|
@ -1,89 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
|
||||
import type { ColumnHeaderOptions } from '../../../../../../../common/types';
|
||||
import { defaultHeaders } from '../../../../../../common/mock';
|
||||
import { HeaderToolTipContent } from '.';
|
||||
|
||||
describe('HeaderToolTipContent', () => {
|
||||
let header: ColumnHeaderOptions;
|
||||
beforeEach(() => {
|
||||
header = cloneDeep(defaultHeaders[0]);
|
||||
});
|
||||
|
||||
test('it renders the category', () => {
|
||||
const wrapper = mount(<HeaderToolTipContent header={header} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="category-value"]').first().text()).toEqual(
|
||||
header.category
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders the name of the field', () => {
|
||||
const wrapper = mount(<HeaderToolTipContent header={header} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="field-value"]').first().text()).toEqual(header.id);
|
||||
});
|
||||
|
||||
test('it renders the expected icon for the header type', () => {
|
||||
const wrapper = mount(<HeaderToolTipContent header={header} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="type-icon"]').first().props().type).toEqual('clock');
|
||||
});
|
||||
|
||||
test('it renders the type of the field', () => {
|
||||
const wrapper = mount(<HeaderToolTipContent header={header} />);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find(`[data-test-subj="type-value-${header.esTypes?.at(0)}"]`)
|
||||
.first()
|
||||
.text()
|
||||
).toEqual(header.esTypes?.at(0));
|
||||
});
|
||||
|
||||
test('it renders multiple `esTypes`', () => {
|
||||
const hasMultipleTypes = { ...header, esTypes: ['long', 'date'] };
|
||||
|
||||
const wrapper = mount(<HeaderToolTipContent header={hasMultipleTypes} />);
|
||||
|
||||
hasMultipleTypes.esTypes.forEach((esType) => {
|
||||
expect(wrapper.find(`[data-test-subj="type-value-${esType}"]`).first().text()).toEqual(
|
||||
esType
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('it renders the description of the field', () => {
|
||||
const wrapper = mount(<HeaderToolTipContent header={header} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="description-value"]').first().text()).toEqual(
|
||||
header.description
|
||||
);
|
||||
});
|
||||
|
||||
test('it does NOT render the description column when the field does NOT contain a description', () => {
|
||||
const noDescription = {
|
||||
...header,
|
||||
description: '',
|
||||
};
|
||||
|
||||
const wrapper = mount(<HeaderToolTipContent header={noDescription} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="description"]').exists()).toEqual(false);
|
||||
});
|
||||
|
||||
test('it renders the expected table content', () => {
|
||||
const wrapper = shallow(<HeaderToolTipContent header={header} />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -1,86 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiIcon, EuiBadge } from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type { ColumnHeaderOptions } from '../../../../../../../common/types';
|
||||
import { getIconFromType } from '../../../../../../common/components/event_details/helpers';
|
||||
import * as i18n from '../translations';
|
||||
|
||||
const IconType = styled(EuiIcon)`
|
||||
margin-right: 3px;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
`;
|
||||
IconType.displayName = 'IconType';
|
||||
|
||||
const P = styled.span`
|
||||
margin-bottom: 5px;
|
||||
`;
|
||||
P.displayName = 'P';
|
||||
|
||||
const ToolTipTableMetadata = styled.span`
|
||||
margin-right: 5px;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
`;
|
||||
ToolTipTableMetadata.displayName = 'ToolTipTableMetadata';
|
||||
|
||||
const ToolTipTableValue = styled.span`
|
||||
word-wrap: break-word;
|
||||
`;
|
||||
ToolTipTableValue.displayName = 'ToolTipTableValue';
|
||||
|
||||
export const HeaderToolTipContent = React.memo<{ header: ColumnHeaderOptions }>(({ header }) => (
|
||||
<>
|
||||
{!isEmpty(header.category) && (
|
||||
<P>
|
||||
<ToolTipTableMetadata data-test-subj="category">
|
||||
{i18n.CATEGORY}
|
||||
{':'}
|
||||
</ToolTipTableMetadata>
|
||||
<ToolTipTableValue data-test-subj="category-value">{header.category}</ToolTipTableValue>
|
||||
</P>
|
||||
)}
|
||||
<P>
|
||||
<ToolTipTableMetadata data-test-subj="field">
|
||||
{i18n.FIELD}
|
||||
{':'}
|
||||
</ToolTipTableMetadata>
|
||||
<ToolTipTableValue data-test-subj="field-value">{header.id}</ToolTipTableValue>
|
||||
</P>
|
||||
<P>
|
||||
<ToolTipTableMetadata data-test-subj="type">
|
||||
{i18n.TYPE}
|
||||
{':'}
|
||||
</ToolTipTableMetadata>
|
||||
<ToolTipTableValue>
|
||||
<IconType data-test-subj="type-icon" type={getIconFromType(header.type)} />
|
||||
{header.esTypes?.map((esType) => (
|
||||
<EuiBadge data-test-subj={`type-value-${esType}`} key={esType}>
|
||||
{esType}
|
||||
</EuiBadge>
|
||||
))}
|
||||
</ToolTipTableValue>
|
||||
</P>
|
||||
{!isEmpty(header.description) && (
|
||||
<P>
|
||||
<ToolTipTableMetadata data-test-subj="description">
|
||||
{i18n.DESCRIPTION}
|
||||
{':'}
|
||||
</ToolTipTableMetadata>
|
||||
<ToolTipTableValue data-test-subj="description-value">
|
||||
{header.description}
|
||||
</ToolTipTableValue>
|
||||
</P>
|
||||
)}
|
||||
</>
|
||||
));
|
||||
HeaderToolTipContent.displayName = 'HeaderToolTipContent';
|
|
@ -9,8 +9,7 @@ import { mockBrowserFields } from '../../../../../common/containers/source/mock'
|
|||
import type { BrowserFields } from '../../../../../../common/search_strategy';
|
||||
import type { ColumnHeaderOptions } from '../../../../../../common/types';
|
||||
import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants';
|
||||
import { defaultHeaders } from './default_headers';
|
||||
import { defaultUdtHeaders } from '../../unified_components/default_headers';
|
||||
import { defaultUdtHeaders } from './default_headers';
|
||||
import {
|
||||
getColumnWidthFromType,
|
||||
getColumnHeaders,
|
||||
|
@ -88,7 +87,7 @@ describe('helpers', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('should return the expected metadata in case of unified header', () => {
|
||||
test('should return the expected metadata in case of default header', () => {
|
||||
const inputHeaders = defaultUdtHeaders;
|
||||
expect(getColumnHeader('@timestamp', inputHeaders)).toEqual({
|
||||
columnHeaderType: 'not-filtered',
|
||||
|
@ -112,7 +111,7 @@ describe('helpers', () => {
|
|||
searchable: true,
|
||||
type: 'date',
|
||||
esTypes: ['date'],
|
||||
initialWidth: 190,
|
||||
initialWidth: 215,
|
||||
},
|
||||
{
|
||||
aggregatable: true,
|
||||
|
@ -122,7 +121,6 @@ describe('helpers', () => {
|
|||
searchable: true,
|
||||
type: 'ip',
|
||||
esTypes: ['ip'],
|
||||
initialWidth: 180,
|
||||
},
|
||||
{
|
||||
aggregatable: true,
|
||||
|
@ -132,10 +130,9 @@ describe('helpers', () => {
|
|||
searchable: true,
|
||||
type: 'ip',
|
||||
esTypes: ['ip'],
|
||||
initialWidth: 180,
|
||||
},
|
||||
];
|
||||
const mockHeader = defaultHeaders.filter((h) =>
|
||||
const mockHeader = defaultUdtHeaders.filter((h) =>
|
||||
['@timestamp', 'source.ip', 'destination.ip'].includes(h.id)
|
||||
);
|
||||
expect(getColumnHeaders(mockHeader, mockBrowserFields)).toEqual(expectedData);
|
||||
|
|
|
@ -1,336 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { defaultHeaders } from './default_headers';
|
||||
import { mockBrowserFields } from '../../../../../common/containers/source/mock';
|
||||
import type { Sort } from '../sort';
|
||||
import { TestProviders } from '../../../../../common/mock/test_providers';
|
||||
import { useMountAppended } from '../../../../../common/utils/use_mount_appended';
|
||||
|
||||
import type { ColumnHeadersComponentProps } from '.';
|
||||
import { ColumnHeadersComponent } from '.';
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import { timelineActions } from '../../../../store';
|
||||
import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
|
||||
import { Direction } from '../../../../../../common/search_strategy';
|
||||
import { getDefaultControlColumn } from '../control_columns';
|
||||
import { testTrailingControlColumns } from '../../../../../common/mock/mock_timeline_control_columns';
|
||||
import type { UseFieldBrowserOptionsProps } from '../../../fields_browser';
|
||||
import { mockTriggersActionsUi } from '../../../../../common/mock/mock_triggers_actions_ui_plugin';
|
||||
import { mockTimelines } from '../../../../../common/mock/mock_timelines_plugin';
|
||||
import { HeaderActions } from '../../../../../common/components/header_actions/header_actions';
|
||||
import { getActionsColumnWidth } from '../../../../../common/components/header_actions';
|
||||
|
||||
jest.mock('../../../../../common/lib/kibana', () => ({
|
||||
useKibana: () => ({
|
||||
services: {
|
||||
timelines: mockTimelines,
|
||||
triggersActionsUi: mockTriggersActionsUi,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockUseFieldBrowserOptions = jest.fn();
|
||||
jest.mock('../../../fields_browser', () => ({
|
||||
useFieldBrowserOptions: (props: UseFieldBrowserOptionsProps) => mockUseFieldBrowserOptions(props),
|
||||
}));
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => {
|
||||
const original = jest.requireActual('react-redux');
|
||||
|
||||
return {
|
||||
...original,
|
||||
useDispatch: () => mockDispatch,
|
||||
};
|
||||
});
|
||||
const timelineId = TimelineId.test;
|
||||
|
||||
describe('ColumnHeaders', () => {
|
||||
const mount = useMountAppended();
|
||||
const ACTION_BUTTON_COUNT = 4;
|
||||
const actionsColumnWidth = getActionsColumnWidth(ACTION_BUTTON_COUNT);
|
||||
const leadingControlColumns = getDefaultControlColumn(ACTION_BUTTON_COUNT).map((x) => ({
|
||||
...x,
|
||||
headerCellRender: HeaderActions,
|
||||
}));
|
||||
const sort: Sort[] = [
|
||||
{
|
||||
columnId: '@timestamp',
|
||||
columnType: 'date',
|
||||
esTypes: ['date'],
|
||||
sortDirection: Direction.desc,
|
||||
},
|
||||
];
|
||||
const defaultProps: ColumnHeadersComponentProps = {
|
||||
actionsColumnWidth,
|
||||
browserFields: mockBrowserFields,
|
||||
columnHeaders: defaultHeaders,
|
||||
isSelectAllChecked: false,
|
||||
onSelectAll: jest.fn,
|
||||
show: true,
|
||||
showEventsSelect: false,
|
||||
showSelectAllCheckbox: false,
|
||||
sort,
|
||||
tabType: TimelineTabs.query,
|
||||
timelineId,
|
||||
leadingControlColumns,
|
||||
trailingControlColumns: [],
|
||||
};
|
||||
|
||||
describe('rendering', () => {
|
||||
test('renders correctly against snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<TestProviders>
|
||||
<ColumnHeadersComponent {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.find('ColumnHeadersComponent')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('it renders the field browser', () => {
|
||||
const mockCloseEditor = jest.fn();
|
||||
mockUseFieldBrowserOptions.mockImplementation(({ editorActionsRef }) => {
|
||||
editorActionsRef.current = { closeEditor: mockCloseEditor };
|
||||
return {};
|
||||
});
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<ColumnHeadersComponent {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="field-browser"]').first().exists()).toEqual(true);
|
||||
});
|
||||
|
||||
test('it renders every column header', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<ColumnHeadersComponent {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
defaultHeaders.forEach((h) => {
|
||||
expect(wrapper.find('[data-test-subj="headers-group"]').first().text()).toContain(h.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#onColumnsSorted', () => {
|
||||
let mockSort: Sort[] = [
|
||||
{
|
||||
columnId: '@timestamp',
|
||||
columnType: 'date',
|
||||
esTypes: ['date'],
|
||||
sortDirection: Direction.desc,
|
||||
},
|
||||
{
|
||||
columnId: 'host.name',
|
||||
columnType: 'string',
|
||||
esTypes: [],
|
||||
sortDirection: Direction.asc,
|
||||
},
|
||||
];
|
||||
let mockDefaultHeaders = cloneDeep(
|
||||
defaultHeaders.map((h) => (h.id === 'message' ? h : { ...h, aggregatable: true }))
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
mockDefaultHeaders = cloneDeep(
|
||||
defaultHeaders.map((h) => (h.id === 'message' ? h : { ...h, aggregatable: true }))
|
||||
);
|
||||
mockSort = [
|
||||
{
|
||||
columnId: '@timestamp',
|
||||
columnType: 'date',
|
||||
esTypes: ['date'],
|
||||
sortDirection: Direction.desc,
|
||||
},
|
||||
{
|
||||
columnId: 'host.name',
|
||||
columnType: 'string',
|
||||
esTypes: [],
|
||||
sortDirection: Direction.asc,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
test('Add column `event.category` as desc sorting', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<ColumnHeadersComponent
|
||||
{...{ ...defaultProps, columnHeaders: mockDefaultHeaders, sort: mockSort }}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find('[data-test-subj="header-event.category"] [data-test-subj="header-sort-button"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
timelineActions.updateSort({
|
||||
id: timelineId,
|
||||
sort: [
|
||||
{
|
||||
columnId: '@timestamp',
|
||||
columnType: 'date',
|
||||
esTypes: ['date'],
|
||||
sortDirection: Direction.desc,
|
||||
},
|
||||
{
|
||||
columnId: 'host.name',
|
||||
columnType: 'string',
|
||||
esTypes: [],
|
||||
sortDirection: Direction.asc,
|
||||
},
|
||||
{
|
||||
columnId: 'event.category',
|
||||
columnType: '',
|
||||
esTypes: [],
|
||||
sortDirection: Direction.desc,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('Change order of column `@timestamp` from desc to asc without changing index position', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<ColumnHeadersComponent
|
||||
{...{ ...defaultProps, columnHeaders: mockDefaultHeaders, sort: mockSort }}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find('[data-test-subj="header-@timestamp"] [data-test-subj="header-sort-button"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
timelineActions.updateSort({
|
||||
id: timelineId,
|
||||
sort: [
|
||||
{
|
||||
columnId: '@timestamp',
|
||||
columnType: 'date',
|
||||
esTypes: ['date'],
|
||||
sortDirection: Direction.asc,
|
||||
},
|
||||
{
|
||||
columnId: 'host.name',
|
||||
columnType: 'string',
|
||||
esTypes: [],
|
||||
sortDirection: Direction.asc,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('Change order of column `host.name` from asc to desc without changing index position', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<ColumnHeadersComponent
|
||||
{...{
|
||||
...defaultProps,
|
||||
columnHeaders: mockDefaultHeaders,
|
||||
sort: mockSort,
|
||||
}}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find('[data-test-subj="header-host.name"] [data-test-subj="header-sort-button"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
timelineActions.updateSort({
|
||||
id: timelineId,
|
||||
sort: [
|
||||
{
|
||||
columnId: '@timestamp',
|
||||
columnType: 'date',
|
||||
esTypes: ['date'],
|
||||
sortDirection: Direction.desc,
|
||||
},
|
||||
{
|
||||
columnId: 'host.name',
|
||||
columnType: '',
|
||||
esTypes: [],
|
||||
sortDirection: Direction.desc,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
test('Does not render the default leading action column header and renders a custom trailing header', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<ColumnHeadersComponent
|
||||
{...{
|
||||
...defaultProps,
|
||||
columnHeaders: mockDefaultHeaders,
|
||||
sort: mockSort,
|
||||
leadingControlColumns: [],
|
||||
trailingControlColumns: testTrailingControlColumns,
|
||||
}}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.exists('[data-test-subj="field-browser"]')).toBeFalsy();
|
||||
expect(wrapper.exists('[data-test-subj="test-header-action-cell"]')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Field Editor', () => {
|
||||
test('Closes field editor when the timeline is unmounted', () => {
|
||||
const mockCloseEditor = jest.fn();
|
||||
mockUseFieldBrowserOptions.mockImplementation(({ editorActionsRef }) => {
|
||||
editorActionsRef.current = { closeEditor: mockCloseEditor };
|
||||
return {};
|
||||
});
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<ColumnHeadersComponent {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(mockCloseEditor).not.toHaveBeenCalled();
|
||||
|
||||
wrapper.unmount();
|
||||
expect(mockCloseEditor).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Closes field editor when the timeline is closed', () => {
|
||||
const mockCloseEditor = jest.fn();
|
||||
mockUseFieldBrowserOptions.mockImplementation(({ editorActionsRef }) => {
|
||||
editorActionsRef.current = { closeEditor: mockCloseEditor };
|
||||
return {};
|
||||
});
|
||||
|
||||
const Proxy = (props: ColumnHeadersComponentProps) => (
|
||||
<TestProviders>
|
||||
<ColumnHeadersComponent {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
const wrapper = mount(<Proxy {...defaultProps} />);
|
||||
expect(mockCloseEditor).not.toHaveBeenCalled();
|
||||
|
||||
wrapper.setProps({ ...defaultProps, show: false });
|
||||
expect(mockCloseEditor).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,316 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import type { DraggableChildrenFn, DroppableProps } from '@hello-pangea/dnd';
|
||||
import { Droppable } from '@hello-pangea/dnd';
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
import type { ControlColumnProps, HeaderActionProps } from '../../../../../../common/types';
|
||||
import { removeColumn, upsertColumn } from '../../../../store/actions';
|
||||
import { DragEffects } from '../../../../../common/components/drag_and_drop/draggable_wrapper';
|
||||
import { DraggableFieldBadge } from '../../../../../common/components/draggables/field_badge';
|
||||
import type { BrowserFields } from '../../../../../common/containers/source';
|
||||
import {
|
||||
DRAG_TYPE_FIELD,
|
||||
droppableTimelineColumnsPrefix,
|
||||
} from '../../../../../common/components/drag_and_drop/helpers';
|
||||
import type { ColumnHeaderOptions, TimelineTabs } from '../../../../../../common/types/timeline';
|
||||
import type { OnSelectAll } from '../../events';
|
||||
import {
|
||||
EventsTh,
|
||||
EventsThead,
|
||||
EventsThGroupData,
|
||||
EventsTrHeader,
|
||||
EventsThGroupActions,
|
||||
} from '../../styles';
|
||||
import type { Sort } from '../sort';
|
||||
import { ColumnHeader } from './column_header';
|
||||
|
||||
import { SourcererScopeName } from '../../../../../sourcerer/store/model';
|
||||
import type { FieldEditorActions } from '../../../fields_browser';
|
||||
import { useFieldBrowserOptions } from '../../../fields_browser';
|
||||
|
||||
export interface ColumnHeadersComponentProps {
|
||||
actionsColumnWidth: number;
|
||||
browserFields: BrowserFields;
|
||||
columnHeaders: ColumnHeaderOptions[];
|
||||
isEventViewer?: boolean;
|
||||
isSelectAllChecked: boolean;
|
||||
onSelectAll: OnSelectAll;
|
||||
show: boolean;
|
||||
showEventsSelect: boolean;
|
||||
showSelectAllCheckbox: boolean;
|
||||
sort: Sort[];
|
||||
tabType: TimelineTabs;
|
||||
timelineId: string;
|
||||
leadingControlColumns: ControlColumnProps[];
|
||||
trailingControlColumns: ControlColumnProps[];
|
||||
}
|
||||
|
||||
interface DraggableContainerProps {
|
||||
children: React.ReactNode;
|
||||
onMount: () => void;
|
||||
onUnmount: () => void;
|
||||
}
|
||||
|
||||
export const DraggableContainer = React.memo<DraggableContainerProps>(
|
||||
({ children, onMount, onUnmount }) => {
|
||||
useEffect(() => {
|
||||
onMount();
|
||||
|
||||
return () => onUnmount();
|
||||
}, [onMount, onUnmount]);
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
);
|
||||
|
||||
DraggableContainer.displayName = 'DraggableContainer';
|
||||
|
||||
export const isFullScreen = ({
|
||||
globalFullScreen,
|
||||
isActiveTimelines,
|
||||
timelineFullScreen,
|
||||
}: {
|
||||
globalFullScreen: boolean;
|
||||
isActiveTimelines: boolean;
|
||||
timelineFullScreen: boolean;
|
||||
}) =>
|
||||
(isActiveTimelines && timelineFullScreen) || (isActiveTimelines === false && globalFullScreen);
|
||||
|
||||
/** Renders the timeline header columns */
|
||||
export const ColumnHeadersComponent = ({
|
||||
actionsColumnWidth,
|
||||
browserFields,
|
||||
columnHeaders,
|
||||
isEventViewer = false,
|
||||
isSelectAllChecked,
|
||||
onSelectAll,
|
||||
show,
|
||||
showEventsSelect,
|
||||
showSelectAllCheckbox,
|
||||
sort,
|
||||
tabType,
|
||||
timelineId,
|
||||
leadingControlColumns,
|
||||
trailingControlColumns,
|
||||
}: ColumnHeadersComponentProps) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [draggingIndex, setDraggingIndex] = useState<number | null>(null);
|
||||
const fieldEditorActionsRef = useRef<FieldEditorActions>(null);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (fieldEditorActionsRef.current) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
fieldEditorActionsRef.current.closeEditor();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!show && fieldEditorActionsRef.current) {
|
||||
fieldEditorActionsRef.current.closeEditor();
|
||||
}
|
||||
}, [show]);
|
||||
|
||||
const renderClone: DraggableChildrenFn = useCallback(
|
||||
(dragProvided, _dragSnapshot, rubric) => {
|
||||
const index = rubric.source.index;
|
||||
const header = columnHeaders[index];
|
||||
|
||||
const onMount = () => setDraggingIndex(index);
|
||||
const onUnmount = () => setDraggingIndex(null);
|
||||
|
||||
return (
|
||||
<EventsTh
|
||||
data-test-subj="draggable-header"
|
||||
{...dragProvided.draggableProps}
|
||||
{...dragProvided.dragHandleProps}
|
||||
ref={dragProvided.innerRef}
|
||||
>
|
||||
<DraggableContainer onMount={onMount} onUnmount={onUnmount}>
|
||||
<DragEffects>
|
||||
<DraggableFieldBadge fieldId={header.id} fieldWidth={header.initialWidth} />
|
||||
</DragEffects>
|
||||
</DraggableContainer>
|
||||
</EventsTh>
|
||||
);
|
||||
},
|
||||
[columnHeaders, setDraggingIndex]
|
||||
);
|
||||
|
||||
const ColumnHeaderList = useMemo(
|
||||
() =>
|
||||
columnHeaders.map((header, draggableIndex) => (
|
||||
<ColumnHeader
|
||||
key={header.id}
|
||||
draggableIndex={draggableIndex}
|
||||
timelineId={timelineId}
|
||||
header={header}
|
||||
isDragging={draggingIndex === draggableIndex}
|
||||
sort={sort}
|
||||
tabType={tabType}
|
||||
/>
|
||||
)),
|
||||
[columnHeaders, timelineId, draggingIndex, sort, tabType]
|
||||
);
|
||||
|
||||
const DroppableContent = useCallback<DroppableProps['children']>(
|
||||
(dropProvided, snapshot) => (
|
||||
<>
|
||||
<EventsThGroupData
|
||||
data-test-subj="headers-group"
|
||||
ref={dropProvided.innerRef}
|
||||
isDragging={snapshot.isDraggingOver}
|
||||
{...dropProvided.droppableProps}
|
||||
>
|
||||
{ColumnHeaderList}
|
||||
</EventsThGroupData>
|
||||
</>
|
||||
),
|
||||
[ColumnHeaderList]
|
||||
);
|
||||
|
||||
const leadingHeaderCells = useMemo(
|
||||
() =>
|
||||
leadingControlColumns ? leadingControlColumns.map((column) => column.headerCellRender) : [],
|
||||
[leadingControlColumns]
|
||||
);
|
||||
|
||||
const trailingHeaderCells = useMemo(
|
||||
() =>
|
||||
trailingControlColumns ? trailingControlColumns.map((column) => column.headerCellRender) : [],
|
||||
[trailingControlColumns]
|
||||
);
|
||||
|
||||
const fieldBrowserOptions = useFieldBrowserOptions({
|
||||
sourcererScope: SourcererScopeName.timeline,
|
||||
editorActionsRef: fieldEditorActionsRef,
|
||||
upsertColumn: (column, index) => dispatch(upsertColumn({ column, id: timelineId, index })),
|
||||
removeColumn: (columnId) => dispatch(removeColumn({ columnId, id: timelineId })),
|
||||
});
|
||||
|
||||
const LeadingHeaderActions = useMemo(() => {
|
||||
return leadingHeaderCells.map(
|
||||
(Header: React.ComponentType<HeaderActionProps> | React.ComponentType | undefined, index) => {
|
||||
const passedWidth = leadingControlColumns[index] && leadingControlColumns[index].width;
|
||||
const width = passedWidth ? passedWidth : actionsColumnWidth;
|
||||
return (
|
||||
<EventsThGroupActions
|
||||
actionsColumnWidth={width}
|
||||
data-test-subj="actions-container"
|
||||
isEventViewer={isEventViewer}
|
||||
key={index}
|
||||
>
|
||||
{Header && (
|
||||
<Header
|
||||
width={width}
|
||||
browserFields={browserFields}
|
||||
columnHeaders={columnHeaders}
|
||||
isEventViewer={isEventViewer}
|
||||
isSelectAllChecked={isSelectAllChecked}
|
||||
onSelectAll={onSelectAll}
|
||||
showEventsSelect={showEventsSelect}
|
||||
showSelectAllCheckbox={showSelectAllCheckbox}
|
||||
sort={sort}
|
||||
tabType={tabType}
|
||||
timelineId={timelineId}
|
||||
fieldBrowserOptions={fieldBrowserOptions}
|
||||
/>
|
||||
)}
|
||||
</EventsThGroupActions>
|
||||
);
|
||||
}
|
||||
);
|
||||
}, [
|
||||
leadingHeaderCells,
|
||||
leadingControlColumns,
|
||||
actionsColumnWidth,
|
||||
browserFields,
|
||||
columnHeaders,
|
||||
fieldBrowserOptions,
|
||||
isEventViewer,
|
||||
isSelectAllChecked,
|
||||
onSelectAll,
|
||||
showEventsSelect,
|
||||
showSelectAllCheckbox,
|
||||
sort,
|
||||
tabType,
|
||||
timelineId,
|
||||
]);
|
||||
|
||||
const TrailingHeaderActions = useMemo(() => {
|
||||
return trailingHeaderCells.map(
|
||||
(Header: React.ComponentType<HeaderActionProps> | React.ComponentType | undefined, index) => {
|
||||
const passedWidth = trailingControlColumns[index] && trailingControlColumns[index].width;
|
||||
const width = passedWidth ? passedWidth : actionsColumnWidth;
|
||||
return (
|
||||
<EventsThGroupActions
|
||||
actionsColumnWidth={width}
|
||||
data-test-subj="actions-container"
|
||||
isEventViewer={isEventViewer}
|
||||
key={index}
|
||||
>
|
||||
{Header && (
|
||||
<Header
|
||||
width={width}
|
||||
browserFields={browserFields}
|
||||
columnHeaders={columnHeaders}
|
||||
isEventViewer={isEventViewer}
|
||||
isSelectAllChecked={isSelectAllChecked}
|
||||
onSelectAll={onSelectAll}
|
||||
showEventsSelect={showEventsSelect}
|
||||
showSelectAllCheckbox={showSelectAllCheckbox}
|
||||
sort={sort}
|
||||
tabType={tabType}
|
||||
timelineId={timelineId}
|
||||
fieldBrowserOptions={fieldBrowserOptions}
|
||||
/>
|
||||
)}
|
||||
</EventsThGroupActions>
|
||||
);
|
||||
}
|
||||
);
|
||||
}, [
|
||||
trailingHeaderCells,
|
||||
trailingControlColumns,
|
||||
actionsColumnWidth,
|
||||
browserFields,
|
||||
columnHeaders,
|
||||
fieldBrowserOptions,
|
||||
isEventViewer,
|
||||
isSelectAllChecked,
|
||||
onSelectAll,
|
||||
showEventsSelect,
|
||||
showSelectAllCheckbox,
|
||||
sort,
|
||||
tabType,
|
||||
timelineId,
|
||||
]);
|
||||
return (
|
||||
<EventsThead data-test-subj="column-headers">
|
||||
<EventsTrHeader>
|
||||
{LeadingHeaderActions}
|
||||
<Droppable
|
||||
direction={'horizontal'}
|
||||
droppableId={`${droppableTimelineColumnsPrefix}-${tabType}.${timelineId}`}
|
||||
isDropDisabled={false}
|
||||
type={DRAG_TYPE_FIELD}
|
||||
renderClone={renderClone}
|
||||
>
|
||||
{DroppableContent}
|
||||
</Droppable>
|
||||
{TrailingHeaderActions}
|
||||
</EventsTrHeader>
|
||||
</EventsThead>
|
||||
);
|
||||
};
|
||||
|
||||
export const ColumnHeaders = React.memo(ColumnHeadersComponent);
|
|
@ -1,47 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { RangePicker } from '.';
|
||||
import { Ranges } from './ranges';
|
||||
|
||||
describe('RangePicker', () => {
|
||||
describe('rendering', () => {
|
||||
test('it renders the ranges', () => {
|
||||
const wrapper = mount(<RangePicker selected={'1 Week'} onRangeSelected={jest.fn()} />);
|
||||
|
||||
Ranges.forEach((range) => {
|
||||
expect(wrapper.text()).toContain(range);
|
||||
});
|
||||
});
|
||||
|
||||
test('it selects the option specified by the "selected" prop', () => {
|
||||
const selected = '1 Month';
|
||||
const wrapper = mount(<RangePicker selected={selected} onRangeSelected={jest.fn()} />);
|
||||
|
||||
expect(wrapper.find('select').props().value).toBe(selected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#onRangeSelected', () => {
|
||||
test('it invokes the onRangeSelected callback when a new range is selected', () => {
|
||||
const oldSelection = '1 Week';
|
||||
const newSelection = '1 Day';
|
||||
const mockOnRangeSelected = jest.fn();
|
||||
|
||||
const wrapper = mount(
|
||||
<RangePicker selected={oldSelection} onRangeSelected={mockOnRangeSelected} />
|
||||
);
|
||||
|
||||
wrapper.find('select').simulate('change', { target: { value: newSelection } });
|
||||
|
||||
expect(mockOnRangeSelected).toBeCalledWith(newSelection);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiSelect } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type { OnRangeSelected } from '../../../events';
|
||||
|
||||
import { Ranges } from './ranges';
|
||||
|
||||
interface Props {
|
||||
selected: string;
|
||||
onRangeSelected: OnRangeSelected;
|
||||
}
|
||||
|
||||
export const rangePickerWidth = 120;
|
||||
|
||||
// TODO: Upgrade Eui library and use EuiSuperSelect
|
||||
const SelectContainer = styled.div`
|
||||
cursor: pointer;
|
||||
width: ${rangePickerWidth}px;
|
||||
`;
|
||||
|
||||
SelectContainer.displayName = 'SelectContainer';
|
||||
|
||||
/** Renders a time range picker for the MiniMap (e.g. 1 Day, 1 Week...) */
|
||||
export const RangePicker = React.memo<Props>(({ selected, onRangeSelected }) => {
|
||||
const onChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
|
||||
onRangeSelected(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<SelectContainer>
|
||||
<EuiSelect
|
||||
data-test-subj="rangePicker"
|
||||
value={selected}
|
||||
options={Ranges.map((range) => ({
|
||||
text: range,
|
||||
}))}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</SelectContainer>
|
||||
);
|
||||
});
|
||||
|
||||
RangePicker.displayName = 'RangePicker';
|
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
/** Enables runtime enumeration of valid `Range`s */
|
||||
export const Ranges: string[] = [i18n.ONE_DAY, i18n.ONE_WEEK, i18n.ONE_MONTH, i18n.ONE_YEAR];
|
|
@ -1,24 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ONE_DAY = i18n.translate('xpack.securitySolution.timeline.rangePicker.oneDay', {
|
||||
defaultMessage: '1 Day',
|
||||
});
|
||||
|
||||
export const ONE_WEEK = i18n.translate('xpack.securitySolution.timeline.rangePicker.oneWeek', {
|
||||
defaultMessage: '1 Week',
|
||||
});
|
||||
|
||||
export const ONE_MONTH = i18n.translate('xpack.securitySolution.timeline.rangePicker.oneMonth', {
|
||||
defaultMessage: '1 Month',
|
||||
});
|
||||
|
||||
export const ONE_YEAR = i18n.translate('xpack.securitySolution.timeline.rangePicker.oneYear', {
|
||||
defaultMessage: '1 Year',
|
||||
});
|
|
@ -1,11 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TextFilter rendering renders correctly against snapshot 1`] = `
|
||||
<FieldText
|
||||
data-test-subj="textFilter"
|
||||
minwidth="100px"
|
||||
onChange={[Function]}
|
||||
placeholder="Filter"
|
||||
value=""
|
||||
/>
|
||||
`;
|
|
@ -1,79 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { DEFAULT_PLACEHOLDER, TextFilter } from '.';
|
||||
|
||||
describe('TextFilter', () => {
|
||||
describe('rendering', () => {
|
||||
test('renders correctly against snapshot', () => {
|
||||
const wrapper = shallow(<TextFilter columnId="foo" minWidth={100} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('placeholder', () => {
|
||||
test('it renders the default placeholder when no filter is specified, and a placeholder is NOT provided', () => {
|
||||
const wrapper = mount(<TextFilter columnId="foo" minWidth={100} />);
|
||||
|
||||
expect(wrapper.find(`input[placeholder="${DEFAULT_PLACEHOLDER}"]`).exists()).toEqual(true);
|
||||
});
|
||||
|
||||
test('it renders the default placeholder when no filter is specified, a placeholder is provided', () => {
|
||||
const placeholder = 'Make a jazz noise here';
|
||||
const wrapper = mount(
|
||||
<TextFilter columnId="foo" placeholder={placeholder} minWidth={100} />
|
||||
);
|
||||
|
||||
expect(wrapper.find(`input[placeholder="${placeholder}"]`).exists()).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('minWidth', () => {
|
||||
test('it applies the value of the minwidth prop to the input', () => {
|
||||
const minWidth = 150;
|
||||
const wrapper = mount(<TextFilter columnId="foo" minWidth={minWidth} />);
|
||||
|
||||
expect(wrapper.find('input').props()).toHaveProperty('minwidth', `${minWidth}px`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('value', () => {
|
||||
test('it renders the value of the filter prop', () => {
|
||||
const filter = 'out the noise';
|
||||
const wrapper = mount(<TextFilter columnId="foo" filter={filter} minWidth={100} />);
|
||||
|
||||
expect(wrapper.find('input').prop('value')).toEqual(filter);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#onFilterChange', () => {
|
||||
test('it invokes the onFilterChange callback when the input is updated', () => {
|
||||
const columnId = 'foo';
|
||||
const oldFilter = 'abcdef';
|
||||
const newFilter = `${oldFilter}g`;
|
||||
const onFilterChange = jest.fn();
|
||||
|
||||
const wrapper = mount(
|
||||
<TextFilter
|
||||
columnId={columnId}
|
||||
filter={oldFilter}
|
||||
minWidth={100}
|
||||
onFilterChange={onFilterChange}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper.find('input').simulate('change', { target: { value: newFilter } });
|
||||
expect(onFilterChange).toBeCalledWith({
|
||||
columnId,
|
||||
filter: newFilter,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFieldText } from '@elastic/eui';
|
||||
import { noop } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type { OnFilterChange } from '../../../events';
|
||||
import type { ColumnId } from '../../column_id';
|
||||
|
||||
interface Props {
|
||||
columnId: ColumnId;
|
||||
filter?: string;
|
||||
minWidth: number;
|
||||
onFilterChange?: OnFilterChange;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_PLACEHOLDER = 'Filter';
|
||||
|
||||
const FieldText = styled(EuiFieldText)<{ minwidth: string }>`
|
||||
min-width: ${(props) => props.minwidth};
|
||||
`;
|
||||
|
||||
FieldText.displayName = 'FieldText';
|
||||
|
||||
/** Renders a text-based column filter */
|
||||
export const TextFilter = React.memo<Props>(
|
||||
({
|
||||
columnId,
|
||||
minWidth,
|
||||
filter = '',
|
||||
onFilterChange = noop,
|
||||
placeholder = DEFAULT_PLACEHOLDER,
|
||||
}) => {
|
||||
const onChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
onFilterChange({ columnId, filter: event.target.value });
|
||||
};
|
||||
|
||||
return (
|
||||
<FieldText
|
||||
data-test-subj="textFilter"
|
||||
minwidth={`${minWidth}px`}
|
||||
placeholder={placeholder}
|
||||
value={filter}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
TextFilter.displayName = 'TextFilter';
|
File diff suppressed because it is too large
Load diff
|
@ -1,93 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer';
|
||||
import { mockTimelineData } from '../../../../../common/mock';
|
||||
import { defaultHeaders } from '../column_headers/default_headers';
|
||||
import { getDefaultControlColumn } from '../control_columns';
|
||||
|
||||
import { DataDrivenColumns, getMappedNonEcsValue } from '.';
|
||||
|
||||
describe('Columns', () => {
|
||||
const headersSansTimestamp = defaultHeaders.filter((h) => h.id !== '@timestamp');
|
||||
const ACTION_BUTTON_COUNT = 4;
|
||||
const leadingControlColumns = getDefaultControlColumn(ACTION_BUTTON_COUNT);
|
||||
|
||||
test('it renders the expected columns', () => {
|
||||
const wrapper = shallow(
|
||||
<DataDrivenColumns
|
||||
ariaRowindex={2}
|
||||
id={mockTimelineData[0]._id}
|
||||
actionsColumnWidth={50}
|
||||
checked={false}
|
||||
columnHeaders={headersSansTimestamp}
|
||||
data={mockTimelineData[0].data}
|
||||
ecsData={mockTimelineData[0].ecs}
|
||||
hasRowRenderers={false}
|
||||
notesCount={0}
|
||||
renderCellValue={DefaultCellRenderer}
|
||||
timelineId="test"
|
||||
columnValues={'abc def'}
|
||||
showCheckboxes={false}
|
||||
selectedEventIds={{}}
|
||||
loadingEventIds={[]}
|
||||
onEventDetailsPanelOpened={jest.fn()}
|
||||
onRowSelected={jest.fn()}
|
||||
showNotes={false}
|
||||
isEventPinned={false}
|
||||
toggleShowNotes={jest.fn()}
|
||||
refetch={jest.fn()}
|
||||
eventIdToNoteIds={{}}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
trailingControlColumns={[]}
|
||||
setEventsLoading={jest.fn()}
|
||||
setEventsDeleted={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('getMappedNonEcsValue', () => {
|
||||
const existingField = 'Descarte';
|
||||
const existingValue = ['IThinkThereforeIAm'];
|
||||
|
||||
test('should return the value if the fieldName is found', () => {
|
||||
const result = getMappedNonEcsValue({
|
||||
data: [{ field: existingField, value: existingValue }],
|
||||
fieldName: existingField,
|
||||
});
|
||||
|
||||
expect(result).toBe(existingValue);
|
||||
});
|
||||
|
||||
test('should return undefined if the value cannot be found in the array', () => {
|
||||
const result = getMappedNonEcsValue({
|
||||
data: [{ field: existingField, value: existingValue }],
|
||||
fieldName: 'nonExistent',
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should return undefined when data is an empty array', () => {
|
||||
const result = getMappedNonEcsValue({ data: [], fieldName: existingField });
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should return undefined when data is undefined', () => {
|
||||
const result = getMappedNonEcsValue({ data: undefined, fieldName: existingField });
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,472 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiScreenReaderOnly } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import { getOr } from 'lodash/fp';
|
||||
import { DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME } from '@kbn/securitysolution-t-grid';
|
||||
|
||||
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
|
||||
import type {
|
||||
SetEventsDeleted,
|
||||
SetEventsLoading,
|
||||
ActionProps,
|
||||
ControlColumnProps,
|
||||
RowCellRender,
|
||||
} from '../../../../../../common/types';
|
||||
import type {
|
||||
CellValueElementProps,
|
||||
ColumnHeaderOptions,
|
||||
TimelineTabs,
|
||||
} from '../../../../../../common/types/timeline';
|
||||
import type { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
|
||||
import { ARIA_COLUMN_INDEX_OFFSET } from '../../helpers';
|
||||
import type { OnRowSelected } from '../../events';
|
||||
import type { inputsModel } from '../../../../../common/store';
|
||||
import {
|
||||
EventsTd,
|
||||
EVENTS_TD_CLASS_NAME,
|
||||
EventsTdContent,
|
||||
EventsTdGroupData,
|
||||
EventsTdGroupActions,
|
||||
} from '../../styles';
|
||||
|
||||
import { StatefulCell } from './stateful_cell';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface CellProps {
|
||||
_id: string;
|
||||
ariaRowindex: number;
|
||||
index: number;
|
||||
header: ColumnHeaderOptions;
|
||||
data: TimelineNonEcsData[];
|
||||
ecsData: Ecs;
|
||||
hasRowRenderers: boolean;
|
||||
notesCount: number;
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
tabType?: TimelineTabs;
|
||||
timelineId: string;
|
||||
}
|
||||
|
||||
interface DataDrivenColumnProps {
|
||||
id: string;
|
||||
actionsColumnWidth: number;
|
||||
ariaRowindex: number;
|
||||
checked: boolean;
|
||||
columnHeaders: ColumnHeaderOptions[];
|
||||
columnValues: string;
|
||||
data: TimelineNonEcsData[];
|
||||
ecsData: Ecs;
|
||||
eventIdToNoteIds: Readonly<Record<string, string[]>>;
|
||||
isEventPinned: boolean;
|
||||
isEventViewer?: boolean;
|
||||
loadingEventIds: Readonly<string[]>;
|
||||
notesCount: number;
|
||||
onEventDetailsPanelOpened: () => void;
|
||||
onRowSelected: OnRowSelected;
|
||||
refetch: inputsModel.Refetch;
|
||||
onRuleChange?: () => void;
|
||||
hasRowRenderers: boolean;
|
||||
selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>;
|
||||
showCheckboxes: boolean;
|
||||
showNotes: boolean;
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
tabType?: TimelineTabs;
|
||||
timelineId: string;
|
||||
toggleShowNotes: () => void;
|
||||
trailingControlColumns: ControlColumnProps[];
|
||||
leadingControlColumns: ControlColumnProps[];
|
||||
setEventsLoading: SetEventsLoading;
|
||||
setEventsDeleted: SetEventsDeleted;
|
||||
}
|
||||
|
||||
const SPACE = ' ';
|
||||
|
||||
export const shouldForwardKeyDownEvent = (key: string): boolean => {
|
||||
switch (key) {
|
||||
case SPACE: // fall through
|
||||
case 'Enter':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const onKeyDown = (keyboardEvent: React.KeyboardEvent) => {
|
||||
const { altKey, ctrlKey, key, metaKey, shiftKey, target, type } = keyboardEvent;
|
||||
|
||||
const targetElement = target as Element;
|
||||
|
||||
// we *only* forward the event to the (child) draggable keyboard wrapper
|
||||
// if the keyboard event originated from the container (TD) element
|
||||
if (shouldForwardKeyDownEvent(key) && targetElement.className?.includes(EVENTS_TD_CLASS_NAME)) {
|
||||
const draggableKeyboardWrapper = targetElement.querySelector<HTMLDivElement>(
|
||||
`.${DRAGGABLE_KEYBOARD_WRAPPER_CLASS_NAME}`
|
||||
);
|
||||
|
||||
const newEvent = new KeyboardEvent(type, {
|
||||
altKey,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
ctrlKey,
|
||||
key,
|
||||
metaKey,
|
||||
shiftKey,
|
||||
});
|
||||
|
||||
if (key === ' ') {
|
||||
// prevent the default behavior of scrolling the table when space is pressed
|
||||
keyboardEvent.preventDefault();
|
||||
}
|
||||
|
||||
draggableKeyboardWrapper?.dispatchEvent(newEvent);
|
||||
}
|
||||
};
|
||||
|
||||
const TgridActionTdCell = ({
|
||||
action: Action,
|
||||
width,
|
||||
actionsColumnWidth,
|
||||
ariaRowindex,
|
||||
columnId,
|
||||
columnValues,
|
||||
data,
|
||||
ecsData,
|
||||
eventIdToNoteIds,
|
||||
index,
|
||||
isEventPinned,
|
||||
isEventViewer,
|
||||
eventId,
|
||||
loadingEventIds,
|
||||
notesCount,
|
||||
onEventDetailsPanelOpened,
|
||||
onRowSelected,
|
||||
refetch,
|
||||
rowIndex,
|
||||
hasRowRenderers,
|
||||
onRuleChange,
|
||||
selectedEventIds,
|
||||
showCheckboxes,
|
||||
showNotes,
|
||||
tabType,
|
||||
timelineId,
|
||||
toggleShowNotes,
|
||||
setEventsLoading,
|
||||
setEventsDeleted,
|
||||
}: ActionProps & {
|
||||
columnId: string;
|
||||
hasRowRenderers: boolean;
|
||||
actionsColumnWidth: number;
|
||||
notesCount: number;
|
||||
selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>;
|
||||
}) => {
|
||||
const displayWidth = width ? width : actionsColumnWidth;
|
||||
return (
|
||||
<EventsTdGroupActions
|
||||
width={displayWidth}
|
||||
data-test-subj="event-actions-container"
|
||||
tabIndex={0}
|
||||
>
|
||||
<EventsTd
|
||||
$ariaColumnIndex={index + ARIA_COLUMN_INDEX_OFFSET}
|
||||
key={tabType != null ? `${eventId}_${tabType}` : `${eventId}`}
|
||||
onKeyDown={onKeyDown}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
width={width}
|
||||
>
|
||||
<EventsTdContent data-test-subj="cell-container">
|
||||
<>
|
||||
<EuiScreenReaderOnly data-test-subj="screenReaderOnly">
|
||||
<p>{i18n.YOU_ARE_IN_A_TABLE_CELL({ row: ariaRowindex, column: index + 2 })}</p>
|
||||
</EuiScreenReaderOnly>
|
||||
{Action && (
|
||||
<Action
|
||||
ariaRowindex={ariaRowindex}
|
||||
width={width}
|
||||
checked={Object.keys(selectedEventIds).includes(eventId)}
|
||||
columnId={columnId}
|
||||
columnValues={columnValues}
|
||||
eventId={eventId}
|
||||
data={data}
|
||||
ecsData={ecsData}
|
||||
eventIdToNoteIds={eventIdToNoteIds}
|
||||
index={index}
|
||||
isEventPinned={isEventPinned}
|
||||
isEventViewer={isEventViewer}
|
||||
loadingEventIds={loadingEventIds}
|
||||
onEventDetailsPanelOpened={onEventDetailsPanelOpened}
|
||||
onRowSelected={onRowSelected}
|
||||
refetch={refetch}
|
||||
rowIndex={rowIndex}
|
||||
onRuleChange={onRuleChange}
|
||||
showCheckboxes={showCheckboxes}
|
||||
showNotes={showNotes}
|
||||
timelineId={timelineId}
|
||||
toggleShowNotes={toggleShowNotes}
|
||||
setEventsLoading={setEventsLoading}
|
||||
setEventsDeleted={setEventsDeleted}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</EventsTdContent>
|
||||
{hasRowRenderers ? (
|
||||
<EuiScreenReaderOnly data-test-subj="hasRowRendererScreenReaderOnly">
|
||||
<p>{i18n.EVENT_HAS_AN_EVENT_RENDERER(ariaRowindex)}</p>
|
||||
</EuiScreenReaderOnly>
|
||||
) : null}
|
||||
|
||||
{notesCount ? (
|
||||
<EuiScreenReaderOnly data-test-subj="hasNotesScreenReaderOnly">
|
||||
<p>{i18n.EVENT_HAS_NOTES({ row: ariaRowindex, notesCount })}</p>
|
||||
</EuiScreenReaderOnly>
|
||||
) : null}
|
||||
</EventsTd>
|
||||
</EventsTdGroupActions>
|
||||
);
|
||||
};
|
||||
|
||||
const TgridTdCell = ({
|
||||
_id,
|
||||
ariaRowindex,
|
||||
index,
|
||||
header,
|
||||
data,
|
||||
ecsData,
|
||||
hasRowRenderers,
|
||||
notesCount,
|
||||
renderCellValue,
|
||||
tabType,
|
||||
timelineId,
|
||||
}: CellProps) => {
|
||||
const ariaColIndex = index + ARIA_COLUMN_INDEX_OFFSET;
|
||||
return (
|
||||
<EventsTd
|
||||
$ariaColumnIndex={ariaColIndex}
|
||||
key={tabType != null ? `${header.id}_${tabType}` : `${header.id}`}
|
||||
onKeyDown={onKeyDown}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
width={header.initialWidth}
|
||||
>
|
||||
<EventsTdContent data-test-subj="cell-container">
|
||||
<>
|
||||
<EuiScreenReaderOnly data-test-subj="screenReaderOnly">
|
||||
<p>{i18n.YOU_ARE_IN_A_TABLE_CELL({ row: ariaRowindex, column: ariaColIndex })}</p>
|
||||
</EuiScreenReaderOnly>
|
||||
<StatefulCell
|
||||
rowIndex={ariaRowindex - 1}
|
||||
colIndex={ariaColIndex - 1}
|
||||
data={data}
|
||||
header={header}
|
||||
eventId={_id}
|
||||
linkValues={getOr([], header.linkField ?? '', ecsData)}
|
||||
renderCellValue={renderCellValue}
|
||||
tabType={tabType}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</>
|
||||
</EventsTdContent>
|
||||
{hasRowRenderers ? (
|
||||
<EuiScreenReaderOnly data-test-subj="hasRowRendererScreenReaderOnly">
|
||||
<p>{i18n.EVENT_HAS_AN_EVENT_RENDERER(ariaRowindex)}</p>
|
||||
</EuiScreenReaderOnly>
|
||||
) : null}
|
||||
|
||||
{notesCount ? (
|
||||
<EuiScreenReaderOnly data-test-subj="hasNotesScreenReaderOnly">
|
||||
<p>{i18n.EVENT_HAS_NOTES({ row: ariaRowindex, notesCount })}</p>
|
||||
</EuiScreenReaderOnly>
|
||||
) : null}
|
||||
</EventsTd>
|
||||
);
|
||||
};
|
||||
|
||||
export const DataDrivenColumns = React.memo<DataDrivenColumnProps>(
|
||||
({
|
||||
ariaRowindex,
|
||||
actionsColumnWidth,
|
||||
columnHeaders,
|
||||
columnValues,
|
||||
data,
|
||||
ecsData,
|
||||
eventIdToNoteIds,
|
||||
isEventPinned,
|
||||
isEventViewer,
|
||||
id: _id,
|
||||
loadingEventIds,
|
||||
notesCount,
|
||||
onEventDetailsPanelOpened,
|
||||
onRowSelected,
|
||||
refetch,
|
||||
hasRowRenderers,
|
||||
onRuleChange,
|
||||
renderCellValue,
|
||||
selectedEventIds,
|
||||
showCheckboxes,
|
||||
showNotes,
|
||||
tabType,
|
||||
timelineId,
|
||||
toggleShowNotes,
|
||||
trailingControlColumns,
|
||||
leadingControlColumns,
|
||||
setEventsLoading,
|
||||
setEventsDeleted,
|
||||
}) => {
|
||||
const trailingActionCells = useMemo(
|
||||
() =>
|
||||
trailingControlColumns ? trailingControlColumns.map((column) => column.rowCellRender) : [],
|
||||
[trailingControlColumns]
|
||||
);
|
||||
const leadingAndDataColumnCount = useMemo(
|
||||
() => leadingControlColumns.length + columnHeaders.length,
|
||||
[leadingControlColumns, columnHeaders]
|
||||
);
|
||||
const TrailingActions = useMemo(
|
||||
() =>
|
||||
trailingActionCells.map((Action: RowCellRender | undefined, index) => {
|
||||
return (
|
||||
Action && (
|
||||
<TgridActionTdCell
|
||||
action={Action}
|
||||
width={trailingControlColumns[index].width}
|
||||
actionsColumnWidth={actionsColumnWidth}
|
||||
ariaRowindex={ariaRowindex}
|
||||
checked={Object.keys(selectedEventIds).includes(_id)}
|
||||
columnId={trailingControlColumns[index].id || ''}
|
||||
columnValues={columnValues}
|
||||
onRowSelected={onRowSelected}
|
||||
data-test-subj="actions"
|
||||
eventId={_id}
|
||||
data={data}
|
||||
key={index}
|
||||
index={leadingAndDataColumnCount + index}
|
||||
rowIndex={ariaRowindex}
|
||||
ecsData={ecsData}
|
||||
loadingEventIds={loadingEventIds}
|
||||
onEventDetailsPanelOpened={onEventDetailsPanelOpened}
|
||||
showCheckboxes={showCheckboxes}
|
||||
eventIdToNoteIds={eventIdToNoteIds}
|
||||
isEventPinned={isEventPinned}
|
||||
isEventViewer={isEventViewer}
|
||||
notesCount={notesCount}
|
||||
refetch={refetch}
|
||||
hasRowRenderers={hasRowRenderers}
|
||||
onRuleChange={onRuleChange}
|
||||
selectedEventIds={selectedEventIds}
|
||||
showNotes={showNotes}
|
||||
tabType={tabType}
|
||||
timelineId={timelineId}
|
||||
toggleShowNotes={toggleShowNotes}
|
||||
setEventsLoading={setEventsLoading}
|
||||
setEventsDeleted={setEventsDeleted}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}),
|
||||
[
|
||||
trailingControlColumns,
|
||||
_id,
|
||||
data,
|
||||
ecsData,
|
||||
onRowSelected,
|
||||
isEventPinned,
|
||||
isEventViewer,
|
||||
actionsColumnWidth,
|
||||
ariaRowindex,
|
||||
columnValues,
|
||||
eventIdToNoteIds,
|
||||
hasRowRenderers,
|
||||
leadingAndDataColumnCount,
|
||||
loadingEventIds,
|
||||
notesCount,
|
||||
onEventDetailsPanelOpened,
|
||||
onRuleChange,
|
||||
refetch,
|
||||
selectedEventIds,
|
||||
showCheckboxes,
|
||||
showNotes,
|
||||
tabType,
|
||||
timelineId,
|
||||
toggleShowNotes,
|
||||
trailingActionCells,
|
||||
setEventsLoading,
|
||||
setEventsDeleted,
|
||||
]
|
||||
);
|
||||
const ColumnHeaders = useMemo(
|
||||
() =>
|
||||
columnHeaders.map((header, index) => (
|
||||
<TgridTdCell
|
||||
_id={_id}
|
||||
index={index}
|
||||
header={header}
|
||||
key={tabType != null ? `${header.id}_${tabType}` : `${header.id}`}
|
||||
ariaRowindex={ariaRowindex}
|
||||
data={data}
|
||||
ecsData={ecsData}
|
||||
hasRowRenderers={hasRowRenderers}
|
||||
notesCount={notesCount}
|
||||
renderCellValue={renderCellValue}
|
||||
tabType={tabType}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
)),
|
||||
[
|
||||
_id,
|
||||
ariaRowindex,
|
||||
columnHeaders,
|
||||
data,
|
||||
ecsData,
|
||||
hasRowRenderers,
|
||||
notesCount,
|
||||
renderCellValue,
|
||||
tabType,
|
||||
timelineId,
|
||||
]
|
||||
);
|
||||
return (
|
||||
<EventsTdGroupData data-test-subj="data-driven-columns">
|
||||
{ColumnHeaders}
|
||||
{TrailingActions}
|
||||
</EventsTdGroupData>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
DataDrivenColumns.displayName = 'DataDrivenColumns';
|
||||
|
||||
export const getMappedNonEcsValue = ({
|
||||
data,
|
||||
fieldName,
|
||||
}: {
|
||||
data?: TimelineNonEcsData[];
|
||||
fieldName: string;
|
||||
}): string[] | undefined => {
|
||||
/*
|
||||
While data _should_ always be defined
|
||||
There is the potential for race conditions where a component using this function
|
||||
is still visible in the UI, while the data has since been removed.
|
||||
To cover all scenarios where this happens we'll check for the presence of data here
|
||||
*/
|
||||
if (!data || data.length === 0) return undefined;
|
||||
const item = data.find((d) => d.field === fieldName);
|
||||
if (item != null && item.value != null) {
|
||||
return item.value;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const useGetMappedNonEcsValue = ({
|
||||
data,
|
||||
fieldName,
|
||||
}: {
|
||||
data?: TimelineNonEcsData[];
|
||||
fieldName: string;
|
||||
}): string[] | undefined => {
|
||||
return useMemo(() => getMappedNonEcsValue({ data, fieldName }), [data, fieldName]);
|
||||
};
|
|
@ -1,171 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { mount } from 'enzyme';
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { defaultHeaders, mockTimelineData } from '../../../../../common/mock';
|
||||
import type { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
|
||||
import type {
|
||||
ColumnHeaderOptions,
|
||||
CellValueElementProps,
|
||||
} from '../../../../../../common/types/timeline';
|
||||
import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
|
||||
|
||||
import { StatefulCell } from './stateful_cell';
|
||||
import { useGetMappedNonEcsValue } from '.';
|
||||
|
||||
/**
|
||||
* This (test) component implement's `EuiDataGrid`'s `renderCellValue` interface,
|
||||
* as documented here: https://elastic.github.io/eui/#/tabular-content/data-grid
|
||||
*
|
||||
* Its `CellValueElementProps` props are a superset of `EuiDataGridCellValueElementProps`.
|
||||
* The `setCellProps` function, defined by the `EuiDataGridCellValueElementProps` interface,
|
||||
* is typically called in a `useEffect`, as illustrated by `EuiDataGrid`'s code sandbox example:
|
||||
* https://codesandbox.io/s/zhxmo
|
||||
*/
|
||||
const RenderCellValue: React.FC<CellValueElementProps> = ({ columnId, data, setCellProps }) => {
|
||||
const value = useGetMappedNonEcsValue({
|
||||
data,
|
||||
fieldName: columnId,
|
||||
});
|
||||
useEffect(() => {
|
||||
// branching logic that conditionally renders a specific cell green:
|
||||
if (columnId === defaultHeaders[0].id) {
|
||||
if (value?.length) {
|
||||
setCellProps({
|
||||
style: {
|
||||
backgroundColor: 'green',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [columnId, data, setCellProps, value]);
|
||||
|
||||
return <div data-test-subj="renderCellValue">{value}</div>;
|
||||
};
|
||||
|
||||
describe('StatefulCell', () => {
|
||||
const rowIndex = 123;
|
||||
const colIndex = 0;
|
||||
const eventId = '_id-123';
|
||||
const linkValues = ['foo', 'bar', '@baz'];
|
||||
const timelineId = TimelineId.test;
|
||||
|
||||
let header: ColumnHeaderOptions;
|
||||
let data: TimelineNonEcsData[];
|
||||
beforeEach(() => {
|
||||
data = cloneDeep(mockTimelineData[0].data);
|
||||
header = cloneDeep(defaultHeaders[0]);
|
||||
});
|
||||
|
||||
test('it invokes renderCellValue with the expected arguments when tabType is specified', () => {
|
||||
const renderCellValue = jest.fn();
|
||||
|
||||
mount(
|
||||
<StatefulCell
|
||||
rowIndex={rowIndex}
|
||||
colIndex={colIndex}
|
||||
data={data}
|
||||
header={header}
|
||||
eventId={eventId}
|
||||
linkValues={linkValues}
|
||||
renderCellValue={renderCellValue}
|
||||
tabType={TimelineTabs.query}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(renderCellValue).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
columnId: header.id,
|
||||
eventId,
|
||||
data,
|
||||
header,
|
||||
isExpandable: true,
|
||||
isExpanded: false,
|
||||
isDetails: false,
|
||||
linkValues,
|
||||
rowIndex,
|
||||
colIndex,
|
||||
scopeId: timelineId,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('it invokes renderCellValue with the expected arguments when tabType is NOT specified', () => {
|
||||
const renderCellValue = jest.fn();
|
||||
|
||||
mount(
|
||||
<StatefulCell
|
||||
rowIndex={rowIndex}
|
||||
colIndex={colIndex}
|
||||
data={data}
|
||||
header={header}
|
||||
eventId={eventId}
|
||||
linkValues={linkValues}
|
||||
renderCellValue={renderCellValue}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(renderCellValue).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
columnId: header.id,
|
||||
eventId,
|
||||
data,
|
||||
header,
|
||||
isExpandable: true,
|
||||
isExpanded: false,
|
||||
isDetails: false,
|
||||
linkValues,
|
||||
rowIndex,
|
||||
colIndex,
|
||||
scopeId: timelineId,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders the React.Node returned by renderCellValue', () => {
|
||||
const renderCellValue = () => <div data-test-subj="renderCellValue" />;
|
||||
|
||||
const wrapper = mount(
|
||||
<StatefulCell
|
||||
rowIndex={rowIndex}
|
||||
colIndex={colIndex}
|
||||
data={data}
|
||||
header={header}
|
||||
eventId={eventId}
|
||||
linkValues={linkValues}
|
||||
renderCellValue={renderCellValue}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="renderCellValue"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
test("it renders a div with the styles set by `renderCellValue`'s `setCellProps` argument", () => {
|
||||
const wrapper = mount(
|
||||
<StatefulCell
|
||||
rowIndex={rowIndex}
|
||||
colIndex={colIndex}
|
||||
data={data}
|
||||
header={header}
|
||||
eventId={eventId}
|
||||
linkValues={linkValues}
|
||||
renderCellValue={RenderCellValue}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="statefulCell"]').getDOMNode().getAttribute('style')
|
||||
).toEqual('background-color: green;');
|
||||
});
|
||||
});
|
|
@ -1,71 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import type { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
|
||||
import type {
|
||||
ColumnHeaderOptions,
|
||||
CellValueElementProps,
|
||||
TimelineTabs,
|
||||
} from '../../../../../../common/types/timeline';
|
||||
|
||||
export interface CommonProps {
|
||||
className?: string;
|
||||
'aria-label'?: string;
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
|
||||
const StatefulCellComponent = ({
|
||||
rowIndex,
|
||||
colIndex,
|
||||
data,
|
||||
header,
|
||||
eventId,
|
||||
linkValues,
|
||||
renderCellValue,
|
||||
tabType,
|
||||
timelineId,
|
||||
}: {
|
||||
rowIndex: number;
|
||||
colIndex: number;
|
||||
data: TimelineNonEcsData[];
|
||||
header: ColumnHeaderOptions;
|
||||
eventId: string;
|
||||
linkValues: string[] | undefined;
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
tabType?: TimelineTabs;
|
||||
timelineId: string;
|
||||
}) => {
|
||||
const [cellProps, setCellProps] = useState<CommonProps & HTMLAttributes<HTMLDivElement>>({});
|
||||
return (
|
||||
<div data-test-subj="statefulCell" {...cellProps}>
|
||||
{renderCellValue({
|
||||
columnId: header.id,
|
||||
eventId,
|
||||
data,
|
||||
header,
|
||||
isDraggable: true,
|
||||
isExpandable: true,
|
||||
isExpanded: false,
|
||||
isDetails: false,
|
||||
isTimeline: true,
|
||||
linkValues,
|
||||
rowIndex,
|
||||
colIndex,
|
||||
setCellProps,
|
||||
scopeId: timelineId,
|
||||
key: tabType != null ? `${timelineId}-${tabType}` : timelineId,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
StatefulCellComponent.displayName = 'StatefulCellComponent';
|
||||
|
||||
export const StatefulCell = React.memo(StatefulCellComponent);
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const YOU_ARE_IN_A_TABLE_CELL = ({ column, row }: { column: number; row: number }) =>
|
||||
i18n.translate('xpack.securitySolution.timeline.youAreInATableCellScreenReaderOnly', {
|
||||
values: { column, row },
|
||||
defaultMessage: 'You are in a table cell. row: {row}, column: {column}',
|
||||
});
|
||||
|
||||
export const EVENT_HAS_AN_EVENT_RENDERER = (row: number) =>
|
||||
i18n.translate('xpack.securitySolution.timeline.eventHasEventRendererScreenReaderOnly', {
|
||||
values: { row },
|
||||
defaultMessage:
|
||||
'The event in row {row} has an event renderer. Press shift + down arrow to focus it.',
|
||||
});
|
||||
|
||||
export const EVENT_HAS_NOTES = ({ notesCount, row }: { notesCount: number; row: number }) =>
|
||||
i18n.translate('xpack.securitySolution.timeline.eventHasNotesScreenReaderOnly', {
|
||||
values: { notesCount, row },
|
||||
defaultMessage:
|
||||
'The event in row {row} has {notesCount, plural, =1 {a note} other {{notesCount} notes}}. Press shift + right arrow to focus notes.',
|
||||
});
|
|
@ -1,164 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { mount, type ComponentType as EnzymeComponentType } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { TestProviders } from '../../../../../common/mock';
|
||||
|
||||
import { EventColumnView } from './event_column_view';
|
||||
import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer';
|
||||
import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline';
|
||||
import { TimelineTypeEnum } from '../../../../../../common/api/timeline';
|
||||
import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
|
||||
import { getDefaultControlColumn } from '../control_columns';
|
||||
import { testLeadingControlColumn } from '../../../../../common/mock/mock_timeline_control_columns';
|
||||
import { mockTimelines } from '../../../../../common/mock/mock_timelines_plugin';
|
||||
import { mockCasesContract } from '@kbn/cases-plugin/public/mocks';
|
||||
import { getActionsColumnWidth } from '../../../../../common/components/header_actions';
|
||||
|
||||
jest.mock('../../../../../common/components/header_actions/add_note_icon_item', () => {
|
||||
return {
|
||||
AddEventNoteAction: jest.fn(() => <div data-test-subj="add-note-button-mock" />),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../../../common/hooks/use_experimental_features');
|
||||
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
|
||||
jest.mock('../../../../../common/hooks/use_selector', () => ({
|
||||
useShallowEqualSelector: jest.fn(),
|
||||
useDeepEqualSelector: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../../../../common/components/user_privileges', () => {
|
||||
return {
|
||||
useUserPrivileges: () => ({
|
||||
listPrivileges: { loading: false, error: undefined, result: undefined },
|
||||
detectionEnginePrivileges: { loading: false, error: undefined, result: undefined },
|
||||
endpointPrivileges: {},
|
||||
kibanaSecuritySolutionsPrivileges: { crud: true, read: true },
|
||||
}),
|
||||
};
|
||||
});
|
||||
jest.mock('../../../../../common/components/guided_onboarding_tour/tour_step');
|
||||
jest.mock('../../../../../common/lib/kibana', () => {
|
||||
const originalModule = jest.requireActual('../../../../../common/lib/kibana');
|
||||
|
||||
return {
|
||||
...originalModule,
|
||||
useKibana: () => ({
|
||||
services: {
|
||||
timelines: { ...mockTimelines },
|
||||
data: {
|
||||
search: jest.fn(),
|
||||
query: jest.fn(),
|
||||
},
|
||||
application: {
|
||||
capabilities: {
|
||||
siem: { crud_alerts: true, read_alerts: true },
|
||||
},
|
||||
},
|
||||
cases: mockCasesContract(),
|
||||
},
|
||||
}),
|
||||
useNavigateTo: () => ({
|
||||
navigateTo: jest.fn(),
|
||||
}),
|
||||
useToasts: jest.fn().mockReturnValue({
|
||||
addError: jest.fn(),
|
||||
addSuccess: jest.fn(),
|
||||
addWarning: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('EventColumnView', () => {
|
||||
useIsExperimentalFeatureEnabledMock.mockReturnValue(false);
|
||||
(useShallowEqualSelector as jest.Mock).mockReturnValue(TimelineTypeEnum.default);
|
||||
const ACTION_BUTTON_COUNT = 4;
|
||||
const leadingControlColumns = getDefaultControlColumn(ACTION_BUTTON_COUNT);
|
||||
|
||||
const props = {
|
||||
ariaRowindex: 2,
|
||||
id: 'event-id',
|
||||
actionsColumnWidth: getActionsColumnWidth(ACTION_BUTTON_COUNT),
|
||||
associateNote: jest.fn(),
|
||||
columnHeaders: [],
|
||||
columnRenderers: [],
|
||||
data: [
|
||||
{
|
||||
field: 'host.name',
|
||||
},
|
||||
],
|
||||
ecsData: {
|
||||
_id: 'id',
|
||||
},
|
||||
eventIdToNoteIds: {},
|
||||
expanded: false,
|
||||
hasRowRenderers: false,
|
||||
loading: false,
|
||||
loadingEventIds: [],
|
||||
notesCount: 0,
|
||||
onEventDetailsPanelOpened: jest.fn(),
|
||||
onRowSelected: jest.fn(),
|
||||
refetch: jest.fn(),
|
||||
renderCellValue: DefaultCellRenderer,
|
||||
selectedEventIds: {},
|
||||
showCheckboxes: false,
|
||||
showNotes: false,
|
||||
tabType: TimelineTabs.query,
|
||||
timelineId: TimelineId.active,
|
||||
toggleShowNotes: jest.fn(),
|
||||
updateNote: jest.fn(),
|
||||
isEventPinned: false,
|
||||
leadingControlColumns,
|
||||
trailingControlColumns: [],
|
||||
setEventsLoading: jest.fn(),
|
||||
setEventsDeleted: jest.fn(),
|
||||
};
|
||||
|
||||
test('it does NOT render a notes button when isEventsViewer is true', () => {
|
||||
const wrapper = mount(<EventColumnView {...props} isEventViewer={true} />, {
|
||||
wrappingComponent: TestProviders as EnzymeComponentType<{}>,
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-subj="add-note-button-mock"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('it does NOT render a notes button when showNotes is false', () => {
|
||||
const wrapper = mount(<EventColumnView {...props} showNotes={false} />, {
|
||||
wrappingComponent: TestProviders as EnzymeComponentType<{}>,
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-subj="add-note-button-mock"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('it does NOT render a pin button when isEventViewer is true', () => {
|
||||
const wrapper = mount(<EventColumnView {...props} isEventViewer={true} />, {
|
||||
wrappingComponent: TestProviders as EnzymeComponentType<{}>,
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-subj="pin"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('it renders a custom control column in addition to the default control column', () => {
|
||||
const wrapper = mount(
|
||||
<EventColumnView
|
||||
{...props}
|
||||
timelineId={TimelineId.test}
|
||||
leadingControlColumns={[testLeadingControlColumn, ...leadingControlColumns]}
|
||||
/>,
|
||||
{
|
||||
wrappingComponent: TestProviders as EnzymeComponentType<{}>,
|
||||
}
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="expand-event"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="test-body-control-column-cell"]').exists()).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,225 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
|
||||
import type {
|
||||
ControlColumnProps,
|
||||
RowCellRender,
|
||||
SetEventsDeleted,
|
||||
SetEventsLoading,
|
||||
} from '../../../../../../common/types';
|
||||
import type { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
|
||||
import type { OnRowSelected } from '../../events';
|
||||
import { EventsTrData, EventsTdGroupActions } from '../../styles';
|
||||
import { DataDrivenColumns, getMappedNonEcsValue } from '../data_driven_columns';
|
||||
import type { inputsModel } from '../../../../../common/store';
|
||||
import type {
|
||||
ColumnHeaderOptions,
|
||||
CellValueElementProps,
|
||||
TimelineTabs,
|
||||
} from '../../../../../../common/types/timeline';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
actionsColumnWidth: number;
|
||||
ariaRowindex: number;
|
||||
columnHeaders: ColumnHeaderOptions[];
|
||||
data: TimelineNonEcsData[];
|
||||
ecsData: Ecs;
|
||||
eventIdToNoteIds: Readonly<Record<string, string[]>>;
|
||||
isEventPinned: boolean;
|
||||
isEventViewer?: boolean;
|
||||
loadingEventIds: Readonly<string[]>;
|
||||
notesCount: number;
|
||||
onEventDetailsPanelOpened: () => void;
|
||||
onRowSelected: OnRowSelected;
|
||||
refetch: inputsModel.Refetch;
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
onRuleChange?: () => void;
|
||||
hasRowRenderers: boolean;
|
||||
selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>;
|
||||
showCheckboxes: boolean;
|
||||
showNotes: boolean;
|
||||
tabType?: TimelineTabs;
|
||||
timelineId: string;
|
||||
toggleShowNotes: (eventId?: string) => void;
|
||||
leadingControlColumns: ControlColumnProps[];
|
||||
trailingControlColumns: ControlColumnProps[];
|
||||
setEventsLoading: SetEventsLoading;
|
||||
setEventsDeleted: SetEventsDeleted;
|
||||
}
|
||||
|
||||
export const EventColumnView = React.memo<Props>(
|
||||
({
|
||||
id,
|
||||
actionsColumnWidth,
|
||||
ariaRowindex,
|
||||
columnHeaders,
|
||||
data,
|
||||
ecsData,
|
||||
eventIdToNoteIds,
|
||||
isEventPinned = false,
|
||||
isEventViewer = false,
|
||||
loadingEventIds,
|
||||
notesCount,
|
||||
onEventDetailsPanelOpened,
|
||||
onRowSelected,
|
||||
refetch,
|
||||
hasRowRenderers,
|
||||
onRuleChange,
|
||||
renderCellValue,
|
||||
selectedEventIds,
|
||||
showCheckboxes,
|
||||
showNotes,
|
||||
tabType,
|
||||
timelineId,
|
||||
toggleShowNotes,
|
||||
leadingControlColumns,
|
||||
trailingControlColumns,
|
||||
setEventsLoading,
|
||||
setEventsDeleted,
|
||||
}) => {
|
||||
// Each action button shall announce itself to screen readers via an `aria-label`
|
||||
// in the following format:
|
||||
// "button description, for the event in row {ariaRowindex}, with columns {columnValues}",
|
||||
// so we combine the column values here:
|
||||
const columnValues = useMemo(
|
||||
() =>
|
||||
columnHeaders
|
||||
.map(
|
||||
(header) =>
|
||||
getMappedNonEcsValue({
|
||||
data,
|
||||
fieldName: header.id,
|
||||
}) ?? []
|
||||
)
|
||||
.join(' '),
|
||||
[columnHeaders, data]
|
||||
);
|
||||
|
||||
const leadingActionCells = useMemo(
|
||||
() =>
|
||||
leadingControlColumns ? leadingControlColumns.map((column) => column.rowCellRender) : [],
|
||||
[leadingControlColumns]
|
||||
);
|
||||
const LeadingActions = useMemo(
|
||||
() =>
|
||||
leadingActionCells.map((Action: RowCellRender | undefined, index) => {
|
||||
const width = leadingControlColumns[index].width
|
||||
? leadingControlColumns[index].width
|
||||
: actionsColumnWidth;
|
||||
return (
|
||||
<EventsTdGroupActions
|
||||
width={width}
|
||||
data-test-subj="event-actions-container"
|
||||
tabIndex={0}
|
||||
key={index}
|
||||
>
|
||||
{Action && (
|
||||
<Action
|
||||
width={width}
|
||||
rowIndex={ariaRowindex}
|
||||
ariaRowindex={ariaRowindex}
|
||||
checked={Object.keys(selectedEventIds).includes(id)}
|
||||
columnId={leadingControlColumns[index].id || ''}
|
||||
columnValues={columnValues}
|
||||
onRowSelected={onRowSelected}
|
||||
data-test-subj="actions"
|
||||
eventId={id}
|
||||
data={data}
|
||||
index={index}
|
||||
ecsData={ecsData}
|
||||
loadingEventIds={loadingEventIds}
|
||||
onEventDetailsPanelOpened={onEventDetailsPanelOpened}
|
||||
showCheckboxes={showCheckboxes}
|
||||
eventIdToNoteIds={eventIdToNoteIds}
|
||||
isEventPinned={isEventPinned}
|
||||
isEventViewer={isEventViewer}
|
||||
onRuleChange={onRuleChange}
|
||||
refetch={refetch}
|
||||
showNotes={showNotes}
|
||||
tabType={tabType}
|
||||
timelineId={timelineId}
|
||||
toggleShowNotes={toggleShowNotes}
|
||||
setEventsLoading={setEventsLoading}
|
||||
setEventsDeleted={setEventsDeleted}
|
||||
disablePinAction={false}
|
||||
/>
|
||||
)}
|
||||
</EventsTdGroupActions>
|
||||
);
|
||||
}),
|
||||
[
|
||||
actionsColumnWidth,
|
||||
ariaRowindex,
|
||||
columnValues,
|
||||
data,
|
||||
ecsData,
|
||||
eventIdToNoteIds,
|
||||
id,
|
||||
isEventPinned,
|
||||
isEventViewer,
|
||||
leadingActionCells,
|
||||
leadingControlColumns,
|
||||
loadingEventIds,
|
||||
onEventDetailsPanelOpened,
|
||||
onRowSelected,
|
||||
onRuleChange,
|
||||
refetch,
|
||||
selectedEventIds,
|
||||
showCheckboxes,
|
||||
tabType,
|
||||
timelineId,
|
||||
toggleShowNotes,
|
||||
setEventsLoading,
|
||||
setEventsDeleted,
|
||||
showNotes,
|
||||
]
|
||||
);
|
||||
return (
|
||||
<EventsTrData data-test-subj="event-column-view">
|
||||
{LeadingActions}
|
||||
<DataDrivenColumns
|
||||
id={id}
|
||||
actionsColumnWidth={actionsColumnWidth}
|
||||
ariaRowindex={ariaRowindex}
|
||||
columnHeaders={columnHeaders}
|
||||
data={data}
|
||||
ecsData={ecsData}
|
||||
hasRowRenderers={hasRowRenderers}
|
||||
notesCount={notesCount}
|
||||
renderCellValue={renderCellValue}
|
||||
tabType={tabType}
|
||||
timelineId={timelineId}
|
||||
trailingControlColumns={trailingControlColumns}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
checked={Object.keys(selectedEventIds).includes(id)}
|
||||
columnValues={columnValues}
|
||||
onRowSelected={onRowSelected}
|
||||
data-test-subj="actions"
|
||||
loadingEventIds={loadingEventIds}
|
||||
onEventDetailsPanelOpened={onEventDetailsPanelOpened}
|
||||
showCheckboxes={showCheckboxes}
|
||||
eventIdToNoteIds={eventIdToNoteIds}
|
||||
isEventPinned={isEventPinned}
|
||||
isEventViewer={isEventViewer}
|
||||
refetch={refetch}
|
||||
onRuleChange={onRuleChange}
|
||||
selectedEventIds={selectedEventIds}
|
||||
showNotes={showNotes}
|
||||
toggleShowNotes={toggleShowNotes}
|
||||
setEventsLoading={setEventsLoading}
|
||||
setEventsDeleted={setEventsDeleted}
|
||||
/>
|
||||
</EventsTrData>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
EventColumnView.displayName = 'EventColumnView';
|
|
@ -1,111 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import type { ControlColumnProps } from '../../../../../../common/types';
|
||||
import type { inputsModel } from '../../../../../common/store';
|
||||
import type {
|
||||
TimelineItem,
|
||||
TimelineNonEcsData,
|
||||
} from '../../../../../../common/search_strategy/timeline';
|
||||
import type {
|
||||
ColumnHeaderOptions,
|
||||
CellValueElementProps,
|
||||
RowRenderer,
|
||||
TimelineTabs,
|
||||
} from '../../../../../../common/types/timeline';
|
||||
import type { OnRowSelected } from '../../events';
|
||||
import { EventsTbody } from '../../styles';
|
||||
import { StatefulEvent } from './stateful_event';
|
||||
import { eventIsPinned } from '../helpers';
|
||||
|
||||
/** This offset begins at two, because the header row counts as "row 1", and aria-rowindex starts at "1" */
|
||||
const ARIA_ROW_INDEX_OFFSET = 2;
|
||||
|
||||
interface Props {
|
||||
actionsColumnWidth: number;
|
||||
columnHeaders: ColumnHeaderOptions[];
|
||||
containerRef: React.MutableRefObject<HTMLDivElement | null>;
|
||||
data: TimelineItem[];
|
||||
eventIdToNoteIds: Readonly<Record<string, string[]>>;
|
||||
id: string;
|
||||
isEventViewer?: boolean;
|
||||
lastFocusedAriaColindex: number;
|
||||
loadingEventIds: Readonly<string[]>;
|
||||
onRowSelected: OnRowSelected;
|
||||
pinnedEventIds: Readonly<Record<string, boolean>>;
|
||||
refetch: inputsModel.Refetch;
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
onRuleChange?: () => void;
|
||||
rowRenderers: RowRenderer[];
|
||||
selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>;
|
||||
showCheckboxes: boolean;
|
||||
tabType?: TimelineTabs;
|
||||
leadingControlColumns: ControlColumnProps[];
|
||||
trailingControlColumns: ControlColumnProps[];
|
||||
onToggleShowNotes?: (eventId?: string) => void;
|
||||
}
|
||||
|
||||
const EventsComponent: React.FC<Props> = ({
|
||||
actionsColumnWidth,
|
||||
columnHeaders,
|
||||
containerRef,
|
||||
data,
|
||||
eventIdToNoteIds,
|
||||
id,
|
||||
isEventViewer = false,
|
||||
lastFocusedAriaColindex,
|
||||
loadingEventIds,
|
||||
onRowSelected,
|
||||
pinnedEventIds,
|
||||
refetch,
|
||||
onRuleChange,
|
||||
renderCellValue,
|
||||
rowRenderers,
|
||||
selectedEventIds,
|
||||
showCheckboxes,
|
||||
tabType,
|
||||
leadingControlColumns,
|
||||
trailingControlColumns,
|
||||
onToggleShowNotes,
|
||||
}) => (
|
||||
<EventsTbody data-test-subj="events">
|
||||
{data.map((event, i) => (
|
||||
<StatefulEvent
|
||||
actionsColumnWidth={actionsColumnWidth}
|
||||
ariaRowindex={i + ARIA_ROW_INDEX_OFFSET}
|
||||
columnHeaders={columnHeaders}
|
||||
containerRef={containerRef}
|
||||
event={event}
|
||||
eventIdToNoteIds={eventIdToNoteIds}
|
||||
isEventPinned={eventIsPinned({ eventId: event._id, pinnedEventIds })}
|
||||
isEventViewer={isEventViewer}
|
||||
key={`${id}_${tabType}_${event._id}_${event._index}_${
|
||||
!isEmpty(event.ecs.eql?.sequenceNumber) ? event.ecs.eql?.sequenceNumber : ''
|
||||
}`}
|
||||
lastFocusedAriaColindex={lastFocusedAriaColindex}
|
||||
loadingEventIds={loadingEventIds}
|
||||
onRowSelected={onRowSelected}
|
||||
renderCellValue={renderCellValue}
|
||||
refetch={refetch}
|
||||
rowRenderers={rowRenderers}
|
||||
onRuleChange={onRuleChange}
|
||||
selectedEventIds={selectedEventIds}
|
||||
showCheckboxes={showCheckboxes}
|
||||
tabType={tabType}
|
||||
timelineId={id}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
trailingControlColumns={trailingControlColumns}
|
||||
onToggleShowNotes={onToggleShowNotes}
|
||||
/>
|
||||
))}
|
||||
</EventsTbody>
|
||||
);
|
||||
|
||||
export const Events = React.memo(EventsComponent);
|
|
@ -1,266 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { isEventBuildingBlockType } from '@kbn/securitysolution-data-table';
|
||||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import { DocumentDetailsRightPanelKey } from '../../../../../flyout/document_details/shared/constants/panel_keys';
|
||||
import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
|
||||
import { useKibana } from '../../../../../common/lib/kibana';
|
||||
import type {
|
||||
ColumnHeaderOptions,
|
||||
CellValueElementProps,
|
||||
RowRenderer,
|
||||
TimelineTabs,
|
||||
} from '../../../../../../common/types/timeline';
|
||||
import type {
|
||||
TimelineItem,
|
||||
TimelineNonEcsData,
|
||||
} from '../../../../../../common/search_strategy/timeline';
|
||||
import type { OnRowSelected } from '../../events';
|
||||
import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../helpers';
|
||||
import { EventsTrGroup, EventsTrSupplement, EventsTrSupplementContainer } from '../../styles';
|
||||
import { getEventType, isEvenEqlSequence } from '../helpers';
|
||||
import { useEventDetailsWidthContext } from '../../../../../common/components/events_viewer/event_details_width_context';
|
||||
import { EventColumnView } from './event_column_view';
|
||||
import type { inputsModel } from '../../../../../common/store';
|
||||
import { appSelectors } from '../../../../../common/store';
|
||||
import { timelineActions } from '../../../../store';
|
||||
import type { TimelineResultNote } from '../../../open_timeline/types';
|
||||
import { getRowRenderer } from '../renderers/get_row_renderer';
|
||||
import { StatefulRowRenderer } from './stateful_row_renderer';
|
||||
import { NOTES_BUTTON_CLASS_NAME } from '../../properties/helpers';
|
||||
import { StatefulEventContext } from '../../../../../common/components/events_viewer/stateful_event_context';
|
||||
import type {
|
||||
ControlColumnProps,
|
||||
SetEventsDeleted,
|
||||
SetEventsLoading,
|
||||
} from '../../../../../../common/types';
|
||||
|
||||
interface Props {
|
||||
actionsColumnWidth: number;
|
||||
containerRef: React.MutableRefObject<HTMLDivElement | null>;
|
||||
columnHeaders: ColumnHeaderOptions[];
|
||||
event: TimelineItem;
|
||||
eventIdToNoteIds: Readonly<Record<string, string[]>>;
|
||||
isEventViewer?: boolean;
|
||||
lastFocusedAriaColindex: number;
|
||||
loadingEventIds: Readonly<string[]>;
|
||||
onRowSelected: OnRowSelected;
|
||||
isEventPinned: boolean;
|
||||
refetch: inputsModel.Refetch;
|
||||
ariaRowindex: number;
|
||||
onRuleChange?: () => void;
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
rowRenderers: RowRenderer[];
|
||||
selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>;
|
||||
showCheckboxes: boolean;
|
||||
tabType?: TimelineTabs;
|
||||
timelineId: string;
|
||||
leadingControlColumns: ControlColumnProps[];
|
||||
trailingControlColumns: ControlColumnProps[];
|
||||
onToggleShowNotes?: (eventId?: string) => void;
|
||||
}
|
||||
|
||||
const emptyNotes: string[] = [];
|
||||
|
||||
const EventsTrSupplementContainerWrapper = React.memo<PropsWithChildren<unknown>>(
|
||||
({ children }) => {
|
||||
const width = useEventDetailsWidthContext();
|
||||
return <EventsTrSupplementContainer width={width}>{children}</EventsTrSupplementContainer>;
|
||||
}
|
||||
);
|
||||
|
||||
EventsTrSupplementContainerWrapper.displayName = 'EventsTrSupplementContainerWrapper';
|
||||
|
||||
const StatefulEventComponent: React.FC<Props> = ({
|
||||
actionsColumnWidth,
|
||||
containerRef,
|
||||
columnHeaders,
|
||||
event,
|
||||
eventIdToNoteIds,
|
||||
isEventViewer = false,
|
||||
isEventPinned = false,
|
||||
lastFocusedAriaColindex,
|
||||
loadingEventIds,
|
||||
onRowSelected,
|
||||
refetch,
|
||||
renderCellValue,
|
||||
rowRenderers,
|
||||
onRuleChange,
|
||||
ariaRowindex,
|
||||
selectedEventIds,
|
||||
showCheckboxes,
|
||||
tabType,
|
||||
timelineId,
|
||||
leadingControlColumns,
|
||||
trailingControlColumns,
|
||||
onToggleShowNotes,
|
||||
}) => {
|
||||
const { telemetry } = useKibana().services;
|
||||
const trGroupRef = useRef<HTMLDivElement | null>(null);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { openFlyout } = useExpandableFlyoutApi();
|
||||
|
||||
// Store context in state rather than creating object in provider value={} to prevent re-renders caused by a new object being created
|
||||
const [activeStatefulEventContext] = useState({
|
||||
timelineID: timelineId,
|
||||
enableHostDetailsFlyout: true,
|
||||
enableIpDetailsFlyout: true,
|
||||
tabType,
|
||||
});
|
||||
|
||||
const [, setFocusedNotes] = useState<{ [eventId: string]: boolean }>({});
|
||||
|
||||
const eventId = event._id;
|
||||
|
||||
const isDetailPanelExpanded: boolean = false;
|
||||
|
||||
const getNotesByIds = useMemo(() => appSelectors.notesByIdsSelector(), []);
|
||||
const notesById = useDeepEqualSelector(getNotesByIds);
|
||||
const noteIds: string[] = eventIdToNoteIds[eventId] || emptyNotes;
|
||||
|
||||
const notes: TimelineResultNote[] = useMemo(
|
||||
() =>
|
||||
appSelectors.getNotes(notesById, noteIds).map((note) => ({
|
||||
savedObjectId: note.saveObjectId,
|
||||
note: note.note,
|
||||
noteId: note.id,
|
||||
updated: (note.lastEdit ?? note.created).getTime(),
|
||||
updatedBy: note.user,
|
||||
})),
|
||||
[notesById, noteIds]
|
||||
);
|
||||
|
||||
const hasRowRenderers: boolean = useMemo(
|
||||
() => getRowRenderer({ data: event.ecs, rowRenderers }) != null,
|
||||
[event.ecs, rowRenderers]
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const indexName = event._index!;
|
||||
|
||||
const onToggleShowNotesHandler = useCallback(
|
||||
(currentEventId?: string) => {
|
||||
onToggleShowNotes?.(currentEventId);
|
||||
setFocusedNotes((prevShowNotes) => {
|
||||
if (prevShowNotes[eventId]) {
|
||||
// notes are closing, so focus the notes button on the next tick, after escaping the EuiFocusTrap
|
||||
setTimeout(() => {
|
||||
const notesButtonElement = trGroupRef.current?.querySelector<HTMLButtonElement>(
|
||||
`.${NOTES_BUTTON_CLASS_NAME}`
|
||||
);
|
||||
notesButtonElement?.focus();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
return { ...prevShowNotes, [eventId]: !prevShowNotes[eventId] };
|
||||
});
|
||||
},
|
||||
[onToggleShowNotes, eventId]
|
||||
);
|
||||
|
||||
const handleOnEventDetailPanelOpened = useCallback(() => {
|
||||
openFlyout({
|
||||
right: {
|
||||
id: DocumentDetailsRightPanelKey,
|
||||
params: {
|
||||
id: eventId,
|
||||
indexName,
|
||||
scopeId: timelineId,
|
||||
},
|
||||
},
|
||||
});
|
||||
telemetry.reportDetailsFlyoutOpened({
|
||||
location: timelineId,
|
||||
panel: 'right',
|
||||
});
|
||||
}, [eventId, indexName, openFlyout, timelineId, telemetry]);
|
||||
|
||||
const setEventsLoading = useCallback<SetEventsLoading>(
|
||||
({ eventIds, isLoading }) => {
|
||||
dispatch(timelineActions.setEventsLoading({ id: timelineId, eventIds, isLoading }));
|
||||
},
|
||||
[dispatch, timelineId]
|
||||
);
|
||||
|
||||
const setEventsDeleted = useCallback<SetEventsDeleted>(
|
||||
({ eventIds, isDeleted }) => {
|
||||
dispatch(timelineActions.setEventsDeleted({ id: timelineId, eventIds, isDeleted }));
|
||||
},
|
||||
[dispatch, timelineId]
|
||||
);
|
||||
|
||||
return (
|
||||
<StatefulEventContext.Provider value={activeStatefulEventContext}>
|
||||
<EventsTrGroup
|
||||
$ariaRowindex={ariaRowindex}
|
||||
className={STATEFUL_EVENT_CSS_CLASS_NAME}
|
||||
data-test-subj="event"
|
||||
eventType={getEventType(event.ecs)}
|
||||
isBuildingBlockType={isEventBuildingBlockType(event.ecs)}
|
||||
isEvenEqlSequence={isEvenEqlSequence(event.ecs)}
|
||||
isExpanded={isDetailPanelExpanded}
|
||||
ref={trGroupRef}
|
||||
showLeftBorder={!isEventViewer}
|
||||
>
|
||||
<EventColumnView
|
||||
id={eventId}
|
||||
actionsColumnWidth={actionsColumnWidth}
|
||||
ariaRowindex={ariaRowindex}
|
||||
columnHeaders={columnHeaders}
|
||||
data={event.data}
|
||||
ecsData={event.ecs}
|
||||
eventIdToNoteIds={eventIdToNoteIds}
|
||||
hasRowRenderers={hasRowRenderers}
|
||||
isEventPinned={isEventPinned}
|
||||
isEventViewer={isEventViewer}
|
||||
loadingEventIds={loadingEventIds}
|
||||
notesCount={notes.length}
|
||||
onEventDetailsPanelOpened={handleOnEventDetailPanelOpened}
|
||||
onRowSelected={onRowSelected}
|
||||
refetch={refetch}
|
||||
renderCellValue={renderCellValue}
|
||||
onRuleChange={onRuleChange}
|
||||
selectedEventIds={selectedEventIds}
|
||||
showCheckboxes={showCheckboxes}
|
||||
showNotes={true}
|
||||
tabType={tabType}
|
||||
timelineId={timelineId}
|
||||
toggleShowNotes={onToggleShowNotesHandler}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
trailingControlColumns={trailingControlColumns}
|
||||
setEventsLoading={setEventsLoading}
|
||||
setEventsDeleted={setEventsDeleted}
|
||||
/>
|
||||
|
||||
<EventsTrSupplementContainerWrapper>
|
||||
<EuiFlexGroup gutterSize="none" justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EventsTrSupplement>
|
||||
<StatefulRowRenderer
|
||||
ariaRowindex={ariaRowindex}
|
||||
containerRef={containerRef}
|
||||
event={event}
|
||||
lastFocusedAriaColindex={lastFocusedAriaColindex}
|
||||
rowRenderers={rowRenderers}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</EventsTrSupplement>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EventsTrSupplementContainerWrapper>
|
||||
</EventsTrGroup>
|
||||
</StatefulEventContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const StatefulEvent = React.memo(StatefulEventComponent);
|
|
@ -1,407 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mount, type ComponentType as EnzymeComponentType } from 'enzyme';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
import { useKibana, useCurrentUser } from '../../../../common/lib/kibana';
|
||||
import { DefaultCellRenderer } from '../cell_rendering/default_cell_renderer';
|
||||
import { mockBrowserFields } from '../../../../common/containers/source/mock';
|
||||
import { Direction } from '../../../../../common/search_strategy';
|
||||
import {
|
||||
defaultHeaders,
|
||||
mockGlobalState,
|
||||
mockTimelineData,
|
||||
createMockStore,
|
||||
TestProviders,
|
||||
} from '../../../../common/mock';
|
||||
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
|
||||
import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock';
|
||||
|
||||
import type { Props } from '.';
|
||||
import { StatefulBody } from '.';
|
||||
import type { Sort } from './sort';
|
||||
import { getDefaultControlColumn } from './control_columns';
|
||||
import { TimelineId, TimelineTabs } from '../../../../../common/types/timeline';
|
||||
import { defaultRowRenderers } from './renderers';
|
||||
import type { State } from '../../../../common/store';
|
||||
import type { UseFieldBrowserOptionsProps } from '../../fields_browser';
|
||||
import type {
|
||||
DraggableProvided,
|
||||
DraggableStateSnapshot,
|
||||
DroppableProvided,
|
||||
DroppableStateSnapshot,
|
||||
} from '@hello-pangea/dnd';
|
||||
import { DocumentDetailsRightPanelKey } from '../../../../flyout/document_details/shared/constants/panel_keys';
|
||||
import { createTelemetryServiceMock } from '../../../../common/lib/telemetry/telemetry_service.mock';
|
||||
import { createExpandableFlyoutApiMock } from '../../../../common/mock/expandable_flyout';
|
||||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
|
||||
jest.mock('../../../../common/hooks/use_app_toasts');
|
||||
jest.mock('../../../../common/components/guided_onboarding_tour/tour_step');
|
||||
jest.mock(
|
||||
'../../../../detections/components/alerts_table/timeline_actions/use_add_to_case_actions'
|
||||
);
|
||||
|
||||
jest.mock('../../../../common/hooks/use_upselling', () => ({
|
||||
useUpsellingMessage: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../common/components/user_privileges', () => {
|
||||
return {
|
||||
useUserPrivileges: () => ({
|
||||
listPrivileges: { loading: false, error: undefined, result: undefined },
|
||||
detectionEnginePrivileges: { loading: false, error: undefined, result: undefined },
|
||||
endpointPrivileges: {},
|
||||
kibanaSecuritySolutionsPrivileges: { crud: true, read: true },
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const mockUseFieldBrowserOptions = jest.fn();
|
||||
const mockUseKibana = useKibana as jest.Mock;
|
||||
const mockUseCurrentUser = useCurrentUser as jest.Mock<Partial<ReturnType<typeof useCurrentUser>>>;
|
||||
const mockCasesContract = jest.requireActual('@kbn/cases-plugin/public/mocks');
|
||||
jest.mock('../../fields_browser', () => ({
|
||||
useFieldBrowserOptions: (props: UseFieldBrowserOptionsProps) => mockUseFieldBrowserOptions(props),
|
||||
}));
|
||||
|
||||
const useAddToTimeline = () => ({
|
||||
beginDrag: jest.fn(),
|
||||
cancelDrag: jest.fn(),
|
||||
dragToLocation: jest.fn(),
|
||||
endDrag: jest.fn(),
|
||||
hasDraggableLock: jest.fn(),
|
||||
startDragToTimeline: jest.fn(),
|
||||
});
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
const mockSort: Sort[] = [
|
||||
{
|
||||
columnId: '@timestamp',
|
||||
columnType: 'date',
|
||||
esTypes: ['date'],
|
||||
sortDirection: Direction.desc,
|
||||
},
|
||||
];
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => {
|
||||
const original = jest.requireActual('react-redux');
|
||||
|
||||
return {
|
||||
...original,
|
||||
useDispatch: () => mockDispatch,
|
||||
};
|
||||
});
|
||||
|
||||
const mockOpenFlyout = jest.fn();
|
||||
jest.mock('@kbn/expandable-flyout');
|
||||
|
||||
const mockedTelemetry = createTelemetryServiceMock();
|
||||
|
||||
jest.mock('../../../../common/components/link_to', () => {
|
||||
const originalModule = jest.requireActual('../../../../common/components/link_to');
|
||||
return {
|
||||
...originalModule,
|
||||
useGetSecuritySolutionUrl: () =>
|
||||
jest.fn(({ deepLinkId }: { deepLinkId: string }) => `/${deepLinkId}`),
|
||||
useNavigateTo: () => {
|
||||
return { navigateTo: jest.fn() };
|
||||
},
|
||||
useAppUrl: () => {
|
||||
return { getAppUrl: jest.fn() };
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../../common/components/links', () => {
|
||||
const originalModule = jest.requireActual('../../../../common/components/links');
|
||||
return {
|
||||
...originalModule,
|
||||
useGetSecuritySolutionUrl: () =>
|
||||
jest.fn(({ deepLinkId }: { deepLinkId: string }) => `/${deepLinkId}`),
|
||||
useNavigateTo: () => {
|
||||
return { navigateTo: jest.fn() };
|
||||
},
|
||||
useAppUrl: () => {
|
||||
return { getAppUrl: jest.fn() };
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Prevent Resolver from rendering
|
||||
jest.mock('../../graph_overlay');
|
||||
|
||||
jest.mock('../../fields_browser/create_field_button', () => ({
|
||||
useCreateFieldButton: () => <></>,
|
||||
}));
|
||||
|
||||
jest.mock('@elastic/eui', () => {
|
||||
const original = jest.requireActual('@elastic/eui');
|
||||
return {
|
||||
...original,
|
||||
EuiScreenReaderOnly: () => <></>,
|
||||
};
|
||||
});
|
||||
jest.mock('suricata-sid-db', () => {
|
||||
return {
|
||||
db: [],
|
||||
};
|
||||
});
|
||||
jest.mock(
|
||||
'../../../../detections/components/alerts_table/timeline_actions/use_add_to_case_actions',
|
||||
() => {
|
||||
return {
|
||||
useAddToCaseActions: () => {
|
||||
return {
|
||||
addToCaseActionItems: [],
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
jest.mock('@hello-pangea/dnd', () => ({
|
||||
Droppable: ({
|
||||
children,
|
||||
}: {
|
||||
children: (a: DroppableProvided, b: DroppableStateSnapshot) => void;
|
||||
}) =>
|
||||
children(
|
||||
{
|
||||
droppableProps: {
|
||||
'data-rfd-droppable-context-id': '123',
|
||||
'data-rfd-droppable-id': '123',
|
||||
},
|
||||
innerRef: jest.fn(),
|
||||
placeholder: null,
|
||||
},
|
||||
{
|
||||
isDraggingOver: false,
|
||||
draggingOverWith: null,
|
||||
draggingFromThisWith: null,
|
||||
isUsingPlaceholder: false,
|
||||
}
|
||||
),
|
||||
Draggable: ({
|
||||
children,
|
||||
}: {
|
||||
children: (a: DraggableProvided, b: DraggableStateSnapshot) => void;
|
||||
}) =>
|
||||
children(
|
||||
{
|
||||
draggableProps: {
|
||||
'data-rfd-draggable-context-id': '123',
|
||||
'data-rfd-draggable-id': '123',
|
||||
},
|
||||
innerRef: jest.fn(),
|
||||
dragHandleProps: null,
|
||||
},
|
||||
{
|
||||
isDragging: false,
|
||||
isDropAnimating: false,
|
||||
isClone: false,
|
||||
dropAnimation: null,
|
||||
draggingOver: null,
|
||||
combineWith: null,
|
||||
combineTargetFor: null,
|
||||
mode: null,
|
||||
}
|
||||
),
|
||||
DragDropContext: ({ children }: { children: React.ReactNode }) => children,
|
||||
}));
|
||||
|
||||
describe('Body', () => {
|
||||
const getWrapper = async (
|
||||
childrenComponent: JSX.Element,
|
||||
store?: { store: ReturnType<typeof createMockStore> }
|
||||
) => {
|
||||
const wrapper = mount(childrenComponent, {
|
||||
wrappingComponent: TestProviders as EnzymeComponentType<{}>,
|
||||
wrappingComponentProps: store ?? {},
|
||||
});
|
||||
await waitFor(() => wrapper.find('[data-test-subj="suricataRefs"]').exists());
|
||||
|
||||
return wrapper;
|
||||
};
|
||||
const mockRefetch = jest.fn();
|
||||
let appToastsMock: jest.Mocked<ReturnType<typeof useAppToastsMock.create>>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mocked(useExpandableFlyoutApi).mockReturnValue({
|
||||
...createExpandableFlyoutApiMock(),
|
||||
openFlyout: mockOpenFlyout,
|
||||
});
|
||||
|
||||
mockUseCurrentUser.mockReturnValue({ username: 'test-username' });
|
||||
mockUseKibana.mockReturnValue({
|
||||
services: {
|
||||
application: {
|
||||
navigateToApp: jest.fn(),
|
||||
getUrlForApp: jest.fn(),
|
||||
capabilities: {
|
||||
siem: { crud_alerts: true, read_alerts: true },
|
||||
},
|
||||
},
|
||||
cases: mockCasesContract.mockCasesContract(),
|
||||
data: {
|
||||
search: jest.fn(),
|
||||
query: jest.fn(),
|
||||
dataViews: jest.fn(),
|
||||
},
|
||||
uiSettings: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
savedObjects: {
|
||||
client: {},
|
||||
},
|
||||
telemetry: mockedTelemetry,
|
||||
timelines: {
|
||||
getLastUpdated: jest.fn(),
|
||||
getLoadingPanel: jest.fn(),
|
||||
getFieldBrowser: jest.fn(),
|
||||
getUseAddToTimeline: () => useAddToTimeline,
|
||||
},
|
||||
},
|
||||
useNavigateTo: jest.fn().mockReturnValue({
|
||||
navigateTo: jest.fn(),
|
||||
}),
|
||||
});
|
||||
appToastsMock = useAppToastsMock.create();
|
||||
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
|
||||
});
|
||||
|
||||
const ACTION_BUTTON_COUNT = 4;
|
||||
|
||||
const props: Props = {
|
||||
activePage: 0,
|
||||
browserFields: mockBrowserFields,
|
||||
data: [mockTimelineData[0]],
|
||||
id: TimelineId.test,
|
||||
refetch: mockRefetch,
|
||||
renderCellValue: DefaultCellRenderer,
|
||||
rowRenderers: defaultRowRenderers,
|
||||
sort: mockSort,
|
||||
tabType: TimelineTabs.query,
|
||||
totalPages: 1,
|
||||
leadingControlColumns: getDefaultControlColumn(ACTION_BUTTON_COUNT),
|
||||
trailingControlColumns: [],
|
||||
};
|
||||
|
||||
describe('rendering', () => {
|
||||
beforeEach(() => {
|
||||
mockDispatch.mockClear();
|
||||
});
|
||||
|
||||
test('it renders the column headers', async () => {
|
||||
const wrapper = await getWrapper(<StatefulBody {...props} />);
|
||||
expect(wrapper.find('[data-test-subj="column-headers"]').first().exists()).toEqual(true);
|
||||
});
|
||||
|
||||
test('it renders the scroll container', async () => {
|
||||
const wrapper = await getWrapper(<StatefulBody {...props} />);
|
||||
expect(wrapper.find('[data-test-subj="timeline-body"]').first().exists()).toEqual(true);
|
||||
});
|
||||
|
||||
test('it renders events', async () => {
|
||||
const wrapper = await getWrapper(<StatefulBody {...props} />);
|
||||
expect(wrapper.find('[data-test-subj="events"]').first().exists()).toEqual(true);
|
||||
});
|
||||
test('it renders a tooltip for timestamp', async () => {
|
||||
const headersJustTimestamp = defaultHeaders.filter((h) => h.id === '@timestamp');
|
||||
const state: State = {
|
||||
...mockGlobalState,
|
||||
timeline: {
|
||||
...mockGlobalState.timeline,
|
||||
timelineById: {
|
||||
...mockGlobalState.timeline.timelineById,
|
||||
[TimelineId.test]: {
|
||||
...mockGlobalState.timeline.timelineById[TimelineId.test],
|
||||
id: TimelineId.test,
|
||||
columns: headersJustTimestamp,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const store = createMockStore(state);
|
||||
const wrapper = await getWrapper(<StatefulBody {...props} />, { store });
|
||||
|
||||
headersJustTimestamp.forEach(() => {
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="data-driven-columns"]')
|
||||
.first()
|
||||
.find('[data-test-subj="localized-date-tool-tip"]')
|
||||
.exists()
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('event details', () => {
|
||||
beforeEach(() => {
|
||||
mockDispatch.mockReset();
|
||||
});
|
||||
|
||||
test('open the expandable flyout to show event details for query tab', async () => {
|
||||
const wrapper = await getWrapper(<StatefulBody {...props} />);
|
||||
|
||||
wrapper.find(`[data-test-subj="expand-event"]`).first().simulate('click');
|
||||
wrapper.update();
|
||||
expect(mockDispatch).not.toHaveBeenCalled();
|
||||
expect(mockOpenFlyout).toHaveBeenCalledWith({
|
||||
right: {
|
||||
id: DocumentDetailsRightPanelKey,
|
||||
params: {
|
||||
id: '1',
|
||||
indexName: undefined,
|
||||
scopeId: 'timeline-test',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('open the expandable flyout to show event details for pinned tab', async () => {
|
||||
const wrapper = await getWrapper(<StatefulBody {...props} tabType={TimelineTabs.pinned} />);
|
||||
|
||||
wrapper.find(`[data-test-subj="expand-event"]`).first().simulate('click');
|
||||
wrapper.update();
|
||||
expect(mockDispatch).not.toHaveBeenCalled();
|
||||
expect(mockOpenFlyout).toHaveBeenCalledWith({
|
||||
right: {
|
||||
id: DocumentDetailsRightPanelKey,
|
||||
params: {
|
||||
id: '1',
|
||||
indexName: undefined,
|
||||
scopeId: 'timeline-test',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('open the expandable flyout to show event details for notes tab', async () => {
|
||||
const wrapper = await getWrapper(<StatefulBody {...props} tabType={TimelineTabs.notes} />);
|
||||
|
||||
wrapper.find(`[data-test-subj="expand-event"]`).first().simulate('click');
|
||||
wrapper.update();
|
||||
expect(mockDispatch).not.toHaveBeenCalled();
|
||||
expect(mockOpenFlyout).toHaveBeenCalledWith({
|
||||
right: {
|
||||
id: DocumentDetailsRightPanelKey,
|
||||
params: {
|
||||
id: '1',
|
||||
indexName: undefined,
|
||||
scopeId: 'timeline-test',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,271 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { noop } from 'lodash/fp';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import {
|
||||
FIRST_ARIA_INDEX,
|
||||
ARIA_COLINDEX_ATTRIBUTE,
|
||||
ARIA_ROWINDEX_ATTRIBUTE,
|
||||
onKeyDownFocusHandler,
|
||||
} from '@kbn/timelines-plugin/public';
|
||||
import { getActionsColumnWidth } from '../../../../common/components/header_actions';
|
||||
import type { ControlColumnProps } from '../../../../../common/types';
|
||||
import type { CellValueElementProps } from '../cell_rendering';
|
||||
import { DEFAULT_COLUMN_MIN_WIDTH } from './constants';
|
||||
import type { RowRenderer, TimelineTabs } from '../../../../../common/types/timeline';
|
||||
import { RowRendererCount } from '../../../../../common/api/timeline';
|
||||
import type { BrowserFields } from '../../../../common/containers/source';
|
||||
import type { TimelineItem } from '../../../../../common/search_strategy/timeline';
|
||||
import type { inputsModel, State } from '../../../../common/store';
|
||||
import { timelineActions } from '../../../store';
|
||||
import type { OnRowSelected, OnSelectAll } from '../events';
|
||||
import { getColumnHeaders } from './column_headers/helpers';
|
||||
import { getEventIdToDataMapping } from './helpers';
|
||||
import type { Sort } from './sort';
|
||||
import { plainRowRenderer } from './renderers/plain_row_renderer';
|
||||
import { EventsTable, TimelineBody, TimelineBodyGlobalStyle } from '../styles';
|
||||
import { ColumnHeaders } from './column_headers';
|
||||
import { Events } from './events';
|
||||
import { useLicense } from '../../../../common/hooks/use_license';
|
||||
import { selectTimelineById } from '../../../store/selectors';
|
||||
|
||||
export interface Props {
|
||||
activePage: number;
|
||||
browserFields: BrowserFields;
|
||||
data: TimelineItem[];
|
||||
id: string;
|
||||
isEventViewer?: boolean;
|
||||
sort: Sort[];
|
||||
refetch: inputsModel.Refetch;
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
rowRenderers: RowRenderer[];
|
||||
leadingControlColumns: ControlColumnProps[];
|
||||
trailingControlColumns: ControlColumnProps[];
|
||||
tabType: TimelineTabs;
|
||||
totalPages: number;
|
||||
onRuleChange?: () => void;
|
||||
onToggleShowNotes?: (eventId?: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Body component is used everywhere timeline is used within the security application. It is the highest level component
|
||||
* that is shared across all implementations of the timeline.
|
||||
*/
|
||||
export const StatefulBody = React.memo<Props>(
|
||||
({
|
||||
activePage,
|
||||
browserFields,
|
||||
data,
|
||||
id,
|
||||
isEventViewer = false,
|
||||
onRuleChange,
|
||||
refetch,
|
||||
renderCellValue,
|
||||
rowRenderers,
|
||||
sort,
|
||||
tabType,
|
||||
totalPages,
|
||||
leadingControlColumns = [],
|
||||
trailingControlColumns = [],
|
||||
onToggleShowNotes,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const {
|
||||
columns,
|
||||
eventIdToNoteIds,
|
||||
excludedRowRendererIds,
|
||||
isSelectAllChecked,
|
||||
loadingEventIds,
|
||||
pinnedEventIds,
|
||||
selectedEventIds,
|
||||
show,
|
||||
queryFields,
|
||||
selectAll,
|
||||
} = useSelector((state: State) => selectTimelineById(state, id));
|
||||
|
||||
const columnHeaders = useMemo(
|
||||
() => getColumnHeaders(columns, browserFields),
|
||||
[browserFields, columns]
|
||||
);
|
||||
|
||||
const isEnterprisePlus = useLicense().isEnterprise();
|
||||
const ACTION_BUTTON_COUNT = isEnterprisePlus ? 6 : 5;
|
||||
|
||||
const onRowSelected: OnRowSelected = useCallback(
|
||||
({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => {
|
||||
dispatch(
|
||||
timelineActions.setSelected({
|
||||
id,
|
||||
eventIds: getEventIdToDataMapping(data, eventIds, queryFields),
|
||||
isSelected,
|
||||
isSelectAllChecked:
|
||||
isSelected && Object.keys(selectedEventIds).length + 1 === data.length,
|
||||
})
|
||||
);
|
||||
},
|
||||
[data, dispatch, id, queryFields, selectedEventIds]
|
||||
);
|
||||
|
||||
const onSelectAll: OnSelectAll = useCallback(
|
||||
({ isSelected }: { isSelected: boolean }) =>
|
||||
isSelected
|
||||
? dispatch(
|
||||
timelineActions.setSelected({
|
||||
id,
|
||||
eventIds: getEventIdToDataMapping(
|
||||
data,
|
||||
data.map((event) => event._id),
|
||||
queryFields
|
||||
),
|
||||
isSelected,
|
||||
isSelectAllChecked: isSelected,
|
||||
})
|
||||
)
|
||||
: dispatch(timelineActions.clearSelected({ id })),
|
||||
[data, dispatch, id, queryFields]
|
||||
);
|
||||
|
||||
// Sync to selectAll so parent components can select all events
|
||||
useEffect(() => {
|
||||
if (selectAll && !isSelectAllChecked) {
|
||||
onSelectAll({ isSelected: true });
|
||||
}
|
||||
}, [isSelectAllChecked, onSelectAll, selectAll]);
|
||||
|
||||
const enabledRowRenderers = useMemo(() => {
|
||||
if (excludedRowRendererIds && excludedRowRendererIds.length === RowRendererCount)
|
||||
return [plainRowRenderer];
|
||||
|
||||
if (!excludedRowRendererIds) return rowRenderers;
|
||||
|
||||
return rowRenderers.filter((rowRenderer) => !excludedRowRendererIds.includes(rowRenderer.id));
|
||||
}, [excludedRowRendererIds, rowRenderers]);
|
||||
|
||||
const actionsColumnWidth = useMemo(
|
||||
() => getActionsColumnWidth(ACTION_BUTTON_COUNT),
|
||||
[ACTION_BUTTON_COUNT]
|
||||
);
|
||||
|
||||
const columnWidths = useMemo(
|
||||
() =>
|
||||
columnHeaders.reduce(
|
||||
(totalWidth, header) => totalWidth + (header.initialWidth ?? DEFAULT_COLUMN_MIN_WIDTH),
|
||||
0
|
||||
),
|
||||
[columnHeaders]
|
||||
);
|
||||
|
||||
const leadingActionColumnsWidth = useMemo(() => {
|
||||
return leadingControlColumns
|
||||
? leadingControlColumns.reduce(
|
||||
(totalWidth, header) =>
|
||||
header.width ? totalWidth + header.width : totalWidth + actionsColumnWidth,
|
||||
0
|
||||
)
|
||||
: 0;
|
||||
}, [actionsColumnWidth, leadingControlColumns]);
|
||||
|
||||
const trailingActionColumnsWidth = useMemo(() => {
|
||||
return trailingControlColumns
|
||||
? trailingControlColumns.reduce(
|
||||
(totalWidth, header) =>
|
||||
header.width ? totalWidth + header.width : totalWidth + actionsColumnWidth,
|
||||
0
|
||||
)
|
||||
: 0;
|
||||
}, [actionsColumnWidth, trailingControlColumns]);
|
||||
|
||||
const totalWidth = useMemo(() => {
|
||||
return columnWidths + leadingActionColumnsWidth + trailingActionColumnsWidth;
|
||||
}, [columnWidths, leadingActionColumnsWidth, trailingActionColumnsWidth]);
|
||||
|
||||
const [lastFocusedAriaColindex] = useState(FIRST_ARIA_INDEX);
|
||||
|
||||
const columnCount = useMemo(() => {
|
||||
return columnHeaders.length + trailingControlColumns.length + leadingControlColumns.length;
|
||||
}, [columnHeaders, trailingControlColumns, leadingControlColumns]);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
onKeyDownFocusHandler({
|
||||
colindexAttribute: ARIA_COLINDEX_ATTRIBUTE,
|
||||
containerElement: containerRef.current,
|
||||
event: e,
|
||||
maxAriaColindex: columnHeaders.length + 1,
|
||||
maxAriaRowindex: data.length + 1,
|
||||
onColumnFocused: noop,
|
||||
rowindexAttribute: ARIA_ROWINDEX_ATTRIBUTE,
|
||||
});
|
||||
},
|
||||
[columnHeaders.length, containerRef, data.length]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TimelineBody data-test-subj="timeline-body" ref={containerRef}>
|
||||
<EventsTable
|
||||
$activePage={activePage}
|
||||
$columnCount={columnCount}
|
||||
data-test-subj={`${tabType}-events-table`}
|
||||
columnWidths={totalWidth}
|
||||
onKeyDown={onKeyDown}
|
||||
$rowCount={data.length}
|
||||
$totalPages={totalPages}
|
||||
>
|
||||
<ColumnHeaders
|
||||
actionsColumnWidth={actionsColumnWidth}
|
||||
browserFields={browserFields}
|
||||
columnHeaders={columnHeaders}
|
||||
isEventViewer={isEventViewer}
|
||||
isSelectAllChecked={isSelectAllChecked}
|
||||
onSelectAll={onSelectAll}
|
||||
show={show}
|
||||
showEventsSelect={false}
|
||||
showSelectAllCheckbox={false}
|
||||
sort={sort}
|
||||
tabType={tabType}
|
||||
timelineId={id}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
trailingControlColumns={trailingControlColumns}
|
||||
/>
|
||||
|
||||
<Events
|
||||
containerRef={containerRef}
|
||||
actionsColumnWidth={actionsColumnWidth}
|
||||
columnHeaders={columnHeaders}
|
||||
data={data}
|
||||
eventIdToNoteIds={eventIdToNoteIds}
|
||||
id={id}
|
||||
isEventViewer={isEventViewer}
|
||||
lastFocusedAriaColindex={lastFocusedAriaColindex}
|
||||
loadingEventIds={loadingEventIds}
|
||||
onRowSelected={onRowSelected}
|
||||
pinnedEventIds={pinnedEventIds}
|
||||
refetch={refetch}
|
||||
renderCellValue={renderCellValue}
|
||||
rowRenderers={enabledRowRenderers}
|
||||
onRuleChange={onRuleChange}
|
||||
selectedEventIds={selectedEventIds}
|
||||
showCheckboxes={false}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
trailingControlColumns={trailingControlColumns}
|
||||
tabType={tabType}
|
||||
onToggleShowNotes={onToggleShowNotes}
|
||||
/>
|
||||
</EventsTable>
|
||||
</TimelineBody>
|
||||
<TimelineBodyGlobalStyle />
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
StatefulBody.displayName = 'StatefulBody';
|
|
@ -1,147 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
import type { MomentUnit } from './date_ranges';
|
||||
import { getDateRange, getDates } from './date_ranges';
|
||||
|
||||
describe('dateRanges', () => {
|
||||
describe('#getDates', () => {
|
||||
test('given a unit of "year", it returns the four quarters of the year', () => {
|
||||
const unit: MomentUnit = 'year';
|
||||
const end = moment.utc('Mon, 31 Dec 2018 23:59:59 -0700');
|
||||
const current = moment.utc('Mon, 01 Jan 2018 00:00:00 -0700');
|
||||
|
||||
expect(getDates({ unit, end, current })).toEqual(
|
||||
[
|
||||
'2018-01-01T07:00:00.000Z',
|
||||
'2018-04-01T07:00:00.000Z',
|
||||
'2018-07-01T07:00:00.000Z',
|
||||
'2018-10-01T07:00:00.000Z',
|
||||
].map((d) => new Date(d))
|
||||
);
|
||||
});
|
||||
|
||||
test('given a unit of "month", it returns all the weeks of the month', () => {
|
||||
const unit: MomentUnit = 'month';
|
||||
const end = moment.utc('Wed, 31 Oct 2018 23:59:59 -0600');
|
||||
const current = moment.utc('Mon, 01 Oct 2018 00:00:00 -0600');
|
||||
|
||||
expect(getDates({ unit, end, current })).toEqual(
|
||||
[
|
||||
'2018-10-01T06:00:00.000Z',
|
||||
'2018-10-08T06:00:00.000Z',
|
||||
'2018-10-15T06:00:00.000Z',
|
||||
'2018-10-22T06:00:00.000Z',
|
||||
'2018-10-29T06:00:00.000Z',
|
||||
].map((d) => new Date(d))
|
||||
);
|
||||
});
|
||||
|
||||
test('given a unit of "week", it returns all the days of the week', () => {
|
||||
const unit: MomentUnit = 'week';
|
||||
const end = moment.utc('Sat, 27 Oct 2018 23:59:59 -0600');
|
||||
const current = moment.utc('Sun, 21 Oct 2018 00:00:00 -0600');
|
||||
|
||||
expect(getDates({ unit, end, current })).toEqual(
|
||||
[
|
||||
'2018-10-21T06:00:00.000Z',
|
||||
'2018-10-22T06:00:00.000Z',
|
||||
'2018-10-23T06:00:00.000Z',
|
||||
'2018-10-24T06:00:00.000Z',
|
||||
'2018-10-25T06:00:00.000Z',
|
||||
'2018-10-26T06:00:00.000Z',
|
||||
'2018-10-27T06:00:00.000Z',
|
||||
].map((d) => new Date(d))
|
||||
);
|
||||
});
|
||||
|
||||
test('given a unit of "day", it returns all the hours of the day', () => {
|
||||
const unit: MomentUnit = 'day';
|
||||
const end = moment.utc('Tue, 23 Oct 2018 23:59:59 -0600');
|
||||
const current = moment.utc('Tue, 23 Oct 2018 00:00:00 -0600');
|
||||
|
||||
expect(getDates({ unit, end, current })).toEqual(
|
||||
[
|
||||
'2018-10-23T06:00:00.000Z',
|
||||
'2018-10-23T07:00:00.000Z',
|
||||
'2018-10-23T08:00:00.000Z',
|
||||
'2018-10-23T09:00:00.000Z',
|
||||
'2018-10-23T10:00:00.000Z',
|
||||
'2018-10-23T11:00:00.000Z',
|
||||
'2018-10-23T12:00:00.000Z',
|
||||
'2018-10-23T13:00:00.000Z',
|
||||
'2018-10-23T14:00:00.000Z',
|
||||
'2018-10-23T15:00:00.000Z',
|
||||
'2018-10-23T16:00:00.000Z',
|
||||
'2018-10-23T17:00:00.000Z',
|
||||
'2018-10-23T18:00:00.000Z',
|
||||
'2018-10-23T19:00:00.000Z',
|
||||
'2018-10-23T20:00:00.000Z',
|
||||
'2018-10-23T21:00:00.000Z',
|
||||
'2018-10-23T22:00:00.000Z',
|
||||
'2018-10-23T23:00:00.000Z',
|
||||
'2018-10-24T00:00:00.000Z',
|
||||
'2018-10-24T01:00:00.000Z',
|
||||
'2018-10-24T02:00:00.000Z',
|
||||
'2018-10-24T03:00:00.000Z',
|
||||
'2018-10-24T04:00:00.000Z',
|
||||
'2018-10-24T05:00:00.000Z',
|
||||
].map((d) => new Date(d))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getDateRange', () => {
|
||||
let dateSpy: jest.SpyInstance<number, []>;
|
||||
|
||||
beforeEach(() => {
|
||||
dateSpy = jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => new Date(Date.UTC(2018, 10, 23)).valueOf());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
dateSpy.mockReset();
|
||||
});
|
||||
|
||||
test('given a unit of "day", it returns all the hours of the day', () => {
|
||||
const unit: MomentUnit = 'day';
|
||||
|
||||
const dates = getDateRange(unit);
|
||||
expect(dates).toEqual(
|
||||
[
|
||||
'2018-11-23T00:00:00.000Z',
|
||||
'2018-11-23T01:00:00.000Z',
|
||||
'2018-11-23T02:00:00.000Z',
|
||||
'2018-11-23T03:00:00.000Z',
|
||||
'2018-11-23T04:00:00.000Z',
|
||||
'2018-11-23T05:00:00.000Z',
|
||||
'2018-11-23T06:00:00.000Z',
|
||||
'2018-11-23T07:00:00.000Z',
|
||||
'2018-11-23T08:00:00.000Z',
|
||||
'2018-11-23T09:00:00.000Z',
|
||||
'2018-11-23T10:00:00.000Z',
|
||||
'2018-11-23T11:00:00.000Z',
|
||||
'2018-11-23T12:00:00.000Z',
|
||||
'2018-11-23T13:00:00.000Z',
|
||||
'2018-11-23T14:00:00.000Z',
|
||||
'2018-11-23T15:00:00.000Z',
|
||||
'2018-11-23T16:00:00.000Z',
|
||||
'2018-11-23T17:00:00.000Z',
|
||||
'2018-11-23T18:00:00.000Z',
|
||||
'2018-11-23T19:00:00.000Z',
|
||||
'2018-11-23T20:00:00.000Z',
|
||||
'2018-11-23T21:00:00.000Z',
|
||||
'2018-11-23T22:00:00.000Z',
|
||||
'2018-11-23T23:00:00.000Z',
|
||||
].map((d) => new Date(d))
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,76 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
export type MomentUnit = 'year' | 'month' | 'week' | 'day';
|
||||
|
||||
export type MomentIncrement = 'quarters' | 'months' | 'weeks' | 'days' | 'hours';
|
||||
|
||||
export type MomentUnitToIncrement = { [key in MomentUnit]: MomentIncrement };
|
||||
|
||||
const unitsToIncrements: MomentUnitToIncrement = {
|
||||
day: 'hours',
|
||||
month: 'weeks',
|
||||
week: 'days',
|
||||
year: 'quarters',
|
||||
};
|
||||
|
||||
interface GetDatesParams {
|
||||
unit: MomentUnit;
|
||||
end: moment.Moment;
|
||||
current: moment.Moment;
|
||||
}
|
||||
|
||||
/**
|
||||
* A pure function that given a unit (e.g. `'year' | 'month' | 'week'...`) and
|
||||
* a date range, returns a range of `Date`s with a granularity appropriate
|
||||
* to the unit.
|
||||
*
|
||||
* @example
|
||||
* test('given a unit of "year", it returns the four quarters of the year', () => {
|
||||
* const unit: MomentUnit = 'year';
|
||||
* const end = moment.utc('Mon, 31 Dec 2018 23:59:59 -0700');
|
||||
* const current = moment.utc('Mon, 01 Jan 2018 00:00:00 -0700');
|
||||
*
|
||||
* expect(getDates({ unit, end, current })).toEqual(
|
||||
* [
|
||||
* '2018-01-01T07:00:00.000Z',
|
||||
* '2018-04-01T06:00:00.000Z',
|
||||
* '2018-07-01T06:00:00.000Z',
|
||||
* '2018-10-01T06:00:00.000Z'
|
||||
* ].map(d => new Date(d))
|
||||
* );
|
||||
* });
|
||||
*/
|
||||
export const getDates = ({ unit, end, current }: GetDatesParams): Date[] =>
|
||||
current <= end
|
||||
? [
|
||||
current.toDate(),
|
||||
...getDates({
|
||||
current: current.clone().add(1, unitsToIncrements[unit]),
|
||||
end,
|
||||
unit,
|
||||
}),
|
||||
]
|
||||
: [];
|
||||
|
||||
/**
|
||||
* An impure function (it performs IO to get the current `Date`) that,
|
||||
* given a unit (e.g. `'year' | 'month' | 'week'...`), it
|
||||
* returns range of `Date`s with a granularity appropriate to the unit.
|
||||
*/
|
||||
export function getDateRange(unit: MomentUnit): Date[] {
|
||||
const current = moment().utc().startOf(unit);
|
||||
const end = moment().utc().endOf(unit);
|
||||
|
||||
return getDates({
|
||||
current,
|
||||
end, // TODO: this should be relative to `unit`
|
||||
unit,
|
||||
});
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { mockTimelineData } from '../../../../../common/mock';
|
||||
import { defaultUdtHeaders } from '../../unified_components/default_headers';
|
||||
import { defaultUdtHeaders } from '../column_headers/default_headers';
|
||||
import { getFormattedFields } from './formatted_field_udt';
|
||||
import type { DataTableRecord } from '@kbn/discover-utils/types';
|
||||
|
||||
|
|
|
@ -13,13 +13,13 @@ import type { TimelineNonEcsData } from '../../../../../../common/search_strateg
|
|||
import { mockTimelineData } from '../../../../../common/mock';
|
||||
import { TestProviders } from '../../../../../common/mock/test_providers';
|
||||
import { getEmptyValue } from '../../../../../common/components/empty_value';
|
||||
import { defaultHeaders } from '../column_headers/default_headers';
|
||||
|
||||
import { columnRenderers } from '.';
|
||||
import { getColumnRenderer } from './get_column_renderer';
|
||||
import { getValues, findItem, deleteItemIdx } from './helpers';
|
||||
import { useMountAppended } from '../../../../../common/utils/use_mount_appended';
|
||||
import { TimelineId } from '../../../../../../common/types/timeline';
|
||||
import { defaultUdtHeaders } from '../column_headers/default_headers';
|
||||
|
||||
jest.mock('../../../../../common/lib/kibana');
|
||||
|
||||
|
@ -47,7 +47,7 @@ describe('get_column_renderer', () => {
|
|||
columnName,
|
||||
eventId: _id,
|
||||
values: getValues(columnName, nonSuricata),
|
||||
field: defaultHeaders[1],
|
||||
field: defaultUdtHeaders[1],
|
||||
scopeId: TimelineId.test,
|
||||
});
|
||||
|
||||
|
@ -62,7 +62,7 @@ describe('get_column_renderer', () => {
|
|||
columnName,
|
||||
eventId: _id,
|
||||
values: getValues(columnName, nonSuricata),
|
||||
field: defaultHeaders[1],
|
||||
field: defaultUdtHeaders[1],
|
||||
scopeId: TimelineId.test,
|
||||
});
|
||||
const wrapper = mount(
|
||||
|
@ -82,7 +82,7 @@ describe('get_column_renderer', () => {
|
|||
columnName,
|
||||
eventId: _id,
|
||||
values: getValues(columnName, nonSuricata),
|
||||
field: defaultHeaders[7],
|
||||
field: defaultUdtHeaders[7],
|
||||
scopeId: TimelineId.test,
|
||||
});
|
||||
const wrapper = mount(
|
||||
|
@ -100,7 +100,7 @@ describe('get_column_renderer', () => {
|
|||
columnName,
|
||||
eventId: _id,
|
||||
values: getValues(columnName, nonSuricata),
|
||||
field: defaultHeaders[7],
|
||||
field: defaultUdtHeaders[7],
|
||||
scopeId: TimelineId.test,
|
||||
});
|
||||
const wrapper = mount(
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { mockTimelineData, TestProviders } from '../../../../../common/mock';
|
||||
import { defaultColumnHeaderType } from '../column_headers/default_headers';
|
||||
import { REASON_FIELD_NAME } from './constants';
|
||||
import { reasonColumnRenderer } from './reason_column_renderer';
|
||||
import { plainColumnRenderer } from './plain_column_renderer';
|
||||
|
@ -19,6 +18,7 @@ import { RowRendererIdEnum } from '../../../../../../common/api/timeline';
|
|||
import { render } from '@testing-library/react';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { TableId } from '@kbn/securitysolution-data-table';
|
||||
import { defaultColumnHeaderType } from '../column_headers/default_headers';
|
||||
jest.mock('./plain_column_renderer');
|
||||
|
||||
jest.mock('../../../../../common/components/link_to', () => {
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SortIndicator rendering renders correctly against snapshot 1`] = `
|
||||
<EuiToolTip
|
||||
content="Sorted descending"
|
||||
data-test-subj="sort-indicator-tooltip"
|
||||
delay="regular"
|
||||
display="inlineBlock"
|
||||
position="top"
|
||||
>
|
||||
<EuiIcon
|
||||
data-test-subj="sortIndicator"
|
||||
type="sortDown"
|
||||
/>
|
||||
<SortNumber
|
||||
sortNumber={-1}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
`;
|
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { SortColumnTimeline } from '../../../../../../common/types/timeline';
|
||||
|
||||
/** Specifies which column the timeline is sorted on */
|
||||
export type Sort = SortColumnTimeline;
|
|
@ -1,85 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { Direction } from '../../../../../../common/search_strategy';
|
||||
|
||||
import * as i18n from '../translations';
|
||||
|
||||
import { getDirection, SortIndicator } from './sort_indicator';
|
||||
|
||||
describe('SortIndicator', () => {
|
||||
describe('rendering', () => {
|
||||
test('renders correctly against snapshot', () => {
|
||||
const wrapper = shallow(<SortIndicator sortDirection={Direction.desc} sortNumber={-1} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('it renders the expected sort indicator when direction is ascending', () => {
|
||||
const wrapper = mount(<SortIndicator sortDirection={Direction.asc} sortNumber={-1} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual(
|
||||
'sortUp'
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders the expected sort indicator when direction is descending', () => {
|
||||
const wrapper = mount(<SortIndicator sortDirection={Direction.desc} sortNumber={-1} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual(
|
||||
'sortDown'
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders the expected sort indicator when direction is `none`', () => {
|
||||
const wrapper = mount(<SortIndicator sortDirection="none" sortNumber={-1} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual(
|
||||
'empty'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDirection', () => {
|
||||
test('it returns the expected symbol when the direction is ascending', () => {
|
||||
expect(getDirection(Direction.asc)).toEqual('sortUp');
|
||||
});
|
||||
|
||||
test('it returns the expected symbol when the direction is descending', () => {
|
||||
expect(getDirection(Direction.desc)).toEqual('sortDown');
|
||||
});
|
||||
|
||||
test('it returns the expected symbol (undefined) when the direction is neither ascending, nor descending', () => {
|
||||
expect(getDirection('none')).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sort indicator tooltip', () => {
|
||||
test('it returns the expected tooltip when the direction is ascending', () => {
|
||||
const wrapper = mount(<SortIndicator sortDirection={Direction.asc} sortNumber={-1} />);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="sort-indicator-tooltip"]').first().props().content
|
||||
).toEqual(i18n.SORTED_ASCENDING);
|
||||
});
|
||||
|
||||
test('it returns the expected tooltip when the direction is descending', () => {
|
||||
const wrapper = mount(<SortIndicator sortDirection={Direction.desc} sortNumber={-1} />);
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="sort-indicator-tooltip"]').first().props().content
|
||||
).toEqual(i18n.SORTED_DESCENDING);
|
||||
});
|
||||
|
||||
test('it does NOT render a tooltip when sort direction is `none`', () => {
|
||||
const wrapper = mount(<SortIndicator sortDirection="none" sortNumber={-1} />);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="sort-indicator-tooltip"]').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,68 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiIcon, EuiToolTip } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
import * as i18n from '../translations';
|
||||
import { SortNumber } from './sort_number';
|
||||
|
||||
import { Direction } from '../../../../../../common/search_strategy';
|
||||
import type { SortDirection } from '../../../../../../common/types/timeline';
|
||||
|
||||
enum SortDirectionIndicatorEnum {
|
||||
SORT_UP = 'sortUp',
|
||||
SORT_DOWN = 'sortDown',
|
||||
}
|
||||
|
||||
export type SortDirectionIndicator = undefined | SortDirectionIndicatorEnum;
|
||||
|
||||
/** Returns the symbol that corresponds to the specified `SortDirection` */
|
||||
export const getDirection = (sortDirection: SortDirection): SortDirectionIndicator => {
|
||||
switch (sortDirection) {
|
||||
case Direction.asc:
|
||||
return SortDirectionIndicatorEnum.SORT_UP;
|
||||
case Direction.desc:
|
||||
return SortDirectionIndicatorEnum.SORT_DOWN;
|
||||
case 'none':
|
||||
return undefined;
|
||||
default:
|
||||
throw new Error('Unhandled sort direction');
|
||||
}
|
||||
};
|
||||
|
||||
interface Props {
|
||||
sortDirection: SortDirection;
|
||||
sortNumber: number;
|
||||
}
|
||||
|
||||
/** Renders a sort indicator */
|
||||
export const SortIndicator = React.memo<Props>(({ sortDirection, sortNumber }) => {
|
||||
const direction = getDirection(sortDirection);
|
||||
|
||||
if (direction != null) {
|
||||
return (
|
||||
<EuiToolTip
|
||||
content={
|
||||
direction === SortDirectionIndicatorEnum.SORT_UP
|
||||
? i18n.SORTED_ASCENDING
|
||||
: i18n.SORTED_DESCENDING
|
||||
}
|
||||
data-test-subj="sort-indicator-tooltip"
|
||||
>
|
||||
<>
|
||||
<EuiIcon data-test-subj="sortIndicator" type={direction} />
|
||||
<SortNumber sortNumber={sortNumber} />
|
||||
</>
|
||||
</EuiToolTip>
|
||||
);
|
||||
} else {
|
||||
return <EuiIcon data-test-subj="sortIndicator" type={'empty'} />;
|
||||
}
|
||||
});
|
||||
|
||||
SortIndicator.displayName = 'SortIndicator';
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiIcon, EuiNotificationBadge } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
sortNumber: number;
|
||||
}
|
||||
|
||||
export const SortNumber = React.memo<Props>(({ sortNumber }) => {
|
||||
if (sortNumber >= 0) {
|
||||
return (
|
||||
<EuiNotificationBadge color="subdued" data-test-subj="sortNumber">
|
||||
{sortNumber + 1}
|
||||
</EuiNotificationBadge>
|
||||
);
|
||||
} else {
|
||||
return <EuiIcon data-test-subj="sortEmptyNumber" type={'empty'} />;
|
||||
}
|
||||
});
|
||||
|
||||
SortNumber.displayName = 'SortNumber';
|
|
@ -9,7 +9,7 @@ import { TimelineTabs } from '../../../../../common/types';
|
|||
import { DataLoadingState } from '@kbn/unified-data-table';
|
||||
import React from 'react';
|
||||
import { UnifiedTimeline } from '../unified_components';
|
||||
import { defaultUdtHeaders } from '../unified_components/default_headers';
|
||||
import { defaultUdtHeaders } from './column_headers/default_headers';
|
||||
import type { UnifiedTimelineBodyProps } from './unified_timeline_body';
|
||||
import { UnifiedTimelineBody } from './unified_timeline_body';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
|
|
@ -10,7 +10,7 @@ import React, { useEffect, useState, useMemo } from 'react';
|
|||
import { RootDragDropProvider } from '@kbn/dom-drag-drop';
|
||||
import { StyledTableFlexGroup, StyledUnifiedTableFlexItem } from '../unified_components/styles';
|
||||
import { UnifiedTimeline } from '../unified_components';
|
||||
import { defaultUdtHeaders } from '../unified_components/default_headers';
|
||||
import { defaultUdtHeaders } from './column_headers/default_headers';
|
||||
import type { PaginationInputPaginated, TimelineItem } from '../../../../../common/search_strategy';
|
||||
|
||||
export interface UnifiedTimelineBodyProps extends ComponentProps<typeof UnifiedTimeline> {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useGetMappedNonEcsValue } from '../body/data_driven_columns';
|
||||
import { useGetMappedNonEcsValue } from '../../../../common/utils/get_mapped_non_ecs_value';
|
||||
import { columnRenderers } from '../body/renderers';
|
||||
import { getColumnRenderer } from '../body/renderers/get_column_renderer';
|
||||
import type { CellValueElementProps } from '.';
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
buildIsOneOfQueryMatch,
|
||||
buildIsQueryMatch,
|
||||
handleIsOperator,
|
||||
isFullScreen,
|
||||
isPrimitiveArray,
|
||||
showGlobalFilters,
|
||||
} from './helpers';
|
||||
|
@ -392,3 +393,42 @@ describe('isStringOrNumberArray', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFullScreen', () => {
|
||||
describe('globalFullScreen is false', () => {
|
||||
it('should return false if isActiveTimelines is false', () => {
|
||||
const result = isFullScreen({
|
||||
globalFullScreen: false,
|
||||
isActiveTimelines: false,
|
||||
timelineFullScreen: true,
|
||||
});
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
it('should return false if timelineFullScreen is false', () => {
|
||||
const result = isFullScreen({
|
||||
globalFullScreen: false,
|
||||
isActiveTimelines: true,
|
||||
timelineFullScreen: false,
|
||||
});
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
describe('globalFullScreen is true', () => {
|
||||
it('should return true if isActiveTimelines is true and timelineFullScreen is true', () => {
|
||||
const result = isFullScreen({
|
||||
globalFullScreen: true,
|
||||
isActiveTimelines: true,
|
||||
timelineFullScreen: true,
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
it('should return true if isActiveTimelines is false', () => {
|
||||
const result = isFullScreen({
|
||||
globalFullScreen: true,
|
||||
isActiveTimelines: false,
|
||||
timelineFullScreen: false,
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -282,3 +282,14 @@ export const TIMELINE_FILTER_DROP_AREA = 'timeline-filter-drop-area';
|
|||
|
||||
export const getNonDropAreaFilters = (filters: Filter[] = []) =>
|
||||
filters.filter((f: Filter) => f.meta.controlledBy !== TIMELINE_FILTER_DROP_AREA);
|
||||
|
||||
export const isFullScreen = ({
|
||||
globalFullScreen,
|
||||
isActiveTimelines,
|
||||
timelineFullScreen,
|
||||
}: {
|
||||
globalFullScreen: boolean;
|
||||
isActiveTimelines: boolean;
|
||||
timelineFullScreen: boolean;
|
||||
}) =>
|
||||
(isActiveTimelines && timelineFullScreen) || (isActiveTimelines === false && globalFullScreen);
|
||||
|
|
|
@ -29,7 +29,7 @@ import { useTimelineFullScreen } from '../../../common/containers/use_full_scree
|
|||
import { EXIT_FULL_SCREEN_CLASS_NAME } from '../../../common/components/exit_full_screen';
|
||||
import { useResolveConflict } from '../../../common/hooks/use_resolve_conflict';
|
||||
import { sourcererSelectors } from '../../../common/store';
|
||||
import { defaultUdtHeaders } from './unified_components/default_headers';
|
||||
import { defaultUdtHeaders } from './body/column_headers/default_headers';
|
||||
|
||||
const TimelineTemplateBadge = styled.div`
|
||||
background: ${({ theme }) => theme.eui.euiColorVis3_behindText};
|
||||
|
|
|
@ -10,11 +10,6 @@ import { rgba } from 'polished';
|
|||
import styled, { createGlobalStyle } from 'styled-components';
|
||||
import { IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid';
|
||||
|
||||
import type { TimelineEventsType } from '../../../../common/types/timeline';
|
||||
|
||||
import { ACTIONS_COLUMN_ARIA_COL_INDEX } from './helpers';
|
||||
import { EVENTS_TABLE_ARIA_LABEL } from './translations';
|
||||
|
||||
/**
|
||||
* TIMELINE BODY
|
||||
*/
|
||||
|
@ -73,79 +68,6 @@ TimelineBody.displayName = 'TimelineBody';
|
|||
|
||||
export const EVENTS_TABLE_CLASS_NAME = 'siemEventsTable';
|
||||
|
||||
interface EventsTableProps {
|
||||
$activePage: number;
|
||||
$columnCount: number;
|
||||
columnWidths: number;
|
||||
$rowCount: number;
|
||||
$totalPages: number;
|
||||
}
|
||||
|
||||
export const EventsTable = styled.div.attrs<EventsTableProps>(
|
||||
({ className = '', $columnCount, columnWidths, $activePage, $rowCount, $totalPages }) => ({
|
||||
'aria-label': EVENTS_TABLE_ARIA_LABEL({ activePage: $activePage + 1, totalPages: $totalPages }),
|
||||
'aria-colcount': `${$columnCount}`,
|
||||
'aria-rowcount': `${$rowCount + 1}`,
|
||||
className: `siemEventsTable ${className}`,
|
||||
role: 'grid',
|
||||
style: {
|
||||
minWidth: `${columnWidths}px`,
|
||||
},
|
||||
tabindex: '-1',
|
||||
})
|
||||
)<EventsTableProps>`
|
||||
padding: 3px;
|
||||
`;
|
||||
|
||||
/* EVENTS HEAD */
|
||||
|
||||
export const EventsThead = styled.div.attrs(({ className = '' }) => ({
|
||||
className: `siemEventsTable__thead ${className}`,
|
||||
role: 'rowgroup',
|
||||
}))`
|
||||
background-color: ${({ theme }) => theme.eui.euiColorEmptyShade};
|
||||
border-bottom: ${({ theme }) => theme.eui.euiBorderWidthThick} solid
|
||||
${({ theme }) => theme.eui.euiColorLightShade};
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: ${({ theme }) => theme.eui.euiZLevel1};
|
||||
`;
|
||||
|
||||
export const EventsTrHeader = styled.div.attrs(({ className }) => ({
|
||||
'aria-rowindex': '1',
|
||||
className: `siemEventsTable__trHeader ${className}`,
|
||||
role: 'row',
|
||||
}))`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const EventsThGroupActions = styled.div.attrs(({ className = '' }) => ({
|
||||
'aria-colindex': `${ACTIONS_COLUMN_ARIA_COL_INDEX}`,
|
||||
className: `siemEventsTable__thGroupActions ${className}`,
|
||||
role: 'columnheader',
|
||||
tabIndex: '0',
|
||||
}))<{ actionsColumnWidth: number; isEventViewer: boolean }>`
|
||||
display: flex;
|
||||
flex: 0 0
|
||||
${({ actionsColumnWidth, isEventViewer }) =>
|
||||
`${!isEventViewer ? actionsColumnWidth + 4 : actionsColumnWidth}px`};
|
||||
min-width: 0;
|
||||
padding-left: ${({ isEventViewer }) =>
|
||||
!isEventViewer ? '4px;' : '0;'}; // match timeline event border
|
||||
`;
|
||||
|
||||
export const EventsThGroupData = styled.div.attrs(({ className = '' }) => ({
|
||||
className: `siemEventsTable__thGroupData ${className}`,
|
||||
}))<{ isDragging?: boolean }>`
|
||||
display: flex;
|
||||
|
||||
> div:hover .siemEventsHeading__handle {
|
||||
display: ${({ isDragging }) => (isDragging ? 'none' : 'block')};
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
`;
|
||||
|
||||
export const EventsTh = styled.div.attrs<{ role: string }>(
|
||||
({ className = '', role = 'columnheader' }) => ({
|
||||
className: `siemEventsTable__th ${className}`,
|
||||
|
@ -197,76 +119,6 @@ export const EventsThContent = styled.div.attrs(({ className = '' }) => ({
|
|||
}
|
||||
`;
|
||||
|
||||
/* EVENTS BODY */
|
||||
|
||||
export const EventsTbody = styled.div.attrs(({ className = '' }) => ({
|
||||
className: `siemEventsTable__tbody ${className}`,
|
||||
role: 'rowgroup',
|
||||
}))`
|
||||
overflow-x: hidden;
|
||||
`;
|
||||
|
||||
export const EventsTrGroup = styled.div.attrs(
|
||||
({ className = '', $ariaRowindex }: { className?: string; $ariaRowindex: number }) => ({
|
||||
'aria-rowindex': `${$ariaRowindex}`,
|
||||
className: `siemEventsTable__trGroup ${className}`,
|
||||
role: 'row',
|
||||
})
|
||||
)<{
|
||||
className?: string;
|
||||
eventType: Omit<TimelineEventsType, 'all'>;
|
||||
isEvenEqlSequence: boolean;
|
||||
isBuildingBlockType: boolean;
|
||||
isExpanded: boolean;
|
||||
showLeftBorder: boolean;
|
||||
}>`
|
||||
border-bottom: ${({ theme }) => theme.eui.euiBorderWidthThin} solid
|
||||
${({ theme }) => theme.eui.euiColorLightShade};
|
||||
${({ theme, eventType, isEvenEqlSequence, showLeftBorder }) =>
|
||||
showLeftBorder
|
||||
? `border-left: 4px solid
|
||||
${
|
||||
eventType === 'raw'
|
||||
? theme.eui.euiColorLightShade
|
||||
: eventType === 'eql' && isEvenEqlSequence
|
||||
? theme.eui.euiColorPrimary
|
||||
: eventType === 'eql' && !isEvenEqlSequence
|
||||
? theme.eui.euiColorAccent
|
||||
: theme.eui.euiColorWarning
|
||||
}`
|
||||
: ''};
|
||||
${({ isBuildingBlockType }) =>
|
||||
isBuildingBlockType
|
||||
? 'background: repeating-linear-gradient(127deg, rgba(245, 167, 0, 0.2), rgba(245, 167, 0, 0.2) 1px, rgba(245, 167, 0, 0.05) 2px, rgba(245, 167, 0, 0.05) 10px);'
|
||||
: ''};
|
||||
${({ eventType, isEvenEqlSequence }) =>
|
||||
eventType === 'eql'
|
||||
? isEvenEqlSequence
|
||||
? 'background: repeating-linear-gradient(127deg, rgba(0, 107, 180, 0.2), rgba(0, 107, 180, 0.2) 1px, rgba(0, 107, 180, 0.05) 2px, rgba(0, 107, 180, 0.05) 10px);'
|
||||
: 'background: repeating-linear-gradient(127deg, rgba(221, 10, 115, 0.2), rgba(221, 10, 115, 0.2) 1px, rgba(221, 10, 115, 0.05) 2px, rgba(221, 10, 115, 0.05) 10px);'
|
||||
: ''};
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.eui.euiTableHoverColor};
|
||||
}
|
||||
|
||||
${({ isExpanded, theme }) =>
|
||||
isExpanded &&
|
||||
`
|
||||
background: ${theme.eui.euiTableSelectedColor};
|
||||
|
||||
&:hover {
|
||||
${theme.eui.euiTableHoverSelectedColor}
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export const EventsTrData = styled.div.attrs(({ className = '' }) => ({
|
||||
className: `siemEventsTable__trData ${className}`,
|
||||
}))`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const TIMELINE_EVENT_DETAILS_OFFSET = 40;
|
||||
|
||||
interface WidthProp {
|
||||
|
@ -295,57 +147,6 @@ export const EventsTrSupplement = styled.div.attrs(({ className = '' }) => ({
|
|||
}
|
||||
`;
|
||||
|
||||
export const EventsTdGroupActions = styled.div.attrs(({ className = '' }) => ({
|
||||
'aria-colindex': `${ACTIONS_COLUMN_ARIA_COL_INDEX}`,
|
||||
className: `siemEventsTable__tdGroupActions ${className}`,
|
||||
role: 'gridcell',
|
||||
}))<{ width: number }>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 0 0 ${({ width }) => `${width}px`};
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
export const EventsTdGroupData = styled.div.attrs(({ className = '' }) => ({
|
||||
className: `siemEventsTable__tdGroupData ${className}`,
|
||||
}))`
|
||||
display: flex;
|
||||
`;
|
||||
interface EventsTdProps {
|
||||
$ariaColumnIndex?: number;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export const EVENTS_TD_CLASS_NAME = 'siemEventsTable__td';
|
||||
|
||||
export const EventsTd = styled.div.attrs<EventsTdProps>(
|
||||
({ className = '', $ariaColumnIndex, width }) => {
|
||||
const common = {
|
||||
className: `siemEventsTable__td ${className}`,
|
||||
role: 'gridcell',
|
||||
style: {
|
||||
flexBasis: width ? `${width}px` : 'auto',
|
||||
},
|
||||
};
|
||||
|
||||
return $ariaColumnIndex != null
|
||||
? {
|
||||
...common,
|
||||
'aria-colindex': `${$ariaColumnIndex}`,
|
||||
}
|
||||
: common;
|
||||
}
|
||||
)<EventsTdProps>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
|
||||
.siemEventsTable__tdGroupActions &:first-child:last-child {
|
||||
flex: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
export const EventsTdContent = styled.div.attrs(({ className }) => ({
|
||||
className: `siemEventsTable__tdContent ${className != null ? className : ''}`,
|
||||
}))<{ textAlign?: string; width?: number }>`
|
||||
|
@ -363,89 +164,9 @@ export const EventsTdContent = styled.div.attrs(({ className }) => ({
|
|||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* EVENTS HEADING
|
||||
*/
|
||||
|
||||
export const EventsHeading = styled.div.attrs(({ className = '' }) => ({
|
||||
className: `siemEventsHeading ${className}`,
|
||||
}))<{ isLoading: boolean }>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
&:hover {
|
||||
cursor: ${({ isLoading }) => (isLoading ? 'wait' : 'grab')};
|
||||
}
|
||||
`;
|
||||
|
||||
export const EventsHeadingTitleButton = styled.button.attrs(({ className = '' }) => ({
|
||||
className: `siemEventsHeading__title siemEventsHeading__title--aggregatable ${className}`,
|
||||
type: 'button',
|
||||
}))`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-weight: inherit;
|
||||
min-width: 0;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: ${({ theme }) => theme.eui.euiColorPrimary};
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
& > * + * {
|
||||
margin-left: ${({ theme }) => theme.eui.euiSizeXS};
|
||||
}
|
||||
`;
|
||||
|
||||
export const EventsHeadingTitleSpan = styled.span.attrs(({ className }) => ({
|
||||
className: `siemEventsHeading__title siemEventsHeading__title--notAggregatable ${className}`,
|
||||
}))`
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
export const EventsHeadingExtra = styled.div.attrs(({ className = '' }) => ({
|
||||
className: `siemEventsHeading__extra ${className}` as string,
|
||||
}))`
|
||||
margin-left: auto;
|
||||
margin-right: 2px;
|
||||
|
||||
&.siemEventsHeading__extra--close {
|
||||
opacity: 0;
|
||||
transition: all ${({ theme }) => theme.eui.euiAnimSpeedNormal} ease;
|
||||
visibility: hidden;
|
||||
|
||||
.siemEventsTable__th:hover & {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const EventsHeadingHandle = styled.div.attrs(({ className = '' }) => ({
|
||||
className: `siemEventsHeading__handle ${className}`,
|
||||
}))`
|
||||
background-color: ${({ theme }) => theme.eui.euiBorderColor};
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
transition: all ${({ theme }) => theme.eui.euiAnimSpeedNormal} ease;
|
||||
visibility: hidden;
|
||||
width: ${({ theme }) => theme.eui.euiBorderWidthThick};
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.eui.euiColorPrimary};
|
||||
cursor: col-resize;
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* EVENTS LOADING
|
||||
*/
|
||||
|
||||
export const EventsLoading = styled(EuiLoadingSpinner)`
|
||||
margin: 0 2px;
|
||||
vertical-align: middle;
|
||||
|
|
|
@ -35,9 +35,6 @@ jest.mock('../../../../containers/details', () => ({
|
|||
jest.mock('../../../fields_browser', () => ({
|
||||
useFieldBrowserOptions: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../body/events', () => ({
|
||||
Events: () => <></>,
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../sourcerer/containers');
|
||||
jest.mock('../../../../../sourcerer/containers/use_signal_helpers', () => ({
|
||||
|
|
|
@ -14,7 +14,7 @@ import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer'
|
|||
import { defaultHeaders, mockTimelineData } from '../../../../../common/mock';
|
||||
import { TestProviders } from '../../../../../common/mock/test_providers';
|
||||
import { defaultRowRenderers } from '../../body/renderers';
|
||||
import type { Sort } from '../../body/sort';
|
||||
import type { SortColumnTimeline as Sort } from '../../../../../../common/types/timeline';
|
||||
import { TimelineId } from '../../../../../../common/types/timeline';
|
||||
import { useTimelineEvents } from '../../../../containers';
|
||||
import { useTimelineEventsDetails } from '../../../../containers/details';
|
||||
|
@ -38,9 +38,6 @@ jest.mock('../../../../containers/details', () => ({
|
|||
jest.mock('../../../fields_browser', () => ({
|
||||
useFieldBrowserOptions: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../body/events', () => ({
|
||||
Events: () => <></>,
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../sourcerer/containers');
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@ import { useKibana } from '../../../../../common/lib/kibana';
|
|||
import { timelineSelectors } from '../../../../store';
|
||||
import type { Direction } from '../../../../../../common/search_strategy';
|
||||
import { useTimelineEvents } from '../../../../containers';
|
||||
import { defaultHeaders } from '../../body/column_headers/default_headers';
|
||||
import { requiredFieldsForActions } from '../../../../../detections/components/alerts_table/default_config';
|
||||
import { SourcererScopeName } from '../../../../../sourcerer/store/model';
|
||||
import { timelineDefaults } from '../../../../store/defaults';
|
||||
|
@ -37,6 +36,7 @@ import { useTimelineControlColumn } from '../shared/use_timeline_control_columns
|
|||
import { LeftPanelNotesTab } from '../../../../../flyout/document_details/left';
|
||||
import { useNotesInFlyout } from '../../properties/use_notes_in_flyout';
|
||||
import { NotesFlyout } from '../../properties/notes_flyout';
|
||||
import { defaultUdtHeaders } from '../../body/column_headers/default_headers';
|
||||
|
||||
interface PinnedFilter {
|
||||
bool: {
|
||||
|
@ -111,7 +111,7 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
|
|||
}, [pinnedEventIds]);
|
||||
|
||||
const timelineQueryFields = useMemo(() => {
|
||||
const columnsHeader = isEmpty(columns) ? defaultHeaders : columns;
|
||||
const columnsHeader = isEmpty(columns) ? defaultUdtHeaders : columns;
|
||||
const columnFields = columnsHeader.map((c) => c.id);
|
||||
|
||||
return [...columnFields, ...requiredFieldsForActions];
|
||||
|
|
|
@ -30,8 +30,10 @@ import { useDispatch } from 'react-redux';
|
|||
import type { ExperimentalFeatures } from '../../../../../../common';
|
||||
import { allowedExperimentalValues } from '../../../../../../common';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
|
||||
import { defaultUdtHeaders } from '../../unified_components/default_headers';
|
||||
import { defaultColumnHeaderType } from '../../body/column_headers/default_headers';
|
||||
import {
|
||||
defaultUdtHeaders,
|
||||
defaultColumnHeaderType,
|
||||
} from '../../body/column_headers/default_headers';
|
||||
import { useUserPrivileges } from '../../../../../common/components/user_privileges';
|
||||
import { getEndpointPrivilegesInitialStateMock } from '../../../../../common/components/user_privileges/endpoint/mocks';
|
||||
import * as timelineActions from '../../../../store/actions';
|
||||
|
@ -52,10 +54,6 @@ jest.mock('../../../fields_browser', () => ({
|
|||
useFieldBrowserOptions: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../body/events', () => ({
|
||||
Events: () => <></>,
|
||||
}));
|
||||
|
||||
jest.mock('../../../../../sourcerer/containers');
|
||||
jest.mock('../../../../../sourcerer/containers/use_signal_helpers', () => ({
|
||||
useSignalHelpers: () => ({ signalIndexNeedsInit: false }),
|
||||
|
|
|
@ -23,7 +23,6 @@ import { useKibana } from '../../../../../common/lib/kibana';
|
|||
import * as i18n from './translations';
|
||||
import { TimelineTabs } from '../../../../../../common/types/timeline';
|
||||
import { SourcererScopeName } from '../../../../../sourcerer/store/model';
|
||||
import { isFullScreen } from '../../body/column_headers';
|
||||
import { SCROLLING_DISABLED_CLASS_NAME } from '../../../../../../common/constants';
|
||||
import { FULL_SCREEN } from '../../body/column_headers/translations';
|
||||
import { EXIT_FULL_SCREEN } from '../../../../../common/components/exit_full_screen/translations';
|
||||
|
@ -35,6 +34,7 @@ import { useUserPrivileges } from '../../../../../common/components/user_privile
|
|||
import { timelineActions, timelineSelectors } from '../../../../store';
|
||||
import { timelineDefaults } from '../../../../store/defaults';
|
||||
import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
|
||||
import { isFullScreen } from '../../helpers';
|
||||
|
||||
const FullScreenButtonIcon = styled(EuiButtonIcon)`
|
||||
margin: 4px 0 4px 0;
|
||||
|
|
|
@ -5,14 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyoutHeader,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiBadge,
|
||||
} from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlyoutHeader, EuiBadge } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const TabHeaderContainer = styled.div`
|
||||
|
@ -33,46 +26,12 @@ export const StyledEuiFlyoutHeader = styled(EuiFlyoutHeader)`
|
|||
}
|
||||
`;
|
||||
|
||||
export const StyledEuiFlyoutBody = styled(EuiFlyoutBody)`
|
||||
overflow-y: hidden;
|
||||
flex: 1;
|
||||
|
||||
.euiFlyoutBody__overflow {
|
||||
overflow: hidden;
|
||||
mask-image: none;
|
||||
}
|
||||
|
||||
.euiFlyoutBody__overflowContent {
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledEuiFlyoutFooter = styled(EuiFlyoutFooter)`
|
||||
background: none;
|
||||
&.euiFlyoutFooter {
|
||||
${({ theme }) => `padding: ${theme.eui.euiSizeS} 0;`}
|
||||
}
|
||||
`;
|
||||
|
||||
export const FullWidthFlexGroup = styled(EuiFlexGroup)`
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export const ScrollableFlexItem = styled(EuiFlexItem)`
|
||||
${({ theme }) => `margin: 0 ${theme.eui.euiSizeM};`}
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export const SourcererFlex = styled(EuiFlexItem)`
|
||||
align-items: flex-end;
|
||||
`;
|
||||
|
||||
SourcererFlex.displayName = 'SourcererFlex';
|
||||
|
||||
export const VerticalRule = styled.div`
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { TestProviders } from '../../../../../common/mock';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useTimelineColumns } from './use_timeline_columns';
|
||||
import { defaultUdtHeaders } from '../../unified_components/default_headers';
|
||||
import { defaultUdtHeaders } from '../../body/column_headers/default_headers';
|
||||
import type { ColumnHeaderOptions } from '../../../../../../common/types/timeline/columns';
|
||||
|
||||
jest.mock('../../../../../common/hooks/use_experimental_features', () => ({
|
||||
|
|
|
@ -9,7 +9,7 @@ import { useMemo } from 'react';
|
|||
import { SourcererScopeName } from '../../../../../sourcerer/store/model';
|
||||
import { useSourcererDataView } from '../../../../../sourcerer/containers';
|
||||
import { requiredFieldsForActions } from '../../../../../detections/components/alerts_table/default_config';
|
||||
import { defaultUdtHeaders } from '../../unified_components/default_headers';
|
||||
import { defaultUdtHeaders } from '../../body/column_headers/default_headers';
|
||||
import type { ColumnHeaderOptions } from '../../../../../../common/types';
|
||||
import { memoizedGetTimelineColumnHeaders } from './utils';
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import type { BrowserFields, ColumnHeaderOptions } from '@kbn/timelines-plugin/common';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import type { ControlColumnProps } from '../../../../../../common/types';
|
||||
import type { Sort } from '../../body/sort';
|
||||
import type { SortColumnTimeline as Sort } from '../../../../../../common/types/timeline';
|
||||
import type { TimelineItem } from '../../../../../../common/search_strategy';
|
||||
import type { inputsModel } from '../../../../../common/store';
|
||||
import { getColumnHeaders } from '../../body/column_headers/helpers';
|
||||
|
|
|
@ -12,7 +12,7 @@ import { CustomTimelineDataGridBody } from './custom_timeline_data_grid_body';
|
|||
import { mockTimelineData, TestProviders } from '../../../../../common/mock';
|
||||
import type { TimelineItem } from '@kbn/timelines-plugin/common';
|
||||
import type { DataTableRecord } from '@kbn/discover-utils/types';
|
||||
import { defaultUdtHeaders } from '../default_headers';
|
||||
import { defaultUdtHeaders } from '../../body/column_headers/default_headers';
|
||||
import type { EuiDataGridColumn } from '@elastic/eui';
|
||||
import { useStatefulRowRenderer } from '../../body/events/stateful_row_renderer/use_stateful_row_renderer';
|
||||
import { TIMELINE_EVENT_DETAIL_ROW_ID } from '../../body/constants';
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import { createMockStore, mockTimelineData, TestProviders } from '../../../../../common/mock';
|
||||
import React from 'react';
|
||||
import { TimelineDataTable } from '.';
|
||||
import { defaultUdtHeaders } from '../default_headers';
|
||||
import { TimelineId, TimelineTabs } from '../../../../../../common/types';
|
||||
import { DataLoadingState } from '@kbn/unified-data-table';
|
||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
||||
|
@ -18,6 +17,7 @@ import { getColumnHeaders } from '../../body/column_headers/helpers';
|
|||
import { mockSourcererScope } from '../../../../../sourcerer/containers/mocks';
|
||||
import { timelineActions } from '../../../../store';
|
||||
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
|
||||
import { defaultUdtHeaders } from '../../body/column_headers/default_headers';
|
||||
|
||||
jest.mock('../../../../../sourcerer/containers');
|
||||
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ColumnHeaderOptions, ColumnHeaderType } from '../../../../../common/types';
|
||||
import {
|
||||
DEFAULT_COLUMN_MIN_WIDTH,
|
||||
DEFAULT_UNIFIED_TABLE_DATE_COLUMN_MIN_WIDTH,
|
||||
} from '../body/constants';
|
||||
|
||||
export const defaultColumnHeaderType: ColumnHeaderType = 'not-filtered';
|
||||
|
||||
export const defaultUdtHeaders: ColumnHeaderOptions[] = [
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: '@timestamp',
|
||||
initialWidth: DEFAULT_UNIFIED_TABLE_DATE_COLUMN_MIN_WIDTH,
|
||||
esTypes: ['date'],
|
||||
type: 'date',
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'message',
|
||||
initialWidth: DEFAULT_COLUMN_MIN_WIDTH * 2,
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'event.category',
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'event.action',
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'host.name',
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'source.ip',
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'destination.ip',
|
||||
},
|
||||
{
|
||||
columnHeaderType: defaultColumnHeaderType,
|
||||
id: 'user.name',
|
||||
},
|
||||
];
|
|
@ -32,7 +32,7 @@ import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_ex
|
|||
import { TimelineTabs } from '@kbn/securitysolution-data-table';
|
||||
import { DataLoadingState } from '@kbn/unified-data-table';
|
||||
import { getColumnHeaders } from '../body/column_headers/helpers';
|
||||
import { defaultUdtHeaders } from './default_headers';
|
||||
import { defaultUdtHeaders } from '../body/column_headers/default_headers';
|
||||
import type { ColumnHeaderType } from '../../../../../common/types';
|
||||
|
||||
jest.mock('../../../containers', () => ({
|
||||
|
@ -45,10 +45,6 @@ jest.mock('../../fields_browser', () => ({
|
|||
useFieldBrowserOptions: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../body/events', () => ({
|
||||
Events: () => <></>,
|
||||
}));
|
||||
|
||||
jest.mock('../../../../sourcerer/containers');
|
||||
jest.mock('../../../../sourcerer/containers/use_signal_helpers', () => ({
|
||||
useSignalHelpers: () => ({ signalIndexNeedsInit: false }),
|
||||
|
|
|
@ -31,7 +31,6 @@ import { withDataView } from '../../../../common/components/with_data_view';
|
|||
import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context';
|
||||
import type { TimelineItem } from '../../../../../common/search_strategy';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { defaultHeaders } from '../body/column_headers/default_headers';
|
||||
import type {
|
||||
ColumnHeaderOptions,
|
||||
OnChangePage,
|
||||
|
@ -47,7 +46,7 @@ import { TimelineResizableLayout } from './resizable_layout';
|
|||
import TimelineDataTable from './data_table';
|
||||
import { timelineActions } from '../../../store';
|
||||
import { getFieldsListCreationOptions } from './get_fields_list_creation_options';
|
||||
import { defaultUdtHeaders } from './default_headers';
|
||||
import { defaultUdtHeaders } from '../body/column_headers/default_headers';
|
||||
import { getTimelineShowStatusByIdSelector } from '../../../store/selectors';
|
||||
|
||||
const TimelineBodyContainer = styled.div.attrs(({ className = '' }) => ({
|
||||
|
@ -291,7 +290,7 @@ const UnifiedTimelineComponent: React.FC<Props> = ({
|
|||
(columnId: string) => {
|
||||
dispatch(
|
||||
timelineActions.upsertColumn({
|
||||
column: getColumnHeader(columnId, defaultHeaders),
|
||||
column: getColumnHeader(columnId, defaultUdtHeaders),
|
||||
id: timelineId,
|
||||
index: 1,
|
||||
})
|
||||
|
|
|
@ -18,7 +18,7 @@ import { appActions } from '../../common/store/app';
|
|||
import { SourcererScopeName } from '../../sourcerer/store/model';
|
||||
import { InputsModelId } from '../../common/store/inputs/constants';
|
||||
import { TestProviders, mockGlobalState } from '../../common/mock';
|
||||
import { defaultUdtHeaders } from '../components/timeline/unified_components/default_headers';
|
||||
import { defaultUdtHeaders } from '../components/timeline/body/column_headers/default_headers';
|
||||
|
||||
jest.mock('../../common/components/discover_in_timeline/use_discover_in_timeline_context');
|
||||
jest.mock('../../common/containers/use_global_time', () => {
|
||||
|
|
|
@ -19,7 +19,7 @@ import { SourcererScopeName } from '../../sourcerer/store/model';
|
|||
import { appActions } from '../../common/store/app';
|
||||
import type { TimeRange } from '../../common/store/inputs/model';
|
||||
import { useDiscoverInTimelineContext } from '../../common/components/discover_in_timeline/use_discover_in_timeline_context';
|
||||
import { defaultUdtHeaders } from '../components/timeline/unified_components/default_headers';
|
||||
import { defaultUdtHeaders } from '../components/timeline/body/column_headers/default_headers';
|
||||
import { timelineDefaults } from '../store/defaults';
|
||||
|
||||
export interface UseCreateTimelineParams {
|
||||
|
|
|
@ -12,10 +12,9 @@ import {
|
|||
RowRendererIdEnum,
|
||||
} from '../../../common/api/timeline';
|
||||
|
||||
import { defaultHeaders } from '../components/timeline/body/column_headers/default_headers';
|
||||
import { normalizeTimeRange } from '../../common/utils/normalize_time_range';
|
||||
import type { SubsetTimelineModel, TimelineModel } from './model';
|
||||
import { defaultUdtHeaders } from '../components/timeline/unified_components/default_headers';
|
||||
import { defaultUdtHeaders } from '../components/timeline/body/column_headers/default_headers';
|
||||
|
||||
// normalizeTimeRange uses getTimeRangeSettings which cannot be used outside Kibana context if the uiSettings is not false
|
||||
const { from: start, to: end } = normalizeTimeRange({ from: '', to: '' }, false);
|
||||
|
@ -109,7 +108,7 @@ export const timelineDefaults: SubsetTimelineModel &
|
|||
};
|
||||
|
||||
export const getTimelineManageDefaults = (id: string) => ({
|
||||
defaultColumns: defaultHeaders,
|
||||
defaultColumns: defaultUdtHeaders,
|
||||
documentType: '',
|
||||
selectAll: false,
|
||||
id,
|
||||
|
|
|
@ -18,7 +18,10 @@ import type {
|
|||
DataProvidersAnd,
|
||||
} from '../components/timeline/data_providers/data_provider';
|
||||
import { IS_OPERATOR } from '../components/timeline/data_providers/data_provider';
|
||||
import { defaultColumnHeaderType } from '../components/timeline/body/column_headers/default_headers';
|
||||
import {
|
||||
defaultUdtHeaders,
|
||||
defaultColumnHeaderType,
|
||||
} from '../components/timeline/body/column_headers/default_headers';
|
||||
import {
|
||||
DEFAULT_COLUMN_MIN_WIDTH,
|
||||
RESIZED_COLUMN_MIN_WITH,
|
||||
|
@ -50,7 +53,6 @@ import type { TimelineModel } from './model';
|
|||
import { timelineDefaults } from './defaults';
|
||||
import type { TimelineById } from './types';
|
||||
import { Direction } from '../../../common/search_strategy';
|
||||
import { defaultUdtHeaders } from '../components/timeline/unified_components/default_headers';
|
||||
|
||||
jest.mock('../../common/utils/normalize_time_range');
|
||||
jest.mock('../../common/utils/default_date_settings', () => {
|
||||
|
|
|
@ -10,7 +10,6 @@ import { v4 as uuidv4 } from 'uuid';
|
|||
import type { Filter } from '@kbn/es-query';
|
||||
import type { SessionViewConfig } from '../../../common/types';
|
||||
import type { TimelineNonEcsData } from '../../../common/search_strategy';
|
||||
import type { Sort } from '../components/timeline/body/sort';
|
||||
import type {
|
||||
DataProvider,
|
||||
QueryOperator,
|
||||
|
@ -23,15 +22,16 @@ import {
|
|||
TimelineStatusEnum,
|
||||
TimelineTypeEnum,
|
||||
} from '../../../common/api/timeline';
|
||||
import { TimelineId } from '../../../common/types/timeline';
|
||||
import type {
|
||||
ColumnHeaderOptions,
|
||||
TimelineEventsType,
|
||||
SerializedFilterQuery,
|
||||
TimelinePersistInput,
|
||||
SortColumnTimeline,
|
||||
SortColumnTimeline as Sort,
|
||||
} from '../../../common/types/timeline';
|
||||
import type { RowRendererId, TimelineType } from '../../../common/api/timeline';
|
||||
import { TimelineId } from '../../../common/types/timeline';
|
||||
import { normalizeTimeRange } from '../../common/utils/normalize_time_range';
|
||||
import { getTimelineManageDefaults, timelineDefaults } from './defaults';
|
||||
import type { KqlMode, TimelineModel } from './model';
|
||||
|
|
|
@ -40202,8 +40202,6 @@
|
|||
"xpack.securitySolution.timeline.descriptionTooltip": "Description",
|
||||
"xpack.securitySolution.timeline.destination": "Destination",
|
||||
"xpack.securitySolution.timeline.EqlQueryBarLabel": "Requête EQL",
|
||||
"xpack.securitySolution.timeline.eventHasEventRendererScreenReaderOnly": "L'événement de la ligne {row} possède un outil de rendu d'événement. Appuyez sur Maj + flèche vers le bas pour faire la mise au point dessus.",
|
||||
"xpack.securitySolution.timeline.eventHasNotesScreenReaderOnly": "L'événement de la ligne {row} possède {notesCount, plural, =1 {une note} other {{notesCount} des notes}}. Appuyez sur Maj + flèche vers la droite pour faire la mise au point sur les notes.",
|
||||
"xpack.securitySolution.timeline.eventRenderersSwitch.title": "Outils de rendu d'événement",
|
||||
"xpack.securitySolution.timeline.eventRenderersSwitch.warning": "L'activation des outils de rendu d'événement peut avoir un impact sur les performances de la table.",
|
||||
"xpack.securitySolution.timeline.eventsSelect.actions.pinSelected": "Épingler la sélection",
|
||||
|
@ -40259,10 +40257,6 @@
|
|||
"xpack.securitySolution.timeline.properties.unlockDatePickerTooltip": "Cliquer pour synchroniser la plage temporelle des requêtes avec la plage temporelle de la page actuelle.",
|
||||
"xpack.securitySolution.timeline.properties.untitledTemplatePlaceholder": "Modèle sans titre",
|
||||
"xpack.securitySolution.timeline.properties.untitledTimelinePlaceholder": "Chronologie sans titre",
|
||||
"xpack.securitySolution.timeline.rangePicker.oneDay": "1 jour",
|
||||
"xpack.securitySolution.timeline.rangePicker.oneMonth": "1 mois",
|
||||
"xpack.securitySolution.timeline.rangePicker.oneWeek": "1 semaine",
|
||||
"xpack.securitySolution.timeline.rangePicker.oneYear": "1 an",
|
||||
"xpack.securitySolution.timeline.removeFromFavoritesButtonLabel": "Retirer des favoris",
|
||||
"xpack.securitySolution.timeline.saveStatus.unsavedChangesLabel": "Modifications non enregistrées",
|
||||
"xpack.securitySolution.timeline.saveStatus.unsavedLabel": "Non enregistré",
|
||||
|
@ -40312,7 +40306,6 @@
|
|||
"xpack.securitySolution.timeline.userDetails.managed.description": "Les métadonnées de toutes les intégrations de référentiel de ressource sont autorisées dans votre environnement.",
|
||||
"xpack.securitySolution.timeline.userDetails.updatedTime": "Mis à jour le {time}",
|
||||
"xpack.securitySolution.timeline.youAreInAnEventRendererScreenReaderOnly": "Vous êtes dans un outil de rendu d'événement pour la ligne : {row}. Appuyez sur la touche fléchée vers le haut pour quitter et revenir à la ligne en cours, ou sur la touche fléchée vers le bas pour quitter et passer à la ligne suivante.",
|
||||
"xpack.securitySolution.timeline.youAreInATableCellScreenReaderOnly": "Vous êtes dans une cellule de tableau. Ligne : {row}, colonne : {column}",
|
||||
"xpack.securitySolution.timelineEvents.errorSearchDescription": "Une erreur s'est produite lors de la recherche d'événements de la chronologie",
|
||||
"xpack.securitySolution.timelines.allTimelines.errorFetchingTimelinesTitle": "Impossible d'interroger les données de toutes les chronologies",
|
||||
"xpack.securitySolution.timelines.allTimelines.importTimelineTitle": "Importer",
|
||||
|
|
|
@ -39946,8 +39946,6 @@
|
|||
"xpack.securitySolution.timeline.descriptionTooltip": "説明",
|
||||
"xpack.securitySolution.timeline.destination": "送信先",
|
||||
"xpack.securitySolution.timeline.EqlQueryBarLabel": "EQL クエリ",
|
||||
"xpack.securitySolution.timeline.eventHasEventRendererScreenReaderOnly": "行{row}のイベントにはイベントレンダラーがあります。Shiftと下矢印を押すとフォーカスします。",
|
||||
"xpack.securitySolution.timeline.eventHasNotesScreenReaderOnly": "行{row}のイベントには{notesCount, plural, other {{notesCount}個のメモ}}があります。Shiftと右矢印を押すとメモをフォーカスします。",
|
||||
"xpack.securitySolution.timeline.eventRenderersSwitch.title": "イベントレンダラー",
|
||||
"xpack.securitySolution.timeline.eventRenderersSwitch.warning": "イベントレンダリングを有効化すると、テーブルパフォーマンスに影響する可能性があります。",
|
||||
"xpack.securitySolution.timeline.eventsSelect.actions.pinSelected": "選択項目にピン付け",
|
||||
|
@ -40003,10 +40001,6 @@
|
|||
"xpack.securitySolution.timeline.properties.unlockDatePickerTooltip": "クリックすると、クエリの時間範囲と現在のページの時間範囲を同期します。",
|
||||
"xpack.securitySolution.timeline.properties.untitledTemplatePlaceholder": "無題のテンプレート",
|
||||
"xpack.securitySolution.timeline.properties.untitledTimelinePlaceholder": "無題のタイムライン",
|
||||
"xpack.securitySolution.timeline.rangePicker.oneDay": "1日",
|
||||
"xpack.securitySolution.timeline.rangePicker.oneMonth": "1 か月",
|
||||
"xpack.securitySolution.timeline.rangePicker.oneWeek": "1 週間",
|
||||
"xpack.securitySolution.timeline.rangePicker.oneYear": "1 年",
|
||||
"xpack.securitySolution.timeline.removeFromFavoritesButtonLabel": "お気に入りから削除",
|
||||
"xpack.securitySolution.timeline.saveStatus.unsavedChangesLabel": "保存されていない変更",
|
||||
"xpack.securitySolution.timeline.saveStatus.unsavedLabel": "未保存",
|
||||
|
@ -40056,7 +40050,6 @@
|
|||
"xpack.securitySolution.timeline.userDetails.managed.description": "環境で有効になっているアセットリポジトリ統合からのメタデータ。",
|
||||
"xpack.securitySolution.timeline.userDetails.updatedTime": "更新日時{time}",
|
||||
"xpack.securitySolution.timeline.youAreInAnEventRendererScreenReaderOnly": "行 {row} のイベントレンダラーを表示しています。上矢印キーを押すと、終了して現在の行に戻ります。下矢印キーを押すと、終了して次の行に進みます。",
|
||||
"xpack.securitySolution.timeline.youAreInATableCellScreenReaderOnly": "表セルの行 {row}、列 {column} にいます",
|
||||
"xpack.securitySolution.timelineEvents.errorSearchDescription": "タイムラインイベント検索でエラーが発生しました",
|
||||
"xpack.securitySolution.timelines.allTimelines.errorFetchingTimelinesTitle": "すべてのタイムラインデータをクエリできませんでした",
|
||||
"xpack.securitySolution.timelines.allTimelines.importTimelineTitle": "インポート",
|
||||
|
|
|
@ -39991,8 +39991,6 @@
|
|||
"xpack.securitySolution.timeline.descriptionTooltip": "描述",
|
||||
"xpack.securitySolution.timeline.destination": "目标",
|
||||
"xpack.securitySolution.timeline.EqlQueryBarLabel": "EQL 查询",
|
||||
"xpack.securitySolution.timeline.eventHasEventRendererScreenReaderOnly": "位于行 {row} 的事件具有事件呈现程序。按 shift + 向下箭头键以对其聚焦。",
|
||||
"xpack.securitySolution.timeline.eventHasNotesScreenReaderOnly": "位于行 {row} 的事件有{notesCount, plural, =1 {备注} other { {notesCount} 个备注}}。按 shift + 右箭头键以聚焦备注。",
|
||||
"xpack.securitySolution.timeline.eventRenderersSwitch.title": "事件呈现器",
|
||||
"xpack.securitySolution.timeline.eventRenderersSwitch.warning": "启用事件呈现器可能会影响表性能。",
|
||||
"xpack.securitySolution.timeline.eventsSelect.actions.pinSelected": "固定所选",
|
||||
|
@ -40048,10 +40046,6 @@
|
|||
"xpack.securitySolution.timeline.properties.unlockDatePickerTooltip": "单击以将查询时间范围与当前页面的时间范围进行同步。",
|
||||
"xpack.securitySolution.timeline.properties.untitledTemplatePlaceholder": "未命名模板",
|
||||
"xpack.securitySolution.timeline.properties.untitledTimelinePlaceholder": "未命名时间线",
|
||||
"xpack.securitySolution.timeline.rangePicker.oneDay": "1 天",
|
||||
"xpack.securitySolution.timeline.rangePicker.oneMonth": "1 个月",
|
||||
"xpack.securitySolution.timeline.rangePicker.oneWeek": "1 周",
|
||||
"xpack.securitySolution.timeline.rangePicker.oneYear": "1 年",
|
||||
"xpack.securitySolution.timeline.removeFromFavoritesButtonLabel": "从收藏夹中移除",
|
||||
"xpack.securitySolution.timeline.saveStatus.unsavedChangesLabel": "未保存的更改",
|
||||
"xpack.securitySolution.timeline.saveStatus.unsavedLabel": "未保存",
|
||||
|
@ -40101,7 +40095,6 @@
|
|||
"xpack.securitySolution.timeline.userDetails.managed.description": "在您的环境中启用的任何资产存储库集成中的元数据。",
|
||||
"xpack.securitySolution.timeline.userDetails.updatedTime": "已更新 {time}",
|
||||
"xpack.securitySolution.timeline.youAreInAnEventRendererScreenReaderOnly": "您正处于第 {row} 行的事件呈现器中。按向上箭头键退出并返回当前行,或按向下箭头键退出并前进到下一行。",
|
||||
"xpack.securitySolution.timeline.youAreInATableCellScreenReaderOnly": "您处在表单元格中。行:{row},列:{column}",
|
||||
"xpack.securitySolution.timelineEvents.errorSearchDescription": "搜索时间线事件时发生错误",
|
||||
"xpack.securitySolution.timelines.allTimelines.errorFetchingTimelinesTitle": "无法查询所有时间线数据",
|
||||
"xpack.securitySolution.timelines.allTimelines.importTimelineTitle": "导入",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue