[8.0] [Security Solution] Adjusts the width of the Actions column and action icon buttons (#118454) (#118598)

* [Security Solution] Adjusts the width of the `Actions` column and action icon buttons (#118454)

## [Security Solution] Adjusts the width of the `Actions` column and action icon buttons

This PR adjusts the width of the `Actions` column, and normalizes the action icon button sizes throughout the Security Solution, per https://github.com/elastic/kibana/issues/115726

### Before / after screenshots

This section provides before / after screenshots for the following views:

- Alerts
- Alerts > Event rendered
- Rules > Details
- Rules > Details > Event rendered
- Host > Events
- Host > External alerts
- Network > External alerts
- Timeline > Query tab
- Timeline > Correlation tab
- Timeline > Pinned tab
- Observability > alerts (no change)

#### Alerts (before)

![01-security_alerts_before](https://user-images.githubusercontent.com/4459398/141429498-a6040f8b-5bfb-468e-aa1a-993caa7f179c.png)

#### Alerts (after)

![01a-security_alerts_after](https://user-images.githubusercontent.com/4459398/141429618-8ad313e1-fabc-424e-9e7d-c24240861c1d.png)

#### Alerts > Event rendered (before)

![02-security_alerts_event_rendered_before](https://user-images.githubusercontent.com/4459398/141430881-2bfeb57a-9881-47f1-99e4-cc7eadcfff69.png)

#### Alerts > Event rendered (after)

![02a-security_alerts_event_rendered_after](https://user-images.githubusercontent.com/4459398/141430976-88f8099a-81b1-4f1c-99a2-26f86218f909.png)

#### Rules > Details (before)

![03-security_rules_details_before](https://user-images.githubusercontent.com/4459398/141431149-a308f171-a170-4ce9-9616-77e5c08dc406.png)

#### Rules > Details (after)

![03a-security_rules_details_after](https://user-images.githubusercontent.com/4459398/141431221-06701540-97bb-400a-97bf-f2d22cd65caf.png)

#### Rules > Details > Event rendered (before)

![04-security_rule_details_event_rendered_before](https://user-images.githubusercontent.com/4459398/141431394-12b29689-41c8-44b6-b69f-7796f99c5424.png)

#### Rules > Details > Event rendered (after)

![04a-security_rule_details_event_rendered_after](https://user-images.githubusercontent.com/4459398/141431477-049804c0-1455-4216-a241-a44df5c9d398.png)

#### Host > Events (before)

![05-host_events_before](https://user-images.githubusercontent.com/4459398/141431858-31116980-47f7-4779-af26-3b3785638137.png)

#### Host > Events (after)

![05a-host_events_after](https://user-images.githubusercontent.com/4459398/141431956-664f86b9-2ad7-4281-bf82-8278fa23c755.png)

#### Host > External alerts (before)

![06-host_external_alerts_before](https://user-images.githubusercontent.com/4459398/141432103-8cc9c10e-4d2d-42ec-a62c-a1e5867bf2d8.png)

#### Host > External alerts (after)

![06a-host_external_alerts_after](https://user-images.githubusercontent.com/4459398/141432185-4d7e4007-dea9-47f3-af4b-1719f338a5ba.png)

#### Network > External alerts (before)

![07-network_external_alerts_before](https://user-images.githubusercontent.com/4459398/141432331-2bb5a714-f733-4c97-91dc-73ff76633daa.png)

#### Network > External alerts (after)

![07a-network_external_alerts_after](https://user-images.githubusercontent.com/4459398/141432428-b7b20450-db87-44ab-8014-cf4d6032dfe3.png)

#### Timeline > Query tab (before)

![08-timeline_query_tab_before](https://user-images.githubusercontent.com/4459398/141432638-e484813b-275d-4eff-aa38-1705f913ce59.png)

#### Timeline > Query tab (after)

![08a-timeline_query_tab_after](https://user-images.githubusercontent.com/4459398/141434461-1d36bba5-8fd1-484a-bacd-733aede95815.png)

#### Timeline > Correlation tab (before)

![09-timeline_correlation_tab_before](https://user-images.githubusercontent.com/4459398/141434637-33f05447-e3d3-4eac-b38a-3612945e8379.png)

#### Timeline > Correlation tab (after)

![09a-timeline_correlation_tab_after](https://user-images.githubusercontent.com/4459398/141434751-250fd26b-25fc-48cc-8a06-dbb17e53dce7.png)

#### Timeline > Pinned tab (before)

![10-timeline_pinned_tab_before](https://user-images.githubusercontent.com/4459398/141434893-3f2b3d17-7e4b-4e0c-9096-ab1ee57f096f.png)

#### Timeline > Pinned tab (after)

![10a-timeline_pinned_tab_after](https://user-images.githubusercontent.com/4459398/141435431-26eac065-bce4-4a25-99fd-095d447fb6f3.png)

#### Observability > alerts (before)

![11-observability_alerts_before](https://user-images.githubusercontent.com/4459398/141435607-da059e9c-af03-4a21-bb1b-e47d44d61dde.png)

#### Observability > alerts (after / no change)

![11a-observability_alerts_after_no_change](https://user-images.githubusercontent.com/4459398/141435696-52bcc5e1-6823-4b6a-b2da-32e3f8733dc8.png)

### Additional details

- Per [this comment](https://github.com/elastic/kibana/issues/115726#issuecomment-962077067) from @monina-n , the size of all action buttons have been normalized match the size off the `...` overflow button (`28 x 32` at the time of this writing) via the `EuiButtonIcon` `size` prop:

```
size="s"
```

- The horizontal alignment of the `Analyze event` icon was updated by the EUI team in the following PR: https://github.com/elastic/eui/pull/5365

# Conflicts:
#	x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx

* - removed createFieldComponent prop
This commit is contained in:
Andrew Goldstein 2021-11-15 17:07:02 -07:00 committed by GitHub
parent 81f69039af
commit 6f17707bab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 309 additions and 1205 deletions

View file

@ -21,6 +21,7 @@ import { SourcererScopeName } from '../../store/sourcerer/model';
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants';
import type { EntityType } from '../../../../../timelines/common';
import { getDefaultControlColumn } from '../../../timelines/components/timeline/body/control_columns';
export interface OwnProps {
end: string;
@ -79,6 +80,7 @@ const AlertsTableComponent: React.FC<Props> = ({
const dispatch = useDispatch();
const alertsFilter = useMemo(() => [...defaultAlertsFilters, ...pageFilters], [pageFilters]);
const { filterManager } = useKibana().services.data.query;
const ACTION_BUTTON_COUNT = 3;
const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled');
@ -104,6 +106,8 @@ const AlertsTableComponent: React.FC<Props> = ({
);
}, [dispatch, filterManager, tGridEnabled, timelineId]);
const leadingControlColumns = useMemo(() => getDefaultControlColumn(ACTION_BUTTON_COUNT), []);
return (
<StatefulEventsViewer
pageFilters={alertsFilter}
@ -112,6 +116,7 @@ const AlertsTableComponent: React.FC<Props> = ({
end={endDate}
entityType={entityType}
id={timelineId}
leadingControlColumns={leadingControlColumns}
renderCellValue={DefaultCellRenderer}
rowRenderers={defaultRowRenderers}
scopeId={SourcererScopeName.default}

View file

@ -1,485 +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 { waitFor, act } from '@testing-library/react';
import useResizeObserver from 'use-resize-observer/polyfilled';
import '../../mock/match_media';
import { mockIndexNames, mockIndexPattern, TestProviders } from '../../mock';
import { mockEventViewerResponse, mockEventViewerResponseWithEvents } from './mock';
import { StatefulEventsViewer } from '.';
import { EventsViewer } from './events_viewer';
import { defaultHeaders } from './default_headers';
import { useSourcererDataView } from '../../containers/sourcerer';
import {
mockBrowserFields,
mockDocValueFields,
mockRuntimeMappings,
} from '../../containers/source/mock';
import { eventsDefaultModel } from './default_model';
import { useMountAppended } from '../../utils/use_mount_appended';
import { inputsModel } from '../../store/inputs';
import { TimelineId, SortDirection } from '../../../../common/types/timeline';
import { KqlMode } from '../../../timelines/store/timeline/model';
import { EntityType } from '../../../../../timelines/common';
import { AlertsTableFilterGroup } from '../../../detections/components/alerts_table/alerts_filter_group';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
import { useTimelineEvents } from '../../../timelines/containers';
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
import { defaultCellActions } from '../../lib/cell_actions/default_cell_actions';
import { mockTimelines } from '../../mock/mock_timelines_plugin';
jest.mock('../../lib/kibana', () => ({
useKibana: () => ({
services: {
application: {
navigateToApp: jest.fn(),
getUrlForApp: jest.fn(),
capabilities: {
siem: { crud_alerts: true, read_alerts: true },
},
},
uiSettings: {
get: jest.fn(),
},
savedObjects: {
client: {},
},
timelines: { ...mockTimelines },
},
}),
useToasts: jest.fn().mockReturnValue({
addError: jest.fn(),
addSuccess: jest.fn(),
addWarning: jest.fn(),
}),
useGetUserCasesPermissions: jest.fn(),
useDateFormat: jest.fn(),
useTimeZone: jest.fn(),
}));
jest.mock('../../hooks/use_experimental_features');
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
jest.mock('../../../timelines/components/graph_overlay', () => ({
GraphOverlay: jest.fn(() => <div />),
}));
const mockDispatch = jest.fn();
jest.mock('react-redux', () => {
const original = jest.requireActual('react-redux');
return {
...original,
useDispatch: () => mockDispatch,
};
});
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
...original,
useDataGridColumnSorting: jest.fn(),
};
});
jest.mock('../../../timelines/containers', () => ({
useTimelineEvents: jest.fn(),
}));
jest.mock('../../components/url_state/normalize_time_range.ts');
const mockUseSourcererDataView: jest.Mock = useSourcererDataView as jest.Mock;
jest.mock('../../containers/sourcerer');
const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock;
jest.mock('use-resize-observer/polyfilled');
mockUseResizeObserver.mockImplementation(() => ({}));
const mockUseTimelineEvents: jest.Mock = useTimelineEvents as jest.Mock;
jest.mock('../../../timelines/containers');
const from = '2019-08-26T22:10:56.791Z';
const to = '2019-08-27T22:10:56.794Z';
const defaultMocks = {
browserFields: mockBrowserFields,
docValueFields: mockDocValueFields,
runtimeMappings: mockRuntimeMappings,
indexPattern: mockIndexPattern,
loading: false,
selectedPatterns: mockIndexNames,
};
const utilityBar = (refetch: inputsModel.Refetch, totalCount: number) => (
<div data-test-subj="mock-utility-bar" />
);
const eventsViewerDefaultProps = {
browserFields: {},
columns: [],
dataProviders: [],
deletedEventIds: [],
docValueFields: [],
end: to,
entityType: EntityType.ALERTS,
filters: [],
id: TimelineId.detectionsPage,
indexNames: mockIndexNames,
indexPattern: mockIndexPattern,
isLive: false,
isLoadingIndexPattern: false,
itemsPerPage: 10,
itemsPerPageOptions: [],
kqlMode: 'filter' as KqlMode,
query: {
query: '',
language: 'kql',
},
renderCellValue: DefaultCellRenderer,
rowRenderers: defaultRowRenderers,
runtimeMappings: {},
start: from,
sort: [
{
columnId: 'foo',
columnType: 'number',
sortDirection: 'asc' as SortDirection,
},
],
scopeId: SourcererScopeName.timeline,
utilityBar,
};
describe('EventsViewer', () => {
const mount = useMountAppended();
let testProps = {
defaultCellActions,
defaultModel: eventsDefaultModel,
end: to,
entityType: EntityType.ALERTS,
id: TimelineId.test,
renderCellValue: DefaultCellRenderer,
rowRenderers: defaultRowRenderers,
start: from,
scopeId: SourcererScopeName.timeline,
};
beforeEach(() => {
mockUseTimelineEvents.mockReset();
});
beforeAll(() => {
mockUseSourcererDataView.mockImplementation(() => defaultMocks);
});
describe('event details', () => {
useIsExperimentalFeatureEnabledMock.mockReturnValue(false);
beforeEach(() => {
mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponseWithEvents]);
});
test('call the right reduce action to show event details', async () => {
const wrapper = mount(
<TestProviders>
<StatefulEventsViewer {...testProps} />
</TestProviders>
);
act(() => {
wrapper.find(`[data-test-subj="expand-event"]`).first().simulate('click');
});
await waitFor(() => {
expect(mockDispatch).toBeCalledTimes(3);
expect(mockDispatch.mock.calls[1][0]).toEqual({
payload: {
id: 'test',
isLoading: false,
},
type: 'x-pack/timelines/t-grid/UPDATE_LOADING',
});
});
});
});
describe('rendering', () => {
beforeEach(() => {
mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]);
});
test('it renders the "Showing..." subtitle with the expected event count by default', () => {
const wrapper = mount(
<TestProviders>
<StatefulEventsViewer {...testProps} />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().text()).toEqual(
'Showing: 12 events'
);
});
test('should not render the "Showing..." subtitle with the expected event count if showTotalCount is set to false ', () => {
const disableSubTitle = {
...eventsViewerDefaultProps,
showTotalCount: false,
};
const wrapper = mount(
<TestProviders>
<EventsViewer {...disableSubTitle} graphEventId="a valid id" />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().text()).toEqual('');
});
test('it renders the Fields Browser as a settings gear', () => {
const wrapper = mount(
<TestProviders>
<StatefulEventsViewer {...testProps} />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="field-browser"]`).first().exists()).toBe(true);
});
test('it renders the footer containing the pagination', () => {
const wrapper = mount(
<TestProviders>
<StatefulEventsViewer {...testProps} />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="timeline-pagination"]`).first().exists()).toBe(true);
});
defaultHeaders.forEach((header) => {
test(`it renders the ${header.id} default EventsViewer column header`, () => {
testProps = {
...testProps,
// Update with a new id, to force columns back to default.
id: TimelineId.alternateTest,
};
const wrapper = mount(
<TestProviders>
<StatefulEventsViewer {...testProps} />
</TestProviders>
);
defaultHeaders.forEach((h) => {
expect(wrapper.find(`[data-test-subj="header-text-${header.id}"]`).first().exists()).toBe(
true
);
});
});
});
});
describe('loading', () => {
beforeAll(() => {
mockUseSourcererDataView.mockImplementation(() => ({ ...defaultMocks, loading: true }));
});
beforeEach(() => {
mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]);
});
test('it does NOT render fetch index pattern is loading', () => {
const wrapper = mount(
<TestProviders>
<StatefulEventsViewer {...testProps} />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe(
false
);
});
test('it does NOT render when start is empty', () => {
testProps = {
...testProps,
start: '',
};
const wrapper = mount(
<TestProviders>
<StatefulEventsViewer {...testProps} />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe(
false
);
});
test('it does NOT render when end is empty', () => {
testProps = {
...testProps,
end: '',
};
const wrapper = mount(
<TestProviders>
<StatefulEventsViewer {...testProps} />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe(
false
);
});
});
describe('headerFilterGroup', () => {
beforeEach(() => {
mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]);
});
test('it renders the provided headerFilterGroup', () => {
const wrapper = mount(
<TestProviders>
<EventsViewer
{...eventsViewerDefaultProps}
graphEventId={undefined}
headerFilterGroup={
<AlertsTableFilterGroup status={'open'} onFilterGroupChanged={jest.fn()} />
}
/>
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="alerts-table-filter-group"]`).exists()).toBe(true);
});
test('it has a visible HeaderFilterGroupWrapper when Resolver is NOT showing, because graphEventId is undefined', () => {
const wrapper = mount(
<TestProviders>
<EventsViewer
{...eventsViewerDefaultProps}
graphEventId={undefined}
headerFilterGroup={
<AlertsTableFilterGroup status={'open'} onFilterGroupChanged={jest.fn()} />
}
/>
</TestProviders>
);
expect(
wrapper.find(`[data-test-subj="header-filter-group-wrapper"]`).first()
).not.toHaveStyleRule('visibility', 'hidden');
});
test('it has a visible HeaderFilterGroupWrapper when Resolver is NOT showing, because graphEventId is an empty string', () => {
const wrapper = mount(
<TestProviders>
<EventsViewer
{...eventsViewerDefaultProps}
graphEventId=""
headerFilterGroup={
<AlertsTableFilterGroup status={'open'} onFilterGroupChanged={jest.fn()} />
}
/>
</TestProviders>
);
expect(
wrapper.find(`[data-test-subj="header-filter-group-wrapper"]`).first()
).not.toHaveStyleRule('visibility', 'hidden');
});
test('it does NOT have a visible HeaderFilterGroupWrapper when Resolver is showing, because graphEventId is a valid id', () => {
const wrapper = mount(
<TestProviders>
<EventsViewer
{...eventsViewerDefaultProps}
graphEventId="a valid id"
headerFilterGroup={
<AlertsTableFilterGroup status={'open'} onFilterGroupChanged={jest.fn()} />
}
/>
</TestProviders>
);
expect(
wrapper.find(`[data-test-subj="header-filter-group-wrapper"]`).first()
).toHaveStyleRule('visibility', 'hidden');
});
test('it (still) renders an invisible headerFilterGroup (to maintain state while hidden) when Resolver is showing, because graphEventId is a valid id', () => {
const wrapper = mount(
<TestProviders>
<EventsViewer
{...eventsViewerDefaultProps}
graphEventId="a valid id"
headerFilterGroup={
<AlertsTableFilterGroup status={'open'} onFilterGroupChanged={jest.fn()} />
}
/>
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="alerts-table-filter-group"]`).exists()).toBe(true);
});
});
describe('utilityBar', () => {
beforeEach(() => {
mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]);
});
test('it renders the provided utilityBar when Resolver is NOT showing, because graphEventId is undefined', () => {
const wrapper = mount(
<TestProviders>
<EventsViewer {...eventsViewerDefaultProps} graphEventId={undefined} />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="mock-utility-bar"]`).exists()).toBe(true);
});
test('it renders the provided utilityBar when Resolver is NOT showing, because graphEventId is an empty string', () => {
const wrapper = mount(
<TestProviders>
<EventsViewer {...eventsViewerDefaultProps} graphEventId="" />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="mock-utility-bar"]`).exists()).toBe(true);
});
test('it does NOT render the provided utilityBar when Resolver is showing, because graphEventId is a valid id', () => {
const wrapper = mount(
<TestProviders>
<EventsViewer {...eventsViewerDefaultProps} graphEventId="a valid id" />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="mock-utility-bar"]`).exists()).toBe(false);
});
});
describe('header inspect button', () => {
beforeEach(() => {
mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]);
});
test('it renders the inspect button when Resolver is NOT showing, because graphEventId is undefined', () => {
const wrapper = mount(
<TestProviders>
<EventsViewer {...eventsViewerDefaultProps} graphEventId={undefined} />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="inspect-icon-button"]`).exists()).toBe(true);
});
test('it renders the inspect button when Resolver is NOT showing, because graphEventId is an empty string', () => {
const wrapper = mount(
<TestProviders>
<EventsViewer {...eventsViewerDefaultProps} graphEventId="" />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="inspect-icon-button"]`).exists()).toBe(true);
});
test('it does NOT render the inspect button when Resolver is showing, because graphEventId is a valid id', () => {
const wrapper = mount(
<TestProviders>
<EventsViewer {...eventsViewerDefaultProps} graphEventId="a valid id" />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="inspect-icon-button"]`).exists()).toBe(false);
});
});
});

View file

@ -1,395 +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, EuiPanel } from '@elastic/eui';
import { isEmpty } from 'lodash/fp';
import React, { useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import deepEqual from 'fast-deep-equal';
import { useDispatch } from 'react-redux';
import { DataViewBase, Filter, Query } from '@kbn/es-query';
import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { Direction } from '../../../../common/search_strategy';
import { BrowserFields, DocValueFields } from '../../containers/source';
import { useTimelineEvents } from '../../../timelines/containers';
import { useKibana } from '../../lib/kibana';
import { KqlMode } from '../../../timelines/store/timeline/model';
import { HeaderSection } from '../header_section';
import { defaultHeaders } from '../../../timelines/components/timeline/body/column_headers/default_headers';
import { Sort } from '../../../timelines/components/timeline/body/sort';
import { StatefulBody } from '../../../timelines/components/timeline/body';
import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider';
import { Footer, footerHeight } from '../../../timelines/components/timeline/footer';
import {
calculateTotalPages,
combineQueries,
resolverIsShowing,
} from '../../../timelines/components/timeline/helpers';
import { TimelineRefetch } from '../../../timelines/components/timeline/refetch_timeline';
import { EventDetailsWidthProvider } from './event_details_width_context';
import * as i18n from './translations';
import { esQuery } from '../../../../../../../src/plugins/data/public';
import { inputsModel } from '../../store';
import { ExitFullScreen } from '../exit_full_screen';
import { useGlobalFullScreen } from '../../containers/use_full_screen';
import {
ColumnHeaderOptions,
ControlColumnProps,
RowRenderer,
TimelineId,
TimelineTabs,
} from '../../../../common/types/timeline';
import { GraphOverlay } from '../../../timelines/components/graph_overlay';
import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering';
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER } from '../../../timelines/components/timeline/styles';
import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline';
import { useDeepEqualSelector } from '../../hooks/use_selector';
import { defaultControlColumn } from '../../../timelines/components/timeline/body/control_columns';
import { TimelineContext } from '../../../../../timelines/public';
export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px
const UTILITY_BAR_HEIGHT = 19; // px
const COMPACT_HEADER_HEIGHT = EVENTS_VIEWER_HEADER_HEIGHT - UTILITY_BAR_HEIGHT; // px
const UtilityBar = styled.div`
height: ${UTILITY_BAR_HEIGHT}px;
`;
const TitleText = styled.span`
margin-right: 12px;
`;
const StyledEuiPanel = styled(EuiPanel)<{ $isFullScreen: boolean }>`
display: flex;
flex-direction: column;
${({ $isFullScreen }) =>
$isFullScreen &&
`
border: 0;
box-shadow: none;
padding-top: 0;
padding-bottom: 0;
`}
`;
const TitleFlexGroup = styled(EuiFlexGroup)`
margin-top: 8px;
`;
const EventsContainerLoading = styled.div.attrs(({ className = '' }) => ({
className: `${SELECTOR_TIMELINE_GLOBAL_CONTAINER} ${className}`,
}))`
width: 100%;
overflow: hidden;
flex: 1;
display: flex;
flex-direction: column;
`;
const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible: boolean }>`
overflow: hidden;
margin: 0;
display: ${({ $visible }) => ($visible ? 'flex' : 'none')};
`;
const ScrollableFlexItem = styled(EuiFlexItem)`
overflow: auto;
`;
/**
* Hides stateful headerFilterGroup implementations, but prevents the component
* from being unmounted, to preserve the state of the component
*/
const HeaderFilterGroupWrapper = styled.header<{ show: boolean }>`
${({ show }) => (show ? '' : 'visibility: hidden;')}
`;
interface Props {
browserFields: BrowserFields;
columns: ColumnHeaderOptions[];
dataProviders: DataProvider[];
deletedEventIds: Readonly<string[]>;
docValueFields: DocValueFields[];
end: string;
filters: Filter[];
headerFilterGroup?: React.ReactNode;
id: TimelineId;
indexNames: string[];
indexPattern: DataViewBase;
isLive: boolean;
isLoadingIndexPattern: boolean;
itemsPerPage: number;
itemsPerPageOptions: number[];
kqlMode: KqlMode;
query: Query;
onRuleChange?: () => void;
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
rowRenderers: RowRenderer[];
runtimeMappings: MappingRuntimeFields;
start: string;
sort: Sort[];
showTotalCount?: boolean;
utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode;
// If truthy, the graph viewer (Resolver) is showing
graphEventId: string | undefined;
}
const EventsViewerComponent: React.FC<Props> = ({
browserFields,
columns,
dataProviders,
deletedEventIds,
docValueFields,
end,
filters,
headerFilterGroup,
id,
indexNames,
indexPattern,
isLive,
isLoadingIndexPattern,
itemsPerPage,
itemsPerPageOptions,
kqlMode,
onRuleChange,
query,
renderCellValue,
rowRenderers,
runtimeMappings,
start,
sort,
showTotalCount = true,
utilityBar,
graphEventId,
}) => {
const dispatch = useDispatch();
const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen();
const columnsHeader = isEmpty(columns) ? defaultHeaders : columns;
const kibana = useKibana();
const [isQueryLoading, setIsQueryLoading] = useState(false);
useEffect(() => {
dispatch(timelineActions.updateIsLoading({ id, isLoading: isQueryLoading }));
}, [dispatch, id, isQueryLoading]);
const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
const unit = useMemo(() => (n: number) => i18n.UNIT(n), []);
const { queryFields, title } = useDeepEqualSelector((state) => getManageTimeline(state, id));
const justTitle = useMemo(() => <TitleText data-test-subj="title">{title}</TitleText>, [title]);
const titleWithExitFullScreen = useMemo(
() => (
<TitleFlexGroup alignItems="center" data-test-subj="title-flex-group" gutterSize="none">
<EuiFlexItem grow={false}>{justTitle}</EuiFlexItem>
<EuiFlexItem grow={false}>
<ExitFullScreen fullScreen={globalFullScreen} setFullScreen={setGlobalFullScreen} />
</EuiFlexItem>
</TitleFlexGroup>
),
[globalFullScreen, justTitle, setGlobalFullScreen]
);
const combinedQueries = combineQueries({
config: esQuery.getEsQueryConfig(kibana.services.uiSettings),
dataProviders,
indexPattern,
browserFields,
filters,
kqlQuery: query,
kqlMode,
isEventViewer: true,
});
const canQueryTimeline = useMemo(
() =>
combinedQueries != null &&
isLoadingIndexPattern != null &&
!isLoadingIndexPattern &&
!isEmpty(start) &&
!isEmpty(end),
[isLoadingIndexPattern, combinedQueries, start, end]
);
const fields = useMemo(
() => [...columnsHeader.map((c) => c.id), ...(queryFields ?? [])],
[columnsHeader, queryFields]
);
const sortField = useMemo(
() =>
sort.map(({ columnId, columnType, sortDirection }) => ({
field: columnId,
type: columnType,
direction: sortDirection as Direction,
})),
[sort]
);
const [loading, { events, updatedAt, inspect, loadPage, pageInfo, refetch, totalCount = 0 }] =
useTimelineEvents({
docValueFields,
fields,
filterQuery: combinedQueries?.filterQuery,
id,
indexNames,
limit: itemsPerPage,
runtimeMappings,
sort: sortField,
startDate: start,
endDate: end,
skip: !canQueryTimeline || combinedQueries?.filterQuery === undefined, // When the filterQuery comes back as undefined, it means an error has been thrown and the request should be skipped
});
const totalCountMinusDeleted = useMemo(
() => (totalCount > 0 ? totalCount - deletedEventIds.length : 0),
[deletedEventIds.length, totalCount]
);
const subtitle = useMemo(
() =>
showTotalCount
? `${i18n.SHOWING}: ${totalCountMinusDeleted.toLocaleString()} ${unit(
totalCountMinusDeleted
)}`
: null,
[showTotalCount, totalCountMinusDeleted, unit]
);
const nonDeletedEvents = useMemo(
() => events.filter((e) => !deletedEventIds.includes(e._id)),
[deletedEventIds, events]
);
const HeaderSectionContent = useMemo(
() =>
headerFilterGroup && (
<HeaderFilterGroupWrapper
data-test-subj="header-filter-group-wrapper"
show={!resolverIsShowing(graphEventId)}
>
{headerFilterGroup}
</HeaderFilterGroupWrapper>
),
[graphEventId, headerFilterGroup]
);
useEffect(() => {
setIsQueryLoading(loading);
}, [loading]);
const leadingControlColumns: ControlColumnProps[] = [defaultControlColumn];
const trailingControlColumns: ControlColumnProps[] = [];
const timelineContext = useMemo(() => ({ timelineId: id }), [id]);
return (
<StyledEuiPanel
data-test-subj="events-viewer-panel"
$isFullScreen={globalFullScreen && id !== TimelineId.active}
hasBorder
>
{canQueryTimeline ? (
<EventDetailsWidthProvider>
<>
<HeaderSection
id={!resolverIsShowing(graphEventId) ? id : undefined}
height={headerFilterGroup ? COMPACT_HEADER_HEIGHT : EVENTS_VIEWER_HEADER_HEIGHT}
subtitle={utilityBar ? undefined : subtitle}
title={globalFullScreen ? titleWithExitFullScreen : justTitle}
isInspectDisabled={combinedQueries?.filterQuery === undefined}
>
{HeaderSectionContent}
</HeaderSection>
{utilityBar && !resolverIsShowing(graphEventId) && (
<UtilityBar>{utilityBar?.(refetch, totalCountMinusDeleted)}</UtilityBar>
)}
<TimelineContext.Provider value={timelineContext}>
<EventsContainerLoading
data-timeline-id={id}
data-test-subj={`events-container-loading-${loading}`}
>
<TimelineRefetch
id={id}
inputId="global"
inspect={inspect}
loading={loading}
refetch={refetch}
/>
{graphEventId && <GraphOverlay timelineId={id} />}
<FullWidthFlexGroup $visible={!graphEventId} gutterSize="none">
<ScrollableFlexItem grow={1}>
<StatefulBody
activePage={pageInfo.activePage}
browserFields={browserFields}
data={nonDeletedEvents}
id={id}
isEventViewer={true}
onRuleChange={onRuleChange}
refetch={refetch}
renderCellValue={renderCellValue}
rowRenderers={rowRenderers}
sort={sort}
tabType={TimelineTabs.query}
totalPages={calculateTotalPages({
itemsCount: totalCountMinusDeleted,
itemsPerPage,
})}
leadingControlColumns={leadingControlColumns}
trailingControlColumns={trailingControlColumns}
/>
<Footer
activePage={pageInfo.activePage}
data-test-subj="events-viewer-footer"
updatedAt={updatedAt}
height={footerHeight}
id={id}
isLive={isLive}
isLoading={loading}
itemsCount={nonDeletedEvents.length}
itemsPerPage={itemsPerPage}
itemsPerPageOptions={itemsPerPageOptions}
onChangePage={loadPage}
totalCount={totalCountMinusDeleted}
/>
</ScrollableFlexItem>
</FullWidthFlexGroup>
</EventsContainerLoading>
</TimelineContext.Provider>
</>
</EventDetailsWidthProvider>
) : null}
</StyledEuiPanel>
);
};
export const EventsViewer = React.memo(
EventsViewerComponent,
// eslint-disable-next-line complexity
(prevProps, nextProps) =>
deepEqual(prevProps.browserFields, nextProps.browserFields) &&
prevProps.columns === nextProps.columns &&
deepEqual(prevProps.docValueFields, nextProps.docValueFields) &&
prevProps.dataProviders === nextProps.dataProviders &&
prevProps.deletedEventIds === nextProps.deletedEventIds &&
prevProps.end === nextProps.end &&
deepEqual(prevProps.filters, nextProps.filters) &&
prevProps.headerFilterGroup === nextProps.headerFilterGroup &&
prevProps.id === nextProps.id &&
deepEqual(prevProps.indexPattern, nextProps.indexPattern) &&
prevProps.isLive === nextProps.isLive &&
prevProps.itemsPerPage === nextProps.itemsPerPage &&
prevProps.itemsPerPageOptions === nextProps.itemsPerPageOptions &&
prevProps.kqlMode === nextProps.kqlMode &&
deepEqual(prevProps.query, nextProps.query) &&
prevProps.renderCellValue === nextProps.renderCellValue &&
prevProps.rowRenderers === nextProps.rowRenderers &&
prevProps.start === nextProps.start &&
deepEqual(prevProps.sort, nextProps.sort) &&
prevProps.utilityBar === nextProps.utilityBar &&
prevProps.graphEventId === nextProps.graphEventId
);

View file

@ -21,6 +21,7 @@ import { TimelineId } from '../../../../common/types/timeline';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
import { useTimelineEvents } from '../../../timelines/containers';
import { getDefaultControlColumn } from '../../../timelines/components/timeline/body/control_columns';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
import { defaultCellActions } from '../../lib/cell_actions/default_cell_actions';
@ -38,6 +39,7 @@ mockUseResizeObserver.mockImplementation(() => ({}));
const from = '2019-08-27T22:10:56.794Z';
const to = '2019-08-26T22:10:56.791Z';
const ACTION_BUTTON_COUNT = 4;
const testProps = {
defaultCellActions,
@ -46,6 +48,7 @@ const testProps = {
entityType: EntityType.ALERTS,
indexNames: [],
id: TimelineId.test,
leadingControlColumns: getDefaultControlColumn(ACTION_BUTTON_COUNT),
renderCellValue: DefaultCellRenderer,
rowRenderers: defaultRowRenderers,
scopeId: SourcererScopeName.default,

View file

@ -9,8 +9,6 @@ import React, { useCallback, useMemo, useEffect } from 'react';
import { connect, ConnectedProps, useDispatch } from 'react-redux';
import deepEqual from 'fast-deep-equal';
import styled from 'styled-components';
import { isEmpty } from 'lodash/fp';
import { inputsModel, inputsSelectors, State } from '../../store';
import { inputsActions } from '../../store/actions';
import { ControlColumnProps, RowRenderer, TimelineId } from '../../../../common/types/timeline';
@ -28,18 +26,9 @@ import { TGridCellAction } from '../../../../../timelines/common/types';
import { DetailsPanel } from '../../../timelines/components/side_panel';
import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering';
import { useKibana } from '../../lib/kibana';
import { defaultControlColumn } from '../../../timelines/components/timeline/body/control_columns';
import { EventsViewer } from './events_viewer';
import * as i18n from './translations';
import { GraphOverlay } from '../../../timelines/components/graph_overlay';
const EMPTY_CONTROL_COLUMNS: ControlColumnProps[] = [];
const leadingControlColumns: ControlColumnProps[] = [
{
...defaultControlColumn,
headerCellRender: () => <>{i18n.ACTIONS}</>,
},
];
const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>`
height: ${({ $isFullScreen }) => ($isFullScreen ? '100%' : undefined)};
@ -54,6 +43,7 @@ export interface OwnProps {
end: string;
entityType: EntityType;
id: TimelineId;
leadingControlColumns: ControlColumnProps[];
scopeId: SourcererScopeName;
start: string;
showTotalCount?: boolean;
@ -93,6 +83,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
itemsPerPage,
itemsPerPageOptions,
kqlMode,
leadingControlColumns,
pageFilters,
currentFilter,
onRuleChange,
@ -124,8 +115,6 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
} = useSourcererDataView(scopeId);
const { globalFullScreen } = useGlobalFullScreen();
// TODO: Once we are past experimental phase this code should be removed
const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled');
const tGridEventRenderedViewEnabled = useIsExperimentalFeatureEnabled(
'tGridEventRenderedViewEnabled'
);
@ -179,75 +168,45 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
<>
<FullScreenContainer $isFullScreen={globalFullScreen}>
<InspectButtonContainer>
{tGridEnabled ? (
timelinesUi.getTGrid<'embedded'>({
additionalFilters,
browserFields,
bulkActions,
columns,
dataProviders,
defaultCellActions,
deletedEventIds,
docValueFields,
end,
entityType,
filters: globalFilters,
filterStatus: currentFilter,
globalFullScreen,
graphEventId,
graphOverlay,
hasAlertsCrud,
id,
indexNames: selectedPatterns,
indexPattern,
isLive,
isLoadingIndexPattern,
itemsPerPage,
itemsPerPageOptions,
kqlMode,
leadingControlColumns,
onRuleChange,
query,
renderCellValue,
rowRenderers,
runtimeMappings,
setQuery,
sort,
start,
tGridEventRenderedViewEnabled,
trailingControlColumns,
type: 'embedded',
unit,
})
) : (
<EventsViewer
browserFields={browserFields}
columns={columns}
docValueFields={docValueFields}
id={id}
dataProviders={dataProviders}
deletedEventIds={deletedEventIds}
end={end}
isLoadingIndexPattern={isLoadingIndexPattern}
filters={globalFilters}
indexNames={selectedPatterns}
indexPattern={indexPattern}
isLive={isLive}
itemsPerPage={itemsPerPage}
itemsPerPageOptions={itemsPerPageOptions}
kqlMode={kqlMode}
query={query}
onRuleChange={onRuleChange}
renderCellValue={renderCellValue}
rowRenderers={rowRenderers}
runtimeMappings={runtimeMappings}
start={start}
sort={sort}
showTotalCount={isEmpty(graphEventId) ? true : false}
utilityBar={utilityBar}
graphEventId={graphEventId}
/>
)}
{timelinesUi.getTGrid<'embedded'>({
additionalFilters,
browserFields,
bulkActions,
columns,
dataProviders,
defaultCellActions,
deletedEventIds,
docValueFields,
end,
entityType,
filters: globalFilters,
filterStatus: currentFilter,
globalFullScreen,
graphEventId,
graphOverlay,
hasAlertsCrud,
id,
indexNames: selectedPatterns,
indexPattern,
isLive,
isLoadingIndexPattern,
itemsPerPage,
itemsPerPageOptions,
kqlMode,
leadingControlColumns,
onRuleChange,
query,
renderCellValue,
rowRenderers,
runtimeMappings,
setQuery,
sort,
start,
tGridEventRenderedViewEnabled,
trailingControlColumns,
type: 'embedded',
unit,
})}
</InspectButtonContainer>
</FullScreenContainer>
<DetailsPanel
@ -338,6 +297,7 @@ export const StatefulEventsViewer = connector(
prevProps.itemsPerPage === nextProps.itemsPerPage &&
deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) &&
prevProps.kqlMode === nextProps.kqlMode &&
prevProps.leadingControlColumns === nextProps.leadingControlColumns &&
deepEqual(prevProps.query, nextProps.query) &&
prevProps.renderCellValue === nextProps.renderCellValue &&
prevProps.rowRenderers === nextProps.rowRenderers &&

View file

@ -28,6 +28,7 @@ import { inputsModel, inputsSelectors, State } from '../../../common/store';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
import * as i18nCommon from '../../../common/translations';
import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants';
import { getDefaultControlColumn } from '../../../timelines/components/timeline/body/control_columns';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
import { combineQueries } from '../../../timelines/components/timeline/helpers';
import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline';
@ -106,6 +107,7 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
const { addWarning } = useAppToasts();
// TODO: Once we are past experimental phase this code should be removed
const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled');
const ACTION_BUTTON_COUNT = 4;
const getGlobalQuery = useCallback(
(customFilters: Filter[]) => {
@ -369,6 +371,8 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
);
}, [dispatch, defaultTimelineModel, filterManager, tGridEnabled, timelineId]);
const leadingControlColumns = useMemo(() => getDefaultControlColumn(ACTION_BUTTON_COUNT), []);
if (loading || indexPatternsLoading || isEmpty(selectedPatterns)) {
return null;
}
@ -383,6 +387,7 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
entityType="events"
hasAlertsCrud={hasIndexWrite && hasIndexMaintenance}
id={timelineId}
leadingControlColumns={leadingControlColumns}
onRuleChange={onRuleChange}
pageFilters={defaultFiltersMemo}
renderCellValue={RenderCellValue}

View file

@ -15,7 +15,7 @@ import { get } from 'lodash/fp';
import { useRouteSpy } from '../../../../common/utils/route/use_route_spy';
import { buildGetAlertByIdQuery } from '../../../../common/components/exceptions/helpers';
import { EventsTdContent } from '../../../../timelines/components/timeline/styles';
import { DEFAULT_ICON_BUTTON_WIDTH } from '../../../../timelines/components/timeline/helpers';
import { DEFAULT_ACTION_BUTTON_WIDTH } from '../../../../../../timelines/public';
import { Ecs } from '../../../../../common/ecs';
import {
AddExceptionModal,
@ -185,7 +185,7 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps & PropsFromRedux
{addToCaseActionProps && timelinesUi.getAddToCaseAction(addToCaseActionProps)}
{items.length > 0 && (
<div key="actions-context-menu">
<EventsTdContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
<EventsTdContent textAlign="center" width={DEFAULT_ACTION_BUTTON_WIDTH}>
<EuiPopover
id="singlePanel"
button={button}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { TimelineId } from '../../../../common/types/timeline';
@ -21,6 +21,7 @@ import { MatrixHistogram } from '../../../common/components/matrix_histogram';
import { useGlobalFullScreen } from '../../../common/containers/use_full_screen';
import * as i18n from '../translations';
import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution';
import { getDefaultControlColumn } from '../../../timelines/components/timeline/body/control_columns';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
@ -69,7 +70,7 @@ const EventsQueryTabBodyComponent: React.FC<HostsComponentsQueryProps> = ({
}) => {
const dispatch = useDispatch();
const { globalFullScreen } = useGlobalFullScreen();
const ACTION_BUTTON_COUNT = 3;
const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled');
useEffect(() => {
@ -96,6 +97,8 @@ const EventsQueryTabBodyComponent: React.FC<HostsComponentsQueryProps> = ({
};
}, [deleteQuery]);
const leadingControlColumns = useMemo(() => getDefaultControlColumn(ACTION_BUTTON_COUNT), []);
return (
<>
{!globalFullScreen && (
@ -115,6 +118,7 @@ const EventsQueryTabBodyComponent: React.FC<HostsComponentsQueryProps> = ({
end={endDate}
entityType="events"
id={TimelineId.hostsPageEvents}
leadingControlColumns={leadingControlColumns}
pageFilters={pageFilters}
renderCellValue={DefaultCellRenderer}
rowRenderers={defaultRowRenderers}

View file

@ -9,7 +9,7 @@ import React, { MouseEvent } from 'react';
import { EuiContextMenuItem, EuiButtonIcon, EuiToolTip, EuiText } from '@elastic/eui';
import { EventsTdContent } from '../../styles';
import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers';
import { DEFAULT_ACTION_BUTTON_WIDTH } from '../../../../../../../timelines/public';
interface ActionIconItemProps {
ariaLabel?: string;
@ -24,7 +24,7 @@ interface ActionIconItemProps {
}
const ActionIconItemComponent: React.FC<ActionIconItemProps> = ({
width = DEFAULT_ICON_BUTTON_WIDTH,
width = DEFAULT_ACTION_BUTTON_WIDTH,
dataTestSubj,
content,
ariaLabel,
@ -46,6 +46,7 @@ const ActionIconItemComponent: React.FC<ActionIconItemProps> = ({
iconType={iconType}
isDisabled={isDisabled}
onClick={onClick}
size="s"
/>
</EuiToolTip>
)}

View file

@ -28,7 +28,7 @@ import {
useGlobalFullScreen,
useTimelineFullScreen,
} from '../../../../../common/containers/use_full_screen';
import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers';
import { DEFAULT_ACTION_BUTTON_WIDTH } from '../../../../../../../timelines/public';
import { StatefulRowRenderersBrowser } from '../../../row_renderers_browser';
import { EventsTh, EventsThContent } from '../../styles';
import { EventsSelect } from '../column_headers/events_select';
@ -166,7 +166,7 @@ const HeaderActionsComponent: React.FC<HeaderActionProps> = ({
<ActionsContainer>
{showSelectAllCheckbox && (
<EventsTh role="checkbox">
<EventsThContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
<EventsThContent textAlign="center" width={DEFAULT_ACTION_BUTTON_WIDTH}>
<EuiCheckbox
data-test-subj="select-all-events"
id={'select-all-events'}
@ -195,7 +195,7 @@ const HeaderActionsComponent: React.FC<HeaderActionProps> = ({
</EventsTh>
<EventsTh role="button">
<EventsThContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
<EventsThContent textAlign="center" width={DEFAULT_ACTION_BUTTON_WIDTH}>
<EuiToolTip content={fullScreen ? EXIT_FULL_SCREEN : i18n.FULL_SCREEN}>
<EuiButtonIcon
aria-label={
@ -218,7 +218,7 @@ const HeaderActionsComponent: React.FC<HeaderActionProps> = ({
</EventsTh>
{tabType !== TimelineTabs.eql && (
<EventsTh role="button" data-test-subj="timeline-sorting-fields">
<EventsThContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
<EventsThContent textAlign="center" width={DEFAULT_ACTION_BUTTON_WIDTH}>
<EuiToolTip content={i18n.SORT_FIELDS}>
<SortingColumnsContainer>{ColumnSorting}</SortingColumnsContainer>
</EuiToolTip>
@ -228,7 +228,7 @@ const HeaderActionsComponent: React.FC<HeaderActionProps> = ({
{showEventsSelect && (
<EventsTh role="button">
<EventsThContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
<EventsThContent textAlign="center" width={DEFAULT_ACTION_BUTTON_WIDTH}>
<EventsSelect checkState="unchecked" timelineId={timelineId} />
</EventsThContent>
</EventsTh>

View file

@ -19,7 +19,7 @@ import { AddEventNoteAction } from './add_note_icon_item';
import { PinEventAction } from './pin_event_action';
import { EventsTdContent } from '../../styles';
import * as i18n from '../translations';
import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers';
import { DEFAULT_ACTION_BUTTON_WIDTH } from '../../../../../../../timelines/public';
import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector';
import {
setActiveTabTimeline,
@ -136,7 +136,7 @@ const ActionsComponent: React.FC<ActionProps> = ({
<ActionsContainer>
{showCheckboxes && !tGridEnabled && (
<div key="select-event-container" data-test-subj="select-event-container">
<EventsTdContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
<EventsTdContent textAlign="center" width={DEFAULT_ACTION_BUTTON_WIDTH}>
{loadingEventIds.includes(eventId) ? (
<EuiLoadingSpinner size="m" data-test-subj="event-loader" />
) : (
@ -152,13 +152,14 @@ const ActionsComponent: React.FC<ActionProps> = ({
</div>
)}
<div key="expand-event">
<EventsTdContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
<EventsTdContent textAlign="center" width={DEFAULT_ACTION_BUTTON_WIDTH}>
<EuiToolTip data-test-subj="expand-event-tool-tip" content={i18n.VIEW_DETAILS}>
<EuiButtonIcon
aria-label={i18n.VIEW_DETAILS_FOR_ROW({ ariaRowindex, columnValues })}
data-test-subj="expand-event"
iconType="expand"
onClick={onEventDetailsPanelOpened}
size="s"
/>
</EuiToolTip>
</EventsTdContent>
@ -204,7 +205,7 @@ const ActionsComponent: React.FC<ActionProps> = ({
/>
{isDisabled === false ? (
<div>
<EventsTdContent textAlign="center" width={36}>
<EventsTdContent textAlign="center" width={DEFAULT_ACTION_BUTTON_WIDTH}>
<EuiToolTip
data-test-subj="view-in-analyzer-tool-tip"
content={i18n.ACTION_INVESTIGATE_IN_RESOLVER}
@ -217,6 +218,7 @@ const ActionsComponent: React.FC<ActionProps> = ({
data-test-subj="view-in-analyzer"
iconType="analyzeEvent"
onClick={handleClick}
size="s"
/>
</EuiToolTip>
</EventsTdContent>

View file

@ -9,7 +9,7 @@ import React, { useMemo } from 'react';
import { EuiToolTip } from '@elastic/eui';
import { EventsTdContent } from '../../styles';
import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers';
import { DEFAULT_ACTION_BUTTON_WIDTH } from '../../../../../../../timelines/public';
import { eventHasNotes, getPinTooltip } from '../helpers';
import { Pin } from '../../pin';
import { TimelineType } from '../../../../../../common/types/timeline';
@ -41,7 +41,7 @@ const PinEventActionComponent: React.FC<PinEventActionProps> = ({
return (
<div key="timeline-action-pin-tool-tip">
<EventsTdContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
<EventsTdContent textAlign="center" width={DEFAULT_ACTION_BUTTON_WIDTH}>
<EuiToolTip data-test-subj="timeline-action-pin-tool-tip" content={tooltipContent}>
<Pin
ariaLabel={ariaLabel}

View file

@ -2,7 +2,7 @@
exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = `
<ColumnHeadersComponent
actionsColumnWidth={72}
actionsColumnWidth={124}
browserFields={
Object {
"agent": Object {
@ -522,7 +522,7 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = `
"compare": null,
"type": [Function],
},
"width": 140,
"width": 124,
},
]
}

View file

@ -8,13 +8,8 @@
import { mockBrowserFields } from '../../../../../common/containers/source/mock';
import { defaultHeaders } from './default_headers';
import { getActionsColumnWidth, getColumnWidthFromType, getColumnHeaders } from './helpers';
import {
DEFAULT_COLUMN_MIN_WIDTH,
DEFAULT_DATE_COLUMN_MIN_WIDTH,
MINIMUM_ACTIONS_COLUMN_WIDTH,
SHOW_CHECK_BOXES_COLUMN_WIDTH,
} from '../constants';
import { getColumnWidthFromType, getColumnHeaders } from './helpers';
import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants';
import '../../../../../common/mock/match_media';
describe('helpers', () => {
@ -28,28 +23,6 @@ describe('helpers', () => {
});
});
describe('getActionsColumnWidth', () => {
test('returns the default actions column width when isEventViewer is false', () => {
expect(getActionsColumnWidth(false)).toEqual(MINIMUM_ACTIONS_COLUMN_WIDTH);
});
test('returns the minimum actions column width + checkbox width when isEventViewer is false and showCheckboxes is true', () => {
expect(getActionsColumnWidth(false, true)).toEqual(
MINIMUM_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH
);
});
test('returns the minimum actions column width when isEventViewer is true', () => {
expect(getActionsColumnWidth(true)).toEqual(MINIMUM_ACTIONS_COLUMN_WIDTH);
});
test('returns the minimum actions column width + checkbox width when isEventViewer is true and showCheckboxes is true', () => {
expect(getActionsColumnWidth(true, true)).toEqual(
MINIMUM_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH
);
});
});
describe('getColumnHeaders', () => {
test('should return a full object of ColumnHeader from the default header', () => {
const expectedData = [

View file

@ -9,14 +9,7 @@ import { get } from 'lodash/fp';
import { ColumnHeaderOptions } from '../../../../../../common';
import { BrowserFields } from '../../../../../common/containers/source';
import {
DEFAULT_COLUMN_MIN_WIDTH,
DEFAULT_DATE_COLUMN_MIN_WIDTH,
SHOW_CHECK_BOXES_COLUMN_WIDTH,
EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH,
DEFAULT_ACTIONS_COLUMN_WIDTH,
MINIMUM_ACTIONS_COLUMN_WIDTH,
} from '../constants';
import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants';
/** Enriches the column headers with field details from the specified browserFields */
export const getColumnHeaders = (
@ -40,20 +33,3 @@ export const getColumnHeaders = (
export const getColumnWidthFromType = (type: string): number =>
type !== 'date' ? DEFAULT_COLUMN_MIN_WIDTH : DEFAULT_DATE_COLUMN_MIN_WIDTH;
/** Returns the (fixed) width of the Actions column */
export const getActionsColumnWidth = (
isEventViewer: boolean,
showCheckboxes = false,
additionalActionWidth = 0
): number => {
const checkboxesWidth = showCheckboxes ? SHOW_CHECK_BOXES_COLUMN_WIDTH : 0;
const actionsColumnWidth =
checkboxesWidth +
(isEventViewer ? EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH : DEFAULT_ACTIONS_COLUMN_WIDTH) +
additionalActionWidth;
return actionsColumnWidth > MINIMUM_ACTIONS_COLUMN_WIDTH + checkboxesWidth
? actionsColumnWidth
: MINIMUM_ACTIONS_COLUMN_WIDTH + checkboxesWidth;
};

View file

@ -9,7 +9,7 @@ import { shallow } from 'enzyme';
import React from 'react';
import '../../../../../common/mock/match_media';
import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants';
import { getActionsColumnWidth } from '../../../../../../../timelines/public';
import { defaultHeaders } from './default_headers';
import { mockBrowserFields } from '../../../../../common/containers/source/mock';
import { Sort } from '../sort';
@ -21,8 +21,9 @@ import { cloneDeep } from 'lodash/fp';
import { timelineActions } from '../../../../store/timeline';
import { TimelineTabs } from '../../../../../../common/types/timeline';
import { Direction } from '../../../../../../common/search_strategy';
import { defaultControlColumn } from '../control_columns';
import { getDefaultControlColumn } from '../control_columns';
import { testTrailingControlColumns } from '../../../../../common/mock/mock_timeline_control_columns';
import { HeaderActions } from '../actions/header_actions';
jest.mock('../../../../../common/lib/kibana');
@ -39,6 +40,12 @@ const 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,
}));
describe('rendering', () => {
const sort: Sort[] = [
@ -53,7 +60,7 @@ describe('ColumnHeaders', () => {
const wrapper = shallow(
<TestProviders>
<ColumnHeadersComponent
actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH}
actionsColumnWidth={actionsColumnWidth}
browserFields={mockBrowserFields}
columnHeaders={defaultHeaders}
isSelectAllChecked={false}
@ -63,7 +70,7 @@ describe('ColumnHeaders', () => {
sort={sort}
tabType={TimelineTabs.query}
timelineId={timelineId}
leadingControlColumns={[defaultControlColumn]}
leadingControlColumns={leadingControlColumns}
trailingControlColumns={[]}
/>
</TestProviders>
@ -75,7 +82,7 @@ describe('ColumnHeaders', () => {
const wrapper = mount(
<TestProviders>
<ColumnHeadersComponent
actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH}
actionsColumnWidth={actionsColumnWidth}
browserFields={mockBrowserFields}
columnHeaders={defaultHeaders}
isSelectAllChecked={false}
@ -85,7 +92,7 @@ describe('ColumnHeaders', () => {
sort={sort}
tabType={TimelineTabs.query}
timelineId={timelineId}
leadingControlColumns={[defaultControlColumn]}
leadingControlColumns={leadingControlColumns}
trailingControlColumns={[]}
/>
</TestProviders>
@ -98,7 +105,7 @@ describe('ColumnHeaders', () => {
const wrapper = mount(
<TestProviders>
<ColumnHeadersComponent
actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH}
actionsColumnWidth={actionsColumnWidth}
browserFields={mockBrowserFields}
columnHeaders={defaultHeaders}
isSelectAllChecked={false}
@ -108,7 +115,7 @@ describe('ColumnHeaders', () => {
sort={sort}
tabType={TimelineTabs.query}
timelineId={timelineId}
leadingControlColumns={[defaultControlColumn]}
leadingControlColumns={leadingControlColumns}
trailingControlColumns={[]}
/>
</TestProviders>
@ -159,7 +166,7 @@ describe('ColumnHeaders', () => {
const wrapper = mount(
<TestProviders>
<ColumnHeadersComponent
actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH}
actionsColumnWidth={actionsColumnWidth}
browserFields={mockBrowserFields}
columnHeaders={mockDefaultHeaders}
isSelectAllChecked={false}
@ -169,7 +176,7 @@ describe('ColumnHeaders', () => {
sort={mockSort}
tabType={TimelineTabs.query}
timelineId={timelineId}
leadingControlColumns={[defaultControlColumn]}
leadingControlColumns={leadingControlColumns}
trailingControlColumns={[]}
/>
</TestProviders>
@ -203,7 +210,7 @@ describe('ColumnHeaders', () => {
const wrapper = mount(
<TestProviders>
<ColumnHeadersComponent
actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH}
actionsColumnWidth={actionsColumnWidth}
browserFields={mockBrowserFields}
columnHeaders={mockDefaultHeaders}
isSelectAllChecked={false}
@ -213,7 +220,7 @@ describe('ColumnHeaders', () => {
sort={mockSort}
tabType={TimelineTabs.query}
timelineId={timelineId}
leadingControlColumns={[defaultControlColumn]}
leadingControlColumns={leadingControlColumns}
trailingControlColumns={[]}
/>
</TestProviders>
@ -242,7 +249,7 @@ describe('ColumnHeaders', () => {
const wrapper = mount(
<TestProviders>
<ColumnHeadersComponent
actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH}
actionsColumnWidth={actionsColumnWidth}
browserFields={mockBrowserFields}
columnHeaders={mockDefaultHeaders}
isSelectAllChecked={false}
@ -252,7 +259,7 @@ describe('ColumnHeaders', () => {
sort={mockSort}
tabType={TimelineTabs.query}
timelineId={timelineId}
leadingControlColumns={[defaultControlColumn]}
leadingControlColumns={leadingControlColumns}
trailingControlColumns={[]}
/>
</TestProviders>
@ -280,7 +287,7 @@ describe('ColumnHeaders', () => {
const wrapper = mount(
<TestProviders>
<ColumnHeadersComponent
actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH}
actionsColumnWidth={actionsColumnWidth}
browserFields={mockBrowserFields}
columnHeaders={mockDefaultHeaders}
isSelectAllChecked={false}

View file

@ -5,20 +5,6 @@
* 2.0.
*/
/** The minimum (fixed) width of the Actions column */
export const MINIMUM_ACTIONS_COLUMN_WIDTH = 100; // px;
/** Additional column width to include when checkboxes are shown **/
export const SHOW_CHECK_BOXES_COLUMN_WIDTH = 24; // px;
/** The (fixed) width of the Actions column */
export const DEFAULT_ACTIONS_COLUMN_WIDTH = SHOW_CHECK_BOXES_COLUMN_WIDTH * 3; // px;
/**
* The (fixed) width of the Actions column when the timeline body is used as
* an events viewer, which has fewer actions than a regular events viewer
*/
export const EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH = SHOW_CHECK_BOXES_COLUMN_WIDTH * 2; // px;
/** The default minimum width of a column (when a width for the column type is not specified) */
export const DEFAULT_COLUMN_MIN_WIDTH = 180; // px

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { getDefaultControlColumn } from '.';
describe('control columns', () => {
describe('getDefaultControlColumn', () => {
const ACTION_BUTTON_COUNT = 5;
test('it returns the expected defaults', () => {
expect(getDefaultControlColumn(ACTION_BUTTON_COUNT)).toMatchInlineSnapshot(`
Array [
Object {
"headerCellRender": [Function],
"id": "default-timeline-control-column",
"rowCellRender": Object {
"$$typeof": Symbol(react.memo),
"compare": null,
"type": [Function],
},
"width": 152,
},
]
`);
});
});
});

View file

@ -5,15 +5,18 @@
* 2.0.
*/
import React from 'react';
import { ControlColumnProps } from '../../../../../../common/types/timeline';
import { Actions } from '../actions';
import { HeaderActions } from '../actions/header_actions';
import { getActionsColumnWidth } from '../../../../../../../timelines/public';
import * as i18n from '../../../../../common/components/events_viewer/translations';
const DEFAULT_CONTROL_COLUMN_WIDTH = 140;
export const defaultControlColumn: ControlColumnProps = {
id: 'default-timeline-control-column',
width: DEFAULT_CONTROL_COLUMN_WIDTH,
headerCellRender: HeaderActions,
rowCellRender: Actions,
};
export const getDefaultControlColumn = (actionButtonCount: number): ControlColumnProps[] => [
{
headerCellRender: () => <>{i18n.ACTIONS}</>,
id: 'default-timeline-control-column',
rowCellRender: Actions,
width: getActionsColumnWidth(actionButtonCount),
},
];

View file

@ -13,12 +13,14 @@ import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer'
import '../../../../../common/mock/match_media';
import { mockTimelineData } from '../../../../../common/mock';
import { defaultHeaders } from '../column_headers/default_headers';
import { defaultControlColumn } from '../control_columns';
import { getDefaultControlColumn } from '../control_columns';
import { DataDrivenColumns } 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(
@ -45,7 +47,7 @@ describe('Columns', () => {
toggleShowNotes={jest.fn()}
refetch={jest.fn()}
eventIdToNoteIds={{}}
leadingControlColumns={[defaultControlColumn]}
leadingControlColumns={leadingControlColumns}
trailingControlColumns={[]}
setEventsLoading={jest.fn()}
setEventsDeleted={jest.fn()}

View file

@ -9,7 +9,6 @@ import { mount } from 'enzyme';
import React from 'react';
import { TestProviders } from '../../../../../common/mock';
import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants';
import * as i18n from '../translations';
import { EventColumnView } from './event_column_view';
@ -17,9 +16,10 @@ import { DefaultCellRenderer } from '../../cell_rendering/default_cell_renderer'
import { TimelineTabs, TimelineType, TimelineId } from '../../../../../../common/types/timeline';
import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import { defaultControlColumn } from '../control_columns';
import { getDefaultControlColumn } from '../control_columns';
import { testLeadingControlColumn } from '../../../../../common/mock/mock_timeline_control_columns';
import { mockTimelines } from '../../../../../common/mock/mock_timelines_plugin';
import { getActionsColumnWidth } from '../../../../../../../timelines/public';
jest.mock('../../../../../common/hooks/use_experimental_features');
const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock;
@ -57,11 +57,13 @@ jest.mock(
describe('EventColumnView', () => {
useIsExperimentalFeatureEnabledMock.mockReturnValue(false);
(useShallowEqualSelector as jest.Mock).mockReturnValue(TimelineType.default);
const ACTION_BUTTON_COUNT = 4;
const leadingControlColumns = getDefaultControlColumn(ACTION_BUTTON_COUNT);
const props = {
ariaRowindex: 2,
id: 'event-id',
actionsColumnWidth: DEFAULT_ACTIONS_COLUMN_WIDTH,
actionsColumnWidth: getActionsColumnWidth(ACTION_BUTTON_COUNT),
associateNote: jest.fn(),
columnHeaders: [],
columnRenderers: [],
@ -91,7 +93,7 @@ describe('EventColumnView', () => {
toggleShowNotes: jest.fn(),
updateNote: jest.fn(),
isEventPinned: false,
leadingControlColumns: [defaultControlColumn],
leadingControlColumns,
trailingControlColumns: [],
setEventsLoading: jest.fn(),
setEventsDeleted: jest.fn(),
@ -145,7 +147,7 @@ describe('EventColumnView', () => {
<EventColumnView
{...props}
timelineId="timeline-test"
leadingControlColumns={[testLeadingControlColumn, defaultControlColumn]}
leadingControlColumns={[testLeadingControlColumn, ...leadingControlColumns]}
/>,
{
wrappingComponent: TestProviders,

View file

@ -19,7 +19,7 @@ import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock';
import { BodyComponent, StatefulBodyProps } from '.';
import { Sort } from './sort';
import { defaultControlColumn } from './control_columns';
import { getDefaultControlColumn } from './control_columns';
import { useMountAppended } from '../../../../common/utils/use_mount_appended';
import { timelineActions } from '../../../store/timeline';
import { TimelineTabs } from '../../../../../common/types/timeline';
@ -119,6 +119,8 @@ describe('Body', () => {
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
});
const ACTION_BUTTON_COUNT = 4;
const props: StatefulBodyProps = {
activePage: 0,
browserFields: mockBrowserFields,
@ -140,7 +142,7 @@ describe('Body', () => {
showCheckboxes: false,
tabType: TimelineTabs.query,
totalPages: 1,
leadingControlColumns: [defaultControlColumn],
leadingControlColumns: getDefaultControlColumn(ACTION_BUTTON_COUNT),
trailingControlColumns: [],
};

View file

@ -16,6 +16,7 @@ import {
ARIA_COLINDEX_ATTRIBUTE,
ARIA_ROWINDEX_ATTRIBUTE,
onKeyDownFocusHandler,
getActionsColumnWidth,
} from '../../../../../../timelines/public';
import { CellValueElementProps } from '../cell_rendering';
import { DEFAULT_COLUMN_MIN_WIDTH } from './constants';
@ -34,14 +35,13 @@ import { TimelineModel } from '../../../store/timeline/model';
import { timelineDefaults } from '../../../store/timeline/defaults';
import { timelineActions, timelineSelectors } from '../../../store/timeline';
import { OnRowSelected, OnSelectAll } from '../events';
import { getActionsColumnWidth, getColumnHeaders } from './column_headers/helpers';
import { getColumnHeaders } from './column_headers/helpers';
import { getEventIdToDataMapping } from './helpers';
import { 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 { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
interface OwnProps {
@ -61,15 +61,11 @@ interface OwnProps {
onRuleChange?: () => void;
}
const NUM_OF_ICON_IN_TIMELINE_ROW = 2;
export const hasAdditionalActions = (id: TimelineId): boolean =>
[TimelineId.detectionsPage, TimelineId.detectionsRulesDetailsPage, TimelineId.active].includes(
id
);
const EXTRA_WIDTH = 4; // px
export type StatefulBodyProps = OwnProps & PropsFromRedux;
/**
@ -108,6 +104,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
const { queryFields, selectAll } = useDeepEqualSelector((state) =>
getManageTimeline(state, id)
);
const ACTION_BUTTON_COUNT = 5;
const onRowSelected: OnRowSelected = useCallback(
({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => {
@ -158,17 +155,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
return rowRenderers.filter((rowRenderer) => !excludedRowRendererIds.includes(rowRenderer.id));
}, [excludedRowRendererIds, rowRenderers]);
const actionsColumnWidth = useMemo(
() =>
getActionsColumnWidth(
isEventViewer,
showCheckboxes,
hasAdditionalActions(id as TimelineId)
? DEFAULT_ICON_BUTTON_WIDTH * NUM_OF_ICON_IN_TIMELINE_ROW + EXTRA_WIDTH
: 0
),
[isEventViewer, showCheckboxes, id]
);
const actionsColumnWidth = useMemo(() => getActionsColumnWidth(ACTION_BUTTON_COUNT), []);
const columnWidths = useMemo(
() =>

View file

@ -18,7 +18,7 @@ import {
StyledContent,
} from '../../../../common/lib/cell_actions/expanded_cell_value_actions';
const FIELDS_WITHOUT_CELL_ACTIONS = ['@timestamp', 'signal.rule.risk_score', 'signal.reason'];
const FIELDS_WITHOUT_CELL_ACTIONS = ['signal.rule.risk_score', 'signal.reason'];
const hasCellActions = (columnId?: string) => {
return columnId && FIELDS_WITHOUT_CELL_ACTIONS.indexOf(columnId) < 0;
};

View file

@ -14,7 +14,7 @@ import {
EuiBadge,
} from '@elastic/eui';
import { isEmpty } from 'lodash/fp';
import React, { useEffect, useCallback } from 'react';
import React, { useCallback, useEffect, useMemo } from 'react';
import styled from 'styled-components';
import { Dispatch } from 'redux';
import { connect, ConnectedProps, useDispatch } from 'react-redux';
@ -54,7 +54,8 @@ import { useTimelineFullScreen } from '../../../../common/containers/use_full_sc
import { activeTimeline } from '../../../containers/active_timeline_context';
import { DetailsPanel } from '../../side_panel';
import { EqlQueryBarTimeline } from '../query_bar/eql';
import { defaultControlColumn } from '../body/control_columns';
import { HeaderActions } from '../body/actions/header_actions';
import { getDefaultControlColumn } from '../body/control_columns';
import { Sort } from '../body/sort';
import { Sourcerer } from '../../../../common/components/sourcerer';
@ -151,6 +152,8 @@ export type Props = OwnProps & PropsFromRedux;
const NO_SORTING: Sort[] = [];
const trailingControlColumns: ControlColumnProps[] = []; // stable reference
export const EqlTabContentComponent: React.FC<Props> = ({
activeTab,
columns,
@ -181,6 +184,7 @@ export const EqlTabContentComponent: React.FC<Props> = ({
runtimeMappings,
selectedPatterns,
} = useSourcererDataView(SourcererScopeName.timeline);
const ACTION_BUTTON_COUNT = 5;
const isBlankTimeline: boolean = isEmpty(eqlQuery);
@ -244,8 +248,14 @@ export const EqlTabContentComponent: React.FC<Props> = ({
);
}, [loadingSourcerer, timelineId, isQueryLoading, dispatch]);
const leadingControlColumns: ControlColumnProps[] = [defaultControlColumn];
const trailingControlColumns: ControlColumnProps[] = [];
const leadingControlColumns = useMemo(
() =>
getDefaultControlColumn(ACTION_BUTTON_COUNT).map((x) => ({
...x,
headerCellRender: HeaderActions,
})),
[]
);
return (
<>

View file

@ -223,8 +223,6 @@ export const combineQueries = ({
*/
export const STATEFUL_EVENT_CSS_CLASS_NAME = 'event-column-view';
export const DEFAULT_ICON_BUTTON_WIDTH = 24;
export const resolverIsShowing = (graphEventId: string | undefined): boolean =>
graphEventId != null && graphEventId !== '';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EuiButtonIcon, IconSize } from '@elastic/eui';
import { EuiButtonIcon } from '@elastic/eui';
import { noop } from 'lodash/fp';
import React from 'react';
@ -20,14 +20,13 @@ export const getPinIcon = (pinned: boolean): PinIcon => (pinned ? 'pinFilled' :
interface Props {
ariaLabel?: string;
allowUnpinning: boolean;
iconSize?: IconSize;
timelineType?: TimelineTypeLiteral;
onClick?: () => void;
pinned: boolean;
}
export const Pin = React.memo<Props>(
({ ariaLabel, allowUnpinning, iconSize = 'm', onClick = noop, pinned, timelineType }) => {
({ ariaLabel, allowUnpinning, onClick = noop, pinned, timelineType }) => {
const isTemplate = timelineType === TimelineType.template;
const defaultAriaLabel = isTemplate ? i18n.DISABLE_PIN : pinned ? i18n.PINNED : i18n.UNPINNED;
const pinAriaLabel = ariaLabel != null ? ariaLabel : defaultAriaLabel;
@ -36,10 +35,10 @@ export const Pin = React.memo<Props>(
<EuiButtonIcon
aria-label={pinAriaLabel}
data-test-subj="pin"
iconSize={iconSize}
iconType={getPinIcon(pinned)}
onClick={onClick}
isDisabled={isTemplate || !allowUnpinning}
size="s"
/>
);
}

View file

@ -14,6 +14,7 @@ import { connect, ConnectedProps } from 'react-redux';
import deepEqual from 'fast-deep-equal';
import { timelineActions, timelineSelectors } from '../../../store/timeline';
import { HeaderActions } from '../body/actions/header_actions';
import { CellValueElementProps } from '../cell_rendering';
import { Direction } from '../../../../../common/search_strategy';
import { useTimelineEvents } from '../../../containers/index';
@ -37,7 +38,7 @@ import {
} from '../../../../../common/types/timeline';
import { DetailsPanel } from '../../side_panel';
import { ExitFullScreen } from '../../../../common/components/exit_full_screen';
import { defaultControlColumn } from '../body/control_columns';
import { getDefaultControlColumn } from '../body/control_columns';
const StyledEuiFlyoutBody = styled(EuiFlyoutBody)`
overflow-y: hidden;
@ -101,6 +102,8 @@ interface PinnedFilter {
export type Props = OwnProps & PropsFromRedux;
const trailingControlColumns: ControlColumnProps[] = []; // stable reference
export const PinnedTabContentComponent: React.FC<Props> = ({
columns,
timelineId,
@ -121,6 +124,7 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
selectedPatterns,
} = useSourcererDataView(SourcererScopeName.timeline);
const { setTimelineFullScreen, timelineFullScreen } = useTimelineFullScreen();
const ACTION_BUTTON_COUNT = 5;
const filterQuery = useMemo(() => {
if (isEmpty(pinnedEventIds)) {
@ -197,8 +201,14 @@ export const PinnedTabContentComponent: React.FC<Props> = ({
onEventClosed({ tabType: TimelineTabs.pinned, timelineId });
}, [timelineId, onEventClosed]);
const leadingControlColumns: ControlColumnProps[] = [defaultControlColumn];
const trailingControlColumns: ControlColumnProps[] = [];
const leadingControlColumns = useMemo(
() =>
getDefaultControlColumn(ACTION_BUTTON_COUNT).map((x) => ({
...x,
headerCellRender: HeaderActions,
})),
[]
);
return (
<>

View file

@ -116,6 +116,7 @@ const SmallNotesButton = React.memo<SmallNotesButtonProps>(
data-test-subj="timeline-notes-button-small"
iconType="editorComment"
onClick={toggleShowNotes}
size="s"
isDisabled={isTemplate}
/>
);

View file

@ -58,7 +58,8 @@ import { useTimelineFullScreen } from '../../../../common/containers/use_full_sc
import { activeTimeline } from '../../../containers/active_timeline_context';
import { DetailsPanel } from '../../side_panel';
import { ExitFullScreen } from '../../../../common/components/exit_full_screen';
import { defaultControlColumn } from '../body/control_columns';
import { HeaderActions } from '../body/actions/header_actions';
import { getDefaultControlColumn } from '../body/control_columns';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { Sourcerer } from '../../../../common/components/sourcerer';
@ -156,6 +157,8 @@ const EMPTY_EVENTS: TimelineItem[] = [];
export type Props = OwnProps & PropsFromRedux;
const trailingControlColumns: ControlColumnProps[] = []; // stable reference
export const QueryTabContentComponent: React.FC<Props> = ({
activeTab,
columns,
@ -194,6 +197,7 @@ export const QueryTabContentComponent: React.FC<Props> = ({
selectedPatterns,
} = useSourcererDataView(SourcererScopeName.timeline);
const { uiSettings } = useKibana().services;
const ACTION_BUTTON_COUNT = 5;
const getManageTimeline = useMemo(() => timelineSelectors.getManageTimelineById(), []);
const { filterManager: activeFilterManager } = useDeepEqualSelector((state) =>
@ -314,8 +318,14 @@ export const QueryTabContentComponent: React.FC<Props> = ({
return (combinedQueries && combinedQueries.kqlError != null) || false;
}, [combinedQueries]);
const leadingControlColumns: ControlColumnProps[] = [defaultControlColumn];
const trailingControlColumns: ControlColumnProps[] = [];
const leadingControlColumns = useMemo(
() =>
getDefaultControlColumn(ACTION_BUTTON_COUNT).map((x) => ({
...x,
headerCellRender: HeaderActions,
})),
[]
);
return (
<>

View file

@ -104,8 +104,6 @@ interface AdditionalControlColumnProps {
// Override these type definitions to support either a generic custom component or the one used in security_solution today.
headerCellRender: HeaderCellRender;
rowCellRender: RowCellRender;
// If not provided, calculated dynamically
width?: number;
}
export type ControlColumnProps = Omit<

View file

@ -9,7 +9,7 @@ import React, { MouseEvent } from 'react';
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import { EventsTdContent } from '../t_grid/styles';
import { DEFAULT_ICON_BUTTON_WIDTH } from '../t_grid/helpers';
import { DEFAULT_ACTION_BUTTON_WIDTH } from '../t_grid/body/constants';
interface ActionIconItemProps {
ariaLabel?: string;
@ -23,7 +23,7 @@ interface ActionIconItemProps {
}
const ActionIconItemComponent: React.FC<ActionIconItemProps> = ({
width = DEFAULT_ICON_BUTTON_WIDTH,
width = DEFAULT_ACTION_BUTTON_WIDTH,
dataTestSubj,
content,
ariaLabel,

View file

@ -2,7 +2,7 @@
exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = `
<ColumnHeadersComponent
actionsColumnWidth={120}
actionsColumnWidth={124}
browserFields={
Object {
"agent": Object {

View file

@ -4,6 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme';
import { mount } from 'enzyme';
import { omit, set } from 'lodash/fp';
import React from 'react';
@ -17,11 +18,9 @@ import {
getSchema,
} from './helpers';
import {
DEFAULT_ACTION_BUTTON_WIDTH,
DEFAULT_COLUMN_MIN_WIDTH,
DEFAULT_DATE_COLUMN_MIN_WIDTH,
DEFAULT_ACTIONS_COLUMN_WIDTH,
EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH,
SHOW_CHECK_BOXES_COLUMN_WIDTH,
} from '../constants';
import { mockBrowserFields } from '../../../../mock/browser_fields';
import { ColumnHeaderOptions } from '../../../../../common';
@ -47,28 +46,6 @@ describe('helpers', () => {
});
});
describe('getActionsColumnWidth', () => {
test('returns the default actions column width when isEventViewer is false', () => {
expect(getActionsColumnWidth(false)).toEqual(DEFAULT_ACTIONS_COLUMN_WIDTH);
});
test('returns the default actions column width + checkbox width when isEventViewer is false and showCheckboxes is true', () => {
expect(getActionsColumnWidth(false, true)).toEqual(
DEFAULT_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH
);
});
test('returns the events viewer actions column width when isEventViewer is true', () => {
expect(getActionsColumnWidth(true)).toEqual(EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH);
});
test('returns the events viewer actions column width + checkbox width when isEventViewer is true and showCheckboxes is true', () => {
expect(getActionsColumnWidth(true, true)).toEqual(
EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH
);
});
});
describe('getSchema', () => {
const expected: Record<string, BUILT_IN_SCHEMA> = {
date: 'datetime',
@ -343,4 +320,35 @@ describe('helpers', () => {
).toEqual(expected);
});
});
describe('getActionsColumnWidth', () => {
// ideally the following implementation detail wouldn't be part of these tests,
// but without it, the test would be brittle when `euiDataGridCellPaddingM` changes:
const expectedPadding = parseInt(euiThemeVars.euiDataGridCellPaddingM, 10) * 2;
test('it returns the expected width', () => {
const ACTION_BUTTON_COUNT = 5;
const expectedContentWidth = ACTION_BUTTON_COUNT * DEFAULT_ACTION_BUTTON_WIDTH;
expect(getActionsColumnWidth(ACTION_BUTTON_COUNT)).toEqual(
expectedContentWidth + expectedPadding
);
});
test('it returns the minimum width when the button count is zero', () => {
const ACTION_BUTTON_COUNT = 0;
expect(getActionsColumnWidth(ACTION_BUTTON_COUNT)).toEqual(
DEFAULT_ACTION_BUTTON_WIDTH + expectedPadding
);
});
test('it returns the minimum width when the button count is negative', () => {
const ACTION_BUTTON_COUNT = -1;
expect(getActionsColumnWidth(ACTION_BUTTON_COUNT)).toEqual(
DEFAULT_ACTION_BUTTON_WIDTH + expectedPadding
);
});
});
});

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme';
import { EuiDataGridColumnActions } from '@elastic/eui';
import { get, keyBy } from 'lodash/fp';
import React from 'react';
@ -15,12 +16,9 @@ import type {
} from '../../../../../common/search_strategy/index_fields';
import type { ColumnHeaderOptions } from '../../../../../common/types/timeline';
import {
DEFAULT_ACTION_BUTTON_WIDTH,
DEFAULT_COLUMN_MIN_WIDTH,
DEFAULT_DATE_COLUMN_MIN_WIDTH,
SHOW_CHECK_BOXES_COLUMN_WIDTH,
EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH,
DEFAULT_ACTIONS_COLUMN_WIDTH,
MINIMUM_ACTIONS_COLUMN_WIDTH,
} from '../constants';
import { allowSorting } from '../helpers';
@ -127,19 +125,25 @@ export const getColumnHeaders = (
export const getColumnWidthFromType = (type: string): number =>
type !== 'date' ? DEFAULT_COLUMN_MIN_WIDTH : DEFAULT_DATE_COLUMN_MIN_WIDTH;
/** Returns the (fixed) width of the Actions column */
export const getActionsColumnWidth = (
isEventViewer: boolean,
showCheckboxes = false,
additionalActionWidth = 0
): number => {
const checkboxesWidth = showCheckboxes ? SHOW_CHECK_BOXES_COLUMN_WIDTH : 0;
const actionsColumnWidth =
checkboxesWidth +
(isEventViewer ? EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH : DEFAULT_ACTIONS_COLUMN_WIDTH) +
additionalActionWidth;
/**
* Returns the width of the Actions column based on the number of buttons being
* displayed
*
* NOTE: This function is necessary because `width` is a required property of
* the `EuiDataGridControlColumn` interface, so it must be calculated before
* content is rendered. (The width of a `EuiDataGridControlColumn` does not
* automatically size itself to fit all the content.)
*/
export const getActionsColumnWidth = (actionButtonCount: number): number => {
const contentWidth =
actionButtonCount > 0
? actionButtonCount * DEFAULT_ACTION_BUTTON_WIDTH
: DEFAULT_ACTION_BUTTON_WIDTH;
return actionsColumnWidth > MINIMUM_ACTIONS_COLUMN_WIDTH + checkboxesWidth
? actionsColumnWidth
: MINIMUM_ACTIONS_COLUMN_WIDTH + checkboxesWidth;
// `EuiDataGridRowCell` applies additional `padding-left` and
// `padding-right`, which must be added to the content width to prevent the
// content from being partially hidden due to the space occupied by padding:
const leftRightCellPadding = parseInt(euiThemeVars.euiDataGridCellPaddingM, 10) * 2; // parseInt ignores the trailing `px`, e.g. `6px`
return contentWidth + leftRightCellPadding;
};

View file

@ -8,7 +8,8 @@
import { shallow } from 'enzyme';
import React from 'react';
import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants';
import { getActionsColumnWidth } from './helpers';
import { defaultHeaders } from './default_headers';
import { Sort } from '../sort';
@ -51,6 +52,8 @@ const timelineId = 'test';
describe('ColumnHeaders', () => {
const mount = useMountAppended();
const ACTION_BUTTON_COUNT = 4;
const actionsColumnWidth = getActionsColumnWidth(ACTION_BUTTON_COUNT);
describe('rendering', () => {
const sort: Sort[] = [
@ -65,7 +68,7 @@ describe('ColumnHeaders', () => {
const wrapper = shallow(
<TestProviders>
<ColumnHeadersComponent
actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH}
actionsColumnWidth={actionsColumnWidth}
browserFields={mockBrowserFields}
columnHeaders={defaultHeaders}
isSelectAllChecked={false}
@ -88,7 +91,7 @@ describe('ColumnHeaders', () => {
const wrapper = mount(
<TestProviders>
<ColumnHeadersComponent
actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH}
actionsColumnWidth={actionsColumnWidth}
browserFields={mockBrowserFields}
columnHeaders={defaultHeaders}
isSelectAllChecked={false}
@ -111,7 +114,7 @@ describe('ColumnHeaders', () => {
const wrapper = mount(
<TestProviders>
<ColumnHeadersComponent
actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH}
actionsColumnWidth={actionsColumnWidth}
browserFields={mockBrowserFields}
columnHeaders={defaultHeaders}
isSelectAllChecked={false}
@ -172,7 +175,7 @@ describe('ColumnHeaders', () => {
const wrapper = mount(
<TestProviders>
<ColumnHeadersComponent
actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH}
actionsColumnWidth={actionsColumnWidth}
browserFields={mockBrowserFields}
columnHeaders={mockDefaultHeaders}
isSelectAllChecked={false}
@ -216,7 +219,7 @@ describe('ColumnHeaders', () => {
const wrapper = mount(
<TestProviders>
<ColumnHeadersComponent
actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH}
actionsColumnWidth={actionsColumnWidth}
browserFields={mockBrowserFields}
columnHeaders={mockDefaultHeaders}
isSelectAllChecked={false}
@ -255,7 +258,7 @@ describe('ColumnHeaders', () => {
const wrapper = mount(
<TestProviders>
<ColumnHeadersComponent
actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH}
actionsColumnWidth={actionsColumnWidth}
browserFields={mockBrowserFields}
columnHeaders={mockDefaultHeaders}
isSelectAllChecked={false}
@ -293,7 +296,7 @@ describe('ColumnHeaders', () => {
const wrapper = mount(
<TestProviders>
<ColumnHeadersComponent
actionsColumnWidth={DEFAULT_ACTIONS_COLUMN_WIDTH}
actionsColumnWidth={actionsColumnWidth}
browserFields={mockBrowserFields}
columnHeaders={mockDefaultHeaders}
isSelectAllChecked={false}

View file

@ -5,20 +5,28 @@
* 2.0.
*/
/** The minimum (fixed) width of the Actions column */
export const MINIMUM_ACTIONS_COLUMN_WIDTH = 50; // px;
import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme';
/**
* This is the effective width in pixels of an action button used with
* `EuiDataGrid` `leadingControlColumns`. (See Notes below for details)
*
* Notes:
* 1) This constant is necessary because `width` is a required property of
* the `EuiDataGridControlColumn` interface, so it must be calculated before
* content is rendered. (The width of a `EuiDataGridControlColumn` does not
* automatically size itself to fit all the content.)
*
* 2) This is the *effective* width, because at the time of this writing,
* `EuiButtonIcon` has a `margin-left: -4px`, which is subtracted from the
* `width`
*/
export const DEFAULT_ACTION_BUTTON_WIDTH =
parseInt(euiThemeVars.euiSizeXL, 10) - parseInt(euiThemeVars.euiSizeXS, 10); // px
/** Additional column width to include when checkboxes are shown **/
export const SHOW_CHECK_BOXES_COLUMN_WIDTH = 24; // px;
/** The (fixed) width of the Actions column */
export const DEFAULT_ACTIONS_COLUMN_WIDTH = SHOW_CHECK_BOXES_COLUMN_WIDTH * 5; // px;
/**
* The (fixed) width of the Actions column when the timeline body is used as
* an events viewer, which has fewer actions than a regular events viewer
*/
export const EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH = SHOW_CHECK_BOXES_COLUMN_WIDTH * 4; // px;
/** The default minimum width of a column (when a width for the column type is not specified) */
export const DEFAULT_COLUMN_MIN_WIDTH = 180; // px

View file

@ -8,7 +8,7 @@
import { mount } from 'enzyme';
import React from 'react';
import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants';
import { getActionsColumnWidth } from '../column_headers/helpers';
import { EventColumnView } from './event_column_view';
import { TestCellRenderer } from '../../../../mock/cell_renderer';
@ -23,10 +23,11 @@ jest.mock('../../../../hooks/use_selector', () => ({
}));
describe('EventColumnView', () => {
const ACTION_BUTTON_COUNT = 4;
const props = {
ariaRowindex: 2,
id: 'event-id',
actionsColumnWidth: DEFAULT_ACTIONS_COLUMN_WIDTH,
actionsColumnWidth: getActionsColumnWidth(ACTION_BUTTON_COUNT),
associateNote: jest.fn(),
columnHeaders: [],
columnRenderers: [],

View file

@ -50,7 +50,7 @@ import {
import type { TimelineItem, TimelineNonEcsData } from '../../../../common/search_strategy/timeline';
import { getActionsColumnWidth, getColumnHeaders } from './column_headers/helpers';
import { getColumnHeaders } from './column_headers/helpers';
import {
addBuildingBlockStyle,
getEventIdToDataMapping,
@ -58,7 +58,6 @@ import {
mapSortingColumns,
} from './helpers';
import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers';
import type { BrowserFields } from '../../../../common/search_strategy/index_fields';
import type { OnRowSelected, OnSelectAll } from '../types';
import type { Refetch } from '../../../store/t_grid/inputs';
@ -119,19 +118,14 @@ interface OwnProps {
}
const defaultUnit = (n: number) => i18n.ALERTS_UNIT(n);
const NUM_OF_ICON_IN_TIMELINE_ROW = 2;
export const hasAdditionalActions = (id: TimelineId): boolean =>
[TimelineId.detectionsPage, TimelineId.detectionsRulesDetailsPage, TimelineId.active].includes(
id
);
const EXTRA_WIDTH = 4; // px
const ES_LIMIT_COUNT = 9999;
const MIN_ACTION_COLUMN_WIDTH = 96; // px
const EMPTY_CONTROL_COLUMNS: ControlColumnProps[] = [];
const EmptyHeaderCellRender: ComponentType = () => null;
@ -157,7 +151,6 @@ const FIELDS_WITHOUT_CELL_ACTIONS = [
const hasCellActions = (columnId?: string) =>
columnId && FIELDS_WITHOUT_CELL_ACTIONS.indexOf(columnId) < 0;
const transformControlColumns = ({
actionColumnsWidth,
columnHeaders,
controlColumns,
data,
@ -179,7 +172,6 @@ const transformControlColumns = ({
setEventsDeleted,
hasAlertsCrudPermissions,
}: {
actionColumnsWidth: number;
columnHeaders: ColumnHeaderOptions[];
controlColumns: ControlColumnProps[];
data: TimelineItem[];
@ -216,7 +208,7 @@ const transformControlColumns = ({
<>
{HeaderActions && (
<HeaderActions
width={width ?? MIN_ACTION_COLUMN_WIDTH}
width={width}
browserFields={browserFields}
columnHeaders={columnHeaders}
isEventViewer={isEventViewer}
@ -283,13 +275,13 @@ const transformControlColumns = ({
showCheckboxes={showCheckboxes}
tabType={tabType}
timelineId={timelineId}
width={width ?? MIN_ACTION_COLUMN_WIDTH}
width={width}
setEventsLoading={setEventsLoading}
setEventsDeleted={setEventsDeleted}
/>
);
},
width: width ?? actionColumnsWidth,
width,
})
);
@ -618,13 +610,6 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
controlColumns,
data,
isEventViewer,
actionColumnsWidth: hasAdditionalActions(id as TimelineId)
? getActionsColumnWidth(
isEventViewer,
showCheckboxes,
DEFAULT_ICON_BUTTON_WIDTH * NUM_OF_ICON_IN_TIMELINE_ROW + EXTRA_WIDTH
)
: controlColumns.reduce((acc, c) => acc + (c.width ?? MIN_ACTION_COLUMN_WIDTH), 0),
loadingEventIds,
onRowSelected,
onRuleChange,

View file

@ -241,8 +241,6 @@ export const getCombinedFilterQuery = ({
*/
export const STATEFUL_EVENT_CSS_CLASS_NAME = 'event-column-view';
export const DEFAULT_ICON_BUTTON_WIDTH = 24;
export const resolverIsShowing = (graphEventId: string | undefined): boolean =>
graphEventId != null && graphEventId !== '';

View file

@ -57,6 +57,8 @@ export {
addFieldToTimelineColumns,
getTimelineIdFromColumnDroppableId,
} from './components/drag_and_drop/helpers';
export { getActionsColumnWidth } from './components/t_grid/body/column_headers/helpers';
export { DEFAULT_ACTION_BUTTON_WIDTH } from './components/t_grid/body/constants';
export { StatefulFieldsBrowser } from './components/t_grid/toolbar/fields_browser';
export { useStatusBulkActionItems } from './hooks/use_status_bulk_action_items';
// This exports static code and TypeScript types,