[RAC] [TGrid] Bulk actions to EuiDataGrid toolbar (#107141)
* tGrid EuiDataGrid toolbar replace utilityBar * tgrid new prop in observability * types and translations fixes * bulkActions props and encapsulation * update limits * code cleaning * load lazy and remove export from public * add memoization to bulk_actions * icon change and test fixed Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
1e1d669650
commit
b5e8db2443
25 changed files with 799 additions and 159 deletions
|
@ -107,7 +107,7 @@ pageLoadAssetSize:
|
||||||
dataVisualizer: 27530
|
dataVisualizer: 27530
|
||||||
banners: 17946
|
banners: 17946
|
||||||
mapsEms: 26072
|
mapsEms: 26072
|
||||||
timelines: 330000
|
timelines: 327300
|
||||||
screenshotMode: 17856
|
screenshotMode: 17856
|
||||||
visTypePie: 35583
|
visTypePie: 35583
|
||||||
expressionRevealImage: 25675
|
expressionRevealImage: 25675
|
||||||
|
|
|
@ -21,7 +21,12 @@ import {
|
||||||
import type { TimelinesUIStart } from '../../../../timelines/public';
|
import type { TimelinesUIStart } from '../../../../timelines/public';
|
||||||
import type { TopAlert } from './';
|
import type { TopAlert } from './';
|
||||||
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
|
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
|
||||||
import type { ActionProps, ColumnHeaderOptions, RowRenderer } from '../../../../timelines/common';
|
import type {
|
||||||
|
ActionProps,
|
||||||
|
AlertStatus,
|
||||||
|
ColumnHeaderOptions,
|
||||||
|
RowRenderer,
|
||||||
|
} from '../../../../timelines/common';
|
||||||
|
|
||||||
import { getRenderCellValue } from './render_cell_value';
|
import { getRenderCellValue } from './render_cell_value';
|
||||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||||
|
@ -213,6 +218,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
|
||||||
sortDirection: 'desc',
|
sortDirection: 'desc',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
filterStatus: status as AlertStatus,
|
||||||
leadingControlColumns,
|
leadingControlColumns,
|
||||||
trailingControlColumns,
|
trailingControlColumns,
|
||||||
unit: (totalAlerts: number) =>
|
unit: (totalAlerts: number) =>
|
||||||
|
|
|
@ -15,7 +15,8 @@ import { inputsModel, inputsSelectors, State } from '../../store';
|
||||||
import { inputsActions } from '../../store/actions';
|
import { inputsActions } from '../../store/actions';
|
||||||
import { ControlColumnProps, RowRenderer, TimelineId } from '../../../../common/types/timeline';
|
import { ControlColumnProps, RowRenderer, TimelineId } from '../../../../common/types/timeline';
|
||||||
import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline';
|
import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline';
|
||||||
import { SubsetTimelineModel, TimelineModel } from '../../../timelines/store/timeline/model';
|
import type { SubsetTimelineModel, TimelineModel } from '../../../timelines/store/timeline/model';
|
||||||
|
import { Status } from '../../../../common/detection_engine/schemas/common/schemas';
|
||||||
import { Filter } from '../../../../../../../src/plugins/data/public';
|
import { Filter } from '../../../../../../../src/plugins/data/public';
|
||||||
import { InspectButtonContainer } from '../inspect';
|
import { InspectButtonContainer } from '../inspect';
|
||||||
import { useGlobalFullScreen } from '../../containers/use_full_screen';
|
import { useGlobalFullScreen } from '../../containers/use_full_screen';
|
||||||
|
@ -54,6 +55,7 @@ export interface OwnProps {
|
||||||
showTotalCount?: boolean;
|
showTotalCount?: boolean;
|
||||||
headerFilterGroup?: React.ReactNode;
|
headerFilterGroup?: React.ReactNode;
|
||||||
pageFilters?: Filter[];
|
pageFilters?: Filter[];
|
||||||
|
currentFilter?: Status;
|
||||||
onRuleChange?: () => void;
|
onRuleChange?: () => void;
|
||||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||||
rowRenderers: RowRenderer[];
|
rowRenderers: RowRenderer[];
|
||||||
|
@ -83,6 +85,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
||||||
itemsPerPageOptions,
|
itemsPerPageOptions,
|
||||||
kqlMode,
|
kqlMode,
|
||||||
pageFilters,
|
pageFilters,
|
||||||
|
currentFilter,
|
||||||
onRuleChange,
|
onRuleChange,
|
||||||
query,
|
query,
|
||||||
renderCellValue,
|
renderCellValue,
|
||||||
|
@ -160,6 +163,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
||||||
sort,
|
sort,
|
||||||
utilityBar,
|
utilityBar,
|
||||||
graphEventId,
|
graphEventId,
|
||||||
|
filterStatus: currentFilter,
|
||||||
leadingControlColumns,
|
leadingControlColumns,
|
||||||
trailingControlColumns,
|
trailingControlColumns,
|
||||||
})
|
})
|
||||||
|
|
|
@ -390,6 +390,7 @@ export const AlertsTableComponent: React.FC<AlertsTableComponentProps> = ({
|
||||||
pageFilters={defaultFiltersMemo}
|
pageFilters={defaultFiltersMemo}
|
||||||
defaultModel={defaultTimelineModel}
|
defaultModel={defaultTimelineModel}
|
||||||
end={to}
|
end={to}
|
||||||
|
currentFilter={filterGroup}
|
||||||
headerFilterGroup={headerFilterGroup}
|
headerFilterGroup={headerFilterGroup}
|
||||||
id={timelineId}
|
id={timelineId}
|
||||||
onRuleChange={onRuleChange}
|
onRuleChange={onRuleChange}
|
||||||
|
|
|
@ -5,4 +5,11 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { AlertStatus } from './types/timeline/actions';
|
||||||
|
|
||||||
export const DEFAULT_MAX_TABLE_QUERY_SIZE = 10000;
|
export const DEFAULT_MAX_TABLE_QUERY_SIZE = 10000;
|
||||||
|
export const DEFAULT_NUMBER_FORMAT = 'format:number:defaultPattern';
|
||||||
|
|
||||||
|
export const FILTER_OPEN: AlertStatus = 'open';
|
||||||
|
export const FILTER_CLOSED: AlertStatus = 'closed';
|
||||||
|
export const FILTER_IN_PROGRESS: AlertStatus = 'in-progress';
|
||||||
|
|
|
@ -90,3 +90,14 @@ export type ControlColumnProps = Omit<
|
||||||
keyof AdditionalControlColumnProps
|
keyof AdditionalControlColumnProps
|
||||||
> &
|
> &
|
||||||
Partial<AdditionalControlColumnProps>;
|
Partial<AdditionalControlColumnProps>;
|
||||||
|
|
||||||
|
export type OnAlertStatusActionSuccess = (status: AlertStatus) => void;
|
||||||
|
export type OnAlertStatusActionFailure = (status: AlertStatus, error: string) => void;
|
||||||
|
export interface BulkActionsObjectProp {
|
||||||
|
alertStatusActions?: boolean;
|
||||||
|
onAlertStatusActionSuccess?: OnAlertStatusActionSuccess;
|
||||||
|
onAlertStatusActionFailure?: OnAlertStatusActionFailure;
|
||||||
|
}
|
||||||
|
export type BulkActionsProp = boolean | BulkActionsObjectProp;
|
||||||
|
|
||||||
|
export type AlertStatus = 'open' | 'closed' | 'in-progress';
|
||||||
|
|
|
@ -75,8 +75,11 @@ describe('Body', () => {
|
||||||
showCheckboxes: false,
|
showCheckboxes: false,
|
||||||
tabType: TimelineTabs.query,
|
tabType: TimelineTabs.query,
|
||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
|
totalItems: 1,
|
||||||
leadingControlColumns: [],
|
leadingControlColumns: [],
|
||||||
trailingControlColumns: [],
|
trailingControlColumns: [],
|
||||||
|
filterStatus: 'open',
|
||||||
|
refetch: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('rendering', () => {
|
describe('rendering', () => {
|
||||||
|
|
|
@ -11,19 +11,34 @@ import {
|
||||||
EuiDataGridControlColumn,
|
EuiDataGridControlColumn,
|
||||||
EuiDataGridStyle,
|
EuiDataGridStyle,
|
||||||
EuiDataGridToolBarVisibilityOptions,
|
EuiDataGridToolBarVisibilityOptions,
|
||||||
|
EuiLoadingSpinner,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { getOr } from 'lodash/fp';
|
import { getOr } from 'lodash/fp';
|
||||||
import memoizeOne from 'memoize-one';
|
import memoizeOne from 'memoize-one';
|
||||||
import React, { ComponentType, useCallback, useEffect, useMemo, useState } from 'react';
|
import React, {
|
||||||
|
ComponentType,
|
||||||
|
lazy,
|
||||||
|
Suspense,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { connect, ConnectedProps } from 'react-redux';
|
import { connect, ConnectedProps } from 'react-redux';
|
||||||
|
|
||||||
import { SortColumnTimeline, TimelineId, TimelineTabs } from '../../../../common/types/timeline';
|
import {
|
||||||
|
TimelineId,
|
||||||
|
TimelineTabs,
|
||||||
|
BulkActionsProp,
|
||||||
|
SortColumnTimeline,
|
||||||
|
} from '../../../../common/types/timeline';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
CellValueElementProps,
|
CellValueElementProps,
|
||||||
ColumnHeaderOptions,
|
ColumnHeaderOptions,
|
||||||
ControlColumnProps,
|
ControlColumnProps,
|
||||||
RowRenderer,
|
RowRenderer,
|
||||||
|
AlertStatus,
|
||||||
} from '../../../../common/types/timeline';
|
} from '../../../../common/types/timeline';
|
||||||
import type { TimelineItem, TimelineNonEcsData } from '../../../../common/search_strategy/timeline';
|
import type { TimelineItem, TimelineNonEcsData } from '../../../../common/search_strategy/timeline';
|
||||||
|
|
||||||
|
@ -32,15 +47,21 @@ import { getEventIdToDataMapping } from './helpers';
|
||||||
import { Sort } from './sort';
|
import { Sort } from './sort';
|
||||||
|
|
||||||
import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers';
|
import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers';
|
||||||
import { BrowserFields } from '../../../../common/search_strategy/index_fields';
|
import type { BrowserFields } from '../../../../common/search_strategy/index_fields';
|
||||||
import { OnRowSelected, OnSelectAll } from '../types';
|
import type { OnRowSelected, OnSelectAll } from '../types';
|
||||||
import { StatefulFieldsBrowser, tGridActions } from '../../../';
|
import type { Refetch } from '../../../store/t_grid/inputs';
|
||||||
import { TGridModel, tGridSelectors, TimelineState } from '../../../store/t_grid';
|
import { StatefulFieldsBrowser } from '../../../';
|
||||||
|
import { tGridActions, TGridModel, tGridSelectors, TimelineState } from '../../../store/t_grid';
|
||||||
import { useDeepEqualSelector } from '../../../hooks/use_selector';
|
import { useDeepEqualSelector } from '../../../hooks/use_selector';
|
||||||
import { RowAction } from './row_action';
|
import { RowAction } from './row_action';
|
||||||
import * as i18n from './translations';
|
import * as i18n from './translations';
|
||||||
|
import { AlertCount } from '../styles';
|
||||||
import { checkBoxControlColumn } from './control_columns';
|
import { checkBoxControlColumn } from './control_columns';
|
||||||
|
|
||||||
|
const StatefulAlertStatusBulkActions = lazy(
|
||||||
|
() => import('../toolbar/bulk_actions/alert_status_bulk_actions')
|
||||||
|
);
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
activePage: number;
|
activePage: number;
|
||||||
additionalControls?: React.ReactNode;
|
additionalControls?: React.ReactNode;
|
||||||
|
@ -48,16 +69,22 @@ interface OwnProps {
|
||||||
data: TimelineItem[];
|
data: TimelineItem[];
|
||||||
id: string;
|
id: string;
|
||||||
isEventViewer?: boolean;
|
isEventViewer?: boolean;
|
||||||
leadingControlColumns: ControlColumnProps[];
|
|
||||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||||
rowRenderers: RowRenderer[];
|
rowRenderers: RowRenderer[];
|
||||||
sort: Sort[];
|
sort: Sort[];
|
||||||
tabType: TimelineTabs;
|
tabType: TimelineTabs;
|
||||||
trailingControlColumns: ControlColumnProps[];
|
leadingControlColumns?: ControlColumnProps[];
|
||||||
|
trailingControlColumns?: ControlColumnProps[];
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
|
totalItems: number;
|
||||||
|
bulkActions?: BulkActionsProp;
|
||||||
|
filterStatus?: AlertStatus;
|
||||||
|
unit?: (total: number) => React.ReactNode;
|
||||||
onRuleChange?: () => void;
|
onRuleChange?: () => void;
|
||||||
|
refetch: Refetch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const basicUnit = (n: number) => i18n.UNIT(n);
|
||||||
const NUM_OF_ICON_IN_TIMELINE_ROW = 2;
|
const NUM_OF_ICON_IN_TIMELINE_ROW = 2;
|
||||||
|
|
||||||
export const hasAdditionalActions = (id: TimelineId): boolean =>
|
export const hasAdditionalActions = (id: TimelineId): boolean =>
|
||||||
|
@ -200,31 +227,42 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
||||||
sort,
|
sort,
|
||||||
tabType,
|
tabType,
|
||||||
totalPages,
|
totalPages,
|
||||||
|
totalItems,
|
||||||
|
filterStatus,
|
||||||
|
bulkActions = true,
|
||||||
|
unit = basicUnit,
|
||||||
leadingControlColumns = EMPTY_CONTROL_COLUMNS,
|
leadingControlColumns = EMPTY_CONTROL_COLUMNS,
|
||||||
trailingControlColumns = EMPTY_CONTROL_COLUMNS,
|
trailingControlColumns = EMPTY_CONTROL_COLUMNS,
|
||||||
|
refetch,
|
||||||
}) => {
|
}) => {
|
||||||
const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []);
|
const getManageTimeline = useMemo(() => tGridSelectors.getManageTimelineById(), []);
|
||||||
const { queryFields, selectAll } = useDeepEqualSelector((state) =>
|
const { queryFields, selectAll } = useDeepEqualSelector((state) =>
|
||||||
getManageTimeline(state, id)
|
getManageTimeline(state, id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const alertCountText = useMemo(() => `${totalItems.toLocaleString()} ${unit(totalItems)}`, [
|
||||||
|
totalItems,
|
||||||
|
unit,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const selectedCount = useMemo(() => Object.keys(selectedEventIds).length, [selectedEventIds]);
|
||||||
|
|
||||||
const onRowSelected: OnRowSelected = useCallback(
|
const onRowSelected: OnRowSelected = useCallback(
|
||||||
({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => {
|
({ eventIds, isSelected }: { eventIds: string[]; isSelected: boolean }) => {
|
||||||
setSelected!({
|
setSelected({
|
||||||
id,
|
id,
|
||||||
eventIds: getEventIdToDataMapping(data, eventIds, queryFields),
|
eventIds: getEventIdToDataMapping(data, eventIds, queryFields),
|
||||||
isSelected,
|
isSelected,
|
||||||
isSelectAllChecked:
|
isSelectAllChecked: isSelected && selectedCount + 1 === data.length,
|
||||||
isSelected && Object.keys(selectedEventIds).length + 1 === data.length,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setSelected, id, data, selectedEventIds, queryFields]
|
[setSelected, id, data, selectedCount, queryFields]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSelectPage: OnSelectAll = useCallback(
|
const onSelectPage: OnSelectAll = useCallback(
|
||||||
({ isSelected }: { isSelected: boolean }) =>
|
({ isSelected }: { isSelected: boolean }) =>
|
||||||
isSelected
|
isSelected
|
||||||
? setSelected!({
|
? setSelected({
|
||||||
id,
|
id,
|
||||||
eventIds: getEventIdToDataMapping(
|
eventIds: getEventIdToDataMapping(
|
||||||
data,
|
data,
|
||||||
|
@ -234,7 +272,7 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
||||||
isSelected,
|
isSelected,
|
||||||
isSelectAllChecked: isSelected,
|
isSelectAllChecked: isSelected,
|
||||||
})
|
})
|
||||||
: clearSelected!({ id }),
|
: clearSelected({ id }),
|
||||||
[setSelected, clearSelected, id, data, queryFields]
|
[setSelected, clearSelected, id, data, queryFields]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -245,25 +283,87 @@ export const BodyComponent = React.memo<StatefulBodyProps>(
|
||||||
}
|
}
|
||||||
}, [isSelectAllChecked, onSelectPage, selectAll]);
|
}, [isSelectAllChecked, onSelectPage, selectAll]);
|
||||||
|
|
||||||
|
const onAlertStatusActionSuccess = useMemo(() => {
|
||||||
|
if (bulkActions && bulkActions !== true) {
|
||||||
|
return bulkActions.onAlertStatusActionSuccess;
|
||||||
|
}
|
||||||
|
}, [bulkActions]);
|
||||||
|
|
||||||
|
const onAlertStatusActionFailure = useMemo(() => {
|
||||||
|
if (bulkActions && bulkActions !== true) {
|
||||||
|
return bulkActions.onAlertStatusActionFailure;
|
||||||
|
}
|
||||||
|
}, [bulkActions]);
|
||||||
|
|
||||||
|
const showBulkActions = useMemo(() => {
|
||||||
|
if (selectedCount === 0 || !showCheckboxes) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (typeof bulkActions === 'boolean') {
|
||||||
|
return bulkActions;
|
||||||
|
}
|
||||||
|
return bulkActions.alertStatusActions ?? true;
|
||||||
|
}, [selectedCount, showCheckboxes, bulkActions]);
|
||||||
|
|
||||||
const toolbarVisibility: EuiDataGridToolBarVisibilityOptions = useMemo(
|
const toolbarVisibility: EuiDataGridToolBarVisibilityOptions = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
additionalControls: (
|
additionalControls: (
|
||||||
<>
|
<>
|
||||||
{additionalControls ?? null}
|
<AlertCount>{alertCountText}</AlertCount>
|
||||||
{
|
{showBulkActions ? (
|
||||||
<StatefulFieldsBrowser
|
<>
|
||||||
data-test-subj="field-browser"
|
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||||
browserFields={browserFields}
|
<StatefulAlertStatusBulkActions
|
||||||
timelineId={id}
|
data-test-subj="bulk-actions"
|
||||||
columnHeaders={columnHeaders}
|
id={id}
|
||||||
/>
|
totalItems={totalItems}
|
||||||
}
|
filterStatus={filterStatus}
|
||||||
|
onActionSuccess={onAlertStatusActionSuccess}
|
||||||
|
onActionFailure={onAlertStatusActionFailure}
|
||||||
|
refetch={refetch}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
{additionalControls ?? null}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{additionalControls ?? null}
|
||||||
|
<StatefulFieldsBrowser
|
||||||
|
data-test-subj="field-browser"
|
||||||
|
browserFields={browserFields}
|
||||||
|
timelineId={id}
|
||||||
|
columnHeaders={columnHeaders}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
showColumnSelector: { allowHide: false, allowReorder: true },
|
...(showBulkActions
|
||||||
|
? {
|
||||||
|
showColumnSelector: false,
|
||||||
|
showSortSelector: false,
|
||||||
|
showFullScreenSelector: false,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
showColumnSelector: { allowHide: true, allowReorder: true },
|
||||||
|
showSortSelector: true,
|
||||||
|
showFullScreenSelector: true,
|
||||||
|
}),
|
||||||
showStyleSelector: false,
|
showStyleSelector: false,
|
||||||
}),
|
}),
|
||||||
[additionalControls, browserFields, columnHeaders, id]
|
[
|
||||||
|
id,
|
||||||
|
alertCountText,
|
||||||
|
totalItems,
|
||||||
|
filterStatus,
|
||||||
|
browserFields,
|
||||||
|
columnHeaders,
|
||||||
|
additionalControls,
|
||||||
|
showBulkActions,
|
||||||
|
onAlertStatusActionSuccess,
|
||||||
|
onAlertStatusActionFailure,
|
||||||
|
refetch,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [sortingColumns, setSortingColumns] = useState([]);
|
const [sortingColumns, setSortingColumns] = useState([]);
|
||||||
|
|
|
@ -216,3 +216,9 @@ export const INVESTIGATE_IN_RESOLVER_DISABLED = i18n.translate(
|
||||||
defaultMessage: 'This event cannot be analyzed since it has incompatible field mappings',
|
defaultMessage: 'This event cannot be analyzed since it has incompatible field mappings',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const UNIT = (totalCount: number) =>
|
||||||
|
i18n.translate('xpack.timelines.timeline.body.unit', {
|
||||||
|
values: { totalCount },
|
||||||
|
defaultMessage: `{totalCount, plural, =1 {event} other {events}}`,
|
||||||
|
});
|
||||||
|
|
|
@ -22,6 +22,7 @@ import type {
|
||||||
ControlColumnProps,
|
ControlColumnProps,
|
||||||
DataProvider,
|
DataProvider,
|
||||||
RowRenderer,
|
RowRenderer,
|
||||||
|
AlertStatus,
|
||||||
} from '../../../../common/types/timeline';
|
} from '../../../../common/types/timeline';
|
||||||
import {
|
import {
|
||||||
esQuery,
|
esQuery,
|
||||||
|
@ -40,20 +41,15 @@ import { HeaderSection } from '../header_section';
|
||||||
import { StatefulBody } from '../body';
|
import { StatefulBody } from '../body';
|
||||||
import { Footer, footerHeight } from '../footer';
|
import { Footer, footerHeight } from '../footer';
|
||||||
import { LastUpdatedAt } from '../..';
|
import { LastUpdatedAt } from '../..';
|
||||||
import { AlertCount, SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexItem } from '../styles';
|
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexItem } from '../styles';
|
||||||
import * as i18n from './translations';
|
import * as i18n from '../translations';
|
||||||
import { ExitFullScreen } from '../../exit_full_screen';
|
import { ExitFullScreen } from '../../exit_full_screen';
|
||||||
import { Sort } from '../body/sort';
|
import { Sort } from '../body/sort';
|
||||||
import { InspectButtonContainer } from '../../inspect';
|
import { InspectButtonContainer } from '../../inspect';
|
||||||
|
|
||||||
export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px
|
export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px
|
||||||
const UTILITY_BAR_HEIGHT = 19; // px
|
|
||||||
const COMPACT_HEADER_HEIGHT = 36; // px
|
const COMPACT_HEADER_HEIGHT = 36; // px
|
||||||
|
|
||||||
const UtilityBar = styled.div`
|
|
||||||
height: ${UTILITY_BAR_HEIGHT}px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TitleText = styled.span`
|
const TitleText = styled.span`
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
`;
|
`;
|
||||||
|
@ -114,6 +110,7 @@ export interface TGridIntegratedProps {
|
||||||
filters: Filter[];
|
filters: Filter[];
|
||||||
globalFullScreen: boolean;
|
globalFullScreen: boolean;
|
||||||
headerFilterGroup?: React.ReactNode;
|
headerFilterGroup?: React.ReactNode;
|
||||||
|
filterStatus?: AlertStatus;
|
||||||
height?: number;
|
height?: number;
|
||||||
id: TimelineId;
|
id: TimelineId;
|
||||||
indexNames: string[];
|
indexNames: string[];
|
||||||
|
@ -133,8 +130,8 @@ export interface TGridIntegratedProps {
|
||||||
utilityBar?: (refetch: Refetch, totalCount: number) => React.ReactNode;
|
utilityBar?: (refetch: Refetch, totalCount: number) => React.ReactNode;
|
||||||
// If truthy, the graph viewer (Resolver) is showing
|
// If truthy, the graph viewer (Resolver) is showing
|
||||||
graphEventId: string | undefined;
|
graphEventId: string | undefined;
|
||||||
leadingControlColumns: ControlColumnProps[];
|
leadingControlColumns?: ControlColumnProps[];
|
||||||
trailingControlColumns: ControlColumnProps[];
|
trailingControlColumns?: ControlColumnProps[];
|
||||||
data?: DataPublicPluginStart;
|
data?: DataPublicPluginStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,6 +145,7 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
||||||
filters,
|
filters,
|
||||||
globalFullScreen,
|
globalFullScreen,
|
||||||
headerFilterGroup,
|
headerFilterGroup,
|
||||||
|
filterStatus,
|
||||||
id,
|
id,
|
||||||
indexNames,
|
indexNames,
|
||||||
indexPattern,
|
indexPattern,
|
||||||
|
@ -255,13 +253,6 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
||||||
[deletedEventIds.length, totalCount]
|
[deletedEventIds.length, totalCount]
|
||||||
);
|
);
|
||||||
|
|
||||||
const subtitle = useMemo(
|
|
||||||
() => `${totalCountMinusDeleted.toLocaleString()} ${unit && unit(totalCountMinusDeleted)}`,
|
|
||||||
[totalCountMinusDeleted, unit]
|
|
||||||
);
|
|
||||||
|
|
||||||
const additionalControls = useMemo(() => <AlertCount>{subtitle}</AlertCount>, [subtitle]);
|
|
||||||
|
|
||||||
const nonDeletedEvents = useMemo(() => events.filter((e) => !deletedEventIds.includes(e._id)), [
|
const nonDeletedEvents = useMemo(() => events.filter((e) => !deletedEventIds.includes(e._id)), [
|
||||||
deletedEventIds,
|
deletedEventIds,
|
||||||
events,
|
events,
|
||||||
|
@ -301,9 +292,7 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
||||||
>
|
>
|
||||||
{HeaderSectionContent}
|
{HeaderSectionContent}
|
||||||
</HeaderSection>
|
</HeaderSection>
|
||||||
{utilityBar && !resolverIsShowing(graphEventId) && (
|
|
||||||
<UtilityBar>{utilityBar?.(refetch, totalCountMinusDeleted)}</UtilityBar>
|
|
||||||
)}
|
|
||||||
<EventsContainerLoading
|
<EventsContainerLoading
|
||||||
data-timeline-id={id}
|
data-timeline-id={id}
|
||||||
data-test-subj={`events-container-loading-${loading}`}
|
data-test-subj={`events-container-loading-${loading}`}
|
||||||
|
@ -318,7 +307,6 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
||||||
<ScrollableFlexItem grow={1}>
|
<ScrollableFlexItem grow={1}>
|
||||||
<StatefulBody
|
<StatefulBody
|
||||||
activePage={pageInfo.activePage}
|
activePage={pageInfo.activePage}
|
||||||
additionalControls={additionalControls}
|
|
||||||
browserFields={browserFields}
|
browserFields={browserFields}
|
||||||
data={nonDeletedEvents}
|
data={nonDeletedEvents}
|
||||||
id={id}
|
id={id}
|
||||||
|
@ -332,8 +320,12 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
|
||||||
itemsCount: totalCountMinusDeleted,
|
itemsCount: totalCountMinusDeleted,
|
||||||
itemsPerPage,
|
itemsPerPage,
|
||||||
})}
|
})}
|
||||||
|
totalItems={totalCountMinusDeleted}
|
||||||
|
unit={unit}
|
||||||
|
filterStatus={filterStatus}
|
||||||
leadingControlColumns={leadingControlColumns}
|
leadingControlColumns={leadingControlColumns}
|
||||||
trailingControlColumns={trailingControlColumns}
|
trailingControlColumns={trailingControlColumns}
|
||||||
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
<Footer
|
<Footer
|
||||||
activePage={pageInfo.activePage}
|
activePage={pageInfo.activePage}
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
|
||||||
|
|
||||||
export const SHOWING = i18n.translate('xpack.timelines.eventsViewer.showingLabel', {
|
|
||||||
defaultMessage: 'Showing',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ERROR_FETCHING_EVENTS_DATA = i18n.translate(
|
|
||||||
'xpack.timelines.eventsViewer.errorFetchingEventsData',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Failed to query events data',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const EVENTS = i18n.translate('xpack.timelines.eventsViewer.eventsLabel', {
|
|
||||||
defaultMessage: 'Events',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const LOADING_EVENTS = i18n.translate(
|
|
||||||
'xpack.timelines.eventsViewer.footer.loadingEventsDataLabel',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Loading Events',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const UNIT = (totalCount: number) =>
|
|
||||||
i18n.translate('xpack.timelines.eventsViewer.unit', {
|
|
||||||
values: { totalCount },
|
|
||||||
defaultMessage: `{totalCount, plural, =1 {event} other {events}}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ALERTS_UNIT = (totalCount: number) =>
|
|
||||||
i18n.translate('xpack.timelines.eventsViewer.alertsUnit', {
|
|
||||||
values: { totalCount },
|
|
||||||
defaultMessage: `{totalCount, plural, =1 {alert} other {alerts}}`,
|
|
||||||
});
|
|
|
@ -21,6 +21,8 @@ import type {
|
||||||
DataProvider,
|
DataProvider,
|
||||||
RowRenderer,
|
RowRenderer,
|
||||||
SortColumnTimeline,
|
SortColumnTimeline,
|
||||||
|
BulkActionsProp,
|
||||||
|
AlertStatus,
|
||||||
} from '../../../../common/types/timeline';
|
} from '../../../../common/types/timeline';
|
||||||
import {
|
import {
|
||||||
esQuery,
|
esQuery,
|
||||||
|
@ -29,7 +31,6 @@ import {
|
||||||
DataPublicPluginStart,
|
DataPublicPluginStart,
|
||||||
} from '../../../../../../../src/plugins/data/public';
|
} from '../../../../../../../src/plugins/data/public';
|
||||||
import { useDeepEqualSelector } from '../../../hooks/use_selector';
|
import { useDeepEqualSelector } from '../../../hooks/use_selector';
|
||||||
import { Refetch } from '../../../store/t_grid/inputs';
|
|
||||||
import { defaultHeaders } from '../body/column_headers/default_headers';
|
import { defaultHeaders } from '../body/column_headers/default_headers';
|
||||||
import { calculateTotalPages, combineQueries, resolverIsShowing } from '../helpers';
|
import { calculateTotalPages, combineQueries, resolverIsShowing } from '../helpers';
|
||||||
import { tGridActions, tGridSelectors } from '../../../store/t_grid';
|
import { tGridActions, tGridSelectors } from '../../../store/t_grid';
|
||||||
|
@ -38,21 +39,16 @@ import { HeaderSection } from '../header_section';
|
||||||
import { StatefulBody } from '../body';
|
import { StatefulBody } from '../body';
|
||||||
import { Footer, footerHeight } from '../footer';
|
import { Footer, footerHeight } from '../footer';
|
||||||
import { LastUpdatedAt } from '../..';
|
import { LastUpdatedAt } from '../..';
|
||||||
import { AlertCount, SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexItem } from '../styles';
|
import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexItem } from '../styles';
|
||||||
import * as i18n from './translations';
|
import * as i18n from '../translations';
|
||||||
import { InspectButtonContainer } from '../../inspect';
|
import { InspectButtonContainer } from '../../inspect';
|
||||||
import { useFetchIndex } from '../../../container/source';
|
import { useFetchIndex } from '../../../container/source';
|
||||||
|
|
||||||
export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px
|
export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px
|
||||||
const UTILITY_BAR_HEIGHT = 19; // px
|
|
||||||
const COMPACT_HEADER_HEIGHT = 36; // px
|
const COMPACT_HEADER_HEIGHT = 36; // px
|
||||||
const STANDALONE_ID = 'standalone-t-grid';
|
const STANDALONE_ID = 'standalone-t-grid';
|
||||||
const EMPTY_DATA_PROVIDERS: DataProvider[] = [];
|
const EMPTY_DATA_PROVIDERS: DataProvider[] = [];
|
||||||
|
|
||||||
const UtilityBar = styled.div`
|
|
||||||
height: ${UTILITY_BAR_HEIGHT}px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TitleText = styled.span`
|
const TitleText = styled.span`
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
`;
|
`;
|
||||||
|
@ -108,6 +104,7 @@ export interface TGridStandaloneProps {
|
||||||
filters: Filter[];
|
filters: Filter[];
|
||||||
footerText: React.ReactNode;
|
footerText: React.ReactNode;
|
||||||
headerFilterGroup?: React.ReactNode;
|
headerFilterGroup?: React.ReactNode;
|
||||||
|
filterStatus: AlertStatus;
|
||||||
height?: number;
|
height?: number;
|
||||||
indexNames: string[];
|
indexNames: string[];
|
||||||
itemsPerPage: number;
|
itemsPerPage: number;
|
||||||
|
@ -119,10 +116,10 @@ export interface TGridStandaloneProps {
|
||||||
setRefetch: (ref: () => void) => void;
|
setRefetch: (ref: () => void) => void;
|
||||||
start: string;
|
start: string;
|
||||||
sort: SortColumnTimeline[];
|
sort: SortColumnTimeline[];
|
||||||
utilityBar?: (refetch: Refetch, totalCount: number) => React.ReactNode;
|
|
||||||
graphEventId?: string;
|
graphEventId?: string;
|
||||||
leadingControlColumns: ControlColumnProps[];
|
leadingControlColumns: ControlColumnProps[];
|
||||||
trailingControlColumns: ControlColumnProps[];
|
trailingControlColumns: ControlColumnProps[];
|
||||||
|
bulkActions?: BulkActionsProp;
|
||||||
data?: DataPublicPluginStart;
|
data?: DataPublicPluginStart;
|
||||||
unit: (total: number) => React.ReactNode;
|
unit: (total: number) => React.ReactNode;
|
||||||
}
|
}
|
||||||
|
@ -136,6 +133,7 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
||||||
filters,
|
filters,
|
||||||
footerText,
|
footerText,
|
||||||
headerFilterGroup,
|
headerFilterGroup,
|
||||||
|
filterStatus,
|
||||||
indexNames,
|
indexNames,
|
||||||
itemsPerPage,
|
itemsPerPage,
|
||||||
itemsPerPageOptions,
|
itemsPerPageOptions,
|
||||||
|
@ -146,7 +144,6 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
||||||
setRefetch,
|
setRefetch,
|
||||||
start,
|
start,
|
||||||
sort,
|
sort,
|
||||||
utilityBar,
|
|
||||||
graphEventId,
|
graphEventId,
|
||||||
leadingControlColumns,
|
leadingControlColumns,
|
||||||
trailingControlColumns,
|
trailingControlColumns,
|
||||||
|
@ -166,6 +163,7 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
||||||
queryFields,
|
queryFields,
|
||||||
title,
|
title,
|
||||||
} = useDeepEqualSelector((state) => getTGrid(state, STANDALONE_ID ?? ''));
|
} = useDeepEqualSelector((state) => getTGrid(state, STANDALONE_ID ?? ''));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(tGridActions.updateIsLoading({ id: STANDALONE_ID, isLoading: isQueryLoading }));
|
dispatch(tGridActions.updateIsLoading({ id: STANDALONE_ID, isLoading: isQueryLoading }));
|
||||||
}, [dispatch, isQueryLoading]);
|
}, [dispatch, isQueryLoading]);
|
||||||
|
@ -237,13 +235,6 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
||||||
[deletedEventIds.length, totalCount]
|
[deletedEventIds.length, totalCount]
|
||||||
);
|
);
|
||||||
|
|
||||||
const subtitle = useMemo(
|
|
||||||
() => `${totalCountMinusDeleted.toLocaleString()} ${unit && unit(totalCountMinusDeleted)}`,
|
|
||||||
[totalCountMinusDeleted, unit]
|
|
||||||
);
|
|
||||||
|
|
||||||
const additionalControls = useMemo(() => <AlertCount>{subtitle}</AlertCount>, [subtitle]);
|
|
||||||
|
|
||||||
const nonDeletedEvents = useMemo(() => events.filter((e) => !deletedEventIds.includes(e._id)), [
|
const nonDeletedEvents = useMemo(() => events.filter((e) => !deletedEventIds.includes(e._id)), [
|
||||||
deletedEventIds,
|
deletedEventIds,
|
||||||
events,
|
events,
|
||||||
|
@ -310,9 +301,7 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
||||||
>
|
>
|
||||||
{HeaderSectionContent}
|
{HeaderSectionContent}
|
||||||
</HeaderSection>
|
</HeaderSection>
|
||||||
{utilityBar && !resolverIsShowing(graphEventId) && (
|
|
||||||
<UtilityBar>{utilityBar?.(refetch, totalCountMinusDeleted)}</UtilityBar>
|
|
||||||
)}
|
|
||||||
<EventsContainerLoading
|
<EventsContainerLoading
|
||||||
data-timeline-id={STANDALONE_ID}
|
data-timeline-id={STANDALONE_ID}
|
||||||
data-test-subj={`events-container-loading-${loading}`}
|
data-test-subj={`events-container-loading-${loading}`}
|
||||||
|
@ -327,7 +316,6 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
||||||
<ScrollableFlexItem grow={1}>
|
<ScrollableFlexItem grow={1}>
|
||||||
<StatefulBody
|
<StatefulBody
|
||||||
activePage={pageInfo.activePage}
|
activePage={pageInfo.activePage}
|
||||||
additionalControls={additionalControls}
|
|
||||||
browserFields={browserFields}
|
browserFields={browserFields}
|
||||||
data={nonDeletedEvents}
|
data={nonDeletedEvents}
|
||||||
id={STANDALONE_ID}
|
id={STANDALONE_ID}
|
||||||
|
@ -341,8 +329,12 @@ const TGridStandaloneComponent: React.FC<TGridStandaloneProps> = ({
|
||||||
itemsCount: totalCountMinusDeleted,
|
itemsCount: totalCountMinusDeleted,
|
||||||
itemsPerPage: itemsPerPageStore,
|
itemsPerPage: itemsPerPageStore,
|
||||||
})}
|
})}
|
||||||
|
totalItems={totalCountMinusDeleted}
|
||||||
|
unit={unit}
|
||||||
|
filterStatus={filterStatus}
|
||||||
leadingControlColumns={leadingControlColumns}
|
leadingControlColumns={leadingControlColumns}
|
||||||
trailingControlColumns={trailingControlColumns}
|
trailingControlColumns={trailingControlColumns}
|
||||||
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
<Footer
|
<Footer
|
||||||
activePage={pageInfo.activePage}
|
activePage={pageInfo.activePage}
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
|
||||||
|
|
||||||
export const SHOWING = i18n.translate('xpack.timelines.eventsViewer.showingLabel', {
|
|
||||||
defaultMessage: 'Showing',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ERROR_FETCHING_EVENTS_DATA = i18n.translate(
|
|
||||||
'xpack.timelines.eventsViewer.errorFetchingEventsData',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Failed to query events data',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const EVENTS = i18n.translate('xpack.timelines.eventsViewer.eventsLabel', {
|
|
||||||
defaultMessage: 'Events',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const LOADING_EVENTS = i18n.translate(
|
|
||||||
'xpack.timelines.eventsViewer.footer.loadingEventsDataLabel',
|
|
||||||
{
|
|
||||||
defaultMessage: 'Loading Events',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const UNIT = (totalCount: number) =>
|
|
||||||
i18n.translate('xpack.timelines.eventsViewer.unit', {
|
|
||||||
values: { totalCount },
|
|
||||||
defaultMessage: `{totalCount, plural, =1 {event} other {events}}`,
|
|
||||||
});
|
|
|
@ -468,6 +468,9 @@ export const UpdatedFlexItem = styled(EuiFlexItem)<{ show: boolean }>`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const AlertCount = styled.span`
|
export const AlertCount = styled.span`
|
||||||
color: ${({ theme }) => theme.eui.euiTextColors.subdued};
|
|
||||||
font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
|
font-size: ${({ theme }) => theme.eui.euiFontSizeXS};
|
||||||
|
font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold};
|
||||||
|
border-right: ${({ theme }) => theme.eui.euiBorderThin};
|
||||||
|
margin-right: ${({ theme }) => theme.eui.paddingSizes.s};
|
||||||
|
padding-right: ${({ theme }) => theme.eui.paddingSizes.m};
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -0,0 +1,198 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { connect, ConnectedProps, useDispatch } from 'react-redux';
|
||||||
|
import type {
|
||||||
|
AlertStatus,
|
||||||
|
OnAlertStatusActionSuccess,
|
||||||
|
OnAlertStatusActionFailure,
|
||||||
|
} from '../../../../../common';
|
||||||
|
import type { Refetch } from '../../../../store/t_grid/inputs';
|
||||||
|
import { tGridActions, TGridModel, tGridSelectors, TimelineState } from '../../../../store/t_grid';
|
||||||
|
import { BulkActions } from './';
|
||||||
|
import { useAppToasts } from '../../../../hooks/use_app_toasts';
|
||||||
|
import * as i18n from '../../translations';
|
||||||
|
import {
|
||||||
|
SetEventsDeletedProps,
|
||||||
|
SetEventsLoadingProps,
|
||||||
|
useStatusBulkActionItems,
|
||||||
|
} from '../../../../hooks/use_status_bulk_action_items';
|
||||||
|
|
||||||
|
interface OwnProps {
|
||||||
|
id: string;
|
||||||
|
totalItems: number;
|
||||||
|
filterStatus?: AlertStatus;
|
||||||
|
onActionSuccess?: OnAlertStatusActionSuccess;
|
||||||
|
onActionFailure?: OnAlertStatusActionFailure;
|
||||||
|
refetch: Refetch;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StatefulAlertStatusBulkActionsProps = OwnProps & PropsFromRedux;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to render status bulk actions
|
||||||
|
*/
|
||||||
|
export const AlertStatusBulkActionsComponent = React.memo<StatefulAlertStatusBulkActionsProps>(
|
||||||
|
({
|
||||||
|
id,
|
||||||
|
totalItems,
|
||||||
|
filterStatus,
|
||||||
|
selectedEventIds,
|
||||||
|
isSelectAllChecked,
|
||||||
|
clearSelected,
|
||||||
|
onActionSuccess,
|
||||||
|
onActionFailure,
|
||||||
|
refetch,
|
||||||
|
}) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { addSuccess, addError, addWarning } = useAppToasts();
|
||||||
|
|
||||||
|
const [showClearSelection, setShowClearSelection] = useState(false);
|
||||||
|
|
||||||
|
// Catches state change isSelectAllChecked->false (page checkbox) upon user selection change to reset toolbar select all
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSelectAllChecked) {
|
||||||
|
dispatch(tGridActions.setTGridSelectAll({ id, selectAll: false }));
|
||||||
|
} else {
|
||||||
|
setShowClearSelection(false);
|
||||||
|
}
|
||||||
|
}, [dispatch, isSelectAllChecked, id]);
|
||||||
|
|
||||||
|
// Callback for selecting all events on all pages from toolbar
|
||||||
|
// Dispatches to stateful_body's selectAll via TimelineTypeContext props
|
||||||
|
// as scope of response data required to actually set selectedEvents
|
||||||
|
const onSelectAll = useCallback(() => {
|
||||||
|
dispatch(tGridActions.setTGridSelectAll({ id, selectAll: true }));
|
||||||
|
setShowClearSelection(true);
|
||||||
|
}, [dispatch, id]);
|
||||||
|
|
||||||
|
// Callback for clearing entire selection from toolbar
|
||||||
|
const onClearSelection = useCallback(() => {
|
||||||
|
clearSelected({ id });
|
||||||
|
dispatch(tGridActions.setTGridSelectAll({ id, selectAll: false }));
|
||||||
|
setShowClearSelection(false);
|
||||||
|
}, [clearSelected, dispatch, id]);
|
||||||
|
|
||||||
|
const onAlertStatusUpdateSuccess = useCallback(
|
||||||
|
(updated: number, conflicts: number, newStatus: AlertStatus) => {
|
||||||
|
if (conflicts > 0) {
|
||||||
|
// Partial failure
|
||||||
|
addWarning({
|
||||||
|
title: i18n.UPDATE_ALERT_STATUS_FAILED(conflicts),
|
||||||
|
text: i18n.UPDATE_ALERT_STATUS_FAILED_DETAILED(updated, conflicts),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let title: string;
|
||||||
|
switch (newStatus) {
|
||||||
|
case 'closed':
|
||||||
|
title = i18n.CLOSED_ALERT_SUCCESS_TOAST(updated);
|
||||||
|
break;
|
||||||
|
case 'open':
|
||||||
|
title = i18n.OPENED_ALERT_SUCCESS_TOAST(updated);
|
||||||
|
break;
|
||||||
|
case 'in-progress':
|
||||||
|
title = i18n.IN_PROGRESS_ALERT_SUCCESS_TOAST(updated);
|
||||||
|
}
|
||||||
|
addSuccess({ title });
|
||||||
|
}
|
||||||
|
refetch();
|
||||||
|
if (onActionSuccess) {
|
||||||
|
onActionSuccess(newStatus);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[addSuccess, addWarning, onActionSuccess, refetch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onAlertStatusUpdateFailure = useCallback(
|
||||||
|
(newStatus: AlertStatus, error: Error) => {
|
||||||
|
let title: string;
|
||||||
|
switch (newStatus) {
|
||||||
|
case 'closed':
|
||||||
|
title = i18n.CLOSED_ALERT_FAILED_TOAST;
|
||||||
|
break;
|
||||||
|
case 'open':
|
||||||
|
title = i18n.OPENED_ALERT_FAILED_TOAST;
|
||||||
|
break;
|
||||||
|
case 'in-progress':
|
||||||
|
title = i18n.IN_PROGRESS_ALERT_FAILED_TOAST;
|
||||||
|
}
|
||||||
|
addError(error.message, { title });
|
||||||
|
refetch();
|
||||||
|
if (onActionFailure) {
|
||||||
|
onActionFailure(newStatus, error.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[addError, onActionFailure, refetch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setEventsLoading = useCallback(
|
||||||
|
({ eventIds, isLoading }: SetEventsLoadingProps) => {
|
||||||
|
dispatch(tGridActions.setEventsLoading({ id, eventIds, isLoading }));
|
||||||
|
},
|
||||||
|
[dispatch, id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setEventsDeleted = useCallback(
|
||||||
|
({ eventIds, isDeleted }: SetEventsDeletedProps) => {
|
||||||
|
dispatch(tGridActions.setEventsDeleted({ id, eventIds, isDeleted }));
|
||||||
|
},
|
||||||
|
[dispatch, id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const statusBulkActionItems = useStatusBulkActionItems({
|
||||||
|
currentStatus: filterStatus,
|
||||||
|
eventIds: Object.keys(selectedEventIds),
|
||||||
|
setEventsLoading,
|
||||||
|
setEventsDeleted,
|
||||||
|
onUpdateSuccess: onAlertStatusUpdateSuccess,
|
||||||
|
onUpdateFailure: onAlertStatusUpdateFailure,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BulkActions
|
||||||
|
data-test-subj="bulk-actions"
|
||||||
|
timelineId={id}
|
||||||
|
selectedCount={Object.keys(selectedEventIds).length}
|
||||||
|
totalItems={totalItems}
|
||||||
|
showClearSelection={showClearSelection}
|
||||||
|
onSelectAll={onSelectAll}
|
||||||
|
onClearSelection={onClearSelection}
|
||||||
|
bulkActionItems={statusBulkActionItems}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
AlertStatusBulkActionsComponent.displayName = 'AlertStatusBulkActionsComponent';
|
||||||
|
|
||||||
|
const makeMapStateToProps = () => {
|
||||||
|
const getTGrid = tGridSelectors.getTGridByIdSelector();
|
||||||
|
const mapStateToProps = (state: TimelineState, { id }: OwnProps) => {
|
||||||
|
const timeline: TGridModel = getTGrid(state, id);
|
||||||
|
const { selectedEventIds, isSelectAllChecked } = timeline;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isSelectAllChecked,
|
||||||
|
selectedEventIds,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return mapStateToProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
clearSelected: tGridActions.clearSelected,
|
||||||
|
};
|
||||||
|
|
||||||
|
const connector = connect(makeMapStateToProps, mapDispatchToProps);
|
||||||
|
|
||||||
|
type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||||
|
|
||||||
|
export const StatefulAlertStatusBulkActions = connector(AlertStatusBulkActionsComponent);
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export { StatefulAlertStatusBulkActions as default };
|
|
@ -0,0 +1,131 @@
|
||||||
|
/*
|
||||||
|
* 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 { EuiPopover, EuiButtonEmpty, EuiContextMenuPanel } from '@elastic/eui';
|
||||||
|
import numeral from '@elastic/numeral';
|
||||||
|
import React, { useState, useCallback, useMemo } from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { useUiSetting$ } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||||
|
import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants';
|
||||||
|
import * as i18n from './translations';
|
||||||
|
|
||||||
|
interface BulkActionsProps {
|
||||||
|
timelineId: string;
|
||||||
|
totalItems: number;
|
||||||
|
selectedCount: number;
|
||||||
|
showClearSelection: boolean;
|
||||||
|
onSelectAll: () => void;
|
||||||
|
onClearSelection: () => void;
|
||||||
|
bulkActionItems?: JSX.Element[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const BulkActionsContainer = styled.div`
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
BulkActionsContainer.displayName = 'BulkActionsContainer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stateless component integrating the bulk actions menu and the select all button
|
||||||
|
*/
|
||||||
|
const BulkActionsComponent: React.FC<BulkActionsProps> = ({
|
||||||
|
selectedCount,
|
||||||
|
totalItems,
|
||||||
|
showClearSelection,
|
||||||
|
onSelectAll,
|
||||||
|
onClearSelection,
|
||||||
|
bulkActionItems,
|
||||||
|
}) => {
|
||||||
|
const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false);
|
||||||
|
const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT);
|
||||||
|
|
||||||
|
const formattedTotalCount = useMemo(() => numeral(totalItems).format(defaultNumberFormat), [
|
||||||
|
defaultNumberFormat,
|
||||||
|
totalItems,
|
||||||
|
]);
|
||||||
|
const formattedSelectedEventsCount = useMemo(
|
||||||
|
() => numeral(selectedCount).format(defaultNumberFormat),
|
||||||
|
[defaultNumberFormat, selectedCount]
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleIsActionOpen = useCallback(() => {
|
||||||
|
setIsActionsPopoverOpen((currentIsOpen) => !currentIsOpen);
|
||||||
|
}, [setIsActionsPopoverOpen]);
|
||||||
|
|
||||||
|
const closeActionPopover = useCallback(() => {
|
||||||
|
setIsActionsPopoverOpen(false);
|
||||||
|
}, [setIsActionsPopoverOpen]);
|
||||||
|
|
||||||
|
const toggleSelectAll = useCallback(() => {
|
||||||
|
if (!showClearSelection) {
|
||||||
|
onSelectAll();
|
||||||
|
} else {
|
||||||
|
onClearSelection();
|
||||||
|
}
|
||||||
|
}, [onClearSelection, onSelectAll, showClearSelection]);
|
||||||
|
|
||||||
|
const selectedAlertsText = useMemo(
|
||||||
|
() =>
|
||||||
|
showClearSelection
|
||||||
|
? i18n.SELECTED_ALERTS(formattedTotalCount, totalItems)
|
||||||
|
: i18n.SELECTED_ALERTS(formattedSelectedEventsCount, selectedCount),
|
||||||
|
[
|
||||||
|
showClearSelection,
|
||||||
|
formattedTotalCount,
|
||||||
|
formattedSelectedEventsCount,
|
||||||
|
totalItems,
|
||||||
|
selectedCount,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectClearAllAlertsText = useMemo(
|
||||||
|
() =>
|
||||||
|
showClearSelection
|
||||||
|
? i18n.CLEAR_SELECTION
|
||||||
|
: i18n.SELECT_ALL_ALERTS(formattedTotalCount, totalItems),
|
||||||
|
[showClearSelection, formattedTotalCount, totalItems]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BulkActionsContainer data-test-subj="bulk-actions-button-container">
|
||||||
|
<EuiPopover
|
||||||
|
isOpen={isActionsPopoverOpen}
|
||||||
|
anchorPosition="upCenter"
|
||||||
|
panelPaddingSize="s"
|
||||||
|
button={
|
||||||
|
<EuiButtonEmpty
|
||||||
|
aria-label="selectedShowBulkActions"
|
||||||
|
data-test-subj="selectedShowBulkActionsButton"
|
||||||
|
size="xs"
|
||||||
|
iconType="arrowDown"
|
||||||
|
iconSide="right"
|
||||||
|
color="primary"
|
||||||
|
onClick={toggleIsActionOpen}
|
||||||
|
>
|
||||||
|
{selectedAlertsText}
|
||||||
|
</EuiButtonEmpty>
|
||||||
|
}
|
||||||
|
closePopover={closeActionPopover}
|
||||||
|
>
|
||||||
|
<EuiContextMenuPanel size="s" items={bulkActionItems} />
|
||||||
|
</EuiPopover>
|
||||||
|
|
||||||
|
<EuiButtonEmpty
|
||||||
|
size="xs"
|
||||||
|
aria-label="selectAllAlerts"
|
||||||
|
data-test-subj="selectAllAlertsButton"
|
||||||
|
iconType={showClearSelection ? 'cross' : 'pagesSelect'}
|
||||||
|
onClick={toggleSelectAll}
|
||||||
|
>
|
||||||
|
{selectClearAllAlertsText}
|
||||||
|
</EuiButtonEmpty>
|
||||||
|
</BulkActionsContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BulkActions = React.memo(BulkActionsComponent);
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
|
||||||
|
export const SELECTED_ALERTS = (selectedAlertsFormatted: string, selectedAlerts: number) =>
|
||||||
|
i18n.translate('xpack.timelines.toolbar.bulkActions.selectedAlertsTitle', {
|
||||||
|
values: { selectedAlertsFormatted, selectedAlerts },
|
||||||
|
defaultMessage:
|
||||||
|
'Selected {selectedAlertsFormatted} {selectedAlerts, plural, =1 {alert} other {alerts}}',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SELECT_ALL_ALERTS = (totalAlertsFormatted: string, totalAlerts: number) =>
|
||||||
|
i18n.translate('xpack.timelines.toolbar.bulkActions.selectAllAlertsTitle', {
|
||||||
|
values: { totalAlertsFormatted, totalAlerts },
|
||||||
|
defaultMessage:
|
||||||
|
'Select all {totalAlertsFormatted} {totalAlerts, plural, =1 {alert} other {alerts}}',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CLEAR_SELECTION = i18n.translate(
|
||||||
|
'xpack.timelines.toolbar.bulkActions.clearSelectionTitle',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Clear selection',
|
||||||
|
}
|
||||||
|
);
|
|
@ -127,7 +127,7 @@ export const StatefulFieldsBrowserComponent: React.FC<FieldBrowserProps> = ({
|
||||||
className={FIELDS_BUTTON_CLASS_NAME}
|
className={FIELDS_BUTTON_CLASS_NAME}
|
||||||
color="text"
|
color="text"
|
||||||
data-test-subj="show-field-browser"
|
data-test-subj="show-field-browser"
|
||||||
iconType="listAdd"
|
iconType="tableOfContents"
|
||||||
onClick={onShow}
|
onClick={onShow}
|
||||||
size="xs"
|
size="xs"
|
||||||
>
|
>
|
||||||
|
|
|
@ -18,3 +18,99 @@ export const EVENTS_TABLE_ARIA_LABEL = ({
|
||||||
values: { activePage, totalPages },
|
values: { activePage, totalPages },
|
||||||
defaultMessage: 'events; Page {activePage} of {totalPages}',
|
defaultMessage: 'events; Page {activePage} of {totalPages}',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const UNIT = (totalCount: number) =>
|
||||||
|
i18n.translate('xpack.timelines.timeline.unit', {
|
||||||
|
values: { totalCount },
|
||||||
|
defaultMessage: `{totalCount, plural, =1 {event} other {events}}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ALERTS_UNIT = (totalCount: number) =>
|
||||||
|
i18n.translate('xpack.timelines.timeline.alertsUnit', {
|
||||||
|
values: { totalCount },
|
||||||
|
defaultMessage: `{totalCount, plural, =1 {alert} other {alerts}}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BULK_ACTION_OPEN_SELECTED = i18n.translate(
|
||||||
|
'xpack.timelines.timeline.openSelectedTitle',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Open selected',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const BULK_ACTION_CLOSE_SELECTED = i18n.translate(
|
||||||
|
'xpack.timelines.timeline.closeSelectedTitle',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Close selected',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const BULK_ACTION_IN_PROGRESS_SELECTED = i18n.translate(
|
||||||
|
'xpack.timelines.timeline.inProgressSelectedTitle',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Mark in progress',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const BULK_ACTION_FAILED_SINGLE_ALERT = i18n.translate(
|
||||||
|
'xpack.timelines.timeline.updateAlertStatusFailedSingleAlert',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Failed to update alert because it was already being modified.',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const CLOSED_ALERT_SUCCESS_TOAST = (totalAlerts: number) =>
|
||||||
|
i18n.translate('xpack.timelines.timeline.closedAlertSuccessToastMessage', {
|
||||||
|
values: { totalAlerts },
|
||||||
|
defaultMessage:
|
||||||
|
'Successfully closed {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}}.',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const OPENED_ALERT_SUCCESS_TOAST = (totalAlerts: number) =>
|
||||||
|
i18n.translate('xpack.timelines.timeline.openedAlertSuccessToastMessage', {
|
||||||
|
values: { totalAlerts },
|
||||||
|
defaultMessage:
|
||||||
|
'Successfully opened {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}}.',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const IN_PROGRESS_ALERT_SUCCESS_TOAST = (totalAlerts: number) =>
|
||||||
|
i18n.translate('xpack.timelines.timeline.inProgressAlertSuccessToastMessage', {
|
||||||
|
values: { totalAlerts },
|
||||||
|
defaultMessage:
|
||||||
|
'Successfully marked {totalAlerts} {totalAlerts, plural, =1 {alert} other {alerts}} as in progress.',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CLOSED_ALERT_FAILED_TOAST = i18n.translate(
|
||||||
|
'xpack.timelines.timeline.closedAlertFailedToastMessage',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Failed to close alert(s).',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const OPENED_ALERT_FAILED_TOAST = i18n.translate(
|
||||||
|
'xpack.timelines.timeline.openedAlertFailedToastMessage',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Failed to open alert(s)',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const IN_PROGRESS_ALERT_FAILED_TOAST = i18n.translate(
|
||||||
|
'xpack.timelines.timeline.inProgressAlertFailedToastMessage',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Failed to mark alert(s) as in progress',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const UPDATE_ALERT_STATUS_FAILED = (conflicts: number) =>
|
||||||
|
i18n.translate('xpack.timelines.timeline.updateAlertStatusFailed', {
|
||||||
|
values: { conflicts },
|
||||||
|
defaultMessage:
|
||||||
|
'Failed to update { conflicts } {conflicts, plural, =1 {alert} other {alerts}}.',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UPDATE_ALERT_STATUS_FAILED_DETAILED = (updated: number, conflicts: number) =>
|
||||||
|
i18n.translate('xpack.timelines.timeline.updateAlertStatusFailedDetailed', {
|
||||||
|
values: { updated, conflicts },
|
||||||
|
defaultMessage: `{ updated } {updated, plural, =1 {alert was} other {alerts were}} updated successfully, but { conflicts } failed to update
|
||||||
|
because { conflicts, plural, =1 {it was} other {they were}} already being modified.`,
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* 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 { UpdateDocumentByQueryResponse } from 'elasticsearch';
|
||||||
|
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
|
||||||
|
import { AlertStatus } from '../../../timelines/common';
|
||||||
|
|
||||||
|
export const DETECTION_ENGINE_SIGNALS_STATUS_URL = '/api/detection_engine/signals/status';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update alert status by query
|
||||||
|
*
|
||||||
|
* @param query of alerts to update
|
||||||
|
* @param status to update to('open' / 'closed' / 'in-progress')
|
||||||
|
* @param signal AbortSignal for cancelling request
|
||||||
|
*
|
||||||
|
* @throws An error if response is not OK
|
||||||
|
*/
|
||||||
|
export const useUpdateAlertsStatus = (): {
|
||||||
|
updateAlertStatus: (params: {
|
||||||
|
query: object;
|
||||||
|
status: AlertStatus;
|
||||||
|
}) => Promise<UpdateDocumentByQueryResponse>;
|
||||||
|
} => {
|
||||||
|
const { http } = useKibana().services;
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateAlertStatus: ({ query, status }) =>
|
||||||
|
http!.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ status, query }),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,109 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useMemo, useCallback } from 'react';
|
||||||
|
import { EuiContextMenuItem } from '@elastic/eui';
|
||||||
|
import { FILTER_CLOSED, FILTER_IN_PROGRESS, FILTER_OPEN } from '../../common/constants';
|
||||||
|
import * as i18n from '../components/t_grid/translations';
|
||||||
|
import type { AlertStatus } from '../../common/types/timeline';
|
||||||
|
import { useUpdateAlertsStatus } from '../container/use_update_alerts';
|
||||||
|
|
||||||
|
export interface SetEventsLoadingProps {
|
||||||
|
eventIds: string[];
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetEventsDeletedProps {
|
||||||
|
eventIds: string[];
|
||||||
|
isDeleted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StatusBulkActionsProps {
|
||||||
|
eventIds: string[];
|
||||||
|
currentStatus?: AlertStatus;
|
||||||
|
query?: string;
|
||||||
|
setEventsLoading: (param: SetEventsLoadingProps) => void;
|
||||||
|
setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void;
|
||||||
|
onUpdateSuccess: (updated: number, conflicts: number, status: AlertStatus) => void;
|
||||||
|
onUpdateFailure: (status: AlertStatus, error: Error) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getUpdateAlertsQuery = (eventIds: Readonly<string[]>) => {
|
||||||
|
return { bool: { filter: { terms: { _id: eventIds } } } };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useStatusBulkActionItems = ({
|
||||||
|
eventIds,
|
||||||
|
currentStatus,
|
||||||
|
query,
|
||||||
|
setEventsLoading,
|
||||||
|
setEventsDeleted,
|
||||||
|
onUpdateSuccess,
|
||||||
|
onUpdateFailure,
|
||||||
|
}: StatusBulkActionsProps) => {
|
||||||
|
const { updateAlertStatus } = useUpdateAlertsStatus();
|
||||||
|
|
||||||
|
const onClickUpdate = useCallback(
|
||||||
|
async (status: AlertStatus) => {
|
||||||
|
try {
|
||||||
|
setEventsLoading({ eventIds, isLoading: true });
|
||||||
|
|
||||||
|
const queryObject = query ? JSON.parse(query) : getUpdateAlertsQuery(eventIds);
|
||||||
|
const response = await updateAlertStatus({ query: queryObject, status });
|
||||||
|
// TODO: Only delete those that were successfully updated from updatedRules
|
||||||
|
setEventsDeleted({ eventIds, isDeleted: true });
|
||||||
|
|
||||||
|
if (response.version_conflicts > 0 && eventIds.length === 1) {
|
||||||
|
throw new Error(i18n.BULK_ACTION_FAILED_SINGLE_ALERT);
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdateSuccess(response.updated, response.version_conflicts, status);
|
||||||
|
} catch (error) {
|
||||||
|
onUpdateFailure(status, error);
|
||||||
|
} finally {
|
||||||
|
setEventsLoading({ eventIds, isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
eventIds,
|
||||||
|
query,
|
||||||
|
setEventsLoading,
|
||||||
|
updateAlertStatus,
|
||||||
|
setEventsDeleted,
|
||||||
|
onUpdateSuccess,
|
||||||
|
onUpdateFailure,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const items = useMemo(() => {
|
||||||
|
const actionItems = [];
|
||||||
|
if (currentStatus !== FILTER_OPEN) {
|
||||||
|
actionItems.push(
|
||||||
|
<EuiContextMenuItem key="open" onClick={() => onClickUpdate(FILTER_OPEN)}>
|
||||||
|
{i18n.BULK_ACTION_OPEN_SELECTED}
|
||||||
|
</EuiContextMenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (currentStatus !== FILTER_IN_PROGRESS) {
|
||||||
|
actionItems.push(
|
||||||
|
<EuiContextMenuItem key="progress" onClick={() => onClickUpdate(FILTER_IN_PROGRESS)}>
|
||||||
|
{i18n.BULK_ACTION_IN_PROGRESS_SELECTED}
|
||||||
|
</EuiContextMenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (currentStatus !== FILTER_CLOSED) {
|
||||||
|
actionItems.push(
|
||||||
|
<EuiContextMenuItem key="close" onClick={() => onClickUpdate(FILTER_CLOSED)}>
|
||||||
|
{i18n.BULK_ACTION_CLOSE_SELECTED}
|
||||||
|
</EuiContextMenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return actionItems;
|
||||||
|
}, [currentStatus, onClickUpdate]);
|
||||||
|
|
||||||
|
return items;
|
||||||
|
};
|
|
@ -75,7 +75,7 @@ export interface TGridModel extends TGridModelSettings {
|
||||||
showCheckboxes: boolean;
|
showCheckboxes: boolean;
|
||||||
/** Specifies which column the timeline is sorted on, and the direction (ascending / descending) */
|
/** Specifies which column the timeline is sorted on, and the direction (ascending / descending) */
|
||||||
sort: SortColumnTimeline[];
|
sort: SortColumnTimeline[];
|
||||||
/** Events selected on this timeline -- eventId to TimelineNonEcsData[] mapping of data required for batch actions **/
|
/** Events selected on this timeline -- eventId to TimelineNonEcsData[] mapping of data required for bulk actions **/
|
||||||
selectedEventIds: Record<string, TimelineNonEcsData[]>;
|
selectedEventIds: Record<string, TimelineNonEcsData[]>;
|
||||||
savedObjectId: string | null;
|
savedObjectId: string | null;
|
||||||
version: string | null;
|
version: string | null;
|
||||||
|
|
|
@ -23897,10 +23897,6 @@
|
||||||
"xpack.timelines.draggables.field.typeLabel": "型",
|
"xpack.timelines.draggables.field.typeLabel": "型",
|
||||||
"xpack.timelines.draggables.field.viewCategoryTooltip": "カテゴリーを表示します",
|
"xpack.timelines.draggables.field.viewCategoryTooltip": "カテゴリーを表示します",
|
||||||
"xpack.timelines.eventDetails.copyToClipboardTooltip": "クリップボードにコピー",
|
"xpack.timelines.eventDetails.copyToClipboardTooltip": "クリップボードにコピー",
|
||||||
"xpack.timelines.eventsViewer.errorFetchingEventsData": "イベントデータをクエリできませんでした",
|
|
||||||
"xpack.timelines.eventsViewer.eventsLabel": "イベント",
|
|
||||||
"xpack.timelines.eventsViewer.footer.loadingEventsDataLabel": "イベントを読み込み中",
|
|
||||||
"xpack.timelines.eventsViewer.showingLabel": "表示中",
|
|
||||||
"xpack.timelines.exitFullScreenButton": "全画面を終了",
|
"xpack.timelines.exitFullScreenButton": "全画面を終了",
|
||||||
"xpack.timelines.footer.autoRefreshActiveDescription": "自動更新アクション",
|
"xpack.timelines.footer.autoRefreshActiveDescription": "自動更新アクション",
|
||||||
"xpack.timelines.footer.autoRefreshActiveTooltip": "自動更新が有効な間、タイムラインはクエリに一致する最新の {numberOfItems} 件のイベントを表示します。",
|
"xpack.timelines.footer.autoRefreshActiveTooltip": "自動更新が有効な間、タイムラインはクエリに一致する最新の {numberOfItems} 件のイベントを表示します。",
|
||||||
|
|
|
@ -24449,11 +24449,6 @@
|
||||||
"xpack.timelines.draggables.field.typeLabel": "类型",
|
"xpack.timelines.draggables.field.typeLabel": "类型",
|
||||||
"xpack.timelines.draggables.field.viewCategoryTooltip": "查看类别",
|
"xpack.timelines.draggables.field.viewCategoryTooltip": "查看类别",
|
||||||
"xpack.timelines.eventDetails.copyToClipboardTooltip": "复制到剪贴板",
|
"xpack.timelines.eventDetails.copyToClipboardTooltip": "复制到剪贴板",
|
||||||
"xpack.timelines.eventsViewer.errorFetchingEventsData": "无法查询事件数据",
|
|
||||||
"xpack.timelines.eventsViewer.eventsLabel": "事件",
|
|
||||||
"xpack.timelines.eventsViewer.footer.loadingEventsDataLabel": "正在加载事件",
|
|
||||||
"xpack.timelines.eventsViewer.showingLabel": "正在显示",
|
|
||||||
"xpack.timelines.eventsViewer.unit": "{totalCount, plural, other {个事件}}",
|
|
||||||
"xpack.timelines.exitFullScreenButton": "退出全屏",
|
"xpack.timelines.exitFullScreenButton": "退出全屏",
|
||||||
"xpack.timelines.footer.autoRefreshActiveDescription": "自动刷新已启用",
|
"xpack.timelines.footer.autoRefreshActiveDescription": "自动刷新已启用",
|
||||||
"xpack.timelines.footer.autoRefreshActiveTooltip": "自动刷新已启用时,时间线将显示匹配查询的最近 {numberOfItems} 个事件。",
|
"xpack.timelines.footer.autoRefreshActiveTooltip": "自动刷新已启用时,时间线将显示匹配查询的最近 {numberOfItems} 个事件。",
|
||||||
|
|
|
@ -82,6 +82,7 @@ const AppRoot = React.memo(
|
||||||
setRefetch,
|
setRefetch,
|
||||||
start: '',
|
start: '',
|
||||||
rowRenderers: [],
|
rowRenderers: [],
|
||||||
|
filterStatus: 'open',
|
||||||
unit: (n: number) => `${n}`,
|
unit: (n: number) => `${n}`,
|
||||||
})) ??
|
})) ??
|
||||||
null}
|
null}
|
||||||
|
|
Loading…
Reference in a new issue