mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[SECURITY SOLUTION] [RAC] Event rendered view (#108644)
* wip * match design for selecting grid view * wip to integrate event rendered view * wip * integration of the event rendered * fix perPage action on Euibasic table * Add bulding block background color to EventRenderedView * styling * remove header * fix types * fix unit tests * use memo for listProps * fix styling + add feature flag * review I * fix merge * change the gutter size Co-authored-by: Pablo Neves Machado <pablo.nevesmachado@elastic.co> Co-authored-by: Angela Chuang <yi-chun.chuang@elastic.co>
This commit is contained in:
parent
09e8cfd305
commit
3013e10eda
14 changed files with 637 additions and 472 deletions
|
@ -15,6 +15,7 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
metricsEntitiesEnabled: false,
|
||||
ruleRegistryEnabled: false,
|
||||
tGridEnabled: true,
|
||||
tGridEventRenderedViewEnabled: true,
|
||||
trustedAppsByPolicyEnabled: false,
|
||||
excludePoliciesInFilterEnabled: false,
|
||||
uebaEnabled: false,
|
||||
|
|
|
@ -67,7 +67,7 @@ export const getColumns = ({
|
|||
),
|
||||
sortable: false,
|
||||
truncateText: false,
|
||||
width: '180px',
|
||||
width: '132px',
|
||||
render: (values: string[] | null | undefined, data: EventFieldsData) => {
|
||||
const label = data.isObjectArray
|
||||
? i18n.NESTED_COLUMN(data.field)
|
||||
|
|
|
@ -58,7 +58,6 @@ export interface OwnProps {
|
|||
scopeId: SourcererScopeName;
|
||||
start: string;
|
||||
showTotalCount?: boolean;
|
||||
headerFilterGroup?: React.ReactNode;
|
||||
pageFilters?: Filter[];
|
||||
currentFilter?: Status;
|
||||
onRuleChange?: () => void;
|
||||
|
@ -88,7 +87,6 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
entityType,
|
||||
excludedRowRendererIds,
|
||||
filters,
|
||||
headerFilterGroup,
|
||||
id,
|
||||
isLive,
|
||||
itemsPerPage,
|
||||
|
@ -120,6 +118,9 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen();
|
||||
// TODO: Once we are past experimental phase this code should be removed
|
||||
const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled');
|
||||
const tGridEventRenderedViewEnabled = useIsExperimentalFeatureEnabled(
|
||||
'tGridEventRenderedViewEnabled'
|
||||
);
|
||||
useEffect(() => {
|
||||
if (createTimeline != null) {
|
||||
createTimeline({
|
||||
|
@ -153,6 +154,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
<InspectButtonContainer>
|
||||
{tGridEnabled ? (
|
||||
timelinesUi.getTGrid<'embedded'>({
|
||||
id,
|
||||
type: 'embedded',
|
||||
browserFields,
|
||||
columns,
|
||||
|
@ -165,8 +167,6 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
filters: globalFilters,
|
||||
globalFullScreen,
|
||||
graphOverlay,
|
||||
headerFilterGroup,
|
||||
id,
|
||||
indexNames: selectedPatterns,
|
||||
indexPattern,
|
||||
isLive,
|
||||
|
@ -186,6 +186,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
filterStatus: currentFilter,
|
||||
leadingControlColumns,
|
||||
trailingControlColumns,
|
||||
tGridEventRenderedViewEnabled,
|
||||
})
|
||||
) : (
|
||||
<EventsViewer
|
||||
|
@ -198,7 +199,6 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
end={end}
|
||||
isLoadingIndexPattern={isLoadingIndexPattern}
|
||||
filters={globalFilters}
|
||||
headerFilterGroup={headerFilterGroup}
|
||||
indexNames={selectedPatterns}
|
||||
indexPattern={indexPattern}
|
||||
isLive={isLive}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { CoreStart } from '../../../../../../src/core/public';
|
||||
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
|
||||
|
||||
interface RuleNameProps {
|
||||
name: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
const appendSearch = (search?: string) =>
|
||||
isEmpty(search) ? '' : `${search?.startsWith('?') ? search : `?${search}`}`;
|
||||
|
||||
const RuleNameComponents = ({ name, id }: RuleNameProps) => {
|
||||
const { navigateToApp, getUrlForApp } = useKibana<CoreStart>().services.application;
|
||||
|
||||
const hrefRuleDetails = useMemo(
|
||||
() =>
|
||||
getUrlForApp('securitySolution', {
|
||||
deepLinkId: 'rules',
|
||||
path: `/id/${id}${appendSearch(window.location.search)}`,
|
||||
}),
|
||||
[getUrlForApp, id]
|
||||
);
|
||||
const goToRuleDetails = useCallback(
|
||||
(ev) => {
|
||||
ev.preventDefault();
|
||||
navigateToApp('securitySolution', {
|
||||
deepLinkId: 'rules',
|
||||
path: `/id/${id}${appendSearch(window.location.search)}`,
|
||||
});
|
||||
},
|
||||
[navigateToApp, id]
|
||||
);
|
||||
return (
|
||||
// eslint-disable-next-line @elastic/eui/href-or-on-click
|
||||
<EuiLink href={hrefRuleDetails} onClick={goToRuleDetails}>
|
||||
{name}
|
||||
</EuiLink>
|
||||
);
|
||||
};
|
||||
|
||||
export const RuleName = React.memo(RuleNameComponents);
|
|
@ -66,8 +66,10 @@ describe('Body', () => {
|
|||
excludedRowRendererIds: [],
|
||||
id: 'timeline-test',
|
||||
isSelectAllChecked: false,
|
||||
itemsPerPageOptions: [],
|
||||
loadingEventIds: [],
|
||||
loadPage: jest.fn(),
|
||||
querySize: 25,
|
||||
renderCellValue: TestCellRenderer,
|
||||
rowRenderers: [],
|
||||
selectedEventIds: {},
|
||||
|
@ -75,6 +77,7 @@ describe('Body', () => {
|
|||
sort: mockSort,
|
||||
showCheckboxes: false,
|
||||
tabType: TimelineTabs.query,
|
||||
tableView: 'gridView',
|
||||
totalPages: 1,
|
||||
totalItems: 1,
|
||||
leadingControlColumns: [],
|
||||
|
|
|
@ -13,6 +13,8 @@ import {
|
|||
EuiDataGridStyle,
|
||||
EuiDataGridToolBarVisibilityOptions,
|
||||
EuiLoadingSpinner,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { getOr } from 'lodash/fp';
|
||||
import memoizeOne from 'memoize-one';
|
||||
|
@ -67,6 +69,8 @@ import * as i18n from './translations';
|
|||
import { AlertCount } from '../styles';
|
||||
import { checkBoxControlColumn } from './control_columns';
|
||||
import type { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common';
|
||||
import { ViewSelection } from '../event_rendered_view/selector';
|
||||
import { EventRenderedView } from '../event_rendered_view';
|
||||
|
||||
const StatefulAlertStatusBulkActions = lazy(
|
||||
() => import('../toolbar/bulk_actions/alert_status_bulk_actions')
|
||||
|
@ -76,25 +80,28 @@ interface OwnProps {
|
|||
activePage: number;
|
||||
additionalControls?: React.ReactNode;
|
||||
browserFields: BrowserFields;
|
||||
filterQuery: string;
|
||||
bulkActions?: BulkActionsProp;
|
||||
data: TimelineItem[];
|
||||
defaultCellActions?: TGridCellAction[];
|
||||
filterQuery: string;
|
||||
filterStatus?: AlertStatus;
|
||||
id: string;
|
||||
indexNames: string[];
|
||||
isEventViewer?: boolean;
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
rowRenderers: RowRenderer[];
|
||||
tabType: TimelineTabs;
|
||||
itemsPerPageOptions: number[];
|
||||
leadingControlColumns?: ControlColumnProps[];
|
||||
loadPage: (newActivePage: number) => void;
|
||||
trailingControlColumns?: ControlColumnProps[];
|
||||
totalPages: number;
|
||||
totalItems: number;
|
||||
bulkActions?: BulkActionsProp;
|
||||
filterStatus?: AlertStatus;
|
||||
unit?: (total: number) => React.ReactNode;
|
||||
onRuleChange?: () => void;
|
||||
indexNames: string[];
|
||||
querySize: number;
|
||||
refetch: Refetch;
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
rowRenderers: RowRenderer[];
|
||||
tableView: ViewSelection;
|
||||
tabType: TimelineTabs;
|
||||
totalItems: number;
|
||||
totalPages: number;
|
||||
trailingControlColumns?: ControlColumnProps[];
|
||||
unit?: (total: number) => React.ReactNode;
|
||||
}
|
||||
|
||||
const basicUnit = (n: number) => i18n.UNIT(n);
|
||||
|
@ -235,34 +242,37 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
activePage,
|
||||
additionalControls,
|
||||
browserFields,
|
||||
filterQuery,
|
||||
bulkActions = true,
|
||||
clearSelected,
|
||||
columnHeaders,
|
||||
data,
|
||||
defaultCellActions,
|
||||
excludedRowRendererIds,
|
||||
filterQuery,
|
||||
filterStatus,
|
||||
id,
|
||||
indexNames,
|
||||
isEventViewer = false,
|
||||
isSelectAllChecked,
|
||||
itemsPerPageOptions,
|
||||
leadingControlColumns = EMPTY_CONTROL_COLUMNS,
|
||||
loadingEventIds,
|
||||
loadPage,
|
||||
selectedEventIds,
|
||||
setSelected,
|
||||
clearSelected,
|
||||
onRuleChange,
|
||||
showCheckboxes,
|
||||
querySize,
|
||||
refetch,
|
||||
renderCellValue,
|
||||
rowRenderers,
|
||||
selectedEventIds,
|
||||
setSelected,
|
||||
showCheckboxes,
|
||||
sort,
|
||||
tableView = 'gridView',
|
||||
tabType,
|
||||
totalPages,
|
||||
totalItems,
|
||||
filterStatus,
|
||||
bulkActions = true,
|
||||
unit = basicUnit,
|
||||
leadingControlColumns = EMPTY_CONTROL_COLUMNS,
|
||||
totalPages,
|
||||
trailingControlColumns = EMPTY_CONTROL_COLUMNS,
|
||||
indexNames,
|
||||
refetch,
|
||||
unit = basicUnit,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []);
|
||||
|
@ -336,6 +346,43 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
return bulkActions.alertStatusActions ?? true;
|
||||
}, [selectedCount, showCheckboxes, bulkActions]);
|
||||
|
||||
const alertToolbar = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup gutterSize="m" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<AlertCount>{alertCountText}</AlertCount>
|
||||
</EuiFlexItem>
|
||||
{showBulkActions && (
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
<StatefulAlertStatusBulkActions
|
||||
data-test-subj="bulk-actions"
|
||||
id={id}
|
||||
totalItems={totalItems}
|
||||
filterStatus={filterStatus}
|
||||
query={filterQuery}
|
||||
indexName={indexNames.join()}
|
||||
onActionSuccess={onAlertStatusActionSuccess}
|
||||
onActionFailure={onAlertStatusActionFailure}
|
||||
refetch={refetch}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[
|
||||
alertCountText,
|
||||
filterQuery,
|
||||
filterStatus,
|
||||
id,
|
||||
indexNames,
|
||||
onAlertStatusActionFailure,
|
||||
onAlertStatusActionSuccess,
|
||||
refetch,
|
||||
showBulkActions,
|
||||
totalItems,
|
||||
]
|
||||
);
|
||||
|
||||
const toolbarVisibility: EuiDataGridToolBarVisibilityOptions = useMemo(
|
||||
() => ({
|
||||
additionalControls: (
|
||||
|
@ -573,20 +620,39 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
|||
}, [columnHeaders, data, id, renderCellValue, tabType, theme, browserFields, rowRenderers]);
|
||||
|
||||
return (
|
||||
<EuiDataGrid
|
||||
data-test-subj="body-data-grid"
|
||||
aria-label={i18n.TGRID_BODY_ARIA_LABEL}
|
||||
columns={columnsWithCellActions}
|
||||
columnVisibility={{ visibleColumns, setVisibleColumns }}
|
||||
gridStyle={gridStyle}
|
||||
leadingControlColumns={leadingTGridControlColumns}
|
||||
trailingControlColumns={trailingTGridControlColumns}
|
||||
toolbarVisibility={toolbarVisibility}
|
||||
rowCount={data.length}
|
||||
renderCellValue={renderTGridCellValue}
|
||||
inMemory={{ level: 'sorting' }}
|
||||
sorting={{ columns: sortingColumns, onSort }}
|
||||
/>
|
||||
<>
|
||||
{tableView === 'gridView' && (
|
||||
<EuiDataGrid
|
||||
data-test-subj="body-data-grid"
|
||||
aria-label={i18n.TGRID_BODY_ARIA_LABEL}
|
||||
columns={columnsWithCellActions}
|
||||
columnVisibility={{ visibleColumns, setVisibleColumns }}
|
||||
gridStyle={gridStyle}
|
||||
leadingControlColumns={leadingTGridControlColumns}
|
||||
trailingControlColumns={trailingTGridControlColumns}
|
||||
toolbarVisibility={toolbarVisibility}
|
||||
rowCount={data.length}
|
||||
renderCellValue={renderTGridCellValue}
|
||||
inMemory={{ level: 'sorting' }}
|
||||
sorting={{ columns: sortingColumns, onSort }}
|
||||
/>
|
||||
)}
|
||||
{tableView === 'eventRenderedView' && (
|
||||
<EventRenderedView
|
||||
alertToolbar={alertToolbar}
|
||||
browserFields={browserFields}
|
||||
events={data}
|
||||
leadingControlColumns={leadingTGridControlColumns ?? []}
|
||||
onChangePage={loadPage}
|
||||
pageIndex={activePage}
|
||||
pageSize={querySize}
|
||||
pageSizeOptions={itemsPerPageOptions}
|
||||
rowRenderers={rowRenderers}
|
||||
timelineId={id}
|
||||
totalItemCount={totalItems}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -635,4 +701,4 @@ const connector = connect(makeMapStateToProps, mapDispatchToProps);
|
|||
|
||||
type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
|
||||
export const StatefulBody = connector(BodyComponent);
|
||||
export const StatefulBody: React.FunctionComponent<OwnProps> = connector(BodyComponent);
|
||||
|
|
|
@ -0,0 +1,249 @@
|
|||
/*
|
||||
* 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 {
|
||||
CriteriaWithPagination,
|
||||
EuiBasicTable,
|
||||
EuiBasicTableProps,
|
||||
EuiDataGridCellValueElementProps,
|
||||
EuiDataGridControlColumn,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
/* ALERT_REASON, ALERT_RULE_ID, */ ALERT_RULE_NAME,
|
||||
TIMESTAMP,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { get } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import React, { ComponentType, useCallback, useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useUiSetting } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
|
||||
import type { BrowserFields, RowRenderer, TimelineItem } from '../../../../common';
|
||||
import { tGridActions } from '../../../store/t_grid';
|
||||
import { RuleName } from '../../rule_name';
|
||||
import { isEventBuildingBlockType } from '../body/helpers';
|
||||
|
||||
const EventRenderedFlexItem = styled(EuiFlexItem)`
|
||||
div:first-child {
|
||||
padding-left: 0px;
|
||||
div {
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Fix typing issue with EuiBasicTable and styled
|
||||
type BasicTableType = ComponentType<EuiBasicTableProps<TimelineItem>>;
|
||||
|
||||
const StyledEuiBasicTable = styled(EuiBasicTable as BasicTableType)`
|
||||
padding-top: ${({ theme }) => theme.eui.paddingSizes.m};
|
||||
.EventRenderedView__buildingBlock {
|
||||
background: ${({ theme }) => theme.eui.euiColorHighlight};
|
||||
}
|
||||
|
||||
& > div:last-child {
|
||||
height: 72px;
|
||||
}
|
||||
`;
|
||||
|
||||
interface EventRenderedViewProps {
|
||||
alertToolbar: React.ReactNode;
|
||||
browserFields: BrowserFields;
|
||||
events: TimelineItem[];
|
||||
leadingControlColumns: EuiDataGridControlColumn[];
|
||||
onChangePage: (newActivePage: number) => void;
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
pageSizeOptions: number[];
|
||||
rowRenderers: RowRenderer[];
|
||||
timelineId: string;
|
||||
totalItemCount: number;
|
||||
}
|
||||
const PreferenceFormattedDateComponent = ({ value }: { value: Date }) => {
|
||||
const tz = useUiSetting<string>('dateFormat:tz');
|
||||
const dateFormat = useUiSetting<string>('dateFormat');
|
||||
return <>{moment.tz(value, tz).format(dateFormat)}</>;
|
||||
};
|
||||
export const PreferenceFormattedDate = React.memo(PreferenceFormattedDateComponent);
|
||||
|
||||
const EventRenderedViewComponent = ({
|
||||
alertToolbar,
|
||||
browserFields,
|
||||
events,
|
||||
leadingControlColumns,
|
||||
onChangePage,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
pageSizeOptions,
|
||||
rowRenderers,
|
||||
timelineId,
|
||||
totalItemCount,
|
||||
}: EventRenderedViewProps) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const ActionTitle = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
{leadingControlColumns.map((action) => {
|
||||
const ActionHeader = action.headerCellRender;
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<ActionHeader />
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[leadingControlColumns]
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
field: 'actions',
|
||||
name: ActionTitle,
|
||||
truncateText: false,
|
||||
hideForMobile: false,
|
||||
render: (name: unknown, item: unknown) => {
|
||||
const alertId = get(item, '_id');
|
||||
const rowIndex = events.findIndex((evt) => evt._id === alertId);
|
||||
return leadingControlColumns.length > 0
|
||||
? leadingControlColumns.map((action) => {
|
||||
const getActions = action.rowCellRender as (
|
||||
props: EuiDataGridCellValueElementProps
|
||||
) => React.ReactNode;
|
||||
return getActions({
|
||||
columnId: 'actions',
|
||||
isDetails: false,
|
||||
isExpandable: false,
|
||||
isExpanded: false,
|
||||
rowIndex,
|
||||
setCellProps: () => null,
|
||||
});
|
||||
})
|
||||
: null;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'ecs.@timestamp',
|
||||
name: i18n.translate('xpack.timelines.alerts.EventRenderedView.timestamp.column', {
|
||||
defaultMessage: 'Timestamp',
|
||||
}),
|
||||
truncateText: false,
|
||||
hideForMobile: false,
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (name: unknown, item: TimelineItem) => {
|
||||
const timestamp = get(item, `ecs.${TIMESTAMP}`);
|
||||
return <PreferenceFormattedDate value={timestamp} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: `ecs.${ALERT_RULE_NAME}`,
|
||||
name: i18n.translate('xpack.timelines.alerts.EventRenderedView.rule.column', {
|
||||
defaultMessage: 'Rule',
|
||||
}),
|
||||
truncateText: false,
|
||||
hideForMobile: false,
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (name: unknown, item: TimelineItem) => {
|
||||
const ruleName = get(item, `ecs.signal.rule.name`); /* `ecs.${ALERT_RULE_NAME}`*/
|
||||
const ruleId = get(item, `ecs.signal.rule.id}`); /* `ecs.${ALERT_RULE_ID}`*/
|
||||
return <RuleName name={ruleName} id={ruleId} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'eventSummary',
|
||||
name: i18n.translate('xpack.timelines.alerts.EventRenderedView.eventSummary.column', {
|
||||
defaultMessage: 'Event Summary',
|
||||
}),
|
||||
truncateText: false,
|
||||
hideForMobile: false,
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (name: unknown, item: TimelineItem) => {
|
||||
const ecsData = get(item, 'ecs');
|
||||
const reason = get(item, `ecs.signal.reason`); /* `ecs.${ALERT_REASON}`*/
|
||||
const rowRenderersValid = rowRenderers.filter((rowRenderer) =>
|
||||
rowRenderer.isInstance(ecsData)
|
||||
);
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="none" direction="column">
|
||||
{reason && <EuiFlexItem>{reason}</EuiFlexItem>}
|
||||
{rowRenderersValid.length > 0 &&
|
||||
rowRenderersValid.map((rowRenderer) => (
|
||||
<>
|
||||
<EuiHorizontalRule size="half" margin="xs" />
|
||||
<EventRenderedFlexItem>
|
||||
{rowRenderer.renderRow({
|
||||
browserFields,
|
||||
data: ecsData,
|
||||
isDraggable: false,
|
||||
timelineId: 'NONE',
|
||||
})}
|
||||
</EventRenderedFlexItem>
|
||||
</>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
},
|
||||
width: '60%',
|
||||
},
|
||||
],
|
||||
[ActionTitle, browserFields, events, leadingControlColumns, rowRenderers]
|
||||
);
|
||||
|
||||
const handleTableChange = useCallback(
|
||||
(pageChange: CriteriaWithPagination<TimelineItem>) => {
|
||||
if (pageChange.page.index !== pageIndex) {
|
||||
onChangePage(pageChange.page.index);
|
||||
}
|
||||
if (pageChange.page.size !== pageSize) {
|
||||
dispatch(
|
||||
tGridActions.updateItemsPerPage({ id: timelineId, itemsPerPage: pageChange.page.size })
|
||||
);
|
||||
}
|
||||
},
|
||||
[dispatch, onChangePage, pageIndex, pageSize, timelineId]
|
||||
);
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
totalItemCount,
|
||||
pageSizeOptions,
|
||||
hidePerPageOptions: false,
|
||||
}),
|
||||
[pageIndex, pageSize, pageSizeOptions, totalItemCount]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{alertToolbar}
|
||||
<StyledEuiBasicTable
|
||||
compressed
|
||||
items={events}
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
onChange={handleTableChange}
|
||||
rowProps={({ ecs }: TimelineItem) =>
|
||||
isEventBuildingBlockType(ecs)
|
||||
? {
|
||||
className: `EventRenderedView__buildingBlock`,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const EventRenderedView = React.memo(EventRenderedViewComponent);
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButtonEmpty,
|
||||
EuiPopover,
|
||||
EuiSelectable,
|
||||
EuiSelectableOption,
|
||||
EuiTitle,
|
||||
EuiTextColor,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export type ViewSelection = 'gridView' | 'eventRenderedView';
|
||||
|
||||
const ContainerEuiSelectable = styled.div`
|
||||
width: 300px;
|
||||
.euiSelectableListItem__text {
|
||||
white-space: pre-wrap !important;
|
||||
line-height: normal;
|
||||
}
|
||||
`;
|
||||
|
||||
const gridView = i18n.translate('xpack.timelines.alerts.summaryView.gridView.label', {
|
||||
defaultMessage: 'Grid view',
|
||||
});
|
||||
|
||||
const eventRenderedView = i18n.translate(
|
||||
'xpack.timelines.alerts.summaryView.eventRendererView.label',
|
||||
{
|
||||
defaultMessage: 'Event rendered view',
|
||||
}
|
||||
);
|
||||
|
||||
interface SummaryViewSelectorProps {
|
||||
onViewChange: (viewSelection: ViewSelection) => void;
|
||||
viewSelected: ViewSelection;
|
||||
}
|
||||
|
||||
const SummaryViewSelectorComponent = ({ viewSelected, onViewChange }: SummaryViewSelectorProps) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
const onButtonClick = useCallback(() => setIsPopoverOpen((currentVal) => !currentVal), []);
|
||||
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
|
||||
const onChangeSelectable = useCallback(
|
||||
(opts: EuiSelectableOption[]) => {
|
||||
const selected = opts.filter((i) => i.checked === 'on');
|
||||
if (selected.length > 0) {
|
||||
onViewChange((selected[0]?.key ?? 'gridView') as ViewSelection);
|
||||
}
|
||||
setIsPopoverOpen(false);
|
||||
},
|
||||
[onViewChange]
|
||||
);
|
||||
|
||||
const button = useMemo(
|
||||
() => (
|
||||
<EuiButtonEmpty
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
iconSize="s"
|
||||
onClick={onButtonClick}
|
||||
size="xs"
|
||||
flush="both"
|
||||
style={{ fontWeight: 'normal' }}
|
||||
>
|
||||
{viewSelected === 'gridView' ? gridView : eventRenderedView}
|
||||
</EuiButtonEmpty>
|
||||
),
|
||||
[onButtonClick, viewSelected]
|
||||
);
|
||||
|
||||
const options = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: gridView,
|
||||
key: 'gridView',
|
||||
checked: (viewSelected === 'gridView' ? 'on' : undefined) as EuiSelectableOption['checked'],
|
||||
meta: [
|
||||
{
|
||||
text: i18n.translate('xpack.timelines.alerts.summaryView.options.default.description', {
|
||||
defaultMessage:
|
||||
'View as tabular data with the ability to group and sort by specific fields',
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: eventRenderedView,
|
||||
key: 'eventRenderedView',
|
||||
checked: (viewSelected === 'eventRenderedView'
|
||||
? 'on'
|
||||
: undefined) as EuiSelectableOption['checked'],
|
||||
meta: [
|
||||
{
|
||||
text: i18n.translate(
|
||||
'xpack.timelines.alerts.summaryView.options.summaryView.description',
|
||||
{
|
||||
defaultMessage: 'View a rendering of the event flow for each alert',
|
||||
}
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[viewSelected]
|
||||
);
|
||||
|
||||
const renderOption = useCallback((option) => {
|
||||
return (
|
||||
<>
|
||||
<EuiTitle size="xxs">
|
||||
<h6>{option.label}</h6>
|
||||
</EuiTitle>
|
||||
<EuiTextColor color="subdued">
|
||||
<small>{option.meta[0].text}</small>
|
||||
</EuiTextColor>
|
||||
</>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const listProps = useMemo(
|
||||
() => ({
|
||||
rowHeight: 80,
|
||||
showIcons: true,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
panelPaddingSize="none"
|
||||
button={button}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
>
|
||||
<ContainerEuiSelectable>
|
||||
<EuiSelectable
|
||||
aria-label="Basic example"
|
||||
options={options}
|
||||
onChange={onChangeSelectable}
|
||||
renderOption={renderOption}
|
||||
searchable={false}
|
||||
height={160}
|
||||
listProps={listProps}
|
||||
singleSelection={true}
|
||||
>
|
||||
{(list) => list}
|
||||
</EuiSelectable>
|
||||
</ContainerEuiSelectable>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
||||
|
||||
export const SummaryViewSelector = React.memo(SummaryViewSelectorComponent);
|
|
@ -1,35 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`HeaderSection it renders 1`] = `
|
||||
<Header
|
||||
data-test-subj="header-section"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={true}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle
|
||||
size="m"
|
||||
>
|
||||
<h2
|
||||
data-test-subj="header-section-title"
|
||||
>
|
||||
Test title
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<Subtitle
|
||||
data-test-subj="header-section-subtitle"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Header>
|
||||
`;
|
|
@ -1,159 +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 euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { TestProviders } from '../../../mock';
|
||||
|
||||
import { HeaderSection } from './index';
|
||||
|
||||
describe('HeaderSection', () => {
|
||||
test('it renders', () => {
|
||||
const wrapper = shallow(<HeaderSection title="Test title" inspect={null} loading={false} />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('it renders the title', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderSection title="Test title" inspect={null} loading={false} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="header-section-title"]').first().exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('it renders the subtitle when provided', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderSection subtitle="Test subtitle" title="Test title" inspect={null} loading={false} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="header-section-subtitle"]').first().exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('renders the subtitle when not provided (to prevent layout thrash)', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderSection title="Test title" inspect={null} loading={false} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="header-section-subtitle"]').first().exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('it renders supplements when children provided', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderSection title="Test title" inspect={null} loading={false}>
|
||||
<p>{'Test children'}</p>
|
||||
</HeaderSection>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="header-section-supplements"]').first().exists()).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test('it DOES NOT render supplements when children not provided', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderSection title="Test title" inspect={null} loading={false} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="header-section-supplements"]').first().exists()).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test('it applies border styles when border is true', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderSection border title="Test title" inspect={null} loading={false} />
|
||||
</TestProviders>
|
||||
);
|
||||
const siemHeaderSection = wrapper.find('.siemHeaderSection').first();
|
||||
|
||||
expect(siemHeaderSection).toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin);
|
||||
expect(siemHeaderSection).toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.l);
|
||||
});
|
||||
|
||||
test('it DOES NOT apply border styles when border is false', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderSection title="Test title" inspect={null} loading={false} />
|
||||
</TestProviders>
|
||||
);
|
||||
const siemHeaderSection = wrapper.find('.siemHeaderSection').first();
|
||||
|
||||
expect(siemHeaderSection).not.toHaveStyleRule('border-bottom', euiDarkVars.euiBorderThin);
|
||||
expect(siemHeaderSection).not.toHaveStyleRule('padding-bottom', euiDarkVars.paddingSizes.l);
|
||||
});
|
||||
|
||||
test('it splits the title and supplement areas evenly when split is true', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderSection split title="Test title" inspect={null} loading={false}>
|
||||
<p>{'Test children'}</p>
|
||||
</HeaderSection>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('.euiFlexItem--flexGrowZero[data-test-subj="header-section-supplements"]')
|
||||
.first()
|
||||
.exists()
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('it DOES NOT split the title and supplement areas evenly when split is false', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderSection title="Test title" inspect={null} loading={false}>
|
||||
<p>{'Test children'}</p>
|
||||
</HeaderSection>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('.euiFlexItem--flexGrowZero[data-test-subj="header-section-supplements"]')
|
||||
.first()
|
||||
.exists()
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('it renders an inspect button when an `id` is provided', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderSection id="an id" title="Test title" inspect={null} loading={false}>
|
||||
<p>{'Test children'}</p>
|
||||
</HeaderSection>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="inspect-icon-button"]').first().exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('it does NOT an inspect button when an `id` is NOT provided', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<HeaderSection title="Test title" inspect={null} loading={false}>
|
||||
<p>{'Test children'}</p>
|
||||
</HeaderSection>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="inspect-icon-button"]').first().exists()).toBe(false);
|
||||
});
|
||||
});
|
|
@ -1,106 +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, EuiIconTip, EuiTitle, EuiTitleSize } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { InspectQuery } from '../../../store/t_grid/inputs';
|
||||
import { InspectButton } from '../../inspect';
|
||||
|
||||
import { Subtitle } from '../subtitle';
|
||||
|
||||
interface HeaderProps {
|
||||
border?: boolean;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
const Header = styled.header.attrs(() => ({
|
||||
className: 'siemHeaderSection',
|
||||
}))<HeaderProps>`
|
||||
${({ height }) =>
|
||||
height &&
|
||||
css`
|
||||
height: ${height}px;
|
||||
`}
|
||||
margin-bottom: ${({ height, theme }) => (height ? 0 : theme.eui.euiSizeL)};
|
||||
user-select: text;
|
||||
|
||||
${({ border }) =>
|
||||
border &&
|
||||
css`
|
||||
border-bottom: ${({ theme }) => theme.eui.euiBorderThin};
|
||||
padding-bottom: ${({ theme }) => theme.eui.paddingSizes.l};
|
||||
`}
|
||||
`;
|
||||
Header.displayName = 'Header';
|
||||
|
||||
export interface HeaderSectionProps extends HeaderProps {
|
||||
children?: React.ReactNode;
|
||||
height?: number;
|
||||
id?: string;
|
||||
inspect: InspectQuery | null;
|
||||
loading: boolean;
|
||||
split?: boolean;
|
||||
subtitle?: string | React.ReactNode;
|
||||
title: string | React.ReactNode;
|
||||
titleSize?: EuiTitleSize;
|
||||
tooltip?: string;
|
||||
growLeftSplit?: boolean;
|
||||
}
|
||||
|
||||
const HeaderSectionComponent: React.FC<HeaderSectionProps> = ({
|
||||
border,
|
||||
children,
|
||||
height,
|
||||
id,
|
||||
inspect,
|
||||
loading,
|
||||
split,
|
||||
subtitle,
|
||||
title,
|
||||
titleSize = 'm',
|
||||
tooltip,
|
||||
growLeftSplit = true,
|
||||
}) => (
|
||||
<Header data-test-subj="header-section" border={border} height={height}>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={growLeftSplit}>
|
||||
<EuiFlexGroup alignItems="center" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size={titleSize}>
|
||||
<h2 data-test-subj="header-section-title">
|
||||
{title}
|
||||
{tooltip && (
|
||||
<>
|
||||
{' '}
|
||||
<EuiIconTip color="subdued" content={tooltip} size="l" type="iInCircle" />
|
||||
</>
|
||||
)}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
|
||||
<Subtitle data-test-subj="header-section-subtitle" items={subtitle} />
|
||||
</EuiFlexItem>
|
||||
|
||||
{id && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<InspectButton title={title} inspect={inspect} loading={loading} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
||||
{children && (
|
||||
<EuiFlexItem data-test-subj="header-section-supplements" grow={split ? true : false}>
|
||||
{children}
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</Header>
|
||||
);
|
||||
|
||||
export const HeaderSection = React.memo(HeaderSectionComponent);
|
|
@ -47,19 +47,17 @@ import {
|
|||
} from '../helpers';
|
||||
import { tGridActions, tGridSelectors } from '../../../store/t_grid';
|
||||
import { useTimelineEvents } from '../../../container';
|
||||
import { HeaderSection } from '../header_section';
|
||||
import { StatefulBody } from '../body';
|
||||
import { Footer, footerHeight } from '../footer';
|
||||
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexItem } from '../styles';
|
||||
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexGroup, UpdatedFlexItem } from '../styles';
|
||||
import * as i18n from '../translations';
|
||||
import { ExitFullScreen } from '../../exit_full_screen';
|
||||
import { Sort } from '../body/sort';
|
||||
import { InspectButtonContainer } from '../../inspect';
|
||||
import { InspectButton, InspectButtonContainer } from '../../inspect';
|
||||
import { SummaryViewSelector, ViewSelection } from '../event_rendered_view/selector';
|
||||
|
||||
const AlertConsumers: typeof AlertConsumersTyped = AlertConsumersNonTyped;
|
||||
|
||||
export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px
|
||||
const COMPACT_HEADER_HEIGHT = 36; // px
|
||||
|
||||
const TitleText = styled.span`
|
||||
margin-right: 12px;
|
||||
|
@ -80,13 +78,10 @@ const StyledEuiPanel = styled(EuiPanel)<{ $isFullScreen: boolean }>`
|
|||
`}
|
||||
`;
|
||||
|
||||
const TitleFlexGroup = styled(EuiFlexGroup)`
|
||||
margin-top: 8px;
|
||||
`;
|
||||
|
||||
const EventsContainerLoading = styled.div.attrs(({ className = '' }) => ({
|
||||
className: `${SELECTOR_TIMELINE_GLOBAL_CONTAINER} ${className}`,
|
||||
}))`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
|
@ -104,14 +99,6 @@ 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;')}
|
||||
`;
|
||||
|
||||
const SECURITY_ALERTS_CONSUMERS = [AlertConsumers.SIEM];
|
||||
|
||||
export interface TGridIntegratedProps {
|
||||
|
@ -126,7 +113,6 @@ export interface TGridIntegratedProps {
|
|||
filters: Filter[];
|
||||
globalFullScreen: boolean;
|
||||
graphOverlay?: React.ReactNode;
|
||||
headerFilterGroup?: React.ReactNode;
|
||||
filterStatus?: AlertStatus;
|
||||
height?: number;
|
||||
id: TimelineId;
|
||||
|
@ -150,6 +136,7 @@ export interface TGridIntegratedProps {
|
|||
leadingControlColumns?: ControlColumnProps[];
|
||||
trailingControlColumns?: ControlColumnProps[];
|
||||
data?: DataPublicPluginStart;
|
||||
tGridEventRenderedViewEnabled: boolean;
|
||||
}
|
||||
|
||||
const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
||||
|
@ -163,7 +150,6 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
|||
entityType,
|
||||
filters,
|
||||
globalFullScreen,
|
||||
headerFilterGroup,
|
||||
filterStatus,
|
||||
id,
|
||||
indexNames,
|
||||
|
@ -185,6 +171,7 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
|||
graphEventId,
|
||||
leadingControlColumns,
|
||||
trailingControlColumns,
|
||||
tGridEventRenderedViewEnabled,
|
||||
data,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
@ -192,6 +179,7 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
|||
const { uiSettings } = useKibana<CoreStart>().services;
|
||||
const [isQueryLoading, setIsQueryLoading] = useState(false);
|
||||
|
||||
const [tableView, setTableView] = useState<ViewSelection>('gridView');
|
||||
const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []);
|
||||
const unit = useMemo(() => (n: number) => i18n.ALERTS_UNIT(n), []);
|
||||
const { queryFields, title } = useDeepEqualSelector((state) =>
|
||||
|
@ -203,17 +191,6 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
|||
}, [dispatch, id, isQueryLoading]);
|
||||
|
||||
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 = buildCombinedQuery({
|
||||
config: esQuery.getEsQueryConfig(uiSettings),
|
||||
|
@ -295,23 +272,12 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
|||
events,
|
||||
]);
|
||||
|
||||
const HeaderSectionContent = useMemo(
|
||||
() =>
|
||||
headerFilterGroup && (
|
||||
<HeaderFilterGroupWrapper
|
||||
data-test-subj="header-filter-group-wrapper"
|
||||
show={!resolverIsShowing(graphEventId)}
|
||||
>
|
||||
{headerFilterGroup}
|
||||
</HeaderFilterGroupWrapper>
|
||||
),
|
||||
[headerFilterGroup, graphEventId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setIsQueryLoading(loading);
|
||||
}, [loading]);
|
||||
|
||||
const alignItems = tableView === 'gridView' ? 'baseline' : 'center';
|
||||
|
||||
return (
|
||||
<InspectButtonContainer>
|
||||
<StyledEuiPanel
|
||||
|
@ -325,27 +291,23 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
|||
|
||||
{canQueryTimeline ? (
|
||||
<>
|
||||
<HeaderSection
|
||||
id={!resolverIsShowing(graphEventId) ? id : undefined}
|
||||
inspect={inspect}
|
||||
loading={loading}
|
||||
height={
|
||||
headerFilterGroup == null ? COMPACT_HEADER_HEIGHT : EVENTS_VIEWER_HEADER_HEIGHT
|
||||
}
|
||||
title={globalFullScreen ? titleWithExitFullScreen : justTitle}
|
||||
>
|
||||
{HeaderSectionContent}
|
||||
</HeaderSection>
|
||||
<EventsContainerLoading
|
||||
data-timeline-id={id}
|
||||
data-test-subj={`events-container-loading-${loading}`}
|
||||
>
|
||||
{graphOverlay}
|
||||
<EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
|
||||
<UpdatedFlexGroup gutterSize="m" justifyContent="flexEnd" alignItems={alignItems}>
|
||||
<UpdatedFlexItem grow={false} show={!loading}>
|
||||
<InspectButton title={justTitle} inspect={inspect} loading={loading} />
|
||||
</UpdatedFlexItem>
|
||||
<UpdatedFlexItem grow={false} show={!loading}>
|
||||
{!resolverIsShowing(graphEventId) && additionalFilters}
|
||||
</UpdatedFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{tGridEventRenderedViewEnabled && (
|
||||
<UpdatedFlexItem grow={false} show={!loading}>
|
||||
<SummaryViewSelector viewSelected={tableView} onViewChange={setTableView} />
|
||||
</UpdatedFlexItem>
|
||||
)}
|
||||
</UpdatedFlexGroup>
|
||||
|
||||
<FullWidthFlexGroup
|
||||
$visible={!graphEventId && graphOverlay == null}
|
||||
|
@ -382,11 +344,14 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
|||
defaultCellActions={defaultCellActions}
|
||||
id={id}
|
||||
isEventViewer={true}
|
||||
itemsPerPageOptions={itemsPerPageOptions}
|
||||
loadPage={loadPage}
|
||||
onRuleChange={onRuleChange}
|
||||
querySize={pageInfo.querySize}
|
||||
renderCellValue={renderCellValue}
|
||||
rowRenderers={rowRenderers}
|
||||
tabType={TimelineTabs.query}
|
||||
tableView={tableView}
|
||||
totalPages={calculateTotalPages({
|
||||
itemsCount: totalCountMinusDeleted,
|
||||
itemsPerPage,
|
||||
|
@ -399,19 +364,21 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
|||
refetch={refetch}
|
||||
indexNames={indexNames}
|
||||
/>
|
||||
<Footer
|
||||
activePage={pageInfo.activePage}
|
||||
data-test-subj="events-viewer-footer"
|
||||
height={footerHeight}
|
||||
id={id}
|
||||
isLive={isLive}
|
||||
isLoading={loading}
|
||||
itemsCount={nonDeletedEvents.length}
|
||||
itemsPerPage={itemsPerPage}
|
||||
itemsPerPageOptions={itemsPerPageOptions}
|
||||
onChangePage={loadPage}
|
||||
totalCount={totalCountMinusDeleted}
|
||||
/>
|
||||
{tableView === 'gridView' && (
|
||||
<Footer
|
||||
activePage={pageInfo.activePage}
|
||||
data-test-subj="events-viewer-footer"
|
||||
height={footerHeight}
|
||||
id={id}
|
||||
isLive={isLive}
|
||||
isLoading={loading}
|
||||
itemsCount={nonDeletedEvents.length}
|
||||
itemsPerPage={itemsPerPage}
|
||||
itemsPerPageOptions={itemsPerPageOptions}
|
||||
onChangePage={loadPage}
|
||||
totalCount={totalCountMinusDeleted}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ScrollableFlexItem>
|
||||
|
|
|
@ -32,27 +32,20 @@ import {
|
|||
} from '../../../../../../../src/plugins/data/public';
|
||||
import { useDeepEqualSelector } from '../../../hooks/use_selector';
|
||||
import { defaultHeaders } from '../body/column_headers/default_headers';
|
||||
import {
|
||||
calculateTotalPages,
|
||||
combineQueries,
|
||||
getCombinedFilterQuery,
|
||||
resolverIsShowing,
|
||||
} from '../helpers';
|
||||
import { calculateTotalPages, combineQueries, getCombinedFilterQuery } from '../helpers';
|
||||
import { tGridActions, tGridSelectors } from '../../../store/t_grid';
|
||||
import type { State } from '../../../store/t_grid';
|
||||
import { useTimelineEvents } from '../../../container';
|
||||
import { HeaderSection } from '../header_section';
|
||||
import { StatefulBody } from '../body';
|
||||
import { Footer, footerHeight } from '../footer';
|
||||
import { LastUpdatedAt } from '../..';
|
||||
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexItem } from '../styles';
|
||||
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexItem, UpdatedFlexGroup } from '../styles';
|
||||
import * as i18n from '../translations';
|
||||
import { InspectButtonContainer } from '../../inspect';
|
||||
import { InspectButton, InspectButtonContainer } from '../../inspect';
|
||||
import { useFetchIndex } from '../../../container/source';
|
||||
import { AddToCaseAction } from '../../actions/timeline/cases/add_to_case_action';
|
||||
|
||||
export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px
|
||||
const COMPACT_HEADER_HEIGHT = 36; // px
|
||||
const STANDALONE_ID = 'standalone-t-grid';
|
||||
const EMPTY_DATA_PROVIDERS: DataProvider[] = [];
|
||||
|
||||
|
@ -68,6 +61,7 @@ const AlertsTableWrapper = styled.div`
|
|||
const EventsContainerLoading = styled.div.attrs(({ className = '' }) => ({
|
||||
className: `${SELECTOR_TIMELINE_GLOBAL_CONTAINER} ${className}`,
|
||||
}))`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
|
@ -86,14 +80,6 @@ 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;')}
|
||||
`;
|
||||
|
||||
export interface TGridStandaloneProps {
|
||||
alertConsumers: AlertConsumers[];
|
||||
appId: string;
|
||||
|
@ -110,7 +96,6 @@ export interface TGridStandaloneProps {
|
|||
loadingText: React.ReactNode;
|
||||
filters: Filter[];
|
||||
footerText: React.ReactNode;
|
||||
headerFilterGroup?: React.ReactNode;
|
||||
filterStatus: AlertStatus;
|
||||
height?: number;
|
||||
indexNames: string[];
|
||||
|
@ -145,7 +130,6 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
|||
loadingText,
|
||||
filters,
|
||||
footerText,
|
||||
headerFilterGroup,
|
||||
filterStatus,
|
||||
indexNames,
|
||||
itemsPerPage,
|
||||
|
@ -273,19 +257,6 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
|||
events,
|
||||
]);
|
||||
|
||||
const HeaderSectionContent = useMemo(
|
||||
() =>
|
||||
headerFilterGroup && (
|
||||
<HeaderFilterGroupWrapper
|
||||
data-test-subj="header-filter-group-wrapper"
|
||||
show={!resolverIsShowing(graphEventId)}
|
||||
>
|
||||
{headerFilterGroup}
|
||||
</HeaderFilterGroupWrapper>
|
||||
),
|
||||
[headerFilterGroup, graphEventId]
|
||||
);
|
||||
|
||||
const filterQuery = useMemo(
|
||||
() =>
|
||||
getCombinedFilterQuery({
|
||||
|
@ -339,27 +310,18 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
|||
<AlertsTableWrapper>
|
||||
{canQueryTimeline ? (
|
||||
<>
|
||||
<HeaderSection
|
||||
id={!resolverIsShowing(graphEventId) ? STANDALONE_ID : undefined}
|
||||
inspect={inspect}
|
||||
loading={loading}
|
||||
height={
|
||||
headerFilterGroup == null ? COMPACT_HEADER_HEIGHT : EVENTS_VIEWER_HEADER_HEIGHT
|
||||
}
|
||||
title={justTitle}
|
||||
>
|
||||
{HeaderSectionContent}
|
||||
</HeaderSection>
|
||||
|
||||
<EventsContainerLoading
|
||||
data-timeline-id={STANDALONE_ID}
|
||||
data-test-subj={`events-container-loading-${loading}`}
|
||||
>
|
||||
<EuiFlexGroup gutterSize="none" justifyContent="flexEnd">
|
||||
<UpdatedFlexGroup gutterSize="s" justifyContent="flexEnd" alignItems="baseline">
|
||||
<UpdatedFlexItem grow={false} show={!loading}>
|
||||
<InspectButton title={justTitle} inspect={inspect} loading={loading} />
|
||||
</UpdatedFlexItem>
|
||||
<UpdatedFlexItem grow={false} show={!loading}>
|
||||
<LastUpdatedAt updatedAt={updatedAt} />
|
||||
</UpdatedFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</UpdatedFlexGroup>
|
||||
|
||||
<FullWidthFlexGroup direction="row" $visible={!graphEventId} gutterSize="none">
|
||||
<ScrollableFlexItem grow={1}>
|
||||
|
@ -368,25 +330,28 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
|||
browserFields={browserFields}
|
||||
data={nonDeletedEvents}
|
||||
defaultCellActions={defaultCellActions}
|
||||
filterQuery={filterQuery}
|
||||
id={STANDALONE_ID}
|
||||
indexNames={indexNames}
|
||||
isEventViewer={true}
|
||||
itemsPerPageOptions={itemsPerPageOptionsStore}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
loadPage={loadPage}
|
||||
onRuleChange={onRuleChange}
|
||||
refetch={refetch}
|
||||
renderCellValue={renderCellValue}
|
||||
rowRenderers={rowRenderers}
|
||||
onRuleChange={onRuleChange}
|
||||
querySize={pageInfo.querySize}
|
||||
tabType={TimelineTabs.query}
|
||||
tableView="gridView"
|
||||
totalPages={calculateTotalPages({
|
||||
itemsCount: totalCountMinusDeleted,
|
||||
itemsPerPage: itemsPerPageStore,
|
||||
})}
|
||||
totalItems={totalCountMinusDeleted}
|
||||
indexNames={indexNames}
|
||||
filterQuery={filterQuery}
|
||||
unit={unit}
|
||||
filterStatus={filterStatus}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
trailingControlColumns={trailingControlColumns}
|
||||
refetch={refetch}
|
||||
/>
|
||||
<Footer
|
||||
activePage={pageInfo.activePage}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid';
|
||||
import { rgba } from 'polished';
|
||||
import styled, { createGlobalStyle } from 'styled-components';
|
||||
|
@ -459,12 +459,14 @@ export const HideShowContainer = styled.div.attrs<{ $isVisible: boolean }>(
|
|||
})
|
||||
)<{ $isVisible: boolean }>``;
|
||||
|
||||
export const UpdatedFlexItem = styled(EuiFlexItem)<{ show: boolean }>`
|
||||
height: 0px;
|
||||
position: relative;
|
||||
top: ${({ theme }) => theme.eui.paddingSizes.s};
|
||||
${({ show }) => (show ? '' : 'visibility: hidden;')}
|
||||
export const UpdatedFlexGroup = styled(EuiFlexGroup)`
|
||||
position: absolute;
|
||||
z-index: ${({ theme }) => theme.eui.euiZLevel1};
|
||||
right: 0px;
|
||||
`;
|
||||
|
||||
export const UpdatedFlexItem = styled(EuiFlexItem)<{ show: boolean }>`
|
||||
${({ show }) => (show ? '' : 'visibility: hidden;')}
|
||||
`;
|
||||
|
||||
export const AlertCount = styled.span`
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue