Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 97 commits into from
Jun 22, 2021
Merged
Changes from 2 commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
a766816
wip
XavierM Apr 30, 2021
4b4226a
Merge branch 'master' of github.com:elastic/kibana into t-grid-new-home
XavierM May 3, 2021
ff8e573
First pass at standalone and embedded redux stores and usage
kqualters-elastic May 6, 2021
c83fa58
wip
XavierM Apr 30, 2021
eb8ac73
First pass at standalone and embedded redux stores and usage
kqualters-elastic May 6, 2021
129351d
wip
XavierM May 6, 2021
cd05741
Merge branch 't-grid-new-home' of github.com:XavierM/kibana into t-gr…
XavierM May 14, 2021
36c240c
Merge branch 'master' of github.com:elastic/kibana into t-grid-new-home
XavierM May 14, 2021
2bb49a7
clean up
XavierM May 14, 2021
e8b0a4b
wip
XavierM May 17, 2021
b42b00c
Merge branch 'master' of github.com:elastic/kibana into t-grid-new-home
XavierM May 18, 2021
fdeaffc
refact(NA): remove extra pkg_npm target and add specific target folde…
mistic May 18, 2021
752e357
Merge remote-tracking branch 'upstream/master' into remove-extra-pkg_…
mistic May 18, 2021
c3adc62
cleanup
XavierM May 18, 2021
2f619fc
Merge branch 'master' of github.com:elastic/kibana into t-grid-new-home
XavierM May 18, 2021
f047145
Merge branch 'remove-extra-pkg_npm-from-kbn-i18n' of github.com:misti…
XavierM May 18, 2021
df93aa5
- fixes type errors in tests
andrew-goldstein May 19, 2021
30bad21
Merge branch 'master' of github.com:elastic/kibana into t-grid-new-home
XavierM May 19, 2021
067bfd1
WIP remove use_manage_timeline
kqualters-elastic May 19, 2021
1a9a414
wip add query + selector
XavierM May 20, 2021
6bf50ba
Merge branch 't-grid-new-home' of github.com:XavierM/kibana into t-gr…
XavierM May 20, 2021
83310e3
finishing integrating timeline manage context from redux
XavierM May 20, 2021
18ac6db
integrating t-grid in security solution
XavierM May 24, 2021
a80802d
fix RowRender type
XavierM May 25, 2021
8459d5b
WIP begin to move components from package to plugin
kqualters-elastic May 25, 2021
22016a5
Merge branch 't-grid-new-home' of github.com:XavierM/kibana into t-gr…
XavierM May 25, 2021
dc7ac7c
integration of t-grid inside of security solution
XavierM May 25, 2021
bee3f65
wip to make redux work
XavierM May 25, 2021
f552264
little trick to make it render
XavierM May 25, 2021
136ae44
- fixes a few type errors
andrew-goldstein May 27, 2021
aca8810
better integration betwen tgrid and security solutions
XavierM May 28, 2021
980979a
Merge branch 't-grid-new-home' of github.com:XavierM/kibana into t-gr…
XavierM May 28, 2021
642090f
Merge branch 'master' of github.com:elastic/kibana into t-grid-new-home
XavierM May 28, 2021
eab2df6
bringing back tsconfig on timeline
XavierM May 28, 2021
ccb66e3
wip integration t-grid in observability
XavierM Jun 3, 2021
981cb8b
Merge branch 'master' of github.com:elastic/kibana into t-grid-new-home
XavierM Jun 4, 2021
559bfa8
fix types
XavierM Jun 4, 2021
73a2da7
fix type in security solutions
XavierM Jun 5, 2021
31ceae3
add type to import + trie dto get the bundle size as small as possible
XavierM Jun 7, 2021
9a438e1
fix type in integration test
XavierM Jun 7, 2021
5bca60e
fix type in integration test
XavierM Jun 7, 2021
279aa58
Merge branch 't-grid-new-home' of github.com:XavierM/kibana into t-gr…
XavierM Jun 7, 2021
a227538
- fix tests
andrew-goldstein Jun 8, 2021
782db0c
Merge branch 't-grid-new-home' of github.com:XavierM/kibana into t-gr…
andrew-goldstein Jun 8, 2021
3873495
clean up to use technical fields
XavierM Jun 8, 2021
2e70041
Merge branch 't-grid-new-home' of github.com:XavierM/kibana into t-gr…
XavierM Jun 8, 2021
abfd4e8
- fixes unit tests
andrew-goldstein Jun 9, 2021
712fc00
- mocks the `useDateFormat` function of the `useKibana` service to fi…
andrew-goldstein Jun 9, 2021
de5b04e
fix t-grid settings vs create timeline + fix inspect button
XavierM Jun 10, 2021
f97b842
Merge branch 't-grid-new-home' of github.com:XavierM/kibana into t-gr…
XavierM Jun 10, 2021
39fdaa0
Merge branch 'master' of github.com:elastic/kibana into t-grid-new-home
XavierM Jun 10, 2021
f581b11
fix last suites test
XavierM Jun 10, 2021
6b6aa51
Update unit tests, snapshots and lint
kqualters-elastic Jun 10, 2021
1006453
Merge remote-tracking branch 'xavier/t-grid-new-home' into t-grid-new…
kqualters-elastic Jun 10, 2021
9d8504b
Fix bad merge
kqualters-elastic Jun 10, 2021
fb9f220
fix plugin export
XavierM Jun 10, 2021
fd2bf33
Merge remote-tracking branch 'xavier/t-grid-new-home' into t-grid-new…
kqualters-elastic Jun 10, 2021
b740fd3
Fix some failing tests
kqualters-elastic Jun 10, 2021
74f8d02
fix unit tets in timelines plugins
XavierM Jun 10, 2021
c84b084
Merge branch 't-grid-new-home' of github.com:XavierM/kibana into t-gr…
XavierM Jun 10, 2021
080b0d6
fix latest test
XavierM Jun 10, 2021
b28faeb
Merge branch 'master' of github.com:elastic/kibana into t-grid-new-home
XavierM Jun 10, 2021
b3f548e
fix i18n
XavierM Jun 10, 2021
ba5b086
free obs from t-grid
XavierM Jun 10, 2021
1aa1e3b
Fix timeline functional plugin types
kqualters-elastic Jun 10, 2021
c2f9de7
Merge branch 't-grid-new-home' of github.com:XavierM/kibana into t-gr…
kqualters-elastic Jun 14, 2021
411516d
fix store provider
XavierM Jun 14, 2021
70604e1
Merge branch 't-grid-new-home' of github.com:XavierM/kibana into t-gr…
XavierM Jun 14, 2021
e333728
Merge branch 'master' of github.com:elastic/kibana into t-grid-new-home
XavierM Jun 14, 2021
7190e78
Merge branch 'master' of github.com:elastic/kibana into t-grid-new-home
XavierM Jun 15, 2021
4382a10
Update failing defaultHeader test
kqualters-elastic Jun 15, 2021
b2ceb1d
Fix i18n usage in security solution
kqualters-elastic Jun 15, 2021
eda18f9
Fix remaining i18n errors in timelines plugin
kqualters-elastic Jun 15, 2021
3ee7e2c
Dedupe common shared types
kqualters-elastic Jun 16, 2021
9076b4f
move drag and drop utils in package to avoid duplication
XavierM Jun 17, 2021
cd5700b
Merge branch 't-grid-new-home' of github.com:XavierM/kibana into t-gr…
XavierM Jun 17, 2021
bf12fa9
Merge branch 'master' of github.com:elastic/kibana into t-grid-new-home
XavierM Jun 17, 2021
cb04865
More shared type cleanup
kqualters-elastic Jun 17, 2021
c98693a
Merge branch 't-grid-new-home' of github.com:XavierM/kibana into t-gr…
XavierM Jun 17, 2021
fb282b3
add feature flag
XavierM Jun 17, 2021
e8b56cf
review I
XavierM Jun 17, 2021
38dd389
Merge branch 'master' of github.com:elastic/kibana into t-grid-new-home
XavierM Jun 17, 2021
0a9bd47
fix merge with master
XavierM Jun 21, 2021
7262fd5
Merge branch 'master' of github.com:elastic/kibana into t-grid-new-home
XavierM Jun 21, 2021
495c53b
fix i18n translation
XavierM Jun 21, 2021
0c53075
More type deduping
kqualters-elastic Jun 21, 2021
2a9546d
Merge remote-tracking branch 'xavier/t-grid-new-home' into t-grid-new…
kqualters-elastic Jun 21, 2021
d1557f3
Use @kbn/common-utils, fix remaining types
kqualters-elastic Jun 21, 2021
2960665
fix types
XavierM Jun 21, 2021
0e2286b
Merge branch 't-grid-new-home' of github.com:XavierM/kibana into t-gr…
XavierM Jun 21, 2021
8e71b5c
fix tests
XavierM Jun 21, 2021
a4ddf6f
missing type
XavierM Jun 21, 2021
05dd22e
fix cypress tests
XavierM Jun 22, 2021
9815b74
Update limits.yml and fix functional plugin tests
kqualters-elastic Jun 22, 2021
fe4be92
Merge branch 'master' of github.com:elastic/kibana into t-grid-new-home
XavierM Jun 22, 2021
4e4277d
fix cypress test + few bugs found
XavierM Jun 22, 2021
5f43fc4
Fix failing jest test
kqualters-elastic Jun 22, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ import { DropResult, DragDropContext } from 'react-beautiful-dnd';
import { useDispatch } from 'react-redux';
import { Dispatch } from 'redux';
import deepEqual from 'fast-deep-equal';
import { IS_DRAGGING_CLASS_NAME, useAddToTimelineSensor } from '@kbn/securitysolution-t-grid';
import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid';

import { BeforeCapture } from './drag_drop_context';
import { BrowserFields } from '../../containers/source';
@@ -37,6 +37,7 @@ import {
userIsReArrangingProviders,
} from './helpers';
import { useDeepEqualSelector } from '../../hooks/use_selector';
import { useKibana } from '../../lib/kibana';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';

// @ts-expect-error
@@ -91,16 +92,18 @@ const onDragEndHandler = ({
}
};

const sensors = [useAddToTimelineSensor];

/**
* DragDropContextWrapperComponent handles all drag end events
*/
export const DragDropContextWrapperComponent: React.FC<Props> = ({ browserFields, children }) => {
const dispatch = useDispatch();
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const getDataProviders = useMemo(() => dragAndDropSelectors.getDataProvidersSelector(), []);

const { timelines } = useKibana().services;
const useAddToTimelineSensor = useMemo(() => {
return timelines ? timelines.getUseAddToTimelineSensor() : null;
}, [timelines]);
const sensors = [useAddToTimelineSensor];
const {
dataProviders: activeTimelineDataProviders,
timelineType,
Original file line number Diff line number Diff line change
@@ -16,7 +16,6 @@ import {
DATA_COLINDEX_ATTRIBUTE,
DATA_ROWINDEX_ATTRIBUTE,
isTab,
onKeyDownFocusHandler,
} from '@kbn/securitysolution-t-grid';

import { ADD_TIMELINE_BUTTON_CLASS_NAME } from '../../../timelines/components/flyout/add_timeline_button';
@@ -25,7 +24,7 @@ import { BrowserFields, getAllFieldsByName } from '../../containers/source';
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/timeline';
import { getColumnHeaders } from '../../../timelines/components/timeline/body/column_headers/helpers';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';

import { useKibana } from '../../lib/kibana';
import { getColumns } from './columns';
import { EVENT_FIELDS_TABLE_CLASS_NAME, onEventDetailsTabKeyPressed, search } from './helpers';
import { useDeepEqualSelector } from '../../hooks/use_selector';
@@ -94,6 +93,7 @@ export const EventFieldsBrowser = React.memo<Props>(
const dispatch = useDispatch();
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const fieldsByName = useMemo(() => getAllFieldsByName(browserFields), [browserFields]);
const { timelines } = useKibana().services;
const items = useMemo(
() =>
sortBy(['field'], data).map((item, i) => ({
@@ -194,7 +194,7 @@ export const EventFieldsBrowser = React.memo<Props>(
onSkipFocusAfterEventsTable: focusAddTimelineButton,
});
} else {
onKeyDownFocusHandler({
timelines.getOnKeyDownFocusHandler({
colindexAttribute: DATA_COLINDEX_ATTRIBUTE,
containerElement: containerElement?.current,
event: keyboardEvent,
@@ -205,7 +205,7 @@ export const EventFieldsBrowser = React.memo<Props>(
});
}
},
[data, focusAddTimelineButton, focusSearchInput]
[data, focusAddTimelineButton, focusSearchInput, timelines]
);

useEffect(() => {
Original file line number Diff line number Diff line change
@@ -10,11 +10,8 @@ import { noop } from 'lodash/fp';
import React, { useCallback, useRef } from 'react';
import styled from 'styled-components';

import {
DATA_COLINDEX_ATTRIBUTE,
DATA_ROWINDEX_ATTRIBUTE,
onKeyDownFocusHandler,
} from '@kbn/securitysolution-t-grid';
import { DATA_COLINDEX_ATTRIBUTE, DATA_ROWINDEX_ATTRIBUTE } from '@kbn/securitysolution-t-grid';
import { useKibana } from '../../lib/kibana';
import { BrowserFields } from '../../../common/containers/source';

import { getCategoryColumns } from './category_columns';
@@ -66,9 +63,10 @@ interface Props {
export const CategoriesPane = React.memo<Props>(
({ filteredBrowserFields, onCategorySelected, selectedCategoryId, timelineId, width }) => {
const containerElement = useRef<HTMLDivElement | null>(null);
const { timelines } = useKibana().services;
const onKeyDown = useCallback(
(e: React.KeyboardEvent) => {
onKeyDownFocusHandler({
timelines.getOnKeyDownFocusHandler({
colindexAttribute: DATA_COLINDEX_ATTRIBUTE,
containerElement: containerElement?.current,
event: e,
@@ -78,7 +76,7 @@ export const CategoriesPane = React.memo<Props>(
rowindexAttribute: DATA_ROWINDEX_ATTRIBUTE,
});
},
[containerElement, filteredBrowserFields]
[containerElement, filteredBrowserFields, timelines]
);

return (
Original file line number Diff line number Diff line change
@@ -14,10 +14,10 @@ import {
arrayIndexToAriaIndex,
DATA_COLINDEX_ATTRIBUTE,
DATA_ROWINDEX_ATTRIBUTE,
onKeyDownFocusHandler,
} from '@kbn/securitysolution-t-grid';
import { BrowserFields } from '../../../common/containers/source';
import { OnUpdateColumns } from '../timeline/events';
import { useKibana } from '../../lib/kibana';

import { CategoryTitle } from './category_title';
import { FieldItem, getFieldColumns } from './field_items';
@@ -54,9 +54,10 @@ interface Props {
export const Category = React.memo<Props>(
({ categoryId, filteredBrowserFields, fieldItems, onUpdateColumns, timelineId, width }) => {
const containerElement = useRef<HTMLDivElement | null>(null);
const { timelines } = useKibana().services;
const onKeyDown = useCallback(
(keyboardEvent: React.KeyboardEvent) => {
onKeyDownFocusHandler({
timelines.getOnKeyDownFocusHandler({
colindexAttribute: DATA_COLINDEX_ATTRIBUTE,
containerElement: containerElement?.current,
event: keyboardEvent,
@@ -66,7 +67,7 @@ export const Category = React.memo<Props>(
rowindexAttribute: DATA_ROWINDEX_ATTRIBUTE,
});
},
[fieldItems.length]
[fieldItems.length, timelines]
);

const fieldItemsWithRowindex = useMemo(
Original file line number Diff line number Diff line change
@@ -15,7 +15,6 @@ import {
FIRST_ARIA_INDEX,
ARIA_COLINDEX_ATTRIBUTE,
ARIA_ROWINDEX_ATTRIBUTE,
onKeyDownFocusHandler,
} from '@kbn/securitysolution-t-grid';
import { CellValueElementProps } from '../cell_rendering';
import { DEFAULT_COLUMN_MIN_WIDTH } from './constants';
@@ -108,6 +107,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
const { queryFields, selectAll } = useDeepEqualSelector((state) =>
getManageTimeline(state, id)
);
const { timelines } = useKibana().services;

const onRowSelected: OnRowSelected = useCallback(
({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => {
@@ -211,7 +211,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(

const onKeyDown = useCallback(
(e: React.KeyboardEvent) => {
onKeyDownFocusHandler({
timelines.getOnKeyDownFocusHandler({
colindexAttribute: ARIA_COLINDEX_ATTRIBUTE,
containerElement: containerRef.current,
event: e,
@@ -221,7 +221,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
rowindexAttribute: ARIA_ROWINDEX_ATTRIBUTE,
});
},
[columnHeaders.length, containerRef, data.length]
[columnHeaders.length, containerRef, data.length, timelines]
);

return (
41 changes: 41 additions & 0 deletions x-pack/plugins/timelines/common/utils/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { has } from 'lodash/fp';

export interface AppError extends Error {
body: {
message: string;
};
}

export interface KibanaError extends AppError {
body: {
message: string;
statusCode: number;
};
}

export interface SecurityAppError extends AppError {
body: {
message: string;
status_code: number;
};
}

export const isKibanaError = (error: unknown): error is KibanaError =>
has('message', error) && has('body.message', error) && has('body.statusCode', error);

export const isSecurityAppError = (error: unknown): error is SecurityAppError =>
has('message', error) && has('body.message', error) && has('body.status_code', error);

export const isAppError = (error: unknown): error is AppError =>
isKibanaError(error) || isSecurityAppError(error);

export const isNotFoundError = (error: unknown) =>
(isKibanaError(error) && error.body.statusCode === 404) ||
(isSecurityAppError(error) && error.body.status_code === 404);
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { mount } from 'enzyme';
import React from 'react';

import {
ariaIndexToArrayIndex,
arrayIndexToAriaIndex,
getNotesContainerClassName,
getRowRendererClassName,
isArrowRight,
} from './helpers';

describe('helpers', () => {
describe('ariaIndexToArrayIndex', () => {
test('it returns the expected array index', () => {
expect(ariaIndexToArrayIndex(1)).toEqual(0);
});
});

describe('arrayIndexToAriaIndex', () => {
test('it returns the expected aria index', () => {
expect(arrayIndexToAriaIndex(0)).toEqual(1);
});
});

describe('isArrowRight', () => {
test('it returns true if the right arrow key was pressed', () => {
let result = false;
const onKeyDown = (keyboardEvent: React.KeyboardEvent) => {
result = isArrowRight(keyboardEvent);
};

const wrapper = mount(<div onKeyDown={onKeyDown} />);
wrapper.find('div').simulate('keydown', { key: 'ArrowRight' });
wrapper.update();

expect(result).toBe(true);
});

test('it returns false if another key was pressed', () => {
let result = false;
const onKeyDown = (keyboardEvent: React.KeyboardEvent) => {
result = isArrowRight(keyboardEvent);
};

const wrapper = mount(<div onKeyDown={onKeyDown} />);
wrapper.find('div').simulate('keydown', { key: 'Enter' });
wrapper.update();

expect(result).toBe(false);
});
});

describe('getRowRendererClassName', () => {
test('it returns the expected class name', () => {
expect(getRowRendererClassName(2)).toBe('row-renderer-2');
});
});

describe('getNotesContainerClassName', () => {
test('it returns the expected class name', () => {
expect(getNotesContainerClassName(2)).toBe('notes-container-2');
});
});
});
854 changes: 854 additions & 0 deletions x-pack/plugins/timelines/public/components/accessibility/helpers.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

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

import { mount } from 'enzyme';
import React from 'react';

import { TooltipWithKeyboardShortcut } from '.';

const props = {
content: <div>{'To pay respect'}</div>,
shortcut: 'F',
showShortcut: true,
};

describe('TooltipWithKeyboardShortcut', () => {
test('it renders the provided content', () => {
const wrapper = mount(<TooltipWithKeyboardShortcut {...props} />);

expect(wrapper.find('[data-test-subj="content"]').text()).toBe('To pay respect');
});

test('it renders the additionalScreenReaderOnlyContext', () => {
const wrapper = mount(
<TooltipWithKeyboardShortcut {...props} additionalScreenReaderOnlyContext={'field.name'} />
);

expect(wrapper.find('[data-test-subj="additionalScreenReaderOnlyContext"]').text()).toBe(
'field.name'
);
});

test('it renders the expected shortcut', () => {
const wrapper = mount(<TooltipWithKeyboardShortcut {...props} />);

expect(wrapper.find('[data-test-subj="shortcut"]').first().text()).toBe('Press\u00a0F');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 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 { EuiText, EuiScreenReaderOnly } from '@elastic/eui';
import React from 'react';

import * as i18n from './translations';

interface Props {
additionalScreenReaderOnlyContext?: string;
content: React.ReactNode;
shortcut: string;
showShortcut: boolean;
}

const TooltipWithKeyboardShortcutComponent = ({
additionalScreenReaderOnlyContext = '',
content,
shortcut,
showShortcut,
}: Props) => (
<>
<div data-test-subj="content">{content}</div>
{additionalScreenReaderOnlyContext !== '' && (
<EuiScreenReaderOnly data-test-subj="additionalScreenReaderOnlyContext">
<p>{additionalScreenReaderOnlyContext}</p>
</EuiScreenReaderOnly>
)}
{showShortcut && (
<EuiText color="subdued" data-test-subj="shortcut" size="s" textAlign="center">
<span>{i18n.PRESS}</span>
{'\u00a0'}
<span className="euiBadge euiBadge--hollow">{shortcut}</span>
</EuiText>
)}
</>
);

export const TooltipWithKeyboardShortcut = React.memo(TooltipWithKeyboardShortcutComponent);
TooltipWithKeyboardShortcut.displayName = 'TooltipWithKeyboardShortcut';
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { i18n } from '@kbn/i18n';

export const PRESS = i18n.translate(
'xpack.timelines.accessibility.tooltipWithKeyboardShortcut.pressTooltipLabel',
{
defaultMessage: 'Press',
}
);

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { useCallback, useMemo, useState } from 'react';
import { FluidDragActions } from 'react-beautiful-dnd';

import { useAddToTimeline } from '../../../hooks/use_add_to_timeline';

import { draggableKeyDownHandler } from '../helpers';

interface Props {
closePopover?: () => void;
draggableId: string;
fieldName: string;
keyboardHandlerRef: React.MutableRefObject<HTMLDivElement | null>;
openPopover?: () => void;
}

export interface UseDraggableKeyboardWrapper {
onBlur: () => void;
onKeyDown: (keyboardEvent: React.KeyboardEvent) => void;
}

export const useDraggableKeyboardWrapper = ({
closePopover,
draggableId,
fieldName,
keyboardHandlerRef,
openPopover,
}: Props): UseDraggableKeyboardWrapper => {
const { beginDrag, cancelDrag, dragToLocation, endDrag, hasDraggableLock } = useAddToTimeline({
draggableId,
fieldName,
});
const [dragActions, setDragActions] = useState<FluidDragActions | null>(null);

const cancelDragActions = useCallback(() => {
setDragActions((prevDragAction) => {
if (prevDragAction) {
cancelDrag(prevDragAction);
return null;
}
return null;
});
}, [cancelDrag]);

const onKeyDown = useCallback(
(keyboardEvent: React.KeyboardEvent) => {
const draggableElement = document.querySelector<HTMLDivElement>(
`[data-rbd-drag-handle-draggable-id="${draggableId}"]`
);

if (draggableElement) {
if (hasDraggableLock() || (!hasDraggableLock() && keyboardEvent.key === ' ')) {
keyboardEvent.preventDefault();
keyboardEvent.stopPropagation();
}

draggableKeyDownHandler({
beginDrag,
cancelDragActions,
closePopover,
dragActions,
draggableElement,
dragToLocation,
endDrag,
keyboardEvent,
openPopover,
setDragActions,
});

keyboardHandlerRef.current?.focus(); // to handle future key presses
}
},
[
beginDrag,
cancelDragActions,
closePopover,
dragActions,
draggableId,
dragToLocation,
endDrag,
hasDraggableLock,
keyboardHandlerRef,
openPopover,
setDragActions,
]
);

const memoizedReturn = useMemo(
() => ({
onBlur: cancelDragActions,
onKeyDown,
}),
[cancelDragActions, onKeyDown]
);

return memoizedReturn;
};
125 changes: 125 additions & 0 deletions x-pack/plugins/timelines/public/components/drag_and_drop/helpers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright 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 { FluidDragActions, Position } from 'react-beautiful-dnd';
import { KEYBOARD_DRAG_OFFSET } from '../../constants';

import { stopPropagationAndPreventDefault } from '../accessibility/helpers';

/**
* Temporarily disables tab focus on child links of the draggable to work
* around an issue where tab focus becomes stuck on the interactive children
*
* NOTE: This function is (intentionally) only effective when used in a key
* event handler, because it automatically restores focus capabilities on
* the next tick.
*/
export const temporarilyDisableInteractiveChildTabIndexes = (draggableElement: HTMLDivElement) => {
const interactiveChildren = draggableElement.querySelectorAll('a, button');
interactiveChildren.forEach((interactiveChild) => {
interactiveChild.setAttribute('tabindex', '-1'); // DOM mutation
});

// restore the default tabindexs on the next tick:
setTimeout(() => {
interactiveChildren.forEach((interactiveChild) => {
interactiveChild.setAttribute('tabindex', '0'); // DOM mutation
});
}, 0);
};

export const draggableKeyDownHandler = ({
beginDrag,
cancelDragActions,
closePopover,
draggableElement,
dragActions,
dragToLocation,
endDrag,
keyboardEvent,
openPopover,
setDragActions,
}: {
beginDrag: () => FluidDragActions | null;
cancelDragActions: () => void;
closePopover?: () => void;
draggableElement: HTMLDivElement;
dragActions: FluidDragActions | null;
dragToLocation: ({
// eslint-disable-next-line @typescript-eslint/no-shadow
dragActions,
position,
}: {
dragActions: FluidDragActions | null;
position: Position;
}) => void;
keyboardEvent: React.KeyboardEvent;
endDrag: (dragActions: FluidDragActions | null) => void;
openPopover?: () => void;
setDragActions: (value: React.SetStateAction<FluidDragActions | null>) => void;
}) => {
let currentPosition: DOMRect | null = null;

switch (keyboardEvent.key) {
case ' ':
if (!dragActions) {
// start dragging, because space was pressed
if (closePopover != null) {
closePopover();
}
setDragActions(beginDrag());
} else {
// end dragging, because space was pressed
endDrag(dragActions);
setDragActions(null);
}
break;
case 'Escape':
cancelDragActions();
break;
case 'Tab':
// IMPORTANT: we do NOT want to stop propagation and prevent default when Tab is pressed
temporarilyDisableInteractiveChildTabIndexes(draggableElement);
break;
case 'ArrowUp':
currentPosition = draggableElement.getBoundingClientRect();
dragToLocation({
dragActions,
position: { x: currentPosition.x, y: currentPosition.y - KEYBOARD_DRAG_OFFSET },
});
break;
case 'ArrowDown':
currentPosition = draggableElement.getBoundingClientRect();
dragToLocation({
dragActions,
position: { x: currentPosition.x, y: currentPosition.y + KEYBOARD_DRAG_OFFSET },
});
break;
case 'ArrowLeft':
currentPosition = draggableElement.getBoundingClientRect();
dragToLocation({
dragActions,
position: { x: currentPosition.x - KEYBOARD_DRAG_OFFSET, y: currentPosition.y },
});
break;
case 'ArrowRight':
currentPosition = draggableElement.getBoundingClientRect();
dragToLocation({
dragActions,
position: { x: currentPosition.x + KEYBOARD_DRAG_OFFSET, y: currentPosition.y },
});
break;
case 'Enter':
stopPropagationAndPreventDefault(keyboardEvent); // prevents the first item in the popover from getting an errant ENTER
if (!dragActions && openPopover != null) {
openPopover();
}
break;
default:
break;
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

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

import { rgba } from 'polished';
import React from 'react';
import styled from 'styled-components';

interface WidthProp {
width?: number;
}

const Field = styled.div.attrs<WidthProp>(({ width }) => {
if (width) {
return {
style: {
width: `${width}px`,
},
};
}
})<WidthProp>`
background-color: ${({ theme }) => theme.eui.euiColorEmptyShade};
border: ${({ theme }) => theme.eui.euiBorderThin};
box-shadow: 0 2px 2px -1px ${({ theme }) => rgba(theme.eui.euiColorMediumShade, 0.3)},
0 1px 5px -2px ${({ theme }) => rgba(theme.eui.euiColorMediumShade, 0.3)};
font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold};
line-height: ${({ theme }) => theme.eui.euiLineHeight};
padding: ${({ theme }) => theme.eui.paddingSizes.xs};
`;
Field.displayName = 'Field';

/**
* Renders a field (e.g. `event.action`) as a draggable badge
*/

export const DraggableFieldBadge = React.memo<{ fieldId: string; fieldWidth?: number }>(
({ fieldId, fieldWidth }) => (
<Field data-test-subj="field" width={fieldWidth}>
{fieldId}
</Field>
)
);

DraggableFieldBadge.displayName = 'DraggableFieldBadge';
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { i18n } from '@kbn/i18n';

export const CATEGORY = i18n.translate('xpack.timelines.draggables.field.categoryLabel', {
defaultMessage: 'Category',
});

export const COPY_TO_CLIPBOARD = i18n.translate(
'xpack.timelines.eventDetails.copyToClipboardTooltip',
{
defaultMessage: 'Copy to Clipboard',
}
);

export const FIELD = i18n.translate('xpack.timelines.draggables.field.fieldLabel', {
defaultMessage: 'Field',
});

export const TYPE = i18n.translate('xpack.timelines.draggables.field.typeLabel', {
defaultMessage: 'Type',
});

export const VIEW_CATEGORY = i18n.translate(
'xpack.timelines.draggables.field.viewCategoryTooltip',
{
defaultMessage: 'View Category',
}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export * from './field_badge';
10 changes: 8 additions & 2 deletions x-pack/plugins/timelines/public/components/index.tsx
Original file line number Diff line number Diff line change
@@ -11,9 +11,9 @@ import { I18nProvider } from '@kbn/i18n/react';
import { getReduxDeps } from '../store/t_grid';

import { TGrid } from './tgrid';
import { TimelineProps } from '../types';
import { TGridProps } from '../types';

export const Timeline = (props: TimelineProps) => {
export const Timeline = (props: TGridProps) => {
const reduxStuff = getReduxDeps(props.type);
if (props.type === 'standalone') {
return (
@@ -34,3 +34,9 @@ export const Timeline = (props: TimelineProps) => {

// eslint-disable-next-line import/no-default-export
export { Timeline as default };

export * from './accessibility';
export * from './drag_and_drop';
export * from './draggables';
export * from './last_updated';
export * from './loading';
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 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 } from 'enzyme';
import { I18nProvider } from '@kbn/i18n/react';
import { LastUpdatedAt } from './';

jest.mock('@kbn/i18n/react', () => {
const originalModule = jest.requireActual('@kbn/i18n/react');
const FormattedRelative = jest.fn();
FormattedRelative.mockImplementation(() => '2 minutes ago');

return {
...originalModule,
FormattedRelative,
};
});

describe('LastUpdatedAt', () => {
test('it renders correct relative time', () => {
const wrapper = mount(
<I18nProvider>
<LastUpdatedAt updatedAt={1603995240115} />
</I18nProvider>
);

expect(wrapper.text()).toEqual(' Updated 2 minutes ago');
});

test('it only renders icon if "compact" is true', () => {
const wrapper = mount(
<I18nProvider>
<LastUpdatedAt compact updatedAt={1603995240115} />
</I18nProvider>
);

expect(wrapper.text()).toEqual('');
expect(wrapper.find('[data-test-subj="last-updated-at-clock-icon"]').exists()).toBeTruthy();
});

test('it renders updating text if "showUpdating" is true', () => {
const wrapper = mount(
<I18nProvider>
<LastUpdatedAt updatedAt={1603995240115} showUpdating />
</I18nProvider>
);

expect(wrapper.text()).toEqual(' Updating...');
});
});
84 changes: 84 additions & 0 deletions x-pack/plugins/timelines/public/components/last_updated/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright 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, EuiText, EuiToolTip } from '@elastic/eui';
import { FormattedRelative } from '@kbn/i18n/react';
import React, { useEffect, useMemo, useState } from 'react';

import * as i18n from './translations';

interface LastUpdatedAtProps {
compact?: boolean;
updatedAt: number;
showUpdating?: boolean;
}

export const Updated = React.memo<{ date: number; prefix: string; updatedAt: number }>(
({ date, prefix, updatedAt }) => (
<>
{prefix}
{
<FormattedRelative
data-test-subj="last-updated-at-date"
key={`formatedRelative-${date}`}
value={new Date(updatedAt)}
/>
}
</>
)
);

Updated.displayName = 'Updated';

const prefix = ` ${i18n.UPDATED} `;

export const LastUpdatedAt = React.memo<LastUpdatedAtProps>(
({ compact = false, updatedAt, showUpdating = false }) => {
const [date, setDate] = useState(Date.now());

function tick() {
setDate(Date.now());
}

useEffect(() => {
const timerID = setInterval(() => tick(), 10000);
return () => {
clearInterval(timerID);
};
}, []);

const updateText = useMemo(() => {
if (showUpdating) {
return <span> {i18n.UPDATING}</span>;
}

if (!compact) {
return <Updated date={date} prefix={prefix} updatedAt={updatedAt} />;
}

return null;
}, [compact, date, showUpdating, updatedAt]);

return (
<EuiToolTip
data-test-subj="timeline-stream-tool-tip"
content={
<>
<Updated date={date} prefix={prefix} updatedAt={updatedAt} />
</>
}
>
<EuiText size="s">
<EuiIcon data-test-subj="last-updated-at-clock-icon" type="clock" />
{updateText}
</EuiText>
</EuiToolTip>
);
}
);

LastUpdatedAt.displayName = 'LastUpdatedAt';
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { i18n } from '@kbn/i18n';

export const UPDATING = i18n.translate('xpack.timelines.lastUpdated.updating', {
defaultMessage: 'Updating...',
});

export const UPDATED = i18n.translate('xpack.timelines.lastUpdated.updated', {
defaultMessage: 'Updated',
});
90 changes: 90 additions & 0 deletions x-pack/plugins/timelines/public/components/loading/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright 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, EuiLoadingSpinner, EuiPanel, EuiText } from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';

const SpinnerFlexItem = styled(EuiFlexItem)`
margin-right: 5px;
`;

SpinnerFlexItem.displayName = 'SpinnerFlexItem';

interface LoadingProps {
text: string;
height: number | string;
showBorder?: boolean;
width: number | string;
zIndex?: number | string;
position?: string;
}

export const LoadingPanel = React.memo<LoadingProps>(
({
height = 'auto',
showBorder = true,
text,
width,
position = 'relative',
zIndex = 'inherit',
}) => (
<LoadingStaticPanel
className="app-loading"
height={height}
width={width}
position={position}
zIndex={zIndex}
>
<LoadingStaticContentPanel>
<EuiPanel className={showBorder ? '' : 'euiPanel-loading-hide-border'}>
<EuiFlexGroup alignItems="center" direction="row" gutterSize="none">
<SpinnerFlexItem grow={false}>
<EuiLoadingSpinner size="m" />
</SpinnerFlexItem>

<EuiFlexItem grow={false}>
<EuiText>{text}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</LoadingStaticContentPanel>
</LoadingStaticPanel>
)
);

LoadingPanel.displayName = 'LoadingPanel';

export const LoadingStaticPanel = styled.div<{
height: number | string;
position: string;
width: number | string;
zIndex: number | string;
}>`
height: ${({ height }) => height};
position: ${({ position }) => position};
width: ${({ width }) => width};
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
z-index: ${({ zIndex }) => zIndex};
`;

LoadingStaticPanel.displayName = 'LoadingStaticPanel';

export const LoadingStaticContentPanel = styled.div`
flex: 0 0 auto;
align-self: center;
text-align: center;
height: fit-content;
.euiPanel.euiPanel--paddingMedium {
padding: 10px;
}
`;

LoadingStaticContentPanel.displayName = 'LoadingStaticContentPanel';
Original file line number Diff line number Diff line change
@@ -15,7 +15,6 @@ import {
ARIA_COLINDEX_ATTRIBUTE,
ARIA_ROWINDEX_ATTRIBUTE,
FIRST_ARIA_INDEX,
onKeyDownFocusHandler,
} from '@kbn/securitysolution-t-grid';
import { DEFAULT_COLUMN_MIN_WIDTH } from './constants';
import {
@@ -43,6 +42,7 @@ import { tGridActions } from '../../../types';
import { TGridModel, tGridSelectors, TimelineState } from '../../../store/t_grid';
import { useDeepEqualSelector } from '../../../hooks/use_selector';
import { plainRowRenderer } from './renderers/plain_row_renderer';
import { onKeyDownFocusHandler } from '../../accessibility';

interface OwnProps {
activePage: number;
234 changes: 234 additions & 0 deletions x-pack/plugins/timelines/public/hooks/use_add_to_timeline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
/*
* Copyright 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 d3 from 'd3';
import { useCallback } from 'react';
import { DraggableId, FluidDragActions, Position, SensorAPI } from 'react-beautiful-dnd';

import {
EMPTY_PROVIDERS_GROUP_CLASS_NAME,
HIGHLIGHTED_DROP_TARGET_CLASS_NAME,
IS_DRAGGING_CLASS_NAME,
} from '../constants';

let _sensorApiSingleton: SensorAPI;

/**
* This hook is passed (in an array) to the `sensors` prop of the
* `react-beautiful-dnd` `DragDropContext` component. Example:
*
* ```
<DragDropContext onDragEnd={onDragEnd} sensors={[useAddToTimelineSensor]}>
{children}
</DragDropContext>*
* ```
*
* As a side effect of registering this hook with the `DragDropContext`,
* the `SensorAPI` singleton is initialized. This singleton is used
* by the `useAddToTimeline` hook.
*/
export const useAddToTimelineSensor = (api: SensorAPI) => {
_sensorApiSingleton = api;
};

/**
* Returns the position of the specified element
*/
const getPosition = (element: Element): Position => {
const rect = element.getBoundingClientRect();

return { x: rect.left, y: rect.top };
};

const hasDraggableLock = () => _sensorApiSingleton != null && _sensorApiSingleton.isLockClaimed();

/**
* Returns the position of one of the following timeline drop targets
* (in the following order of preference):
* 1) The "Drop anything highlighted..." drop target
* 2) The persistent "empty" data provider group drop target
* 3) `null`, because none of the above targets exist (an error state)
*/
export const getDropTargetCoordinate = (): Position | null => {
// The placeholder in the "Drop anything highlighted here to build an OR query":
const highlighted = document.querySelector(`.${HIGHLIGHTED_DROP_TARGET_CLASS_NAME}`);

if (highlighted != null) {
return getPosition(highlighted);
}

// If at least one provider has been added to the timeline, the "Drop anything
// highlighted..." drop target won't be visible, so we need to drop into the
// empty group instead:
const emptyGroup = document.querySelector(`.${EMPTY_PROVIDERS_GROUP_CLASS_NAME}`);

if (emptyGroup != null) {
emptyGroup.scrollIntoView();
return getPosition(emptyGroup);
}

return null;
};

/**
* Returns the coordinates of the specified draggable
*/
export const getDraggableCoordinate = (draggableId: DraggableId): Position | null => {
// The placeholder in the "Drop anything highlighted here to build an OR query":
const draggable = document.querySelector(`[data-rbd-draggable-id="${draggableId}"]`);

if (draggable != null) {
return getPosition(draggable);
}

return null;
};

/**
* Animates a draggable via `requestAnimationFrame`
*/
export const animate = ({
drag,
dropWhenComplete = true,
fieldName,
values,
}: {
drag: FluidDragActions;
dropWhenComplete?: boolean;
fieldName: string;
values: Position[];
}) => {
requestAnimationFrame(() => {
if (values.length === 0) {
if (dropWhenComplete) {
setTimeout(() => drag.drop(), 0); // schedule the drop the next time around
}

return;
}

drag.move(values[0]);

animate({
drag,
dropWhenComplete,
fieldName,
values: values.slice(1),
});
});
};

/**
* This hook animates a draggable data provider to the timeline
*/
export const useAddToTimeline = ({
draggableId,
fieldName,
}: {
draggableId: DraggableId | undefined;
fieldName: string;
}): {
beginDrag: () => FluidDragActions | null;
cancelDrag: (dragActions: FluidDragActions | null) => void;
dragToLocation: ({
dragActions,
position,
}: {
dragActions: FluidDragActions | null;
position: Position;
}) => void;
endDrag: (dragActions: FluidDragActions | null) => void;
hasDraggableLock: () => boolean;
startDragToTimeline: () => void;
} => {
const startDragToTimeline = useCallback(() => {
if (_sensorApiSingleton == null) {
throw new TypeError(
'To use this hook, the companion `useAddToTimelineSensor` hook must be registered in the `sensors` prop of the `DragDropContext`.'
);
}

if (draggableId == null) {
// A request to start the animation should not have been made, because
// no draggableId was provided
return;
}

// add the dragging class, which will show the flyout data providers (if the flyout button is being displayed):
document.body.classList.add(IS_DRAGGING_CLASS_NAME);

// start the animation after the flyout data providers are visible:
setTimeout(() => {
const draggableCoordinate = getDraggableCoordinate(draggableId);
const dropTargetCoordinate = getDropTargetCoordinate();
const preDrag = _sensorApiSingleton.tryGetLock(draggableId);

if (draggableCoordinate != null && dropTargetCoordinate != null && preDrag != null) {
const steps = 10;
const points = d3.range(steps + 1).map((i) => ({
x: d3.interpolate(draggableCoordinate.x, dropTargetCoordinate.x)(i * 0.1),
y: d3.interpolate(draggableCoordinate.y, dropTargetCoordinate.y)(i * 0.1),
}));

const drag = preDrag.fluidLift(draggableCoordinate);
animate({
drag,
fieldName,
values: points,
});
} else {
document.body.classList.remove(IS_DRAGGING_CLASS_NAME); // it was not possible to perform a drag and drop
}
}, 0);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [_sensorApiSingleton, draggableId]);

const beginDrag = useCallback(() => {
if (draggableId == null) {
// A request to start the drag should not have been made, because no draggableId was provided
return null;
}

const draggableCoordinate = getDraggableCoordinate(draggableId);
const preDrag = _sensorApiSingleton.tryGetLock(draggableId);

return draggableCoordinate != null && preDrag != null
? preDrag.fluidLift(draggableCoordinate)
: null;
}, [draggableId]);

const dragToLocation = useCallback(
({ dragActions, position }: { dragActions: FluidDragActions | null; position: Position }) => {
if (dragActions == null || draggableId == null) {
return;
}

const draggableCoordinate = getDraggableCoordinate(draggableId);

if (draggableCoordinate != null) {
requestAnimationFrame(() => {
dragActions.move(position);
});
}
},
[draggableId]
);

const endDrag = useCallback((dragActions: FluidDragActions | null) => {
if (dragActions !== null) {
dragActions.drop();
}
}, []);

const cancelDrag = useCallback((dragActions: FluidDragActions | null) => {
if (dragActions !== null) {
dragActions.cancel();
}
}, []);

return { beginDrag, cancelDrag, dragToLocation, endDrag, hasDraggableLock, startDragToTimeline };
};
329 changes: 329 additions & 0 deletions x-pack/plugins/timelines/public/mock/event_details.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export const eventHit = {
_index: 'auditbeat-7.8.0-2020.11.05-000003',
_id: 'tkCt1nUBaEgqnrVSZ8R_',
_score: 0,
_type: '',
fields: {
'event.category': ['process'],
'process.ppid': [3977],
'user.name': ['jenkins'],
'process.args': ['go', 'vet', './...'],
message: ['Process go (PID: 4313) by user jenkins STARTED'],
'process.pid': [4313],
'process.working_directory': [
'/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat',
],
'process.entity_id': ['Z59cIkAAIw8ZoK0H'],
'host.ip': ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'],
'process.name': ['go'],
'event.action': ['process_started'],
'agent.type': ['auditbeat'],
'@timestamp': ['2020-11-17T14:48:08.922Z'],
'event.module': ['system'],
'event.type': ['start'],
'host.name': ['beats-ci-immutable-ubuntu-1804-1605624279743236239'],
'process.hash.sha1': ['1eac22336a41e0660fb302add9d97daa2bcc7040'],
'host.os.family': ['debian'],
'event.kind': ['event'],
'host.id': ['e59991e835905c65ed3e455b33e13bd6'],
'event.dataset': ['process'],
'process.executable': [
'/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go',
],
'source.geo.location': [{ coordinates: [118.7778, 32.0617], type: 'Point' }],
'threat.indicator': [
{
'matched.field': ['matched_field', 'other_matched_field'],
first_seen: ['2021-02-22T17:29:25.195Z'],
provider: ['yourself'],
type: ['custom'],
'matched.atomic': ['matched_atomic'],
lazer: [
{
'great.field': ['grrrrr'],
},
{
'great.field': ['grrrrr_2'],
},
],
},
{
'matched.field': ['matched_field_2'],
first_seen: ['2021-02-22T17:29:25.195Z'],
provider: ['other_you'],
type: ['custom'],
'matched.atomic': ['matched_atomic_2'],
lazer: [
{
'great.field': [
{
wowoe: [
{
fooooo: ['grrrrr'],
},
],
astring: 'cool',
aNumber: 1,
anObject: {
neat: true,
},
},
],
},
],
},
],
},
_source: {},
sort: ['1605624488922', 'beats-ci-immutable-ubuntu-1804-1605624279743236239'],
aggregations: {},
};

export const eventDetailsFormattedFields = [
{
category: 'event',
field: 'event.category',
isObjectArray: false,
originalValue: ['process'],
values: ['process'],
},
{
category: 'process',
field: 'process.ppid',
isObjectArray: false,
originalValue: ['3977'],
values: ['3977'],
},
{
category: 'user',
field: 'user.name',
isObjectArray: false,
originalValue: ['jenkins'],
values: ['jenkins'],
},
{
category: 'process',
field: 'process.args',
isObjectArray: false,
originalValue: ['go', 'vet', './...'],
values: ['go', 'vet', './...'],
},
{
category: 'base',
field: 'message',
isObjectArray: false,
originalValue: ['Process go (PID: 4313) by user jenkins STARTED'],
values: ['Process go (PID: 4313) by user jenkins STARTED'],
},
{
category: 'process',
field: 'process.pid',
isObjectArray: false,
originalValue: ['4313'],
values: ['4313'],
},
{
category: 'process',
field: 'process.working_directory',
isObjectArray: false,
originalValue: [
'/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat',
],
values: [
'/var/lib/jenkins/workspace/Beats_beats_PR-22624/src/github.com/elastic/beats/libbeat',
],
},
{
category: 'process',
field: 'process.entity_id',
isObjectArray: false,
originalValue: ['Z59cIkAAIw8ZoK0H'],
values: ['Z59cIkAAIw8ZoK0H'],
},
{
category: 'host',
field: 'host.ip',
isObjectArray: false,
originalValue: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'],
values: ['10.224.1.237', 'fe80::4001:aff:fee0:1ed', '172.17.0.1'],
},
{
category: 'process',
field: 'process.name',
isObjectArray: false,
originalValue: ['go'],
values: ['go'],
},
{
category: 'event',
field: 'event.action',
isObjectArray: false,
originalValue: ['process_started'],
values: ['process_started'],
},
{
category: 'agent',
field: 'agent.type',
isObjectArray: false,
originalValue: ['auditbeat'],
values: ['auditbeat'],
},
{
category: 'base',
field: '@timestamp',
isObjectArray: false,
originalValue: ['2020-11-17T14:48:08.922Z'],
values: ['2020-11-17T14:48:08.922Z'],
},
{
category: 'event',
field: 'event.module',
isObjectArray: false,
originalValue: ['system'],
values: ['system'],
},
{
category: 'event',
field: 'event.type',
isObjectArray: false,
originalValue: ['start'],
values: ['start'],
},
{
category: 'host',
field: 'host.name',
isObjectArray: false,
originalValue: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'],
values: ['beats-ci-immutable-ubuntu-1804-1605624279743236239'],
},
{
category: 'process',
field: 'process.hash.sha1',
isObjectArray: false,
originalValue: ['1eac22336a41e0660fb302add9d97daa2bcc7040'],
values: ['1eac22336a41e0660fb302add9d97daa2bcc7040'],
},
{
category: 'host',
field: 'host.os.family',
isObjectArray: false,
originalValue: ['debian'],
values: ['debian'],
},
{
category: 'event',
field: 'event.kind',
isObjectArray: false,
originalValue: ['event'],
values: ['event'],
},
{
category: 'host',
field: 'host.id',
isObjectArray: false,
originalValue: ['e59991e835905c65ed3e455b33e13bd6'],
values: ['e59991e835905c65ed3e455b33e13bd6'],
},
{
category: 'event',
field: 'event.dataset',
isObjectArray: false,
originalValue: ['process'],
values: ['process'],
},
{
category: 'process',
field: 'process.executable',
isObjectArray: false,
originalValue: [
'/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go',
],
values: [
'/var/lib/jenkins/workspace/Beats_beats_PR-22624/.gvm/versions/go1.14.7.linux.amd64/bin/go',
],
},
{
category: 'source',
field: 'source.geo.location',
isObjectArray: true,
originalValue: [`{"lon":118.7778,"lat":32.0617}`],
values: [`{"lon":118.7778,"lat":32.0617}`],
},
{
category: 'threat',
field: 'threat.indicator.matched.field',
values: ['matched_field', 'other_matched_field', 'matched_field_2'],
originalValue: ['matched_field', 'other_matched_field', 'matched_field_2'],
isObjectArray: false,
},
{
category: 'threat',
field: 'threat.indicator.first_seen',
values: ['2021-02-22T17:29:25.195Z'],
originalValue: ['2021-02-22T17:29:25.195Z'],
isObjectArray: false,
},
{
category: 'threat',
field: 'threat.indicator.provider',
values: ['yourself', 'other_you'],
originalValue: ['yourself', 'other_you'],
isObjectArray: false,
},
{
category: 'threat',
field: 'threat.indicator.type',
values: ['custom'],
originalValue: ['custom'],
isObjectArray: false,
},
{
category: 'threat',
field: 'threat.indicator.matched.atomic',
values: ['matched_atomic', 'matched_atomic_2'],
originalValue: ['matched_atomic', 'matched_atomic_2'],
isObjectArray: false,
},
{
category: 'threat',
field: 'threat.indicator.lazer.great.field',
values: ['grrrrr', 'grrrrr_2'],
originalValue: ['grrrrr', 'grrrrr_2'],
isObjectArray: false,
},
{
category: 'threat',
field: 'threat.indicator.lazer.great.field.wowoe.fooooo',
values: ['grrrrr'],
originalValue: ['grrrrr'],
isObjectArray: false,
},
{
category: 'threat',
field: 'threat.indicator.lazer.great.field.astring',
values: ['cool'],
originalValue: ['cool'],
isObjectArray: false,
},
{
category: 'threat',
field: 'threat.indicator.lazer.great.field.aNumber',
values: ['1'],
originalValue: ['1'],
isObjectArray: false,
},
{
category: 'threat',
field: 'threat.indicator.lazer.great.field.neat',
values: ['true'],
originalValue: ['true'],
isObjectArray: false,
},
];
20 changes: 20 additions & 0 deletions x-pack/plugins/timelines/public/plugin.ts
Original file line number Diff line number Diff line change
@@ -9,6 +9,8 @@ import { TimelinesPluginSetup, TGridProps } from './types';
import { getTimelineLazy } from './methods';
import { tGridActions, getReduxDeps, tGridSelectors } from './store/t_grid';
import { initialTGridState, tGridReducer } from './store/t_grid/reducer';
import { useAddToTimelineSensor } from './hooks/use_add_to_timeline';
import { onKeyDownFocusHandler } from './components/accessibility';

export class TimelinesPlugin implements Plugin<TimelinesPluginSetup> {
constructor(private readonly initializerContext: PluginInitializerContext) {}
@@ -34,6 +36,24 @@ export class TimelinesPlugin implements Plugin<TimelinesPluginSetup> {
getCreatedTgridStore: (type: TGridProps['type']) => {
return getReduxDeps(type);
},
getUseAddToTimelineSensor: () => {
return useAddToTimelineSensor;
},
getLoading: () => {

},
getLastUpdated: () => {

},
getDraggables: () => {

},
getDragAndDrop: () => {

},
getOnKeyDownFocusHandler: () => {
return onKeyDownFocusHandler;
},
};
}

7 changes: 7 additions & 0 deletions x-pack/plugins/timelines/public/types.ts
Original file line number Diff line number Diff line change
@@ -18,6 +18,13 @@ export interface TimelinesPluginSetup {
getCreatedTgridStore?: (
type: 'standalone' | 'embedded'
) => ReduxDeps | ((type: 'standalone' | 'embedded') => Store);
getLoading: () => any;
getLastUpdated: () => any;
getDraggables: () => any;
getDragAndDrop: () => any;
getOnKeyDownFocusHandler: () => any;
getUseAddToTimelineSensor: () => any;
getOnFocusReFocusDraggable: () => any;
}
export interface ReduxDeps {
actions: typeof tGridActions;